Моделирование рынка: Position View (XV)
Введение
Всем привет и добро пожаловать в новую статью из серии, посвящённой созданию системы репликации/моделирования.
В предыдущей статье «Моделирование рынка: Position View (XIV)» я показал, как добавить необходимые детали, чтобы линии тейк-профита / стоп-лосса отображали результат, связанный с их срабатыванием. То есть когда цена достигнет одного из этих уровней — стоп-лосса или тейк-профита, — мы сможем понять, приведёт ли это к прибыли или убытку и какова будет их величина. Пока что мы выражаем этот результат в пунктах, поскольку ещё не работаем с денежными суммами.
В той же статье я пообещал подробно объяснить, как обмен сообщениями и событийно-ориентированное программирование делают возможной работу такого кода, как тот, который я здесь представляю, — в чём многие могут легко убедиться. Я включаю исполняемые файлы во вложение, когда они становятся достаточно стабильными, а также привожу полный код, чтобы другие программисты имели доступ к информации и к тому, что реализуется.
Хотя код, представленный до предыдущей статьи, работает отлично, мы можем пойти гораздо дальше. Чтобы упростить объяснение, я решил снова опубликовать тот же код, но с обновлёнными вызовами. Он работает так же, как и предыдущая версия, и хотя реализация немного изменилась, отдельно объяснять её не нужно: если вы поймёте то, что я объясню в этой статье, то сможете понять и этот код, если следили за этой серией до настоящего момента. Давайте начнём.
Краткое обновление
Ниже вы можете увидеть обновлённый код. Сравните его с версией из предыдущей статьи, и вы увидите, что это всё тот же код, хотя на первый взгляд он может показаться другим. Основной код приведён ниже:
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.127" 008. #property link "https://www.mql5.com/pt/articles/13375" 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) break; 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. //+------------------------------------------------------------------+
Индикатор позиции
Ниже приведён код класса:
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 : private 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. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 179. case CHARTEVENT_CUSTOM + evMsgSetFocus: 180. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 181. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 182. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 183. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 184. m_Info.bClick = false; 185. case CHARTEVENT_CHART_CHANGE: 186. UpdateViewPort(m_Info.price); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. break; 190. case CHARTEVENT_OBJECT_CLICK: 191. sz0 = GetPositionsMouse().szObjNameClick; 192. if (m_Info.bClick) 193. { 194. if (sz0 == def_NameBtnMove) 195. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 196. if (sz0 == def_NameBtnClose) 197. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, PositionGetDouble(m_Info.ev == evMsgCloseTakeProfit ? POSITION_SL : POSITION_TP), PositionGetString(POSITION_SYMBOL)); 198. } 199. m_Info.bClick = false; 200. break; 201. case CHARTEVENT_MOUSE_MOVE: 202. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 203. if (m_Info.weight > 1) 204. { 205. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 206. if (m_Info.ev != evMsgClosePositionEA) 207. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 208. if (m_Info.bClick) 209. { 210. if ((m_Info.ev == evMsgCloseTakeProfit) || (m_Info.ev == evMsgCloseStopLoss)) 211. EventChartCustom(0, (ushort)(m_Info.ev == evMsgCloseTakeProfit ? evMsgNewTakeProfit : evMsgNewStopLoss), m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 212. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 213. } 214. } 215. break; 216. } 217. } 218. //+------------------------------------------------------------------+ 219. }; 220. //+------------------------------------------------------------------+ 221. #undef macro_LineInFocus 222. //+------------------------------------------------------------------+ 223. #undef def_Btn_Close 224. #undef def_PathBtns 225. #undef def_FontName 226. #undef def_FontSize 227. //+------------------------------------------------------------------+ 228. #undef def_NameObjLabel 229. #undef def_NameInfoDirect 230. #undef def_NameBtnMove 231. #undef def_NameBtnClose 232. #undef def_NameHLine 233. //+------------------------------------------------------------------+
C_ElementsTrade
После компиляции этот код работает так же, как и код из предыдущей статьи, поэтому не требует особых пояснений. Тем не менее я буду использовать его в качестве основы для дальнейшего объяснения, поскольку так будет немного легче понять некоторые понятия, которые мне понадобятся. В некоторых случаях вам придётся обратиться к предыдущим статьям, чтобы проследить ход объяснения и понять, как событийное программирование и обмен сообщениями между приложениями позволяют этому, на первый взгляд, бессмысленному коду работать. Итак, устраивайтесь поудобнее, и давайте посмотрим, как использовать эту модель реализации. Я постараюсь объяснить это как можно более понятно.
Почему система работает
Чтобы понять всю систему, начнём с самого основного. Начальная и самая простая структура показана на следующем рисунке:

Давайте начнём разбирать обмен сообщениями, отталкиваясь от этого изображения. Мы уже довольно давно используем эту модель программирования, но до сих пор я не останавливался на её подробном объяснении. Обратите внимание, что от индикатора Chart Trade отходят три стрелки. Каждая из них представляет собой пользовательское событие, которое будет сгенерировано, когда пользователь взаимодействует с Chart Trade с помощью указателя мыши.
Хотя события перехватываются и обрабатываются в коде советника, Chart Trade не генерирует их напрямую для него. На самом деле он генерирует их через MetaTrader 5, который затем передаёт их соответствующему графику. Обратите внимание на эту деталь. Вы можете указать MetaTrader 5, чтобы он распределил событие на текущий график или на любой другой график.
В результате MetaTrader 5 распределит пользовательское событие и вызовет OnChartEvent в приложениях, привязанных к графику. Таким образом, советник сможет перехватить и обработать событие, которое Chart Trade сгенерировал по запросу пользователя. После правильной интерпретации советник отправит запрос на торговый сервер, чтобы открыть или закрыть позицию либо изменить её объём. Это первая часть системы. Когда сервер отвечает на запрос, запускается показанный ниже поток сообщений и событий. Показанный выше поток возникает каждый раз, когда на сервер отправляется запрос.
Тем не менее, обратите внимание, что на схеме есть фиолетовая стрелка. Эта стрелка указывает на то, что советник создаст индикатор позиции. На этом этапе не генерируется ни одного события, которое могли бы перехватить другие приложения.

Однако, как только индикатор появляется на графике, советник генерирует пользовательское событие через MetaTrader 5. MetaTrader 5 распределяет его по графику, а индикатор позиции перехватывает его и получает самые свежие данные о позиции. Обратите внимание, что индикатор позиции не знает о присутствии советника. Точно так же советник не знает, присутствует ли на графике индикатор позиции.
Оба приложения вообще не знают о существовании друг друга. Они лишь генерируют пользовательское событие через MetaTrader 5, а тот распределяет его по графику, указанному приложением-отправителем. Событие будет перехвачено и обработано, только если какое-либо приложение реализует соответствующую обработку. В противном случае оно будет потеряно.
Однако на предыдущем изображении не показано, что происходит, когда пользователь взаимодействует с индикатором позиции, поскольку на нём отсутствует индикатор мыши. Когда между этими двумя индикаторами происходит взаимодействие, работа системы немного меняется, и мы получаем следующую схему.

Если теперь взглянуть на схему работы, может показаться, что всё стало гораздо сложнее, но это не так. На приведённом выше изображении полностью показано, что происходит между индикатором мыши, индикатором позиции и советником, когда все три присутствуют на графике. Теперь мы можем вернуться к коду, приведённому в начале статьи, чтобы понять каждую из стрелок на диаграмме.
Обратите внимание на одну любопытную деталь. Здесь появляется серая стрелка, которая, по-видимому, обозначает обмен информацией между индикатором мыши и индикатором позиции. Однако это не та связь, которую она показывает. СЕРАЯ стрелка обозначает наследование между классами: в строке 25 класс C_ElementsTrade приватно наследуется от класса C_Mouse. Тем не менее, следует осторожно интерпретировать эту схему.
Можно было бы подумать, что нет необходимости держать индикатор мыши на графике, поскольку класс C_ElementsTrade наследуется от класса C_Mouse, но наследование лишь связывает классы и не заменяет индикатор. На графике обязательно должен присутствовать индикатор мыши. В противном случае индикатор позиции не сможет перехватывать события мыши. А теперь будьте очень внимательны.
На этой схеме мы видим стрелку, которая идёт от MetaTrader 5 к индикатору позиции. Эта стрелка обозначает поток событий, поступающих из MetaTrader 5: процедура OnChartEvent в основном коде перехватывает событие, а DispatchMessage внутри класса обрабатывает его. Обратите также внимание, что на диаграмме я отделил событие Update от остальных событий. Почему я это сделал? Потому что вам нужно понять, что здесь есть два типа событий. Первый тип генерирует советник: это события Update. Второй тип генерирует индикатор позиции или MetaTrader 5. В обоих случаях индикатор мыши и советник также перехватывают этот второй тип событий. Однако в их коде не реализована обработка события, поступающего от индикатора позиции, поэтому оно полностью игнорируется. Это же событие, напротив, важно для индикатора позиции.
Когда этот индикатор генерирует пользовательское событие, MetaTrader 5 вызывает OnChartEvent в приложениях, привязанных к графику. Все они перехватывают событие, сгенерированное индикатором позиции и распределённое MetaTrader 5. Однако соответствующая логика обработки реализована только в DispatchMessage индикатора позиции.
Один из моментов, когда это происходит, — как раз выполнение строки 178. Обратите внимание, что там генерируется пользовательское событие evUpdate_Position. Процедура OnChartEvent индикатора позиции перехватывает его в строке 85 и передаёт в DispatchMessage для обработки. В принципе, код мог бы интерпретировать это так, будто событие сгенерировал советник, но это не так. Его генерирует сам индикатор позиции через MetaTrader 5, который затем снова распределяет его приложениям, привязанным к графику. Таким образом, индикатор позиции принудительно обновляет данные позиции. Поскольку событие генерируется после нажатия клавиши ESC, мы можем обработать это условие без необходимости сохранять какие-либо данные в памяти.
Однако это не самый интересный аспект. Помните предыдущую статью, в которой я объяснял, как изменить код, чтобы создать линию стоп-лосса или тейк-профита, если она ещё не существовала, с помощью взаимодействия между индикатором мыши и индикатором позиции? Так вот, эта задача представлена на предыдущем рисунке, хотя это и не столь очевидно. Давайте проследим ход рассуждений. Класс C_ElementsTrade получает доступ к членам, унаследованным от C_Mouse, через закрытое наследование, тогда как индикатор позиции получает обновления от мыши в основном через события, поступающие от MetaTrader 5.
Изначально переменная m_Info.bClick будет иметь значение false. Таким образом, когда MetaTrader 5 сгенерирует событие CHARTEVENT_OBJECT_CLICK, строка 192 не позволит коду выполняться дальше, поскольку именно там проверяется значение m_Info.bClick. Однако, если индикатор мыши сообщает, что щелчок допустим, строка 202 присвоит переменной m_Info.bClick значение true. Поскольку событие CHARTEVENT_OBJECT_CLICK генерируется после CHARTEVENT_MOUSE_MOVE, как мы уже видели в предыдущих статьях, проверка в строке 192 будет пройдена. Таким образом, при щелчке по объекту перемещения строка 195 инициирует пользовательское событие через MetaTrader 5.
Именно здесь работа механизма становится интересной, хотя на первый взгляд всё может показаться немного запутанным. Всегда держите эту схему под рукой в качестве ориентира. На практике выполнение строки 195 равносильно выполнению строки 179. Однако, хотя вызов инициируется внутри того же кода, для приложения он выглядит так, будто поступает извне кода индикатора. В этот момент код обновляет некоторые значения, а затем возвращает управление. Параллельно выполняется строка 199, и на этом обработка событий из строк 179 и 190 завершается. Если на графике есть более одного индикатора позиции, все они будут перехватывать одно и то же пользовательское событие, рассылаемое MetaTrader 5, независимо от того, где именно оно было сгенерировано. Все они будут реагировать почти одинаково. Таким образом, линия, которая должна получить фокус, получит фокус мыши, а остальные его потеряют.
В следующий раз, когда индикатор позиции перехватит событие CHARTEVENT_MOUSE_MOVE, мы проверим в строке 203, есть ли у него фокус. В этом случае строки 205 и 207 покажут смещение линии — либо линии тейк-профита, либо линии стоп-лосса. Ни одна другая линия не будет затронута этим перемещением. Однако это смещение пока ещё не является реальным. То есть смещение на самом деле не происходит по мере перемещения мыши. Это перемещение является виртуальным и ожидает нового события, которое либо подтвердит его, либо отменит. Если его отменить, что и происходит при нажатии клавиши ESC, MetaTrader 5 сгенерирует событие, которое будет перехвачено в строке 174. В результате проверяются все данные, поскольку в строке 178 генерируется событие Update. Таким образом, индикатор позиции снова отображает прежние данные, то есть те, которые по-прежнему зарегистрированы на торговом сервере.
Если же, напротив, принимаются данные, соответствующие движению мыши, поскольку оператор щёлкает по нужной ценовой позиции, то в строке 211 генерируется пользовательское событие, адресованное советнику. Перехватив и обработав его, советник отправляет серверу запрос на обновление положения линии тейк-профита или стоп-лосса. Далее, в строке 212, генерируется ещё одно событие, адресованное всем индикаторам позиции, включая тот, который его сгенерировал, чтобы линии тейк-профита и стоп-лосса потеряли фокус. Тем временем советник ожидает ответа от торгового сервера. Когда поступает ответ, советник генерирует событие Update, адресованное индикатору позиции, чей тикет совпадает со значением, указанным в событии. Таким образом, мы снова возвращаемся к строке 85 процедуры OnChartEvent индикатора позиции.
В этом и заключается главное преимущество обмена сообщениями и событийно-ориентированного программирования. Нам не нужно строго контролировать порядок происходящего, главное, чтобы каждое событие правильно генерировалось, перехватывалось и обрабатывалось. Однако обратите внимание, что индикатор мыши также перехватывает событие. Индикатор позиции генерирует его в строке 183, а индикатор мыши обрабатывает это событие, чтобы скрыть свою линию цены. Мы также могли бы полностью изменить это поведение. Для этого достаточно было бы указать индикатору мыши, что именно он должен делать, и он выполнил бы соответствующее действие. Можно даже активировать какой-нибудь анализ или другое подобное действие, пока мы перемещаем указатель, чтобы задать линию стоп-лосса или тейк-профита.
Как я уже давно отмечаю, нет пределов тому, что мы можем сделать. Поэтому я и посвящаю эту статью тому, чтобы объяснить вам, уважаемый читатель, как использовать событийно-ориентированное программирование и обмен сообщениями. Если вам нужно изменить или проанализировать какой-либо элемент, вы сможете сделать это очень просто, быстро и напрямую на графике. Нет необходимости прибегать к излишне сложным методам и средствам.
Например, несколько человек обратились ко мне с просьбой, цель которой кажется мне вполне разумной. Хотя я и не объяснял, как это сделать, и не побуждал их мыслить именно так, это можно легко реализовать с помощью обмена сообщениями между приложениями.
Просьба заключается в следующем: они хотят, чтобы создаваемый ими советник давал команду сервису репликации/моделирования прекратить генерацию новых тиков или котировок. На первый взгляд это кажется чрезвычайно сложным для реализации, поскольку мы не можем отправить запрос или сообщение, а также напрямую сгенерировать событие для сервиса. Помните, что пользовательские события, сгенерированные, и сообщения, отправленные с помощью этого механизма, всегда привязаны к графику. То есть мы не можем передавать сообщения напрямую сервису, поскольку он не привязан ни к какому конкретному графику. Тем не менее, существует способ передавать или пересылать сообщения сервису. Чтобы разделить эти понятия и показать, как действовать дальше, перейдём к новой теме.
Приостановка сервиса репликации/моделирования непосредственно из советника
Чтобы достичь этой цели, нам понадобится небольшая помощь. Или, точнее говоря, нам понадобится кое-что. Чтобы сервис мог получать информацию из событий или сообщений, ему необходимо отслеживать какой-либо элемент на графике. И снова: сообщения и события направляются на график. НИКОГДА НЕ ЗАБЫВАЙТЕ ОБ ЭТОМ, когда программируете на MQL5. К счастью, у сервиса репликации/моделирования есть индикатор на графике: индикатор управления. Взаимодействуя с ним с помощью индикатора мыши, мы можем указать сервису перейти в режим паузы или в режим воспроизведения.
Однако изначально система не была разработана для моделирования или тестирования автоматизированных стратегий, то есть для использования советника, который помогал бы вам в рыночных операциях. По этой причине в нём до сих пор нет ни события, ни сообщения, которые позволяли бы по запросу советника, другого индикатора или MQL5-скрипта, привязанного к графику, переводить сервис в режим паузы или в режим воспроизведения. Чтобы этого добиться, нам придётся слегка изменить код.
Не переживайте: это очень простые изменения, которые вы без труда сможете внести. Вам нужно будет всего лишь добавить одну строку в советник, индикатор или скрипт, который вы разрабатываете. Это позволит переключать сервис репликации/моделирования между режимом воспроизведения и режимом паузы.
Давайте теперь посмотрим, что нужно изменить в исходном коде системы репликации/моделирования. Для этого найдите класс C_Controls и перейдите к процедуре DispatchMessage. Здесь необходимо добавить обработку пользовательских событий, которые позволят переключать сервис между режимом воспроизведения и режимом паузы. Ниже приведён исходный фрагмент, который мы будем изменять:
179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 190. info.dValue = dparam; 191. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 192. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 193. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 194. break; 195. case CHARTEVENT_OBJECT_DELETE: 196. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 197. { 198. if (sparam == def_ObjectCtrlName(ePlay)) 199. { 200. delete m_Section[ePlay].Btn; 201. m_Section[ePlay].Btn = NULL; 202. SetPlay(m_Section[ePlay].state); 203. }else 204. { 205. RemoveCtrlSlider(); 206. CreateCtrlSlider(); 207. } 208. } 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 212. { 213. case ePlay: 214. SetPlay(!m_Section[ePlay].state); 215. if (m_Section[ePlay].state) 216. { 217. RemoveCtrlSlider(); 218. m_Slider.Minimal = iPinPosX; 219. }else CreateCtrlSlider(); 220. break; 221. case eLeft: 222. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 223. break; 224. case eRight: 225. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 226. break; 227. case ePin: 228. if (six == -1) 229. { 230. six = x; 231. sps = (short)iPinPosX; 232. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 233. } 234. iPinPosX = sps + x - six; 235. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 236. break; 237. }else if (six > 0) 238. { 239. six = -1; 240. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 241. } 242. break; 243. } 244. ChartRedraw(GetInfoTerminal().ID); 245. } 246. //+------------------------------------------------------------------+
Исходный фрагмент из C_Controls
Обратите внимание, что в строке 213 есть фильтр, который определяет, где был выполнен щелчок. Если нажать кнопку воспроизведения, сервис переключится из режима паузы в режим воспроизведения. Если сервис уже находится в режиме воспроизведения, произойдёт обратное. Это изменение выполняется при выполнении строки 214. Таким образом, нам нужно лишь сгенерировать пользовательское событие в этой процедуре. Когда OnChartEvent индикатора управления перехватит его, DispatchMessage сможет обработать это событие и указать сервису репликации/моделирования, нужно ли переключиться в режим воспроизведения или в режим паузы. Вот и всё.
Для этого откройте файл Defines.mqh и добавьте два новых события, приведённых ниже:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. #define def_IndexTimeFrame 4 18. //+------------------------------------------------------------------+ 19. union uCast_Double 20. { 21. double dValue; 22. long _long; // 1 информация 23. datetime _datetime; // 1 информация 24. uint _32b[sizeof(double) / sizeof(uint)]; // 2 блока данных 25. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 блока данных 26. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 байтов данных 27. }; 28. //+------------------------------------------------------------------+ 29. enum EnumEvents { 30. evTicTac, //Событие тик-так 31. evHideMouse, //Скрыть линию цены мыши 32. evShowMouse, //Показать линию цены мыши 33. evHideBarTime, //Скрыть время бара 34. evShowBarTime, //Показать время бара 35. evHideDailyVar, //Скрыть дневное изменение 36. evShowDailyVar, //Показать дневное изменение 37. evHidePriceVar, //Скрыть мгновенное изменение 38. evShowPriceVar, //Показать мгновенное изменение 39. evCtrlReplayInit, //Инициализировать управление реплеем 40. evChartTradeBuy, //Событие рыночной покупки 41. evChartTradeSell, //Событие рыночной продажи 42. evChartTradeCloseAll, //Событие для закрытия позиций 43. evChartTrade_At_EA, //Событие для связи 44. evEA_At_ChartTrade, //Событие для связи 45. evChatWriteSocket, //Событие для мини-чата 46. evChatReadSocket, //Событие для мини-чата 47. evUpdate_Position, //Событие для связи 48. evMsgClosePositionEA, //Событе для связи 49. evMsgCloseTakeProfit, //Событие для связи 50. evMsgCloseStopLoss, //Событие для связи 51. evMsgNewTakeProfit, //Событие для связи 52. evMsgNewStopLoss, //Событие для связи 53. evMsgSetFocus, //Событие для связи 54. evMsgServiceSwapStatus //Событие для остановки сервиса репликации/моделирования 55. }; 56. //+------------------------------------------------------------------+ 57. enum EnumPriority { //Список приоритетов для объектов 58. ePriorityNull = -1, 59. ePriorityDefault = 0 60. }; 61. //+------------------------------------------------------------------+
Defines.mqh
Строка 54 содержит идентификатор нового пользовательского события. Когда будете использовать его в своём коде, не забудьте подключить файл Defines.mqh, чтобы компилятор знал правильное значение. Эта строка позволяет переключать состояние сервиса репликации/моделирования.
EventChartCustom(0, evMsgServiceSwapStatus, 0, 0, "");
Поэтому в какой-то момент в своём коде вам нужно будет добавить следующую строку: если сервис находится в режиме воспроизведения, он переключится в режим паузы и будет ждать разрешения на продолжение. Если сервис находится в режиме паузы, он получит разрешение перейти в режим воспроизведения. Как видите, это очень просто. Однако точное место, куда нужно вставить эту строку, и условие, которое запустит её выполнение, зависят от того, что именно вы реализуете, поэтому здесь невозможно показать практический пример. Тем не менее те, кому понадобится выполнить эту задачу, будут знать, где и как её использовать. Помни также, что вам не обязательно генерировать и событие режима паузы, и событие режима воспроизведения. Скорее всего, вы будете использовать в основном событие режима паузы, поскольку система может вернуться в режим воспроизведения напрямую за счёт взаимодействия между индикатором мыши и индикатором управления.
Теперь давайте посмотрим, что нужно добавить или изменить в предыдущем фрагменте, чтобы индикатор управления мог указывать сервису репликации/моделирования, что ему делать. Ниже приведён новый фрагмент процедуры DispatchMessage класса C_Controls:
179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evMsgServiceSwapStatus): 190. SetPlay(!m_Section[ePlay].state); 191. if (m_Section[ePlay].state) 192. { 193. RemoveCtrlSlider(); 194. m_Slider.Minimal = iPinPosX; 195. }else CreateCtrlSlider(); 196. break; 197. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 198. info.dValue = dparam; 199. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 200. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 201. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 202. break; 203. case CHARTEVENT_OBJECT_DELETE: 204. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 205. { 206. if (sparam == def_ObjectCtrlName(ePlay)) 207. { 208. delete m_Section[ePlay].Btn; 209. m_Section[ePlay].Btn = NULL; 210. SetPlay(m_Section[ePlay].state); 211. }else 212. { 213. RemoveCtrlSlider(); 214. CreateCtrlSlider(); 215. } 216. } 217. break; 218. case CHARTEVENT_MOUSE_MOVE: 219. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 220. { 221. case ePlay: 222. EventChartCustom(0, evMsgServiceSwapStatus, 0, 0, ""); 223. break; 224. case eLeft: 225. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 226. break; 227. case eRight: 228. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 229. break; 230. case ePin: 231. if (six == -1) 232. { 233. six = x; 234. sps = (short)iPinPosX; 235. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 236. } 237. iPinPosX = sps + x - six; 238. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 239. break; 240. }else if (six > 0) 241. { 242. six = -1; 243. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 244. } 245. break; 246. } 247. ChartRedraw(GetInfoTerminal().ID); 248. } 249. //+------------------------------------------------------------------+
Новый фрагмент C_Controls
Внимательно посмотрите, и вы увидите, что я применяю тот же механизм, который использовал на протяжении всей статьи: индикатор управления генерирует пользовательское событие, которое сам же перехватывает и обрабатывает. Событие генерируется в строке 222; OnChartEvent его перехватывает, а DispatchMessage обрабатывает в строке 189. Это гарантирует, что, когда ваш советник сгенерирует это же событие, оно будет обработано по той же схеме перехвата и обработки. Однако стоит учесть одну деталь. Если советник сгенерирует событие именно в тот момент, когда вы нажмёте кнопку паузы на индикаторе управления, в очереди накопятся два события. В результате сервис перейдет в режим паузы, а сразу после этого вернется в режим воспроизведения. Возможен и обратный вариант. При необходимости вы можете изменить это поведение.
Однако, поскольку я не показываю, как разработать эту систему так, чтобы она стала альтернативой тестеру стратегий MetaTrader 5, не думаю, что этот аспект представляет собой слишком большую проблему. Вам нужно лишь действовать спокойно и помнить, что вы сможете улучшить или изменить систему так, как сочтете наиболее интересным.
Заключительные замечания
В этой статье я постарался как можно проще объяснить, как использовать обмен сообщениями между приложениями. Цель в том, чтобы вы могли создать что-то работоспособное максимально простым и эффективным способом, когда это возможно. Не знаю, удалось ли мне донести идею, лежащую в основе этой концепции, ведь тому, кто сталкивается с ней впервые, ее не так легко понять. Кроме того, я показал, как изменить систему репликации/моделирования для отладки советника или любого другого кода, который вы разрабатываете. Я сделал это так же просто и прямо, применив ту же технику, которая описывалась на протяжении всей статьи, чтобы сделать материал ещё интереснее.
Поскольку эта серия статей о разработке системы репликации/моделирования носит исключительно учебный характер, я не мог упустить возможность объяснить механизм, который уже давно используется в коде. Надеюсь, это побудит тех, кто только что присоединился, ознакомиться с предыдущими статьями. Возможно, среди этих энтузиастов и начинающих программистов найдется кто-то, кто действительно сможет добиться выдающихся результатов. То, что я объясняю здесь, нельзя освоить за одну ночь. Потребуется посвятить много времени изучению и практической работе с этим механизмом. Тем не менее, уверяю вас, что изучение этой концепции и усилия, направленные на её понимание, определённо того стоят. Увидимся в следующей статье, где мы продолжим разрабатывать нашу систему репликации/моделирования. Желаю всем больших успехов в учёбе. До следующей встречи.
| Файл | Описание |
|---|---|
| Experts\Expert Advisor.mq5 | Показывает взаимодействие между Chart Trade и советником (для взаимодействия необходимо использовать Mouse Study). |
| Indicators\Chart Trade.mq5 | Создает окно для настройки ордера, который будет отправлен (для взаимодействия необходимо использовать Mouse Study) |
| Indicators\Market Replay.mq5 | Создаёт элементы управления для взаимодействия с сервисом репликации/моделирования рынка (для взаимодействия необходимо использовать Mouse Study). |
| Indicators\Mouse Study.mq5 | Обеспечивает взаимодействие между графическими элементами управления и пользователем (необходимо как для работы системы репликации/моделирования, так и на реальном рынке) |
| Indicators\Order Indicator.mq5 | Отображает рыночные ордеры и позволяет взаимодействовать с этими ордерамы и управлять ими. |
| Indicators\Position View.mq5 | Отображает рыночные позиции и позволяет взаимодействовать с этими позициями и управлять ими. |
| Services\Market Replay.mq5 | Создает и поддерживает сервис репликации/моделирования рынка (основной файл системы) |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/13375
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Классы (III)
Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5
Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования