Как реализовать свой критерий оптимизации
Введение
Время от времени высказываются мнения о необходимости расширения набора критериев оптимизации в тестере 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.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
На форуме был вопрос 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Ну и дальше симметрично можно достраивать. Хотя при большом числе параметров возможно лучше сохранять вектор текущих значений индексов в файл и модифицировать его по ходу оптимизации. Но с ростом числа параметров и полный перебор становится всё менее реалистичной целью.
Нет, неправильно, нужно один раз запустить оптимизацию по счётчику, как описано в статье.
Похоже, что мне нужно тестер как следует изучить.
Вы привели пример критерия, по которому можно отсортировать полученные результаты... Зачем? Ведь все это можно сделать в том же экселе...
Другое дело, если б ваш критерий мог отсеять, как лишние найденные варианты по заданному значению, не в конце оптимизации, а во время каждого прохода....
Возможен ли такой вариант? Тогда бы время оптимизации значительно сократилось..