Продолжим серию статей по
программированию на MQL5. На этот раз рассмотрим, как можно получать
результаты по каждому проходу оптимизации во время оптимизации
параметров эксперта. При этом сделаем так, чтобы, если условие-(я), которое-(ые)
будет-(ут) настраиваться во внешних параметрах, исполняется, показатели
этого прохода будут записываться в файл. Кроме показателей тестов будем
сохранять ещё параметры, по которым был получен этот результат.
Для реализации задуманного возьмём уже готового эксперта с простым торговым алгоритмом из статьи Как устанавливать/модифицировать торговые уровни и не получить ошибку? и просто добавим туда все необходимые функции. Я подготовил код подобно тому, как это было сделано в последних статьях. То есть, все функции распределены по разным файлам и подключены к основному файлу проекта. Как подключать файлы к проекту можно посмотреть в статье Использование индикаторов для формирования условий в эксперте.
Чтобы получить доступ к данным во время оптимизации в MQL5 есть специальные функции: OnTesterInit(), OnTester(), OnTesterPass() и OnTesterDeinit(). Кратко рассмотрим каждую из них:
Теперь нужно разобраться, что такое фреймы. Фрейм это своего рода структура данных отдельного прохода оптимизации. Фреймы во время оптимизации сохраняются в архив *.mqd, который создаётся в каталоге MetaTrader 5/MQL5/Files/Tester. К данным (фреймам) этого архива можно обращаться, как во время оптимизации "на лету", так и после окончания оптимизации. Например, на сайте mql5.com в статье Визуализируй стратегию в тестере MetaTrader 5 показан пример того, как можно визуализировать процесс оптимизации "на лету" и затем включить просмотр всех результатов после оптимизации.
В этой статье мы будем использовать такие функции для работы с фреймами:
Более подробную информацию по всем вышеперечисленным функциям можно посмотреть в Справочном руководстве по языку MQL5.
Начнём, как всегда с внешних параметров. Ниже показано, какие параметры нужно добавить к тем, что уже есть (выделено ):
С помощью параметра OptimizationReport будем указывать программе, производить запись результатов и параметров в файл во время оптимизации или нет.
В данном примере сделаем возможность указывать до трёх критериев, по которым будут выбираться результаты для записи. Также добавим правило (параметр RuleCriterionSelection), когда можно указать, записывать результат, если исполнятся все указанные условия (AND) или хотя бы одно из них (OR). Для этого создадим в файле ENUM.mqh перечисление (см. код ниже):
В качестве критериев будут выступать основные показатели результатов тестирования. Для них тоже нужно создать перечисление:
Каждый из показателей будет проверяться на превышение указанного во внешних параметрах значения. Исключение только Equity DD Max %, так как нужно производить выбор ориентируясь на минимальную просадку.
Также нужно добавить несколько глобальных переменных (см. код ниже):
И ещё нам понадобятся вот такие массивы для работы (в файле ARRAYS.mqh):
В главном файле эксперта нужно добавить функции, которые описывались в начале статьи так, как это показано ниже:
Если сейчас запустить процесс оптимизации, то в терминале откроется график с символом и периодом, на котором запущен эксперт. Сообщения из функций, которые показаны в коде выше, будут выводиться в журнал терминала, а не в журнал тестера. При таком коде, как показано выше, в самом начале оптимизации в журнал будет выведено сообщение из функции OnTesterInit(). Но во время и по окончании оптимизации никаких сообщений Вы не увидите в журнале. После оптимизации, если удалить открытый тестером график, в журнал будет выведено сообщение из функции OnTesterDeinit(). В чём же дело?
Дело в том, что для корректной работы в функции OnTester() нужно использовать функцию FrameAdd() для добавления фрейма, как это показано в коде ниже.
Теперь во время оптимизации мы будем видеть, как после каждого прохода в журнал выводится сообщение из функции OnTesterPass(), а после окончания оптимизации из функции OnTesterDeinit() придёт сообщение об окончании оптимизации. Сообщение об окончании оптимизации придёт также и, если остановить оптимизацию вручную.
Теперь всё готово для того, чтобы сосредоточиться на функциях, в которых будут производиться создание каталогов и файлов, определение установленных на оптимизацию параметров и запись данных тех результатов, которые проходят по условиям.
Создадим файл OPTIMIZATION_REPORT.mqh и подключим его к проекту. В самом начале этого файла напишем функцию GetTestStatistic(), в которую будем передавать массив для заполнения показателями каждого очередного прохода во время оптимизации.
Функцию GetTestStatistic() нужно разместить перед добавлением фрейма, как это показано ниже:
В качестве последнего аргумента в функцию FrameAdd() передаётся массив, но при необходимости можно даже передать файл с данными.
Теперь можно проверить в функции OnTesterPass() полученные данные. Для проверки, как это работает, просто пока выведем в журнал терминала прибыль каждого результата. Для того, чтобы получить значения текущего фрейма, нужно использовать FrameNext(). Пример показан ниже:
Если не использовать функцию FrameNext(), то значения в массиве stat_values будут нулевые. Если же всё сделано правильно, то получим результат, как показано на скриншоте ниже:
Кстати, если запустить оптимизацию на изменяя внешние параметры, то результаты в тестер загрузятся из кэша минуя функции OnTesterPass() и OnTesterDeinit(). Об этом просто нужно помнить, чтобы не подумать, что где-то спряталась ошибка.
Далее создадим функцию CreateWriteOptimizationReport(). Именно в ней будут производиться все основные действия. Ниже представлен её код:
Получилась довольно большая функция, рассмотрим её подробнее. В самом начале после объявления переменных и массивов получаем данные фрейма с помощью функции FrameNext(), как это было показано выше в примерах. Далее с помощью функции FrameInputs() получаем список параметров в строковой массив list_params[] и общее количество параметров в переменную amount_params.
В списке параметров, который отдаёт нам функция FrameInputs(), те параметры, которые включены на оптимизацию (отмечены флажками в тестере) расположены в этом списке в самом начале независимо от того, в какой последовательности они идут в списке внешних параметров эксперта.
Затем идёт цикл, в котором производится перебор списка параметров. На самом первом проходе заполняется массив критериев criterion[] и массив значений критериев criterion_value[]. Подсчёт используемых критериев производится в функции CountUseCriteria() и только, если включен режим AND и текущий параметр последний. Ниже представлен код функции CountUseCriteria():
В этом же цикле далее, на каждом проходе, производится проверка на то, включен ли параметр на оптимизацию или нет. Для этого используется функция CheckParameterOptimization(), в которую передаётся текущий в цикле внешний параметр для проверки. Если функция возвращает true, то это значит, что параметр включен на оптимизацию. Ниже можно ознакомиться с кодом этой функции:
Если функция CheckParameterOptimization() сообщает, что параметр включен на оптимизацию, то заполняются массивы для названий param_name и значений параметров param_value. Массив для названий оптимизируемых параметров заполняется только на первом проходе.
Далее в двух циклах формируется строка значений показателей теста и значений параметров для записи в файл.
После этого с помощью функции CreateOptimizationReport() создаётся файл для записи на первом проходе. Ниже представлен код этой функции с подробными комментариями:
Цель функции CreateOptimizationReport() сформировать заголовки, создать при необходимости каталоги в общей папке терминала и также создать очередной файл для записи. То есть, файлы от предыдущих оптимизаций остаются и каждый раз создаётся новый файл с порядковым номером. После создания файла в него записываются заголовки. Сам файл остаётся открытым до конца оптимизации.
В коде выше выделена строка с функцией CheckCreateGetPath(). В ней создаются каталоги для сохранения файлов с результатами оптимизации.
Код функции CheckCreateGetPath():
В коде выше подробные комментарии и с ним не сложно будет разобраться, но выделим только основные моменты.
Сначала производится проверка на наличие корневой папки для результатов оптимизации DATA_OPTIMIZATION. Если такая папка есть, то это отмечается в переменной exist_root_folder.
Далее хэндл поиска устанавливается в папке DATA_OPTIMIZATION и уже в ней производится проверка наличия папки с именем эксперта.
Затем производится подсчёт файлов в папке эксперта. И наконец после этого, по результатам проверок, при необходимости (если папки не обнаружены), создаются папки и возвращается путь для нового файла с порядковым номером. Если была ошибка, то возвращается пустая строка.
Осталось рассмотреть функцию WriteOptimizationReport(), в которой производится проверка условий для записи данных в файл и запись, если условие выполняется. Ниже представлен код этой функции:
В коде выше выделены строки с функциями, которые рассмотрим ниже.
В зависимости от того, какое правило для проверки критериев выбрано, используется соответствующая функция. Если нужно, чтобы все указанные критерии совпадали, то используется функция AccessCriterionAND():
Если же нужно, чтобы хотя бы один из указанных критериев совпал, то используется функция AccessCriterionOR():
Функция GetAmountStrings() переводит указатель в конец файла и возвращает количество строк в файле (см. код ниже):
Всё готово. Теперь нужно функцию CreateWriteOptimizationReport() поместить в тело функции OnTesterPass(). А в функции OnTesterDeinit() закрыть файл результатов оптимизации.
Теперь протестируем эксперта. Оптимизируем его параметры в сети распределённых вычислений MQL5 Cloud Network. Настройки тестера нужно установить так, как показано на скриншоте ниже:
Установим на оптимизацию все параметры эксперта и настроим параметры критериев так, чтобы в файл записывались те результаты, у которых значение показателя Profit Factor выше 1, а Recovery Factor выше 2 (см. скриншот ниже):
Сеть распределённых вычислений пропустила через себя 101000 проходов всего лишь за ~5 минут! Если бы я не использовал сеть, то на оптимизацию бы ушло несколько дней. Отличная возможность для тех, кто ценит своё время.
Полученный файл можно теперь открыть в Excel. Из 101000 проходов было выбрано 719 результатов для записи в файл. На скриншоте ниже я выделил столбцы с теми показателями, по которым отбирались результаты для записи:
На этом закончим статью. На самом деле тема по анализу результатов оптимизации ещё далеко не раскрыта полностью и к этому вопросу мы ещё вернёмся в будущих статьях.
Ниже можно скачать архив с файлами эксперта для изучения.
Успехов!
Скачать эксперт WriteResOptByCriterion.zip
Для реализации задуманного возьмём уже готового эксперта с простым торговым алгоритмом из статьи Как устанавливать/модифицировать торговые уровни и не получить ошибку? и просто добавим туда все необходимые функции. Я подготовил код подобно тому, как это было сделано в последних статьях. То есть, все функции распределены по разным файлам и подключены к основному файлу проекта. Как подключать файлы к проекту можно посмотреть в статье Использование индикаторов для формирования условий в эксперте.
Чтобы получить доступ к данным во время оптимизации в MQL5 есть специальные функции: OnTesterInit(), OnTester(), OnTesterPass() и OnTesterDeinit(). Кратко рассмотрим каждую из них:
- - OnTesterInit() - с помощью этой функции определяется начало оптимизации.
- - OnTester() - в этой функции будет производиться добавление так называемых фреймов во время оптимизации после каждого прохода. Что такое фреймы будет объясняться ниже.
- - OnTesterPass() - эта функция принимает фреймы во время оптимизации после каждого прохода.
- - OnTesterDeinit() - в этой функции генерируется событие об окончании оптимизации параметров эксперта.
Теперь нужно разобраться, что такое фреймы. Фрейм это своего рода структура данных отдельного прохода оптимизации. Фреймы во время оптимизации сохраняются в архив *.mqd, который создаётся в каталоге MetaTrader 5/MQL5/Files/Tester. К данным (фреймам) этого архива можно обращаться, как во время оптимизации "на лету", так и после окончания оптимизации. Например, на сайте mql5.com в статье Визуализируй стратегию в тестере MetaTrader 5 показан пример того, как можно визуализировать процесс оптимизации "на лету" и затем включить просмотр всех результатов после оптимизации.
В этой статье мы будем использовать такие функции для работы с фреймами:
- - FrameAdd() - добавление данных из файла или из массива.
- - FrameNext() - вызов с получением одного числового значения либо всех данных фрейма.
- - FrameInputs() - получает input-параметры, на которых сформирован фрейм с заданным номером прохода.
Более подробную информацию по всем вышеперечисленным функциям можно посмотреть в Справочном руководстве по языку MQL5.
Начнём, как всегда с внешних параметров. Ниже показано, какие параметры нужно добавить к тем, что уже есть (выделено ):
//+------------------------------------------------------------------+ //| ВНЕШНИЕ ПАРАМЕТРЫ ЭКСПЕРТА | //+------------------------------------------------------------------+ input int AmountBars = 2; // Amount Bear/Bull Bars For Buy/Sell sinput double Lot = 0.1; // Lot input double TakeProfit = 100; // Take Profit (p) input double StopLoss = 50; // Stop Loss (p) input double TrailingSL = 10; // Trailing Stop (p) input bool ReversePosition = true; // On/Off Reverse //--- sinput string dlm=""; // * * * * * * * * * * * * * * * * * * * * * * * * * * sinput bool OptimizationReport = true; // On/Off Optimization Report sinput MODE_RULE RuleCriterionSelection = AND; // Rule Of Criterion Selection //--- sinput ENUM_STAT Criterion_01 = NO_CRITERION; // 01 _ Name Criterion sinput double ValCriter_01 = 0; // -- Value Criterion //--- sinput ENUM_STAT Criterion_02 = NO_CRITERION; // 02 _ Name Criterion sinput double ValCriter_02 = 0; // -- Value Criterion //--- sinput ENUM_STAT Criterion_03 = NO_CRITERION; // 03 _ Name Criterion sinput double ValCriter_03 = 0; // -- Value Criterion //---
С помощью параметра OptimizationReport будем указывать программе, производить запись результатов и параметров в файл во время оптимизации или нет.
В данном примере сделаем возможность указывать до трёх критериев, по которым будут выбираться результаты для записи. Также добавим правило (параметр RuleCriterionSelection), когда можно указать, записывать результат, если исполнятся все указанные условия (AND) или хотя бы одно из них (OR). Для этого создадим в файле ENUM.mqh перечисление (см. код ниже):
//+------------------------------------------------------------------+ //| ПЕРЕЧИСЛЕНИЕ ПРАВИЛ ПРОВЕРКИ ИСПОЛНЕНИЯ КРИТЕРИЕВ | //+------------------------------------------------------------------+ enum MODE_RULE { RULE_AND = 0, // AND RULE_OR = 1 // OR }; //---
В качестве критериев будут выступать основные показатели результатов тестирования. Для них тоже нужно создать перечисление:
//+------------------------------------------------------------------+ //| ПЕРЕЧИСЛЕНИЕ СТАТИСТИЧЕСКИХ ПОКАЗАТЕЛЕЙ | //+------------------------------------------------------------------+ enum ENUM_STAT { NO_CRITERION = 0, // NO CRITERION eSTAT_PROFIT = 1, // Profit eSTAT_DEALS = 2, // Total Deals eSTAT_PROFIT_FACTOR = 3, // Profit Factor eSTAT_EXPECTED_PAYOFF = 4, // Expected Payoff eSTAT_EQUITY_DDREL_PERCENT = 5, // Equity DD Max % eSTAT_RECOVERY_FACTOR = 6, // Recovery Factor eSTAT_SHARPE_RATIO = 7 // Sharpe Ratio }; //---
Каждый из показателей будет проверяться на превышение указанного во внешних параметрах значения. Исключение только Equity DD Max %, так как нужно производить выбор ориентируясь на минимальную просадку.
Также нужно добавить несколько глобальных переменных (см. код ниже):
//--- string opt_path=""; // Путь для сохранения папок и файлов int CountUseCriteria=0; // Количество используемых критериев //--- int opt_file_handle=-1; // Хэндл файла для записи результатов оптимизации //---
И ещё нам понадобятся вот такие массивы для работы (в файле ARRAYS.mqh):
//--- int criterion[3]; // Критерии для формирования отчёта оптимизации double criterion_value[3], // Значения критериев stat_values[AMOUNT_STAT_VALUE]; // Массив для показателей теста //--- // Массив входных оптимизируемых параметров string input_params[5]= { "AmountBars", "TakeProfit", "StopLoss", "TrailingSL", "ReversePosition" }; //---
В главном файле эксперта нужно добавить функции, которые описывались в начале статьи так, как это показано ниже:
//+------------------------------------------------------------------+ //| НАЧАЛО ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterInit() { Print(__FUNCTION__,"(): Start Optimization \n-----------"); } //+------------------------------------------------------------------+ //| РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ | //+------------------------------------------------------------------+ double OnTester() { // Если включена запись результатов оптимизации if(OptimizationReport) { } //--- return(0.0); } //+------------------------------------------------------------------+ //| ОЧЕРЕДНОЙ ПРОХОД ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterPass() { Print(__FUNCTION__,"(): ..."); //--- // Если включена запись результатов оптимизации if(OptimizationReport) { } } //+------------------------------------------------------------------+ //| ЗАВЕРШЕНИЕ ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterDeinit() { Print("-----------\n",__FUNCTION__,"(): End Optimization"); //--- // Если включена запись результатов оптимизации if(OptimizationReport) { } } //---
Если сейчас запустить процесс оптимизации, то в терминале откроется график с символом и периодом, на котором запущен эксперт. Сообщения из функций, которые показаны в коде выше, будут выводиться в журнал терминала, а не в журнал тестера. При таком коде, как показано выше, в самом начале оптимизации в журнал будет выведено сообщение из функции OnTesterInit(). Но во время и по окончании оптимизации никаких сообщений Вы не увидите в журнале. После оптимизации, если удалить открытый тестером график, в журнал будет выведено сообщение из функции OnTesterDeinit(). В чём же дело?
Дело в том, что для корректной работы в функции OnTester() нужно использовать функцию FrameAdd() для добавления фрейма, как это показано в коде ниже.
//+------------------------------------------------------------------+ //| РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ | //+------------------------------------------------------------------+ double OnTester() { // Если включена запись результатов оптимизации if(OptimizationReport) { // Создадим фрейм FrameAdd("Statistic",1,0,stat_values); } //--- return(0.0); } //---
Теперь во время оптимизации мы будем видеть, как после каждого прохода в журнал выводится сообщение из функции OnTesterPass(), а после окончания оптимизации из функции OnTesterDeinit() придёт сообщение об окончании оптимизации. Сообщение об окончании оптимизации придёт также и, если остановить оптимизацию вручную.
Теперь всё готово для того, чтобы сосредоточиться на функциях, в которых будут производиться создание каталогов и файлов, определение установленных на оптимизацию параметров и запись данных тех результатов, которые проходят по условиям.
Создадим файл OPTIMIZATION_REPORT.mqh и подключим его к проекту. В самом начале этого файла напишем функцию GetTestStatistic(), в которую будем передавать массив для заполнения показателями каждого очередного прохода во время оптимизации.
//+------------------------------------------------------------------+ //| ЗАПОЛНЯЕТ МАССИВ ДАННЫМИ РЕЗУЛЬТАТА ТЕСТА | //+------------------------------------------------------------------+ void GetTestStatistic(double &stat_array[]) { double pf=0,sr=0; // Вспомогательные переменные для корректировки значений //--- stat_array[0]=TesterStatistics(STAT_PROFIT); // 01 - Чистая прибыль по окончании тестирования stat_array[1]=TesterStatistics(STAT_DEALS); // 02 - Количество совершенных сделок //--- // 03 - Прибыльность – отношение STAT_GROSS_PROFIT/STAT_GROSS_LOSS pf=TesterStatistics(STAT_PROFIT_FACTOR); if(pf==DBL_MAX) { stat_array[2]=0; } else { stat_array[2]=pf; } //--- // 04 - Математическое ожидание выигрыша stat_array[3]=TesterStatistics(STAT_EXPECTED_PAYOFF); //--- // 05 - Максимальная просадка средств в процентах stat_array[4]=TesterStatistics(STAT_EQUITY_DDREL_PERCENT); //--- // 06 - Фактор восстановления – отношение STAT_PROFIT/STAT_BALANCE_DD stat_array[5]=TesterStatistics(STAT_RECOVERY_FACTOR); //--- // 07 - Коэффициент Шарпа - показатель эффективности инвестиционного портфеля (актива) sr=TesterStatistics(STAT_SHARPE_RATIO); if(pf==DBL_MAX) { stat_array[6]=0; } else { stat_array[6]=sr; } } //---
Функцию GetTestStatistic() нужно разместить перед добавлением фрейма, как это показано ниже:
//+------------------------------------------------------------------+ //| РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ | //+------------------------------------------------------------------+ double OnTester() { // Если включена запись результатов оптимизации if(OptimizationReport) { // Заполним массив показателями теста GetTestStatistic(stat_values); //--- // Создадим фрейм FrameAdd("Statistic",1,0,stat_values); } //--- return(0.0); } //---
В качестве последнего аргумента в функцию FrameAdd() передаётся массив, но при необходимости можно даже передать файл с данными.
Теперь можно проверить в функции OnTesterPass() полученные данные. Для проверки, как это работает, просто пока выведем в журнал терминала прибыль каждого результата. Для того, чтобы получить значения текущего фрейма, нужно использовать FrameNext(). Пример показан ниже:
//+------------------------------------------------------------------+ //| ОЧЕРЕДНОЙ ПРОХОД ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterPass() { // Если включена запись результатов оптимизации if(OptimizationReport) { string name=""; // публичное имя/метка фрейма ulong pass=0; // номер прохода в оптимизации, на котором добавлен фрейм long id=0; // публичный id фрейма double val=0.0; // Одиночное числовое значение фрейма //--- FrameNext(pass,name,id,val,stat_values); //--- Print(__FUNCTION__,"(): pass: "+IntegerToString(pass)+"; STAT_PROFIT: ",DoubleToString(stat_values[0],2)); } } //---
Если не использовать функцию FrameNext(), то значения в массиве stat_values будут нулевые. Если же всё сделано правильно, то получим результат, как показано на скриншоте ниже:
Кстати, если запустить оптимизацию на изменяя внешние параметры, то результаты в тестер загрузятся из кэша минуя функции OnTesterPass() и OnTesterDeinit(). Об этом просто нужно помнить, чтобы не подумать, что где-то спряталась ошибка.
Далее создадим функцию CreateWriteOptimizationReport(). Именно в ней будут производиться все основные действия. Ниже представлен её код:
//+------------------------------------------------------------------+ //| СОЗДАЁТ И ЗАПИСЫВАЕТ ОТЧЁТ РЕЗУЛЬТАТОВ ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void CreateWriteOptimizationReport() { static int count_pass=0; // Счётчик проходов count_pass++; //--- int amount_params=0; // Кол-во параметров эксперта int count_opt_param=0; // Счётчик оптимизируемых параметров string string_write=""; // Строка для записи bool criteria_list=false; // Для определения начала списка параметров-критериев int pos_number=0; // Номер позиции в строке //--- string name=""; // публичное имя/метка фрейма ulong pass=0; // номер прохода в оптимизации, на котором добавлен фрейм long id=0; // публичный id фрейма double val=0.0; // Одиночное числовое значение фрейма //--- string list_params[]; // Список параметров эксперта вида "parameterN=valueN" string param_name[]; // Массив имён параметров string param_value[]; // Массив значений параметров //--- // Поместим статистические показатели в массив FrameNext(pass,name,id,val,stat_values); //--- // Получим номер прохода, список параметров, кол-во параметров FrameInputs(pass,list_params,amount_params); //--- // Пройдём в цикле по списку параметров (от верхнего в списке) // В начале списка идут параметры, которые отмечены флажками на оптимизацию for(int i=0; i<amount_params; i++) { if(count_pass==1) // На первом проходе получим критерии для отбора результатов { string prm_value=""; // Значение параметра static int c=0,v=0,trigger=0; // Счётчики и триггер //--- // Установим флаг, если дошли до списка критериев if(StringFind(list_params[i],"RuleCriterionSelection",0)>=0) { criteria_list=true; continue; } //--- // На последнем параметре посчитаем используемые критерии, // если выбран режим AND (И) if(RuleCriterionSelection==RULE_AND && i==amount_params-1) { CountUseCriteria(); } //--- // Если дошли до критериев в списке параметров if(criteria_list) { if(trigger==0) // Определяем имена критериев { pos_number=StringFind(list_params[i],"=",0)+1; // Определим позицию в строке prm_value=StringSubstr(list_params[i],pos_number); // Возьмём значение параметра //--- criterion[c]=(int)StringToInteger(prm_value); trigger=1; // Следующий параметр будет значением c++; continue; } //--- if(trigger==1) // Определяем значения критериев { pos_number=StringFind(list_params[i],"=",0)+1; // Определим позицию в строке prm_value=StringSubstr(list_params[i],pos_number); // Возьмём значение параметра //--- criterion_value[v]=StringToDouble(prm_value); trigger=0; // Следующий параметр будет критерием v++; continue; } } } //--- // Если параметр включен на оптимизацию... if(CheckParameterOptimization(list_params[i])) { // Увеличим счётчик оптимизируемых параметров count_opt_param++; //--- // Названия оптимизируемых параметров заносим в массив // только на первом проходе (для заголовков) if(count_pass==1) { ArrayResize(param_name,count_opt_param); // Массив названий параметров pos_number=StringFind(list_params[i],"=",0)+1; // Определим позицию param_name[i]=StringSubstr(list_params[i],0,pos_number-1); // Возьмём имя параметра } //--- ArrayResize(param_value,count_opt_param); // Массив значений параметров pos_number=StringFind(list_params[i],"=",0)+1; // Определим позицию param_value[i]=StringSubstr(list_params[i],pos_number); // Возьмём значение параметра } } //--- // Сформируем строку до оптимизируемых параметров for(int i=0; i<AMOUNT_STAT_VALUE; i++) { StringAdd(string_write,DoubleToString(stat_values[i],2)+","); } //--- // Дополним строку оптимизируемыми параметрами for(int i=0; i<count_opt_param; i++) { // Если последнее значение в строке if(i==count_opt_param-1) { StringAdd(string_write,param_value[i]); break; } // без разделителя else { StringAdd(string_write,param_value[i]+","); } // с разделителем } //--- // На первом проходе создадим файл-отчёт оптимизации с заголовками if(count_pass==1) { CreateOptimizationReport(param_name); } //--- // Запишем данные в файл результатов оптимизации WriteOptimizationReport(string_write); } //---
Получилась довольно большая функция, рассмотрим её подробнее. В самом начале после объявления переменных и массивов получаем данные фрейма с помощью функции FrameNext(), как это было показано выше в примерах. Далее с помощью функции FrameInputs() получаем список параметров в строковой массив list_params[] и общее количество параметров в переменную amount_params.
В списке параметров, который отдаёт нам функция FrameInputs(), те параметры, которые включены на оптимизацию (отмечены флажками в тестере) расположены в этом списке в самом начале независимо от того, в какой последовательности они идут в списке внешних параметров эксперта.
Затем идёт цикл, в котором производится перебор списка параметров. На самом первом проходе заполняется массив критериев criterion[] и массив значений критериев criterion_value[]. Подсчёт используемых критериев производится в функции CountUseCriteria() и только, если включен режим AND и текущий параметр последний. Ниже представлен код функции CountUseCriteria():
//+------------------------------------------------------------------+ //| ВОЗВРАЩАЕТ КОЛИЧЕСТВО ИСПОЛЬЗУЕМЫХ КРИТЕРИЕВ | //+------------------------------------------------------------------+ void CountUseCriteria() { CountUseCriteria=0; // Обнуление //--- int size_array=ArraySize(criterion); // Получим размер массива критериев //--- // Пройдём в цикле по списку критериев и... for(int i=0; i<size_array; i++) { //...посчитаем используемые if(criterion[i]!=NO_CRITERION) { CountUseCriteria++; } } } //---
В этом же цикле далее, на каждом проходе, производится проверка на то, включен ли параметр на оптимизацию или нет. Для этого используется функция CheckParameterOptimization(), в которую передаётся текущий в цикле внешний параметр для проверки. Если функция возвращает true, то это значит, что параметр включен на оптимизацию. Ниже можно ознакомиться с кодом этой функции:
//+------------------------------------------------------------------+ //| ПРОВЕРЯЕТ, УСТАНОВЛЕН ЛИ ВНЕШНИЙ ПАРАМЕТР НА ОПТИМИЗАЦИЮ | //+------------------------------------------------------------------+ bool CheckParameterOptimization(string param) { bool enable; long prm,prm_start,prm_step,prm_stop; //--- // Определим позицию символа "=" в строке int pos_number=StringFind(param,"=",0); //--- // Получим значения параметра ParameterGetRange(StringSubstr(param,0,pos_number), enable,prm,prm_start,prm_step,prm_stop); //--- return(enable); // Вернём состояние параметра } //---
Если функция CheckParameterOptimization() сообщает, что параметр включен на оптимизацию, то заполняются массивы для названий param_name и значений параметров param_value. Массив для названий оптимизируемых параметров заполняется только на первом проходе.
Далее в двух циклах формируется строка значений показателей теста и значений параметров для записи в файл.
После этого с помощью функции CreateOptimizationReport() создаётся файл для записи на первом проходе. Ниже представлен код этой функции с подробными комментариями:
//+------------------------------------------------------------------+ //| СОЗДАЁТ ФАЙЛ ОТЧЁТ ПРОХОДОВ ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void CreateOptimizationReport(string ¶m_name[]) { int count_files=1; // Счётчик файлов оптимизации int size_param_name=ArraySize(param_name); // Получим размер массива //--- // Сформируем заголовок до оптимизируемых параметров string Headers="№,PROFIT,TOTAL DEALS,PROFIT FACTOR," "EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,"; //--- // Дополним заголовок оптимизируемыми параметрами for(int i=0; i<size_param_name; i++) { if(i==size_param_name-1) StringAdd(Headers,param_name[i]); else StringAdd(Headers,param_name[i]+","); } //--- // Получим путь для создания файла оптимизации и // кол-во файлов для порядкового номера opt_path=CheckCreateGetPath(count_files); //--- // Если ошибка при получении директории, выходим if(opt_path=="") { Print("Empty path: ",opt_path); return; } else { opt_file_handle=FileOpen(opt_path+"\optimization_results"+IntegerToString(count_files)+".csv", FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_COMMON,","); //--- if(opt_file_handle!=INVALID_HANDLE) { FileWrite(opt_file_handle,Headers); } } } //---
Цель функции CreateOptimizationReport() сформировать заголовки, создать при необходимости каталоги в общей папке терминала и также создать очередной файл для записи. То есть, файлы от предыдущих оптимизаций остаются и каждый раз создаётся новый файл с порядковым номером. После создания файла в него записываются заголовки. Сам файл остаётся открытым до конца оптимизации.
В коде выше выделена строка с функцией CheckCreateGetPath(). В ней создаются каталоги для сохранения файлов с результатами оптимизации.
Код функции CheckCreateGetPath():
//+------------------------------------------------------------------+ //| ПРОВЕРЯЕТ КАТАЛОГ РЕЗУЛЬТАТОВ ОПТИМИЗАЦИИ И | //| ПРИ НЕОБХОДИМОСТИ СОЗДАЁТ НУЖНЫЕ ПАПКИ | //+------------------------------------------------------------------+ string CheckCreateGetPath(int &count_files) { long handle_search=-1; // Хэндл поиска string file_name="",// Имя найденного файла/папки при поиске path=""; // Директория для поиска файла/папки //--- string file="*.csv",// Любой csv-файл folder="*"; // Любой файл/папка для поиска string root_folder="DATA_OPTIMIZATION\\",// Корневая папка folder_expert=NAME_EXPERT+"\\"; // Папка эксперта //--- bool exist_root_folder=false,// Признак существования корневой папки exist_folder_expert=false; // Признак существования папки эксперта //--- //______________________________________________________________ // Ищем корневую папку DATA_OPTIMIZATION в общей папке терминала path=folder; //--- // Установим хэндл поиска в папке ..\File\ handle_search=FileFindFirst(path,file_name,FILE_COMMON); //--- // Выведем в журнал путь к общей папке терминала Print("TERMINAL_COMMONDATA_PATH: ",COMMONDATA_PATH); //--- // Если первая папка корневая, ставим флаг if(file_name==root_folder) { exist_root_folder=true; Print("Корневая папка "+root_folder+" существует."); } //--- // Если хэндл поиска получен if(handle_search!=INVALID_HANDLE) { if(!exist_root_folder) // Если первая папка была не корневой { // Перебираем все файлы с целью поиска корневой папки while(FileFindNext(handle_search,file_name)) { // Если находим, то ставим флаг if(file_name==root_folder) { exist_root_folder=true; Print("Корневая папка "+root_folder+" существует."); break; } } } //--- // Закроем хэндл поиска корневой папки FileFindClose(handle_search); handle_search=-1; } else { Print("Ошибка при получении хэндла поиска " "либо директория "+COMMONDATA_PATH+" пуста: ",ErrorDesc(GetLastError())); } //--- //______________________________________________ // Ищем папку эксперта в папке DATA_OPTIMIZATION path=root_folder+folder; //--- // Установим хэндл поиска в папке ..\Files\DATA_OPTIMIZATION\ handle_search=FileFindFirst(path,file_name,FILE_COMMON); //--- // Если первая папка и есть папка эксперта if(file_name==folder_expert) { exist_folder_expert=true; // Запомним это Print("Папка эксперта "+folder_expert+" существует."); } //--- if(handle_search!=INVALID_HANDLE) // Если хэндл поиска получен { // Если первая папка была не папкой эксперта if(!exist_folder_expert) { // Перебираем все файлы в папке DATA_OPTIMIZATION с целью поиска папки эксперта while(FileFindNext(handle_search,file_name)) { // Если находим, то ставим флаг if(file_name==folder_expert) { exist_folder_expert=true; Print("Папка эксперта "+folder_expert+" существует."); break; } } } //--- // Закроем хэндл поиска корневой папки FileFindClose(handle_search); handle_search=-1; } else { Print("Ошибка при получении хэндла поиска либо директория "+path+" пуста."); } //--- //____________________________________ // Сформируем путь для подсчёта файлов path=root_folder+folder_expert+folder; //--- // Установим хэндл поиска в папке результатов оптимизации ..\Files\DATA_OPTIMIZATION\OPTn.csv handle_search=FileFindFirst(path,file_name,FILE_COMMON); //--- // Если папка не пуста, откроем счёт if(StringFind(file_name,"optimization_results",0)>=0) { count_files++; } //--- // Если хэндл поиска получен if(handle_search!=INVALID_HANDLE) { // Перебираем все файлы в папке эксперта с целью подсчёта while(FileFindNext(handle_search,file_name)) { count_files++; } //--- Print("Всего файлов: ",count_files); //--- // Закроем хэндл поиска папки эксперта FileFindClose(handle_search); handle_search=-1; } else { Print("Ошибка при получении хэндла поиска либо директория "+path+" пуста"); } //--- //______________________________________________ // По результатам проверки создадим нужные папки if(!exist_root_folder) // Если нет корневой папки DATA_OPTIMIZATION { if(FolderCreate("DATA_OPTIMIZATION",FILE_COMMON)) { exist_root_folder=true; Print("Создана корневая папка ..\Files\DATA_OPTIMIZATION\\"); } else { Print("Ошибка при создании корневой папки DATA_OPTIMIZATION: ", ErrorDesc(GetLastError())); return(""); } } //--- if(!exist_folder_expert) // Если нет папки эксперта { if(FolderCreate(root_folder+NAME_EXPERT,FILE_COMMON)) { exist_folder_expert=true; Print("Создана папка эксперта ..\Files\DATA_OPTIMIZATION\\"+folder_expert+""); } else { Print("Ошибка при создании папки эксперта ..\Files\\"+folder_expert+"\: ", ErrorDesc(GetLastError())); return(""); } } //--- // Если нужные папки есть... if(exist_root_folder && exist_folder_expert) { // ...вернём путь, в котором будет создан файл // для записи результатов оптимизации return(root_folder+NAME_EXPERT); } //--- return(""); } //---
В коде выше подробные комментарии и с ним не сложно будет разобраться, но выделим только основные моменты.
Сначала производится проверка на наличие корневой папки для результатов оптимизации DATA_OPTIMIZATION. Если такая папка есть, то это отмечается в переменной exist_root_folder.
Далее хэндл поиска устанавливается в папке DATA_OPTIMIZATION и уже в ней производится проверка наличия папки с именем эксперта.
Затем производится подсчёт файлов в папке эксперта. И наконец после этого, по результатам проверок, при необходимости (если папки не обнаружены), создаются папки и возвращается путь для нового файла с порядковым номером. Если была ошибка, то возвращается пустая строка.
Осталось рассмотреть функцию WriteOptimizationReport(), в которой производится проверка условий для записи данных в файл и запись, если условие выполняется. Ниже представлен код этой функции:
//+------------------------------------------------------------------+ //| ПРОИЗВОДИТ ЗАПИСЬ РЕЗУЛЬТАТОВ ОПТИМИЗАЦИИ ПО КРИТЕРИЯМ | //+------------------------------------------------------------------+ void WriteOptimizationReport(string string_write) { bool condition=false; // Для проверки условия //--- // Если хотя бы один критерий выполняется if(RuleCriterionSelection==RULE_OR) { condition=AccessCriterionOR(); } //--- // Если все критерии выполняются if(RuleCriterionSelection==RULE_AND) { condition=AccessCriterionAND(); } //--- // Если условия по критериям выполняются if(condition) { // Если файл для записи результатов оптимизации открыт if(opt_file_handle!=INVALID_HANDLE) { int amount_strings=0; // Счётчик строк //--- // Получим количество строк в файле и переместим указатель в конец amount_strings=GetAmountStrings(); //--- // Запишем строку с критериями FileWrite(opt_file_handle,IntegerToString(amount_strings),string_write); } else { Print("Хэндл файла для записи результатов оптимизации не валиден!"); } } } //---
В коде выше выделены строки с функциями, которые рассмотрим ниже.
В зависимости от того, какое правило для проверки критериев выбрано, используется соответствующая функция. Если нужно, чтобы все указанные критерии совпадали, то используется функция AccessCriterionAND():
//+------------------------------------------------------------------+ //| AND: | //| ВОЗВРАЩАЕТ ДОПУСК К ЗАПИСИ СТРОКИ ОПТИМИЗАЦИОННОГО ПРОХОДА | //| ЕСЛИ ВСЕ ИЗ УКАЗАННЫХ КРИТЕРИЕВ ВЫПОЛНЯЮТСЯ | //+------------------------------------------------------------------+ bool AccessCriterionAND() { int count=0; // Счётчик критериев int size_array=ArraySize(criterion); // Получим размер массива //--- // Пройдём в цикле по массиву критериев и // определим исполняются ли все условия для записи показателей в файл for(int i=0; i<size_array; i++) { // Переходим к следующей итерации, если критерий не установлен if(criterion[i]==NO_CRITERION) { continue; } //--- if(criterion[i]==eSTAT_PROFIT) { if(stat_values[0]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // PROFIT } //--- if(criterion[i]==eSTAT_DEALS) { if(stat_values[1]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // TOTAL DEALS } //--- if(criterion[i]==eSTAT_PROFIT_FACTOR) { if(stat_values[2]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // PROFIT FACTOR } //--- if(criterion[i]==eSTAT_EXPECTED_PAYOFF) { if(stat_values[3]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // EXPECTED PAYOFF } //--- if(criterion[i]==eSTAT_EQUITY_DDREL_PERCENT) { if(stat_values[4]<criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // EQUITY DD REL PERC } //--- if(criterion[i]==eSTAT_RECOVERY_FACTOR) { if(stat_values[5]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // RECOVERY FACTOR } //--- if(criterion[i]==eSTAT_SHARPE_RATIO) { if(stat_values[6]>criterion_value[i]) { count++; if(count==CountUseCriteria) return(true); } // SHARPE RATIO } } //--- // Условия не исполняются для записи return(false); } //---
Если же нужно, чтобы хотя бы один из указанных критериев совпал, то используется функция AccessCriterionOR():
//+------------------------------------------------------------------+ //| OR: | //| ВОЗВРАЩАЕТ ДОПУСК К ЗАПИСИ СТРОКИ ОПТИМИЗАЦИОННОГО ПРОХОДА | //| ЕСЛИ ХОТЯ БЫ ОДИН ИЗ УКАЗАННЫХ КРИТЕРИЕВ ВЫПОЛНЯЕТСЯ | //+------------------------------------------------------------------+ bool AccessCriterionOR() { int size_array=ArraySize(criterion); // Получим размер массива критериев //--- // Пройдём в цикле по массиву критериев и // определим исполняются ли все условия для записи показателей в файл for(int i=0; i<size_array; i++) { if(criterion[i]==NO_CRITERION) { continue; } //--- if(criterion[i]==eSTAT_PROFIT) { if(stat_values[0]>criterion_value[i]) return(true); } // PROFIT //--- if(criterion[i]==eSTAT_DEALS) { if(stat_values[1]>criterion_value[i]) return(true); } // TOTAL DEALS //--- if(criterion[i]==eSTAT_PROFIT_FACTOR) { if(stat_values[2]>criterion_value[i]) return(true); } // PROFIT FACTOR //--- if(criterion[i]==eSTAT_EXPECTED_PAYOFF) { if(stat_values[3]>criterion_value[i]) return(true); } // EXPECTED PAYOFF //--- if(criterion[i]==eSTAT_EQUITY_DDREL_PERCENT) { if(stat_values[4]<criterion_value[i]) return(true); } // EQUITY DD REL PERC //--- if(criterion[i]==eSTAT_RECOVERY_FACTOR) { if(stat_values[5]>criterion_value[i]) return(true); } // RECOVERY FACTOR //--- if(criterion[i]==eSTAT_SHARPE_RATIO) { if(stat_values[6]>criterion_value[i]) return(true); } // SHARPE RATIO } //--- // Условия не исполняются для записи return(false); } //---
Функция GetAmountStrings() переводит указатель в конец файла и возвращает количество строк в файле (см. код ниже):
//+------------------------------------------------------------------+ //| СЧИТАЕТ КОЛИЧЕСТВО СТРОК В ФАЙЛЕ | //+------------------------------------------------------------------+ int GetAmountStrings() { int count_strings=0; // Счётчик строк ulong tell_seek=0,tseek=0; // Положение указателя //--- FileSeek(opt_file_handle,0,SEEK_SET); // Переместим указатель в начало //--- // Читать пока текущее положение файлового указателя не окажется в конце файла while(!FileIsEnding(opt_file_handle)) { if(_StopFlag) { break; } //--- while(!FileIsLineEnding(opt_file_handle)) // Считаем всю строку { if(_StopFlag) { break; } //--- FileReadString(opt_file_handle); // Прочитаем строку //--- tell_seek=FileTell(opt_file_handle); // Получим положение указателя //--- if(FileIsLineEnding(opt_file_handle)) // Если это конец строки { // Переход на другую строку, если это не конец файла if(!FileIsEnding(opt_file_handle)) { tseek=tell_seek+1; } // Увеличим счётчик для указателя //--- FileSeek(opt_file_handle,tseek,SEEK_SET); // Переместим указатель count_strings++; // Увеличим счётчик строк break; } } //--- // Если это конец файла, то выйдем из цикла if(FileIsEnding(opt_file_handle)) { break; } } //--- // Переместим указатель в конец файла для записи FileSeek(opt_file_handle,0,SEEK_END); //--- return(count_strings); // Вернём кол-во строк } //---
Всё готово. Теперь нужно функцию CreateWriteOptimizationReport() поместить в тело функции OnTesterPass(). А в функции OnTesterDeinit() закрыть файл результатов оптимизации.
//+------------------------------------------------------------------+ //| ОЧЕРЕДНОЙ ПРОХОД ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterPass() { // Если включена запись результатов оптимизации if(OptimizationReport) { CreateWriteOptimizationReport(); } } //+------------------------------------------------------------------+ //| ЗАВЕРШЕНИЕ ОПТИМИЗАЦИИ | //+------------------------------------------------------------------+ void OnTesterDeinit() { Print("-----------\n",__FUNCTION__,"(): End Optimization"); //--- // Если включена запись результатов оптимизации if(OptimizationReport) { // Закрываем файл результатов оптимизации FileClose(opt_file_handle); } } //---
Теперь протестируем эксперта. Оптимизируем его параметры в сети распределённых вычислений MQL5 Cloud Network. Настройки тестера нужно установить так, как показано на скриншоте ниже:
Установим на оптимизацию все параметры эксперта и настроим параметры критериев так, чтобы в файл записывались те результаты, у которых значение показателя Profit Factor выше 1, а Recovery Factor выше 2 (см. скриншот ниже):
Сеть распределённых вычислений пропустила через себя 101000 проходов всего лишь за ~5 минут! Если бы я не использовал сеть, то на оптимизацию бы ушло несколько дней. Отличная возможность для тех, кто ценит своё время.
Полученный файл можно теперь открыть в Excel. Из 101000 проходов было выбрано 719 результатов для записи в файл. На скриншоте ниже я выделил столбцы с теми показателями, по которым отбирались результаты для записи:
На этом закончим статью. На самом деле тема по анализу результатов оптимизации ещё далеко не раскрыта полностью и к этому вопросу мы ещё вернёмся в будущих статьях.
Ниже можно скачать архив с файлами эксперта для изучения.
Успехов!
Скачать эксперт WriteResOptByCriterion.zip
Комментариев нет :
Отправить комментарий