English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Рецепты MQL5 - Сохраняем результаты оптимизации торгового эксперта по указанным критериям

Рецепты MQL5 - Сохраняем результаты оптимизации торгового эксперта по указанным критериям

MetaTrader 5Тестер | 1 октября 2013, 16:46
5 830 19
Anatoli Kazharski
Anatoli Kazharski

Введение

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

 

Процесс разработки

Для реализации задуманного возьмем уже готового эксперта с простым торговым алгоритмом из статьи Рецепты MQL5 - Как не получить ошибку при установке/изменении торговых уровней? и просто добавим туда все необходимые функции. Я подготовил код подобно тому, как это было сделано в последних статьях. То есть, все функции распределены по разным файлам и подключены к основному файлу проекта. Как подключать файлы к проекту можно посмотреть в статье Рецепты MQL5 - Использование индикаторов для формирования условий торговли в эксперте.

Чтобы получить доступ к данным во время оптимизации в MQL5 есть специальные функции: OnTesterInit(), OnTester(), OnTesterPass() и OnTesterDeinit(). Кратко рассмотрим каждую из них:

  • OnTesterInit() - с помощью этой функции определяется начало оптимизации.
  • OnTester() - в этой функции будет производиться добавление так называемых фреймов во время оптимизации после каждого прохода. Что такое фреймы будет объясняться ниже.
  • OnTesterPass() - эта функция принимает фреймы во время оптимизации после каждого прохода.
  • OnTesterDeinit() - в этой функции генерируется событие об окончании оптимизации параметров эксперта.

Теперь нужно разобраться, что такое фреймы. Фрейм это своего рода структура данных отдельного прохода оптимизации. Фреймы во время оптимизации сохраняются в архив *.mqd, который создается в каталоге MetaTrader 5/MQL5/Files/Tester. К данным (фреймам) этого архива можно обращаться, как во время оптимизации "на лету", так и после окончания оптимизации. Например, в статье Визуализируй стратегию в тестере MetaTrader 5 показан пример того, как можно визуализировать процесс оптимизации "на лету" и затем включить просмотр всех результатов после оптимизации.

В этой статье мы будем использовать такие функции для работы с фреймами:

  • FrameAdd() - добавление данных из файла или из массива.
  • FrameNext() - вызов с получением одного числового значения либо всех данных фрейма.
  • FrameInputs() - получает input-параметры, на которых сформирован фрейм с заданным номером прохода.

Более подробную информацию по всем вышеперечисленным функциям можно посмотреть в Справочном руководстве по языку MQL5. Начнем, как всегда с внешних параметров. Ниже показано, какие параметры нужно добавить к тем, что уже есть:

//--- Внешние параметры эксперта
input  int              NumberOfBars           = 2;              // Количество однонаправленных баров
sinput double           Lot                    = 0.1;            // Лот
input  double           TakeProfit             = 100;            // Тейк Профит
input  double           StopLoss               = 50;             // Стоп Лосс
input  double           TrailingStop           = 10;             // Трейлинг Стоп
input  bool             Reverse                = true;           // Разворот позиции
sinput string           delimeter=""; // --------------------------------
sinput bool             LogOptimizationReport  = true;           // Запись результатов в файл
sinput CRITERION_RULE   CriterionSelectionRule = RULE_AND;       // Условие для записи
sinput ENUM_STATS       Criterion_01           = C_NO_CRITERION; // 01 - Название критерия
sinput double           CriterionValue_01      = 0;              // ---- Значение критерия
sinput ENUM_STATS       Criterion_02           = C_NO_CRITERION; // 02 - Название критерия
sinput double           CriterionValue_02      = 0;              // ---- Значение критерия
sinput ENUM_STATS       Criterion_03           = C_NO_CRITERION; // 03 - Название критерия
sinput double           CriterionValue_03      = 0;              // ---- Значение критерия

С помощью параметра LogOptimizationReport будем указывать программе, производить запись результатов и параметров в файл во время оптимизации или нет.

В данном примере сделаем возможность указывать до трех критериев, по которым будут выбираться результаты для записи. Также добавим правило (параметр CriterionSelectionRule), когда можно указать, записывать результат, если исполнятся все указанные условия (И) или хотя бы одно из них (ИЛИ). Для этого создадим в файле Enums.mqh перечисление:

//--- Правила проверки исполнения критериев
enum CRITERION_RULE
  {
   RULE_AND = 0, // И
   RULE_OR  = 1  // ИЛИ
  };

В качестве критериев будут выступать основные показатели результатов тестирования. Для них тоже нужно создать перечисление:

//--- Статистические показатели
enum ENUM_STATS
  {
   C_NO_CRITERION              = 0, // Нет критерия
   C_STAT_PROFIT               = 1, // Прибыль
   C_STAT_DEALS                = 2, // Всего сделок
   C_STAT_PROFIT_FACTOR        = 3, // Прибыльность
   C_STAT_EXPECTED_PAYOFF      = 4, // Матожидание выигрыша
   C_STAT_EQUITY_DDREL_PERCENT = 5, // Максимальная просадка по средствам, %
   C_STAT_RECOVERY_FACTOR      = 6, // Фактор восстановления
   C_STAT_SHARPE_RATIO         = 7  // Коэффициент Шарпа
  };

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

Также нужно добавить несколько глобальных переменных (см. код ниже):

//--- Глобальные переменные
int    AllowedNumberOfBars=0;       // Для проверки значения внешнего параметра NumberOfBars
string OptimizationResultsPath=""// Путь к папке для сохранения папок и файлов
int    UsedCriteriaCount=0;         // Количество используемых критериев
int    OptimizationFileHandle=-1;   // Хэндл файла для записи результатов оптимизации

И еще нам понадобятся вот такие массивы для работы:

int    criteria[3];                    // Критерии для формирования отчета оптимизации
double criteria_values[3];             // Значения критериев
double stat_values[STAT_VALUES_COUNT]; // Массив для показателей теста

В главном файле эксперта нужно добавить функции обработки событий тестера, которые описывались в начале статьи:

//+------------------------------------------------------------------+
//| Начало оптимизации                                               |
//+------------------------------------------------------------------+
void OnTesterInit()
  {
   Print(__FUNCTION__,"(): Start Optimization \n-----------");
  }
//+------------------------------------------------------------------+
//| Обработчик события окончания тестирования                        |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
      //---
//---
   return(0.0);
  }
//+------------------------------------------------------------------+
//| Очередной проход оптимизации                                     |
//+------------------------------------------------------------------+
void OnTesterPass()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
      //---
  }
//+------------------------------------------------------------------+
//| Завершение оптимизации                                           |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   Print("-----------\n",__FUNCTION__,"(): End Optimization");
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
      //---
  }

Если сейчас запустить процесс оптимизации, то в терминале откроется график с символом и периодом, на котором запущен эксперт. Сообщения из функций, которые показаны в коде выше, будут выводиться в журнал терминала, а не в журнал тестера. В самом начале оптимизации в журнал будет выведено сообщение из функции OnTesterInit(). Но во время и по окончании оптимизации никаких сообщений в журнале вы не увидите. После оптимизации, если удалить открытый тестером график, в журнал будет выведено сообщение из функции OnTesterDeinit(). В чем же дело?

Дело в том, что для корректной работы в функции OnTester() нужно использовать функцию FrameAdd() для добавления фрейма, как это показано в примере ниже.

//+------------------------------------------------------------------+
//| Обработчик события окончания тестирования                        |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
     {
      //--- Создадим фрейм
      FrameAdd("Statistics",1,0,stat_values);
     }
//---
   return(0.0);
  }

Теперь во время оптимизации мы будем видеть, как после каждого прохода в журнал выводится сообщение из функции OnTesterPass(), а после окончания оптимизации из функции OnTesterDeinit() придет сообщение об окончании оптимизации. Сообщение об окончании оптимизации придет также и, если остановить оптимизацию вручную.

Рис.1 - Вывод сообщений в журнал из функций тестирования и оптимизации

Рис.1 - Вывод сообщений в журнал из функций тестирования и оптимизации

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

Создадим файл FileFunctions.mqh и подключим его к проекту. В самом начале этого файла напишем функцию GetTestStatistics(), в которую по ссылке будем передавать массив для заполнения показателями каждого очередного прохода во время оптимизации.

//+------------------------------------------------------------------+
//| Заполняет массив результатами теста                              |
//+------------------------------------------------------------------+
void GetTestStatistics(double &stat_array[])
  {
//--- Вспомогательные переменные для корректировки значений
   double profit_factor=0,sharpe_ratio=0;
//---
   stat_array[0]=TesterStatistics(STAT_PROFIT);                // Чистая прибыль по окончании тестирования
   stat_array[1]=TesterStatistics(STAT_DEALS);                 // Количество совершенных сделок
//---
   profit_factor=TesterStatistics(STAT_PROFIT_FACTOR);         // Прибыльность – отношение STAT_GROSS_PROFIT/STAT_GROSS_LOSS 
   stat_array[2]=(profit_factor==DBL_MAX) ? 0 : profit_factor; // скорректируем при необходимости
//---
   stat_array[3]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // Математическое ожидание выигрыша
   stat_array[4]=TesterStatistics(STAT_EQUITY_DDREL_PERCENT);  // Максимальная просадка средств в процентах
   stat_array[5]=TesterStatistics(STAT_RECOVERY_FACTOR);       // Фактор восстановления – отношение STAT_PROFIT/STAT_BALANCE_DD
//---
   sharpe_ratio=TesterStatistics(STAT_SHARPE_RATIO);           // Коэффициент Шарпа - показатель эффективности инвестиционного портфеля (актива)
   stat_array[6]=(sharpe_ratio==DBL_MAX) ? 0 : sharpe_ratio;   // скорректируем при необходимости
  }

Функцию GetTestStatistics() нужно разместить перед добавлением фрейма:

//+------------------------------------------------------------------+
//| Обработчик события окончания тестирования                        |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
     {
      //--- Заполним массив показателями теста
      GetTestStatistics(stat_values);
      //--- Создадим фрейм
      FrameAdd("Statistics",1,0,stat_values);
     }
//---
   return(0.0);
  }

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

Теперь можно проверить в функции OnTesterPass() полученные данные. Для проверки, как это работает, просто пока выведем в журнал терминала прибыль каждого результата. Для того, чтобы получить значения текущего фрейма, нужно использовать FrameNext(). Пример показан ниже:

//+------------------------------------------------------------------+
//| Очередной проход оптимизации                                     |
//+------------------------------------------------------------------+
void OnTesterPass()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
     {
      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 будут нулевые. Если же все сделано правильно, то получим результат, как показано на скриншоте ниже:

Рис. 2 - Вывод сообщений в журнал из функции OnTesterPass()

Рис. 2 - Вывод сообщений в журнал из функции OnTesterPass()

Кстати, если запустить оптимизацию не изменяя внешние параметры, то результаты в тестер загрузятся из кэша, минуя функции OnTesterPass() и OnTesterDeinit(). Об этом просто нужно помнить, чтобы не подумать, что где-то спряталась ошибка.

Далее в файле FileFunctions.mqh создадим функцию CreateOptimizationReport(). Именно в ней будут производиться все основные действия. Ниже представлен ее код:

//+------------------------------------------------------------------+
//| Создает и записывает отчет результатов оптимизации               |
//+------------------------------------------------------------------+
void CreateOptimizationReport()
  {
   static int passes_count=0;                // Счетчик проходов
   int        parameters_count=0;            // Количество параметров эксперта
   int        optimized_parameters_count=0;  // Счетчик оптимизируемых параметров
   string     string_to_write="";            // Строка для записи
   bool       include_criteria_list=false;   // Для определения начала списка параметров-критериев
   int        equality_sign_index=0;         // Индекс знака '=' в строке
   string     name            ="";           // Публичное имя/метка фрейма
   ulong      pass            =0;            // Номер прохода в оптимизации, на котором добавлен фрейм
   long       id              =0;            // Публичный id фрейма
   double     value           =0.0;          // Одиночное числовое значение фрейма
   string     parameters_list[];             // Список параметров эксперта вида "parameterN=valueN"
   string     parameter_names[];             // Массив имен параметров
   string     parameter_values[];            // Массив значений параметров
//--- Увеличим счетчик проходов
   passes_count++;
//--- Поместим статистические показатели в массив
   FrameNext(pass,name,id,value,stat_values);
//--- Получим номер прохода, список параметров, кол-во параметров
   FrameInputs(pass,parameters_list,parameters_count);
//--- Пройдем в цикле по списку параметров (от верхнего в списке)
//    В начале списка идут параметры, которые отмечены флажками на оптимизацию
   for(int i=0; i<parameters_count; i++)
     {
      //--- На первом проходе получим критерии для отбора результатов
      if(passes_count==1)
        {
         string current_value="";      // Текущее значение параметра
         static int c=0,v=0,trigger=0; // Счетчики и триггер
         //--- Установим флаг, если дошли до списка критериев
         if(StringFind(parameters_list[i],"CriterionSelectionRule",0)>=0)
           {
            include_criteria_list=true;
            continue;
           }
         //--- На последнем параметре посчитаем используемые критерии,
         //    если выбран режим AND (И)
         if(CriterionSelectionRule==RULE_AND && i==parameters_count-1)
            CalculateUsedCriteria();
         //--- Если дошли до критериев в списке параметров
         if(include_criteria_list)
           {
            //--- Определяем имена критериев
            if(trigger==0)
              {
               equality_sign_index=StringFind(parameters_list[i],"=",0)+1;          // Определим позицию знака '=' в строке
               current_value =StringSubstr(parameters_list[i],equality_sign_index); // Возьмем значение параметра
               //---
               criteria[c]=(int)StringToInteger(current_value);
               trigger=1; // Следующий параметр будет значением
               c++;
               continue;
              }
            //--- Определяем значения критериев
            if(trigger==1)
              {
               equality_sign_index=StringFind(parameters_list[i],"=",0)+1;          // Определим позицию знака '=' в строке
               current_value=StringSubstr(parameters_list[i],equality_sign_index);  // Возьмем значение параметра
               //---           
               criteria_values[v]=StringToDouble(current_value);
               trigger=0; // Следующий параметр будет критерием
               v++; 
               continue;
              }
           }
        }
      //--- Если параметр включен на оптимизацию
      if(ParameterEnabledForOptimization(parameters_list[i]))
        {
         //--- Увеличим счетчик оптимизируемых параметров
         optimized_parameters_count++;
         //--- Названия оптимизируемых параметров заносим в массив
         //    только на первом проходе (для заголовков)
         if(passes_count==1)
           {
            //--- Увеличим размер массива значений параметров
            ArrayResize(parameter_names,optimized_parameters_count);
            //--- Определим позицию знака '='          
            equality_sign_index=StringFind(parameters_list[i],"=",0);
            //--- Возьмем имя параметра
            parameter_names[i]=StringSubstr(parameters_list[i],0,equality_sign_index);
           }
         //--- Увеличим размер массива значений параметров
         ArrayResize(parameter_values,optimized_parameters_count);
         //--- Определим позицию знака '='
         equality_sign_index=StringFind(parameters_list[i],"=",0)+1;
         //--- Возьмем значение параметра
         parameter_values[i]=StringSubstr(parameters_list[i],equality_sign_index);
        }
     }
//--- Сформируем строку значений до оптимизируемых параметров
   for(int i=0; i<STAT_VALUES_COUNT; i++)
      StringAdd(string_to_write,DoubleToString(stat_values[i],2)+",");
//--- Дополним строку значений значениями оптимизируемых параметров
   for(int i=0; i<optimized_parameters_count; i++)
     {
      //--- Если последнее значение в строке, то без разделителя
      if(i==optimized_parameters_count-1)
        {
         StringAdd(string_to_write,parameter_values[i]);
         break;
        }
      //--- Иначе с разделителем
      else
         StringAdd(string_to_write,parameter_values[i]+",");
     }
//--- На первом проходе создадим файл-отчет оптимизации с заголовками
   if(passes_count==1)
      WriteOptimizationReport(parameter_names);
//--- Запишем данные в файл результатов оптимизации
   WriteOptimizationResults(string_to_write);
  }

Получилась довольно большая функция, рассмотрим ее подробнее. В самом начале после объявления переменных и массивов получаем данные фрейма с помощью функции FrameNext(), как это было показано выше в примерах. Далее с помощью функции FrameInputs() получаем список параметров в строковой массив parameters_list[] и общее количество параметров в переменную parameters_count.

В списке параметров, который отдает нам функция FrameInputs(), оптимизируемые параметры (отмечены флажками в тестере) расположены в самом начале, независимо от того, в какой последовательности они идут в списке внешних параметров эксперта.

Затем идет цикл, в котором производится перебор списка параметров. На самом первом проходе заполняется массив критериев criteria[] и массив значений критериев criteria_values[]. Подсчет используемых критериев производится в функции CalculateUsedCriteria() и только, если включен режим AND и текущий параметр - последний:

//+------------------------------------------------------------------+
//| Рассчитывает количество используемых критериев                   |
//+------------------------------------------------------------------+
void CalculateUsedCriteria()
  {
   UsedCriteriaCount=0; // Обнуление
//--- Пройдем в цикле по списку критериев
   for(int i=0; i<ArraySize(criteria); i++)
     {
      //--- посчитаем используемые
      if(criteria[i]!=C_NO_CRITERION)
         UsedCriteriaCount++;
     }
  }

В этом же цикле далее на каждом проходе проверяется, выбран ли параметр для оптимизацию или нет. Для этого используется функция ParameterEnabledForOptimization(), в которую передается текущий внешний параметр для проверки. Если функция возвращает true, значит, параметр будет участвовать в оптимизации.

//+------------------------------------------------------------------+
//| Проверяет, выбран ли внешний параметр для оптимизации            |
//+------------------------------------------------------------------+
bool ParameterEnabledForOptimization(string parameter_string)
  {
   bool enable;
   long value,start,step,stop;
//--- Определим позицию символа '=' в строке
   int equality_sign_index=StringFind(parameter_string,"=",0);
//--- Получим значения параметра
   ParameterGetRange(StringSubstr(parameter_string,0,equality_sign_index),
                     enable,value,start,step,stop);
//--- Вернем состояние параметра
   return(enable);
  }

В этом случае заполняются массивы для названий parameter_names и значений параметров parameter_values. Массив для названий оптимизируемых параметров заполняется только на первом проходе.

Далее в двух циклах формируется строка значений показателей теста и значений параметров для записи в файл. После этого с помощью функции WriteOptimizationReport() создается файл для записи на первом проходе.

//+------------------------------------------------------------------+
//| Создает файл отчет проходов оптимизации                          |
//+------------------------------------------------------------------+
void WriteOptimizationReport(string &parameter_names[])
  {
   int files_count     =1; // Счетчик файлов оптимизации
//--- Сформируем заголовок до оптимизируемых параметров
   string headers="#,PROFIT,TOTAL DEALS,PROFIT FACTOR,EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,";
//--- Дополним заголовок оптимизируемыми параметрами
   for(int i=0; i<ArraySize(parameter_names); i++)
     {
      if(i==ArraySize(parameter_names)-1)
         StringAdd(headers,parameter_names[i]);
      else
         StringAdd(headers,parameter_names[i]+",");
     }
//--- Получим путь для создания файла оптимизации и кол-во файлов для порядкового номера
   OptimizationResultsPath=CreateOptimizationResultsFolder(files_count);
//--- Если ошибка при получении директории, выходим
   if(OptimizationResultsPath=="")
     {
      Print("Empty path: ",OptimizationResultsPath);
      return;
     }
   else
     {
      OptimizationFileHandle=FileOpen(OptimizationResultsPath+"\optimization_results"+IntegerToString(files_count)+".csv",
                                      FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_COMMON,",");
      //---
      if(OptimizationFileHandle!=INVALID_HANDLE)
         FileWrite(OptimizationFileHandle,headers);
     }
  }

Цель функции WriteOptimizationReport() - сформировать заголовки, создать при необходимости каталоги в общей папке терминала, а также создать очередной файл для записи. То есть, файлы от предыдущих оптимизаций остаются и каждый раз создается новый файл с порядковым номером. После создания файла в него записываются заголовки. Сам файл остается открытым до конца оптимизации.

В коде выше есть строка с функцией CreateOptimizationResultsFolder(). В ней создаются каталоги для сохранения файлов с результатами оптимизации:

//+------------------------------------------------------------------+
//| Создает папки для результатов оптимизации                        |
//+------------------------------------------------------------------+
string CreateOptimizationResultsFolder(int &files_count)
  {
   long   search_handle       =INVALID_HANDLE;        // Хэндл поиска
   string returned_filename   ="";                    // Имя найденного объекта (файла/папки)
   string path                ="";                    // Директория для поиска файла/папки
   string search_filter       ="*";                   // Фильтр поиска (* - проверить все файлы/папки)
   string root_folder         ="OPTIMIZATION_DATA\\"; // Корневая папка
   string expert_folder       =EXPERT_NAME+"\\";      // Папка эксперта
   bool   root_folder_exists  =false;                 // Признак существования корневой папки
   bool   expert_folder_exists=false;                 // Признак существования папки эксперта
//--- Ищем корневую папку OPTIMIZATION_DATA в общей папке терминала
   path=search_filter;
//--- Установим хэндл поиска в общей папке всех клиентских терминалов \Files
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- Выведем в журнал путь к общей папке терминала
   Print("TERMINAL_COMMONDATA_PATH: ",COMMONDATA_PATH);
//--- Если первая папка корневая, ставим флаг
   if(returned_filename==root_folder)
     {
      root_folder_exists=true;
      Print("Корневая папка "+root_folder+" существует.");
     }
//--- Если хэндл поиска получен
   if(search_handle!=INVALID_HANDLE)
     {
      //--- Если первая папка была не корневой
      if(!root_folder_exists)
        {
         //--- Перебираем все файлы с целью поиска корневой папки
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- Если находим, то ставим флаг
            if(returned_filename==root_folder)
              {
               root_folder_exists=true;
               Print("Корневая папка "+root_folder+" существует.");
               break;
              }
           }
        }
      //--- Закроем хэндл поиска корневой папки
      FileFindClose(search_handle);
     }
   else
     {
      Print("Ошибка при получении хэндла поиска "
            "либо директория "+COMMONDATA_PATH+" пуста: ",ErrorDescription(GetLastError()));
     }
//--- Ищем папку эксперта в папке OPTIMIZATION_DATA
   path=root_folder+search_filter;
//--- Установим хэндл поиска в папке ..\Files\OPTIMIZATION_DATA\
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- Если первая папка и есть папка эксперта
   if(returned_filename==expert_folder)
     {
      expert_folder_exists=true; // Запомним это
      Print("Папка эксперта "+expert_folder+" существует.");
     }
//--- Если хэндл поиска получен
   if(search_handle!=INVALID_HANDLE)
     {
      //--- Если первая папка была не папкой эксперта
      if(!expert_folder_exists)
        {
         //--- Перебираем все файлы в папке DATA_OPTIMIZATION с целью поиска папки эксперта
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- Если находим, то ставим флаг
            if(returned_filename==expert_folder)
              {
               expert_folder_exists=true;
               Print("Папка эксперта "+expert_folder+" существует.");
               break;
              }
           }
        }
      //--- Закроем хэндл поиска корневой папки
      FileFindClose(search_handle);
     }
   else
      Print("Ошибка при получении хэндла поиска либо директория "+path+" пуста.");
//--- Сформируем путь для подсчета файлов
   path=root_folder+expert_folder+search_filter;
//--- Установим хэндл поиска в папке результатов оптимизации ..\Files\OPTIMIZATION_DATA\
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- Если папка не пуста, откроем счет
   if(StringFind(returned_filename,"optimization_results",0)>=0)
      files_count++;
//--- Если хэндл поиска получен
   if(search_handle!=INVALID_HANDLE)
     {
      //--- Посчитаем все файлы в папке эксперта
      while(FileFindNext(search_handle,returned_filename))
         files_count++;
      //---
      Print("Всего файлов: ",files_count);
      //--- Закроем хэндл поиска папки эксперта
      FileFindClose(search_handle);
     }
   else
      Print("Ошибка при получении хэндла поиска либо директория "+path+" пуста");
//--- По результатам проверки создадим нужные папки
//    Если нет корневой папки OPTIMIZATION_DATA
   if(!root_folder_exists)
     {
      if(FolderCreate("OPTIMIZATION_DATA",FILE_COMMON))
        {
         root_folder_exists=true;
         Print("Создана корневая папка ..\Files\OPTIMIZATION_DATA\\");
        }
      else
        {
         Print("Ошибка при создании корневой папки OPTIMIZATION_DATA: ",
               ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- Если нет папки эксперта
   if(!expert_folder_exists)
     {
      if(FolderCreate(root_folder+EXPERT_NAME,FILE_COMMON))
        {
         expert_folder_exists=true;
         Print("Создана папка эксперта ..\Files\OPTIMIZATION_DATA\\"+expert_folder);
        }
      else
        {
         Print("Ошибка при создании папки эксперта ..\Files\\"+expert_folder+"\: ",
               ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- Если нужные папки есть
   if(root_folder_exists && expert_folder_exists)
     {
      //--- Вернем путь, в котором будет создан файл для записи результатов оптимизации
      return(root_folder+EXPERT_NAME);
     }
//---
   return("");
  }

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

Сначала производится проверка на наличие корневой папки для результатов оптимизации OPTIMIZATION_DATA. Если такая папка есть, то это отмечается в переменной root_folder_exists. Далее хэндл поиска устанавливается в папке OPTIMIZATION_DATA, и уже в ней производится проверка наличия папки с именем эксперта.

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

Осталось рассмотреть функцию WriteOptimizationResults(), в которой производится проверка условий для записи данных в файл и запись, если условие выполняется. Ниже представлен код этой функции:

//+------------------------------------------------------------------+
//| Производит запись результатов оптимизации по критериям           |
//+------------------------------------------------------------------+
void WriteOptimizationResults(string string_to_write)
  {
   bool condition=false; // Для проверки условия
//--- Если хотя бы один критерий выполняется
   if(CriterionSelectionRule==RULE_OR)
      condition=AccessCriterionOR();
//--- Если все критерии выполняются
   if(CriterionSelectionRule==RULE_AND)
      condition=AccessCriterionAND();
//--- Если условия по критериям выполняются
   if(condition)
     {
      //--- Если файл для записи результатов оптимизации открыт
      if(OptimizationFileHandle!=INVALID_HANDLE)
        {
         int strings_count=0; // Счетчик строк
         //--- Получим количество строк в файле и переместим указатель в конец
         strings_count=GetStringsCount();
         //--- Запишем строку с критериями
         FileWrite(OptimizationFileHandle,IntegerToString(strings_count),string_to_write);
        }
      else
         Print("Хэндл файла для записи результатов оптимизации невалиден!");
     }
  }

Рассмотрим строки с функциями, которые выделены в коде. В зависимости от того, какое правило для проверки критериев выбрано, используется соответствующая функция. Если нужно, чтобы все указанные критерии совпадали, то используется функция AccessCriterionAND():

//+------------------------------------------------------------------+
//| Проверка нескольких условий для записи в файл                    |
//+------------------------------------------------------------------+
bool AccessCriterionAND()
  {
   int count=0; // Счетчик критериев
//--- Пройдем в цикле по массиву критериев и определим,
//    исполняются ли все условия для записи показателей в файл
   for(int i=0; i<ArraySize(criteria); i++)
     {
      //--- Переходим к следующей итерации, если критерий не установлен
      if(criteria[i]==C_NO_CRITERION)
         continue;
      //--- PROFIT
      if(criteria[i]==C_STAT_PROFIT)
        {
         if(stat_values[0]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- TOTAL DEALS
      if(criteria[i]==C_STAT_DEALS)
        {
         if(stat_values[1]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- PROFIT FACTOR
      if(criteria[i]==C_STAT_PROFIT_FACTOR)
        {
         if(stat_values[2]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- EXPECTED PAYOFF
      if(criteria[i]==C_STAT_EXPECTED_PAYOFF)
        {
         if(stat_values[3]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- EQUITY DD REL PERC
      if(criteria[i]==C_STAT_EQUITY_DDREL_PERCENT)
        {
         if(stat_values[4]<criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- RECOVERY FACTOR
      if(criteria[i]==C_STAT_RECOVERY_FACTOR)
        {
         if(stat_values[5]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- SHARPE RATIO
      if(criteria[i]==C_STAT_SHARPE_RATIO)
        {
         if(stat_values[6]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
     }
//--- Условия не исполняются для записи
   return(false);
  }

Если же нужно, чтобы хотя бы один из указанных критериев совпал, то используется функция AccessCriterionOR():

//+------------------------------------------------------------------+
//| Проверка на исполнение одного из условий для записи в файл       |
//+------------------------------------------------------------------+
bool AccessCriterionOR()
  {
//--- Пройдем в цикле по массиву критериев и определим, 
//    исполняются ли все условия для записи показателей в файл
   for(int i=0; i<ArraySize(criteria); i++)
     {
     //---
      if(criteria[i]==C_NO_CRITERION)
         continue;
      //--- PROFIT
      if(criteria[i]==C_STAT_PROFIT)
        {
         if(stat_values[0]>criteria_values[i])
            return(true);
        }
      //--- TOTAL DEALS
      if(criteria[i]==C_STAT_DEALS)
        {
         if(stat_values[1]>criteria_values[i])
            return(true);
        }
      //--- PROFIT FACTOR
      if(criteria[i]==C_STAT_PROFIT_FACTOR)
        {
         if(stat_values[2]>criteria_values[i])
            return(true);
        }
      //--- EXPECTED PAYOFF
      if(criteria[i]==C_STAT_EXPECTED_PAYOFF)
        {
         if(stat_values[3]>criteria_values[i])
            return(true);
        }
      //--- EQUITY DD REL PERC
      if(criteria[i]==C_STAT_EQUITY_DDREL_PERCENT)
        {
         if(stat_values[4]<criteria_values[i])
            return(true);
        }
      //--- RECOVERY FACTOR
      if(criteria[i]==C_STAT_RECOVERY_FACTOR)
        {
         if(stat_values[5]>criteria_values[i])
            return(true);
        }
      //--- SHARPE RATIO
      if(criteria[i]==C_STAT_SHARPE_RATIO)
        {
         if(stat_values[6]>criteria_values[i])
            return(true);
        }
     }
//--- Условия не исполняются для записи
   return(false);
  }

Функция GetStringsCount() переводит указатель в конец файла и возвращает количество строк в файле:

//+------------------------------------------------------------------+
//| Считает количество строк в файле                                 |
//+------------------------------------------------------------------+
int GetStringsCount()
  {
   int   strings_count =0;  // Счетчик строк
   ulong offset        =0;  // Смещение для определения положения файлового указателя
//--- Переместим указатель в начало
   FileSeek(OptimizationFileHandle,0,SEEK_SET);
//--- Читать пока текущее положение файлового указателя не окажется в конце файла
   while(!FileIsEnding(OptimizationFileHandle) || !IsStopped())
     {
      //--- Считаем всю строку
      while(!FileIsLineEnding(OptimizationFileHandle) || !IsStopped())
        {
         //--- Прочитаем строку
         FileReadString(OptimizationFileHandle);
         //--- Получим положение указателя
         offset=FileTell(OptimizationFileHandle);
         //--- Если это конец строки
         if(FileIsLineEnding(OptimizationFileHandle))
           {
            //--- Переход на другую строку, 
            //    если это не конец файла, то увеличим счетчик для указателя
            if(!FileIsEnding(OptimizationFileHandle))
               offset++;
            //--- Переместим указатель
            FileSeek(OptimizationFileHandle,offset,SEEK_SET);
            //--- Увеличим счетчик строк
            strings_count++;
            break;
           }
        }
      //--- Если это конец файла, то выйдем из цикла
      if(FileIsEnding(OptimizationFileHandle))
         break;
     }
//--- Переместим указатель в конец файла для записи
   FileSeek(OptimizationFileHandle,0,SEEK_END);
//--- Вернем кол-во строк
   return(strings_count);
  }

Все готово. Теперь нужно функцию CreateOptimizationReport() поместить в тело функции OnTesterPass(), а в функции OnTesterDeinit() закрыть хэндл файла с результатами оптимизации.

//+------------------------------------------------------------------+
//| Очередной проход оптимизации                                     |
//+------------------------------------------------------------------+
void OnTesterPass()
  {
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
      CreateOptimizationReport();
  }
//+------------------------------------------------------------------+
//| Завершение оптимизации                                           |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   Print("-----------\n",__FUNCTION__,"(): End Optimization");
//--- Если включена запись результатов оптимизации
   if(LogOptimizationReport)
     {
      //--- Закрываем файл результатов оптимизации
      FileClose(OptimizationFileHandle);
     }
  }

Теперь протестируем эксперта. Оптимизируем его параметры в сети распределенных вычислений MQL5 Cloud Network. Настройки тестера нужно установить так, как показано на скриншоте ниже:

Рис. 3 - Настройки тестера

Рис. 3 - Настройки тестера

Установим на оптимизацию все параметры эксперта и настроим параметры критериев так, чтобы в файл записывались те результаты, у которых значение показателя Profit Factor выше 1, а Recovery Factor выше 2 (см. скриншот ниже):

Рис. 4 - Настройки эксперта для оптимизации параметров

Рис. 4 - Настройки эксперта для оптимизации параметров

Сеть распределенных вычислений MQL5 Cloud Network пропустила через себя 101000 проходов всего лишь за ~5 минут! Если бы я не использовал сеть, то на оптимизацию бы ушло несколько дней. Отличная возможность для тех, кто ценит свое время.

Полученный файл можно теперь открыть в Excel. Из 101000 проходов было выбрано 719 результатов для записи в файл. На скриншоте ниже я выделил столбцы с теми показателями, по которым отбирались результаты для записи:

Рис. 5 - Результаты оптимизации в Excel

Рис. 5 - Результаты оптимизации в Excel

 

Заключение

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (19)
Aleksey Vyazmikin
Aleksey Vyazmikin | 18 нояб. 2017 в 23:03
Dennis Kirichenko:

Ну Анатолий Вам правильно заметил. Собирать результаты в список или динамический массив (мне нравится больше список) по ходу оптимизации, в теле обработчика OnTesterPass(). А в конце оптимизации, в теле обработчика OnTesterDeinit(), провести сортировку списка/массива по нужному критерию и сохранить его.


Что-то не увидел про список, что Вы имеете ввиду?

Denis Kirichenko
Denis Kirichenko | 18 нояб. 2017 в 23:10
Aleksey Vyazmikin:

Что-то не увидел про список, что Вы имеете ввиду?


Имею в виду то, что есть такой тип данных - CList - список. В него удобно собирать результаты в Вашей задаче. Но предварительно результаты нужно обернуть в тип узла - это потомок CObject.

Aleksey Vyazmikin
Aleksey Vyazmikin | 19 нояб. 2017 в 02:20
Dennis Kirichenko:

Имею в виду то, что есть такой тип данных - CList - список. В него удобно собирать результаты в Вашей задаче. Но предварительно результаты нужно обернуть в тип узла - это потомок CObject.


Понятно - глянул, и понял, что там заморочек не мало - надо разбираться, если пользоваться... а примеры какие то сложные мне попались. Может покажете, как это сделать на примере этой конкретной задачи?

Alexander Zhogolko
Alexander Zhogolko | 15 дек. 2017 в 22:04

Огромнейшее Вам СПАСИБО, Анатолий!

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

Вроде как тривиальная задача, но мудохался 2 дня, потом обиделся на метаквот и забил на две недели.

Сегодня, матерясь на метаквотов (ладно уж они как-то странно реализовали функции OnTester и OnTesterPass, так еще им и впадлу в справочнике упомянуть, что эти функции необходимо FrameAdd и FrameNext типа "инициализировать", иначе они через ж... работают), с оглядкой на ваш пример таки дописал, то что было нужно))

Nextor
Nextor | 24 июл. 2021 в 21:54
Не работает, при оптимизации в облаке сохраняет менее половины результатов.
Итоги MetaTrader AppStore за 3 квартал 2013 года Итоги MetaTrader AppStore за 3 квартал 2013 года
Подошел к концу очередной квартал этого года, и мы решили подвести его итоги для MetaTrader AppStore - магазина торговых роботов и технических индикаторов для платформ MetaTrader. Всего к концу отчетного квартала более 500 разработчиков разместили в Маркете свыше 1 200 продуктов для MetaTrader 4 и MetaTrader 5.
Расширение стандартной библиотеки MQL5 и повторное использование кода Расширение стандартной библиотеки MQL5 и повторное использование кода
Стандартная библиотека MQL5 значительно упрощает жизнь разработчика. Однако она не может удовлетворить все требования абсолютно всех разработчиков в мире, поэтому если вы хотите иметь в своем распоряжении больше пользовательских элементов, вам необходимо расширить ее. В статье описывается интеграция обычного индикатора ZigZag в стандартную библиотеку. В ходе работы мы придерживались принципов разработки, применяемых в компании MetaQuotes.
Рецепты MQL5 - Озвучиваем торговые события в MetaTrader 5 Рецепты MQL5 - Озвучиваем торговые события в MetaTrader 5
В этой статье мы рассмотрим такие вопросы, как включение в файл эксперта звуковых файлов и, соответственно, озвучивание торговых событий. Включение файлов означает, что звуковые файлы будут находиться внутри эксперта, и если передать скомпилированную версию эксперта (*.ex5) другому пользователю, то не нужно будет передавать ему звуковые файлы и объяснять при этом, в какую папку их положить.
Как заработать на сервисах MetaTrader AppStore и Trading Signals, не являясь продавцом и поставщиком Как заработать на сервисах MetaTrader AppStore и Trading Signals, не являясь продавцом и поставщиком
Начать зарабатывать на MQL5.com, не будучи при этом продавцом программ на Маркете или поставщиком доходных сигналов, можно прямо сейчас. Выбирайте продукты по ключевым для вас критериям и ссылайтесь на них на различных интернет-ресурсах. Зацепите потенциального покупателя - и деньги ваши!