Моделирование рынка: Position View (XIV)
Введение
Всем привет и добро пожаловать в новую статью из серии о том, как создать систему репликации/моделирования.
В предыдущей статье «Моделирование рынка: Position View (XIII)» я показал, как посмотреть результат открытой операции и проверить, приносит ли позиция прибыль или убыток. Тем не менее мы можем расширить и улучшить эту же систему, чтобы превратить индикатор позиции в практичный и полезный инструмент для повседневного использования. В этой статье мы адаптируем его к различным ситуациям. Поэтому я приглашаю вас проследить за развитием этой статьи, поскольку те же приёмы окажутся полезными и в других проектах, разработанных на MQL5.
Реализация автоматического подбора высоты
Пожалуй, одной из самых трудоёмких задач при создании объектов с помощью кода является то, как точно задать их размер. Часто нам приходится задавать их высоту и ширину, компилировать код и запускать приложение в MetaTrader 5, чтобы проверить, подходят ли выбранные размеры.
С некоторыми типами объектов эта задача не представляет сложности. Тем не менее эта работа утомительна и требует времени, особенно когда мы ещё не знаем, какие размеры понадобятся объекту на более позднем этапе разработки. Итак, одним из объектов, которые мы будем часто использовать, является OBJ_EDIT. Мы будем использовать его для отображения значений трейдеру. Этот объект лучше всего подходит, когда нам нужно отображать текст, который должен оставаться в пределах заранее заданного фона. В предыдущей статье значение прибыли или убытка отображается на фоне, который позволяет сразу определить, находимся ли мы в плюсе или в минусе.
Однако здесь возникает проблема: нужно правильно задать высоту и ширину объекта OBJ_EDIT. Задать размеры объекта может быть сложно тем, что для одних значений требуется четыре символа, а для других — больше. Дело не в том, чтобы вручную подгонять текущие размеры, а в том, что произойдёт, если вы, уважаемый читатель, увлечённый и заинтересованный в изучении программирования на MQL5, захотите использовать другой шрифт или иной размер шрифта. Используя тот же код, вы, возможно, обнаружите, что текст обрезается или отображается не так, как вы ожидали. Несоответствие между размером шрифта и размерами OBJ_EDIT вызывает разочарование, когда мы только учимся программировать и не знаем, что именно нужно изменить, чтобы это исправить.
К счастью, есть практичное и эффективное решение для настройки размеров объекта, хотя немногие программисты им пользуются. Это решение полезно при разработке интерфейса для личного использования, поскольку избавляет от необходимости вручную подгонять высоту и ширину OBJ_EDIT в зависимости от количества символов.
Чтобы правильно автоматизировать процесс настройки размеров, перейдём к классу C_ElementsTrade и добавим новые определения, переменные и структуру данных. Я буду постепенно вносить изменения в C_ElementsTrade, чтобы вы, мой уважаемый читатель, могли понять, как мы структурируем класс, и вам было проще доработать код. Итак, исходя из того, что было показано в предыдущей статье, мы изменим класс C_ElementsTrade. Ниже я покажу результат.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_NameHLine m_Info.szPrefixName + "#HLINE" 005. #define def_NameBtnClose m_Info.szPrefixName + "#CLOSE" 006. #define def_NameBtnMove m_Info.szPrefixName + "#MOVE" 007. #define def_NameInfoDirect m_Info.szPrefixName + "#DIRECT" 008. #define def_NameObjLabel m_Info.szPrefixName + "#PROFIT" 009. //+------------------------------------------------------------------+ 010. #define macro_LineInFocus(A) ObjectSetInteger(0, def_NameHLine, OBJPROP_YSIZE, m_Info.weight = (A ? 3 : 1)); 011. //+------------------------------------------------------------------+ 012. #define def_PathBtns "Images\\Market Replay\\Orders\\" 013. #define def_Btn_Close def_PathBtns + "Btn_Close.bmp" 014. #resource "\\" + def_Btn_Close; 015. //+------------------------------------------------------------------+ 016. #include "..\Auxiliar\C_Mouse.mqh" 017. //+------------------------------------------------------------------+ 018. #ifdef def_FontName 019. "Why are you trying to do this?" 020. #else 021. #define def_FontName "Lucida Console" 022. #define def_FontSize 10 023. #endif 024. //+------------------------------------------------------------------+ 025. class C_ElementsTrade : public C_Mouse 026. { 027. private : 028. //+------------------------------------------------------------------+ 029. struct st00 030. { 031. struct st_01 032. { 033. uchar Width, 034. Height, 035. digits; 036. }Text; 037. ulong ticket; 038. string szPrefixName, 039. szDescr; 040. EnumEvents ev; 041. double price, 042. open; 043. bool bClick, 044. bIsBuy; 045. char weight; 046. color _color; 047. }m_Info; 048. //+------------------------------------------------------------------+ 049. void UpdateViewPort(const double price) 050. { 051. int x, y; 052. 053. ChartTimePriceToXY(0, 0, 0, price, x, y); 054. x = (m_Info.ev == evMsgClosePositionEA ? 150 : (m_Info.ev == evMsgCloseTakeProfit ? 220 : 290)); 055. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 056. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 057. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 058. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 059. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10); 060. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 061. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + m_Info.Text.Width + 20); 062. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 063. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + m_Info.Text.Width + 20); 064. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 065. } 066. //+------------------------------------------------------------------+ 067. inline void CreateLinePrice(void) 068. { 069. string szObj; 070. 071. CreateObjectGraphics(szObj = def_NameHLine, OBJ_RECTANGLE_LABEL, m_Info._color, (EnumPriority)(ePriorityDefault)); 072. ObjectSetInteger(0, szObj, OBJPROP_BGCOLOR, m_Info._color); 073. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH)); 074. ObjectSetInteger(0, szObj, OBJPROP_BORDER_TYPE, BORDER_FLAT); 075. ObjectSetInteger(0, szObj, OBJPROP_CORNER, CORNER_LEFT_UPPER); 076. ObjectSetString(0, szObj, OBJPROP_TOOLTIP, m_Info.szDescr); 077. macro_LineInFocus(false); 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateBoxInfo(const bool bMove) 081. { 082. string szObj; 083. const char c[] = {(char)(bMove ? 'u' : (m_Info.bIsBuy ? 236 : 238)), 0}; 084. 085. CreateObjectGraphics(szObj = (bMove ? def_NameBtnMove : def_NameInfoDirect), OBJ_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 086. ObjectSetString(0, szObj, OBJPROP_FONT, "Wingdings"); 087. ObjectSetString(0, szObj, OBJPROP_TEXT, CharArrayToString(c)); 088. ObjectSetInteger(0, szObj, OBJPROP_COLOR, (bMove ? m_Info._color : (m_Info.bIsBuy ? clrForestGreen : clrFireBrick))); 089. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, (bMove ? 17 : 15)); 090. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 091. } 092. //+------------------------------------------------------------------+ 093. inline void CreateObjectInfoText(void) 094. { 095. string szObj; 096. 097. CreateObjectGraphics(szObj = def_NameObjLabel, OBJ_EDIT, clrNONE, (EnumPriority)(ePriorityDefault)); 098. ObjectSetString(0, szObj, OBJPROP_FONT, def_FontName); 099. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, def_FontSize); 100. ObjectSetInteger(0, szObj, OBJPROP_COLOR, clrBlack); 101. ObjectSetInteger(0, szObj, OBJPROP_BORDER_COLOR, m_Info._color); 102. ObjectSetInteger(0, szObj, OBJPROP_ALIGN, ALIGN_CENTER); 103. ObjectSetInteger(0, szObj, OBJPROP_READONLY, true); 104. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 105. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 106. } 107. //+------------------------------------------------------------------+ 108. inline void CreateButtonClose(void) 109. { 110. string szObj; 111. 112. CreateObjectGraphics(szObj = def_NameBtnClose, OBJ_BITMAP_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 113. ObjectSetString(0, szObj, OBJPROP_BMPFILE, 0, "::" + def_Btn_Close); 114. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 115. } 116. //+------------------------------------------------------------------+ 117. public : 118. //+------------------------------------------------------------------+ 119. C_ElementsTrade(const ulong ticket, const EnumEvents ev, color _color, char digits, string szDescr = "\n", const bool IsBuy = true) 120. :C_Mouse(0, "") 121. { 122. uint w, h; 123. 124. ZeroMemory(m_Info); 125. m_Info.szPrefixName = StringFormat("%I64u@%03d", m_Info.ticket = ticket, (int)(m_Info.ev = ev)); 126. m_Info._color = _color; 127. m_Info.szDescr = szDescr; 128. m_Info.bIsBuy = IsBuy; 129. m_Info.Text.digits = digits; 130. TextSetFont(def_FontName, def_FontSize * -10); 131. TextGetSize(StringFormat("%." + (string)digits + "f", 8888.88888), w, h); 132. m_Info.Text.Width = (uchar) w + 4; 133. m_Info.Text.Height = (uchar) h + 4; 134. } 135. //+------------------------------------------------------------------+ 136. ~C_ElementsTrade() 137. { 138. ObjectsDeleteAll(0, m_Info.szPrefixName); 139. } 140. //+------------------------------------------------------------------+ 141. inline void UpdatePrice(const double open, const double price) 142. { 143. if (price > 0) 144. { 145. CreateLinePrice(); 146. CreateButtonClose(); 147. }else 148. ObjectsDeleteAll(0, m_Info.szPrefixName); 149. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 150. if (price > 0) 151. CreateObjectInfoText(); 152. m_Info.open = open; 153. UpdateViewPort(m_Info.price = (price > 0 ? price : open)); 154. if (m_Info.ev != evMsgClosePositionEA) 155. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 156. } 157. //+------------------------------------------------------------------+ 158. void ViewValue(const double profit) 159. { 160. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, StringFormat("%." + (string)m_Info.Text.digits + "f", (profit < 0 ? -(profit) : profit))); 161. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, (profit >= 0 ? clrPaleGreen : clrCoral)); 162. ChartRedraw(); 163. } 164. //+------------------------------------------------------------------+ 165. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 166. { 167. string sz0; 168. long _lparam = lparam; 169. double _dparam = dparam; 170. 171. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 172. switch (id) 173. { 174. case (CHARTEVENT_KEYDOWN): 175. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 176. _lparam = (long) m_Info.ticket; 177. _dparam = 0; 178. case CHARTEVENT_CUSTOM + evMsgSetFocus: 179. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 180. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 181. m_Info.bClick = false; 182. case CHARTEVENT_CHART_CHANGE: 183. UpdateViewPort(m_Info.price); 184. if (m_Info.ev != evMsgClosePositionEA) 185. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 186. break; 187. case CHARTEVENT_OBJECT_CLICK: 188. sz0 = GetPositionsMouse().szObjNameClick; 189. if (m_Info.bClick) switch (m_Info.ev) 190. { 191. case evMsgClosePositionEA: 192. if (sz0 == def_NameBtnClose) 193. EventChartCustom(0, evMsgClosePositionEA, m_Info.ticket, 0, ""); 194. break; 195. case evMsgCloseTakeProfit: 196. if (sz0 == def_NameBtnClose) 197. EventChartCustom(0, evMsgCloseTakeProfit, m_Info.ticket, PositionGetDouble(POSITION_SL), PositionGetString(POSITION_SYMBOL)); 198. else if (sz0 == def_NameBtnMove) 199. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseTakeProfit, ""); 200. break; 201. case evMsgCloseStopLoss: 202. if (sz0 == def_NameBtnClose) 203. EventChartCustom(0, evMsgCloseStopLoss, m_Info.ticket, PositionGetDouble(POSITION_TP), PositionGetString(POSITION_SYMBOL)); 204. else if (sz0 == def_NameBtnMove) 205. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseStopLoss, ""); 206. break; 207. } 208. m_Info.bClick = false; 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 212. if (m_Info.weight > 1) 213. { 214. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 215. if (m_Info.ev != evMsgClosePositionEA) 216. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 217. if (m_Info.bClick) 218. { 219. switch (m_Info.ev) 220. { 221. case evMsgCloseTakeProfit: 222. EventChartCustom(0, evMsgNewTakeProfit, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 223. break; 224. case evMsgCloseStopLoss: 225. EventChartCustom(0, evMsgNewStopLoss, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 226. break; 227. } 228. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 229. } 230. } 231. break; 232. } 233. } 234. //+------------------------------------------------------------------+ 235. }; 236. //+------------------------------------------------------------------+ 237. #undef macro_LineInFocus 238. //+------------------------------------------------------------------+ 239. #undef def_Btn_Close 240. #undef def_PathBtns 241. #undef def_FontName 242. #undef def_FontSize 243. //+------------------------------------------------------------------+ 244. #undef def_NameObjLabel 245. #undef def_NameInfoDirect 246. #undef def_NameBtnMove 247. #undef def_NameBtnClose 248. #undef def_NameHLine 249. //+------------------------------------------------------------------+
C_ElementsTrade.mqh
Хорошо. Первое, что вы заметите, — это конструкция, которая на первый взгляд кажется странной, если посмотреть на строку 18. Вы, уважаемый читатель, возможно, задаётесь вопросом: почему в коде используется это условие препроцессора? Причина в том, что при работе в C/C++ с библиотеками функций или другими повторно используемыми модулями в разных файлах могут определяться одинаковые имена, что приводит к конфликтам. Следует различать этапы: сначала препроцессор обрабатывает директивы и подготавливает активный исходный код, а затем компилятор транслирует этот код. В C/C++ #error — это диагностическая директива препроцессора. Когда она встречается в активной ветке, то выдаёт диагностическое сообщение и останавливает трансляцию. Цель этой директивы — явно указать на недопустимую конфигурацию или условие, обнаруженные до создания исполняемого файла.
MQL5 не предоставляет эквивалентной директивы #error. Поэтому в коде используется нераспознаваемая директива внутри условной ветви, чтобы намеренно вызвать ошибку, если определённый макрос def_FontName уже был определён. Нераспознаваемая директива служит защитой от переопределения или конфликта конфигурации, хотя и не воспроизводит формальную семантику и настраиваемое сообщение директивы #error в C/C++. Рассмотрим, что происходит.
На этапе предварительной обработки строка кода 18 с помощью #ifdef проверяет, определён ли уже макрос def_FontName. Если это так, активная ветвь включает строку кода 19. Поскольку MQL5 не поддерживает #error, препроцессор обнаруживает нераспознанную директиву и трансляция прерывается до создания исполняемого файла. Текст, записанный после #error, не следует интерпретировать как сообщение, создаваемое директивой, совместимой с MQL5. Если def_FontName не определён, эта ветвь отбрасывается, а препроцессор сохраняет блок между #else и #endif, чтобы компилятор мог его транслировать.
Если макрос def_FontName не был определён, препроцессор сохраняет блок между #else и #endif, в котором задаются имя и размер шрифта. Централизация обоих определений позволяет модифицировать их, не изменяя несколько участков кода. После определения имени и размера шрифта обратите внимание на строку кода 25. Обратите внимание, что наследование, которое раньше было закрытым, теперь стало открытым. Сейчас изменение наследования с private на public не кажется важным, но в конце статьи именно оно позволит реализовать дальнейшие изменения. В строке кода 31 объявляется структура, которая хранит размеры и данные форматирования, необходимые для отображения текста.
Функция UpdateViewPort, расположенная в строке кода 49, использует новые переменные, чтобы применить к полю OBJ_EDIT рассчитанные ширину и высоту. В строке кода 93 эти размеры повторно используются при настройке графического объекта. В обоих местах функция считывает уже сохранённые значения; конструктор класса инициализирует их, начиная со строки кода 119. Теперь обратите внимание на то, как мы инициализируем размеры, потому что вам, возможно, придётся изменить этот код, если вы расширите функциональность.
Обратите внимание, что в строке 122 мы объявляем две новые локальные переменные, а до строки 130 предыдущий код остаётся таким, как вы его уже видели. При создании экземпляра конструктор выполняет строки 130 и 131, в которых настраивается шрифт и вычисляются ширина и высота текста в пикселях. Компилятор лишь проверяет и транслирует эти вызовы. Затем в строках 132 и 133 к каждому размеру добавляется по четыре пикселя, чтобы оставить по два пикселя с каждой стороны. Кроме того, я внёс в класс и другие незначительные изменения, которые не требуют подробного объяснения. Однако я хочу отметить одно важное дополнение.
Строки 184 и 215 проверяют, соответствует ли текущий экземпляр C_ElementsTrade линии тейк-профита или стоп-лосса, прежде чем вычислить её расстояние относительно цены открытия. А теперь обратите внимание. В предыдущей статье я показал, что поле OBJ_EDIT, расположенное над линией цены открытия, позволяет отображать текущую прибыль или убыток. В этом расширении мы повторно используем ту же логику, чтобы другие поля OBJ_EDIT отображали расстояние в пунктах между ценой открытия и линиями тейк-профита или стоп-лосса. Нет необходимости заново программировать всю логику отображения.
Точно так же, как поле OBJ_EDIT на линии цены открытия показывает текущую прибыль или убыток, поля, связанные с тейк-профитом и стоп-лоссом, отображают расстояние в пунктах относительно цены открытия. В строках 185 и 216 рассчитывается расстояние в пунктах, а результат записывается в поля OBJ_EDIT тейк-профита и стоп-лосса. Поскольку эта же функция также обрабатывает экземпляр, связанный с ценой открытия, условия в строках 184 и 215 исключают этот экземпляр и не позволяют перезаписать значение прибыли или убытка, отображаемое на линии цены открытия.
Эта версия компилируется, но во время выполнения по-прежнему ведёт себя некорректно. Поэтому нам необходимо обновить код индикатора. Ниже я привожу полную изменённую версию.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property icon "/Images/Market Replay/Icons/Positions.ico" 004. #property description "Indicator for tracking an open position on the server." 005. #property description "This should preferably be used together with an Expert Advisor." 006. #property description "For more details see the same article." 007. #property version "1.126" 008. #property link "https://www.mql5.com/pt/articles/13361" 009. #property indicator_chart_window 010. #property indicator_plots 0 011. //+------------------------------------------------------------------+ 012. #define def_ShortName "Position View" 013. //+------------------------------------------------------------------+ 014. #include <Market Replay\Order System\C_ElementsTrade.mqh> 015. #include <Market Replay\Defines.mqh> 016. //+------------------------------------------------------------------+ 017. input ulong user00 = 0; //Для использования в советнике 018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen; 025. char digits; 026. bool bIsBuy; 027. }m_Infos; 028. //+------------------------------------------------------------------+ 029. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 030. //+------------------------------------------------------------------+ 031. bool CheckCatch(ulong ticket) 032. { 033. ZeroMemory(m_Infos); 034. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 035. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 036. if (ObjectFind(0, m_Infos.szShortName) >= 0) 037. { 038. m_Infos.ticket = 0; 039. return false; 040. } 041. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 042. m_Infos.digits = (char)SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 043. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 044. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 045. 046. return true; 047. } 048. //+------------------------------------------------------------------+ 049. inline void ProfitNow(void) 050. { 051. double ask, bid; 052. 053. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 054. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 055. if (Open != NULL) 056. (*Open).ViewValue((m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 057. } 058. //+------------------------------------------------------------------+ 059. int OnInit() 060. { 061. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 062. if (!CheckCatch(user00)) 063. { 064. ChartIndicatorDelete(0, 0, def_ShortName); 065. return INIT_FAILED; 066. } 067. 068. return INIT_SUCCEEDED; 069. } 070. //+------------------------------------------------------------------+ 071. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 072. { 073. ProfitNow(); 074. 075. return rates_total; 076. } 077. //+------------------------------------------------------------------+ 078. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 079. { 080. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 081. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 082. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 083. switch (id) 084. { 085. case CHARTEVENT_CUSTOM + evUpdate_Position: 086. if (lparam != m_Infos.ticket) return; 087. if (!PositionSelectByTicket(m_Infos.ticket)) 088. { 089. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 090. return; 091. }; 092. if (Open == NULL) Open = new C_ElementsTrade(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, m_Infos.digits, StringFormat("%I64u : Position opening price.", m_Infos.ticket), m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)); 093. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, m_Infos.digits, StringFormat("%I64u : Take Profit price.", m_Infos.ticket), m_Infos.bIsBuy); 094. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, m_Infos.digits, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket), m_Infos.bIsBuy); 095. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 096. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 097. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 098. ProfitNow(); 099. break; 100. } 101. ChartRedraw(); 102. }; 103. //+------------------------------------------------------------------+ 104. void OnDeinit(const int reason) 105. { 106. delete Open; 107. delete Take; 108. delete Stop; 109. } 110. //+------------------------------------------------------------------+
Индикатор позиции
Изменения в коде оставлены в качестве упражнения, чтобы вы могли изучить принцип их работы. Поскольку это лишь незначительные изменения, я не буду здесь вдаваться в подробности. При выполнении обновлённого кода вы получите результат, показанный в следующей анимации:

Обратите внимание: когда мы перемещаем линию тейк-профита или стоп-лосса, индикатор показывает, сколько пунктов отделяет её от цены открытия и линии, которую мы настраиваем. Один нюанс: если вы переместите линию тейк-профита так, что значение станет отрицательным, индикатор это отразит, и значение начнёт отображаться красным цветом, как у линии стоп-лосса. Изменение цвета означает, что если тейк-профит отображается красным, то при закрытии позиции в этой точке вы понесёте убытки. То же самое относится и к стоп-лоссу. Когда стоп-лосс перейдёт в область, где значение начнёт отображаться зелёным цветом — таким же, как у тейк-профита, — это будет означать, что, если позиция закроется в этой точке, вы, разумеется, получите прибыль, равную указанному количеству пунктов. Принцип работы индикатора прост и понятен. В этом нет ничего сложного.
Однако есть одна деталь. Обратите внимание, что у позиции присутствуют и линия тейк-профита, и линия стоп-лосса. Но что произойдёт, если вы попытаетесь создать одну из них с помощью элементов управления Position View? Будет ли графическое поле расстояния показывать количество пунктов в процессе создания? Хорошо. Не внося никаких дополнительных изменений в код, вы получите результат, показанный в следующей анимации.

Обратите внимание, что поле OBJ_EDIT для расстояния появляется только после того, как вы щёлкнете по цене, на которой будет создан тейк-профит или стоп-лосс. С этого момента поле показывает расстояние в пунктах между подтверждённой линией и ценой открытия. Расчёт работает для обеих линий. Во время первоначального перетаскивания подтверждённой линии тейк-профита или стоп-лосса ещё не существует; мы лишь перемещаем графический объект взаимодействия. Хотя это не мешает создать линию, сначала всё же приходится подтверждать цену, а уже потом корректировать её положение, как мы видели в первой анимации. Такое поведение выглядит неестественно, поэтому его нужно улучшить. Изменение индикатора кажется сложной задачей, словно для этого требуется докторская степень по программированию или гениальный ум с уровнем интеллекта в пять миллионов. Требует ли такая доработка столь высокого уровня сложности? Чтобы ответить на этот вопрос, перейдём к следующей теме, поскольку так мы сможем лучше разграничить эти два вопроса.
Добавление более интуитивного взаимодействия
Теперь мы реализуем это решение, поскольку MQL5 следует тому же принципу, что и событийно-ориентированное программирование. Разработчики часто используют эту модель при создании DLL. Я знаю, что сначала событийно-ориентированное программирование может поначалу показаться запутанным и не слишком логичным. Но поверьте мне, уважаемый читатель: я уже давно работаю с событийно-ориентированным программированием и могу с уверенностью сказать, что для его понимания и освоения требуются практика и упорство. Вам придётся написать несколько событийно-ориентированных приложений, чтобы полностью понять механизм, который я постараюсь здесь объяснить.
Идея заключается в том, чтобы как можно меньше изменять код и при этом обеспечить более интуитивное взаимодействие. При попытке создать линию тейк-профита или стоп-лосса необходимо во время перетаскивания обновлять поле расстояния OBJ_EDIT значением, соответствующим текущей вертикальной координате курсора. Так мы будем показывать предварительное расстояние до подтверждения линии. Пока тейк-профит или стоп-лосс ещё не существует на сервере, присутствует только графический объект взаимодействия, который позволяет выбрать тип линии, переместить курсор к нужной цене и подтвердить создание щелчком мыши. Выбор и перетаскивание работают правильно, но поле OBJ_EDIT не вычисляет расстояние между этой предварительной ценой и ценой открытия. Наша первая задача — обновлять поле во время перетаскивания.
Вторая проблема возникает, когда мы временно отображаем поле расстояния OBJ_EDIT перед созданием линии. После начала создания пользователь отменяет операцию клавишей ESC. В этом случае линия стоп-лосс или тейк-профит не создаётся. Отмена уже работает, но при этом также требуется скрывать или сбрасывать графическое поле расстояния. В противном случае это поле останется видимым на графике со значением, соответствующим несуществующей линии, и будет предоставлять трейдеру ложную информацию.
Нам нужно решить две задачи с минимальным количеством изменений: обновлять поле OBJ_EDIT во время перетаскивания и скрывать его после отмены операции. Многие начали бы бездумно добавлять и переписывать код, а также изменять значительную часть C_ElementsTrade, чтобы обновлять и скрывать поле расстояния. Это был бы вполне приемлемый вариант, но здесь мы изменим только код индикатора позиции.
Ниже приведён новый код.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property icon "/Images/Market Replay/Icons/Positions.ico" 004. #property description "Indicator for tracking an open position on the server." 005. #property description "This should preferably be used together with an Expert Advisor." 006. #property description "For more details see the same article." 007. #property version "1.126" 008. #property link "https://www.mql5.com/pt/articles/13361" 009. #property indicator_chart_window 010. #property indicator_plots 0 011. //+------------------------------------------------------------------+ 012. #define def_ShortName "Position View" 013. //+------------------------------------------------------------------+ 014. #include <Market Replay\Order System\C_ElementsTrade.mqh> 015. #include <Market Replay\Defines.mqh> 016. //+------------------------------------------------------------------+ 017. input ulong user00 = 0; //Для использования в советнике 018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen; 025. char digits; 026. bool bIsBuy; 027. }m_Infos; 028. //+------------------------------------------------------------------+ 029. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 030. //+------------------------------------------------------------------+ 031. bool CheckCatch(ulong ticket) 032. { 033. ZeroMemory(m_Infos); 034. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 035. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 036. if (ObjectFind(0, m_Infos.szShortName) >= 0) 037. { 038. m_Infos.ticket = 0; 039. return false; 040. } 041. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 042. m_Infos.digits = (char)SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 043. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 044. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 045. 046. return true; 047. } 048. //+------------------------------------------------------------------+ 049. inline void ProfitNow(void) 050. { 051. double ask, bid; 052. 053. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 054. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 055. if (Open != NULL) 056. (*Open).ViewValue((m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 057. } 058. //+------------------------------------------------------------------+ 059. int OnInit() 060. { 061. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 062. if (!CheckCatch(user00)) 063. { 064. ChartIndicatorDelete(0, 0, def_ShortName); 065. return INIT_FAILED; 066. } 067. 068. return INIT_SUCCEEDED; 069. } 070. //+------------------------------------------------------------------+ 071. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 072. { 073. ProfitNow(); 074. 075. return rates_total; 076. } 077. //+------------------------------------------------------------------+ 078. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 079. { 080. switch (id) 081. { 082. case CHARTEVENT_CUSTOM + evMsgSetFocus: 083. if (lparam != m_Infos.ticket) break; 084. switch ((EnumEvents)dparam) 085. { 086. case evMsgCloseTakeProfit: 087. (*Take).UpdatePrice(m_Infos.priceOpen, (*Take).GetPositionsMouse().Position.Price); 088. break; 089. case evMsgCloseStopLoss: 090. (*Stop).UpdatePrice(m_Infos.priceOpen, (*Stop).GetPositionsMouse().Position.Price); 091. break; 092. } 093. break; 094. case CHARTEVENT_CUSTOM + evUpdate_Position: 095. if (lparam != m_Infos.ticket) break; 096. if (!PositionSelectByTicket(m_Infos.ticket)) 097. { 098. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 099. return; 100. }; 101. if (Open == NULL) Open = new C_ElementsTrade(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, m_Infos.digits, StringFormat("%I64u : Position opening price.", m_Infos.ticket), m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)); 102. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, m_Infos.digits, StringFormat("%I64u : Take Profit price.", m_Infos.ticket), m_Infos.bIsBuy); 103. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, m_Infos.digits, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket), m_Infos.bIsBuy); 104. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 105. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 106. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 107. ProfitNow(); 108. break; 109. } 110. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 111. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 112. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 113. ChartRedraw(); 114. }; 115. //+------------------------------------------------------------------+ 116. void OnDeinit(const int reason) 117. { 118. delete Open; 119. delete Take; 120. delete Stop; 121. } 122. //+------------------------------------------------------------------+
Код индикатора
В следующей анимации мы нажимаем клавишу ESC и отменяем создание линии.

Следующая анимация показывает результат при создании линии.

При изучении кода индикатора его поведение может показаться труднообъяснимым. Сравните его с предыдущей версией, которую мы рассматривали в этой же статье. Вы увидите, что различия действительно есть. Но даже если они и есть, почему модифицированная версия индикатора, в которой гораздо меньше изменений, чем можно было бы ожидать, позволяет практически завершить его реализацию? То, что всего несколько изменений позволяют почти полностью реализовать индикатор, кажется магией или колдовством, словно кто-то заключил сделку. Но нет, уважаемый читатель. Мы получаем этот результат благодаря правильному применению механизмов событий, сообщений и повторного использования кода, которые предоставляет MQL5.
Давайте разберёмся, почему для получения желаемого результата оказалось достаточным изменить только код индикатора. Обратите внимание. В строке 49 объявлена вспомогательная функция, в которой сосредоточена логика обновления, ранее находившаяся внутри OnCalculate. Когда MetaTrader 5 вызовет OnCalculate, строка 73 выполнит эту функцию, чтобы пересчитать и обновить отображение позиции. Ключевое слово inline в строке 49 не превращает функцию в макрос и не заставляет компилятор MQL5 копировать её тело в каждой точке вызова. В MQL5 ключевое слово inline сохранено для совместимости с C++, но не изменяет сгенерированный код; компилятор автоматически решает, применять ли развёртывание в точке вызова как оптимизацию. С точки зрения кода это по-прежнему функция с обычными вызовами и параметрами.
Вспомогательная функция позволяет повторно использовать одну и ту же логику в двух обработчиках. Строка кода 73 выполняется, когда MetaTrader 5 вызывает OnCalculate; строка кода 107 выполняется из OnChartEvent, чтобы обновить отображение после получения события графика. Теперь оставим в стороне остальные разделы кода и сосредоточимся на OnChartEvent, где координируется реакция на события графика. Понимание потока событий и вызовов играет огромную роль в вашем становлении как программиста, особенно при работе с событийно-ориентированными приложениями.
Этот поток зависит от события, которое генерирует пользователь или MetaTrader 5. Когда терминал обнаруживает это событие, он вызывает OnChartEvent и передаёт обработчику связанные с ним данные. Если вы будете рассматривать эту функцию так, будто программа выполняет её инструкции во время компиляции, вы не поймёте сам механизм. Но если вы будете следовать за потоком событий и вызовов, эта последовательность начнёт обретать смысл. Внутри OnChartEvent мы перенесли инструкции из строк 110–112 из начала обработчика в его конец. Таким образом, обработчик сначала обрабатывает полученное сообщение, а затем обновляет общее отображение.
В строке кода 178 файла C_ElementsTrade идентификатор сообщения evMsgSetFocus определяется как тот, который OnChartEvent проверяет в строке 82. Когда обработчик получает этот идентификатор, строка 83 проверяет, принадлежит ли тикет позиции, которой управляет Position View. Далее строка 84 распределяет выполнение в зависимости от выбранного типа линии: ветвь, доходящая до строки 87, вычисляет и отображает предварительное расстояние до тейк-профита, а ветвь, доходящая до строки 90, делает то же самое для стоп-лосса. Благодаря этим двум ветвям Position View поддерживает синхронизацию графического отображения расстояния при создании или корректировке каждой линии.
Но остаётся вопрос: как изменённый код индикатора создаёт то поведение, которое мы видим в анимациях? Связь с C_ElementsTrade заключается в вызовах в строках 87 и 90: они передают цену открытия и предварительную цену линии в функцию, которая вычисляет и обновляет поле OBJ_EDIT с расстоянием.
Именно так, уважаемый читатель. Этот механизм требует внимательно отслеживать поток событий. Наверняка вы ожидали, что для того, чтобы показанное в анимациях имело смысл, потребуется изменить код класса C_ElementsTrade. Тем не менее, мы всё же изменили класс, хотя и довольно тонко. Помните, в предыдущей теме я упоминал, что мы изменили наследование? Это изменение позволяет индикатору обращаться к унаследованной функции, которую используют строки кода 87 и 90. Каждый вызов преобразует вертикальную координату курсора в цену на графике, вычисляет разницу относительно цены открытия и записывает полученное расстояние в соответствующее поле OBJ_EDIT. Функция принимает две цены: цену открытия позиции и опорную цену линии. Когда линия уже подтверждена, второе значение — это её фактическая цена; во время перетаскивания — предварительная цена, определяемая по положению курсора. В строках 105 и 106 выполняется тот же расчёт с ценами тейк-профита и стоп-лосса, уже подтверждёнными на сервере.
Строки 105 и 106 обновляют графическую индикацию, отображая цену тейк-профита или стоп-лосса, зарегистрированную на сервере. В свою очередь, строки кода 87 и 90 используют предварительную цену, рассчитанную на основе вертикальной координаты курсора. На этом этапе на сервере ещё не существует линии тейк-профита или стоп-лосса с такой ценой; существуют только графический объект взаимодействия и локальная индикация расстояния. Дева Мария. Теперь анализ действительно усложнился. Итак, когда мы выбираем объект взаимодействия и перемещаем курсор, именно строки 87 и 90 перемещают этот объект в анимации? НЕТ.
В предыдущей версии вызовов в строках 87 и 90 не было. Эти вызовы НЕ ПЕРЕМЕЩАЮТ ГРАФИЧЕСКИЙ ОБЪЕКТ: они получают предварительную цену, связанную с курсором, вычисляют её расстояние до цены открытия и обновляют соответствующее поле OBJ_EDIT до того, как линия будет подтверждена. Таким образом повторно используется та же логика расчёта и визуализации без утверждения, что линия уже существует на сервере.
Заключительные замечания
Я знаю, что поначалу код может показаться запутанным. Однако понять, как это работает, просто глядя на код, не так-то легко, а обмен сообщениями и событиями трудно понять без понимания лежащей в основе логики. Тем не менее, мне нужно немного больше места, чтобы подробно объяснить, что здесь происходит. В следующей статье я временно отложу разработку системы репликации/моделирования, чтобы подробнее объяснить, как приложения и MetaTrader 5 обмениваются сообщениями и событиями.
Таким образом, те, кто хочет научиться использовать событийно-ориентированную модель программирования, будут знать, с чего начать. Чтобы понять, как работает этот механизм, мне потребовалось много времени на его изучение. Когда я это понял, я стал разрабатывать и писать программы по-другому.
Я хочу, чтобы вы, уважаемый читатель, поняли, как координируются события, сообщения и графические обновления. Многие программисты тратят по нескольку часов на решения, которые можно реализовать гораздо быстрее и понятнее с помощью событийно-ориентированного программирования и обмена сообщениями.
Чтобы проверить работу системы сообщений и событий, я приложу уже скомпилированные файлы. Так вы избежите компиляции всех файлов и снизите риск допустить ошибки при компиляции. На этом я пока прощаюсь. До встречи в следующей статье, где я обещаю объяснить механизм обмена сообщениями и событийно-ориентированное программирование. До следующей встречи.
| Файл | Описание |
|---|---|
| Experts\Expert Advisor.mq5 | Показывает взаимодействие между Chart Trade и советником (для взаимодействия необходимо использовать Mouse Study) |
| Indicators\Chart Trade.mq5 | Создает окно для настройки ордера, который будет отправлен (для взаимодействия необходимо использовать Mouse Study) |
| Indicators\Market repetición.mq5 | Создаёт элементы управления для взаимодействия со сервисом репликации/моделирования рынка (для взаимодействия необходимо использовать Mouse Study) |
| Indicators\Mouse Study.mq5 | Обеспечивает взаимодействие между графическими элементами управления и пользователем (необходимо как для работы системы репликации/моделирования, так и на реальном рынке) |
| Indicators\Order Indicator.mq5 | Отображает рыночные ордера и позволяет взаимодействовать с ними и управлять ими. |
| Indicators\Position View.mq5 | Отображает рыночные позиции и позволяет взаимодействовать с ними и управлять ими. |
| Services\Market repetición.mq5 | Создаёт и поддерживает сервис репликации/моделирования рынка (главный файл системы репликации/моделирования рынка). |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/13361
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание и использование Скиллов для создания артефактов MQL
Нейросети в трейдинге: Долговременная память торговых сценариев (Объект верхнего уровня)
Автоматизация торговых стратегий в MQL5 (Часть 50): Стратегия Turtle Soup на снятии ликвидности
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования