English 中文 Español Deutsch 日本語 Português
Как реализовать свой критерий оптимизации

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

MetaTrader 4Тестер | 3 августа 2007, 11:54
3 879 7
Nikolai Shevchuk
Nikolai Shevchuk

Введение

Время от времени высказываются мнения о необходимости расширения набора критериев оптимизации в тестере 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 тестирования и реализацию собственного генетического алгоритма.



Заключение

Мотивом для внимания к данной проблеме послужила тема https://www.mql5.com/ru/forum/104152. Толчком к написанию советника послужила тема https://www.mql5.com/ru/forum/104222.

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (7)
Candid
Candid | 5 сент. 2007 в 10:14

На форуме был вопрос Gull'а по организации перебора более чем двух параметров. Вроде получается так:

Все возможные наборы параметров представим(мысленно) в виде многомерного массива. Таким образом есть текущие значения индексов i, j, k, l, ... и их диапазоны I, J, K, L, ... . Тогда имеем для двух параметров:

i = (Counter-1) % I

j = (Counter-1) / I

для трёх:

i = (Counter-1) % I

j = (Counter-1) / I % J

k = (Counter-1) / I / J

для четырёх:

i = (Counter-1) % I

j = (Counter-1) / I % J

k = (Counter-1) / I / J % K

l = (Counter-1) / I / J / K

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

Martes
Martes | 4 дек. 2007 в 15:45
Правильно ли я понимаю, что в данном примере для оптимизации нам придется 200 раз вручную запускать советника?
Candid
Candid | 5 дек. 2007 в 07:05
Нет, неправильно, нужно один раз запустить оптимизацию по счётчику, как описано в статье.
Martes
Martes | 14 дек. 2007 в 11:01
lna01:
Нет, неправильно, нужно один раз запустить оптимизацию по счётчику, как описано в статье.

Похоже, что мне нужно тестер как следует изучить.
Alexei Kharchenko
Alexei Kharchenko | 19 мая 2008 в 12:58

Вы привели пример критерия, по которому можно отсортировать полученные результаты... Зачем? Ведь все это можно сделать в том же экселе...

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

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

Язык MQL4 для "чайников". Технические индикаторы и встроенные функции Язык MQL4 для "чайников". Технические индикаторы и встроенные функции
Это третья статья из цикла "Язык MQL4 для 'чайников'". Сейчас мы будем разбираться, как использовать встроенные функции и функции для работы с техническими индикаторами. Последние будут жизненно необходимы при разработке в дальнейшем ваших советников и индикаторов. Кроме того, мы на простом примере посмотрим, как можно отслеживать торговые сигналы для входа в рынок, что бы вы поняли, как правильно использовать индикаторы. А в конце статьи вы узнаете кое-что новенькое и интересное про сам язык.
Брейкпойнты в тестере – это возможно! Брейкпойнты в тестере – это возможно!
Статья посвящается программной эмуляции точки останова при прогоне на тестерe с выводом отладочной информации.
Универсальный шаблон экспертов Универсальный шаблон экспертов
Данная статья поможет начинающим трейдерам создавать гибко-настраиваемые эксперты.
Что такое Мартингейл и имеет ли смысл им пользоваться? Что такое Мартингейл и имеет ли смысл им пользоваться?
В статье содержится подробное описание системы мартингейл, а также точные математические вычисления необходимые для ответа на вопрос "Имеет ли смысл применять мартингейл?".