English Español Deutsch 日本語 Português
preview
Разработка системы репликации (Часть 31): Проект советника — класс C_Mouse (V)

Разработка системы репликации (Часть 31): Проект советника — класс C_Mouse (V)

MetaTrader 5Тестер | 5 марта 2024, 16:00
456 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье Разработка системы репликации (Часть 30): Проект советника — класс C_Mouse (IV), мы посмотрели, как можно изменить, добавить или адаптировать под свой стиль систему классов, чтобы протестировать новый ресурс или модель. Таким образом, мы избежим зависимостей в основном коде, сохраняя его всегда надежным, стабильным и прочным, поскольку любые новые ресурсы будут помещены в основную систему только после того, как создаваемая вами модель будет полностью подходящей. Основная проблема при разработке системы репликации/моделирования, и, возможно, именно она делает эту работу такой сложной, заключается в создании механизмов, максимально приближенных, если не идентичных, к той системе, которую мы будем использовать, когда будем торговать на реальном счете. Нет смысла в создании системы для создания или запуска репликации/моделирования, если в момент использования реальной учетной записи те же ресурсы будут отсутствовать.

Глядя на систему класса C_Mouse и классы исследования, показанные в предыдущих статьях, можно заметить, что при использовании на реальном рынке, будь то демо или реальный счет, таймер всегда будет сообщать вам, когда начнется следующий бар. Но, используя систему репликации/моделирования, мы не рассчитываем на это. В итоге получается сообщение. На первый взгляд может показаться, что такое нарушение симметрии не имеет особого значения. Но если мы позволим бесполезным вещам накапливаться, не исправляя и не удаляя их, то в итоге получим груду совершенно бесполезного хлама, который будет только мешать решению тех вопросов, которые нам действительно нужно решить. Разрабатывать способ установки таймера необходимо таким образом, чтобы во время репликации/моделирования он мог сообщить нам, сколько времени осталось, что может показаться на первый взгляд простым и быстрым решением. Многие просто пытаются приспособиться и использовать ту же систему, что и в случае с торговым сервером. Но есть один момент, который многие не учитывают, когда думают о таком решении: при репликации, и это не говоря уже о моделировании, часы работают по-другому. Это происходит по нескольким причинам:

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

Чтобы дать вам представление о том, с чем мы имеем дело на самом деле и насколько сложной может быть установка таймера в системе репликации/моделирования, давайте посмотрим на рисунок 01.

Рисунок 01

Рисунок 01 - Таймер на реальном рынке

На этом рисунке 01 мы видим, как работает таймер, указывающий, когда новый бар появится на графике . Это короткий пересказ. Обратите внимание, что время от времени у нас будет возникать событие OnTime. Это вызовет событие Update, которое обновит значение таймера. Таким образом, мы можем представить, сколько времени осталось до появления нового бара. Однако, для того чтобы функция Update знала, какое значение будет представлено, она запрашивает у функции GetBarTime, сколько времени еще потребуется для появления бара. А GetBarTime будет использовать функцию TimeCurrent, которая не выполняется на сервере, но на самом деле фиксирует локальное время на нем. Таким образом, мы можем узнать, сколько времени прошло с момента срабатывания сервера на последнем баре, и на основе данного значения рассчитать, сколько времени осталось до срабатывания нового бара. Это самая простая часть всей этой истории, поскольку нам не нужно беспокоиться о том, приостановилась ли система или прошло определенное время. Это произойдет, если мы пользуемся активом, данные которого поступают непосредственно с торгового сервера. Когда мы работаем с репликацией/моделированием, всё становится гораздо сложнее. Большая проблема заключается не в том, что мы имеем дело с репликацией/моделированием. Проблема заключается в том, чтобы придумать способ, с помощью которого при репликации/моделировании можно обойти вызов TimeCurrent, так как именно в данный момент и возникает вся проблема. Но это надо сделать с минимальными изменениями. Мы просто хотим обойти систему вызова TimeCurrent. Однако при работе с сервером мы хотим, чтобы система работала так, как показано на рисунке 01.

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


Планирование

Планирование того, как это сделать, - пожалуй, самая простая часть материала, поскольку мы просто отправим системе значение времени, вычисленное сервисом репликации/моделирования. Это самая простая часть. Просто используем для этого глобальную переменную терминала, и всё готово. Мы уже рассмотрели в этой серии статей, как мы используем данные переменные. В одной из статей мы ввели элемент управления, чтобы иметь возможность сообщить сервису, что пользователь хочет сделать. Многие думают, что с помощью этих переменных можно передавать только данные типа double, забывая об одном факте: двоичные числа- это просто двоичные числа, то есть, они ни в ком случае не представляют никакой информации, кроме нулей и единиц. Таким образом, мы можем передавать любую информацию через эти глобальные переменные терминала. Конечно, при условии, что нам удастся расположить биты логическим образом, чтобы впоследствии можно было восстановить информацию. К счастью, тип DateTime, вопреки мнению многих, на самом деле является значением типа ulong. Мы используем 64 бита, то есть для размещения значения DateTime требуется 8 байт информации, где мы будем иметь представление полной даты и времени, включая год, месяц, день, час, минуту и секунды. Это всё, что нам нужно, чтобы обойти вызов TimeCurrent, поскольку он использует и возвращает то же самое значение в 8 байтах. Поскольку тип Double использует ровно 64 бита для передачи информации внутри платформы,он и будет нашим решением.

Но всё не так просто. У нас есть несколько значительных проблем, которые будут лучше понятны во время объяснения того, как реализуется данная система. Несмотря на то, что сервис довольно прост и легок в построении, в нем есть небольшие сложности, связанные с тем, что его придется модифицировать, чтобы обеспечить класс информацией, которая может быть представлена в таймере. В конце построения мы получим результат, показанный на рисунке 02:

Рисунок 02

Рисунок 02 - Общий таймер.

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


Реализация

Эта часть - самая интересная в данной системе. На данном этапе мы посмотрим, как можно обойти вызов TimeCurrent соответствующим образом. Пользователь не должен знать, использует ли он репликацию или находится в контакте с сервером. Чтобы начать реализацию системы, нам нужно добавить (что должно быть понятно из предыдущих тем) новую глобальную переменную терминала. В то же время нам нужны правильные средства для передачи информации DateTime через переменную Double. Для этого мы будем использовать заголовочный файл Interprocess.mqh. Мы также добавляем следующие пункты:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_SymbolReplay                "RePlay"
#define def_GlobalVariableReplay        def_SymbolReplay + " Infos"
#define def_GlobalVariableIdGraphics    def_SymbolReplay + " ID"
#define def_GlobalVariableServerTime    def_SymbolReplay + " Time"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market " + def_SymbolReplay
//+------------------------------------------------------------------+
union u_Interprocess
{
   union u_0
   {
      double  df_Value;       // Value of the terminal global variable...
      ulong   IdGraphic;      // Contains the Graph ID of the asset...
   }u_Value;
   struct st_0
   {
      bool    isPlay;         // Indicates whether we are in Play or Pause mode...
      bool    isWait;         // Tells the user to wait...
      ushort  iPosShift;      // Value between 0 and 400...
   }s_Infos;
   datetime   ServerTime;
};
//+------------------------------------------------------------------+

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

~C_Replay()
   {
      ArrayFree(m_Ticks.Info);
      ArrayFree(m_Ticks.Rate);
      m_IdReplay = ChartFirst();
      do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         ChartClose(m_IdReplay);
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++);
      CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomSymbolDelete(def_SymbolReplay);
      GlobalVariableDel(def_GlobalVariableReplay);
      GlobalVariableDel(def_GlobalVariableIdGraphics);
      GlobalVariableDel(def_GlobalVariableServerTime);
      Print("Finished replay service...");
   }

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

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }

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

void CreateGlobalVariable(const string szName, const double value)
   {
      GlobalVariableDel(szName);
      GlobalVariableTemp(szName);     
      GlobalVariableSet(szName, value);
   }

Обратите внимание, что код создания и инициализации очень прост. Это сделано для того, чтобы при закрытии платформы и запросе на сохранение глобальных переменных терминала, те переменные, которые используются в системе репликации/моделирования, не были сохранены. Это вообще не является проблемой, поскольку мы не хотим, чтобы такие значения хранились и извлекались позже. Теперь мы можем приступить к рассмотрению показаний таймера. Но если это сделать в данный момент, то содержащееся в нем значение всегда будет одно и то же, и не обязательно принесет нам какую-либо практическую пользу. Это будет выглядеть так, как если бы сервис репликации/моделирования была приостановлен. Мы хотим, чтобы таймер двигался, когда он активен, то есть в режиме воспроизведения. Таким образом, мы смоделируем, какими должны быть требования к функции TimeCurrent, в которой мы получаем соответствующие данные о времени и дате, найденные на сервере. Для этого нам нужно, чтобы значение глобальной переменной изменялось приблизительно каждую секунду. Правильно было бы установить таймер для данного процесса. Но поскольку у нас нет возможности сделать это, нам нужно другое средство для реализации такого изменения значения глобальной переменной.

Чтобы попытаться определить время, нам также еще потребуется внести некоторые дополнения в класс C_Replay. Их можно увидеть в приведенном ниже коде:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      iTest = 0;
      while ((iTest == 0) && (!_StopFlag))
      {
         iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
         iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
         iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
         if (iTest == 0) Sleep(100);
      }
      if ((iTest < 0) || (_StopFlag)) return false;
      AdjustPositionToReplay(bViewBuider);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 0;
      while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
      {
         iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
         CreateBarInReplay(true);
         while ((iPos > 200) && (!_StopFlag))
         {
            if (ChartSymbol(m_IdReplay) == "") return false;
            GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            if (!Info.s_Infos.isPlay) return true;
            Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
            GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            Sleep(195);
            iPos -= 200;
	    iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               Info.ServerTime += 1;
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               

Мы добавляем новую переменную, которая поможет нам попробовать немного приблизиться к моменту, когда начнется новый бар. Однако, это не будет сделано каким угодно способом. Поскольку мы можем перевести время вперед, нам нужно, чтобы значение, указанное нашим "сервером", сбросилось. Это будет сделано в тот момент, когда функция уже будет указывать на новую точку наблюдения. Даже если мы останемся в режиме паузы на некоторое время, мы должны быть уверены, что значение будет соответствующим. Настоящая проблема возникает именно тогда, когда мы собираемся настроить таймер. Мы не можем просто захватить какое-либо значение. Система может немного опережать или отставать от реального времени, отображаемого на компьютерных часах. По этой причине, на этом раннем этапе мы стараемся подойти к правильному времени. У нас встроен таймер в генераторе баров. Мы будем использовать его как ориентир. Он выдает тик примерно каждые 195 миллисекунд, что приближает нас к счету в 5 единиц. Поскольку мы начали с нулевого значения, мы проверим, является ли значение счетчика больше 4. Когда это произойдет, мы увеличим время на одну единицу, то есть на 1 секунду, и данное значение будет помещено в глобальную переменную терминала, чтобы остальная часть системы могла его использовать. Затем весь цикл повторится.

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

//...

   Sleep(195);
   iPos -= 200;
   iCount++;
   if (iCount > 4)
   {
      iCount = 0;
      GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      Info.ServerTime += 1;
      Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
   }

//...

Вам может показаться, что это безумие. В каком-то смысле соглашусь с тем, что это немного безумный шаг с моей стороны. Но при добавлении именно данной строки, мы сможем поддерживать синхронность на приемлемом уровне. В случае, если актив обладает хорошей ликвидностью в такой степени, что сделки выводятся за относительно короткий промежуток времени, желательно менее чем за 1 секунду, то у нас будет достаточно синхронизированная система. Она может быть очень близка к почти идеальной системе. Но это достигается только в том случае, если актив действительно обладает достаточной ликвидностью. Вы можете спросить себя: что это за безумие, что происходит в этой выделенной строке?! Давайте немного подумаем: Каждые 5 циклов длительностью примерно 195 миллисекунд мы будем выполнять код для обновления таймера. Таким образом, частота обновления составляет около 975 миллисекунд, то есть, в каждом цикле не хватает 25 миллисекунд. Но на самом деле данная величина не является постоянной. Иногда она немного больше, иногда немного меньше. Не стоит пытаться настроить синхронизацию с помощью новой команды sleep, чтобы заставить систему затормозить для преодоления данной разницы. На первый взгляд, это должно сработать. Но со временем эти «микроразличия» становятся настолько большими, что вся система окажется вне синхронности. Чтобы решить данную проблему, мы делаем кое-что другое. Вместо того, чтобы пытаться точно установить время, мы используем сам бар для обеспечения синхронности. При выполнении функции CreateBarInReplay, она всегда будет указывать на текущий тик. Сравнивая значение времени этого тика со значением времени, которое находится в глобальной переменной, мы сможем получить в некоторых случаях значение больше единицы, в данном случае 1 секунды. Если данное значение меньше, то есть переменная Info.ServerTime задерживается во времени на накопленные 25 миллисекунд. Будет использоваться текущее значение времени тика, что позволит исправить разницу и сделать таймер очень близким к идеальному значению. Но, как я уже сообщал в начале объяснения, данный механизм автоматически настраивает систему в том случае, если используемый нами актив обладает достаточной ликвидностью. Если торговля длительное время стоит на месте, а сделки выводятся каждые 5-10 минут между одной и другой сделкой, то пострадает точность системы отсчёта времени. Это из-за того, что каждую секунду она будет отставать в среднем на 25 миллисекунд (это среднее, а не точное значение).

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


Адаптируем класс C_Study

Чтобы приступить к модификации, первое, что нужно сделать, - это внести изменения, которые вы видите чуть ниже:

void Update(void)
   {
      switch (m_Info.Status)
      {
         case eCloseMarket: m_Info.szInfo = "Closed Market";                             break;
         case eAuction    : m_Info.szInfo = "Auction";                                   break;
         case eInReplay   :
         case eInTrading  : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS);    break;
         case eInReplay   : m_Info.szInfo = "In Replay";                                 break;
         default          : m_Info.szInfo = "ERROR";
      }
      Draw();
   }

Мы удаляем вычеркнутую строку и переводим ее на более высший уровень, чтобы она имела тот же статус и функции, что и при использовании системы на физическом рынке. Таким образом мы уравниваем условия игры, т.е. мы e нас будет одинаковое поведение в обеих ситуациях, как в репликации/моделировании, так и на демо/реальном счете. Теперь, когда мы это сделали, мы можем посмотреть первую модификацию, внесенную в код процедуры GetBarTime. Это можно увидеть чуть ниже:

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
      }else dt = TimeCurrent();
                                
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = iTime(GetInfoTerminal().szSymbol, PERIOD_CURRENT, 0) + PeriodSeconds();

      return m_Info.Rate.time - dt;
   }

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

Чтобы решить эти проблемы, нам нужно внести в код класса два изменения. Это более насущный вопрос. Первая модификация показана ниже:

void Update(void)
   {
      datetime dt;
                                
      switch (m_Info.Status)
      {
         case eCloseMarket:
            m_Info.szInfo = "Closed Market";
            break;
         case eInReplay   :
         case eInTrading  :
            dt = GetBarTime();
            if (dt < ULONG_MAX)
            {
               m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
               break;
            }
         case eAuction    :
            m_Info.szInfo = "Auction";
            break;
         default          :
            m_Info.szInfo = "ERROR";
      }
      Draw();
   }

Код Update, показанный выше, несмотря на странный и сложный вид, гораздо проще и удобнее, чем кажется. У нас есть следующий сценарий. Если мы находимся в системе репликации или даже на реальном рынке и получим от функции GetBarTime значение ULONG_MAX, то у нас будет выведено сообщение об аукционе. Если значение меньше этого ULONG_MAX, а в обычных ситуациях это всегда так, то выведется значение таймера.

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

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
      int i0 = PeriodSeconds();
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
         if (dt == ULONG_MAX) return ULONG_MAX;
      }else dt = TimeCurrent();
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = (datetime)(((ulong) dt / i0) * i0)) + i0;

      return m_Info.Rate.time - dt;
   }

Этот дружественный код полностью решает нашу проблему, по крайней мере, на данный момент, поскольку нам придется вносить дополнения в служебный код. Но пока давайте разберемся, что здесь происходит. В случае с физическим рынком, где мы используем функцию TimeCurrent, ничего не меняется. Это всё в принципе так. Но когда мы находимся в системе репликации, всё меняется очень своеобразно. Поэтому уделите внимание пониманию того, как системе удается отображать происходящее, независимо от того, что происходит с данными репликации или моделирования. В случае, если сервис помещает значение ULONG_MAX в глобальную переменную терминала, или если данная переменная не найдена. Функция GetBarTime должна возвращать значение ULONG_MAX. После этого процедура Update сообщит нам, что мы находимся в режиме аукциона.  Это делается в этих точках. Дальнейший прогресс таймера будет невозможен. Теперь наступает интересная часть, которая решает нашу вторую проблему. В отличие от использования системы на активе, подключенном к торговому серверу, где мы всегда будем синхронизированы, при использовании репликации/моделирования всё может выйти из-под контроля, и мы можем столкнуться с довольно необычными ситуациями. Чтобы решить данную проблему, мы используем этот расчет, который работает как для физического рынка, так и для нашей развивающейся системы. В этом расчете мы заменяем старый метод, когда нужно было знать время открытия текущего бара. Таким образом, нам удалось решить обе проблемы при использовании репликации/моделирования.

Однако, нам нужно вернуться к классу C_Replay, чтобы система могла указать, когда актив попал на аукцион. Данная часть относительно проста, так как всё, что нам нужно сделать,- это внести значение ULONG_MAX в глобальную переменную terminal. Поймите, что это было объяснено относительно просто, ведь перед нами другие проблемы. Но давайте посмотрим, как это будет выглядеть на практике.


Адаптируем класс C_Replay к системе связи

Первое, что мы сделаем в классе C_Replay, - изменим следующий код:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = ULONG_MAX;
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }

Обратите внимание, что мы удалили одну строку и заменили ее другой. Именно это приведет к тому, что при запуске сервиса репликации/моделирования и запуске актива на графике будет отображаться сообщение о сделках. Это показывает нам, что система теперь общается правильно, так как перед воспроизведением мы должны думать, что система действительно находится в процессе аукциона. Это была легкая часть, теперь перейдем к сложной. Бывает, что, когда сервис уже запущен, нам нужно узнать, вышел ли актив на аукцион или нет, у таких вещей есть свои причины. Актив может попасть на аукцион по целому ряду причин, и каждая из них может быть совершенно неожиданной, или из-за того, что произошло гораздо большее расхождение, чем могло произойти на самом деле, потому что книга просто закрылась, или из-за того, что в результате ордера книга была полностью очищена, и так далее. Причина, по которой актив был выставлен на аукцион, нас, по сути, не волнует. Нас волнует, что произойдет, когда он будет выставлен на аукцион. Существуют особые правила, которые определяют, как будет проводиться аукцион, но для нас важнее всего следующее правило: какое минимальное время актив должен находиться на аукционе? Именно в этом заключается суть. Если это время меньше 1 минуты, то актив на реальном рынке, безусловно, может войти и выйти из сделок без того, чтобы система репликации/моделирования смогла это обнаружить. Или, скорее, это изменение невозможно будет воспринять, поскольку оно всегда будет происходить в пределах самого короткого временного интервала, который можно использовать для определения времени бара.

Самый простой механизм, который можно использовать в репликации/моделировании, чтобы определить, что актив вошел в аукцион, заключается в следующем: проверить разницу во времени между одним баром и другим. Если эта разница превышает 1 минуту, мы должны сообщить пользователю, что актив только что вошел в процесс аукциона, и таким образом приостановить его на весь этот период. Этот вид механизма пригодится вам в дальнейшем. Пока что мы будем заниматься только его разработкой и внедрением, оставив другие вопросы для другого момента. Давайте посмотрим, как решить эту проблему. Это можно увидеть в следующем коде:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      iTest = 0;
      while ((iTest == 0) && (!_StopFlag))
      {
         iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
         iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
         iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
         if (iTest == 0) Sleep(100);
      }
      if ((iTest < 0) || (_StopFlag)) return false;
      AdjustPositionToReplay(bViewBuider);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 0;
      while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
      {
         iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
         CreateBarInReplay(true);
         while ((iPos > 200) && (!_StopFlag))
         {
            if (ChartSymbol(m_IdReplay) == "") return false;
            GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            if (!Info.s_Infos.isPlay) return true;
            Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
            GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            Sleep(195);
            iPos -= 200;
            iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               if ((m_Ticks.Info[m_ReplayCount].time - m_Ticks.Info[m_ReplayCount - 1].time) > 60) Info.ServerTime = ULONG_MAX; else
               {
                  Info.ServerTime += 1;
                  Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
               };
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               

Обратите внимание: мы проверяли разницу во времени между одним тиком и другим. Если данная разница превышает 60 секунд, то есть больше, чем самое короткое время создания бара, мы сообщим, что это «аукционный звонок», и вся система репликации/моделирования укажет на аукцион. Если разница во времени меньше или равна 60 секундам, это означает, что актив всё еще активен и таймер должен быть запущен, о чем говорилось в этой статье. На этом мы завершили очередной этап.


Заключение

Сегодня мы рассмотрели, как вполне практичным, безопасным, надежным и эффективным способом добавить таймер, указывающий на появление нового бара. Мы пережили несколько моментов, когда казалось, что это невозможно, но на самом деле нет ничего невозможного. Пожалуй, это будет чуть сложнее или труднее преодолеть, но мы всегда сможем найти подходящий способ решения проблем. Главное здесь - показать, что всегда нужно стараться создать решение, использование которого должно быть применимо во всех ситуациях, где оно требуется. Нет смысла программировать что-то для того, чтобы пользователь или даже мы сами изучали и анализировали возможности, а когда мы приступаем к применению такой модели, она должна использовать инструменты, отличающиеся от тех, которые использовались для разработки анализа. В итоге это полностью отбивает желание использовать один из инструментов или препятствует проведению исследований. Поэтому всегда помните: Если нужно что-то сделать, то это нужно сделать правильно, а не так, чтобы всё работало в теории, а на практике демонстрировало совсем иное поведение.

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

Прикрепленные файлы |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Разработка системы репликации (Часть 32): Система ордеров (I) Разработка системы репликации (Часть 32): Система ордеров (I)
Из всего, что было разработано до настоящего момента, данная система, как вы наверняка заметите и со временем согласитесь, - является самым сложным. Сейчас нам нужно сделать нечто очень простое: заставить нашу систему имитировать работу торгового сервера на практике. Эта необходимость точно реализовывать способ моделирования действий торгового сервера кажется простым делом. По крайней мере, на словах. Но нам нужно сделать это так, чтобы для пользователя системы репликации/моделирования всё происходило как можно более незаметно или прозрачно.
Разработка системы репликации (Часть 30): Проект советника — класс C_Mouse (IV) Разработка системы репликации (Часть 30): Проект советника — класс C_Mouse (IV)
Сегодня мы изучим технику, которая может очень сильно помочь нам на разных этапах нашей профессиональной жизни в качестве программиста. Вопреки мнению многих, ограничена не сама платформа, а знания человека, который говорит об ограничениях. В данной статье будет рассказано о том, что с помощью здравого смысла и творческого подхода можно сделать платформу MetaTrader 5 гораздо более интересной и универсальной, не прибегая к созданию безумных программ или чего-то подобного, и создать простой, но безопасный и надежный код. Мы будем использовать свою изобретательность, чтобы изменить уже существующий код, не удаляя и не добавляя ни одной строки в исходный код.
Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 4) Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 4)
Статья является четвертой частью серии, описывающей этапы разработки нативного MQL5-клиента для протокола MQTT. В этой части мы рассматриваем свойства MQTT v5.0, их семантику, то, как мы читаем некоторые из них, а также приводим краткий пример того, как свойства можно использовать для расширения протокола.
Популяционные алгоритмы оптимизации: Устойчивость к застреванию в локальных экстремумах (Часть I) Популяционные алгоритмы оптимизации: Устойчивость к застреванию в локальных экстремумах (Часть I)
Эта статья представляет уникальный эксперимент, цель которого - исследовать поведение популяционных алгоритмов оптимизации в контексте их способности эффективно покидать локальные минимумы при низком разнообразии в популяции и достигать глобальных максимумов. Работа в этом направлении позволит глубже понять, какие конкретные алгоритмы могут успешно продолжать поиск из координат, установленных пользователем в качестве отправной точки, и какие факторы влияют на их успешность в этом процессе.