
Разработка системы репликации (Часть 73): Неожиданный способ оповещений (II)
Введение
В предыдущей статье "Разработка системы репликации (Часть 72): Неожиданный способ оповещений (I)" я начал показывать, как можно использовать индикатор для передачи информации, которую иным способом было бы невозможно получить. Все бы хорошо, но на самом деле реализовать код в нашем приложении репликации/моделирования не так-то просто, как это может показаться после прочтения статьи. Возможно, вы думаете, что я преувеличиваю, и что реализация подобных вещей довольна проста и понятна, а я просто создаю интригу.
Мне искренне хотелось бы заинтриговать вас с помощью этой темы, но ситуация гораздо сложнее, чем вы можете себе представить. Я приложил все усилия, чтобы статьи были понятны всем тем, кто действительно хочет научиться работать с MQL5. А текущая тема — это то, что пока еще никем не исследовалось, по крайней мере на момент написания этой статьи. Не то чтобы это было что-то невообразимое, но, по крайней мере, весьма экзотическое и очень необычное. По этой причине я пытаюсь подробно показать, как следует действовать в таких беспрецедентных случаях, когда приходится делать что-то, что еще никто не делал.
Продолжаем реализацию
Прежде чем перейти к теме, касающейся собственно сервиса, который, несомненно, станет самой сложной частью всей реализации, давайте рассмотрим код индикатора управления. Это связано с тем, что в предыдущей статье я представил только код заголовочного файла C_Controls.mqh. Поскольку информации уже было много, я решил закончить объяснение про индикатор управления в этой статье. Итак, начнем с изучения исходного кода.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property icon "/Images/Market Replay/Icons/Replay - Device.ico" 04. #property description "Control indicator for the Replay-Simulator service." 05. #property description "This one doesn't work without the service loaded." 06. #property version "1.73" 07. #property link "https://www.mql5.com/pt/articles/12363" 08. #property indicator_chart_window 09. #property indicator_plots 0 10. #property indicator_buffers 1 11. //+------------------------------------------------------------------+ 12. #include <Market Replay\Service Graphics\C_Controls.mqh> 13. //+------------------------------------------------------------------+ 14. C_Controls *control = NULL; 15. //+------------------------------------------------------------------+ 16. input long user00 = 0; //ID 17. //+------------------------------------------------------------------+ 18. double m_Buff[]; 19. int m_RatesTotal = 0; 20. //+------------------------------------------------------------------+ 21. int OnInit() 22. { 23. if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) 24. SetUserError(C_Terminal::ERR_PointerInvalid); 25. if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == 0)) 26. { 27. Print("Control indicator failed on initialization."); 28. return INIT_FAILED; 29. } 30. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 31. ArrayInitialize(m_Buff, EMPTY_VALUE); 32. 33. return INIT_SUCCEEDED; 34. } 35. //+------------------------------------------------------------------+ 36. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 37. { 38. (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff); 39. 40. return rates_total; 41. } 42. //+------------------------------------------------------------------+ 43. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 44. { 45. (*control).DispatchMessage(id, lparam, dparam, sparam); 46. (*control).SetBuffer(m_RatesTotal, m_Buff); 47. } 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. switch (reason) 52. { 53. case REASON_TEMPLATE: 54. Print("Modified template. Replay // simulation system shutting down."); 55. case REASON_INITFAILED: 56. case REASON_PARAMETERS: 57. case REASON_REMOVE: 58. case REASON_CHARTCLOSE: 59. ChartClose(user00); 60. break; 61. } 62. delete control; 63. } 64. //+------------------------------------------------------------------+
Исходный код индикатора управления
Если вы сравните этот код с предыдущими, вы сразу заметите одно отличие в строке 25. Ранее проверка переменной _LastError производилась по константе ERR_SUCCESS. Однако это вызывало некоторые проблемы. Это связано с тем, что индикатор иногда помещался на график, когда переменная _LastError содержала значение, отличное от ERR_SUCCESS.
Мне потребовалось некоторое время, чтобы понять, почему иногда инициализация давала сбой. Это странно, потому что даже после вызова библиотеки ResetLastError и отладки кода для удаления всех ошибок, при возврате конструктора класса C_Controls, _LastError иногда был с каким-то значением.
И самое странное в этой истории то, что во многих случаях ошибка была связана с каким-то другим символом или открытым графиком. Поэтому, по всей видимости — и я хочу быть предельно ясным в этом вопросе, поскольку ничего не утверждаю — существует некая утечка информации между различными графиками. Но даже когда присутствовал только график кастомизированного символа, возвращалась ошибка, никак не связанная с приложением. В связи с этим, я решил изолировать ошибки. Только ошибки, определяемые путем вызова библиотеки SetUserError, будут приводить к завершению работы индикатора и его исчезновению с графика.
Но на самом деле, в этом коде нам нужно обратить пристальное внимание на функцию OnCalculate. Почему это так важно? Дело в том, что функция OnChartEvent запускается в разное время, в основном из-за движений мыши. Не забывайте, что в нашем приложении мы используем мышь. А вот функция OnCalculate срабатывает всякий раз, когда на графике символа появляется новый тик или котировка. Окей. То есть, даже при использовании только клавиатуры или какого-либо интерфейса, где нажатия кнопок вызывают определенные события, вызов OnChartEvent будет происходить. Но мы можем обеспечить более быстрое обновление, вернее сказать, мы можем гарантировать, что информация в буфере будет максимально актуальной. Для этого мы используем функцию OnCalculate.
Поэтому посмотрите на строку 38. Она довольно проста и понятна, и выполняет ту же задачу, что и строка 46. Однако, в отличие от последней, в строке 38 буфер будет обновляться с каждой новой котировкой, принимаемой индикатором. В том числе и тогда, когда мышь остается неподвижной, или событие мыши не произошло, OnCalculate будет работать практически постоянно, гарантируя, что OnChartEvent будет активироваться только после изменений.
Теперь вы знаете, как работает наш индикатор управления. Но не забывайте, что при смене таймфрейма выполняется функция OnDeInit, которая удаляет индикатор с графика, а затем вызывается функция OnInit. Так что, прежде чем что-либо отобразится на графике, выполняются OnCalculate и OnChartEvent. Нам нужно, чтобы данные в буфере всегда были актуальными.
Отлично, можем ли мы приступить к изменению заголовочного файла C_Replay.mqh? Да, мы могли бы сделать это прямо сейчас. Однако, я не уверен, что все новички действительно поняли, как будет происходить эта коммуникация. Поэтому я прошу вас, дорогой читатель, проявить немного больше терпения. Если вы уже поняли, как это произойдет, или должно произойти, проявите терпение, чтобы те, кто еще не очень разобрался в теме, также смогли все узнать и понять. Мы вернемся к рассмотрению этой темы в одном из следующих уроков.
Понятие быстрого обмена информацией
Большинство людей с небольшим опытом работы с MQL5, вероятно, подумают, что обмен информацией, а точнее, чтение буфера индикатора из сервиса, — это простое дело. Все, что нужно сделать, это использовать handle, также известный как идентификатор. И на самом деле, это работает очень хорошо. Но есть одна важная деталь, или, вернее, проблема использования handle в подобных случаях.
Возможно, у вас не было такого опыта или вы думаете, что я преувеличиваю, но как это возможно, что использование handle становится проблемой? Вроде всегда так делали, и это всегда работало? Хорошо, я не буду спорить ни с вашей логикой, ни с опытом. Необоснованные аргументы ни к чему не приведут. Вместо того, чтобы спорить и пытаться что-то друг другу доказать, как насчет того, чтобы просто попробовать? Тогда никто не сможет ничего возразить, поскольку то, что было испытано и подтверждено, не может быть опровергнуто. Это становится правдой, и точка.
Чтобы продемонстрировать, что использование handle для доступа к данным в буфере, являющемся частью индикатора, является довольно неустойчивым в случае доступа через сервис, давайте проясним: ситуация НЕУСТОЙЧИВА при изменении таймфрейма. Очень важно иметь это в виду. Проблема заключается в изменении таймфрейма. Давайте сделаем вот что: напишем два кода и проведем тестирование. Не волнуйтесь, это будут довольно простые и короткие коды. Вот первый из них: код индикатора.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. #property indicator_buffers 1 07. //+------------------------------------------------------------------+ 08. #include <Market Replay\Defines.mqh> 09. //+------------------------------------------------------------------+ 10. double m_Buff[]; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 15. ArrayInitialize(m_Buff, EMPTY_VALUE); 16. IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); 17. 18. return INIT_SUCCEEDED; 19. } 20. //+------------------------------------------------------------------+ 21. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 22. { 23. m_Buff[rates_total - 1] = 1.0 * def_IndicatorTimeFrame; 24. 25. return rates_total; 26. } 27. //+------------------------------------------------------------------+
Исходный код индикатора для тестирования
Обратите внимание, что в пятой строке мы сообщаем компилятору, что не хотим выводить на экран никакой информации. Таким образом, мы избежим появления оповещений при каждой новой компиляции. В шестой строке мы объявляем, что будем использовать буфер. В строку 8 мы включаем заголовочный файл, который используется в приложении репликации/моделирования. Это важно, поскольку идея состоит в том, чтобы понять, как данные будут поступать в систему репликации/моделирования. В строке 10 находится наш буфер. В теле кода OnInit мы инициализируем буфер и объявляем имя индикатора. Обратите внимание на эту деталь, так как эта информация позже нам понадобится.
Теперь обратите внимание на тело функции OnCalculate. В строке 23 мы помещаем значение таймфрейма в буфер. Это то же самое значение, которое было показано в предыдущей статье, которую очень важно внимательно изучить и понять. Хорошо, я думаю, что каждый из нас, с большим или меньшим опытом, точно знает, что делает индикатор и как он работает, хотя бы на базовом уровне. Теперь давайте рассмотрим второй код.
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret, handle; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. handle = ChartIndicatorGet(id, 0, "TEST"); 19. do 20. { 21. ret = CopyBuffer(handle, 0, 0, 1, Buff); 22. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 23. Sleep(250); 24. } while ((!_StopFlag) && (ret > 0)); 25. IndicatorRelease(handle); 26. } 27. //+------------------------------------------------------------------+
Исходный код сервиса для тестирования
Во второй строке мы сообщаем компилятору, что код будет сервисом. В строке шесть мы добавляем возможность, чтобы пользователь мог настроить, какой символ будет отслеживаться сервисом. До этого момента все идет нормально и никаких проблем нет. Между строками 10 и 12 мы объявляем переменные. Теперь важный момент: чтобы сервис работал, необходимо, чтобы был открыт хотя бы один график. В противном случае, у нас возникнут проблемы, поскольку мы не проводили никаких тестов. Однако, если есть хотя бы один открытый график, строка 14 захватит ID этого графика, и мы сможем выполнить поиск, чтобы локализовать график символа, указанного в строке 6. Как только мы его найдем, мы получим правильный ID.
С помощью ID графика, мы просим MetaTrader 5 предоставить нам handle или идентификатор индикатора. Обратите внимание на использующееся здесь имя. Оно должно совпадать с указанным в строке 16 исходного кода индикатора. Теперь мы входим в цикл; поскольку у нас уже есть значение handle, мы можем использовать его для чтения буфера индикатора. Это делается в строке 21. В строке 22 мы выводим на терминал информацию, полученную из буфера, а также значение, возвращаемое при его чтении. Этот цикл будет выполняться до тех пор, пока выполняются условия, указанные в строке 24, то есть, пока сервис не прекратит свою работу, или чтение не станет отрицательным. Как только это произойдет, handle будет освобожден в строке 25.
Хорошо, мы все согласны, что этот код действительно прочтет буфер индикатора, и что выведенное значение будет соответствовать ожидаемому значению, согласно тестов, выполненных в предыдущей статье. Итак, давайте применим это на практике в MetaTrader 5. Чтобы было проще, можно увидеть результат, посмотрев следующее видео:
Но что здесь произошло? Вы не верите этому видео и считаете, что вас пытаются обмануть? Ладно, вы вольны верить во что хотите. По факту, вам вовсе необязательно мне верить. Выполните любые необходимые тесты, имея в виду, что для считывания буфера индикатора следует использовать handle или идентификатор, и то, что вы увидели в видео, произойдет на самом деле. Но почему?
Секрет кроется в идентификаторе или handle. Когда MetaTrader 5 меняет таймфрейм, значение идентификатора handle меняется. Однако, код продолжает оперировать идентификатором, который больше недействителен, поскольку MetaTrader 5 заменил график новым handle. Когда вы используете советник или индикатор для захвата handle, а затем и для считывания буфера индикатора, handle обновляется, даже если его значение остается прежним. Это происходит потому, что и советник, и любые другие индикаторы повторно вставляются в график, что вызывает актуализацию вызовов. В этот момент обновляется handle, что позволяет правильно прочитать буфер.
Однако сервис работает вне графика, следовательно, значение handle не будет обновлено. По этой причине прочитанное значение всегда одинаково. Вы можете подумать, что вас это не касается, это никак не применимо, и что сервис каким-то образом работает за пределами «реального мира». Хорошо, давайте проверим эту гипотезу. Таким образом, у нас будет конкретное доказательство того, что я говорил о handle. Для этого нам нужно будет только изменить код сервиса. Новый код показан ниже:
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. do 19. { 20. ret = CopyBuffer(ChartIndicatorGet(id, 0, "TEST"), 0, 0, 1, Buff); 21. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 22. Sleep(250); 23. } while ((!_StopFlag) && (ret > 0)); 24. } 25. //+------------------------------------------------------------------+
Исходный код сервиса для тестирования
Обратите внимание, что код претерпел небольшие, почти незаметные изменения. Но главная деталь — и мне хотелось бы это подчеркнуть — находится в строке 20. Теперь handle или идентификатор перестал быть статическим и стал динамическим. То есть, даже если сервис не знает, каков таймфрейм, или какого рода изменения происходят на графике, он сможет определить правильный индикатор и прочесть его буфер. Для простоты понимания, вы можете увидеть результат выполнения на видео ниже:
Ну, теперь вы действительно думаете, что это шутка. Как это возможно? Спокойно, дорогой читатель. Как я уже упоминал, я не хотел показывать изменения в файле C_Replay.mqh, предварительно их не объяснив.
Видите, как что-то, казалось бы, простое, но вполне приемлемое, может полностью изменить происходящее? Как я уже говорил в начале статьи, мы всегда должны экспериментировать с чем-то максимально простым. Я вижу, как многие люди пытаются создавать сложные коды, не имея никакой прочной, надежной основы. В конце концов, они сдаются, потому что не могут понять и применить все эти мелкие детали.
Правда в том, что всегда приходится сопоставлять вычислительные затраты с затратами на реализацию или выполнение. Бесполезно иметь работающий код, если он медленный. То же самое происходит и в обратном направлении: быстрый код, который не функционирует, так же бесполезен.
Тот факт, что handle или идентификатор во втором коде является динамическим, делает его выполнение немного медленнее, чем в первой версии того же сервиса. Это связано с тем, что во второй версии, при каждом выполнении кода производится дополнительный вызов. В первой версии, поскольку handle известен заранее, его значение просто запрашивается в переменной, что намного быстрее.
Это именно те затраты, которые вам всегда следует учитывать. Поэтому всегда лучше тестировать вещи на более простом коде. Но тогда, как это будет реализовано в заголовочном файле C_Replay.mqh, который отвечает за управление службой репликации/моделирования? Чтобы ответить на этот вопрос, перейдем к следующей теме.
Изменяем файл 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 SendEventCustom(const ENUM_BOOK_TYPE Arg1 = BOOK_TYPE_BUY_MARKET) 038. { 039. MqlBookInfo book[1]; 040. 041. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 042. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 043. m_IndControl.Memory._8b[7] = 'D'; 044. m_IndControl.Memory._8b[6] = 'M'; 045. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 046. book[0].price = 1.0; 047. book[0].volume = 1; 048. book[0].type = Arg1; 049. CustomBookAdd(def_SymbolReplay, book, 1); 050. } 051. //+------------------------------------------------------------------+ 052. inline void CheckIndicatorControl(void) 053. { 054. static uchar memTimeFrame = 0; 055. static C_Controls::eObjectControl memMode = m_IndControl.Mode; 056. double Buff[]; 057. 058. if (CopyBuffer(ChartIndicatorGet(m_Infos.IdReplay, 0, "Market Replay Control"), 0, 0, 1, Buff) < 0) ChartClose(m_Infos.IdReplay); 059. m_IndControl.Memory.dValue = Buff[0]; 060. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] >= m_IndControl.Position) 061. { 062. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 063. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 064. if (m_IndControl.Memory._8b[def_IndexTimeFrame] == memTimeFrame) 065. { 066. memMode = m_IndControl.Mode; 067. return; 068. } 069. memTimeFrame = m_IndControl.Memory._8b[def_IndexTimeFrame]; 070. m_IndControl.Mode = memMode; 071. } 072. SendEventCustom(m_IndControl.Mode != C_Controls::ePlay ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY); 073. } 074. //+------------------------------------------------------------------+ 075. inline void UpdateIndicatorControl(void) 076. { 077. double Buff[]; 078. 079. if (m_IndControl.Handle == INVALID_HANDLE) return; 080. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 081. { 082. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 083. m_IndControl.Memory.dValue = Buff[0]; 084. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 085. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 086. { 087. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 088. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 089. m_IndControl.Memory._8b[7] = 'D'; 090. m_IndControl.Memory._8b[6] = 'M'; 091. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 092. } 093. } 094. //+------------------------------------------------------------------+ 095. void SweepAndCloseChart(void) 096. { 097. long id; 098. 099. if ((id = ChartFirst()) > 0) do 100. { 101. if (ChartSymbol(id) == def_SymbolReplay) 102. ChartClose(id); 103. }while ((id = ChartNext(id)) > 0); 104. } 105. //+------------------------------------------------------------------+ 106. inline int RateUpdate(bool bCheck) 107. { 108. static int st_Spread = 0; 109. 110. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 111. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 112. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 113. 114. return 0; 115. } 116. //+------------------------------------------------------------------+ 117. inline void CreateBarInReplay(bool bViewTick) 118. { 119. bool bNew; 120. double dSpread; 121. int iRand = rand(); 122. 123. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 124. { 125. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 126. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 127. { 128. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 129. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 130. { 131. m_Infos.tick[0].ask = m_Infos.tick[0].last; 132. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 133. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 134. { 135. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 136. m_Infos.tick[0].bid = m_Infos.tick[0].last; 137. } 138. } 139. if (bViewTick) 140. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 141. RateUpdate(true); 142. } 143. m_Infos.CountReplay++; 144. } 145. //+------------------------------------------------------------------+ 146. void AdjustViewDetails(void) 147. { 148. MqlRates rate[1]; 149. 150. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 151. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 152. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 153. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 154. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 155. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 156. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 157. if (rate[0].close > 0) 158. { 159. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 160. m_Infos.tick[0].last = rate[0].close; 161. else 162. { 163. m_Infos.tick[0].bid = rate[0].close; 164. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 165. } 166. m_Infos.tick[0].time = rate[0].time; 167. m_Infos.tick[0].time_msc = rate[0].time * 1000; 168. }else 169. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 170. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 171. } 172. //+------------------------------------------------------------------+ 173. void AdjustPositionToReplay(void) 174. { 175. int nPos, nCount; 176. 177. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 178. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 179. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 180. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 181. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 182. CreateBarInReplay(false); 183. } 184. //+------------------------------------------------------------------+ 185. void WaitIndicatorLoad(const string szArg, const bool ViewCtrl = true) 186. { 187. Print("Waiting for ", szArg); 188. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, szArg) == INVALID_HANDLE)) 189. { 190. if (ViewCtrl) CheckIndicatorControl(); 191. Sleep(100); 192. } 193. } 194. //+------------------------------------------------------------------+ 195. public : 196. //+------------------------------------------------------------------+ 197. C_Replay() 198. :C_ConfigService() 199. { 200. Print("************** Market Replay Service **************"); 201. srand(GetTickCount()); 202. SymbolSelect(def_SymbolReplay, false); 203. CustomSymbolDelete(def_SymbolReplay); 204. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 205. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 206. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 207. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 208. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 209. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 210. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 211. SymbolSelect(def_SymbolReplay, true); 212. m_Infos.CountReplay = 0; 213. m_IndControl.Handle = INVALID_HANDLE; 214. m_IndControl.Mode = C_Controls::ePause; 215. m_IndControl.Position = 0; 216. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 217. } 218. //+------------------------------------------------------------------+ 219. ~C_Replay() 220. { 221. SweepAndCloseChart(); 222. IndicatorRelease(m_IndControl.Handle); 223. SymbolSelect(def_SymbolReplay, false); 224. CustomSymbolDelete(def_SymbolReplay); 225. Print("Finished replay service..."); 226. } 227. //+------------------------------------------------------------------+ 228. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 229. { 230. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 231. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 232. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 233. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 234. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 235. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 236. SweepAndCloseChart(); 237. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 238. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 239. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 240. else 241. Print("Apply template: ", szNameTemplate, ".tpl"); 242. 243. return true; 244. } 245. //+------------------------------------------------------------------+ 246. bool InitBaseControl(const ushort wait = 1000) 247. { 248. int handle; 249. 250. Sleep(wait); 251. AdjustViewDetails(); 252. Print("Loading Control Indicator..."); 253. if ((handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 254. ChartIndicatorAdd(m_Infos.IdReplay, 0, handle); 255. IndicatorRelease(handle); 256. WaitIndicatorLoad("Market Replay Control", false); 257. SendEventCustom(); 258. WaitIndicatorLoad("Indicator Mouse Study"); 259. UpdateIndicatorControl(); 260. SendEventCustom(); 261. 262. return def_CheckLoopService; 263. } 264. //+------------------------------------------------------------------+ 265. bool LoopEventOnTime(void) 266. { 267. int iPos, iCycles; 268. MqlBookInfo book[1]; 269. ENUM_BOOK_TYPE typeMsg, memBook; 270. 271. book[0].price = 1.0; 272. book[0].volume = 1; 273. book[0].type = BOOK_TYPE_BUY_MARKET; 274. CustomBookAdd(def_SymbolReplay, book, 1); 275. SendEventCustom(); 276. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 277. { 278. UpdateIndicatorControl(); 279. CheckIndicatorControl(); 280. Sleep(200); 281. } 282. m_MemoryData = GetInfoTicks(); 283. AdjustPositionToReplay(); 284. iPos = iCycles = 0; 285. SendEventCustom(memBook = BOOK_TYPE_BUY); 286. book[0].type = BOOK_TYPE_BUY; 287. CustomBookAdd(def_SymbolReplay, book, 1); 288. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 289. { 290. if (m_IndControl.Mode == C_Controls::ePause) return true; 291. 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); 292. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 293. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != memBook) 294. SendEventCustom(memBook = typeMsg); 295. { 296. book[0].type = typeMsg; 297. CustomBookAdd(def_SymbolReplay, book, 1); 298. } 299. CreateBarInReplay(true); 300. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 301. { 302. Sleep(195); 303. iPos -= 200; 304. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 305. UpdateIndicatorControl(); 306. CheckIndicatorControl(); 307. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 308. } 309. } 310. 311. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 312. } 313. }; 314. //+------------------------------------------------------------------+ 315. #undef def_SymbolReplay 316. #undef def_CheckLoopService 317. #undef def_MaxSlider 318. //+------------------------------------------------------------------+
Исходный код файла C_Replay.mqh
Обратите внимание, что все и каждая выделенная строка должны быть удалены из кода. Но сначала давайте разберемся, почему так много строк было удалено.
Первым делом нам следует обратить внимание на строку 23. Как уже упоминалось в предыдущей теме,НЕЛЬЗЯ ИСПОЛЬЗОВАТЬ СТАТИЧЕСКИЙ HANDLE. Поэтому данное значение handle устарело, и поэтому, различные части кода, ссылавшиеся на него, были удалены. Кроме того, из кода была удалена целая процедура. Я имею в виду UpdateIndicatorControl, которая находилась между строками 75 и 93, поэтому любые ссылки на эту процедуру также были удалены. Это значит, что нам нужно каким-то образом заменить то, что ранее выполнялось посредством UpdateIndicatorControl.
Мы очень скоро дойдем до этого момента, а пока, прежде чем продолжить, давайте кратко рассмотрим две функции. Первая — InitBaseControl, которая начинается в строке 243. Обратите внимание, что теперь она содержит некоторые небольшие изменения. Целью данных изменений является улучшение пользовательского опыта при использовании приложения и стандартизация инициализации индикаторов. Давайте посмотрим, что здесь происходит.
Между строками 253 и 255 мы пытаемся загрузить индикатор управления. Теперь обратите внимание на такой факт: установка индикатора не происходит мгновенно. При ее выполнении наблюдается небольшая задержка. По этой причине, перед выполнением строки 257, мы должны убедиться, что индикатор управления уже находится на графике. Это делается с помощью вызова строки 253. Обратите внимание, что имена, указанные в строках 256 и 258, — это имена индикаторов, которые мы ожидаем загрузить. Эти вызовы выполняются на строке 185, и с этого момента все начинает становиться интересным. Так что будьте внимательны, иначе вы рискуете упустить важные детали.
Цикл в строке 188 будет ожидать загрузки указанного индикатора на график. Когда строка 256 требует ожидания загрузки индикатора управления, тест в строке 187 предотвращает проверку этого индикатора. Однако, когда строка 255 требует ожидания загрузки индикатора мыши, тест в строке 190 будет проверять буфер индикатора управления. Но почему так происходит? Потому что это дает возможность пользователю изменять таймфрейм. Чтобы лучше это понять, давайте рассмотрим строку 52.
В строке 52 находится процедура, которая реализует чтение буфера индикатора управления. А теперь самое интересное. Здесь у нас есть две статические переменные, объявленные в строках 54 и 55, но сейчас нас интересует только та, что в строке 54. Обратите внимание, что мы инициализируем ее с нуля. Если в строке 58, где мы пытаемся прочитать буфер индикатора управления, возвращаемое значение меньше нуля, это означает, что индикатор был удален с графика. Поскольку пользователь не может вернуть его на график, а это необходимо, мы закрываем график. Это приведет к автоматическому закрытию приложения. Вот почему мы не можем проверять индикатор управления во время ожидания его загрузки.
Итак, в строке 60 мы проверяем, больше ли значение индикатора управления, чем на анализируемой позиции в сервисе. Если это так, значит, нам следует быстро продвинуться вперед, когда пользователь нажимает кнопку play. Но что действительно интересно, это тест, который выполняется в строке 64. Этот тест — ахиллесова пята временной блокировки, о которой я рассказывал в предыдущей теме. Если пользователь не менял таймфрейм, этот тест будет верным, и в этом случае мы ничего не делаем.
Если же тест ложный, то в строке 69 мы сохраняем новый таймфрейм, а в строке 70 восстановим последнее состояние индикатора. Это потому что в строке 72 мы вызовем другую процедуру. Итак, теперь мы обращаемся к строке 37. И вот тут все становится по-настоящему интересным, поскольку именно здесь мы просим MetaTrader 5 запустить события на графике. Разделение этих двух действий обусловлено тем, что иногда мы хотим просто инициировать события, а иногда мы хотим фактически проверить и подтвердить, произошли ли какие-либо изменения в значениях индикатора управления.
Обратите внимание, что все содержимое процедуры SendEventCustom уже существовало в коде предыдущей версии. Поэтому я не вижу необходимости объяснять, что здесь происходит, плюс, сам код довольно прост и понятен. Полагаю, я довольно ясно объяснил, что мы начали реализовывать в коде, и нам осталось поговорить об изменениях, реализованных в функции LoopEventOnTime. Хоть изменения и не являются особенно радикальными и не меняют полностью способ выполнения функции, они делают более очевидной причину того, почему старая процедура UpdateIndicatorControl была разделена на две отдельные процедуры. Итак, перейдем к объяснению, которое будет кратким.
В строке 275 мы просим MetaTrader 5 отправить пользовательские события. Это позволит корректно настроить как индикатор мыши, так и индикатор управления, чтобы можно было начать использовать систему. Хотя эту строку 275 можно было бы отбросить при первом выполнении, после нажатия кнопки play в первый раз, нам нужно запустить ее снова, чтобы проверить данные, поскольку она будет выполняться снова, когда мы войдем в режим паузы.
В строке 279 мы не отправляем никаких событий, но нам нужно наблюдать за тем, что происходит в индикаторе управления. Как только пользователь нажмет кнопку воспроизведения, цикл на строке 276 закроется, и начнется процесс моделирования и репликации.
Далее, в строке 285 создается новый запрос на отправку пользовательских событий. В этом случае цель состоит в том, чтобы позволить индикатору мыши отображать оставшееся время до конца бара.
Еще одно небольшое различие появляется в строках 293 и 294, что довольно легко понять, поскольку их задача — изменить состояние индикатора мыши. Это говорит нам о том, вошел ли символ в аукцион или вышел из него.
Последнее отличие находится в строке 306, где мы проверяем, был ли изменен таймфрейм. Если это произойдет, индикаторы будут переустановлены, как я объяснил в начале этой темы.
Вот и все изменения, которые нужно было сделать. На следующем видео показано поведение системы во время изменения таймфрейма.
Заключение
В двух последних статьях я показал и объяснил, насколько важно практиковаться и стремиться использовать язык программирования до предела. Хотя многие могут подумать, что фактического обучения в этих статьях не произошло, не стоит этого делать. Уверен, что многие предполагали, что то, что я здесь объяснил, на самом деле невозможно или даже неосуществимо. Однако, прежде чем пытаться изменить основной код, я продемонстрировал, что необходимо сначала придумать гипотетическое решение, а затем создать максимально простой код для проверки выдвинутой гипотезы.
Главный урок заключается в том, чтобы никогда не сдаваться с первой попытки. Если что-то не сработало с первого раза, измените подход, но продолжайте проверять ту же гипотезу. Это именно то, что произошло здесь. Мне хотелось поместить в буфер индикатора какую-то информацию, которая четко и достоверно сообщала бы мне, когда изменился таймфрейм. Сделать это внутри графика было просто. Так что гипотеза была следующей: возможно ли сделать так, чтобы сервис обнаружил это изменение? Первая реализация не удалась, но нам все равно удалось прочитать значение, указанное в таймфрейме. Однако, это значение отображало лишь таймфрейм на момент первого размещения индикатора на графике.
Именно здесь зарождается концепция. Если бы я просто сдался, вместо того, чтобы попробовать новый подход, при котором хэндл получался бы при каждом вызове, я бы не заметил возможности доступа к реальным данным, которые изменялись индикатором при каждой итерации. Но, изменив фокус, мы открываем новую дверь, показывая, что мы можем выйти далеко за рамки того, что многие считают возможным.
Вот как все устроено: вы выдвигаете гипотезу, проверяете ее, и даже если первоначальные результаты не соответствуют вашим ожиданиям, но частично работают, вы корректируете подход, чтобы получить данные другим способом, но придерживаясь исходной гипотезы.
В приложении вы найдете исполняемые файлы, необходимые для использования системы репликации/моделирования. А следующей статье мы начнем изучать другие функции, которые необходимо будет добавить в наше приложение. Так что, до встречи в следующей статье серии!
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12363
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





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