English 中文 Español Deutsch 日本語 Português
preview
Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору

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

MetaTrader 5Примеры | 7 января 2020, 10:57
3 705 11
Andrey Azatskiy
Andrey Azatskiy

Введение

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

  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, сохраняет данные истории. По завершению цикла добавляются поля со следующими коэффициентами и показателями:

  • PL
  • Total trades
  • Consecutive wins
  • Consecutive Drawdowns
  • Recovery factor
  • Profit factor
  • Payoff
  • Drawdown by pl

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);//
  };

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

  • _path_to_file
  • _mutex_name

Первое поле содержит путь к файлу, в который будут записаны данные, а второе — имя используемого мьютекса. Реализация именованного мьютекса вынесена в 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
Прикрепленные файлы |
Include.zip (33.02 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (11)
Andrey Azatskiy
Andrey Azatskiy | 2 февр. 2020 в 22:25
Kristian Kafarov:

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

Нарочно сейчас перекомпилировал и запустил с нуля проект старый. У меня все заработало. Так что не смогу воспроизвести ошибку.

fxsaber
fxsaber | 2 февр. 2020 в 23:50
Kristian Kafarov:

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

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


ЗЫ Для общего случая можно создавать кастомный символ, который получается из оригинального выкидыванием интервала.

Kristian Kafarov
Kristian Kafarov | 3 февр. 2020 в 12:08
fxsaber:

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

ЗЫ Для общего случая можно создавать кастомный символ, который получается из оригинального выкидыванием интервала.

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

С инструментами Андрея можно дать задание провести мастер-терминалу k оптимизаций, в каждой из которых будет свой параметр "номер участка валидации". Затем правда придется еще писать дополнение чтобы свести статистику воедино.

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

Другой вариант - функция OnTesterDeinit(). Я уже реализовал в ней полноценное WFO, там же можно легко сделать кросс-валидацию по любому критерию. Но "правильным" оно будет только при полном переборе, потому что делается путем перебора фреймов всего участка тестирования. Полный перебор в большинстве случаев нереален. А если запускать генетику, набор фреймов будет уже нечестный, потому что она в процессе опты выбирает результаты в т.ч. по участкам, которые мы хотим сделать проверочными. Хотя насколько это реально повредит - вопрос. Если отношение длины проверочного участка к общему невелико, у генетики должно остаться достаточное количество вариантов, где проверочный оказывается хреновым. А после всей такой общей опты можно оставить еще один участок, не участвовавший в ней, и на нем проверить результат.

Andrey Khatimlianskii
Andrey Khatimlianskii | 3 февр. 2020 в 21:33
Kristian Kafarov:

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

С инструментами Андрея можно дать задание провести мастер-терминалу k оптимизаций, в каждой из которых будет свой параметр "номер участка валидации". Затем правда придется еще писать дополнение чтобы свести статистику воедино.

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

Есть еще инструмент fxsaber-а, он поможет с остальным.

Kristian Kafarov
Kristian Kafarov | 4 февр. 2020 в 09:25
Andrey Khatimlianskii:

Есть еще инструмент fxsaber-а, он поможет с остальным.

Офигеть, fxsaber сделал ровно то, что мне нужно было. Спасибо за ссыль!

Эконометрический подход к поиску рыночных закономерностей: автокорреляция, тепловые карты и диаграммы рассеяния Эконометрический подход к поиску рыночных закономерностей: автокорреляция, тепловые карты и диаграммы рассеяния
Расширенное исследование сезонных характеристик: автокорреляция тепловые карты и диаграммы рассеяния. Целью текущей статьи является показать, что "память рынка" имеет сезонный характер, который выражается через максимизацию корреляции приращений произвольного порядка.
Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXX): Отложенные торговые запросы - управление объектами-запросами Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXX): Отложенные торговые запросы - управление объектами-запросами
В прошлой статье создали классы объектов отложенных запросов, соответствующие общей концепции объектов библиотеки. Сегодня займёмся классом, позволяющем управлять объектами отложенных запросов.
Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXI): Отложенные торговые запросы - открытие позиций по условиям Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXI): Отложенные торговые запросы - открытие позиций по условиям
Начиная с этой статьи, мы создадим функционал, позволяющий производить торговлю при помощи отложенных запросов по условию. Например, при наступлении или превышении некоего времени, либо при превышении заданного размера прибыли, либо при регистрации события закрытия позиции по стоплосс.
Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота
Если прошлая статья повествовала о создании DLL-библиотеки, которая будет использоваться в нашем автооптимизаторе и в роботе, то продолжение будет целиком посвящено языку MQL5.