
Разработка системы репликации (Часть 71): Настройка времени (IV)
Введение
В предыдущей статье, "Разработка системы репликации (Часть 70): Настройка времени (III), мы рассказали о необходимых модификациях в указателе мыши. Данные изменения предназначены для того, чтобы позволить указателю мыши получать события стакана цен. Это происходит, когда указатель мыши используется совместно с приложением репликации/моделирования. Возможно, все эти изменения вызвали у вас возмущение и недоумение. И я знаю, что многие из них на первый взгляд не имеют смысла и гораздо более запутаны, чем мне хотелось бы продемонстрировать. Но очень важно, чтобы вы полностью усвоили данный материал, каким бы запутанным он ни казался на первый взгляд. Я знаю, что у многих из вас возникнут трудности с пониманием того, что я пытаюсь объяснить. Однако без понимания предыдущих материалов (где использовался более простой сервис для объяснений работы), будет гораздо сложнее понять то, что будет объяснено здесь.
Прежде чем пытаться понять тему этой статьи, вы должны убедиться в том, что вы поняли то, что было показано в предыдущей статье. Особенно та часть, которая связана с добавлением события стакана цен к пользовательскому символу и получением в связи с этим возможности использовать функцию OnCalculate таким образом, который раньше был невозможен. Для этого нам нужно использовать вызов iSpread для получения данных, которые MetaTrader 5 будет предоставлять нам.
В сегодняшней статье мы перенесем (точнее, перепишем) часть данного кода из тестового сервиса в сервис репликации/моделирования. Вопрос не столько в том, как это сделать, а в том, как нам надо это сделать.
Хочу напомнить что до предыдущей статьи при работе с сервисом репликации/моделирования указатель мыши загружался через шаблон. Однако теперь мы больше не будем делать это таким образом. Можно попробовать оставить указатель мыши загруженным через шаблон, но из-за некоторых практических вопросов мы начнем размещать указатель мыши вручную на графике символа, который используется для генерации репликации/моделирования. Так что не удивляйтесь, если в видео я покажу всё именно так. У меня есть свои причины, но я не буду вдаваться в подробности. Давайте теперь начнем переписывать код из тестового сервиса в сервис репликации/моделирования.
Начинаем переписывать
Первое, что мы сделаем, - изменим часть кода файла C_Replay.mqh. Обратите внимание на следующий фрагмент:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Print("Waiting for Mouse Indicator..."); 201. Sleep(wait); 202. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 203. if (def_CheckLoopService) 204. { 205. AdjustViewDetails(); 206. Print("Waiting for Control Indicator..."); 207. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 208. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 209. UpdateIndicatorControl(); 210. } 211. 212. return def_CheckLoopService; 213. } 214. //+------------------------------------------------------------------+
Фрагмент кода из файла C_Terminal.mqh
Данный фрагмент является оригинальным кодом. Теперь обратите внимание на следующий момент. Этот код был разработан так, чтобы он функционировал, когда указатель мыши находился в шаблоне. Однако, поскольку впредь мы будем устанавливать указатель мыши вручную, этот код больше не подходит. На самом деле, он всё равно будет работать, но мы можем улучшить его, чтобы получить лучшую конфигурацию с точки зрения обмена сообщениями и исполнения. Поэтому вот новый код, который будет использоваться:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Sleep(wait); 201. AdjustViewDetails(); 202. Print("Loading Control Indicator..."); 203. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 204. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 205. Print("Waiting for Mouse Indicator..."); 206. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 207. UpdateIndicatorControl(); 208. 209. return def_CheckLoopService; 210. } 211. //+------------------------------------------------------------------+
Фрагмент из файла C_Replay.mqh
В принципе, код практически не изменился. Однако отображаемые сообщения стали более понятными, а порядок выполнения тоже изменился. Таким образом, мы сначала попытаемся загрузить индикатор управления, который является ресурсом приложения репликации/моделирования. Это связано с тем, что он встроен в исполняемый файл репликации/моделирования. Только после этого будет загружен указатель мыши. Обратите внимание: если индикатор управления, включенный в сервис, не может быть загружен, это свидетельствует о серьезном инциденте. Но если он загружен правильно, то пользователю сообщается о необходимости загрузить указатель мыши. На мой взгляд, это гораздо лучше. Однако не стесняйтесь располагать элементы в любом удобном для вас порядке. В любом случае, индикатор управления не будет работать, если на графике нет указателя мыши.
Это изменение носит фактически эстетический характер. Давайте теперь перейдем к изменениям, которые действительно поддержат сообщения стакана. Если вы не поняли предыдущую статью, вернитесь к ней и разберитесь в материалах с помощью этого кода. Не пытайтесь понять, как всё работает, с помощью кода, который будет показан, начиная с этого момента. Если вы попробуете так сделать, то окажетесь в полном замешательстве.
Нам необходимо добавить новую строку в конструктор класса. Эту новую строку можно увидеть в фрагменте кода ниже:
149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+
Фрагмент из файла C_Replay.mqh
Новая строка - это как раз строка 163. Как только это будет сделано, мы сможем использовать сообщения стакана. Теперь обратите внимание на один момент. На самом деле, важный момент находится не в заголовочном файле C_Replay.mqh, а в указателе мыши. Поэтому мы позаимствуем фрагмент индикатора, чтобы лучше понять его работу. Потому что это важно.
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
Фрагмент файла указателя мыши
Прошу заметить, что в строке 34 мы указываем начальное состояние указателя мыши. Это покажет, что рынок закрыт. Но на самом деле мы не управляем физическим рынком. Мы используем приложение, основная задача которого - позволить нам провести моделирование возможных движений рынка. Поэтому сообщение, отображаемое на указателе мыши, когда мы находимся в пользовательском символе репликации/моделирования, неверно. Однако это очень просто исправить. Прежде чем исправлять это, важно понять, что в момент появления контрольного индикатора на графике, моделирование фактически находится в режиме паузы. Это происходит, когда приложение запускается с самого начала. Теперь мы должны принять решение, которое окажет капитальное влияние на последующие события.
Давайте подумаем: когда мы находимся в режиме паузы, должен ли указатель мыши показывать сообщение об аукционе или оставшееся время бара? Если он должен отображать информацию о баре, то мы хотим, чтобы оно появлялось до начала первого воспроизведения или только после его запуска? Это, пожалуй, немного запутанно. Давайте рассмотрим следующее: перед официальным открытием рынка проводится аукцион, чтобы участники могли разместить свои ордера по лучшей цене, то есть там, где они действительно хотят купить или продать. Поэтому, как только приложение репликации/моделирования заставит MetaTrader 5 открыть график и заметить указатель мыши, мы должны увидеть сообщение об аукционе. Это факт. Теперь, когда включены моделирование или репликация, какое сообщение должно появляться при активации паузы? Мы хотим видеть сообщение об аукционе или сообщение об оставшемся времени текущего бара? Именно этот вопрос нам и предстоит рассмотреть. В любом случае, первое, что мы должны сделать, это убедиться в том, что при запуске приложения оно показывает, что мы находимся на аукционе.
Сделать это просто. Посмотрите следующий фрагмент:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 231. { 232. if (m_IndControl.Mode == C_Controls::ePause) return true; 233. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 234. CreateBarInReplay(true); 235. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 236. { 237. Sleep(195); 238. iPos -= 200; 239. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 240. UpdateIndicatorControl(); 241. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 242. } 243. } 244. 245. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 246. } 247. }; 248. //+------------------------------------------------------------------+
Фрагмент из файла C_Replay.mqh
Это самая простая часть того, что нам нужно реализовать. Мы расскажем о реализации пошагово, чтобы вы могли четко следить за тем, что делается. Прошу заметить, что в строке 216 появилась новая переменная. В действительности это массив длиной в одну единицу. Теперь начинается самое интересное.
Вы, вероятно, представляете, что значения, генерируемые в событии стакана цен, должны иметь какое-то значение. Ну, на самом деле они не должны ничего значить. Нам просто нужно убедиться, что значения, которые вызывают событие стакана цен, следуют логике. На самом деле, они не должны ничего означать, если только мы не хотим смоделировать стакан цен. Но у нас нет таких намерений, по крайней мере пока. Возможно, они появятся в будущем.
Однако строки 218 и 219 будут использоваться для отображения чего-либо в стакане в MetaTrader 5. Данные значения не имеют особого смысла. Они служат только для поддержки того, что нас действительно интересует, а именно строки 220, где мы указываем в стакане цен, что у нас есть индикативная позиция аукциона. Если вы так и не поняли, прочтите предыдущую статью еще раз, чтобы понять это. В строке 221 мы даем указание MetaTrader 5 запустить событие пользовательского стакана цен. Это событие будет перехвачено функцией OnEventBook, которая в данном случае находится в указателе мыши. Результат: каждый раз, когда выполняется функция LoopEventOnTime, указатель мыши эффективно показывает, что мы находимся на аукционе. Есть два случая, когда функция LoopEventOnTime выполняется с самого начала: первый случай возникает при запуске приложения. Второй случай возникает, когда пользователь взаимодействует с индикатором управления, при нажатии кнопки паузы системы. В этот момент выполняется строка 232, а затем снова выполняется функция LoopEventOnTime. Вы заметили, как всё просто? Теперь, когда у нас есть сообщение об аукционе, как нам можно отобразить информацию об оставшемся времени для баров? Это самая простая часть. Для этого необходимо изменить предыдущий фрагмент на следующий:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. book[0].type = BOOK_TYPE_BUY; 231. CustomBookAdd(def_SymbolReplay, book, 1); 232. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 233. { 234. if (m_IndControl.Mode == C_Controls::ePause) return true; 235. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 236. CreateBarInReplay(true); 237. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 238. { 239. Sleep(195); 240. iPos -= 200; 241. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 242. UpdateIndicatorControl(); 243. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 244. } 245. } 246. 247. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 248. } 249. }; 250. //+------------------------------------------------------------------+
Фрагмент из файла C_Replay.mqh
Вы заметили разницу? Если вы не заметили, то это связано с тем, что вы очень рассеяны, так как разница заключается именно во включении строк 230 и 231. Эти две простые строки означают, что в тот момент, когда пользователь нажимает кнопку воспроизведения в сервисе репликации/моделирования, указатель мыши получает событие пользовательского стакана цен. Это событие означает, что мы переходим из статуса аукциона в статус торгового символа, и таким образом, оставшееся время текущего бара начнет отображаться на указателе мыши. Всё очень просто, не так ли? Но теперь нам предстоит решить несколько более сложную задачу.
На реальном рынке, то есть когда мы подключены к торговому серверу, возможны случаи, когда символ приостанавливается или выставляется на аукцион. Это происходит из-за некоторых очень специфических правил, о которых мы рассказывали в предыдущей статье. Вот что мы сделаем: создадим довольно простое правило. Если временной интервал между тиками символа составляет 60 секунд или более, мы заставим указатель мыши указать, что символ вошел в аукцион. Это самая простая часть. Сложнее будет заставить указатель мыши снова показывать оставшееся время бара.
Можно предложить следующее: когда символ входит в аукцион, мы отправляем константу BOOK_TYPE_BUY_MARKET в стакан цен, а когда он выходит из аукциона, мы отправляем BOOK_TYPE_BUY. Да, именно так мы и должны поступить. Но как это сделать? Давайте посмотрим: мы не хотим, чтобы функция LoopEventOnTime выполнялась с самого начала. Мы хотим, чтобы система работала внутри цикла, который начинается в строке 232 и заканчивается в строке 245, поэтому если мы отправили константы BOOK_TYPE_BUY_MARKET и BOOK_TYPE_BUY внутрь этого цикла, нам придется быть осторожными. Это связано с тем, что каждый вызов CustomBookAdd с этими разными константами обязательно произведет нежелательный эффект на того, кто смотрит на указатель мыши. В результате индикатор будет мигать и постоянно чередоваться между оставшимся временем и словом "АУКЦИОН".
По этой причине мы должны быть креативными и реализовать решение, которое не вызовет такого эффекта, но в то же время решит нашу проблему. Предлагаемое нами решение показано ниже:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+
Фрагмент из файла C_Replay.mqh
Я знаю, что это выглядит не очень элегантно, но, по крайней мере, это работает. При желании можно попробовать сделать это в разных условиях, увеличивая или уменьшая время. Вы свободны в своих действиях. Но прежде чем что-то менять, давайте разберемся, что происходит в данном фрагменте?
Обратите внимание на следующее: в строке 217 я объявляю новую переменную. Она будет использоваться для размещения одной из возможных констант, которые могут быть использованы в стакане. В строке 237 мы используем тернарный оператор для ускорения кода, поскольку наша идея состоит в том, чтобы оценить условие и в зависимости от него присвоить значение переменной typeMsg. Прошу заметить: я мог бы еще больше уплотнить этот код, но объяснение получилось бы слишком сложным. Объясняется это так: после того, как одной из констант присваивается значение typeMsg, мы проверяем, отличается ли ее значение от последнего значения, вызванного как событие пользовательской стакана. Если да, то в строке 239 мы присваиваем новую константу, которая будет использоваться, а в строке 240 вызываем CustomBookAdd. Та часть, на которую действительно нужно обратить внимание, связана со значением, которое сравнивается с переменной iPos в строке 237. Обратите внимание, что здесь мы сравниваем со значением 60 000 (шестьдесят тысяч). Но почему значение именно такое, разве мы не использовали ограничение в одну минуту? Да, но, возможно, вы забыли, что одна минута - это шестьдесят секунд. Прошу заметить, что в строке 236 значение, присвоенное iPos, указано в миллисекундах, а одна секунда равна тысяче миллисекунд. Теперь вы понимаете, почему мы сравниваем iPos со значением шестьдесят тысяч? Так происходит, потому что значение указывается в миллисекундах. Поэтому, если мы решим изменить данное значение на более подходящее, нам придется убедиться в том, что мы установили правильное значение. Иначе информация, отображаемая на указателе мыши, будет сильно отличаться от той, которую мы ожидаем увидеть, когда символ остается в течение определенного времени без добавления тиков на график.
Итак, наконец-то можно увидеть окончательный код файла C_Replay.mqh. В этот момент всё, что связано со временем уже реализовано должным образом, по крайней мере, так, чтобы в итоге получилось то, что показываем на видео ниже:
Полный код заголовочного файла C_Replay.mqh можно увидеть ниже:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "C_ConfigService.mqh" 005. #include "C_Controls.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_IndicatorControl "Indicators\\Market Replay.ex5" 008. #resource "\\" + def_IndicatorControl 009. //+------------------------------------------------------------------+ 010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) 011. //+------------------------------------------------------------------+ 012. #define def_ShortNameIndControl "Market Replay Control" 013. #define def_MaxSlider (def_MaxPosSlider + 1) 014. //+------------------------------------------------------------------+ 015. class C_Replay : public C_ConfigService 016. { 017. private : 018. struct st00 019. { 020. C_Controls::eObjectControl Mode; 021. uCast_Double Memory; 022. ushort Position; 023. int Handle; 024. }m_IndControl; 025. struct st01 026. { 027. long IdReplay; 028. int CountReplay; 029. double PointsPerTick; 030. MqlTick tick[1]; 031. MqlRates Rate[1]; 032. }m_Infos; 033. stInfoTicks m_MemoryData; 034. //+------------------------------------------------------------------+ 035. inline bool MsgError(string sz0) { Print(sz0); return false; } 036. //+------------------------------------------------------------------+ 037. inline void UpdateIndicatorControl(void) 038. { 039. double Buff[]; 040. 041. if (m_IndControl.Handle == INVALID_HANDLE) return; 042. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 043. { 044. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 045. m_IndControl.Memory.dValue = Buff[0]; 046. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 047. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 048. }else 049. { 050. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 051. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 052. m_IndControl.Memory._8b[7] = 'D'; 053. m_IndControl.Memory._8b[6] = 'M'; 054. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 055. } 056. } 057. //+------------------------------------------------------------------+ 058. void SweepAndCloseChart(void) 059. { 060. long id; 061. 062. if ((id = ChartFirst()) > 0) do 063. { 064. if (ChartSymbol(id) == def_SymbolReplay) 065. ChartClose(id); 066. }while ((id = ChartNext(id)) > 0); 067. } 068. //+------------------------------------------------------------------+ 069. inline int RateUpdate(bool bCheck) 070. { 071. static int st_Spread = 0; 072. 073. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 074. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 075. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 076. 077. return 0; 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateBarInReplay(bool bViewTick) 081. { 082. bool bNew; 083. double dSpread; 084. int iRand = rand(); 085. 086. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 087. { 088. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 089. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 090. { 091. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 092. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 093. { 094. m_Infos.tick[0].ask = m_Infos.tick[0].last; 095. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 096. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 097. { 098. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 099. m_Infos.tick[0].bid = m_Infos.tick[0].last; 100. } 101. } 102. if (bViewTick) 103. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 104. RateUpdate(true); 105. } 106. m_Infos.CountReplay++; 107. } 108. //+------------------------------------------------------------------+ 109. void AdjustViewDetails(void) 110. { 111. MqlRates rate[1]; 112. 113. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 114. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 115. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 116. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 117. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 118. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 119. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 120. if (rate[0].close > 0) 121. { 122. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 123. m_Infos.tick[0].last = rate[0].close; 124. else 125. { 126. m_Infos.tick[0].bid = rate[0].close; 127. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 128. } 129. m_Infos.tick[0].time = rate[0].time; 130. m_Infos.tick[0].time_msc = rate[0].time * 1000; 131. }else 132. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 133. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 134. } 135. //+------------------------------------------------------------------+ 136. void AdjustPositionToReplay(void) 137. { 138. int nPos, nCount; 139. 140. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 141. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 142. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 143. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 144. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 145. CreateBarInReplay(false); 146. } 147. //+------------------------------------------------------------------+ 148. public : 149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+ 172. ~C_Replay() 173. { 174. SweepAndCloseChart(); 175. IndicatorRelease(m_IndControl.Handle); 176. SymbolSelect(def_SymbolReplay, false); 177. CustomSymbolDelete(def_SymbolReplay); 178. Print("Finished replay service..."); 179. } 180. //+------------------------------------------------------------------+ 181. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 182. { 183. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 184. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 185. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 186. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 187. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 188. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 189. SweepAndCloseChart(); 190. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 191. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 192. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 193. else 194. Print("Apply template: ", szNameTemplate, ".tpl"); 195. 196. return true; 197. } 198. //+------------------------------------------------------------------+ 199. bool InitBaseControl(const ushort wait = 1000) 200. { 201. Sleep(wait); 202. AdjustViewDetails(); 203. Print("Loading Control Indicator..."); 204. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 205. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 206. Print("Waiting for Mouse Indicator..."); 207. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 208. UpdateIndicatorControl(); 209. 210. return def_CheckLoopService; 211. } 212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+ 257. #undef def_SymbolReplay 258. #undef def_CheckLoopService 259. #undef def_MaxSlider 260. //+------------------------------------------------------------------+
Исходный код файла C_Replay.mqh
Это довольно интересный, но безобидный недостаток.
Хорошо. Хотя эта система работает замечательно, как можно видеть на изображении, но есть одна проблема. Даже если я не показал этот момент в видео, я думаю, что если вы немного понаблюдаете и поразмышляете, то заметите, что проблема существует. Но прежде, чем рассказать, в чем заключается недостаток, проверьте, поняли ли вы, как работает MetaTrader 5. Если не знаете, как работает MetaTrader 5 в различных сценариях, то, скорее всего, вы не обнаружили недостатков в этом программировании. Недостаток заключается в том, что мы НЕ МОЖЕМ МЕНЯТЬ ТАЙМФРЕЙМ ГРАФИКА в течение всего времени использования приложения репликации/моделирования.
Теперь вы, наверное, задаетесь вопросом: почему я не могу изменить таймфрейм графика? Что произойдет, если я попытаюсь делать это? Что произойдет - и это совершенно точно - так это сработает системная ошибка. Однако данная ошибка (сразу будет объяснено, как её исправить) возникает только в очень специфических сценариях. Мы активируем ее только в том случае, если сервис репликации/моделирования находится в режиме выполнения. Если он находится в режиме паузы или если система обнаруживает, что символ находится на аукционе из-за количества транзакций, которые мы определили, ошибка не будет вызвана. Опять же, это произойдет только в том случае, если мы находимся в режиме воспроизведения и на график добавляются тики.
Теперь я снова обращаюсь к вам: вы следили за этими статьями о том, как разработать систему репликации/моделирования, а теперь вы имеете представление о том, почему происходит этот сбой и в чем именно он заключается? Если ответ положительный, тогда отлично! Это значит, что вы учились для того, чтобы всё больше и больше понимать, что такое MQL5 и как работает MetaTrader 5. Если ответ отрицательный, это означает, что вы всё еще находитесь на ранней стадии обучения. Не расстраивайтесь, наоборот, пусть это послужит мотивацией для еще более усердной учебы.
Давайте рассмотрим этот (совершенно безвредный) недостаток. Он сработает только в том случае, если мы находимся в тестовом режиме, а ликвидности достаточно для того, чтобы условие аукциона не срабатывало. При изменении таймфрейма графика эта ошибка приводит к интересным последствиям. Ошибка заключается именно в этом: при соблюдении условий, указанных выше, как только мы изменим таймфрейм графика, указатель мыши начнет показывать, что рынок закрыт. Кроме того, оставшееся время работы бара не будет отображаться. Нам придется активировать режим паузы, а затем снова включить режим воспроизведения.
Возможно, вы сейчас в полном замешательстве, "как это возможно, что при изменении таймфрейма графика, указатель мыши будет показывать, что рынок закрыт? Это бессмысленно". На самом деле, я должен согласиться с вами. Но если у вас есть немного больше опыта, тогда вы знаете, что когда мы меняем таймфрейм графика, MetaTrader 5 удаляет все индикаторы и другие элементы графика. Затем снова открывает график и рисует бары в соответствии с новыми критериями таймфрейма. Вот так работает MetaTrader 5. Поэтому, когда указатель мыши вернется на график, он будет показывать его первоначальную конфигурацию. Чтобы узнать, что это за конфигурация, посмотрите просто на приведенный ниже фрагмент кода, извлеченный из указателя мыши:
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
Фрагмент указателя мыши
Теперь прошу заметить следующее: в строке 34 данного фрагмента мы указываем значение состояния. Это означает, что рынок закрыт. Однако тики по-прежнему поступают от сервиса репликации/моделирования. Именно здесь и кроется проблема. Как видите, это безвредно и не вызывает серьезных проблем, это просто некая помеха для устранения которой нам приходится приостанавливать работу сервиса, а затем возобновлять ее, после чего всё возвращается на круги своя.
Можно придумать тысячу и один способ решить эту проблему. Однако решение, которое мы рассмотрим более необычно, чем можно себе представить. Но перед тем, как изучать решение, можно узнать почему ошибка исчезает, когда мы нажимаем на паузу, а затем на воспроизведение? Если вы не знаете, то давайте рассмотрим, почему данное действие заставляет ошибку исчезнуть. Для этого посмотрим на код в заголовочном файле C_Replay.mqh, а именно на функцию LoopEventOnTime. Когда происходит сбой, указатель мыши сообщает нам, что рынок закрыт. При включении режима паузы, строка 235 выводит нас из цикла, который начинается со строки 233. Это возвращает функцию в основной код. Поскольку возврат функции равен true, основной код сервиса должен снова вызвать функцию LoopEventOnTime. На этом этапе строка 222 вызывает добавление пользовательского события в стакан. Данное событие использует значение, указанное в строке 221, поэтому указатель мыши изменит отображаемую информацию с "рынок закрыт" на "рынок в аукционе". Когда мы повторно включаем сервис нажатием воспроизведения, строка 232 осуществит новый вызов и добавит в стакан еще одно пользовательское событие. Но на этот раз в качестве константы будет использоваться значение, определенное в строке 231, что позволит указателю мыши снова отображать оставшееся время текущего бара.
По этой причине, когда сбой происходит из-за смены таймфрейма графика, просто приостановите, а затем возобновите работу сервиса, чтобы вернуть всё в норму.
Однако, хотя данная ошибка не так сильно влияет на указатель мыши, она раздражает пользователей, когда дело доходит до другого индикатора - индикатор управления. В этом случае делать фактически нечего. Мы должны дождаться провала теста в строке 42 кода в строке заголовка C_Replay.mqh. И почему нам нужно, чтобы данный тест провалился в строке 42, чтобы индикатор управления снова был виден на графике? Причина в том, что только в случае неудачи данного теста выполняется строка 54. Эта строка должна быть выполнена, чтобы индикатор управления обновился. После обновления индикатор управления будет перерисован на графике, если он был скрыт по какой-либо причине.
Хорошо. Теперь вы, наверное, поняли, что ситуация становится всё сложнее. Если обнаруженную ошибку указателя мыши можно устранить простым нажатием кнопки управления для переключения из режима воспроизведения в режим паузы, а затем снова в режим воспроизведения, то что делать, если индикатор управления не виден? Как устранить неисправность указателя мыши? Это связано с тем, что мы не сможем приостановить работу приложения, чтобы сервис перестал отправлять тики. Но если сервис не прекратит отправку тиков, нам придется ждать, пока тест строки 42 не завершится неудачей. Это произойдет только после того, как на график будет добавлено определенное количество тиков. Теперь у нас появилась настоящая проблема.
Тогда можно подумать: "Всё, что мне нужно сделать, - это посмотреть на таймфреймы графика сервиса". Если он изменится, я попрошу, чтобы индикатор управления и указатель мыши получали соответствующие данные, чтобы они правильно сбрасывались. Хорошо. Но знаете ли вы, как сделать так, чтобы сервис обнаружил, что на графике произошла смена таймфрейма? Возможно, есть способ. Но я уверяю вас, что это будет гораздо сложнее, чем решение, которое будет показано. Не в этой статье, а в следующей. Я хочу, чтобы вы попытались найти решение данной проблемы, дорогие читатели.
Заключение
В данной статье мы показали, как можно объединить код, использованный в предыдущей статье, чтобы протестировать и проверить работу пользовательской системы сообщений стакана с сервисом, который существует уже некоторое время. Мы также продемонстрировали, что зачастую безобидные, на первый взгляд, ошибки в реализации могут стать причиной серьезной головной боли.
Однако я также оставил нерешенной проблему таймфреймов (и я уверен, что многие задаются вопросом, как я её решу). Иными словами, как сделать так, чтобы сервис мог обнаружить, что таймфреймы графика, за которым он следит, каким-то образом изменились. Я покажу данный тип решения в следующей статье, поскольку не хочу оставлять вас в полной растерянности, пытаясь понять, почему что-то работает или не работает.
В этом и заключается работа программиста: найти решение, когда никто другой его не видит. Проблемы будут присутствовать всегда. Если бы их не существовало, программирование было бы бессмысленным и невеселым занятием. Поэтому чем больше проблем, тем веселее, потому что они заставляют нас мыслить нестандартно. Они избавляют нас от рутины, когда мы всегда делаем одни и те же вещи одним и тем же способом.
А мы с вами увидимся в следующей статье. Перед тем, как читать новую статью, я предлагаю вам попытаться придумать решение проблемы смены таймфрейма.
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12335
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования