Директивы препроцессора для тестера

В разделе об Общих свойствах программ мы впервые познакомились с директивами #property в MQL-программах. Затем нам встречались директивы, предназначенные для скриптов, сервисов и индикаторов. Есть своя группа директив и для тестера. Некоторые из них мы уже упоминали, в частности, tester_everytick_calculate влияет на расчет индикаторов.

В следующей таблице представлены все директивы тестера. Ниже приведены пояснения.

Директива

Описание

tester_indicator "строка"

Имя пользовательского индикатора в формате "имя_индикатора.ex5"

tester_file "строка"

Имя файла в формате "имя_файла.расширение" с исходными данными, необходимыми для теста программы

tester_library "строка"

Имя библиотеки с расширением, например, "библиотека.ex5" или "библиотека.dll"

tester_set "строка"

Имя файла в формате "имя_файла.set" с настройками значений и диапазонов оптимизации входных параметров программы

tester_no_cache

Отключение чтения имеющегося кэша предыдущих оптимизаций (opt-файлов)

tester_everytick_calculate

Отключение экономного режима расчета индикаторов в тестере

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

Директива tester_indicator требуется для подключения к процессу тестирования тех индикаторов, которые не упомянуты в исходном коде тестируемой программы в виде константных строк (литералов). Как правило, необходимый индикатор может быть определен компилятором автоматически из вызова функций iCustom, если его имя в явном виде задано в соответствующем параметре, например, iCustom(symbol, period, "indicator_name",...). Однако так бывает не всегда.

Допустим, мы пишем универсальный эксперт, который способен использовать разные индикаторы скользящего среднего, а не только стандартные встроенные. Тогда мы можем завести входную переменную для указания имени индикатора пользователем. И вызов iCustom превратится в iCustom(symbol, period, CustomIndicatorName,...), где CustomIndicatorName — входная переменная эксперта, содержимое которой не известно в момент компиляции. Более того, разработчик в таком случае, скорее всего, применит IndicatorCreate вместо iCustom, так как количество и типы параметров индикатора должны также настраиваться. В подобных случаях, для отладки программы или её демонстрации с конкретным индикатором, тестеру нужно сообщить его имя с помощью директивы tester_indicator.

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

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

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

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

Обратите внимание, что компилятор не выдает предупреждений, если файла, указанного в директиве tester_file, не существует.

Подключаемые файлы должны находиться в "песочнице" терминала MQL5/Files/.

Директива tester_library сообщает тестеру о необходимости передать на агенты библиотеку — вспомогательную программу, способную работать только в контексте другой MQL-программы. О библиотеках мы подробно поговорим в отдельном разделе.

Необходимые для тестирования библиотеки определяются автоматически по директивам #import в исходном коде. Однако, если какая-либо библиотека используется внешним индикатором, то необходимо включить данное свойство. Библиотека может быть как с расширением dll, так и с расширением ex5.

Директива tester_set оперирует set-файлами с настройками MQL-программы. Указанный в директиве файл станет доступен из контекстного меню тестера и позволит пользователю быстро применить настройки.

Если имя указано без пути, set-файл должен лежать в том же каталоге, где эксперт. Это несколько неожиданно, т.к. по умолчанию каталог set-файлов другой — Presets, и именно туда они сохраняются по командам из интерфейса терминала. Чтобы подключить set-файл из данного каталога, нужно явно указать его в директиве и предварить косой чертой, которая обозначает абсолютный путь внутри папки MQL5.

#property tester_set "/Presets/xyz.set"

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

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

Если в названии set-файла указать имя эксперта и номер версии как "<expert_name>_<number>.set", то он автоматически добавится в меню загрузки версий параметров под номером версии <number>. Например, имя "MACD Sample_4.set" означает, что это set-файл для эксперта "MACD Sample.mq5" с номером версии равным 4.

Желающие могут изучить формат set-файлов: для этого достаточно вручную сохранить настройки тестирования/оптимизации в тестере стратегий и затем открыть созданный таким образом файл в текстовом редакторе.

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

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

Директива tester_everytick_calculate предназначена для включения режима расчета индикатора на каждом тике в тестере.

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

Однако некоторые программы могут требовать пересчета индикаторов на каждом тике. Именно в таких случаях и пригодится свойство tester_everytick_calculate.

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

  • при тестировании в визуальном режиме;
  • при наличии в индикаторе функций EventChartCustom, OnChartEvent, OnTimer;

Данное свойство касается только работы в тестере стратегий — в терминале индикаторы всегда считаются на каждом поступившем тике.

В эксперте FrameTransfer.mq5 уже была на самом деле использована директива:

#property tester_set "FrameTransfer.set"

Просто мы не акцентировали на этом внимание. Файл "FrameTransfer.set" находится рядом с исходным кодом. В том же эксперте нам пригодилась и другая директива из вышеприведенной таблицы:

#property tester_no_cache

В дополнение рассмотрим пример директивы tester_file. Ранее в разделе про автонастройку параметров экспертов при оптимизации мы представили эксперт BandOsMApro.mq5, в котором потребовалось ввести несколько теневых параметров для передачи диапазонов оптимизации в наш исходный код, выполняющийся на агентах.

Директива tester_file позволит нам избавиться от этих лишних параметров. Новую версию эксперта назовем BandOsMAprofile.mq5.

Поскольку мы теперь знакомы с директивой tester_set, добавим в новую версию уже упоминавшийся ранее файл /Presets/MQL5Book/BandOsMA.set.

#property tester_set "/Presets/MQL5Book/BandOsMA.set"

Информацию о диапазоне и шаге изменения периодов FastOsMA и SlowOsMA будем сохранять в файл "BandOsMAprofile.csv" вместо трех дополнительных входных параметров FastShadow4Optimization, SlowShadow4Optimization, StepsShadow4Optimization.

#define SETTINGS_FILE "BandOsMAprofile.csv"
#property tester_file SETTINGS_FILE
   
const string SettingsFile = SETTINGS_FILE;

Теневой параметр FastSlowCombo4Optimization по-прежнему нужен для полного перебора разрешенных комбинаций периодов.

input group "A U X I L I A R Y"
sinput int FastSlowCombo4Optimization = 0;   // (reserved for optimization)

Напомним, его диапазон для оптимизации мы находим в функции Iterate. Первый раз мы её вызываем в OnTesterInit с полным перебором сочетаний быстрого и медленного периодов.

В принципе, мы могли бы сохранить все допустимые сочетания в массив структур PairOfPeriods и записать его в двоичный файл для передачи на агенты. Тогда на агентах наш эксперт мог бы прочитать из файла готовый массив и по индексу FastSlowCombo4Optimization извлечь из массива соответствующую пару FastOsMA и SlowOsMA.

Вместо этого мы остановимся на минимальном изменении рабочей логики программы: будем по прежнему восстанавливать пару периодов за счет второго вызова Iterate в обработчике OnInit. Только на этот раз диапазон и шаг перебора значений периодов мы получим не из теневых параметров, а из CSV-файла.

Вот изменения в OnTesterInit.

int OnTesterInit()
{
   ...
         // проверим есть ли файл уже до компиляции
         // - если нет, тестер не сможет отослать его на агенты
         const bool preExisted = FileIsExist(SettingsFile);
         
         // запишем настройки в файл для передачи программам-копиям на агенты
         int handle = FileOpen(SettingsFileFILE_WRITE | FILE_CSV | FILE_ANSI",");
         FileWrite(handle"FastOsMA"start1step1stop1);
         FileWrite(handle"SlowOsMA"start2step2stop2);
         FileClose(handle);
         
         if(!preExisted)
         {
            PrintFormat("Required file %s is missing. It has been just created."
               " Please restart again.",
               SettingsFile);
            ChartClose();
            return INIT_FAILED;
         }
   ...
   return INIT_SUCCEEDED;
}

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

Если вы хотите исключить этот шаг, можете заранее создать пустой файл MQL5/Files/BandOsMAprofile.csv.

Обработчик OnInit преобразился следующим образом.

int OnInit()
{
   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;
   
   PairOfPeriods p = {FastOsMASlowOsMA}; // исходные параметры по умолчанию
   int handle = FileOpen(SettingsFileFILE_READ | FILE_TXT | FILE_ANSI);
   
   // во время оптимизации нужен файл с теневыми параметрами
   if(MQLInfoInteger(MQL_OPTIMIZATION) && handle == INVALID_HANDLE)
   {
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(handle != INVALID_HANDLE)
   {
      if(FastSlowCombo4Optimization != -1)
      {
         // если теневая копия есть, считываем значения периодов из неё
         const string line1 = FileReadString(handle);
         string settings[];
         if(StringSplit(line1, ',', settings) == 4)
         {
            int FastStart = (int)StringToInteger(settings[1]);
            int FastStep = (int)StringToInteger(settings[2]);
            int FastStop = (int)StringToInteger(settings[3]);
            const string line2 = FileReadString(handle);
            if(StringSplit(line2, ',', settings) == 4)
            {
               int SlowStart = (int)StringToInteger(settings[1]);
               int SlowStep = (int)StringToInteger(settings[2]);
               int SlowStop = (int)StringToInteger(settings[3]);
               p = Iterate(FastStartFastStopFastStep,
                  SlowStartSlowStopSlowStepFastSlowCombo4Optimization);
               PrintFormat("MA periods are restored from shadow: FastOsMA=%d SlowOsMA=%d",
                  p.fastp.slow);
            }
         }
      }
      FileClose(handle);
   }

При запуске одиночных тестов после оптимизации мы увидим в журнале раскодированные значения периодов FastOsMA и SlowOsMA на основе оптимизированного значения FastSlowCombo4Optimization. В дальнейшем мы можем подставить эти значение в параметры-периоды, а csv-файл удалить. Также мы предусмотрели, что файл не будет учитываться, если в FastSlowCombo4Optimization поставить значение -1.