Моделирование рынка: Position View (XIII)
Введение
Привет всем и добро пожаловать в новую статью из серии о том, как построить систему репликации/моделирования.
В предыдущей статье, Моделирование рынка: Position View (XII), мы улучшили индикатор позиции, чтобы сделать его более удобным и приятным в использовании. Тем не менее, это пока ещё не полностью практичный инструмент. Он уже включает функции, которые многие могли бы счесть очень сложными для реализации, хотя мы добавили их довольно простым способом. Теперь мы внесем некоторые незначительные изменения, чтобы повысить его полезность.
Не беспокойтесь: изменения будет легко понять, и они заложат основу для создания более проработанного и функционального решения. Давайте начнём.
Удаление класса C_IndicatorPosition
Прежде чем удалять класс C_IndicatorPosition из окончательного кода, стоит объяснить причину. Мы хотим удалить промежуточный слой между индикатором и классом C_ElementsTrade. Возможно, вам интересно, почему мы меняем архитектуру именно сейчас. Это довольно просто. До этого момента работа индикатора позиции не была полностью определена. Мы временно сохранили класс C_IndicatorPosition в качестве слоя разделения, чтобы изолировать реализацию от возможных сбоев или радикальных изменений, как показано на следующем изображении.

На предыдущей схеме чёрная линия представляет связь между индикатором и классом C_IndicatorPosition. Из этого класса выходят три линии: красная, синяя и зелёная, которые соответствуют компонентам, которые должны быть отображены на графике: стоп-лосс, цена открытия и тейк-профит соответственно. Хотя эта структура была полезной до настоящего момента, на следующем этапе она будет нам мешать. Поэтому мы удалим класс C_IndicatorPosition из проекта и встроим в индикатор методы и переменные-члены, которые сейчас определяет этот класс, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property icon "/Images/Market Replay/Icons/Positions.ico" 04. #property description "Indicator for tracking an open position on the server." 05. #property description "This should preferably be used together with an Expert Advisor." 06. #property description "For more details see the same article." 07. #property version "1.125" 08. #property link "https://www.mql5.com/pt/articles/13356" 09. #property indicator_chart_window 10. #property indicator_plots 0 11. //+------------------------------------------------------------------+ 12. #define def_ShortName "Position View" 13. //+------------------------------------------------------------------+ 14. #include <Market Replay\Order System\C_ElementsTrade.mqh> 15. #include <Market Replay\Defines.mqh> 16. //+------------------------------------------------------------------+ 17. input ulong user00 = 0; //For Expert Advisor use 18. //+------------------------------------------------------------------+ 19. struct st00 20. { 21. ulong ticket; 22. string szShortName; 23. }m_Infos; 24. //+------------------------------------------------------------------+ 25. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 26. //+------------------------------------------------------------------+ 27. bool CheckCatch(ulong ticket) 28. { 29. ZeroMemory(m_Infos); 30. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 31. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 32. if (ObjectFind(0, m_Infos.szShortName) >= 0) 33. { 34. m_Infos.ticket = 0; 35. return false; 36. } 37. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 38. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 39. 40. return true; 41. } 42. //+------------------------------------------------------------------+ 43. int OnInit() 44. { 45. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 46. if (!CheckCatch(user00)) 47. { 48. ChartIndicatorDelete(0, 0, def_ShortName); 49. return INIT_FAILED; 50. } 51. 52. return INIT_SUCCEEDED; 53. } 54. //+------------------------------------------------------------------+ 55. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 56. { 57. return rates_total; 58. } 59. //+------------------------------------------------------------------+ 60. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 61. { 62. double value; 63. 64. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 65. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 66. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 67. switch (id) 68. { 69. case CHARTEVENT_CUSTOM + evUpdate_Position: 70. if (lparam != m_Infos.ticket) return; 71. if (!PositionSelectByTicket(m_Infos.ticket)) 72. { 73. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 74. return; 75. }; 76. if (Open == NULL) Open = new C_ElementsTrade(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, StringFormat("%I64u : Position opening price.", m_Infos.ticket), PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); 77. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, StringFormat("%I64u : Take Profit price.", m_Infos.ticket)); 78. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket)); 79. (*Open).UpdatePrice(0, value = PositionGetDouble(POSITION_PRICE_OPEN)); 80. (*Take).UpdatePrice(value, PositionGetDouble(POSITION_TP)); 81. (*Stop).UpdatePrice(value, PositionGetDouble(POSITION_SL)); 82. break; 83. } 84. ChartRedraw(); 85. }; 86. //+------------------------------------------------------------------+ 87. void OnDeinit(const int reason) 88. { 89. delete Open; 90. delete Take; 91. delete Stop; 92. } 93. //+------------------------------------------------------------------+
Исходный код индикатора позиции
Приведённый выше код уже включает методы, переменные и логику обновления, которые раньше определялись в C_IndicatorPosition. На следующей диаграмме представлена новая структура.

На первый взгляд, архитектура кажется более простой, тогда как код индикатора может показаться более сложным. Ни одно из этих впечатлений не является точным: распределение ответственности между индикатором и вспомогательными классами не упростилось и не усложнилось, а было лишь реорганизовано.
Это изменение необходимо, потому что новая метка прибыли или убытка должна иметь прямой доступ к компоненту, представляющему цену открытия. Теперь мы будем отображать результат позиции — либо в денежном выражении, либо в виде количества тиков прибыли или убытка. Таким образом, трейдеру будет проще решить, целесообразно ли его закрывать.
"Ух ты, теперь становится интересно. Можно ли сохранить класс C_IndicatorPosition?" Да, но добавлять функцию только для доступа к указателю Open нецелесообразно. Этот указатель ссылается на графическую линию и метку, которые отмечают цену открытия позиции. Разъяснив причину изменений, перейдём к следующему разделу.
Разбираемся с индикацией прибыли или убытка
Пока мы опустим другие метрики позиции, такие как объём торгов и денежный результат, и будем отображать только то, приносит ли позиция прибыль или убыток. Сначала мы рассчитаем разницу между ценой открытия и ценой закрытия, выраженную в количестве пунктов; позже мы преобразуем эту разницу в денежный результат, поскольку финансовый расчёт требует дополнительных модификаций.
Теперь давайте подумаем, как получить текущую цену инструмента. Недостаточно использовать функцию OnCalculate, потому что трейдер может работать с системой перекрёстных ордеров, и в этом случае эта функция, возможно, не будет идеальным вариантом. Тем не менее, мы будем исходить из того, что трейдер работает с аналогичным символом. Например, можно анализировать полную историю долларового контракта, торгуя при этом текущий мини-долларовый контракт. В этом случае разница между двумя символами не представляет проблемы.
Для пояснения: на B3 (Бразильская биржа) существуют контракты, которые не являются торгуемыми, например, история фьючерсных контрактов. Профессиональные трейдеры часто используют данную историю в качестве ориентира при работе с текущим контрактом. Это также может применяться при мультисимвольной торговле: анализируется один инструмент, а ордеры исполняются по другому с использованием системы перекрёстных ордеров. Для тех, кто торгует валютными парами на FOREX, такой способ разделения анализируемого символа и торгуемого символа может быть менее привычным.
Мы также можем создать график отношения между инструментами. На FOREX торговля на основе этого графика требует отправки ордеров по торгуемой паре через систему перекрёстных ордеров. Поэтому это объяснение может быть вам полезно, если вы точно понимаете, как работает такая торговля. Мы оставим эту мультисимвольную торговлю и использование графиков соотношений для другого раза и вернёмся к нашей теме.
Поэтому мы воспользуемся функцией OnCalculate, чтобы показать, получим ли мы прибыль или понесём убытки. Однако недостаточно просто взять текущую цену, хотя на первый взгляд это может показаться самым простым решением. Обратите внимание на разницу между ценой входа и котировкой, доступной для закрытия позиции. Когда мы покупаем, мы исполняем ордер против доступной ликвидности по цене ASK; когда мы продаём, мы исполняем его против доступной ликвидности по цене BID. Чтобы закрыть длинную позицию, мы должны продать против цены BID. Аналогично, чтобы закрыть позицию на продажу, мы должны купить по цене, равной цене ASK.
В предыдущих статьях о системе репликации/моделирования мы рассмотрели два способа построения графиков: построение по LAST и построение по BID. Если вам незнакома данная концепция, пожалуйста, ознакомьтесь с этими статьями. Независимо от типа графика, параметр цены функции OnCalculate всегда содержит самую актуальную цену. Хотя данное поведение можно изменить, мы откажемся от этого варианта, поскольку нас интересует последняя цена, а не среднее значение. На графике LAST цена соответствует цене последней совершённой сделки, которая может быть BID или ASK. При построении по BID отображается именно цена BID, то есть лучшая цена покупки, котируемая на рынке.
В этом и заключается первая проблема. Построение по LAST обычно используется на счетах типа NETTING, тогда как построение по BID является обычным для FOREX. Возможно, вы думаете, что достаточно было бы прочитать POSITION_PROFIT и отобразить прибыль или убыток, рассчитанные через это свойство, с помощью графической текстовой метки. В принципе, это кажется разумным, но такой подход может показывать прибыль, когда закрытие позиции привело бы к убытку. Причина в спреде.
Возможно, вам не совсем понятно, как рассчитывается POSITION_PROFIT. Проблема в том, что это свойство не различает, является ли позиция покупкой или продажей, и, кроме того, не учитывает СПРЕД в расчёте. Такой спред может существенно подорвать капитал. Вы можете видеть прибыль в POSITION_PROFIT, закрыть позицию и обнаружить затем, что сделка принесла убыток.
Поэтому мы откажемся от POSITION_PROFIT и будем рассчитывать результат закрытия напрямую. Если позиция длинная, мы проверим результат её закрытия через продажу, исполненную по цене BID; если позиция короткая, мы рассчитаем результат покупки, исполненной по цене ASK. При использовании данного индикатора в системе кросс-ордеров старайтесь выбирать символ с хорошей ликвидностью. Таким образом, колебания цен будут лучше отражать фактическое поведение рынка.
Добавляем индикацию прибыли или убытка
Мы уже знаем цель и её обоснование. Теперь мы можем запрограммировать это с помощью простой демонстрации. Для этого мы немного изменим индикатор. В следующем блоке представлен полный обновленный код:
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.125" 008. #property link "https://www.mql5.com/pt/articles/13356" 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; //For Expert Advisor use 018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen; 025. bool bIsBuy; 026. }m_Infos; 027. //+------------------------------------------------------------------+ 028. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 029. //+------------------------------------------------------------------+ 030. bool CheckCatch(ulong ticket) 031. { 032. ZeroMemory(m_Infos); 033. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 034. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 035. if (ObjectFind(0, m_Infos.szShortName) >= 0) 036. { 037. m_Infos.ticket = 0; 038. return false; 039. } 040. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 041. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 042. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 043. 044. return true; 045. } 046. //+------------------------------------------------------------------+ 047. int OnInit() 048. { 049. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 050. if (!CheckCatch(user00)) 051. { 052. ChartIndicatorDelete(0, 0, def_ShortName); 053. return INIT_FAILED; 054. } 055. 056. return INIT_SUCCEEDED; 057. } 058. //+------------------------------------------------------------------+ 059. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 060. { 061. double ask, bid; 062. 063. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 064. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 065. 066. Comment(StringFormat("BID : %f ||| ASK : %f ||| PROFIT : %f", bid, ask, (m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask))); 067. 068. return rates_total; 069. } 070. //+------------------------------------------------------------------+ 071. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 072. { 073. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 074. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 075. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 076. switch (id) 077. { 078. case CHARTEVENT_CUSTOM + evUpdate_Position: 079. if (lparam != m_Infos.ticket) return; 080. if (!PositionSelectByTicket(m_Infos.ticket)) 081. { 082. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 083. return; 084. }; 085. if (Open == NULL) Open = new C_ElementsTrade(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, StringFormat("%I64u : Position opening price.", m_Infos.ticket), m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)); 086. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, StringFormat("%I64u : Take Profit price.", m_Infos.ticket)); 087. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket)); 088. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 089. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 090. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 091. break; 092. } 093. ChartRedraw(); 094. }; 095. //+------------------------------------------------------------------+ 096. void OnDeinit(const int reason) 097. { 098. delete Open; 099. delete Take; 100. delete Stop; 101. } 102. //+------------------------------------------------------------------+
Исходный код индикатора позиции
Давайте рассмотрим изменения. Структура в строке 19 включает новые поля для хранения имени символа, направления позиции и цены открытия. Сначала мы инициализируем имя символа, как указано в строке 40. Мы получаем его из позиции, а не из константы _Symbol, поскольку индикатор может работать с символом, отличным от того, который отображается на графике. Получив имя, мы перейдем от функции OnCalculate к функции OnChartEvent, расположенной в строке 71.
В этой функции мы инициализируем другие поля структуры: m_Infos.bIsBuy в строке 85 и m_Infos.priceOpen в строке 88. Это инициализирует все поля, необходимые для расчета. Перед созданием графической индикации стоит проверить работу расчёта. Протестируйте это на данном этапе разработки, а затем перейдите к строке 59, где находится функция OnCalculate.
Отлично, логика функции OnCalculate проста. В строках 63 и 64 мы запросили котировки ASK и BID у MetaTrader 5. Строка 66 отображает в левом верхнем углу графика котировки, участвующие в расчёте. Там отображаются лучшая цена покупки (BID), лучшая цена продажи (ASK) и результат, который мы получили бы при закрытии сделки в этот момент. Расчёт зависит от типа позиции: отрицательный результат указывает на убыток при закрытии; положительный — на прибыль. Всё очень просто и понятно, без сложностей.
После проверки работы расчёта мы можем вывести результат PROFIT в графической метке, привязанной к линии цены открытия. Стоит уточнить, что реализация поддерживает позиции со средней ценой входа и изменения торгуемого объёма. При изменении цены открытия индикатор автоматически корректируется и отображает цену, зафиксированную торговым сервером. Пока мы не рассчитываем финансовую сумму, а только смещение цены, выраженное в количестве тиков, будь то в пользу позиции или против неё.
Хорошо, чтобы внести ясность, перейдём к следующему разделу.
Создаём индикацию на линии цены
Реализация этой индикации проста: достаточно создать текстовую графическую метку, настроить её свойства и разместить рядом с линией цены открытия. Всё просто. Пока оставим код индикатора и поработаем с классом 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. class C_ElementsTrade : private C_Mouse 019. { 020. private : 021. //+------------------------------------------------------------------+ 022. struct st00 023. { 024. ulong ticket; 025. string szPrefixName, 026. szDescr; 027. EnumEvents ev; 028. double price; 029. bool bClick, 030. bIsBuy; 031. char weight, 032. digits; 033. color _color; 034. }m_Info; 035. //+------------------------------------------------------------------+ 036. void UpdateViewPort(const double price) 037. { 038. int x, y; 039. 040. ChartTimePriceToXY(0, 0, 0, price, x, y); 041. x = (m_Info.ev == evMsgClosePositionEA ? 150 : (m_Info.ev == evMsgCloseTakeProfit ? 220 : 290)); 042. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 043. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 044. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 045. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 046. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10); 047. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - 8); 048. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + 80); 049. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 050. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + 30); 051. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 052. } 053. //+------------------------------------------------------------------+ 054. inline void CreateLinePrice(void) 055. { 056. string szObj; 057. 058. CreateObjectGraphics(szObj = def_NameHLine, OBJ_RECTANGLE_LABEL, m_Info._color, (EnumPriority)(ePriorityDefault)); 059. ObjectSetInteger(0, szObj, OBJPROP_BGCOLOR, m_Info._color); 060. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH)); 061. ObjectSetInteger(0, szObj, OBJPROP_BORDER_TYPE, BORDER_FLAT); 062. ObjectSetInteger(0, szObj, OBJPROP_CORNER, CORNER_LEFT_UPPER); 063. ObjectSetString(0, szObj, OBJPROP_TOOLTIP, m_Info.szDescr); 064. macro_LineInFocus(false); 065. } 066. //+------------------------------------------------------------------+ 067. inline void CreateBoxInfo(const bool bMove) 068. { 069. string szObj; 070. const char c[] = {(char)(bMove ? 'u' : (m_Info.bIsBuy ? 236 : 238)), 0}; 071. 072. CreateObjectGraphics(szObj = (bMove ? def_NameBtnMove : def_NameInfoDirect), OBJ_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 073. ObjectSetString(0, szObj, OBJPROP_FONT, "Wingdings"); 074. ObjectSetString(0, szObj, OBJPROP_TEXT, CharArrayToString(c)); 075. ObjectSetInteger(0, szObj, OBJPROP_COLOR, (bMove ? m_Info._color : (m_Info.bIsBuy ? clrForestGreen : clrFireBrick))); 076. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, (bMove ? 17 : 15)); 077. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateObjectInfoText(void) 081. { 082. string szObj; 083. 084. CreateObjectGraphics(szObj = def_NameObjLabel, OBJ_EDIT, clrNONE, (EnumPriority)(ePriorityDefault)); 085. ObjectSetString(0, szObj, OBJPROP_FONT, "Lucida Console"); 086. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, 10); 087. ObjectSetInteger(0, szObj, OBJPROP_COLOR, clrBlack); 088. ObjectSetInteger(0, szObj, OBJPROP_BORDER_COLOR, m_Info._color); 089. ObjectSetInteger(0, szObj, OBJPROP_ALIGN, ALIGN_CENTER); 090. ObjectSetInteger(0, szObj, OBJPROP_READONLY, true); 091. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, 17); 092. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, 60); 093. } 094. //+------------------------------------------------------------------+ 095. inline void CreateButtonClose(void) 096. { 097. string szObj; 098. 099. CreateObjectGraphics(szObj = def_NameBtnClose, OBJ_BITMAP_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 100. ObjectSetString(0, szObj, OBJPROP_BMPFILE, 0, "::" + def_Btn_Close); 101. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 102. } 103. //+------------------------------------------------------------------+ 104. public : 105. //+------------------------------------------------------------------+ 106. C_ElementsTrade(const ulong ticket, const EnumEvents ev, color _color, char digits, string szDescr = "\n", const bool IsBuy = true) 107. :C_Mouse(0, "") 108. { 109. ZeroMemory(m_Info); 110. m_Info.szPrefixName = StringFormat("%I64u@%03d", m_Info.ticket = ticket, (int)(m_Info.ev = ev)); 111. m_Info._color = _color; 112. m_Info.szDescr = szDescr; 113. m_Info.bIsBuy = IsBuy; 114. m_Info.digits = digits; 115. } 116. //+------------------------------------------------------------------+ 117. ~C_ElementsTrade() 118. { 119. ObjectsDeleteAll(0, m_Info.szPrefixName); 120. } 121. //+------------------------------------------------------------------+ 122. inline void UpdatePrice(const double open, const double price) 123. { 124. if (price > 0) 125. { 126. CreateLinePrice(); 127. CreateButtonClose(); 128. }else 129. ObjectsDeleteAll(0, m_Info.szPrefixName); 130. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 131. if (m_Info.ev == evMsgClosePositionEA) 132. CreateObjectInfoText(); 133. UpdateViewPort(m_Info.price = (price > 0 ? price : open)); 134. } 135. //+------------------------------------------------------------------+ 136. void ViewValue(const double profit) 137. { 138. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, StringFormat("%." + (string)m_Info.digits + "f", (profit < 0 ? -(profit) : profit))); 139. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, (profit >= 0 ? clrPaleGreen : clrCoral)); 140. ChartRedraw(); 141. } 142. //+------------------------------------------------------------------+ 143. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 144. { 145. string sz0; 146. long _lparam = lparam; 147. double _dparam = dparam; 148. 149. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 150. switch (id) 151. { 152. case (CHARTEVENT_KEYDOWN): 153. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 154. _lparam = (long) m_Info.ticket; 155. _dparam = 0; 156. case CHARTEVENT_CUSTOM + evMsgSetFocus: 157. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 158. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 159. m_Info.bClick = false; 160. case CHARTEVENT_CHART_CHANGE: 161. UpdateViewPort(m_Info.price); 162. break; 163. case CHARTEVENT_OBJECT_CLICK: 164. sz0 = GetPositionsMouse().szObjNameClick; 165. if (m_Info.bClick) switch (m_Info.ev) 166. { 167. case evMsgClosePositionEA: 168. if (sz0 == def_NameBtnClose) 169. EventChartCustom(0, evMsgClosePositionEA, m_Info.ticket, 0, ""); 170. break; 171. case evMsgCloseTakeProfit: 172. if (sz0 == def_NameBtnClose) 173. EventChartCustom(0, evMsgCloseTakeProfit, m_Info.ticket, PositionGetDouble(POSITION_SL), PositionGetString(POSITION_SYMBOL)); 174. else if (sz0 == def_NameBtnMove) 175. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseTakeProfit, ""); 176. break; 177. case evMsgCloseStopLoss: 178. if (sz0 == def_NameBtnClose) 179. EventChartCustom(0, evMsgCloseStopLoss, m_Info.ticket, PositionGetDouble(POSITION_TP), PositionGetString(POSITION_SYMBOL)); 180. else if (sz0 == def_NameBtnMove) 181. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseStopLoss, ""); 182. break; 183. } 184. m_Info.bClick = false; 185. break; 186. case CHARTEVENT_MOUSE_MOVE: 187. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 188. if (m_Info.weight > 1) 189. { 190. UpdateViewPort(GetPositionsMouse().Position.Price); 191. if (m_Info.bClick) 192. { 193. switch (m_Info.ev) 194. { 195. case evMsgCloseTakeProfit: 196. EventChartCustom(0, evMsgNewTakeProfit, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 197. break; 198. case evMsgCloseStopLoss: 199. EventChartCustom(0, evMsgNewStopLoss, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 200. break; 201. } 202. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 203. } 204. } 205. break; 206. } 207. } 208. //+------------------------------------------------------------------+ 209. }; 210. //+------------------------------------------------------------------+ 211. #undef macro_LineInFocus 212. //+------------------------------------------------------------------+ 213. #undef def_Btn_Close 214. #undef def_PathBtns 215. //+------------------------------------------------------------------+ 216. #undef def_NameObjLabel 217. #undef def_NameInfoDirect 218. #undef def_NameBtnMove 219. #undef def_NameBtnClose 220. #undef def_NameHLine 221. //+------------------------------------------------------------------+
C_ElementsTrade
Сначала мы рассмотрим класс C_ElementsTrade, а затем внесём необходимые изменения в индикатор. Поскольку большая часть кода не изменилась, мы сосредоточимся на нововведениях. Первое из них встречается в определении строки 08. Далее вводится переменная из строки 32, о назначении которой скажем чуть ниже.
При добавлении метки прибыли или убытка необходимо пересчитать расположение графических линий и меток. Проверьте изменения в процедуре UpdateViewPort, определённой в строке 36. Я не буду их детализировать, потому что они просты, но стоит иметь их в виду для правильного позиционирования каждого элемента.
Строка 80 включает процедуру, которая создаёт графическую метку и отображает в ней результат, рассчитанный индикатором. Поскольку этот паттерн уже встречался несколько раз в серии, мы не будем повторять объяснение. Стоит также рассмотреть конструктор класса, определённый в строке 106. Теперь конструктор получает количество знаков после запятой, которое будет отображаться в метке. С этим количеством десятичных знаков метка правильно отображает изменение цены, выраженное в количестве тиков или пунктов, что соответствует прибыли или убытку. Хорошо, теперь у нас есть и новая процедура.
Строка 136 определяет новую общедоступную процедуру, которую будет использовать индикатор. Хотя она содержит всего три инструкции, стоит уточнить, как она форматирует разницу в пунктах и как обновляет графическую метку. Процедура получает единственный параметр: количество пунктов, отделяющее цену открытия от лучшей доступной котировки для закрытия позиции.
В строке 138 вызывается функция StringFormat из MQL5. Её синтаксис может показаться необычным: она выводит как положительное число количество пунктов, отделяющее цену открытия от лучшей цены закрытия, и использует количество десятичных знаков, настроенное для символа. Не следует путать эту разницу в ценах с финансовой суммой. Чтобы понять разницу, полезно знать минимальный шаг цены для каждого символа: для одних оно составляет 5 единиц цены, для других — 0.01, а для третьих — 0.00001.
Хотя на первый взгляд это может показаться сложным, именно это и решает StringFormat: он автоматически подстраивает количество десятичных знаков под минимальный шаг цены символа. Отображение большего количества десятичных знаков, чем необходимо, затруднило бы чтение. Мы могли бы установить пять десятичных знаков, но это решение не имело бы смысла для символа, которому требуется только два десятичных знака — или вовсе ни одного, как в случае с фьючерсом на индекс B3 (Бразильская биржа).
Формат построен следующим образом. Сначала мы открываем двойные кавычки, пишем знак процента и добавляем символ точки перед закрытием кавычек. ВАЖНО: МЕЖДУ ЭТИМ СИМВОЛОМ И ДВОЙНЫМИ КАВЫЧКАМИ НЕ ДОЛЖНО БЫТЬ ПРОБЕЛА. Затем мы указываем количество десятичных знаков, сохранённое в digits. Затем мы снова открываем кавычки и пишем f, которая идентифицирует число с плавающей точкой. ТАКЖЕ НЕ ДОЛЖНО БЫТЬ НИКАКОГО ПРОБЕЛА МЕЖДУ НАЧАЛЬНОЙ КАВЫЧКОЙ И БУКВОЙ f. После f мы можем добавить литеральные символы, которые должны сопровождать отформатированное число, при условии, что строка заканчивается двойными кавычками. Поскольку строка формата строится во время выполнения, давайте рассмотрим несколько примеров.
Предположим, что символ использует два десятичных знака. Когда будет выполняться строка 138, конкатенация её фрагментов создаст во время выполнения строку формата "%.2f". Тогда StringFormat будет знать, что нужно отображать два десятичных знака. Тот же самый код работает без перекомпиляции и с символом, которому требуется пять десятичных знаков; в этом случае переменная digits определит, что результирующая строка будет "%.5f".
Хотя на первый взгляд это может показаться сложным, механизм прост и дает полезный результат. Строка 139 сразу указывает, положительный или отрицательный результат. Число всегда отображается положительным, но цвет фона указывает, привела бы позиция к убытку или к возможной прибыли. Вас может удивить, что, за исключением FOREX, когда BID и ASK совпадают, биржевая сделка всегда начинается с убытка: войти и сразу выйти по рынку означает потерять деньги. Это не возможность, это РЕАЛЬНОСТЬ.
Строка 140 приводит к немедленному обновлению графика. Обновления графических элементов обычно попадают в очередь выполнения и иногда требуют некоторого времени, чтобы отобразиться на графике. Данный вызов позволяет избежать этой задержки. Помимо описанных изменений, остальной код остаётся без изменений. Теперь давайте рассмотрим изменения в индикаторе, полный код которого приведен ниже:
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.125" 008. #property link "https://www.mql5.com/pt/articles/13356" 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; //For Expert Advisor use 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. int OnInit() 050. { 051. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 052. if (!CheckCatch(user00)) 053. { 054. ChartIndicatorDelete(0, 0, def_ShortName); 055. return INIT_FAILED; 056. } 057. 058. return INIT_SUCCEEDED; 059. } 060. //+------------------------------------------------------------------+ 061. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 062. { 063. double ask, bid; 064. 065. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 066. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 067. 068. Comment(StringFormat("BID : %f ||| ASK : %f ||| PROFIT : %f", bid, ask, (m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask))); 069. if (Open != NULL) (*Open).ViewValue((m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 070. 071. return rates_total; 072. } 073. //+------------------------------------------------------------------+ 074. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 075. { 076. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 077. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 078. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 079. switch (id) 080. { 081. case CHARTEVENT_CUSTOM + evUpdate_Position: 082. if (lparam != m_Infos.ticket) return; 083. if (!PositionSelectByTicket(m_Infos.ticket)) 084. { 085. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 086. return; 087. }; 088. 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)); 089. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, m_Infos.digits, StringFormat("%I64u : Take Profit price.", m_Infos.ticket)); 090. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, m_Infos.digits, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket)); 091. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 092. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 093. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 094. break; 095. } 096. ChartRedraw(); 097. }; 098. //+------------------------------------------------------------------+ 099. void OnDeinit(const int reason) 100. { 101. delete Open; 102. delete Take; 103. delete Stop; 104. } 105. //+------------------------------------------------------------------+
Исходный код индикатора позиции
Оставшиеся изменения сводятся к объявлению переменной и её инициализации. В строку 25 добавляется переменная, которую мы передаём в конструктор C_ElementsTrade. Переменная, объявленная в строке 25, инициализируется в строке 42 числом десятичных знаков, определённым для символа позиции. Имя символа получается в строке 41, после того как позиция была найдена в строке 35.
Как и ожидалось, конструкторы в строках 88–90 тоже немного изменились. Однако главное новшество находится в OnCalculate: по сравнению с начальной версией статьи добавлена только строка 69. Мы можем удалить строку 68, поскольку она лишь отображает результат для проверки. Строка 69 повторяет в точности тот же расчет.
Таким образом, метка будет отображать разницу между ценой открытия и лучшей ценой выхода, будь то исполнение по BID или по ASK. Я настаиваю: ТО, ЧТО ВЫ УВИДИТЕ, НЕ ЯВЛЯЕТСЯ ФИНАНСОВОЙ СУММОЙ, А КОЛИЧЕСТВОМ ПУНКТОВ, ОТДЕЛЯЮЩИХ ЦЕНУ ОТКРЫТИЯ ОТ ЛУЧШЕЙ ЦЕНЫ ЗАКРЫТИЯ. Не интерпретируйте это число как денежную прибыль или убыток. Для получения финансовой суммы требуется другой расчёт, который мы рассмотрим позже.
Следующая анимация показывает результат, так что на данном этапе его можно не проверять локально.

Обратите внимание, что линия последней сделки, или цена закрытия, может меняться без изменения результата выхода по рынку. Это происходит, потому что котировка BID или ASK остаётся прежней, даже если цена закрытия меняется.
Заключительные идеи
В этой статье я показал, как без особых усилий реализовать графическую метку, которая указывает, приносит ли позиция убыток или прибыль. Процедура проста и эффективна. Многие начинающие трейдеры не до конца понимают влияние спреда и из-за этого могут часто нести убытки. Даже без глубоких знаний этот индикатор позволит вам легко распознавать, когда следует закрывать позицию. Это помогает яснее понять, когда позицию лучше закрыть, и снижает риск получить результат, отличный от ожидаемого. Расчет включает в себя спред на момент закрытия, в отличие от простого считывания POSITION_PROFIT.
Прикрепленный файл содержит все необходимые компоненты для локального тестирования приложения. Я рекомендую использовать демо-счет. Не запускайте систему на реальном счете, пока полностью не разберетесь, как она работает. Материал разработан в учебных целях, а не как окончательное и безошибочное приложение.
| Файл | Описание |
|---|---|
| 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/13356
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий в MQL5 (Часть 49): Разворотный паттерн "Квазимодо" (QM)
Как мы построили самую мощную торговую платформу с Machine Learning: хроника развития MQL и MetaTrader по архивам, форумам и релизам
От начального до среднего уровня: Классы (I)
Автоматизация торговых стратегий в MQL5 (Часть 46): Снятие ликвидности после пробоя структуры (BoS)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования