Моделирование рынка: Position View (XVIII)
Введение
Всем привет и добро пожаловать в очередную статью из серии, посвящённой созданию системы репликации/моделирования.
В предыдущей статье «Моделирование рынка: Position View (XVII)» я показал, как сделать так, чтобы индикатор позиции отображал различные типы данных. Таким образом, трейдер может видеть состояние позиции как процентное изменение, финансовый результат, число тиков или разницу значений. Вы, мой дорогой читатель, можете легко дополнить это или даже улучшить.
Хотя индикатор позиции уже почти готов к использованию в системе репликации/моделирования для тестирования системы и проверки нашей способности читать рынок, мы пока не можем этого сделать из-за некоторых нерешённых вопросов, связанных с самим индикатором.
Кроме того, нам необходимо исправить некоторые незначительные ошибки. Если трейдер случайно допустит определённые ошибки во время торговли, он не сможет пользоваться индикатором позиции, потому что тот перестанет работать корректно. В большинстве случаев индикатор будет заставлять трейдера выполнять неинтуитивные действия.
Поэтому в этой статье мы продолжим ещё немного улучшать систему, чтобы ещё на шаг приблизиться к нашей конечной цели. Начнем с первой темы.
Отображение объема
Первое, что мы сделаем, — это отобразим объем открытой позиции. Зачем нам нужна эта информация? Потому что это позволяет трейдеру адекватно оценивать открытый объем и планировать различные стратегии для увеличения прибыли или снижения риска, например с помощью другой одновременной сделки. Поскольку эти вопросы полностью выходят за рамки данной статьи, я не буду подробно останавливаться на подобных стратегиях. Здесь мы сосредоточимся на программировании, а не на объяснении того, как торговать на рынке.
Чтобы отобразить объем открытой позиции, нам нужно лишь внести небольшое изменение в код. Для этого нужно добавить в индикатор объект OBJ_EDIT или OBJ_LABEL. Почему бы не использовать OBJ_TEXT? Причина в его системе позиционирования. В отличие от OBJ_EDIT и OBJ_LABEL, объект OBJ_TEXT размещается с помощью координат цены и времени. Это не мешает его использованию, но с другими типами объектов работать значительно проще, поскольку мы будем использовать декартовы координаты X и Y.
Хорошо. Как программисты, мы должны решить, какой из этих объектов подходит лучше всего. Лично я не вижу большой разницы между использованием того или другого, хотя OBJ_EDIT позволяет сделать оформление немного аккуратнее. Это связано с тем, что мы можем лучше контролировать его свойства и не дать тексту выглядеть как отдельная “висящая” надпись в индикаторе. По этой причине мы будем использовать OBJ_EDIT.
Первое изменение, которое нам нужно внести, показано в следующем фрагменте:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameHLine m_Info.szPrefixName + "#HLINE" 05. #define def_NameBtnClose m_Info.szPrefixName + "#CLOSE" 06. #define def_NameBtnMove m_Info.szPrefixName + "#MOVE" 07. #define def_NameInfoDirect m_Info.szPrefixName + "#DIRECT" 08. #define def_NameObjLabel m_Info.szPrefixName + "#PROFIT" 09. #define def_NameBackGround m_Info.szPrefixName + "#BACKGROUND" 10. #define def_NameVolume m_Info.szPrefixName + "#VOLUME" 11. //+------------------------------------------------------------------+ 12. #define macro_LineInFocus(A) ObjectSetInteger(0, def_NameHLine, OBJPROP_YSIZE, m_Info.weight = (A ? 3 : 1)); 13. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Обратите внимание, что в строке 10 мы добавили новое определение, задающее имя объекта, который вскоре создадим. В исходном коде уже есть вызов, позволяющий создать объект OBJ_EDIT. Мы изменим этот вызов, как показано ниже.
117. //+------------------------------------------------------------------+ 118. inline void CreateObjectInfoText(const string szObj, const color _color) 119. { 120. CreateObjectGraphics(szObj, OBJ_EDIT, clrNONE, (EnumPriority)(ePriorityDefault)); 121. ObjectSetString(0, szObj, OBJPROP_FONT, def_FontName); 122. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, def_FontSize); 123. ObjectSetInteger(0, szObj, OBJPROP_COLOR, clrBlack); 124. ObjectSetInteger(0, szObj, OBJPROP_BORDER_COLOR, _color); 125. ObjectSetInteger(0, szObj, OBJPROP_ALIGN, ALIGN_CENTER); 126. ObjectSetInteger(0, szObj, OBJPROP_READONLY, true); 127. } 128. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Отлично. Теперь мы должны изменить исходный код следующим образом:
171. //+------------------------------------------------------------------+ 172. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 173. { 174. m_Info.sizeText = 0; 175. ObjectsDeleteAll(0, m_Info.szPrefixName); 176. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 177. m_Info.var = (var > 0 ? var : m_Info.var); 178. if (price > 0) 179. { 180. CreateLinePrice(); 181. CreateButtonClose(); 182. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 183. } 184. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 185. m_Info.open = open; 186. UpdateViewPort(m_Info.price = (price > 0 ? price : open)); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. } 190. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Изменение вносится в строке 182. После этого обновления предыдущий код продолжает работать точно так же. Изменение, внесенное в строке 118, и новое определение в строке 10 позволяют нам приступить к созданию нужного элемента. Теперь начинается самое интересное. Но сначала внесем еще одно небольшое изменение, чтобы лучше использовать имеющийся код. Изменение показано в следующем фрагменте.
137. //+------------------------------------------------------------------+ 138. inline void AdjustDinamic(const string szObj, const string szTxt) 139. { 140. uint w, h; 141. 142. TextSetFont(def_FontName, def_FontSize * -10); 143. TextGetSize(szTxt, w, h); 144. m_Info.Text.Height = (uchar) h + 4; 145. m_Info.Text.Width = (uchar) w + 4; 146. m_Info.Text.Width = UpdateViewPort(0, m_Info.Text.Width, h = 32); 147. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 148. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 149. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? 8 : 0)); 150. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YSIZE, m_Info.Text.Height + 5); 151. } 152. //+------------------------------------------------------------------+ . . . 195. //+------------------------------------------------------------------+ 196. void ViewValue(const double profit) 197. { 198. string szTxt; 199. 200. switch (m_Info.ViewMode) 201. { 202. case stInfos::eValue: 203. szTxt = StringFormat("%." + (string)m_Info.Text.digits + "f", MathAbs(profit)); 204. break; 205. case stInfos::eFinance: 206. szTxt = StringFormat("$ %." + (string)m_Info.Text.digits + "f", (MathAbs(profit) / m_Info.var) * m_Info.volume); 207. break; 208. case stInfos::eTicks: 209. szTxt = StringFormat("%d", (uint)MathRound(MathAbs(profit) / m_Info.tickSize)); 210. break; 211. case stInfos::ePercentage: 212. szTxt = StringFormat("%.2f%%", NormalizeDouble((MathAbs(profit) / (m_Info.open ? m_Info.open : m_Info.price)) * 100, 2)); 213. break; 214. } 215. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, szTxt); 216. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, (profit >= 0 ? clrPaleGreen : clrCoral)); 217. if (StringLen(szTxt) != m_Info.sizeText) 218. { 219. AdjustDinamic(def_NameObjLabel, szTxt); 220. m_Info.sizeText = StringLen(szTxt); 221. } 222. } 223. //+------------------------------------------------------------------+
Фрагмент из C_ElementsTrade
Обратите внимание, что я изменил количество параметров процедуры в строке 138. Таким образом, в строках 147 и 148 мы можем повторно использовать этот же код для отображения объема. Поскольку мы изменили эту процедуру, нам также необходимо сразу обновить ее вызов. Это делается в строке 219. Благодаря этой доработке мы заметно расширяем возможности повторного использования кода и сокращаем объем нового кода, который нам придется написать.
Теперь мы можем создать и разместить объект OBJ_EDIT, который будет содержать открытый объем. Это несложная задача; нужно лишь правильно настроить и расположить созданные объекты. Чтобы создать объект и с самого начала присвоить ему правильное значение, мы изменим процедуру UpdatePrice, как показано ниже.
171. //+------------------------------------------------------------------+ 172. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 173. { 174. m_Info.sizeText = 0; 175. ObjectsDeleteAll(0, m_Info.szPrefixName); 176. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 177. m_Info.var = (var > 0 ? var : m_Info.var); 178. if (price > 0) 179. { 180. CreateLinePrice(); 181. CreateButtonClose(); 182. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 183. } 184. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 185. m_Info.open = open; 186. m_Info.price = (price > 0 ? price : open); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. else 190. { 191. CreateObjectInfoText(def_NameVolume, clrBlack); 192. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 193. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 194. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 195. }; 196. UpdateViewPort(m_Info.price); 197. } 198. //+------------------------------------------------------------------+
Фрагмент из C_ElementsTrade
Отлично. Теперь мы уже создаем объект. Теперь давайте посмотрим, что происходит в коде. В строке 187 мы проверяем, используется ли класс для сегмента цены открытия позиции, или для другого сегмента. Поскольку мы хотим отобразить объем в сегменте цены открытия, это условие должно выполняться. Когда выполнение доходит до строки 189, начинается собственно работа. В строке 191 мы создаем объект OBJ_EDIT, который будет получать значение объема. В строке 192 мы задаем цвет его фона. Именно строки 193 и 194 требуют пояснения. Что в них особенного? Эти строки позволяют индикатору позиции адаптироваться к типу информации, которую он должен отображать.
Скорее всего, вы пока не понимаете, что происходит, но принцип работы прост. Когда нужно отобразить значение с плавающей точкой, например 0.02, код автоматически подстроит формат. Если же значение можно представить как целое число, дробная часть будет опущена. Например, будет отображаться 234, так как в этом случае не имело бы смысла добавлять точку и два нуля после неё. Почему это делается именно так? Если вы торгуете символом или на рынке, где дробные значения не поддерживаются, нет смысла отображать дробное значение. Например, если индикатор используется на таком рынке, как B3, бразильская фондовая биржа, где не торгуются дробные доли одного и того же символа, объем будет отображаться только целым числом.
Если же вы торгуете на бирже или рынке, где допускаются дробные значения, например на американском рынке или FOREX, индикатор будет отображать объем с десятичными знаками. Именно это и делает данный код, хотя он применяет несколько иной критерий. На самом деле он проверяет не символ и не используемый рынок, а только значение объема. Если объем содержит десятичную часть, например 10.25, индикатор отобразит это значение. Однако если на том же символе объем равен 10.00, индикатор отобразит только 10 и опустит дробную часть. Именно это и делает код в строках 193 и 194 таким странным.
Обратите внимание ещё на одну деталь: функция UpdateViewPort сменила своё место и теперь находится в строке 196. Раньше при обновлении объема объект не перемещался вместе с сегментом и оказывался смещён относительно своего правильного положения. Это происходило потому, что обновление выполнялось не в нужный момент. Теперь, поскольку UpdateViewPort вызывается позже, обновление выполняется корректно.
Отлично. Мы уже присвоили значение объекту OBJ_EDIT, но нам ещё нужно отобразить его на графике. Для этого нам нужно обратиться к коду, отвечающему за расположение всех объектов, то есть к процедуре UpdateViewPort. Сначала нам нужно изменить одну небольшую деталь в процедуре AdjustDinamic, как показано ниже.
137. //+------------------------------------------------------------------+ 138. inline void AdjustDinamic(const string szObj, const string szTxt) 139. { 140. uint w, h; 141. 142. TextSetFont(def_FontName, def_FontSize * -10); 143. TextGetSize(szTxt, w, h); 144. m_Info.Text.Height = (uchar) h + 4; 145. m_Info.Text.Width = (uchar) w + 4; 146. m_Info.Text.Width = UpdateViewPort(0, m_Info.Text.Width, h = 32); 147. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 148. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 149. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? m_Info.Text.Width + 8 : 0)); 150. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YSIZE, m_Info.Text.Height + 5); 151. } 152. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Обратите внимание, что пришлось немного скорректировать расчёт в строке 149, чтобы фон имел правильные размеры. После внесения этого изменения мы можем перейти к UpdateViewPort, код которого приведён в следующем фрагменте.
55. //+------------------------------------------------------------------+ 56. short UpdateViewPort(const double price, short size = 0, uint ui = 0) 57. { 58. static short _SizeControls; 59. static short _Width; 60. uint x, y; 61. 62. if (size > 0) 63. { 64. size += (short)(ui + 8); 65. _SizeControls = (_SizeControls > size ? _SizeControls : size); 66. size = (short)(_SizeControls - ui - 12); 67. _Width = (_Width > size ? _Width : size); 68. }else 69. { 70. ChartTimePriceToXY(0, 0, 0, price, x, y); 71. x = 125 + (m_Info.ev == evMsgClosePositionEA ? 0 : _Width + (m_Info.ev == evMsgCloseTakeProfit ? _SizeControls : (_SizeControls * 2))); 72. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 73. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 74. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 75. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 76. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10 + (m_Info.ev == evMsgClosePositionEA ? _Width + 2 : 0)); 77. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 78. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + (_Width * 2) + 20); 79. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 80. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + _Width + 20); 81. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 82. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XDISTANCE, x - 10); 83. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YDISTANCE, y - ((m_Info.Text.Height + 5) / 2)); 84. ObjectSetInteger(0, def_NameVolume, OBJPROP_XDISTANCE, x + 10); 85. ObjectSetInteger(0, def_NameVolume, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 86. } 87. return _Width; 88. } 89. //+------------------------------------------------------------------+
Фрагмент C_ElementsTrade
Теперь у нас есть правильный код для отображения всех сегментов. Результат можно увидеть в следующей анимации.

Обратите внимание, что индикатор подстраивается, чтобы избежать наложения объектов друг на друга. Хотя на данный момент этот подход хорошо работает со счетами типа NETTING, он не особенно подходит для счетов типа HEDGING, поскольку будут возникать наложения между объектами, представляющими разные позиции. Даже на счетах типа NETTING по-прежнему существует проблема: по мере увеличения количества объектов они всё сильнее смещаются по оси X. В какой-то момент это неизбежно станет проблемой, но пока мы не будем на этом останавливаться.
Поскольку предыдущий фрагмент довольно прост и содержит лишь ряд корректировок положения, я не считаю необходимым объяснять каждую деталь. Достаточно нарисовать это на бумаге, чтобы легко понять, как распределяются объекты. На этом мы завершаем эту тему и можем перейти к следующей.
Предотвращение исчезновения объектов
Одна из самых деликатных проблем при работе с объектами на графике возникает, когда удаляется объект, необходимый для корректного использования приложения в MetaTrader 5. До сих пор мы не затрагивали этот вопрос, так как были сосредоточены на программировании основных функций индикатора позиции. Теперь, когда у нас стало гораздо больше кода, мы должны учитывать, что, если трейдер или пользователь удалит критически важный объект, это нарушит корректную работу индикатора. Это связано с тем, что все управление осуществляется с помощью объектов, присутствующих на графике.
Поэтому мы должны обеспечить, чтобы в случае случайного удаления любого из этих объектов он был создан заново и вновь размещен на графике. Таким образом, пользователь или трейдер сохранит доступ к функциям, доступным через индикатор позиции.
Обеспечить, чтобы определенные объекты всегда присутствовали на графике — несложная задача. Однако при их повторном размещении следует соблюдать осторожность, так как необходимо придерживаться определенного порядка. В противном случае они окажутся скрыты за другими объектами, принадлежащими тому же сегменту класса C_ElementsTrade. Пожалуй, самым критически важным объектом является кнопка закрытия. Без нее мы не можем напрямую отправлять запросы на торговый сервер, поскольку именно она отвечает за генерацию пользовательского события для советника. Тем не менее, мы не ограничимся только этой кнопкой. Мы обеспечим, чтобы все объекты корректно сохранялись на графике, чтобы трейдер или пользователь не испытывали затруднений при использовании индикатора позиции.
Для этого мы воспользуемся событием. MetaTrader 5 генерирует это событие каждый раз, когда с графика удаляется какой-либо объект. Поскольку класс C_Terminal уже указывает MetaTrader 5, что мы хотим получать это событие, нет необходимости объявлять его заново в коде, так как это уже реализовано через наследование.
Отлично. Давайте начнем добавлять необходимый код в класс C_ElementsTrade. Это не особенно сложно, но требует некоторого внимания. В коде, по сути, есть только два места, где объекты могут быть удалены. Прежде чем начать, нужно убедиться, что объекты удаляются именно в этих двух местах. Очевидно, в обоих случаях используется вызов ObjectsDeleteAll или другой вызов, предназначенный для удаления объектов с графика. Поскольку это происходит только в двух местах кода, мы можем ограничиться изменениями в показанном ниже фрагменте.
168. //+------------------------------------------------------------------+ 169. ~C_ElementsTrade() 170. { 171. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 172. ObjectsDeleteAll(0, m_Info.szPrefixName); 173. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 174. } 175. //+------------------------------------------------------------------+ 176. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 177. { 178. m_Info.sizeText = 0; 179. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 180. ObjectsDeleteAll(0, m_Info.szPrefixName); 181. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 182. m_Info.volume = (vol > 0 ? vol : m_Info.volume);
Фрагмент из C_ElementsTrade
Этот фрагмент не позволяет MetaTrader 5 генерировать событие, когда необходимо удалить объекты с графика. Поскольку код продолжает обновляться, нумерация строк тоже меняется. Однако, если вы внесли все изменения из предыдущей темы, то сможете найти места, указанные в этом фрагменте. Обратите внимание, что были добавлены четыре новые строки. Строки 171 и 179 отключают обработку события, а строки 173 и 181 снова его включают. Тем не менее, лучше сделать это иначе, чтобы снизить вероятность ошибок при программировании.
Хотя предыдущий фрагмент и решает задачу, в идеале эти три строки лучше сгруппировать более подходящим образом. Один из вариантов — использовать макрос, другой — создать процедуру внутри класса. Поскольку макрос пришлось бы явно удалять в конце заголовочного файла, а я не хочу вас путать, мы создадим процедуру. Вы можете увидеть это в следующем фрагменте.
154. //+------------------------------------------------------------------+ 155. inline void RemoveAllsObjects(void) 156. { 157. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 158. ObjectsDeleteAll(0, m_Info.szPrefixName); 159. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 160. } 161. //+------------------------------------------------------------------+ 162. public : 163. //+------------------------------------------------------------------+ . . . 175. //+------------------------------------------------------------------+ 176. ~C_ElementsTrade() 177. { 178. RemoveAllsObjects(); 179. } 180. //+------------------------------------------------------------------+ 181. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 182. { 183. m_Info.sizeText = 0; 184. RemoveAllsObjects(); 185. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 186. m_Info.var = (var > 0 ? var : m_Info.var); 187. if (price > 0) 188. {
Фрагмент из C_ElementsTrade
В этом фрагменте я показываю, где была создана процедура и как был изменен предыдущий код, чтобы использовать ее внутри класса C_ElementsTrade. Обратите внимание, что теперь код гораздо проще. Возможно, вы подумаете, что процедура в строке 155 не нужна, поскольку предыдущий фрагмент уже решал эту проблему. Однако по мере усложнения кода очень легко забыть сделать что-то важное. Чтобы избежать проблем в будущем, общий код лучше всегда выносить в отдельную функцию или процедуру.
Таким образом, если впоследствии потребуется что-то изменить, достаточно будет изменить только одну часть кода. Это гораздо лучше, чем обновлять несколько мест, разбросанных по всему коду. После того как мы объяснили, зачем была создана процедура в строке 155, можно, наконец, перейти к анализу процедуры DispatchMessage, приведенной ниже.
235. //+------------------------------------------------------------------+ 236. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 237. { 238. string sz0; 239. long _lparam = lparam; 240. double _dparam = dparam; 241. 242. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 243. switch (id) 244. { . . . 294. case CHARTEVENT_OBJECT_DELETE: 295. if (StringFind(sparam, m_Info.szPrefixName) < 0) break; 296. UpdatePrice(m_Info.open, m_Info.price); 297. break; 298. } 299. } 300. //+------------------------------------------------------------------+ 301.
Фрагмент C_ElementsTrade
Обратите внимание, что я выделяю только то, что нас действительно интересует. Вероятно, вы уже задаетесь вопросом, работает ли это. Ответ: более или менее. Прежде чем объяснить, почему мы не можем просто ответить «да» или «нет», давайте посмотрим, как работает этот фрагмент. В этом разделе я упомянул, что MetaTrader 5 генерирует событие при удалении объекта с графика. В этот момент строка 294 перехватывает событие, отправленное MetaTrader 5. Имя удалённого объекта передаётся через аргумент sparam. Обратитесь к документации для получения дополнительной информации.
Нам нужно проверить, совпадает ли имя, переданное MetaTrader 5, с одним из имен, созданных индикатором позиции. Существует несколько способов выполнить эту проверку, но здесь мы используем один из самых простых. В строке 295 мы используем вызов из библиотеки MQL5, чтобы проверить, совпадает ли префикс имени объекта, созданного индикатором позиции, с именем, переданным MetaTrader 5. Если они не совпадают, мы игнорируем оставшуюся часть процедуры восстановления. Если совпадает, то в строке 296 мы вызываем у класса C_ElementsTrade повторное создание объектов.
Теперь обратите внимание на следующее: процедура UpdatePrice заново создаёт объекты, поэтому код работает, как и предполагалось. Однако есть одна небольшая проблема. Она возникает, когда у позиции отсутствует один из сегментов, например тейк-профит. В этом случае на графике останется только объект для перемещения линии. Если пользователь или трейдер удалит этот объект, вызов в строке 296 снова создаст все остальные объекты, а не только объект перемещения. Поэтому мы не можем утверждать, что код работает правильно; на самом деле он работает лишь частично. Чтобы устранить эту проблему, нам нужно обновить код. Не беспокойтесь, изменение простое и показано в следующем фрагменте.
180. //+------------------------------------------------------------------+ 181. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 182. { 183. m_Info.sizeText = 0; 184. RemoveAllsObjects(); 185. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 186. m_Info.var = (var > 0 ? var : m_Info.var); 187. if (price > 0) 188. { 189. CreateLinePrice(); 190. CreateButtonClose(); 191. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 192. } 193. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 194. m_Info.open = open; 195. m_Info.price = (price > 0 ? price : -open); 196. if (m_Info.ev != evMsgClosePositionEA) 197. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 198. else 199. { 200. CreateObjectInfoText(def_NameVolume, clrBlack); 201. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 202. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 203. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 204. }; 205. UpdateViewPort(MathAbs(m_Info.price)); 206. } 207. //+------------------------------------------------------------------+ . . . 235. //+------------------------------------------------------------------+ 236. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 237. { 238. string sz0; 239. long _lparam = lparam; 240. double _dparam = dparam; 241. 242. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 243. switch (id) 244. { 245. case (CHARTEVENT_KEYDOWN): 246. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 247. _lparam = (long) m_Info.ticket; 248. _dparam = 0; 249. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 250. case CHARTEVENT_CUSTOM + evMsgSetFocus: 251. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 252. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 253. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 254. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 255. m_Info.bClick = false; 256. case CHARTEVENT_CHART_CHANGE: 257. UpdateViewPort(MathAbs(m_Info.price)); 258. if (m_Info.ev != evMsgClosePositionEA) 259. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 260. break; 261. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition:
Фрагмент C_ElementsTrade
Обратите особое внимание, потому что речь идет об очень тонком моменте. Обратите внимание на строку 195. В ней мы задаем, что значение m_Info.price будет отрицательным, если параметр price меньше или равен нулю. Это произойдет только в том случае, если основной код укажет, что у сегмента нет значения. Чтобы лучше это понять, помните: когда основной код индикатора, показанный в предыдущих статьях, пытается получить значение линии тейк-профита или стоп-лосса, а этой линии у позиции нет, параметр price будет равен нулю. Если оно существует, его значение будет отличным от нуля.
Именно здесь и происходит магия. Если значение больше нуля, предыдущий фрагмент может восстановить объекты сегмента. Если же оно равно нулю, мы присваиваем m_Info.price отрицательное значение. Это отрицательное значение соответствует цене открытия позиции. Поскольку нет ни одной линии, которую нужно было бы создать заново, индикатор будет отображать только объект, позволяющий перемещать цену, чтобы мы могли создать недостающую линию. Таким образом, если пользователь или трейдер попытается удалить именно этот объект, MetaTrader 5 сгенерирует событие CHARTEVENT_OBJECT_DELETE, и строка 296 вызовет процедуру UpdatePrice. В этот момент, в отличие от того, что происходило раньше, будет заново создан только объект перемещения.
Почему это происходит? Возможно, вы еще не поняли причину. Единственное, что нам пришлось сделать, — это обновить код, чтобы m_Info.price мог принимать отрицательное значение. Чтобы это понять, посмотрите на строку 296 и обратите внимание, что вторым аргументом, передаваемым в UpdatePrice, является именно m_Info.price. Теперь обратите внимание на строку 187. Это значение соответствует второму аргументу вызова в строке 181. Поскольку оно отрицательное, условие в строке 187 возвращает false и не позволяет заново создать остальные объекты.
Однако возможность того, что m_Info.price будет отрицательным, влечет за собой определенные последствия, поэтому нам нужно внести в код дополнительные изменения. Обратите внимание, что в строках 197, 205, 257 и 259 пришлось добавить вызов функции из библиотеки MQL5. Это связано с тем, что мы не можем выполнять такие вычисления напрямую с отрицательными значениями. MathAbs решает эту проблему, возвращая абсолютное значение. Таким образом, приложив минимум усилий, мы исправили код и сделали так, что индикатор позиции не позволил пользователю или трейдеру удалить с графика объекты, созданные самим индикатором.
Прежде чем закончить, давайте решим еще одну небольшую проблему. Обратите внимание, что в предыдущем фрагменте есть дублирующийся блок кода. Дублирование легко заметить по приведенному фрагменту: строки 257–259 содержат код, который также встречается в строках 196–205. Хотя на первый взгляд это не кажется дублированием, на самом деле это так. Мы решим эту задачу так же, как и раньше: создадим процедуру, чтобы устранить дублирование. Обновленный фрагмент приведен ниже.
161. //+------------------------------------------------------------------+ 162. inline void ChartChange(void) 163. { 164. UpdateViewPort(MathAbs(m_Info.price)); 165. if (m_Info.ev != evMsgClosePositionEA) 166. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 167. } 168. //+------------------------------------------------------------------+ 169. public : 170. //+------------------------------------------------------------------+ . . . 187. //+------------------------------------------------------------------+ 188. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 189. { 190. m_Info.sizeText = 0; 191. RemoveAllsObjects(); 192. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 193. m_Info.var = (var > 0 ? var : m_Info.var); 194. if (price > 0) 195. { 196. CreateLinePrice(); 197. CreateButtonClose(); 198. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 199. } 200. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 201. m_Info.open = open; 202. m_Info.price = (price > 0 ? price : -open); 203. if (m_Info.ev == evMsgClosePositionEA) 204. { 205. CreateObjectInfoText(def_NameVolume, clrBlack); 206. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 207. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 208. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 209. }; 210. ChartChange(); 211. } 212. //+------------------------------------------------------------------+ . . . 240. //+------------------------------------------------------------------+ 241. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 242. { 243. string sz0; 244. long _lparam = lparam; 245. double _dparam = dparam; 246. 247. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 248. switch (id) 249. { 250. case (CHARTEVENT_KEYDOWN): 251. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 252. _lparam = (long) m_Info.ticket; 253. _dparam = 0; 254. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 255. case CHARTEVENT_CUSTOM + evMsgSetFocus: 256. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 257. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 258. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 259. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 260. m_Info.bClick = false; 261. case CHARTEVENT_CHART_CHANGE: 262. ChartChange(); 263. break; 264. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition:
Фрагмент C_ElementsTrade
Теперь у нас есть новая процедура в строке 162. Эта процедура является закрытой в классе C_ElementsTrade. Здесь есть важная деталь: хотя в классе C_Terminal существует ещё одна процедура с таким же именем, это не вызывает никакой неоднозначности для компилятора, поскольку каждая из них является закрытой в том классе, где она объявлена. Хотя C_ElementsTrade наследует процедуры C_Terminal, когда код из строки 162 вызывается из строк 210 и 262, компилятор может определить, что это не процедура, определённая в C_Terminal. Это связано с тем, что эта процедура недоступна за пределами данного класса. Поэтому то, что C_ElementsTrade наследует от C_Terminal, в данном случае не вызывает никаких конфликтов. Таким образом, нам удалось полностью удалить из системы ещё один блок дублирующегося кода.
Заключительные замечания
В этой статье я показал — или, по крайней мере, постарался показать как можно более наглядно, — как изменять и развивать код, способный решать определённые задачи, минимально затрагивая уже существующий код. Ничего из этого не было бы возможно, если бы система не была разработана по модульному принципу. Хотя поначалу разработка систем таким образом может показаться сложной, на самом деле всё не так уж сложно. Тем не менее, это требует, чтобы вы продолжали думать, практиковаться и учиться, чтобы в полной мере использовать возможности, которые может предоставить тот или иной язык программирования.
Во вложениях к статье я оставлю доступной уже скомпилированную версию кода, а также остальные приложения, необходимые для тестирования индикатора позиции. Так вы сможете увидеть его в работе, прежде чем разбирать код, представленный в статьях. Тем не менее, мы ещё не завершили работу над этим индикатором. В нём ещё есть некоторые моменты, которые нужно лучше реализовать в коде, прежде чем мы сможем действительно использовать его в системе репликации/моделирования. Речь идёт не о серьёзных проблемах, а о небольших недочётах, которые необходимо исправить, чтобы система репликации/моделирования могла правильно использовать индикатор. Мы всё ближе к тому, чтобы получить по-настоящему работоспособную систему. Продолжайте следить за этой серией статей. Увидимся в следующей части, где мы продолжим двигаться к созданию полностью работоспособной системы репликации/моделирования.
| Файл | Описание |
|---|---|
| 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/13457
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
От начального до среднего уровня: Очереди, списки и деревья (VIII)
Осваиваем графики Kagi в MQL5 (Часть 2): Реализация автоматизированной торговли на основе Kagi
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования