Авто-настройка: ParameterGetRange и ParameterSetRange

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

...
Best result 90.61004580175876 produced at generation 25. Next generation 26
genetic pass (26, 388) tested with error "incorrect input parameters" in 0:00:00.021
genetic pass (26, 436) tested with error "incorrect input parameters" in 0:00:00.007
genetic pass (26, 439) tested with error "incorrect input parameters" in 0:00:00.007
genetic pass (26, 363) tested with error "incorrect input parameters" in 0:00:00.008
genetic pass (26, 365) tested with error "incorrect input parameters" in 0:00:00.008
...

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

   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;

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

Поскольку мы применяем генетическую оптимизацию, в каждом поколении оказывается несколько забракованных образцов, которые не участвуют в дальнейших мутациях. По тем или иным причинам оптимизатор MetaTrader 5 не восполняет эти потери, то есть не генерирует им замену. А меньший размер популяции может негативно сказываться на качестве. Таким образом, следует придумать способ, как обеспечить перебор входных настроек только в корректных сочетаниях. Тут нам на помощь приходят две функции MQL5 API: ParameterGetRange и ParameterSetRange.

Обе функции имеют по два перегруженных прототипа, отличающихся типами параметров: long и double. Вот как описаны 2 варианта функции ParameterGetRange.

bool ParameterGetRange(const string name, bool &enable, long &value, long &start, long &step, long &stop)

bool ParameterGetRange(const string name, bool &enable, double &value, double &start, double &step, double &stop)

Функция получает для заданной по имени входной переменной информацию о её текущем значении (value), диапазоне значений (start, stop) и шаге изменения (step) при оптимизации. Кроме того в переменную enable записывается признак того, включена ли оптимизация по входной переменной с именем name.

Функция возвращает признак успеха (true) или ошибки (false).

Функция может вызываться только из трех специальных обработчиков, связанных с оптимизацией: OnTesterInit, OnTesterPass и OnTesterDeinit. Мы расскажем про них в следующем разделе. Но как можно догадаться из названий, OnTesterInit вызывается перед началом оптимизации, OnTesterDeinit — по окончании оптимизации, а OnTesterPass — после каждого прохода в процессе оптимизации. Нас пока интересует только OnTesterInit. Она, также как и две другие функции, не имеет параметров и может быть описана с типом void, то есть ничего не возвращать.

Два варианта функции ParameterSetRange имеют похожие прототипы и выполняют обратное действие: задают оптимизационные свойства входного параметра эксперта.

bool ParameterSetRange(const string name, bool enable, long value, long start, long step, long stop)

bool ParameterSetRange(const string name, bool enable, double value, double start, double step, double stop)

Функция устанавливает правила модификации input-переменной с названием name при оптимизации: значение, шаг изменения, начальное и конечное значения.

Эта функция может вызываться только из обработчика OnTesterInit при запуске оптимизации в тестере стратегий.

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

Функция позволяет использовать в оптимизации даже переменные, объявленные с модификатором sinput (они недоступны для включения в оптимизацию пользователем).

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

Усовершенствуем эксперт BandOsMA с использованием новых функций. Обновленная версия прилагается под именем BandOsMApro.mq5 ("pro" можно условно расшифровать как "parameter range optimization").

Итак, у нас появляется обработчик OnTesterInit, в котором мы считываем настройки для параметров FastOsMA и SlowOsMA и проверяем, включены ли они в оптимизацию. Если да, требуется их выключить, и предложить что-то взамен.

void OnTesterInit()
{
   bool enabled1enabled2;
   long value1start1step1stop1;
   long value2start2step2stop2;
   if(ParameterGetRange("FastOsMA"enabled1value1start1step1stop1)
   && ParameterGetRange("SlowOsMA"enabled2value2start2step2stop2))
   {
      if(enabled1 && enabled2)
      {
         if(!ParameterSetRange("FastOsMA"falsevalue1start1step1stop1)
         || !ParameterSetRange("SlowOsMA"falsevalue2start2step2stop2))
         {
            Print("Can't disable optimization by FastOsMA and SlowOsMA: ",
               E2S(_LastError));
            return;
         }
         ...
      }
   }
   else
   {
      Print("Can't adjust optimization by FastOsMA and SlowOsMA: "E2S(_LastError));
   }
}

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

void OnTesterDeinit()
{
}

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

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

В функции Iterate мы пробегаем в двух вложенных циклах по периодам быстрой и медленной MA и подсчитываем количество допустимых сочетаний, т.е. когда период i меньше периода j. Необязательный параметр find потребуется нам при вызове Iterate из OnInit, чтобы по порядковому номеру сочетания вернуть пару i и j. И поскольку требуется возвращать 2 числа, мы объявили для них структуру PairOfPeriods.

struct PairOfPeriods
{
   int fast;
   int slow;
};
   
PairOfPeriods Iterate(const long start1const long stop1const long step1,
   const long start2const long stop2const long step2,
   const long find = -1)
{
   int count = 0;
   for(int i = (int)start1i <= (int)stop1i += (int)step1)
   {
      for(int j = (int)start2j <= (int)stop2j += (int)step2)
      {
         if(i < j)
         {
            if(count == find)
            {
               PairOfPeriods p = {ij};
               return p;
            }
            ++count;
         }
      }
   }
   PairOfPeriods p = {count0};
   return p;
}

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

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

А пока вернемся в OnTesterInit и организуем на MQL5 оптимизацию по параметру FastSlowCombo4Optimization в нужном диапазоне с помощью ParameterSetRange.

void OnTesterInit()
{
   ...
         PairOfPeriods p = Iterate(start1stop1step1start2stop2step2);
         const int count = p.fast;
         ParameterSetRange("FastSlowCombo4Optimization"true001count);
         PrintFormat("Parameter FastSlowCombo4Optimization is enabled with maximum: %d",
            count);
   ...
}

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

Во время теста на агенте по номеру в FastSlowCombo4Optimization следует получить пару периодов, вызвав вновь Iterate, на этот раз с заполненным параметром find. Но проблема в том, что для этой операции требуется знать изначальные диапазоны и шаг изменения параметров FastOsMA и SlowOsMA. А эта информация есть только в терминале. Значит, нам нужно как-то передать её на агент.

Сейчас мы применим пока единственное известное нам решение: добавим еще 3 теневых параметра оптимизации и установим для них некие значения. В будущем мы познакомимся с технологией передачи файлов на агенты (см. Директивы препроцессора для тестера). Тогда мы сможем записать в файл весь массив посчитанных функцией Iterate индексов и отправить его на агенты. Тогда мы избавимся от трех лишних теневых параметров оптимизации.

Итак, добавим 3 входных параметра:

sinput ulong FastShadow4Optimization = 0;    // (reserved for optimization)
sinput ulong SlowShadow4Optimization = 0;    // (reserved for optimization)
sinput ulong StepsShadow4Optimization = 0;   // (reserved for optimization)

Мы используем тип ulong для экономии, чтобы в каждое значение упаковать по 2 целых int-числа. А вот как они заполняются в OnTesterInit.

void OnTesterInit()
{
   ...
         const ulong fast = start1 | (stop1 << 16);
         const ulong slow = start2 | (stop2 << 16);
         const ulong step = step1 | (step2 << 16);
         ParameterSetRange("FastShadow4Optimization"falsefastfast1fast);
         ParameterSetRange("SlowShadow4Optimization"falseslowslow1slow);
         ParameterSetRange("StepsShadow4Optimization"falsestepstep1step);
   ...
}

Все 3 параметра — неоптимизируемые (false во втором аргументе).

На этом мы разобрались с функцией OnTesterInit и должны обратиться к приемной стороне — обработчику OnInit.

int OnInit()
{
   // оставим проверку для одиночных тестов
   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;
   
   // при оптимизации требуем наличия теневых параметров   
   if(MQLInfoInteger(MQL_OPTIMIZATION) && StepsShadow4Optimization == 0)
   {
      return INIT_PARAMETERS_INCORRECT;
   }
   
   PairOfPeriods p = {FastOsMASlowOsMA}; // по умолчанию работаем с обычными параметрами
   if(FastShadow4Optimization && SlowShadow4Optimization && StepsShadow4Optimization)
   {
      // если теневые параметра заполнены, раскодируем их в периоды
      int FastStart = (int)(FastShadow4Optimization & 0xFFFF);
      int FastStop = (int)((FastShadow4Optimization >> 16) & 0xFFFF);
      int SlowStart = (int)(SlowShadow4Optimization & 0xFFFF);
      int SlowStop = (int)((SlowShadow4Optimization >> 16) & 0xFFFF);
      int FastStep = (int)(StepsShadow4Optimization & 0xFFFF);
      int SlowStep = (int)((StepsShadow4Optimization >> 16) & 0xFFFF);
      
      p = Iterate(FastStartFastStopFastStep,
         SlowStartSlowStopSlowStepFastSlowCombo4Optimization);
      PrintFormat("MA periods are restored from shadow: FastOsMA=%d SlowOsMA=%d",
         p.fastp.slow);
   }
   
   strategy = new SimpleStrategy(
      new BandOsMaSignal(p.fastp.slowSignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}

Напоминаем, что мы можем с помощью функции MQLInfoInteger определить все режимы работы эксперта, включая и те, что связаны с тестером и оптимизацией. Задав в качестве параметра один из элементов перечисления ENUM_MQL_INFO_INTEGER, мы получим в результате логический признак (true/false):

  • MQL_TESTER - программа работает в тестере;
  • MQL_VISUAL_MODE - тестер запущен в визуальном режиме;
  • MQL_OPTIMIZATION - тестовый проход выполняется в ходе оптимизации (а не отдельно);
  • MQL_FORWARD - тестовый проход выполняется на форвард-периоде после оптимизации (если задано настройками оптимизации);
  • MQL_FRAME_MODE - эксперт запущен в особом сервисном режиме на графике терминала (а не на агенте) для управления оптимизацией (об этом подробнее в следующем разделе).

Режимы работы MQL-программ, связанные с тестером

Режимы работы MQL-программ, связанные с тестером

Все готово для запуска оптимизации. Сразу в момент её начала, при упоминавшихся настройках Presets/MQL5Book/BandOsMA.set, мы увидим сообщение в журнале Эксперты терминала:

Parameter FastSlowCombo4Optimization is enabled with maximum: 698

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

...
Best result 91.02452934181422 produced at generation 39. Next generation 42
Best result 91.56338892567393 produced at generation 42. Next generation 43
Best result 91.71026391877101 produced at generation 43. Next generation 44
Best result 91.71026391877101 produced at generation 43. Next generation 45
Best result 92.48460871443507 produced at generation 45. Next generation 46
...

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

Но у нашего решения есть и один минус. Теперь в рабочих настройках эксперта фигурирует не пара периодов в параметрах FastOsMA и SlowOsMA, а порядковый номер их комбинации среди всех возможных (FastSlowCombo4Optimization). Единственное, что мы можем сделать, это выводить раскодированные периоды в функции OnInit, что и было продемонстрировано выше.

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

MA periods are restored from shadow: FastOsMA=27 SlowOsMA=175

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