Моделирование рынка: Position View (XX)
Введение
Всем привет и добро пожаловать в очередную статью из серии, посвящённой созданию системы репликации/моделирования рынка.
В предыдущей статье «Моделирование рынка: Position View (XIX)» мы добавили механизм, позволяющий проверить, правильно ли мы устанавливаем стоп-лосс или тейк-профит. Таким образом, оба уровня можно настроить так, чтобы текущая цена символа оставалась между ними. Вы, вероятно, заметили, что мы сделали это довольно простым и практичным способом. Я старался придерживаться этого подхода на протяжении всей серии: вносить простые изменения, максимально использовать уже реализованный код и излагать каждый шаг как можно более наглядно. Цель состоит в том, чтобы вы могли следить за ходом разработки и, прежде всего, изменять код, чтобы реализовать решение, которое лучше соответствует вашим потребностям.
В этой статье мы будем придерживаться того же подхода. Некоторое время назад мы начали разрабатывать механизм, который сейчас рассмотрим и реализуем более подробно. Возможно, эта функция не покажется вам особенно необходимой. Однако я уверяю вас, что это будет очень интересно благодаря тому, как мы реализуем эту функцию. Мы внесём небольшое изменение, чтобы показать трейдеру или пользователю, где ранее находился уровень стоп-лосса или тейк-профита, прежде чем вы начнёте перемещать его на новое место.
Чтобы вы лучше поняли, что мы собираемся реализовать, имейте в виду следующее: когда мы выбираем один из сегментов — тейк-профит или стоп-лосс — чтобы изменить его положение, значение не обновляется, пока мы перемещаем линию. Сначала нужно завершить перемещение — либо щелчком мыши, либо нажатием клавиши ESC. Только после этого индикатор позиции обращается к советнику, чтобы тот отправил запрос на торговый сервер.
Цель этого запроса — изменить исходное значение тейк-профита или стоп-лосса. Однако, пока мы перемещаем линию, сервер продолжает хранить предыдущее значение. В этой статье мы как раз реализуем способ отображения этого предыдущего значения. Мы создадим способ показывать, где по-прежнему находится ценовой уровень, зарегистрированный на сервере. Таким образом, если позиция закроется до того, как вы закончите перемещать линию на новый уровень, мы будем знать, почему это произошло, и поведение индикатора позиции не будет казаться странным.
Чтобы создать — или, точнее, изменить — индикатор позиции для реализации такого поведения, сначала нужно понять некоторые детали. Перейдём к первому разделу статьи, чтобы начать вносить необходимые изменения.
Как создать ценовую тень
Как я уже упоминал во введении, мы создадим своего рода тень, задача которой — показать, где находилась или где по-прежнему находится цена до завершения выполняемого нами перемещения. С точки зрения реализации создание этой тени довольно интересно, поскольку мы не будем делать это каким-то совсем уж несуразным способом. Мы просто укажем MetaTrader 5, как следует интерпретировать и обрабатывать объекты индикатора позиции.
На этом этапе разработки вы, возможно, думаете, что добавление новой функциональности в индикатор заставит нас написать большой объём кода или, как минимум, существенно изменить существующий. Однако в этом не будет необходимости. Поскольку код разработан так, чтобы быть максимально модульным, нам придётся изменить очень немногое. Возможно, потребуется добавить какую-то новую процедуру, но в первую очередь всегда следует стараться использовать то, что уже реализовано. Таким образом, мы сводим к минимуму объём тестирования, необходимого для того, чтобы гарантировать правильную работу, и максимально ускоряем разработку любого приложения.
Исходя из идеи изменять как можно меньше кода, давайте посмотрим, как создать эту тень. Прежде всего, нам нужно определить один важный аспект: цвета. Мы не хотим создавать путаницу при просмотре графика. Поэтому мы определим новые цвета и новые имена, как показано ниже. Это позволит нам отличать объекты, которые являются частью тени, от тех, которые к ней не относятся. Для этого нам нужно лишь добавить в код то, что показано в следующем фрагменте.
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. #define def_NameBackGround m_Info.szPrefixName + "#BACKGROUND" 010. #define def_NameVolume m_Info.szPrefixName + "#VOLUME" 011. //+------------------------------------------------------------------+ 012. #define def_GhostColor clrDarkGray 013. #define macro_GhostName(A) (m_Info.szPrefixName + "[GHOST]" + (A != "" ? StringSubstr(A, StringFind(A, "#")) : "")) 014. //+------------------------------------------------------------------+ 015. #define macro_LineInFocus(A) ObjectSetInteger(0, def_NameHLine, OBJPROP_YSIZE, m_Info.weight = (A ? 3 : 1)); 016. //+------------------------------------------------------------------+ . . . 332. //+------------------------------------------------------------------+ 333. #undef def_GhostColor 334. #undef macro_GhostName 335. //+------------------------------------------------------------------+ 336. #undef macro_LineInFocus 337. //+------------------------------------------------------------------+ 338. #undef def_Btn_Close 339. #undef def_PathBtns 340. #undef def_FontName 341. #undef def_FontSize 342. //+------------------------------------------------------------------+ 343. #undef def_NameVolume 344. #undef def_NameBackGround 345. #undef def_NameObjLabel 346. #undef def_NameInfoDirect 347. #undef def_NameBtnMove 348. #undef def_NameBtnClose 349. #undef def_NameHLine 350. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Посмотрите, как реализация начинает обретать форму. В строке 12 мы определяем цвет, который будем использовать. В строке 13 мы объявляем макрос, цель которого — позволить нам изменять имена объектов. Однако этот макрос сам по себе работать не будет. Это лишь небольшая часть более широкой реализации. Давайте двигаться постепенно. Сначала обратите внимание, что строки 333 и 334 уже содержат код, отвечающий за удаление определений, созданных в строках 12 и 13. Прежде чем продолжить, давайте посмотрим, что происходит в макросе на строке 13. Чтобы это понять, обратите внимание на имена объектов в строках с 4 по 10. Все они начинаются с предопределённого значения и имеют один общий элемент: символ #. Именно это нас и интересует.
В макросе мы по-прежнему будем использовать предопределённое значение, чтобы упростить удаление объектов, созданных индикатором позиции. Между префиксом и остальной частью исходного имени, заданной в строках с 4 по 10, мы добавим текст [GHOST], оставив остальную часть имени без изменений. Именно в этом и заключается назначение функций StringSubstr и StringFind: удалить из имени часть, соответствующую m_info.szPrefixName, и сохранить остальную часть исходного имени. Например, при использовании макроса для изменения имени фонового объекта имя вида 1685452684#BACKGROUND будет преобразовано в 1685452684[GHOST]#BACKGROUND. Всё это достигается исключительно с помощью макроса. Однако, как я уже отмечал, сам по себе макрос работать не будет. Нам нужен дополнительный код, который обеспечит работу этого механизма. Если мы изменим имена объектов, не обеспечив возможности дальнейшего доступа к ним, то рискуем полностью нарушить работу индикатора.
Чтобы вы поняли, как будет выполняться этот процесс, мы рассмотрим очень простой пример кода, показывающий, как работает переименование. Полный код приведён ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. //+------------------------------------------------------------------+ 07. #define prefixName "Obj #" 08. #define macro_Swap(A) (prefixName + "[GHOST]" + StringSubstr(A, StringFind(A, "#") + 1)) 09. #define def_ObjectName(A) (prefixName + (string)(A)) 10. //+------------------------------------------------------------------+ 11. void ChartOfTest(string sz1, int x, int y, int zOrder) 12. { 13. long id = ChartID(); 14. 15. ObjectCreate(id, sz1, OBJ_CHART, 0, 0, 0); 16. ObjectSetInteger(id, sz1, OBJPROP_XDISTANCE, x); 17. ObjectSetInteger(id, sz1, OBJPROP_YDISTANCE, y); 18. ObjectSetInteger(id, sz1, OBJPROP_ZORDER, zOrder); 19. ObjectSetInteger(id, sz1, OBJPROP_SELECTABLE, true); 20. } 21. //+------------------------------------------------------------------+ 22. int OnInit() 23. { 24. ChartOfTest(def_ObjectName(1), 160, 140, 1); 25. 26. return INIT_SUCCEEDED; 27. } 28. //+------------------------------------------------------------------+ 29. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 30. { 31. return rates_total; 32. } 33. //+------------------------------------------------------------------+ 34. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 35. { 36. switch (id) 37. { 38. case CHARTEVENT_OBJECT_CLICK: 39. if (StringSubstr(sparam, 0, StringFind(sparam, "#") + 1) == prefixName) 40. ObjectSetString(0, def_ObjectName(1), OBJPROP_NAME, macro_Swap(def_ObjectName(1))); 41. break; 42. } 43. } 44. //+------------------------------------------------------------------+ 45. void OnDeinit(const int reason) 46. { 47. ObjectsDeleteAll(ChartID(), prefixName); 48. } 49. //+------------------------------------------------------------------+
Демонстрационный код
Я знаю, что это может показаться вам немного запутанным, но на самом деле всё довольно просто. Мы лишь проверяем и пытаемся понять, что будем делать в индикаторе позиции. Идея заключается в том, чтобы создать объект, который в данном случае будет типа OBJ_CHART. Объект создаётся в строке 15, а его имя задаётся в строке 24. Чтобы задать это имя, мы используем определение из строки 9, которое включает префикс, заданный в строке 7. Использование этого префикса позволяет нам легко удалить объект OBJ_CHART, созданный кодом, как видно из строки 47. А теперь обратите внимание. Когда вы добавите это приложение на график и нажмёте сочетание клавиш CTRL + B, откроется окно, в котором вы сможете увидеть все объекты, присутствующие на графике. Вы можете увидеть это на следующем изображении.

Если объект OBJ_CHART не отображается в списке, достаточно нажать кнопку LIST ALL, как показано на изображении. Обратите внимание на имя, которое отображает MetaTrader 5. Затем закройте окно со списком объектов и щёлкните по объекту OBJ_CHART. В результате MetaTrader 5 сгенерирует событие, которое будет перехвачено строкой 38 тестового кода. В строке 39 мы проверяем, начинается ли имя объекта, по которому щёлкнули, с префикса, заданного в строке 7. Если это условие выполняется, в строке 40 мы изменим имя объекта OBJ_CHART. Но какое имя мы ему присвоим?
Новое имя генерируется с помощью макроса. Мы должны указать исходное имя объекта, и на его основе макрос в строке 8 создаст для него новое имя. Обратите внимание, что код этого макроса очень похож на тот, который мы добавляем в заголовочный файл C_ElementsTrade.mqh. После присвоения нового имени снова нажмите CTRL + B и убедитесь, что объект теперь отображается под другим именем, как показано на следующем изображении.

Если вы этого никогда не делали, попробуйте и посмотрите на результат. В некоторых случаях это может быть довольно интересным приёмом. Именно данный механизм мы и будем использовать в индикаторе позиции для создания тени. Теперь, когда у нас есть необходимые основы и знания, мы можем продолжить. Чтобы лучше разделить разные части, перейдём к новому разделу.
Создание ценовой тени
Теперь, когда вы знаете, что можно изменять имена объектов — если, конечно, вы ещё не знали об этой возможности, — пришло время применить это знание, чтобы создать нужную нам тень. Но сначала давайте посмотрим, как мы это сделаем. Когда мы создаём новые объекты, они оказываются в верхней части списка объектов. Как мы уже видели в предыдущих статьях, новые объекты накладываются на старые, которые становятся фоновыми объектами. Таким образом, самый простой способ создать тень заключается в том, чтобы оставить предыдущие объекты на графике, переименовать их, а затем создать новые. Вот так просто.
Однако текущий код не позволяет нам продолжать отображать объекты после их переименования. Прежде чем разбирать изменения, хочу уточнить, что на этом первом этапе нас не беспокоит наличие дублирующегося кода. Главное — добиться того, чтобы всё работало так, как мы ожидаем: мы должны иметь возможность создавать ценовую тень и при этом сохранять прежнее поведение. Поэтому сначала нам нужно решить, как отображать тень, чтобы можно было двигаться дальше в реализации. Для этого достаточно изменить код, как показано в следующем фрагменте.
029. //+------------------------------------------------------------------+ 030. class C_ElementsTrade : private C_Mouse 031. { 032. private : 033. //+------------------------------------------------------------------+ 034. struct stInfos 035. { 036. struct st_01 037. { 038. short Width, 039. Height, 040. digits; 041. }Text; 042. ulong ticket; 043. string szPrefixName, 044. szDescr, 045. szSymbol, 046. szMsgGhost; 047. EnumEvents ev; 048. double price, 049. open, 050. volume, 051. var, 052. tickSize, 053. tpsl, 054. limit, 055. priceGhost; 056. bool bClick, 057. bIsBuy; 058. char weight; 059. color _color; 060. int sizeText; 061. enum e1 {eValue, eFinance, eTicks, ePercentage} ViewMode; 062. }m_Info; 063. //+------------------------------------------------------------------+ 064. short UpdateViewPort(const double price, short size = 0, uint ui = 0) 065. { 066. static short _SizeControls; 067. static short _Width; 068. uint x, y; 069. 070. if (size > 0) 071. { 072. size += (short)(ui + 8); 073. _SizeControls = (_SizeControls > size ? _SizeControls : size); 074. size = (short)(_SizeControls - ui - 12); 075. _Width = (_Width > size ? _Width : size); 076. }else 077. { 078. ChartTimePriceToXY(0, 0, 0, price, x, y); 079. x = 125 + (m_Info.ev == evMsgClosePositionEA ? 0 : _Width + (m_Info.ev == evMsgCloseTakeProfit ? _SizeControls : (_SizeControls * 2))); 080. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 081. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 082. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 083. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 084. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10 + (m_Info.ev == evMsgClosePositionEA ? _Width + 2 : 0)); 085. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 086. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + (_Width * 2) + 20); 087. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 088. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + _Width + 20); 089. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 090. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XDISTANCE, x - 10); 091. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YDISTANCE, y - ((m_Info.Text.Height + 5) / 2)); 092. ObjectSetInteger(0, def_NameVolume, OBJPROP_XDISTANCE, x + 10); 093. ObjectSetInteger(0, def_NameVolume, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 094. if (m_Info.szMsgGhost != "") 095. { 096. ChartTimePriceToXY(0, 0, 0, m_Info.priceGhost, x, y); 097. x = 125 + (m_Info.ev == evMsgClosePositionEA ? 0 : _Width + (m_Info.ev == evMsgCloseTakeProfit ? _SizeControls : (_SizeControls * 2))); 098. ObjectSetInteger(0, macro_GhostName(def_NameHLine), OBJPROP_XDISTANCE, x); 099. ObjectSetInteger(0, macro_GhostName(def_NameHLine), OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 100. ObjectSetInteger(0, macro_GhostName(def_NameObjLabel), OBJPROP_XDISTANCE, x + 10 + (m_Info.ev == evMsgClosePositionEA ? _Width + 2 : 0)); 101. ObjectSetInteger(0, macro_GhostName(def_NameObjLabel), OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 102. ObjectSetInteger(0, macro_GhostName(def_NameBtnMove), OBJPROP_XDISTANCE, x + _Width + 20); 103. ObjectSetInteger(0, macro_GhostName(def_NameBtnMove), OBJPROP_YDISTANCE, y); 104. ObjectSetInteger(0, macro_GhostName(def_NameBackGround), OBJPROP_XDISTANCE, x - 10); 105. ObjectSetInteger(0, macro_GhostName(def_NameBackGround), OBJPROP_YDISTANCE, y - ((m_Info.Text.Height + 5) / 2)); 106. } 107. } 108. return _Width; 109. } 110. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Здесь вы можете увидеть некоторые изменения. Во-первых, в строках 46 и 55 мы объявляем две новые переменные. Обе инициализируются в конструкторе, как и остальные. Однако из-за того, как конструктор инициализирует структуру stInfos, обе получают свои предопределённые значения. Переменная szMsgGhost изначально пуста, а priceGhost инициализируется нулевым значением. Следовательно, нам не нужно выполнять никакой дополнительной инициализации. Обратите внимание на проверку в строке 94, которая не позволяет нам пытаться нарисовать то, чего ещё нет на графике. Поскольку изначально m_Info.szMsgGhost пуста, проверка не пройдёт. Возможно, вы задаётесь вопросом, почему мы не проверяем m_Info.priceGhost, хотя, казалось бы, это было бы гораздо проще. Действительно, такая проверка была бы проще, но чуть позже вы увидите, что это повлекло бы за собой некоторые неудобства. По этой причине мы используем m_Info.szMsgGhost.
Обратите внимание, что код в строках с 96 по 105 очень похож на тот, который задаёт положение объектов, не входящих в тень. Однако, хотя оба фрагмента очень похожи, этот содержит некоторые особенности, которые вы легко заметите.
Хорошо. Теперь мы можем отобразить на графике объекты-тени, которые будут выступать в качестве теней исходных объектов и указывать, где находилась цена. Следующее изменение показано в приведённом ниже фрагменте.
160. //+------------------------------------------------------------------+ 161. inline void AdjustDinamic(const string szObj, const string szTxt) 162. { 163. uint w, h; 164. 165. TextSetFont(def_FontName, def_FontSize * -10); 166. TextGetSize(szTxt, w, h); 167. m_Info.Text.Height = (uchar) h + 4; 168. m_Info.Text.Width = (uchar) w + 4; 169. m_Info.Text.Width = UpdateViewPort(0, m_Info.Text.Width, h = 32); 170. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 171. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 172. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? m_Info.Text.Width + 8 : 0)); 173. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YSIZE, m_Info.Text.Height + 5); 174. if (m_Info.szMsgGhost != "") 175. { 176. ObjectSetInteger(0, macro_GhostName(def_NameBackGround), OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? m_Info.Text.Width + 8 : 0)); 177. ObjectSetInteger(0, macro_GhostName(def_NameBackGround), OBJPROP_YSIZE, m_Info.Text.Height + 5); 178. ObjectSetString(0, macro_GhostName(def_NameObjLabel), OBJPROP_TEXT, m_Info.szMsgGhost); 179. } 180. } 181. //+------------------------------------------------------------------+ 182. inline void ChartChange(void) 183. { 184. UpdateViewPort(MathAbs(m_Info.price)); 185. m_Info.limit = (m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 186. if (m_Info.ev != evMsgClosePositionEA) 187. ViewValue(m_Info.limit); 188. } 189. //+------------------------------------------------------------------+ 190. void CreateGhost(void) 191. { 192. ObjectSetInteger(0, def_NameHLine, OBJPROP_BGCOLOR, def_GhostColor); 193. ObjectSetInteger(0, def_NameHLine, OBJPROP_COLOR, def_GhostColor); 194. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, def_GhostColor); 195. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BORDER_COLOR, def_GhostColor); 196. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_COLOR, def_GhostColor); 197. ObjectSetInteger(0, def_NameBackGround, OBJPROP_BGCOLOR, def_GhostColor); 198. ObjectSetInteger(0, def_NameBackGround, OBJPROP_COLOR, def_GhostColor); 199. ObjectSetString(0, def_NameHLine, OBJPROP_NAME, macro_GhostName(def_NameHLine)); 200. ObjectSetString(0, def_NameBtnMove, OBJPROP_NAME, macro_GhostName(def_NameBtnMove)); 201. ObjectSetString(0, def_NameObjLabel, OBJPROP_NAME, macro_GhostName(def_NameObjLabel)); 202. ObjectSetString(0, def_NameBackGround, OBJPROP_NAME, macro_GhostName(def_NameBackGround)); 203. m_Info.priceGhost = MathAbs(m_Info.price); 204. m_Info.szMsgGhost = ""; 205. } 206. //+------------------------------------------------------------------+ 207. public : 208. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Этот фрагмент немного длиннее. Хотя всё довольно просто, здесь есть несколько моментов, на которые стоит обратить внимание. Первое, что вы заметите: кода RemoveAllsObjects, реализованного в статье 130 этой серии (/es/articles/13457), здесь больше нет. Это показывает, что код постоянно изменяется и совершенствуется. Если вы последовательно применяли эти фрагменты по мере их публикации, то увидите, что они идеально стыкуются друг с другом и формируют исходный код, соответствующий именно этому этапу разработки.
В этом фрагменте я хочу обратить ваше внимание на несколько моментов. Во-первых, в строке 178 мы присваиваем объекту OBJ_EDIT, который будет выступать в роли тени, то же значение, которое было получено, когда условие в строке 174 выполняется. Однако обратите внимание, что это значение всегда будет оставаться постоянным. Важно, чтобы вы это имели в виду, потому что чуть позже я объясню одну деталь, которая пока что вносит определённое ограничение. Ничего серьёзного, но обратите внимание на строку 178.
Теперь нам нужно сосредоточиться на тени, которая создаётся при выполнении процедуры в строке 190. Эта строка кажется почти волшебной, потому что она берёт все исходные объекты и превращает их в тени или призраки. Обратите внимание, что этот процесс аналогичен тому, который мы рассматривали в предыдущем разделе. Там мы меняли только имя, а здесь, помимо имени, также изменяем некоторые свойства объектов. В любом случае в итоге мы получаем то, что нам нужно: объекты, которые будут формировать тень. Обратите внимание на одну деталь: затрагиваются почти все объекты, но объект, который отображается на графике как кнопка закрытия позиции, мы не изменяем. Он будет обработан в другой части кода.
Возможно, у вас возникнет вопрос: в процедуре из строки 190, где мы создаём тень, не следует ли присвоить m_Info.szMsgGhost какое-нибудь значение? В конце концов, эта переменная получает пустую строку, тогда как m_Info.priceGhost получает абсолютное значение цены. Почему так происходит? Если мы создаём тени, было бы логично присвоить m_Info.szMsgGhost какое-нибудь значение, чтобы они отображались на графике.
Если вы подумали именно так, то я должен вас поздравить, потому что это означает, что вы начинаете понимать, как должен работать код и какой была бы наиболее очевидная первоначальная реализация. Однако всё не всегда оказывается столь очевидным. Иногда нам приходится принудительно задавать определённое поведение, чтобы получить желаемый результат. Чтобы понять, почему переменные получают эти значения в строках 203 и 204, нужно проанализировать, что происходит ещё в двух местах кода. Чтобы упростить объяснение, я разделю их на два фрагмента. Первый показан ниже.
208. //+------------------------------------------------------------------+ 209. C_ElementsTrade(const ulong ticket, string szSymbol, const EnumEvents ev, color _color, char digits, double ticksize, string szDescr = "\n", const bool IsBuy = true) 210. :C_Mouse(0, "") 211. { 212. ZeroMemory(m_Info); 213. m_Info.szPrefixName = StringFormat("%I64u@%03d", m_Info.ticket = ticket, (int)(m_Info.ev = ev)); 214. m_Info._color = _color; 215. m_Info.szDescr = szDescr; 216. m_Info.bIsBuy = IsBuy; 217. m_Info.Text.digits = digits; 218. m_Info.tickSize = ticksize; 219. m_Info.szSymbol = szSymbol; 220. } 221. //+------------------------------------------------------------------+ 222. ~C_ElementsTrade() 223. { 224. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 225. ObjectsDeleteAll(0, m_Info.szPrefixName); 226. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 227. } 228. //+------------------------------------------------------------------+ 229. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0, const double special = -1) 230. { 231. m_Info.sizeText = 0; 232. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 233. ObjectsDeleteAll(0, m_Info.szPrefixName + "#"); 234. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 235. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 236. m_Info.var = (var > 0 ? var : m_Info.var); 237. m_Info.tpsl = (special >= 0 ? special : m_Info.tpsl); 238. if (price > 0) 239. { 240. CreateLinePrice(); 241. CreateButtonClose(); 242. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 243. } 244. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 245. m_Info.open = open; 246. m_Info.price = (price > 0 ? price : -open); 247. if (m_Info.ev == evMsgClosePositionEA) 248. { 249. CreateObjectInfoText(def_NameVolume, clrBlack); 250. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 251. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 252. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 253. }; 254. ChartChange(); 255. } 256. //+------------------------------------------------------------------+ 257. void ViewValue(const double profit, const bool Enabled = true) 258. { 259. string szTxt; 260. color _cor; 261. static double memSL, memTP; 262. 263. if (Enabled) 264. { 265. switch (m_Info.ViewMode) 266. { 267. case stInfos::eValue: 268. szTxt = StringFormat("%." + (string)m_Info.Text.digits + "f", MathAbs(profit)); 269. break; 270. case stInfos::eFinance: 271. szTxt = StringFormat("$ %." + (string)m_Info.Text.digits + "f", (MathAbs(profit) / m_Info.var) * m_Info.volume); 272. break; 273. case stInfos::eTicks: 274. szTxt = StringFormat("%d", (uint)MathRound(MathAbs(profit) / m_Info.tickSize)); 275. break; 276. case stInfos::ePercentage: 277. szTxt = StringFormat("%.2f%%", NormalizeDouble((MathAbs(profit) / (m_Info.open ? m_Info.open : m_Info.price)) * 100, 2)); 278. break; 279. } 280. m_Info.szMsgGhost = ((m_Info.szMsgGhost == "") && (m_Info.ev != evMsgClosePositionEA) ? szTxt : m_Info.szMsgGhost); 281. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, szTxt); 282. if (StringLen(szTxt) != m_Info.sizeText) 283. { 284. AdjustDinamic(def_NameObjLabel, szTxt); 285. m_Info.sizeText = StringLen(szTxt); 286. } 287. } 288. _cor = (m_Info.limit >= 0 ? clrPaleGreen : clrCoral); 289. switch (m_Info.ev) 290. { 291. case evMsgCloseTakeProfit: 292. memTP = (Enabled ? memTP : profit); 293. _cor = (m_Info.limit < memTP ? clrYellow : _cor); 294. break; 295. case evMsgCloseStopLoss: 296. memSL = (Enabled ? memSL : profit); 297. _cor = (m_Info.limit > memSL ? clrYellow : _cor); 298. break; 299. case evMsgClosePositionEA: 300. _cor = (profit >= 0 ? clrPaleGreen : clrCoral); 301. break; 302. } 303. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, _cor); 304. } 305. //+------------------------------------------------------------------+
Фрагмент из C_ElementsTrade
Обратите внимание, что теперь деструктор, расположенный в строке 222, будет удалять все объекты, как и положено. Однако UpdatePrice, расположенная в строке 229, больше этого делать не будет. В предыдущей версии и UpdatePrice, и деструктор удаляли все объекты. Стоит пояснить один аспект, который до сих пор оставался неявным, поскольку предполагалось, что все это и так легко поймут. То, что UpdatePrice удаляет объекты перед их повторным созданием, имеет смысл только в том случае, если учитывать возможность того, что линии стоп-лосса или тейк-профита были удалены. Вероятно, вы думаете, что при вызове UpdatePrice мы просто обновляем уровень, на котором находятся стоп-лосс или тейк-профит. Однако на деле всё работает немного иначе.
Идея в том, чтобы показать, где на самом деле находились бы эти точки. Однако, если какая-либо из них отсутствует в позиции, необходимо удалить с графика соответствующие объекты. На первый взгляд создание единой процедуры для выполнения этой задачи может показаться хорошей идеей. Однако, поскольку нам в любом случае пришлось бы проверять, есть ли у позиции стоп-лосс или тейк-профит, практичнее удалить объекты, а затем заново создать и разместить их должным образом. По этой причине UpdatePrice удаляет объекты графика.
Однако, поскольку сейчас мы превращаем эти же объекты в тени других объектов, уже находящихся на графике, нам нужно частично ограничить их удаление. Помните, я упоминал, что мы не будем создавать тень для кнопки закрытия позиции и что в тот конкретный момент её не нужно удалять, поскольку она будет удалена в другом месте? Так вот, когда при создании тени будет выполнена строка 233, эта кнопка будет удалена с графика.
А теперь обратите внимание. Если существует линия стоп-лосса или тейк-профита, значение, полученное в строке 246, соответствует цене, на которой находится этот уровень. Если же одно или оба этих значения отсутствуют, позиция может оставаться открытой до тех пор, пока её не закроют вручную или пока её не закроет сервер, если речь идёт о дейтрейдинге. В этом случае m_Info.price содержит цену открытия с отрицательным знаком. Поскольку нам нужно знать уровень, на котором должен быть нарисован объект перемещения, мы не можем напрямую использовать это отрицательное значение. При создании теней в строке 190 мы должны преобразовать его с помощью функции MathAbs.
Теперь посмотрим, когда m_Info.szMsgGhost получает значение. Обратите внимание на строку 280. Именно там мы присваиваем ему значение. Однако у вас может возникнуть следующий вопрос: функция ViewValue вызывается не один раз; не приведёт ли это к выполнению строки 94? Нет. Дело в том, что мы используем двойную проверку, чтобы строка 280 не присваивала значение m_Info.szMsgGhost, когда этого происходить не должно. Таким образом, копия значения, показываемого пользователю, будет отображаться в момент создания тени. Однако у этого механизма есть проблема, которую мы решим позже. Пока что вам не о чем беспокоиться. Чтобы всё работало правильно, нам ещё нужно проанализировать второй фрагмент, о котором я упоминал ранее и который приведён ниже.
305. //+------------------------------------------------------------------+ 306. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 307. { 308. string sz0; 309. long _lparam = lparam; 310. double _dparam = dparam; 311. 312. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 313. switch (id) 314. { 315. case (CHARTEVENT_KEYDOWN): 316. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 317. _lparam = (long) m_Info.ticket; 318. _dparam = 0; 319. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 320. case CHARTEVENT_CUSTOM + evMsgSetFocus: 321. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 322. { 323. CreateGhost(); 324. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 325. }else 326. { 327. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 328. ObjectsDeleteAll(0, macro_GhostName("")); 329. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 330. } 331. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 332. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 333. m_Info.bClick = false; 334. case CHARTEVENT_CHART_CHANGE: 335. ChartChange(); 336. break;
Фрагмент C_ElementsTrade
Здесь я показываю только ту часть, которую нам действительно нужно проанализировать. Остальная часть кода остаётся без изменений. Однако именно в этом фрагменте и начинается весь процесс. Обратите внимание на строку 321, где мы проверяем, работаем ли с нужным сегментом. Раньше мы просто отправляли запрос в UpdatePrice. Теперь мы действуем несколько иначе, поскольку именно в этом месте реализуется тень. Если условие в строке 321 выполняется, сначала мы вызываем процедуру, отвечающую за преобразование текущих объектов в объекты-тени. Далее мы отправляем запрос в UpdatePrice.
Таким образом, тень остаётся на заднем плане, а новые объекты, с которыми мы будем работать, появляются на переднем плане. Так создаётся впечатление, что мы оставляем тень на предыдущем уровне, где находилась цена. При использовании системы вы заметите, что предыдущая информация сохраняется. Именно в этом и заключается смысл затенения предыдущего положения цены.
Если проверка даёт отрицательный результат, мы просто удаляем объекты-тени с графика и возвращаем цену на её прежнее место, где находилась тень. Удаление выполняется в строке 328, тогда как возврат цены осуществляется самой системой, как и раньше. Таким образом, внеся всего несколько изменений в код, мы получаем результат, показанный в следующей анимации.

Вероятно, вам интересно узнать, что на самом деле означает эта тень. Ответ очень прост: он показывает, где на сервере находится тейк-профит или стоп-лосс. Эта ситуация, хорошо видимая на графике и в предыдущей анимации, позволяет избежать ложного впечатления, будто при смещении ценовой линии мы одновременно смещаем и этот уровень на сервере. Менее опытные пользователи или те, кто видит систему впервые, могут прийти к такому выводу. Благодаря такому представлению гораздо проще и нагляднее понять, что происходит.
Однако, как я уже отмечал, в системе есть небольшой недостаток, который нам нужно исправить. Когда вы выбираете сегмент — будь то тейк-профит или стоп-лосс — чтобы изменить ценовой уровень, и нажимаете на объект OBJ_EDIT, чтобы изменить режим отображения данных, информация обновляется во всех объектах. Однако тень не получает этого обновления, что искажает интерпретацию данных. Это небольшое неудобство может привести к тому, что некоторым трейдерам или пользователям будет некомфортно пользоваться системой. Хотя решение относительно простое, оно требует изменения текущей структуры кода. Кроме того, поскольку я также хочу удалить дублирующийся код, в этой статье мы не будем рассматривать это решение.
Заключительные замечания
В этой статье я показал вам то, что многие сочли бы довольно сложным для реализации: создание тени, указывающей трейдеру или пользователю, где находится предыдущая цена. На самом деле называть его «предыдущим» не совсем правильно, поскольку этот уровень по-прежнему присутствует в открытой позиции и всё ещё может быть активирован, если на этом конкретном уровне произойдёт сделка. Хотя система ещё требует некоторых доработок, как я объяснял на протяжении всей статьи, она уже достаточно функциональна, чтобы ею пользоваться.
Тем не менее, эти статьи о системе репликации/моделирования рынка носят исключительно учебный характер. Поэтому, если вы хотите использовать какое-либо из этих приложений на реальном счёте, вам следует принять необходимые меры предосторожности, так как они не были специально оптимизированы для этой цели. Если же вы действительно хотите понять, как работает вся система, можете как следует протестировать её на демосчетах.
Для тех, кто хочет поэкспериментировать с системой, но не знает, как создать исполняемые файлы, я приложил уже скомпилированные приложения. Вам нужно лишь поместить их рядом с теми, которые вы уже используете, — или заменить предыдущие версии, — и использовать их на демо-счёте, чтобы увидеть работу, показанную в анимации в предыдущем разделе. В следующей статье мы исправим ошибку с тенью, которая в настоящее время не отражает изменение режима отображения, выполненное в индикаторе. Кроме того, мы удалим дублирующийся код из класса C_ElementsTrade. Это будет интересный шаг, и, кроме того, нас отделяет всего один шаг от начала использования этих четырёх приложений в нашей системе репликации/моделирования рынка. Нам нужно лишь исправить некоторые детали, чтобы возобновить работу над сервисом репликации/моделирования рынка. Не пропустите следующую статью из этой серии.
| Файл | Описание |
|---|---|
| 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/13523
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода
От начального до среднего уровня: Перегрузка операторов (II)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования