
Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору
Введение
Вашему вниманию представляется третья статья из цикла статей про создание автооптимизатора для скользящей непрерывной оптимизации, остальные статьи можно прочесть по следующим ссылкам:
- Непрерывная скользящая оптимизация (Часть 1): Механизм работы с отчетами оптимизации
- Непрерывная скользящая оптимизация (Часть 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 ¶ms[]);// 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 ¶ms[],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 ¶ms[]) { 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 ¶ms[],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 ¶ms[]); 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, которую нужно взять из прошлой статьи.
В архиве содержатся следующие файлы:
- CustomGeneric
- GenericSorter.mqh
- ICustomComparer.mqh
- History manager
- AutoLoader.mqh
- CustomComissionManager.mqh
- DealHistoryGetter.mqh
- DealsHistory.mqh
- ReportCreator.mqh
- ShortReport.mqh
- XmlHistoryWriter





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
До роботов дело даже не дошло, я просто пытаюсь вызвать вашу панельку из первых двух статей, запуская эксперт OptimisationManagerExtention. После этого терминал вылетает.
Нарочно сейчас перекомпилировал и запустил с нуля проект старый. У меня все заработало. Так что не смогу воспроизвести ошибку.
Больше всего хочу реализовать кросс-валидацию, чтобы бить историю на K частей, каждую из них выкидывать по очереди, оптимизировать на оставшихся, затем проверять на выкинутой, и так K раз.
Выделенное не получится в общем случае. Нужно в своей ТС задать два входных параметра, определяющих не торговый (выкинутый) интервал. Тогда реально.
ЗЫ Для общего случая можно создавать кастомный символ, который получается из оригинального выкидыванием интервала.
Выделенное не получится в общем случае. Нужно в своей ТС задать два входных параметра, определяющих не торговый (выкинутый) интервал. Тогда реально.
ЗЫ Для общего случая можно создавать кастомный символ, который получается из оригинального выкидыванием интервала.
Я ровно так и собирался. Только параметра достаточно одного, потому что разбиение идет на равные части. Параметр указывает номер выкидываемого участка. Ну можно еще добавить параметр "число частей".
С инструментами Андрея можно дать задание провести мастер-терминалу k оптимизаций, в каждой из которых будет свой параметр "номер участка валидации". Затем правда придется еще писать дополнение чтобы свести статистику воедино.
Все было бы в сто раз проще, если бы в тестере была возможность принудительно перебрать некоторые параметры полностью во время генетики. Тогда результаты опты можно анализировать, разделив их по параметру "номер участка".
Другой вариант - функция OnTesterDeinit(). Я уже реализовал в ней полноценное WFO, там же можно легко сделать кросс-валидацию по любому критерию. Но "правильным" оно будет только при полном переборе, потому что делается путем перебора фреймов всего участка тестирования. Полный перебор в большинстве случаев нереален. А если запускать генетику, набор фреймов будет уже нечестный, потому что она в процессе опты выбирает результаты в т.ч. по участкам, которые мы хотим сделать проверочными. Хотя насколько это реально повредит - вопрос. Если отношение длины проверочного участка к общему невелико, у генетики должно остаться достаточное количество вариантов, где проверочный оказывается хреновым. А после всей такой общей опты можно оставить еще один участок, не участвовавший в ней, и на нем проверить результат.
Я ровно так и собирался. Только параметра достаточно одного, потому что разбиение идет на равные части. Параметр указывает номер выкидываемого участка. Ну можно еще добавить параметр "число частей".
С инструментами Андрея можно дать задание провести мастер-терминалу k оптимизаций, в каждой из которых будет свой параметр "номер участка валидации". Затем правда придется еще писать дополнение чтобы свести статистику воедино.
Все было бы в сто раз проще, если бы в тестере была возможность принудительно перебрать некоторые параметры полностью во время генетики. Тогда результаты опты можно анализировать, разделив их по параметру "номер участка".
Есть еще инструмент fxsaber-а, он поможет с остальным.
Есть еще инструмент fxsaber-а, он поможет с остальным.
Офигеть, fxsaber сделал ровно то, что мне нужно было. Спасибо за ссыль!