Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору

Andrey Azatskiy | 7 января, 2020

Введение

Вашему вниманию представляется третья статья из цикла статей про создание автооптимизатора для скользящей непрерывной оптимизации, остальные статьи можно прочесть по следующим ссылкам:

  1. Непрерывная скользящая оптимизация (Часть 1): Механизм работы с отчетами оптимизации
  2. Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота

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

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

Выгрузка аккумулированной истории торгов

Иногда для более подробного анализа истории или же для иных нужд требуется произвести выгрузку истории торгов в файл. К сожалению, подобный интерфейс пока не был внедрен в терминал, однако при помощи упомянутых в прошлой статье классов данная задача может быть реализована и в директории, где лежат файлы описанных классов, также находятся два файла — "ShortReport.mqh" и "DealsHistory.mqh", выполняющие поставленную задачу, но с разной мерой детализации.

Начнем их рассмотрение с файла "ShortReport.mqh". Данный файл содержит ряд функций и макросов, главной из которых является функция "SaveReportToFile", однако обо всем по порядку. Первым делом рассмотрим функцию write, которая пишет данные в файл. 

//+------------------------------------------------------------------+
//| File writer                                                      |
//+------------------------------------------------------------------+
void writer(string fileName,string headder,string row)
  {
   bool isFile=FileIsExist(fileName,FILE_COMMON); // Флаг существует ли файл
   int file_handle=FileOpen(fileName,FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_WRITE|FILE_SHARE_READ); // Открываем файл
   if(file_handle) // Если файл открылся
     {
      FileSeek(file_handle,0,SEEK_END); // Перемещаем курсор в конец файла
      if(!isFile) // Если это новосозданный файл, пишем заголовок
         FileWrite(file_handle,headder);
      FileWrite(file_handle,row); // Пишем сообщение
      FileClose(file_handle); // Закрываем файл
     }
  }

Запись происходит в песочницу файлов Terminal/Common/Files. Идея данной функции — осуществлять запись файла с добавлением в него строк, поэтому после открытия файла и получения его хендла, мы  перемещаемся в конец файла. Если файл был только что создан, то мы  записываем переданные заголовки, в противном случае игнорируем этот параметр.

Что касается макроса, то он создан лишь для удобства добавления в файл параметров робота.

#define WRITE_BOT_PARAM(fileName,param) writer(fileName,"",#param+";"+(string)param);

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

Основной метод — SaveReportToFile — довольно длинен, по этому приведем лишь отрывки кода. Все что он делает— это создает экземпляр класса CDealHistoryGetter и получает массив аккумулированной истории торгов, где одна строка обозначает одну сделку.

DealDetales history[];
CDealHistoryGetter dealGetter(_comission_manager);
dealGetter.getDealsDetales(history,0,TimeCurrent());

Затем, проверив не пустая ли история, создает экземпляр класса CReportCreator и получает структуры с основными коэффициентами:

if(ArraySize(history)==0)
   return;

CReportCreator reportCreator(_comission_manager);
reportCreator.Create(history,0);

TotalResult totalResult;
reportCreator.GetTotalResult(totalResult);
PL_detales pl_detales;
reportCreator.GetPL_detales(pl_detales);

Далее в цикле, используя функцию writer, сохраняет данные истории. По завершению цикла добавляются поля со следующими коэффициентами и показателями:

writer(fileName,"","==========================================================================================================");
writer(fileName,"","PL;"+DoubleToString(totalResult.total.PL)+";");
int total_trades=pl_detales.total.profit.orders+pl_detales.total.drawdown.orders;
writer(fileName,"","Total trdes;"+IntegerToString(total_trades));
writer(fileName,"","Consecutive wins;"+IntegerToString(pl_detales.total.profit.dealsInARow));
writer(fileName,"","Consecutive DD;"+IntegerToString(pl_detales.total.drawdown.dealsInARow));
writer(fileName,"","Recovery factor;"+DoubleToString(totalResult.total.recoveryFactor)+";");
writer(fileName,"","Profit factor;"+DoubleToString(totalResult.total.profitFactor)+";");
double payoff=MathAbs(totalResult.total.averageProfit/totalResult.total.averageDD);
writer(fileName,"","Payoff;"+DoubleToString(payoff)+";");
writer(fileName,"","Drawdown by pl;"+DoubleToString(totalResult.total.maxDrawdown.byPL)+";");

На этом работа метода завершается. Теперь продемонстрируем, как легко можно выгружать историю торгов, добавив эту возможность к роботу из стандартной поставки "Experts/Examples/Moving Average/Moving Average.mq5". Первым делом следует подключить наш файл:

#include <History manager/ShortReport.mqh>

Затем добавляем переменные во входные параметры, задающие пользовательскую комиссию и проскальзывание:

input double custom_comission = 0; // Custom comission;
input int custom_shift = 0; // custom shift;

Если мы желаем, чтобы задаваемая нами комиссия и проскальзывание задавались не условно, а директивно (см описание класса CDealHistoryGetter в прошлой статье), то перед подключением файла определяем параметр ONLY_CUSTOM_COMISSION так, как в примере ниже:

#define ONLY_CUSTOM_COMISSION
#include <History manager/ShortReport.mqh>

Далее создаем экземпляр класса CCCM и в методе OnInit добавляем комиссию и проскальзывание в данный экземпляр класса, хранящего комиссии.

CCCM _comission_manager_;

...

int OnInit(void)
  {
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);  

...

  }

Затем в методе OnDeinit добавляем следующие строки кода:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string file_name = __FILE__+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);

      WRITE_BOT_PARAM(file_name,MaximumRisk);      // Maximum Risk in percentage
      WRITE_BOT_PARAM(file_name,DecreaseFactor);   // Descrease factor
      WRITE_BOT_PARAM(file_name,MovingPeriod);     // Moving Average period
      WRITE_BOT_PARAM(file_name,MovingShift);      // Moving Average shift
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;
     }
  }

Теперь всегда после удаления экземпляра робота будет проверяться условие — запущен ли он в тестере? Если он запущен в тестере, то будет вызываться  функция, сохраняющая историю торгов робота в файл с именем "имя_компилируемого_файла Report.csv". После всех данных, что будут записаны в файл, мы добавляем еще 6 строк — это входные параметры данного файла. И теперь каждый раз после запуска данного эксперта в тестере в режиме тестирования мы будем получать файлик с описанием совершенных им сделок, который будет перезаписываться каждый раз, когда мы будем запускать новый тест. Файлик будет храниться в песочнице файлов в директории Common/Files.

Выгрузка разбитой на сделки истории торгов

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

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

void AddRow(string item, string &str)
  {
   str += (item + ";");
  }

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

void WriteDetalesReport(string fileName,CCCM *_comission_manager)
  {

   if(FileIsExist(fileName,FILE_COMMON))
     {
      FileDelete(fileName,FILE_COMMON);
     }

   CDealHistoryGetter dealGetter(_comission_manager);

   DealKeeper deals[];
   dealGetter.getHistory(deals,0,TimeCurrent());

   int total= ArraySize(deals);

   string headder = "Asset;From;To;Deal DT (Unix seconds); Deal DT (Unix miliseconds);"+
                    "ENUM_DEAL_TYPE;ENUM_DEAL_ENTRY;ENUM_DEAL_REASON;Volume;Price;Comission;"+
                    "Profit;Symbol;Comment";

   for(int i=0; i<total; i++)
     {
      DealKeeper selected = deals[i];
      string asset = selected.symbol;
      datetime from = selected.DT_min;
      datetime to = selected.DT_max;

      for(int j=0; j<ArraySize(selected.deals); j++)
        {
         string row;
         AddRow(asset,row);
         AddRow((string)from,row);
         AddRow((string)to,row);

         AddRow((string)selected.deals[j].DT,row);
         AddRow((string)selected.deals[j].DT_msc,row);
         AddRow(EnumToString(selected.deals[j].type),row);
         AddRow(EnumToString(selected.deals[j].entry),row);
         AddRow(EnumToString(selected.deals[j].reason),row);
         AddRow((string)selected.deals[j].volume,row);
         AddRow((string)selected.deals[j].price,row);
         AddRow((string)selected.deals[j].comission,row);
         AddRow((string)selected.deals[j].profit,row);
         AddRow(selected.deals[j].symbol,row);
         AddRow(selected.deals[j].comment,row);

         writer(fileName,headder,row);

        }

      writer(fileName,headder,"");
     }


  }

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

void OnDeinit(const int reason)
  {
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string file_name = __FILE__+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);

      WRITE_BOT_PARAM(file_name,MaximumRisk);      // Maximum Risk in percentage
      WRITE_BOT_PARAM(file_name,DecreaseFactor);   // Descrease factor
      WRITE_BOT_PARAM(file_name,MovingPeriod);     // Moving Average period
      WRITE_BOT_PARAM(file_name,MovingShift);      // Moving Average shift
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom commission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(__FILE__+" Deals Report.csv", &_comission_manager_);
     }
  }

Для более наглядной картины того, как происходит выгрузка, продемонстрируем пустой шаблон эксперта с добавленными методами, гарантирующими выгрузку отчета:

//+------------------------------------------------------------------+
//|                                                         Test.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define ONLY_CUSTOM_COMISSION
#include <History manager/DealsHistory.mqh>

input double custom_comission   = 0;       // Custom comission;
input int    custom_shift       = 0;       // custom shift;

CCCM _comission_manager_;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string arr[];
      StringSplit(__FILE__,'.',arr);
      string file_name = arr[0]+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom commission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(arr[0]+" Deals Report.csv", &_comission_manager_);
     }
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+

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

Обертка для DLL, создающей аккумулированную историю торгов

Первая статья из данного цикла статей была посвящена созданию DLL-библиотеки на языке C#, работающей с отчетами оптимизаций. Напомню, так как для наших целей (реализация непрерывной скользящей оптимизации) наиболее удобно оперировать XML-файлами, то была создана DLL-библиотека, которая умеет читать, писать, а также сортировать полученные отчеты. Из эксперта нам понадобится лишь функционал записи данных, но так как оперировать функциями в чистом виде куда менее удобно и затратно, чем объектами, то был создан класс-обертка функционала выгрузки данных. Данный объект располагается в файле "XmlHistoryWriter.mqh" и называется, соответственно, СXmlHistoryWriter. Помимо рассматриваемого объекта, в нем определена структура параметров робота, которая понадобится нам для передачи списка параметров робота в данный объект. Как обычно, рассмотрим по порядку все имеющиеся детали реализации данного функционала. 

Для того чтобы иметь возможность создавать отчет оптимизации, мы подключаем файл ReportCreator.mqh, а для задействования статических методов класса из описанной в первой статье DLL-библиотеки импортируем ее, причем сама библиотека уже должна находиться в директории MQL5/Libraries.

#include "ReportCreator.mqh"
#import "ReportManager.dll"

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

struct BotParams
  {
   string            name,value;
  };

#define ADD_TO_ARR(arr, value) \
{\
   int s = ArraySize(arr);\
   ArrayResize(arr,s+1,s+1);\
   arr[s] = value;\
}

#define APPEND_BOT_PARAM(Var,BotParamArr) \
{\
   BotParams param;\
   param.name = #Var;\
   param.value = (string)Var;\
   \
   ADD_TO_ARR(BotParamArr,param);\
}

Так как мы будем оперировать с коллекцией объектов, то проще будет работать с динамическими массивами (во всяком случае, превычнее). Для удобства добавления элементов в динамический массив был создан макрос ADD_TO_ARR, который изменяет размер коллекции и затем добавляет в нее переданный элемент. На макрос выбор пал именно из за его универсальности, соответственно, теперь можно добавлять в динамический массив значения любого типа довольно быстро.

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

Строковый формат нужен для корректности соответствия настроек в (*.set) файлах и данных, сохраняемых в наш (*.xml) файл. Как уже рассматривалось в прошлых статьях, set-файлы хранят входные параметры роботов в виде ключ-значение, причем в качестве ключа принимается имя переменной как в коде, а в качестве значения — значение, задаваемого данному входному параметру, причем все перечисления (num) должны задаваться в виде типа int, а не в виде результата работы функции EnumToString(). Описываемый макрос как раз конвертирует все параметры в строку в нужном формате, и все перечисления также переводятся сначала в int, а затем уже в строковый формат.

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

void CopyBotParams(BotParams &dest[], const BotParams &src[])
  {
   int total = ArraySize(src);
   for(int i=0; i<total; i++)
     {
      ADD_TO_ARR(dest,src[i]);
     }
  }

Она нужна, так как стандартная функция ArrayCopy отказывается работать с массивом структур. 

Сам же класс-обертка объявлен следующим образом:

class CXmlHistoryWriter
  {
private:
   const string      _path_to_file,_mutex_name;
   CReportCreator    _report_manager;

   string            get_path_to_expert();//

   void              append_bot_params(const BotParams  &params[]);//
   void              append_main_coef(PL_detales &pl_detales,
                                      TotalResult &totalResult);//
   double            get_average_coef(CoefChartType type);
   void              insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);//
   void              append_days_pl();//

public:
                     CXmlHistoryWriter(string file_name,string mutex_name,
                     CCCM *_comission_manager);//
                     CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager);
                    ~CXmlHistoryWriter(void) {_report_manager.Clear();} //

   void              Write(const BotParams &params[],datetime start_test,datetime end_test);//
  };

Для записи в файл в нем объявлено два строковых константных поля:

Первое поле содержит путь к файлу, в который будут записаны данные, а второе — имя используемого мьютекса. Реализация именованного мьютекса вынесена в DLL C#, и она стандартна. Сам же мьютекс нам необходим, так как процесс оптимизации будет происходить в разных потоках на разных ядрах и разных процессах (один запуск робота — один процесс), посему у нас может возникнуть ситуация, когда две оптимизации завершились, и два и более процессов стараются одновременно записать результаты в один и тот же файл, что недопустимо. Для устранения этой опасности используется объект синхронизации на основе ядра операционной системы — именованный мьютекс. 

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

Начнем рассмотрение реализации класса с конструктора класса.

CXmlHistoryWriter::CXmlHistoryWriter(string file_name,
                                     string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
   _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name),
   _report_manager(_comission_manager)
  {
  }
CXmlHistoryWriter::CXmlHistoryWriter(string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
   _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+MQLInfoString(MQL_PROGRAM_NAME)+"_"+"Report.xml"),
   _report_manager(_comission_manager)
  {
  }

Данный класс содержит два конструктора, которые сами по себе ничем не примечательны. Однако стоит уделить внимание второму конструктору, который  сам задает имя файла, где хранится отчет оптимизаций. Дело в том, что в автооптимизаторе, рассмотрение которого будет производиться в следующей статье, будет возможность задавать свои менеджеры оптимизаций, но в том менеджере оптимизаций, что зашит по умолчанию, уже реализовано соглашение о наименовании файлов с отчетом генерируемых роботом, и как раз второй конструктор задает его. Согласно данному соглашению, имя файла должно начинаться с имени робота, затем нижнее подчеркивание и приписка "_Report.xml". Также несмотря на то что DLL может писать файл отчета повсеместно на компьютере, для подчеркивания принадлежности данного файла к работе терминала мы будем всегда хранить его в директории Common из песочницы MetaTrader5. 

Метод получающий путь к эксперту:

string CXmlHistoryWriter::get_path_to_expert(void)
  {
   string arr[];
   StringSplit(MQLInfoString(MQL_PROGRAM_PATH),'\\',arr);
   string relative_dir=NULL;

   int total= ArraySize(arr);
   bool save= false;
   for(int i=0; i<total; i++)
     {
      if(save)
        {
         if(relative_dir== NULL)
            relative_dir=arr[i];
         else
            relative_dir+="\\"+arr[i];
        }

      if(StringCompare("Experts",arr[i])==0)
         save=true;
     }

   return relative_dir;
  }

Путь к эксперту нам понадобится для автоматического запуска выбранного эксперта. Для этого в ini-файле, передаваемом при старте терминала, потребуется указать его путь, но относительно директории Experts, а не полный, который мы получаем как результат работы  функции получения пути до текущего эксперта. Поэтому мы сначала разбиваем полученный путь на его составляющие, где в качестве разделителя выступает слеш, а затем в цикле  ищем, начиная с самой первой директории, директорию "Experts", а когда директория найдена —  формируем путь до робота , начиная со следующей директории (или же файла робота если он лежит прямо в корне искомой директории).

Метод append_bot_params:

Данный метод является оберткой для импортируемого метода с аналогичным именем и его реализация следующая:

void CXmlHistoryWriter::append_bot_params(const BotParams &params[])
  {

   int total= ArraySize(params);
   for(int i=0; i<total; i++)
     {
      ReportWriter::AppendBotParam(params[i].name,params[i].value);
     }
  }

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

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

Метод get_average_coef создан для вычисления средних значений коэффициентов путем обычной средней по переданным графикам коэффициентов. Он используется для вычисления среднего профит фактора и среднего фактора восстановления.  

Метод insert_day является просто удобной для вызова оберткой импортируемому методу ReportWriter::AppendDay, а метод append_days_pl уже использует ранее упомянутую обертку.

Среди всех этих методов оберток существует один публичный метод, который выступает в роли основного, он приводит в исполнение весь мезанизм сохранения данных — им является метод Write.

void CXmlHistoryWriter::Write(const BotParams &params[],datetime start_test,datetime end_test)
  {
   if(!_report_manager.Create())
     {
      Print("##################################");
      Print("Can`t create report:");
      Print("###################################");
      return;
     }
   TotalResult totalResult;
   _report_manager.GetTotalResult(totalResult);
   PL_detales pl_detales;
   _report_manager.GetPL_detales(pl_detales);

   append_bot_params(params);
   append_main_coef(pl_detales,totalResult);

   ReportWriter::AppendVaR(totalResult.total.VaR_absolute.VAR_90,
                           totalResult.total.VaR_absolute.VAR_95,
                           totalResult.total.VaR_absolute.VAR_99,
                           totalResult.total.VaR_absolute.Mx,
                           totalResult.total.VaR_absolute.Std);

   ReportWriter::AppendMaxPLDD(pl_detales.total.profit.totalResult,
                               pl_detales.total.drawdown.totalResult,
                               pl_detales.total.profit.orders,
                               pl_detales.total.drawdown.orders,
                               pl_detales.total.profit.dealsInARow,
                               pl_detales.total.drawdown.dealsInARow);
   append_days_pl();

   string error_msg=ReportWriter::MutexWriter(_mutex_name,get_path_to_expert(),AccountInfoString(ACCOUNT_CURRENCY),
                    _report_manager.GetBalance(),
                    (int)AccountInfoInteger(ACCOUNT_LEVERAGE),
                    _path_to_file,
                    _Symbol,(int)Period(),
                    start_test,
                    end_test);
   if(StringCompare(error_msg,"")!=0)
     {
      Print("##################################");
      Print("Error while creating (*.xml) report file:");
      Print("_________________________________________");
      Print(error_msg);
      Print("###################################");
     }
  }

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

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

Класс автоматического формирования отчета торгов по завершению процесса тестирования — CAutoUpLoader, он находится в файле AutoLoader.mqh. Для работы данного класса мы должны добавить ссылку на предыдущий описанный класс формирования отчетности в XML-формате.

#include <History manager/XmlHistoryWriter.mqh>

Сама же сигнатура данного класса проста:

class CAutoUploader
  {
private:

   datetime          From,Till;
   CCCM              *comission_manager;
   BotParams         params[];
   string            mutexName;

public:
                     CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[]);
   virtual          ~CAutoUploader(void);

   virtual void      OnTick();

  };

Как можно заметить, класс имеет перегружаемый метод OnTick, а также виртуальный деструктор. Все это требуется чтобы его можно было применять как при помощи агрегации, так и при помощи наследования. Поясним что имеется ввиду. Задача данного класса — на каждом тике перезаписывать время завершения процесса тестирования, а также запоминать время начала тестирования — эти параметры необходимы нам для использования предыдущего рассмотренного объекта. Соответственно, у нас есть несколько подходов к его применению — мы либо можем просто инстанцировать данный объект где-нибудь в классе робота (если вы пишете роботов при помощи ООП разумеется), либо в глобальной зоне видимости — что подойдет для тех кто пишет роботов в C стиле.

После этого необходимо в функции OnTick() вызывать метод OnTick() экземпляра данного класса, а после уничтожения объекта данного класса, в его деструкторе, произойдет выгрузка отчета торгов. Вторым же методом применения класса является просто наследование от него класса робота, именно для этого создан виртуальный деструктор и перегружаемый метод OnTick(). Как результат применения второго метода, мы вообще не будем следить за данным классом и будем работать лишь с роботом.  Реализация данного класса проста, как уже можно было догадаться — он просто делегирует работу классу CXmlHistoryWriter:

void CAutoUploader::OnTick(void)
  {
   if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||
      MQLInfoInteger(MQL_TESTER)==1)
     {
      if(From == 0)
         From = iTime(_Symbol,PERIOD_M1,0);
      Till=iTime(_Symbol,PERIOD_M1,0);
     }
  }
CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager),
   mutexName(_mutexName)
  {
   CopyBotParams(params,_params);
  }
CAutoUploader::~CAutoUploader(void)
  {
   if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||
      MQLInfoInteger(MQL_TESTER)==1)
     {
      CXmlHistoryWriter historyWriter(mutexName,
                                      comission_manager);

      historyWriter.Write(params,From,Till);
     }
  }

Дабы картина была еще более понятной, расширим наш шаблон написания роботов, приведенный выше, путем добавления в него описанного функционала:

//+------------------------------------------------------------------+
//|                                                         Test.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define ONLY_CUSTOM_COMISSION
#include <History manager/DealsHistory.mqh>
#include <History manager/AutoLoader.mqh>

class CRobot;

input double custom_comission   = 0;       // Custom comission;
input int    custom_shift       = 0;       // custom shift;

CCCM _comission_manager_;
CRobot *bot;
const string my_mutex = "My Mutex Name for this expert";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);

   BotParams params[];

   APPEND_BOT_PARAM(custom_comission,params);
   APPEND_BOT_PARAM(custom_shift,params);

   bot = new CRobot(&_comission_manager_,my_mutex,params);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string arr[];
      StringSplit(__FILE__,'.',arr);
      string file_name = arr[0]+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(arr[0]+" Deals Report.csv", &_comission_manager_);
     }

   delete bot;
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   bot.OnTick();
  }
//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| Основной класс робота                                            |
//+------------------------------------------------------------------+
class CRobot : CAutoUploader
  {
public:
                     CRobot(CCCM *_comission_manager, string _mutexName, BotParams &_params[]) : CAutoUploader(_comission_manager,_mutexName,_params)
     {}

   void              OnTick() override;
  };

//+------------------------------------------------------------------+
//| Метод запускающий логику робота                                  |
//+------------------------------------------------------------------+
void CRobot::OnTick(void)
  {
   CAutoUploader::OnTick();

   Print("Тут должен быть запуск логики робота");
  }
//+------------------------------------------------------------------+

Итак, первое что мы делаем — это добавляем ссылку на файл, где хранится наш класс обертка для автоматической выгрузки отчетов в XML-формате и  предопределяем наш класс робота, так как удобнее его реализовывать и описывать в конце проекта. Вообще, обычно я делаю алгоритмы именно в виде MQL5-проектов — это намного удобнее одностраничного подхода, потому что класс с роботом и сопутствующие классы разбиты по файлам, однако для удобства приведения примера все было помещено в один файл.
Затем описываем наш класс, для примера это будет пустой класс с одним лишь перегруженным методом OnTick. В данном примере мы прибегли к использованию второго способа применения класса CAutoUploader — наследованию. Стоит заметить, что в перегруженном методе OnTick необходимо явно вызвать метод OnTick базового класса, дабы наш подсчет дат не прекратился, а от этого зависит вся работа автооптимизатора.

Следующим шагом создаем указатель на класс с роботом, так как удобнее его заполнять из метода OnInit, нежели из глобальной области видимости, а также  создаем переменную, хранящую имя мьютекса.

В методе OnInit инстанцируем нашего робота, а в OnDeinit удаляем. Для того чтобы в нашего робота передавался коллбек прихода нового тика вызываем на указателе на робот перегруженный метод OnTick(), и как только все эти действия были выполнены — пишем нашего робота в классе CRobot.

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

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

Заключение

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

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

В приложенном архиве находятся две папки, обе должны быть разархивированы в директорию MQL/Include. Также в директорию MQL5/Libraries должна быть добавлена библиотека  ReportManager.dll, которую нужно взять из прошлой  статьи.

В архиве содержатся следующие файлы:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • AutoLoader.mqh
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • DealsHistory.mqh
    • ReportCreator.mqh
    • ShortReport.mqh
    • XmlHistoryWriter