Мультивалютное тестирование

Как известно, тестер MetaTrader 5 позволяет проверять стратегии, торгующие на нескольких инструментах. Чисто технически, с оглядкой на доступные "железные" ресурсы компьютера, можно моделировать одновременную торговлю по всем доступным инструментам.

Тестирование таких стратегий налагает на тестер несколько дополнительных технических требований:

  • генерация последовательностей тиков для всех инструментов;
  • расчет индикаторов для всех инструментов;
  • расчет маржевых требований и эмуляция прочих торговых условий по всем инструментам.

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

Агент закачивает недостающую историю с небольшим запасом, чтобы обеспечить необходимые данные для расчета индикаторов или копирования экспертом на момент начала тестирования. Минимальный объем истории, скачиваемой с торгового сервера, зависит от таймфрейма. Так для таймфреймов D1 и меньше он составляет один год. Иными словами, предварительная история закачивается от начала предыдущего года относительно стартовой даты тестера. Это дает как минимум 1 год истории, если тестирование задано с первого января, и как максимум чуть меньше двух лет, если тестирование идет с декабря. Для недельного таймфрейма запрашивается история в 100 баров, то есть примерно два года (в году 52 недели). Для тестирования на месячном таймфрейме агент запросит 100 месяцев (то есть историю примерно за 8 лет: 12 месяцев * 8 лет = 96). В любом случае, на более младших таймфреймах, чем рабочий, будет доступно пропорционально большее количество баров. Если существующих данных не хватает для предопределенной глубины предварительной истории, об этом факте будет запись в журнале тестирования.

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

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

Обращение к данным чужого символа происходят в следующих случаях:

  • использование технических индикаторов, iCustom, IndicatorCreate на паре символ/таймфрейм
  • запрос к Обзору рынка по чужому символу:
    • SeriesInfoInteger
    • Bars
    • SymbolSelect
    • SymbolIsSynchronized
    • SymbolInfoDouble
    • SymbolInfoInteger
    • SymbolInfoString
    • SymbolInfoTick
    • SymbolInfoSessionQuote
    • SymbolInfoSessionTrade
    • MarketBookAdd
    • MarketBookGet
  • запрос к таймсерии по паре символ/таймфрейм функциями:
    • CopyBuffer
    • CopyRates
    • CopyTime
    • CopyOpen
    • CopyHigh
    • CopyLow
    • CopyClose
    • CopyTickVolume
    • CopyRealVolume
    • CopySpread

Кроме того, можно явно запросить историю для нужных символов с помощью вызова функции SymbolSelect в обработчике OnInit — загрузка истории будет произведена заранее, до начала тестирования советника.

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

Для каждого инструмента генерируется собственная тиковая последовательность согласно установленному режиму генерации тиков.

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

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

Например, если мы тестируем эксперт на символе EURUSD, и здесь открылась новая часовая свеча, то мы получим событие OnTick. Но при этом нет никакой гарантии, что новая свеча открылась по символу GBPUSD, который, допустим, нас тоже интересует.

Таким образом, алгоритм синхронизации подразумевает, что нужно проверять котировки всех инструментов и дождаться равенства времен открытия последних баров.

Это не вызывает вопросов до тех пор, пока используются режимы тестирования на реальных тиках, эмуляции всех тиков или OHLC M1. При этих режимах в пределах одной свечи генерируется достаточное количество тиков, чтобы дождаться момента синхронизации баров с разных символов. Достаточно завершить работу функции OnTick и проверить появление нового бара на GBPUSD на следующем тике. Но при тестировании в режиме "Только цены открытия" другого тика не будет, так как эксперт вызывается только один раз за бар, и может показаться, что этот режим не годится для тестирования мультивалютных экспертов. На самом деле тестер позволяет засечь момент, когда на другом символе откроется новый бар с помощью функции Sleep (в цикле) или таймера.

Для начала рассмотрим пример эксперта SyncBarsBySleep.mq5, демонстрирующего синхронизацию баров через Sleep.

Пара входных параметров позволяет задать размер паузы (Pause) в секундах для ожидания "чужих" баров, а также название "чужого" символа (OtherSymbol) — он должен отличаться от символа графика.

input uint Pause = 1;                   // Pause (seconds)
input string OtherSymbol = "USDJPY";

Для выявления закономерностей в запаздывании времен открытия баров опишем простой класс BarTimeStatistics. Он содержит поле для подсчета общего числа баров (total) и числа баров, на которых не было изначально синхронизации (late), то есть "чужой" символ запаздывал.

class BarTimeStatistics
{
public:
   int total;
   int late;
   
   BarTimeStatistics(): total(0), late(0) { }
   
   ~BarTimeStatistics()
   {
      PrintFormat("%d bars on %s was late among %d total bars on %s (%2.1f%%)",
         lateOtherSymboltotal_Symbollate * 100.0 / total);
   }
};

Полученную статистику объект данного класса печатает в своем деструкторе. Поскольку мы собираемся сделать этот объект статическим, отчет выведется в самом конце теста.

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

void OnTick()
{
   const TICK_MODEL model = getTickModel();
   if(model != TICK_MODEL_OPEN_PRICES)
   {
      static bool shownOnce = false;
      if(!shownOnce)
      {
         Print("This EA is intended to run in \"Open Prices\" mode");
         shownOnce = true;
      }
   }

Далее в OnTick идет непосредственно рабочий алгоритм синхронизации.

   // время последнего известного бара для _Symbol
   static datetime lastBarTime = 0;
   // признак синхронизированности
   static bool synchonized = false;
   // счетчики баров
   static BarTimeStatistics stats;
   
   const datetime currentTime = iTime(_Symbol_Period0);
   
   // если выполняемся первый раз или бар изменился, сохраняем бар
   if(lastBarTime != currentTime)
   {
      stats.total++;
      lastBarTime = currentTime;
      PrintFormat("Last bar on %s is %s"_SymbolTimeToString(lastBarTime));
      synchonized = false;
   }
   
   // время последнего известного бара для другого символа
   datetime otherTime;
   bool late = false;
   
   // ждем пока времена двух баров не станут одинаковыми
   while(currentTime != (otherTime = iTime(OtherSymbol_Period0)))
   {
      late = true;
      PrintFormat("Wait %d seconds..."Pause);
      Sleep(Pause * 1000);
   }
   if(latestats.late++;
   
   // здесь мы оказываемся после синхронизации, сохраним новый статус
   if(!synchonized)
   {
      // используем TimeTradeServer() т.к. TimeCurrent() не меняется в отсутствие тиков
      Print("Bars are in sync at "TimeToString(TimeTradeServer(),
         TIME_DATE | TIME_SECONDS));
      // больше не выводим сообщение до следующей рассинхронизации
      synchonized = true;
   }
   // здесь будет ваш синхронный алгоритм
   // ...
}

Настроим тестер для работы эксперта на EURUSD, H1, как наиболее ликвидном инструменте. Параметры эксперта оставим по умолчанию, то есть "чужим" символом будет USDJPY.

В результате теста журнал будет содержать примерно такие записи (в журнале намеренно оставлены строки о подкачке истории USDJPY, которая произошла при первом обращении к iTime).

2022.04.15 00:00:00   Last bar on EURUSD is 2022.04.15 00:00

USDJPY: load 27 bytes of history data to synchronize in 0:00:00.001

USDJPY: history synchronized from 2020.01.02 to 2022.04.20

USDJPY,H1: history cache allocated for 8109 bars and contains 8006 bars from 2021.01.04 00:00 to 2022.04.14 23:00

USDJPY,H1: 1 bar from 2022.04.15 00:00 added

USDJPY,H1: history begins from 2021.01.04 00:00

2022.04.15 00:00:00   Bars are in sync at 2022.04.15 00:00:00

2022.04.15 01:00:00   Last bar on EURUSD is 2022.04.15 01:00

2022.04.15 01:00:00   Wait 1 seconds...

2022.04.15 01:00:01   Bars are in sync at 2022.04.15 01:00:01

2022.04.15 02:00:00   Last bar on EURUSD is 2022.04.15 02:00

2022.04.15 02:00:00   Wait 1 seconds...

2022.04.15 02:00:01   Bars are in sync at 2022.04.15 02:00:01

...

2022.04.20 23:59:59   95 bars on USDJPY was late among 96 total bars on EURUSD (99.0%)

Видно, что бары на USDJPY регулярно запаздывают. Если выбрать в настройках тестера USDJPY, H1, а в параметрах эксперта EURUSD, получим обратную картину.

2022.04.15 00:00:00   Last bar on USDJPY is 2022.04.15 00:00

EURUSD: load 27 bytes of history data to synchronize in 0:00:00.002

EURUSD: history synchronized from 2018.01.02 to 2022.04.20

EURUSD,H1: history cache allocated for 8109 bars and contains 8006 bars from 2021.01.04 00:00 to 2022.04.14 23:00

EURUSD,H1: 1 bar from 2022.04.15 00:00 added

EURUSD,H1: history begins from 2021.01.04 00:00

2022.04.15 00:00:00   Bars are in sync at 2022.04.15 00:00:00

2022.04.15 01:00:00   Last bar on USDJPY is 2022.04.15 01:00

2022.04.15 01:00:00   Wait 1 seconds...

2022.04.15 01:00:01   Bars are in sync at 2022.04.15 01:00:01

2022.04.15 02:00:00   Last bar on USDJPY is 2022.04.15 02:00

2022.04.15 02:00:00   Wait 1 seconds...

2022.04.15 02:00:01   Bars are in sync at 2022.04.15 02:00:01

...

2022.04.20 23:59:59   23 bars on EURUSD was late among 96 total bars on USDJPY (24.0%)

Здесь в большинстве случаев ждать не приходилось — бары на EURUSD уже существовали в момент формирования бара на USDJPY.

Есть и другой способ синхронизации баров — с помощью таймера. Пример такого эксперта SyncBarsByTimer.mq5 прилагается к книге. Обратите внимание, что в нем события таймера, как правило, приходятся внутрь бара (т.к. вероятность попасть точно на начало ничтожно мала). Из-за этого бары оказываются синхронизированными почти всегда.

Мы могли бы еще напомнить о возможности синхронизации баров с помощью индикатора шпиона EventTickSpy.mq5, но он основан на пользовательских событиях, которые работают только при визуальном тестировании. Кроме того, для подобных индикаторов, требующих реагировать на каждый тик, важно использовать директиву #property tester_everytick_calculate. Мы уже говорили о ней в разделе о Тестировании индикаторов и еще раз напомним в разделе о специфических директивах для тестера.