Español Português
preview
Разработка системы репликации (Часть 68): Настройка времени (I)

Разработка системы репликации (Часть 68): Настройка времени (I)

MetaTrader 5Примеры | 19 февраля 2025, 08:42
333 0
Daniel Jose
Daniel Jose

Введение

В статье "Разработка системы репликации (Часть 66): Нажатие кнопки воспроизведения в сервисе (VII)", мы рассмотрели, как заставить указатель мыши сообщать нам, сколько времени осталось до окончания текущего бара и до начала построения нового бара на графике. Однако, несмотря на то что данный метод работает очень хорошо и достаточно эффективно, у него есть недостаток. Хотя многие думают, что проблема кроется в самом методе, дело заключается в ликвидности символа. Если вы внимательно читали объяснение в статье, то заметили, что указатель мыши обновляет информацию только при появлении новой котировки.

А всё потому что нам нужно, чтобы MetaTrader 5 сгенерировал вызов события OnCalculate, чтобы индикатор получил новое значение в секундах и смог рассчитать оставшееся время бара. Как вы уже поняли, это значение помещается в спред. Однако именно потому что мы полагаемся на значение, предоставляемое сервисом в спреде, мы не можем правильно сообщить оставшееся время бара на символе или в момент низкой ликвидности. И если символ войдет на торги, у нас возникнут большие проблемы, так как мы можем находиться в состоянии ожидания несколько минут. У нас нет возможности должным образом управлять ситуацией, связанной с торгом. Даже в символах с хорошей ликвидностью из-за торга не будет сгенерировано событие OnCalculate. Поэтому мы должны обновить сервис репликации/моделирования, чтобы пользователь получал правильную информацию в этих двух случаях. Первый - когда ликвидность низкая и в секунду генерируется мало сделок. Второй - когда символ выставляется на торги. Прежде чем переходить к следующему этапу разработки, нам нужно решить эти два вопроса.


Реализуем решение, когда ликвидность низкая

Первый случай, когда мы имеем низкую ликвидность, является относительно простым, так как необходимо лишь изменить код сервиса, по крайней мере, в общих чертах. Возможно, нужно изменить что-то еще. В любом случае, давайте попробуем решить данную проблему, работая только над исходным кодом сервиса репликации/моделирования.

Чтобы проверить, действительно ли это возможно, мы временно откажемся от нашего исходного кода. Мы создадим еще один, чтобы проверить, можно ли сделать так, чтобы при остановке сервиса указатель мыши обновлялся относительно оставшегося времени до закрытия текущего бара.

Код, который мы создадим, должен быть максимально простым, ничего слишком сложного или замысловатого. Идея заключается в том, чтобы протестировать и проверить, можем ли мы отправлять данные с сервера в индикатор, не изменяя информацию на графике. Поэтому ниже представляем исходный код, который будет использоваться:

01. //+------------------------------------------------------------------+
02. #property service
03. #property copyright "Daniel Jose"
04. #property description "Data synchronization demo service."
05. //+------------------------------------------------------------------+
06. #include <Market Replay\Defines.mqh>
07. //+------------------------------------------------------------------+
08. #define def_Loop ((!_StopFlag) && (ChartSymbol(id) != ""))
09. //+------------------------------------------------------------------+
10. void OnStart()
11. {
12.    long id;
13.    int handle;
14.    MqlRates Rate[1];
15.    
16.    Print("Starting Test Service...");
17.    SymbolSelect(def_SymbolReplay, false);
18.    CustomSymbolDelete(def_SymbolReplay);
19.    CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
20.    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0.5);
21.    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 5);
22.    Rate[0].close = 105;
23.    Rate[0].open = 100;
24.    Rate[0].high = 110;
25.    Rate[0].low = 95;
26.    Rate[0].tick_volume = 5;
27.    Rate[0].spread = 1;
28.    Rate[0].real_volume = 10;
29.    Rate[0].time = D'10.03.2023 09:00';
30.    CustomRatesUpdate(def_SymbolReplay, Rate, 1);
31.    Rate[0].time = D'10.03.2023 09:30';
32.    CustomRatesUpdate(def_SymbolReplay, Rate, 1);
33.    SymbolSelect(def_SymbolReplay, true);
34.    id = ChartOpen(def_SymbolReplay, PERIOD_M30);
35.    Sleep(1000);
36.    if ((handle = iCustom(NULL, 0, "\\Indicators\\Mouse Study.ex5")) != INVALID_HANDLE)
37.       ChartIndicatorAdd(id, 0, handle);
38.    IndicatorRelease(handle);
39.    Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS));
40.    while (def_Loop)
41.    {
42.       CustomRatesUpdate(def_SymbolReplay, Rate, 1);
43.       Sleep(250);
44.       Rate[0].spread++;
45.       if (Rate[0].spread == 60)
46.       {
47.          Rate[0].time += 60;
48.          Rate[0].spread = 0;
49.          Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS));
50.          EventChartCustom(id, evSetServerTime, (long)(Rate[0].time), 0, "");
51.       }
52.    }
53.    ChartClose(id);
54.    SymbolSelect(def_SymbolReplay, false);
55.    CustomSymbolDelete(def_SymbolReplay);
56.    Print("Finished Test Service...");   
57. }
58. //+------------------------------------------------------------------+

Исходный код тестового сервиса

Помните, что для правильного объяснения необходимо убедиться, что указатель мыши скомпилирован и находится в том месте, которое мы покажем во время объяснения. Иначе проверка будет провалена. Давайте теперь рассмотрим, что необходимо выполнить и как использовать это в своих интересах, если проверка окажется эффективной для того, что мы собираемся сделать.

В первых четырех строках содержатся объявления и свойства о типе генерируемого исполняемого файла. В шестой строке мы включаем заголовочный файл только для того, чтобы избежать необходимости объявления здесь дополнительных вещей. В восьмой строке мы определяем управляющую директиву для цикла, который мы выполним далее. В каком-то смысле единственная действительно важная строка среди этих восьми - вторая строка, остальное можно удалить, а нужные коды поместить в соответствующие места. Но во второй строке важно сообщить MetaTrader 5, что сгенерированный исполняемый файл должен быть сервисом. В противном случае это будет скрипт, а это не то, что мы хотим проверить.

С 10-й строки начинаются интересные события. Между строками 12 и 14 мы объявляем переменные, которые будем использовать. Затем, с строки 17 по 21, мы создаем пользовательский символ. Эти шаги стандартны, поэтому можно пойти немного дальше. Далее, между строками 22 и 28 мы создаем RATE для бара. Нас действительно интересует строка 29, где мы определяем, когда создается бар. В строке 30 мы сообщаем MetaTrader 5 данные этого бара. Теперь внимание! В строке 31 мы создадим второй бар с теми же данными, но на этот раз они будут сдвинуты на 30 минут. Причина сдвига на 30 минут заключается в том, что график будет открываться с 30-минутным таймфреймом. Если поместить другое значение времени на бар, который мы создаем в строке 31, указатель мыши будет вычислять, сколько времени осталось до его закрытия. Поэтому обратите внимание на эту деталь. Еще один момент, который следует иметь в виду: значения НЕ сообщаются в секундах.. Значение надо выражать в минутах, а в секундах никогда. Следующее действие, которое мы выполняем, - это создание бара в строке 32.

В строке 34 мы открываем график. Как было показано в предыдущей части этого цикла, прежде чем сервис добавит что-либо на график, будь то объекты или даже индикатор, нам нужно заставить сервис немного подождать. Это необходимо для того, чтобы платформа MetaTrader 5 в самом деле построила график и поместила его на экран. Это ожидание происходит в строке 35, где мы ждем секунду, прежде чем программа продолжит выполнение.

И снова нужно обратить внимание на новую информацию в строке 36. В данной строке мы пытаемся создать обработчик для размещения указателя мыши на графике. Функция iCustom возвращает значение, необходимое для добавления индикатора (если оно найдено). Если оно не найдено, мы получим недопустимое значение, которое не будем использовать. Поэтому указатель мыши обязательно должен находиться в назначенном месте. Иначе проверка провалится. Если нам удастся найти индикатор, мы добавим его на график в строке 37, поэтому мы удалим его в строке 38, так как обработчик, возвращенный вызовом iCustom, больше не понадобится.

Теперь наступает время тестирования. В действительности нас интересует следующее: проверить будет ли оно работать или нет. Если сработает, то мы сможем решить проблему в случае с символами с низкой ликвидностью. Если не сработает, придется искать другое решение. Давайте разберемся, как выполняется данная проверка, которая находится в цикле, начинающемся в строке 40 и заканчивающемся в строке 52. Остальная часть кода (между строками 53 и 56) предназначена только для завершения проверки, поэтому не имеет отношения к данной статье. Давайте теперь посмотрим, как работает тест.


Как работает проверка

Все начинается со строки 40, где осуществляется вход в цикл. Если график открыт и пользователь не остановил сервис, цикл будет выполняться бесконечно. Внутри увидим строку 42. Данная строка принудительно обновит данные бара. Но следует учесть, что в бар не будет передана никакая новая информация, только то, что мы изменяем в этой проверке. Всё в порядке. В строке 43 мы создаем небольшую задержку, чтобы обновление не было слишком быстрым. Прошу заметить величину задержки: мы не ждем даже одной секунды. Значение составляет четверть секунды, что будет важно позже, когда будем просматривать результаты в MetaTrader 5.

В строке 44 мы увеличиваем значение спреда. В результате у указателя мыши будет создано впечатление, что прошла одна секунда, в то время как на самом деле время, прошедшее за счет используемой нами задержки, гораздо меньше. До этого момента всё должно происходить следующим образом: когда открывается график и отображается указатель мыши, MetaTrader 5 должен генерировать события OnCalculate, которые должны уменьшить время, оставшееся до закрытия бара. Можно проверить, происходит ли это. Однако, поскольку я уже видел, как это происходит, у меня хватило терпения и любопытства выяснить, что произойдет, когда значение спреда превысит значение 60. Почему для нас важно именно значение 60? Причина в том, что при достижении данного значения должен появиться новый одноминутный бар, даже если используется график с месячным таймфреймом. Это не имеет значения. Для правильной индикации необходимо, чтобы MetaTrader 5 получил информацию о появлении нового минутного бара.

Но мы должны быть уверены, что наш код справится с этим. Иначе MetaTrader 5 не сможет самостоятельно решить подобную проблему. Поэтому в строке 45 мы проверяем значение через 60 итераций цикла. Когда это значение достигнуто, в строке 47 мы добавляем одну минуту (60 секунд) к исходному времени бара. Таким образом, когда функция в строке 42 выполнится, MetaTrader 5 узнает о существовании нового одноминутного бара. Однако этого не совсем достаточно. В строке 48 мы должны сбрасывать счетчик итераций, чтобы новый цикл был проанализирован снова. Чтобы проверить это, в строке 49 мы выводим значение, которое мы сообщили в MetaTrader 5.

Теперь один важный момент: пока о существовании этого нового бара известно только MetaTrader 5. Однако, как вы могли заметить в статье, где мы реализовали систему, которая сообщает нам об оставшемся времени бара, мы не можем напрямую использовать значение времени, предоставляемое функцией OnCalculate. По крайней мере на данный момент. Я думаю о способе преодоления этого ограничения, чтобы строку 50 не нужно было выполнять. Но пока мы не преодолеем это ограничение, нам нужна строка 50, чтобы заставить MetaTrader 5 генерировать пользовательское событие. Это сообщит указателю мыши о том, что сформировался новый бар. Таким образом, индикатор будет знать, в какой временной точке мы работаем, и сможет сообщить нам о времени, которое останется до закрытия текущего бара и до начала нового.

Если не хотите компилировать данный сервис и проверять его в MetaTrader 5, не беда. В видео ниже я показываю, как вел себя сервис и можно ли использовать данный тестовый код в сервисе репликации/моделирования. С нашей точки зрения, проверка прошла очень хорошо, продемонстрировав возможность и целесообразность включения протестированного механизма в приложение для репликации/моделирования. Это позволит нам в моменты с низкой ликвидностью или на символах с низкой ликвидностью иметь информацию о том, сколько времени осталось до появления нового бара на графике.


Чтобы добиться этого, добавление протестированного механизма в приложение репликации/моделирования потребует небольшого изменения в очень специфической части кода. Эта часть находится в заголовочном файле C_Replay.mqh. Но прежде чем приступать к реализации модификаций, важно понять еще один момент. Чтобы разделить объяснения, перейдем к новой теме.


Время, время, время. Как мы можем понять время

Хотя выполнение некоторых заданий может показаться чем-то запутанным, у нас нет другого выхода: придется показать нечто такое, что может быть очень сложным, но вполне возможным и разумным для выполнения. И всё это связано с длиной данных.

Если посмотреть на функцию OnCalculate, то можно увидеть, что значение спреда задается в виде целого числа, что означает, что на 64-битных процессорах используется 32 бита. Проведя простую проверку, также видно, что длина бита datetime равна 64 битам. Всё в порядке. Хорошо, но как эти данные могут нам помочь? Спокойно, дорогие читатели. Спокойно. Теперь нам нужно произвести некоторые расчеты, но они несложные, это на самом деле очень просто.

Необходимо, чтобы сервис заставлял MetaTrader 5 генерировать какое-либо событие для обновления таймера указателя мыши. Это факт, и этого мы добиваемся двумя разными способами. Один - через вызов OnCalculate, а другой - через пользовательское событие, которое запускается время от времени. Чтобы увидеть, где это происходит в сервисе, давайте проверим следующий фрагмент из заголовочного файла C_Replay.mqh:

068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074. 
075.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
076.             {
077.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
078.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
079.                {                  
080.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
081.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
082.                   {
083.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
084.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
085.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
086.                   {
087.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
088.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
089.                   }
090.                }
091.                if (bViewTick)
092.                {
093.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
094.                   if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, "");
095.                }
096.                m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time);
097.                CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
098.             }
099.             m_Infos.CountReplay++;
100.          }
101. //+------------------------------------------------------------------+

Фрагмент из файла C_Replay.mqh

Об этом мы уже рассказывали в статье "Разработка системы репликации (Часть 66): Нажатием кнопки воспроизведения в сервисе (VII)", мы подкрепим объяснение. При каждом вызове данной процедуры и добавлении нового тика к бару, который строится на графике, в строку 96 включается значение спреда в секундах. Это позволяет рассчитать оставшееся время работы бара. Хорошо. И каждый раз, когда появляется новый одноминутный бар, в строке 94 выполняется проверка, которая оказывается истинной, заставляя MetaTrader 5 генерировать пользовательское событие. Это событие позволяет указателю мыши сбросить время, чтобы определить, когда был создан новый бар. Мы делаем это, чтобы не добавлять дополнительный вызов, который будет интенсивно выполняться при наведении курсора мыши. Я объяснил это в вышеупомянутой статье.

В любом случае, делая всё таким образом, мы теряем время и пространство. Мы могли бы быть немного эффективнее и передать больше информации, чем при нынешнем способе. Теперь наступает время расчетов. Нам нужно синхронизировать события каждую секунду; быстрее нельзя. У нас уже есть отправная точка: в минуте 60 секунд, в часе 60 минут, а в сутках 24 часа. Всё в порядке. Если посчитать, то получается 86 400 секунд в день (приблизительно, поскольку в сутках не всегда ровно 24 часа). Однако для наших целей мы будем считать, что в одних сутках 24 часа. Хорошо. 32-битное значение может содержать максимум 4 294 967 295, если оно беззнаковое. Снова предположим, что мы имеем дело не со знаковым значением, а это означает, что в 32 битах мы можем хранить примерно 49 дней и 12 часов в пересчете на секунды. Не упускайте этот факт. В одном значении спреда, размер которого составляет 32 бита, можно хранить примерно 49 дней. Однако наше приложение для репликации/моделирования вряд ли будет использоваться для анализов и исследований продолжительностью более одного-двух дней подряд. Таким образом, мы можем идеально синхронизировать данные, используя только информацию, которая содержится в спреде, без необходимости заставлять MetaTrader 5 делать пользовательский вызов, чтобы сообщить указателю мыши о появлении нового бара.

Теперь всё стало интересно, не так ли? Но чтобы было еще интереснее, мы реализуем более амбициозную модель, чтобы у нас не было ограничения в 49 дней. Хотя я сомневаюсь, что кто-то достигнет этого предела, я всё равно реализую кое-что другое. Мы будем использовать биты, так сказать, более контролируемым образом. Однако для этого нам придется внести некоторые изменения в указатель мыши. Ничего сложного, учитывая, чего мы можем добиться с помощью этих изменений.

Но даст ли нам это то решение, которое мы собираемся реализовать? Чтобы выяснить, так ли это, нам нужно рассмотреть другой фрагмент, который также принадлежит заголовочному файлу C_Replay.mqh. Это можно увидеть ниже:

207. //+------------------------------------------------------------------+
208.       bool LoopEventOnTime(void)
209.          {         
210.             int iPos;
211. 
212.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
213.             {
214.                UpdateIndicatorControl();
215.                Sleep(200);
216.             }
217.             m_MemoryData = GetInfoTicks();
218.             AdjustPositionToReplay();
219.             EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, "");
220.             iPos = 0;
221.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
222.             {
223.                if (m_IndControl.Mode == C_Controls::ePause) return true;
224.                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);
225.                CreateBarInReplay(true);
226.                while ((iPos > 200) && (def_CheckLoopService))
227.                {
228.                   Sleep(195);
229.                   iPos -= 200;
230.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
231.                   UpdateIndicatorControl();
232.                }
233.             }
234. 
235.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
236.          }
237. };
238. //+------------------------------------------------------------------+

Фрагмент из файла C_Replay.mqh

Хорошо, наша проблема возникает, когда символ менее ликвиден. Иными словами, тики не генерируются непрерывно каждую секунду. Бывают случаи, когда ликвидность позволяет сгенерировать один тик в секунду, что желательно для наших целей. В другое время, однако, такой ликвидности достичь не получается. Тем не менее, можно увидеть, что в строке 225 происходит вызов фрагмента, который был показан выше. Даже если время между тиками превысит одну секунду, вызов в строке 225 всё равно произойдет. Так что да, использование спреда в качестве средства сообщения времени вполне осуществимо. Однако реализация всех связанных с этим аспектов может оказаться довольно сложной. Поэтому в данной статье невозможно рассказать обо всех деталях. Но начнем мы с этого.


Вступаем в новую фазу

Первое, что нужно сделать, это изменить заголовочный файл Defines.mqh. Модификации можно увидеть ниже:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_VERSION_DEBUG
05. //+------------------------------------------------------------------+
06. #ifdef def_VERSION_DEBUG
07.    #define macro_DEBUG_MODE(A) \
08.                Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A));
09. #else
10.    #define macro_DEBUG_MODE(A)
11. #endif
12. //+------------------------------------------------------------------+
13. #define def_SymbolReplay      "RePlay"
14. #define def_MaxPosSlider      400
15. #define def_MaskTimeService   0xFED00000
16. //+------------------------------------------------------------------+
17. union uCast_Double
18. {
19.    double   dValue;
20.    long     _long;                                  // 1 Information
21.    datetime _datetime;                              // 1 Information
22.    uint     _32b[sizeof(double) / sizeof(uint)];    // 2 Informations
23.    ushort   _16b[sizeof(double) / sizeof(ushort)];  // 4 Informations
24.    uchar    _8b [sizeof(double) / sizeof(uchar)];   // 8 Informations
25. };
26. //+------------------------------------------------------------------+
27. enum EnumEvents    {
28.          evHideMouse,               //Hide mouse price line
29.          evShowMouse,               //Show mouse price line
30.          evHideBarTime,             //Hide bar time
31.          evShowBarTime,             //Show bar time
32.          evHideDailyVar,            //Hide daily variation
33.          evShowDailyVar,            //Show daily variation
34.          evHidePriceVar,            //Hide instantaneous variation
35.          evShowPriceVar,            //Show instantaneous variation
36.          evSetServerTime,           //Replay/simulation system timer
37.          evCtrlReplayInit,          //Initialize replay control
38.                   };
39. //+------------------------------------------------------------------+

Исходный код файла Defines.mqh

Обратите внимание, что строка 36 зачеркнута, а это означает, что ее больше не существует в конечном файле. Таким же образом, теперь в строке 15 у нас есть новое определение, которое будет работать как маска, чтобы нам было легче действовать. То что строка 36, которая являлась событием, отвечающим за обновление таймера, была удалена, означает, что мы внесем некоторые изменения, чтобы сделать новый код совместимым с новой политикой сообщений.

Подобно тому, как это было сделано с файлом Defines.mqh, мы также внесем некоторые изменения в другой заголовочный файл - файл Macros.mqh. Новый файл можно увидеть ниже:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define macroRemoveSec(A) (A - (A % 60))
05. #define macroGetDate(A)   (A - (A % 86400))
06. #define macroGetSec(A)    (A - (A - (A % 60)))
07. #define macroGetTime(A)   (A % 86400)
08. //+------------------------------------------------------------------+
09. #define macroColorRGBA(A, B) ((uint)((B << 24) | (A & 0x00FF00) | ((A & 0xFF0000) >> 16) | ((A & 0x0000FF) << 16)))
10. #define macroTransparency(A) (((A > 100 ? 100 : (100 - A)) * 2.55) / 255.0)
11. //+------------------------------------------------------------------+

Исходный код файла Macros.mqh

В данном случае мы добавляем только строку 07, которая представляет собой макрос, предназначенный для извлечения значения времени из структуры переменной datetime. Это очень просто, так что мы можем продолжить анализ другого заголовочного файла. Мы имеем в виду файл C_Stuty.mqh, который можно просмотреть в полном объеме ниже: Этот файл является частью указателя мыши.

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "..\C_Mouse.mqh"
005. //+------------------------------------------------------------------+
006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_"
007. //+------------------------------------------------------------------+
008. class C_Study : public C_Mouse
009. {
010.    private   :
011. //+------------------------------------------------------------------+
012.       struct st00
013.       {
014.          eStatusMarket  Status;
015.          MqlRates       Rate;
016.          string         szInfo,
017.                         szBtn1,
018.                         szBtn2,
019.                         szBtn3;
020.          color          corP,
021.                         corN;
022.          int            HeightText;
023.          bool           bvT, bvD, bvP;
024.          datetime       TimeDevice;
025.       }m_Info;
026. //+------------------------------------------------------------------+
027.       void Draw(void)
028.          {
029.             double v1;
030.             
031.             if (m_Info.bvT)
032.             {
033.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18);
034.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo);
035.             }
036.             if (m_Info.bvD)
037.             {
038.                v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2);
039.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1);
040.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP));
041.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
042.             }
043.             if (m_Info.bvP)
044.             {
045.                v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2);
046.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1);
047.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP));
048.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
049.             }
050.          }
051. //+------------------------------------------------------------------+
052. inline void CreateObjInfo(EnumEvents arg)
053.          {
054.             switch (arg)
055.             {
056.                case evShowBarTime:
057.                   C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise);
058.                   m_Info.bvT = true;
059.                   break;
060.                case evShowDailyVar:
061.                   C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0)));
062.                   m_Info.bvD = true;
063.                   break;
064.                case evShowPriceVar:
065.                   C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0)));
066.                   m_Info.bvP = true;
067.                   break;
068.             }
069.          }
070. //+------------------------------------------------------------------+
071. inline void RemoveObjInfo(EnumEvents arg)
072.          {
073.             string sz;
074.             
075.             switch (arg)
076.             {
077.                case evHideBarTime:
078.                   sz = m_Info.szBtn1;
079.                   m_Info.bvT = false;
080.                   break;
081.                case evHideDailyVar:
082.                   sz = m_Info.szBtn2;
083.                   m_Info.bvD   = false;
084.                   break;
085.                case evHidePriceVar:
086.                   sz = m_Info.szBtn3;
087.                   m_Info.bvP = false;
088.                   break;
089.             }
090.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
091.             ObjectDelete(GetInfoTerminal().ID, sz);
092.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
093.          }
094. //+------------------------------------------------------------------+
095.    public   :
096. //+------------------------------------------------------------------+
097.       C_Study(long IdParam, string szShortName, color corH, color corP, color corN)
098.          :C_Mouse(IdParam, szShortName, corH, corP, corN)
099.          {
100.             if (_LastError != ERR_SUCCESS) return;
101.             ZeroMemory(m_Info);
102.             m_Info.Status = eCloseMarket;
103.             m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1));
104.             m_Info.corP = corP;
105.             m_Info.corN = corN;
106.             CreateObjInfo(evShowBarTime);
107.             CreateObjInfo(evShowDailyVar);
108.             CreateObjInfo(evShowPriceVar);
109.          }
110. //+------------------------------------------------------------------+
111.       void Update(const eStatusMarket arg)
112.          {
113.             int i0;
114.             datetime dt;
115.                      
116.             switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
117.             {
118.                case eCloseMarket   :
119.                   m_Info.szInfo = "Closed Market";
120.                   break;
121.                case eInReplay      :
122.                case eInTrading   :
123.                   i0 = PeriodSeconds();
124.                  dt = (m_Info.Status == eInReplay ? (datetime) m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent());
125.                   dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent());
126.                   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
127.                   if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS);
128.                   break;
129.                case eAuction      :
130.                   m_Info.szInfo = "Auction";
131.                   break;
132.                default            :
133.                   m_Info.szInfo = "ERROR";
134.             }
135.             Draw();
136.          }
137. //+------------------------------------------------------------------+
138. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
139.          {
140.             C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
141.             switch (id)
142.             {
143.                case CHARTEVENT_CUSTOM + evHideBarTime:
144.                   RemoveObjInfo(evHideBarTime);
145.                   break;
146.                case CHARTEVENT_CUSTOM + evShowBarTime:
147.                   CreateObjInfo(evShowBarTime);
148.                   break;
149.                case CHARTEVENT_CUSTOM + evHideDailyVar:
150.                   RemoveObjInfo(evHideDailyVar);
151.                   break;
152.                case CHARTEVENT_CUSTOM + evShowDailyVar:
153.                   CreateObjInfo(evShowDailyVar);
154.                   break;
155.                case CHARTEVENT_CUSTOM + evHidePriceVar:
156.                   RemoveObjInfo(evHidePriceVar);
157.                   break;
158.                case CHARTEVENT_CUSTOM + evShowPriceVar:
159.                   CreateObjInfo(evShowPriceVar);
160.                   break;
161.                case (CHARTEVENT_CUSTOM + evSetServerTime):
162.                   m_Info.TimeDevice = (datetime)lparam;
163.                   break;
164.                case CHARTEVENT_MOUSE_MOVE:
165.                   Draw();
166.                   break;
167.             }
168.             ChartRedraw(GetInfoTerminal().ID);
169.          }
170. //+------------------------------------------------------------------+
171. };
172. //+------------------------------------------------------------------+
173. #undef def_ExpansionPrefix
174. #undef def_MousePrefixName
175. //+------------------------------------------------------------------+

Исходный код файла C_Study.mqh

Все зачеркнутые строки в этом файле должны быть удалены из исходного файла. Хотя зачеркнутые строки были частью старого кода, который отвечал за то, чтобы пользовательское событие поддерживало синхронизацию таймера индикатора, в этом больше не будет необходимости, так как теперь мы будем использовать другой способ для выполнения этой работы. Однако важно отметить один момент в этом заголовочном файле. Посмотрите на строку 124 и сравните ее со строкой 125. Хотя указано, что вся строка была заменена, на самом деле произошло удаление переменной m_Info.TimeDevice. Это связано с тем, что она уже не нужна, так как она использовалась для синхронизации секундомера. Теперь у нас возникла проблема, скажем так, в коде указателя мыши. Полный код индикатора можно увидеть ниже:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "This is an indicator for graphical studies using the mouse."
04. #property description "This is an integral part of the Replay / Simulator system."
05. #property description "However it can be used in the real market."
06. #property version "1.68"
07. #property icon "/Images/Market Replay/Icons/Indicators.ico"
08. #property link "https://www.mql5.com/pt/articles/"
09. #property indicator_chart_window
10. #property indicator_plots 0
11. #property indicator_buffers 1
12. #property indicator_applied_price PRICE_CLOSE
13. //+------------------------------------------------------------------+
14. double GL_PriceClose;
15. datetime GL_TimeAdjust;
16. //+------------------------------------------------------------------+
17. #include <Market Replay\Auxiliar\Study\C_Study.mqh>
18. //+------------------------------------------------------------------+
19. C_Study *Study       = NULL;
20. //+------------------------------------------------------------------+
21. input color user02   = clrBlack;                         //Price Line
22. input color user03   = clrPaleGreen;                     //Positive Study
23. input color user04   = clrLightCoral;                    //Negative Study
24. //+------------------------------------------------------------------+
25. C_Study::eStatusMarket m_Status;
26. int m_posBuff = 0;
27. double m_Buff[];
28. //+------------------------------------------------------------------+
29. int OnInit()
30. {
31.    ResetLastError();
32.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
33.    if (_LastError != ERR_SUCCESS) return INIT_FAILED;
34.    if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
35.    {
36.       MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
37.       OnBookEvent((*Study).GetInfoTerminal().szSymbol);
38.       m_Status = C_Study::eCloseMarket;
39.    }else
40.       m_Status = C_Study::eInReplay;
41.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
42.    ArrayInitialize(m_Buff, EMPTY_VALUE);
43.    
44.    return INIT_SUCCEEDED;
45. }
46. //+------------------------------------------------------------------+
47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[],
48.                 const double& high[], const double& low[], const double& close[], const long& tick_volume[], 
49.                const long& volume[], const int& spread[]) 
50. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[])
51. {
52.    GL_PriceClose = close[rates_total - 1];
53.    GL_PriceClose = price[rates_total - 1];
54.    GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0);
55.    if (_Symbol == def_SymbolReplay)
56.       GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService);
57.    m_posBuff = rates_total;
58.    (*Study).Update(m_Status);   
59.    
60.    return rates_total;
61. }
62. //+------------------------------------------------------------------+
63. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
64. {
65.    (*Study).DispatchMessage(id, lparam, dparam, sparam);
66.    (*Study).SetBuffer(m_posBuff, m_Buff);
67.    
68.    ChartRedraw((*Study).GetInfoTerminal().ID);
69. }
70. //+------------------------------------------------------------------+
71. void OnBookEvent(const string &symbol)
72. {
73.    MqlBookInfo book[];
74.    C_Study::eStatusMarket loc = m_Status;
75.    
76.    if (symbol != (*Study).GetInfoTerminal().szSymbol) return;
77.    MarketBookGet((*Study).GetInfoTerminal().szSymbol, book);
78.    m_Status = (ArraySize(book) == 0 ? C_Study::eCloseMarket : C_Study::eInTrading);
79.    for (int c0 = 0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++)
80.       if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction;
81.    if (loc != m_Status) (*Study).Update(m_Status);
82. }
83. //+------------------------------------------------------------------+
84. void OnDeinit(const int reason)
85. {
86.    if (reason != REASON_INITFAILED)
87.    {
88.       if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
89.          MarketBookRelease((*Study).GetInfoTerminal().szSymbol);
90.    }
91.    delete Study;
92. }
93. //+------------------------------------------------------------------+

Исходный код указателя мыши

В определенные моменты данный код вел себя так, что вызывал у меня недовольство во время тестирования, поскольку некоторые аспекты не работали должным образом. Я также пробовал и другие способы, но об этом я расскажу позже. Некоторые ситуации не имеют смысла: они должны работать, но по какой-то непонятной нам причине не работают. Однако я оставлю данные объяснения на другой раз, когда буду более спокоен. Здесь я хотел бы обратить ваше внимание на некоторые необходимые изменения.

Давайте начнем с конца, чтобы понять начало. Обратите внимание, что строки между позициями 47 и 49 были зачеркнуты и заменены строкой 50, а это означает, что обработчик события OnCalculate больше не будет получать все эти параметры. Возможно, вы зададитесь вопросом, почему я изменил тактику. Это связано с моими словами: не стоит делать то, что я делаю сейчас. Я постараюсь объяснить причину данного изменения, но не здесь, а когда объясню код сервиса. Прошу заметить, что строка 52 была заменена на строку 53, а строка 54 - на строки 55 и 56. Помимо этих изменений, есть еще одно важное: появление строки 12. Но прежде чем говорить о строке 12, давайте посмотрим, что происходит в строках 55 и 56.

Когда сервис обновляет RATE (позже увидим, как это делается), MetaTrader 5 запускает событие Calculate, которое обрабатывается в функции OnCalculate. Всё в порядке. Хорошо. Однако если таймфрейм символа отличается от минутного, значение rates_total НЕ БУДЕТ ОБНОВЛЯТЬСЯ до появления нового бара. Поскольку спред будет использоваться только в режиме репликации/моделирования, мы проверяем, соответствует ли символ ожидаемому. В этом случае нам нужно будет захватить значение спреда с помощью функции iSpread. Причину объясним в следующей статье, как и остальную часть вычислений в строке 56, поскольку на данном этапе она может не иметь смысла.

Однако, поскольку для получения правильного значения мы будем использовать функцию библиотеки iSpread, нет необходимости использовать функцию OnCalculate со всеми вышеперечисленными параметрами. Следовательно, строка 53 будет непосредственно отражать цену. Но теперь возникает вопрос: по какой цене? Потому что есть разные цены, которые мы можем использовать. Здесь в дело вступает строка 12. В этой строке в качестве свойства индикатора объявляется, какую именно цену мы будем использовать. В этом месте мы определяем цену как цену закрытия бара. Таким образом, мы сохраняем совместимость указателя мыши со значениями, которые ранее работали на графике.


Заключительные идеи

В этой статье мы затронули некоторые нерешенные вопросы, которые будут рассмотрены в следующих статьях. Здесь есть несколько аспектов, которые могут не иметь смысла, если мы сосредоточимся только на том, что представлено в этой статье. Среди них - причина, по которой мы используем функцию iSpread, а не получаем значение спреда непосредственно из вызова OnCalculate, поскольку последний получает значение спреда из MetaTrader 5. Кроме того, есть еще один вопрос, который еще не был показан и объяснен: что происходит, когда символ выставляется на торги. Эта тема очень своеобразна, если мы используем пользовательский символ. Это связано не с тем, что указатель мыши не может показать, что символ находится в торгах, но по другим причинам. 

Способ достижения данной цели уже существует и работает. Достаточно взглянуть на код функции OnBookEvent указателя мыши. Как видно, достаточно сообщить значения BOOK_TYPE_BUY_MARKET или BOOK_TYPE_SELL_MARKET, чтобы указатель мыши показал, что символ находится на торгах. Впрочем, как это сделать, я расскажу в другой раз. Пока что мы оставим это здесь и увидимся в следующей статье.

Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12309

Прикрепленные файлы |
Anexo.zip (420.65 KB)
Интеграция MQL5: Python Интеграция MQL5: Python
Python — известный и популярный язык программирования со множеством функций, особенно в областях финансов, науки о данных, искусственного интеллекта и машинного обучения. Python — мощный инструмент, который может быть полезен и в трейдинге. MQL5 позволяет нам использовать этот мощный язык для эффективного достижения наших целей. В этой статье мы рассмотрим некоторые базовые сведения о Python и расскажем, как его можно интегрировать в MQL5.
Построение модели для ограничения диапазона сигналов по тренду (Часть 8): Разработка советника (I) Построение модели для ограничения диапазона сигналов по тренду (Часть 8): Разработка советника (I)
В этой статье мы разработаем наш первый советник на MQL5 на основе индикатора, который мы создали в предыдущей статье. Мы рассмотрим все функции, необходимые для автоматизации процесса, включая управление рисками. Это позволит перейти от ручного выполнения сделок к автоматизированным системам.
Нейросети в трейдинге: Гибридные модели последовательностей графов (GSM++) Нейросети в трейдинге: Гибридные модели последовательностей графов (GSM++)
Гибридные модели последовательностей графов (GSM++) объединяют сильные стороны различных архитектур, обеспечивая высокую точность анализа данных и оптимизацию вычислительных затрат. Эти модели эффективно адаптируются к динамическим рыночным данным, улучшая представление и обработку финансовой информации.
Нейросети в трейдинге: Двухмерные модели пространства связей (Окончание) Нейросети в трейдинге: Двухмерные модели пространства связей (Окончание)
Продолжаем знакомство с инновационным фреймворком Chimera — двухмерной моделью пространства состояний, использующей нейросетевые технологии для анализа многомерных временных рядов. Этот метод обеспечивает высокую точность прогнозирования при низких вычислительных затратах.