| / | Статьи |
Cтатьи
Тестер
Как реализовать свой критерий оптимизации
Авторизуйтесь или зарегистрируйтесь , чтобы добавить новую статью
|
Как реализовать свой критерий оптимизации [ en ]ВведениеВремя от времени высказываются мнения о необходимости расширения набора критериев оптимизации в тестере 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. Запускаем оптимизацию. По окончании идем в папку [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. Предупреждение:
все права на данные материалы
принадлежат MetaQuotes Software Corp. Полная или частичная перепечатка запрещена.
Вы привели пример критерия, по которому можно отсортировать полученные результаты... Зачем? Ведь все это можно сделать в том же экселе... Другое дело, если б ваш критерий мог отсеять, как лишние найденные варианты по заданному значению, не в конце оптимизации, а во время каждого прохода.... Возможен ли такой вариант? Тогда бы время оптимизации значительно сократилось..
19.05.2008 14:58 kharko
lna01 писал(а): Нет, неправильно, нужно один раз запустить оптимизацию по счётчику, как описано в статье. Похоже, что мне нужно тестер как следует изучить.
14.12.2007 12:01 Martes
Нет, неправильно, нужно один раз запустить оптимизацию по счётчику,
как описано в статье.
05.12.2007 08:05 Candid
Правильно ли я понимаю, что в данном примере для оптимизации нам
придется 200 раз вручную запускать советника?
04.12.2007 16:45 Martes
На форуме был вопрос 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Ну и дальше симметрично можно достраивать. Хотя при большом числе параметров возможно лучше сохранять вектор текущих значений индексов в файл и модифицировать его по ходу оптимизации. Но с ростом числа параметров и полный перебор становится всё менее реалистичной целью.
05.09.2007 12:14 Candid
getch писал(а): Хороший приём. Действительно количество необходимых внешних
переменных нарастает очень быстро. Пример более компактной реализации объявления внешних переменных:
08.08.2007 12:04 Candid
Пример более компактной реализации объявления внешних переменных: #define STR_SHABLON "<!--D-->" #define MAX_SIZE 100 string Shablon1 = "Begin = <!--D-->, End = <!--D-->, Step = <!--D-->"; extern string Str1 = "Begin = 0.12, End = 3.45, Step = 1.23"; string Shablon2 = "<!--D-->; Var23 = <!--D-->; Offset = <!--D-->; Maximum = <!--D-->"; extern string Str2 = "78; Var23 = 23.5; Offset = 56; Maximum = 1000"; string Shablon3 = "Seq: <!--D-->, <!--D-->"; extern string Str3 = "Seq: 12, 34.56, 8, 9, 0, 5.68"; double Example[MAX_SIZE]; int start() { int Count, i; Count = StrToDoubleS(Str1, Shablon1); Print(Str1); for (i = 0; i < Count; i++) Print(i + ": " + Example[i]); Count = StrToDoubleS(Str2, Shablon2); Print(Str2); for (i = 0; i < Count; i++) Print(i + ": " + Example[i]); Count = StrToDoubleS(Str3, Shablon3); Print(Str3); for (i = 0; i < Count; i++) Print(i + ": " + Example[i]); return(0); }Сама функция int StrToDoubleS( string Str, string Shablon ) берет из строчки соотетствующие числовые значения согласно заданному шаблону. Возвращает количество взятых чисел. Исходный текст функции: int StrToDoubleS( string Str, string Shablon ) { string StrTemp; int Pos, LengthSh; double Sum = 0; int Count = 0; LengthSh = StringLen(STR_SHABLON); Pos = StringFind(Shablon, STR_SHABLON); while (Pos != -1) { if (Pos != 0) StrTemp = StringSubstr(Shablon, 0, Pos); else StrTemp = ""; Shablon = StringSubstr(Shablon, Pos + LengthSh); Pos = StringFind(Str, StrTemp); if (Pos == -1) return(Count); Pos += StringLen(StrTemp); Str = StringSubstr(Str, Pos); Example[Count] = StrToDouble(Str); Count++; Pos = StringFind(Shablon, STR_SHABLON); } Pos = StringFind(Str, StrTemp); LengthSh = StringLen(StrTemp); while(Pos != -1) { Pos += LengthSh; Str = StringSubstr(Str, Pos); Example[Count] = StrToDouble(Str); Count++; Pos = StringFind(Str, StrTemp); } return(Count); }
07.08.2007 17:19 getch
7 комментариев
|