Как реализовать свой критерий оптимизации

Автор: lexy Вторник, Сентябрь 9th, 2014 Нет комментариев

Рубрика: Разное

Время от времени высказываются мнения о необходимости расширения набора критериев оптимизации в тестере MT4. Можно предположить однако, что какие бы критерии не добавлялись разработчиками, всегда будут пользователи и ситуации для которых нужного среди них не найдётся. Есть ли выход из положения в рамках MQL4 и платформы MetaTrader? Да, есть. В предлагаемой статье на примере стандартного советника Moving Average реализовано применение пользовательского критерия оптимизации. В качестве такового выбрано отношение прибыль/просадка.

Советник

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

   if (AccountEquity() > MaxEqu) MaxEqu = AccountEquity();
   if (MaxEqu-AccountEquity() > MaxDD) MaxDD = MaxEqu-AccountEquity();

Для обработки последнего тика их необходимо продублировать в deinit(). После этого можно рассчитать значение критерия оптимизации.

    Criterion = (AccountBalance()-StartBalance)/MaxDD;

Теперь можно заняться главным — сопровождением процесса оптимизации. У нас есть проблема: в MQL4 отсутствует штатное средство определения момента окончания оптимизации. Единственным известным автору способом её решения является так называемая «оптимизация по счётчику». Суть приёма в том, что единственным варьируемым параметром советника делается специальная внешняя переменная-счётчик. Возникает, однако, одно серьёзное последствие — мы лишаемся возможности варьировать реальные параметры советника штатным образом и должны организовывать это самостоятельно. Другая неприятность состоит в превращении кеша оптимизации из нашего союзника в нашего врага. Но поставленная цель окупит эти издержки, поэтому продолжим.

Добавим внешние переменные:

extern int Counter                    = 1;    // Счётчик проходов тестера
extern int TestsNumber                = 200;  // Контрольная цифра - общее число проходов
extern int MovingPeriodStepsNumber    = 20;   // Число шагов оптимизации для MovingPeriod
extern int MovingShiftStepsNumber     = 10;   // Число шагов оптимизации для MovingShift
extern double MovingPeriodLow         = 150;  // Нижняя граница диапазона оптимизации для MovingPeriod
extern double MovingShiftLow          = 1;    // Нижняя граница диапазона оптимизации для MovingShift
extern double MovingPeriodStep        = 1;    // Шаг оптимизации для MovingPeriod
extern double MovingShiftStep         = 1;    // Шаг оптимизации для MovingShift

Первым идёт тот самый счётчик проходов. Следующая переменная — контрольная (и справочная). Далее для двух предназначенных к оптимизации штатных переменных советника Moving Average задаётся число шагов, нижний предел и шаг оптимизации. Легко заметить некоторую избыточность: если мы собираемся делать полный перебор (а именно его мы собираемся делать) произведение MovingPeriodStepsNumber и MovingShiftStepsNumber должно быть равно TestsNumber.

После каждого прохода тестирования советник полностью завершает работу и следующий проход можно считать его реинкарнацией. У нас есть два средства для организации «генетической памяти»: глобальные переменные и запись в файл. Будут использованы оба.

Модифицируем функцию init():

int init() {
  if (IsTesting() && TestsNumber > 0) {
    if (GlobalVariableCheck("FilePtr")==false || Counter == 1) {
      FilePtr = 0;
      GlobalVariableSet("FilePtr",0);
    } else {
      FilePtr = GlobalVariableGet("FilePtr");
    }
    MovingPeriod = MovingPeriodLow+((Counter-1)/MovingShiftStepsNumber)*MovingPeriodStep;
    MovingShift = MovingShiftLow+((Counter-1)%MovingShiftStepsNumber)*MovingShiftStep;
    StartBalance = AccountBalance();
    MaxEqu = 0;
    MaxDD = 0;
  }
  return(0);
}

Наша добавка расположена внутри условия работы только в тестере и при отличном от нуля TestsNumber. Таким образом задание TestsNumber=0 превратит советник обратно в стандартный Moving Average. Поскольку речь идёт об оптимизации, мы должны использовать любую возможность для ускорения процесса. По этой причине код начинается с обеспечения поддержки сквозного (сквозь проходы тестера) указателя файловой позиции с помощью глобальной переменной . Затем идут расчёт значений варьируемых параметров и инициализация переменных, используемых для расчёта критерия оптимизации.

Основную работу предстоит проделать в функции deinit(). По результатам тестирования будем сохранять в текстовом файле значение критерия оптимизации, значения оптимизируемых параметров и номер прохода тестера. По окончании оптимизации её результаты будут отсортированы по критерию оптимизации и сохранены в тот же файл. Таким образом, мы должны обработать три ситуации: первый запуск, последний запуск и всё остальное. Для их разделения будем использовать счётчик проходов тестера (Counter). Обрабатываем первый запуск:

    if (Counter == 1) {
// Первый проход, создаём/обнуляем файл данных.
      h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,';');
      FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
// Запомним в глобальной переменной положение файлового указателя после записи
      FilePtr = FileTell(h);
      GlobalVariableSet("FilePtr",FilePtr);
      FileClose(h);

Особенность обработки последующих запусков состоит в том, что данные в файл дописываются:

    } else {
//  После того как первый запуск обработан, данные в файл будем дописывать
      h=FileOpen("test.txt",FILE_CSV|FILE_READ|FILE_WRITE,';');
// Пришло время воспользоваться записанным в глобальной переменной файловым указателем
      FilePtr = GlobalVariableGet("FilePtr");
      FileSeek(h,FilePtr, SEEK_SET);
      FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
// И снова запомним положение файлового указателя
      FilePtr = FileTell(h);
      GlobalVariableSet("FilePtr",FilePtr);

В этом месте займёмся обработкой последнего запуска:

      if (Counter == TestsNumber) {
        ArrayResize(Data,TestsNumber);
// Возвращаем файловый указатель в начало
        FileSeek(h,0,SEEK_SET);
// Читаем результаты всех тестирований из файла
        int i = 0;
        while (i<TestsNumber && FileIsEnding(h)== false) {
          for (int j=0;j<4;j++) {
            Data[i][j]=FileReadNumber(h);
          }
          i++;
        }
// И сортируем массив по нашему критерию оптимизации
        ArraySort(Data,WHOLE_ARRAY,0,MODE_DESCEND);
// Пожалуй немного оформим результат. Для этого придётся переоткрыть файл
        FileClose(h);
        h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,' ');
        FileWrite(h,"  Критерий","     MovingPeriod"," MovingShift"," Счётчик");
        for (i=0;i<TestsNumber;i++) {
          FileWrite(h,DoubleToStr(Data[i][0],10),"        ",Data[i][1],"        ",Data[i][2],"        ",Data[i][3]);
        }

Массив был заранее объявлен как double Data[][4]. Вот собственно и всё, осталось убрать за собой:

        GlobalVariableDel("FilePtr");
      }
      FileClose(h);
    }
  }

Компилируем, открываем тестер, выбираем наш советник. После этого открываем окно свойств советника и проверяем четыре вещи:

- Произведение MovingPeriodStepsNumber на MovingShiftStepsNumber ДОЛЖНО быть равно TestsNumber.
- Оптимизация должна делаться ТОЛЬКО для Counter,
- Диапазон оптимизации ДОЛЖЕН быть от 1 до TestsNumber с шагом 1.
- Генетический алгоритм должен быть отключён.

Запускаем оптимизацию. По окончании идем в папку [Meta Trader]\tester\files и смотрим результат в файле test.txt. Автор проделал это для EURUSD_H1 с середины 2004 г. по ценам открытия и увидел следующее:

В заключение вернёмся к упоминанию кеша оптимизации в качестве врага. Дело в том, что когда результаты тестирования берутся из кеша, функции init() и deinit() не запускаются. В результате при повторных запусках оптимизации все или часть вариантов могут оказаться неучтёнными. Более того, поскольку реальное число проходов окажется меньше TestsNumber, в массиве Data окажется некоторое количество нулей. Автору известны два способа перестраховки от «эффекта кеша»: перекомпиляция советника или закрытие/пауза/открытие окна тестера.
Вмешательство кеша можно детектировать с помощью независимого подсчёта проходов. Для организации такого подсчёта с помощью специальной глобальной переменной в прилагаемом к статье коде советника имеются три закомментированных вставки:

// Код независимого счётчика проходов
    if (GlobalVariableCheck("TestsCnt")==false || Counter == 1) {
      TestsCnt = 0;
      GlobalVariableSet("TestsCnt",0);
    } else {
      TestsCnt = GlobalVariableGet("TestsCnt");
    }
// Код независимого счётчика проходов
    TestsCnt++;
    GlobalVariableSet("TestsCnt",TestsCnt);
// Код независимого счётчика проходов
        GlobalVariableDel("TestsCnt");

И последнее. Внимательный читатель возможно обратил внимание на то, что без переменной FilePtr (и сопутствующей ей глобальной переменной) вполне можно обойтись — запись ведь всегда ведётся в конец файла а чтение с начала. Так для чего она в коде? Ответ будет таким: Данный советник предназначен для демонстрации метода сопровождения оптимизации. Метод позволяет организовать работу «на лету» с результатами предыдущих тестирований, и вот тут сквозной указатель файловой позиции может оказаться чрезвычайно полезным. Как и независимый счётчик тестирований. В качестве примера задач, требующих организации работы с предыдущими результатами «на лету», можно назвать организацию out-of-sample тестирования и реализацию собственного генетического алгоритма.

Заключение

Мотивом для внимания к данной проблеме послужила тема http://forum.mql4.com/ru/7531. Толчком к написанию советника послужила тема http://forum.mql4.com/ru/7605.

Прикрепленные файлы:
 Test Moving Average.mq4 (11.6 Kb)
Источник: mql4.com

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты