LifeHack для трейдера: Сравнительный отчет нескольких тестирований
Vladimir Karputov | 27 октября, 2016
Содержание
- Введение
- Необходимые действия
- 1. Входные параметры. Выбор советника для тестирования
- 2. И снова об common.ini
- 2.1. common.ini -> original.ini
- 2.2. Поиск раздела [Common] при помощи регулярных выражений
- 2.3. Создаём четыре файла: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini и myconfiguration4.ini
- 2.4. Редактирование ini-файлов (копируем в них общий раздел [Common] и индивидуальные разделы [Tester])
- 3. Парсинг и редактирование mq5 файла выбранного советника
- 3.1 Секрет №3
- 3.2. Встраиваем "#include"
- 3.3. Встраиваем "double OnTester()"
- 3.4. Сложный случай: в коде уже есть и DistributionOfProfits.mqh, и/или OnTester()
- 4. Копирование эксперта в папки подчинённых терминалов
- 5. Запуск подчинённых терминалов
- 6. Сравнительный отчёт
- Заключение
Введение
Поочерёдный запуск тестирования советника на нескольких символах — не очень наглядный процесс, так как результаты тестов по каждому символу приходится сохранять в отдельные файлы и уже только после этого сравнивать. Я же предлагаю изменить этот подход и проводить одновременное тестирование советника сразу на нескольких символах. В таком случае можно результаты тестов собрать в одном месте и визуально их сравнить.
Некоторые решения были уже частично рассмотрены в статьях:
- LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше
- LifeHack для трейдера: "Тихая" оптимизация или Строим распределения трейдов
- Регулярные выражения для трейдеров
Схема работы будет такая:
- Определяемся с советником, который будет тестироваться (Win API)
- Парсим код советника и дописываем в него вызов библиотеки графического отчета (Win API, MQL5 и регулярные выражения)
- Парсим common.ini основного терминала и подготавливаем индивидуальные common.ini для каждого терминала (Win API, MQL5 и регулярные выражения)
- копируем индивидуальные common.ini по папкам терминалов (Win API)
- Копируем индивидуальные common.ini по папкам терминалов (Win API)
- Парсим отчёты подчинённых терминалов
- Сводим отчеты подчиненных терминалов в один общий отчет
Необходимые действия
Перед запуском советника нужно провести "синхронизацию" главного и подчинённых терминалов.
- И в главном, и в подчиненных терминалах должен быть запущен один и тот же торговый счет.
- В настройках всех подчинённых терминалов необходимо включить опцию "Разрешить использование dll". Если вы запускаете терминалы с ключом \Portable — зайдите в каталог установки терминала (при помощи проводника или другого файлового менеджера), запустите терминал "terminal64.exe" и выставьте в настройках "Разрешить использование dll".
- Библиотека "DistributionOfProfits.mqh" должна быть во всех каталогах данных (каталог данных\MQL5\Include\DistributionOfProfits.mqh) подчинённых терминалов.
1. Входные параметры. Выбор советника для тестирования
Так как на моем компьютере четыре ядра, то я могу запустить только четыре агента тестирования. Значит, одновременно (или с небольшой задержкой в несколько секунд) я могу запустить только четыре терминала — по одному на каждый агент. Именно поэтому во входных параметрах отображены четыре группы настроек:
Параметры:
- folder of the MetaTrader#ххх installation — папка, в которую инсталлирован терминал
- the tested symbol for the terminal #xxx — символ, на котором будет запущен тестер стратегий
- the tested period for the terminal #xxx — период, на котором будет запущен тестер стратегий
- correct name of the file of the terminal — название файла терминала
- sleeping in milliseconds — пауза между запусками подчинённых терминалов
- date of beginning testing (only year, month and day) — дата начала тестирования
- dates of end testing (only year, month and day) — дата окончания тестирования
- initial deposit — депозит
- leverage — плечо
Перед стартом основных алгоритмов нужно связать папки установки подчинённых терминалов и их каталоги данных в папке AppData. Вот пример простого скрипта Check_TerminalPaths.mq5:
//+------------------------------------------------------------------+ //| Check_TerminalPaths.mq5 | //| Copyright 2009, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Print("TERMINAL_PATH = ",TerminalInfoString(TERMINAL_PATH)); Print("TERMINAL_DATA_PATH = ",TerminalInfoString(TERMINAL_DATA_PATH)); Print("TERMINAL_COMMONDATA_PATH = ",TerminalInfoString(TERMINAL_COMMONDATA_PATH)); } //+------------------------------------------------------------------+
Этот скрипт выводит три параметра:
- TERMINAL_PATH — папка, из которой запущен терминал
- TERMINAL_DATA_PATH — папка, в которой хранятся данные терминала
- TERMINAL_COMMONDATA_PATH — общая папка всех клиентских терминалов, установленных на компьютере
Пример для трёх терминалов (один из них запущен с ключом /Portable):
// Терминал запускается в основном режиме TERMINAL_PATH = C:\Program Files\MetaTrader 5 TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common // Терминал запускается в основном режиме TERMINAL_PATH = D:\MetaTrader 5 3 TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\0C46DDCEB43080B0EC647E0C66170465 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common // Терминал запускается в режиме Portable TERMINAL_PATH = D:\MetaTrader 5 5 TERMINAL_DATA_PATH = D:\MetaTrader 5 5 TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common
Подробнее о сопоставлении папок терминалов и их папками в AppData можно прочитать в этих разделах одной из моих предыдущих статей:
- Связь между папкой установки и каталогом данных в папке AppData
- Сопоставляем папку установки и папку AppData Подчинённых терминалов
Выбор советника осуществляется при помощи системного диалога "Открыть файл" (функция GetOpenFileNameW):
Подробнее про вызов системного диалога "Открыть файл" я уже говорил в статье "LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше": 4.2. Выбираем советник при помощи системного диалога "Открыть файл".
В данной редакции (файл GetOpenFileNameW.mqh, версия 1.003) внесены изменения в функцию OpenFileName:
//+------------------------------------------------------------------+ //| Creates an Open dialog box | //+------------------------------------------------------------------+ string OpenFileName(const string filter_description="Editable code", const string filter="\0*.mq5\0", const string title="Select source file") { string path=NULL; if(GetOpenFileName(path,filter_description+filter,TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\",title)) return(path); else { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(NULL); } }
Теперь задавать фильтр поиска файлов стало удобнее. Кроме того, обратите внимание: теперь фильтр ищет файлы в редактируемом формате *.mq5 (в предыдущей статье производился поиск скомпилированных *ex5 файлов).
2. И снова об common.ini
Теперь перейдем к описанию работы функции CopyCommonIni() файла Compare multiple tests.mq5.
Запуск подчинённых терминалов осуществляется с указанием собственного конфигурационного файла. Подчинённых терминалов у нас четыре, а значит, и *.ini файлов тоже будет четыре: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini, myconfiguration4.ini. Файл myconfigurationХ.ini создаётся на базе файла common.ini терминала, из которого запускается наш советник. Путь к файлу common.ini:
TERMINAL_DATA_PATH\config\common.ini
Алгоритм работы по созданию и редактированию файлов myconfiguration.ini выглядит так:
- common.ini копируем в папку TERMINAL_COMMONDATA_PATH\Files\original.ini (WinAPI CopyFileW)
- в файле original.ini ищем раздел [Common] (MQL5 + регулярные выражения).
На примере моего главного терминала (в этот терминале не осуществлён вход в mql5.communiyty) этот раздел выглядит так:
[Common] Login=5116256 ProxyEnable=0 ProxyType=0 ProxyAddress= ProxyAuth= CertInstall=0 NewsEnable=0 NewsLanguages=
- создаём четыре файла: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini и myconfiguration4.ini (MQL5)
- редактируем эти четыре файла (копируем в них общий раздел [Common] и индивидуальные разделы [Tester]) (MQL5)
2.1. common.ini -> original.ini
Это, вероятно, самый лёгкий код: получаю в переменные пути к папкам "Data Folder" и "Common Data Folder", инициализирую переменную значением "original.ini"
string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); // path to Data Folder string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);// path to Common Data Folder string original_ini="original.ini"; string arr_common[]; //--- string full_name_common_ini=terminal_data_path+"\\config\\common.ini"; // full path to the common.ini file string full_name_original_ini=common_data_path+"\\Files\\"+original_ini; // full path to the original.ini file //--- common.ini -> original.ini if(!CopyFileW(full_name_common_ini,full_name_original_ini,false)) { PrintFormat("Failed with error: %x",kernel32::GetLastError()); return(false); }
При помощи Win API функции CopyFileW копирую конфигурационный файл "common.ini" в файл "original.ini".
2.2. Поиск раздела [Common] при помощи регулярных выражений
Для поиска и копирования раздела [Common] применим регулярные выражения. Задача перед нами стоит не совсем обычная, ведь файл common.ini состоит из очень коротких СТРОК, а в конце строк всегда ставятся символы конца строки (невидимые символы). Можно пойти двумя путями:
Построчное считывание | Считывание всего файла в одну переменную |
---|---|
|
|
Тестовый файл "test_original.ini":
[Charts] ProfileLast=Default MaxBars=100000 PrintColor=0 SaveDeleted=0 TradeLevels=1 TradeLevelsDrag=0 ObsoleteLasttime=1475473485 [Common] Login=1783501 ProxyEnable=0 ProxyType=0 ProxyAddress= ProxyAuth= CertInstall=0 NewsEnable=0 [Tester] Expert=test Symbol=EURUSD Period=H1 Deposit=10000 Model=4 Optimization=0 FromDate=2016.01.22 ToDate=2016.06.06 Report=TesterReport ReplaceReport=1 UseLocal=1 Port=3000 Visual=0 ShutdownTerminal=0
На "test_original.ini" файле можно потренироваться в применении регулярных выражений при помощи скрипта "Receiving lines.mq5". В настройках скрипта можно выбрать два режима работы:
- построчное считывание и поиск в каждой строке
- или считывание всего файла в одну переменную.
Несколько примеров сравнения в работе этих двух методов:
Построчное считывание | Считывание всего файла в одну переменную |
---|---|
Запрос: "Prox(.*)0" - ищем слово "Prox" - затем любой символ, кроме перевода строки или другого разделителя Unicode-строки, встречающийся ноль или более раз (жадный) "(.*)" - закончиться поиск должен при нахождении цифры "0" | |
12: 0: ProxyEnable=0, 13: 0: ProxyType=0, | : 0: ProxyEnable=0ProxyType=0ProxyAddress=ProxyAuth=CertInstall=0NewsEnable=0[Tester]Expert=test Symbol=EURUSD Period=H1 Deposit=10000 Model=4 Optimization=0 FromDate=2016.01.22 ToDate=2016.06.06 Report=TesterReport ReplaceReport=1 UseLocal=1 Port=3000 Visual=0 ShutdownTerminal=0, |
Как видите, здесь выдано два результата | А здесь в выдаче только один результат, но в нём очень много лишнего (сработал жадный запрос) |
Запрос: "Prox(.*?)0" - ищем слово "Prox" - затем любой символ, кроме перевода строки или другого разделителя Unicode-строки, встречающийся ноль или более раз (не жадный) "(.*?)" - закончиться поиск должен при нахождении цифры "0" | |
12: 0: ProxyEnable=0, 13: 0: ProxyType=0, | : 0: ProxyEnable=0, 1: ProxyType=0, 2: ProxyAddress=ProxyAuth=CertInstall=0, |
Здесь снова два результата | А вот в этом случае получены три результата, при этом третий — совсем не тот, что я хотел получить. |
Какой же метод выбрать для вычленения целого блока "[Common]" — построчное считывание или считывание в одну переменную? Я выбрал построчное считывание и такой алгоритм:
- поиск строки "[Common]" (MQL5);
- после нахождения — запись найденной строки в массив;
- дальше продолжаем записывать в массив строки, пока регулярное выражение не обнаружит символ "[".
Пример этого подхода реализован в скрипте "Receiving lines v.2.mq5":
//+------------------------------------------------------------------+ //| Receiving lines v.2.mq5 | //| Copyright 2016, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2016, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.000" #property description "Singling the block \"[Common]\"" #property script_show_inputs #include <RegularExpressions\Regex.mqh> //--- input string file_name="test_original.ini"; // file name input string str_format="(\\[)(.*?)(\\])"; //--- int m_handel; bool m_found_Common=false; // after finding of the word "[Common]" - the flag will be true //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string arr_text[]; // array for rezult //--- Print("format: ",str_format); m_handel=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_TXT); if(m_handel==INVALID_HANDLE) { Print("Operation FileOpen failed, error ",GetLastError()); return; } Regex *rgx=new Regex(str_format); while(!FileIsEnding(m_handel)) { string str=FileReadString(m_handel); if(str=="[Common]") { m_found_Common=true; int size=ArraySize(arr_text); ArrayResize(arr_text,size+1,10); arr_text[size]=str; continue; // goto while... } if(m_found_Common) { MatchCollection *matches=rgx.Matches(str); int count=matches.Count(); if(count>0) { if(count>1) { Print("Alarm! matches.Count()==",count); return; } delete matches; break; // goto FileClose... } else { delete matches; // if no match is found } int size=ArraySize(arr_text); ArrayResize(arr_text,size+1,10); arr_text[size]=str; } } FileClose(m_handel); delete rgx; Regex::ClearCache(); //--- testing int size=ArraySize(arr_text); for(int i=0;i<size;i++) { Print(arr_text[i]); } } //+------------------------------------------------------------------+
Результат работы скрипта:
2016.10.05 06:58:09.276 Receiving lines v.2 (EURUSD,M1) format: (\[)(.*?)(\]) 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) [Common] 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) Login=1783501 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) ProxyEnable=0 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) ProxyType=0 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) ProxyAddress= 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) ProxyAuth= 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) CertInstall=0 2016.10.05 06:58:09.277 Receiving lines v.2 (EURUSD,M1) NewsEnable=0
Как видите, скрипт точно выделил из файла "test_original.ini" блок параметров "[Common]". Алгоритм из скрипта "Receiving lines v.2.mq5" я практически без изменений использую в функции SearchBlock(). Функция SearchBlock() при удачном нахождении блока параметров "[Common]" записывает этот блок в служебный массив arr_common[].
2.3. Создаём четыре файла: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini и myconfiguration4.ini
Все четыре файла создаются последовательным вызовом данного кода (обратите внимание на флаги, используемые при открытии файлов):
//+------------------------------------------------------------------+ //| Open new File | //+------------------------------------------------------------------+ bool IniFileOpen(const string name_file,int &handle) { handle=FileOpen(name_file,FILE_WRITE|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("Operation FileOpen file ",name_file," failed, error ",GetLastError()); return(false); } //--- return(true); }
2.4. Редактирование ini-файлов (копируем в них общий раздел [Common] и индивидуальные разделы [Tester])
Ранее в служебный массив arr_common[] был записан блок параметров [Common]. Теперь этот массив записывается во все четыре файла:
//--- recording block "[Common]" int arr_common_size=ArraySize(arr_common); for(int i=0;i<arr_common_size;i++) { FileWrite(handle1,arr_common[i]); FileWrite(handle2,arr_common[i]); FileWrite(handle3,arr_common[i]); FileWrite(handle4,arr_common[i]); } //--- recording block "[Tester]" string expert_short_name="D0E820_test"; WriteBlockTester(handle1,expert_short_name,ExtTerminal1Symbol,ExtTerminal1Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3000); WriteBlockTester(handle2,expert_short_name,ExtTerminal2Symbol,ExtTerminal2Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3001); WriteBlockTester(handle3,expert_short_name,ExtTerminal3Symbol,ExtTerminal3Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3002); WriteBlockTester(handle4,expert_short_name,ExtTerminal4Symbol,ExtTerminal4Timeframes,ExtDeposit, ExtLeverage,ExtTerminaTick,ExtFromDate,ExtToDate,expert_short_name,3003); //--- close the files FileClose(handle1); FileClose(handle2); FileClose(handle3); FileClose(handle4);
Дальше идёт формирование блока параметров [Tester]: для каждого терминала подготавливаются уникальные параметры (символ и таймфрейм) а также общие параметры (даты начала и окончания тестирования, начальный депозит, плечо).
Созданные файлы myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini и myconfiguration4.ini остаются в общей папке данных (TERMINAL_COMMONDATA_PATH\Files\). Хендлы этих файлов закрываем.
3. Парсинг и редактирование mq5-файла выбранного советника
Задачи, которые нужно решить:
- встроить вызов include файла графического анализа торговой истории (подробнее см. в Запуск графиков аналитики из тестера стратегий);
- встраивание в советник функции OnTester() с вызовом графического анализа.
Почему секрет номер три? Дело в том, что Секрет №1 и Секрет №2 были показаны ранее, в статье LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше.
Рассмотрим следующую ситуацию: терминал запускается из командной строки, и при этом указывается конфигурационный ini-файл. В ini-файле указываем имя советника, который будет запущен в тестере при старте терминала. В данном случае будем иметь в виду, что мы указываем имя советника, который ещё не скомпилирован.
Секрет №3.
Так вот, имя советника НУЖНО писать без расширения. Применительно к этой статье это будет выглядеть так:
NewsEnable=0 [Tester] Expert=D0E820_test Symbol=GBPAUD
Терминал при старте сначала будет искать СКОМПИЛИРОВАННЫЙ ФАЙЛ (применительно к статье, терминал будет искать Expert=D0E820_test.ex5). И только в случае, если терминал не найдёт скомпилированный файл, терминал начнёт компиляцию указанного в in-файле советника.
Исходя из этого, перед тем как приступать к работе по редактированию выбранного советника, нужно пройтись по папкам подчинённых терминалов и удалить скомпилированные версии выбранного советника (конкретно в нашем случае нужно удалить файлы D0E820_test.ex5). Удалять будем при помощи Win API функции DeleteFileW:
return(INIT_FAILED);
//--- delete all files: expert_short_name+".ex5"
ResetLastError();
string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); // path to Common Data Folder
//---
string edited_expert=common_data_path+"\\Files\\"+expert_short_name+".mq5";
//--- delete expert_short_name+".ex5" files
string compiled_expert=expert_short_name+".ex5";
DeleteFileW(slaveTerminalDataPath1+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath2+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath3+"\\MQL5\\Experts\\"+compiled_expert);
DeleteFileW(slaveTerminalDataPath4+"\\MQL5\\Experts\\"+compiled_expert);
//--- delete expert_short_name+".set" files
А теперь нужно обязательно удалить *.set файлы - дело в том, что если Вы измените в выбранном советнике некоторые входные параметры, тестеры всё равно запустятся с параметрами, которые были на предыдущем запуске. Поэтому удалим *.set файлы:
string set_files=expert_short_name+".set";
DeleteFileW(slaveTerminalDataPath1+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath2+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath3+"\\Tester\\"+set_files);
DeleteFileW(slaveTerminalDataPath4+"\\Tester\\"+set_files);
//--- delete expert_short_name+".htm" files (reports)
Также удалим файл отчёта тестера из папок подчинённых терминалов:
//--- delete expert_short_name+".htm" files (reports)
string file_report=expert_short_name+".htm";
DeleteFileW(slaveTerminalDataPath1+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath2+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath3+"\\"+file_report);
DeleteFileW(slaveTerminalDataPath4+"\\"+file_report);
//--- сopying an expert in the TERMINAL_COMMONDATA_PATH\Files folder
if(!CopyFileW(expert_full_name,edited_expert,false))
Для чего мы удаляем файлы отчетов? Это нужно для отслеживания момента, когда во всех подчинённых терминалах появятся файлы отчёта — тогда можно будет парсить эти файлы для создания страницы сравнения полученных результатов тестирования по нескольким символам.
И только после удаления компилированных файлов можно скопировать выбранный файл советника в папку TERMINAL_COMMONDATA_PATH для последующей работы с файлом средствами MQL5:
//--- сopying an expert in the TERMINAL_COMMONDATA_PATH\Files folder
if(!CopyFileW(expert_full_name,edited_expert,false))
{
PrintFormat("Failed CopyFileW expert_full_name with error: %x",kernel32::GetLastError());
return(INIT_FAILED);
}
//--- parsing advisor file
Описание функции Compare multiple tests.mq5::ParsingEA().
В общем случае нужно определить, есть ли уже в файле советника строка "#include <DistributionOfProfits.mqh>". Если такой строки нет, то ее нужно встроить в советник. При этом нужно понимать, что может существовать много вариантов:
Варианты | Подходит/не подходит |
---|---|
"#include <DistributionOfProfits.mqh>" | подходит (идеальный вариант) |
"#include <DistributionOfProfits.mqh>" | подходит (в этом варианте после слова "#include" стоит не пробел, а знак табуляции) |
" #include <DistributionOfProfits.mqh>" | подходит (в этом варианте перед словом "#include" стоит знак табуляции) |
"//#include <DistributionOfProfits.mqh>" | не подходит (это просто комментарий) |
также может быть такой вариант, когда после "#include" стоит не пробел, а знак табуляции или несколько пробелов. В результате для поиска было создано такое регулярное выражение:
"(\\s+?#include|^#include)(.*?)(<DistributionOfProfits.mqh)"
Вот как расшифровывается выражение (\\s+?#include|^#include): (один или больше пробелов, не жадный, далее "#include") или (строка начинается со слова "#include"). За поиск строки отвечает функция NumberRegulars(). Здесь сразу вводится переменная "name_Object_CDistributionOfProfits" — в ней мы сохраним имя объекта CDistributionOfProfits. Это может пригодиться позже, если придётся делать сложный поиск.
//+------------------------------------------------------------------+ //| Insert #include <DistributionOfProfits.mqh> | //| Insert call graphical analysis of trade | //+------------------------------------------------------------------+ bool ParsingEA() { //--- find #include <DistributionOfProfits.mqh> int number=0; string name_Object_CDistributionOfProfits="ExtDistribution"; // CDistributionOfProfits object name string expressions="(\\s+?#include|^#include)(.*?)(<DistributionOfProfits.mqh)"; if(!NumberRegulars(expert_short_name+".mq5",expressions,number)) return(false); if(number==0) // a regular expression is not found { //--- add #include <DistributionOfProfits.mqh> string array[]; ArrayResize(array,2); array[0]="#include <DistributionOfProfits.mqh>"; array[1]="CDistributionOfProfits "+name_Object_CDistributionOfProfits+";"; if(!InsertLine(expert_short_name+".mq5",0,array)) return(false); Print("Line \"#include\" is insert");
Если строка не найдена, значит, нужно встроить ее в советник (функция InsertLine()). Принцип работы такой: построчно считываем файл советника во временный массив. Когда номер строки совпадает с заданным ("position"), то в массив вставляется нужный кусок кода (код берётся из массива "text"). После полного прочтения файл советника удаляется, затем сразу создаётся новый файл с таким же именем. В него записывается информация из временного массива:
//+------------------------------------------------------------------+ //| Insert a line in a file | //+------------------------------------------------------------------+ bool InsertLine(const string name_file,const uint position,string &array_text[]) { int handle; int size_arr=ArraySize(array_text); //--- handle=FileOpen(name_file,FILE_READ|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("Operation FileOpen file ",name_file," failed, error ",GetLastError()); return(false); } int line=0; string arr_temp[]; ArrayResize(arr_temp,0,1000); while(!FileIsEnding(handle)) { string str_text=FileReadString(handle,-1); if(line==position) { for(int i=0;i<size_arr;i++) { int size=ArraySize(arr_temp); ArrayResize(arr_temp,size+1,1000); arr_temp[size]=array_text[i]; } } int size=ArraySize(arr_temp); ArrayResize(arr_temp,size+1,1000); arr_temp[size]=str_text; line++; } FileClose(handle); FileDelete(name_file,FILE_COMMON); //--- handle=FileOpen(name_file,FILE_WRITE|FILE_ANSI|FILE_TXT|FILE_COMMON); if(handle==INVALID_HANDLE) { Print("Operation FileOpen file ",name_file," failed, error ",GetLastError()); return(false); } int size=ArraySize(arr_temp); for(int i=0;i<size;i++) { FileWrite(handle,arr_temp[i]); } FileClose(handle); //--- return(true); }
Теперь задача становится намного сложнее, так как слово "OnTester" может встретиться в коде программы в самых разных вариациях. Например, оно может полностью отсутствовать в коде — вероятно, это самый простой вариант. Можно встретить классический вариант:
double OnTester() {
Это тоже не очень сложно. Но бунтарей среди разработчиков хватает, поэтому мы можем столкнуться и с таким стилем программирования:
double OnTester() {
И, пожалуй, один из сложных вариантов:
/*
//+-------------------------------+
//| |
//+-------------------------------+
double OnTester()
{
...
}
...
*/
Итак, для того, чтобы определиться, встречается ли вообще объявление функции OnTetster в коде, применим такое регулярное выражение:
"(\\s+?double|^double)(.+?)(OnTester\\(\\))(.*)" | |
"(\\s+?double" | \\s пробел, \\s+ пробел встречающийся минимум один раз, \\s+? пробел встречающийся минимум один раз, не жадный оператор, \\s+?double пробел встречающийся минимум один раз, не жадный оператор, и слово "double". |
"|" | | или |
"^double)" | строка начинается со слова double |
"(.+?)" | . любой символ, кроме перевода строки или другого разделителя Unicode-строки, .+ любой символ, кроме перевода строки или другого разделителя Unicode-строки встречающийся один или более раз, .+? любой символ, кроме перевода строки или другого разделителя Unicode-строки встречающийся один или более раз, не жадный |
"(OnTester\\(\\))" | OnTester\\(\\) слово OnTester() |
"(.*)" | . любой символ, кроме перевода строки или другого разделителя Unicode-строки, .* любой символ, кроме перевода строки или другого разделителя Unicode-строки встречающийся ноль или более раз |
В самом простом случае, когда регулярные выражения вернут в поиске ноль, вставляем вызов функции OnTester():
//--- expressions="(\\s+?double|^double)(.+?)(OnTester\\(\\))(.*)"; if(!NumberRegulars(expert_short_name+".mq5",expressions,number)) return(false); if(number==0) // a regular expression is not found { //--- add function OnTester if(!InsertLine(expert_short_name+".mq5",2, "double OnTester()"+ " {"+ " double ret=0.0;"+ " ExtDistribution.AnalysisTradingHistory(0);"+ " ExtDistribution.ShowDistributionOfProfits();"+ " return(ret);"+ " }")) return(false); Print("Line \"OnTester\" is insert"); }
Итого, если в коде не было ни "#include <DistributionOfProfits.mqh>" ни функции "OnTester()", исходный файл будет иметь такой вид (на примере, когда выбрали файл MACD Sample.mq5):
#include <DistributionOfProfits.mqh> CDistributionOfProfits ExtDistribution; double OnTester() { double ret=0.0; ExtDistribution.AnalysisTradingHistory(0); ExtDistribution.ShowDistributionOfProfits(); return(ret); } //+------------------------------------------------------------------+ //| MACD Sample.mq5 | //| Copyright 2009-2016, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2009-2016, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "5.50"
Выглядит этот код не очень эстетично, но, тем не менее, свою задачу он выполняет. В п.п. 3.1 и 3.2 были рассмотрены простые случаи (самые лёгкие) — когда в коде советника изначально не было ни объявления библиотеки графического анализа, ни функции OnTrade(). Далее рассмотрим более сложные случаи — когда в коде изначально есть и объявление библиотеки графического анализа, и/или функция OnTester().
3.4. Сложный случай: в коде уже есть и DistributionOfProfits.mqh, и/или OnTester()
Сложный поиск выполняется в функции AdvancedSearch():
//+------------------------------------------------------------------+ //| Advanced Search | //| only_ontester=true | //| - search only function OnTester() | //| only_ontester=false | //| - search #include <DistributionOfProfits.mqh> | //| and function OnTester() | //+------------------------------------------------------------------+ bool AdvancedSearch(const string name_file,const string name_object,const bool only_ontester)
Параметры:
- name_file — название файла советника
- name_object — имя объекта класса CDistributionOfProfits
- only_ontester — флаг поиска, при only_ontester=true будем искать только функцию OnTester().
В самом начале весь файл считывается во временный массив
string arr_temp[];
— так будет удобнее работать.
Затем последовательно вызываются несколько служебных кодов:
RemovalMultiLineComments() — в этом коде из массива удаляются все многострочные комментарии;
RemovalComments() — здесь удаляются однострочные комментарии;
DeleteZeroLine() — из массива удаляются все строки с нулевой длиной.
Если входной параметр only_ontester==false, значит запускаем поиск строки "#include <DistributionOfProfits.mqh> " — за это отвечает функция FindInclude():
Функция FindInclude() ищет вхождение строки "#include <DistributionOfProfits.mqh>" и запоминает номер строки в переменную "function_position" (напомню, что предварительно, в п. 3.1. Встраиваем "#include", мы при помощи регулярных выражений уже определили, что в коде гарантированно есть строка "#include <DistributionOfProfits.mqh>"). Далее делается попытка найти строку "CDistributionOfProfits". Если такая строка найдена, то из нее получаем имя переменной для класса "CDistributionOfProfits". Если такая строка не найдена, то ее необходимо будет вставить в позицию, сразу после "function_position".
Если входной параметр only_ontester==true, значит, запускаем поиск функции Ontester(). Как только нашли, то ищем в ней строки обращения к библиотеке графического анализа — за это отвечает функция FindFunctionOnTester().
4. Копирование эксперта в папки подчинённых терминалов
Копирование экспертов выполняется в OnInit():
//--- parsing advisor file if(!ParsingEA()) return(INIT_FAILED); //--- сopying an expert in the terminal folders ResetLastError(); if(!CopyFileW(edited_expert,slaveTerminalDataPath1+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("Failed CopyFileW #1 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath2+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("Failed CopyFileW #2 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath3+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("Failed CopyFileW #3 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(edited_expert,slaveTerminalDataPath4+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("Failed CopyFileW #4 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); }
5. Запуск подчинённых терминалов
Перед запуском подчинённых терминалов, пожалуйста, проверьте следующее: торговый счёт в главном терминале, на котором запускается наш советник, должен быть во всех подчинённых терминалах. Также во всех подчинённых терминалах нужно разрешить dll:
Если этого не сделать, то подчинённый терминал не сможет запустить советник (напомню, в нашем советнике очень активно применяются вызовы Win API), и в Тестере, во вкладке "Журнал" будет примерно такое сообщение об ошибке:
2016.10.13 11:28:57 Core 1 2016.02.03 00:00:00 DLL loading is not allowed
Подробнее про системную функцию ShellExecuteW: ShellExecuteW. Между запусками терминалов делается пауза, а непосредственным запуском руководит функция "LaunchSlaveTerminal".
if(!CopyFileW(edited_expert,slaveTerminalDataPath4+"\\MQL5\\Experts\\"+expert_short_name+".mq5",false)) { PrintFormat("Failed CopyFileW #4 with error: %x",kernel32::GetLastError()); return(INIT_FAILED); } //--- launching Slave Terminals Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_1,common_data_path+"\\Files\\myconfiguration1.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_2,common_data_path+"\\Files\\myconfiguration2.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_3,common_data_path+"\\Files\\myconfiguration3.ini"); Sleep(ExtSleeping); LaunchSlaveTerminal(ExtInstallationPathTerminal_4,common_data_path+"\\Files\\myconfiguration4.ini"); } //--- return(INIT_SUCCEEDED); }
6. Сравнительный отчёт
Мы не зря прикладывали столько усилий для парсинга кода выбранного советника: мы внедряли в код выбранного советника вызов библиотеки графического анализа позиций в разрезе времени открытия позиций (эта библиотека описывалась в статье LifeHack для трейдера: "Тихая" оптимизация или Строим распределения трейдов"). Благодаря внедрённому коду каждый советник в подчинённом терминале по окончании тестирования создаёт и автоматически открывает вот такую html-страницу:
Ранее в конфигурационном ini-файле, в блоке [Tester], мы записывали такой параметр "Report":
Expert=D0E820_test
Symbol=GBPAUD
Period=PERIOD_H1
Deposit=100000
Leverage=1:100
Model=0
ExecutionMode=0
FromDate=2016.10.03
ToDate=2016.10.15
ForwardMode=0
Report=D0E820_test
ReplaceReport=1
Port=3000
ShutdownTerminal=0
Это название файла (D0E820_test.htm), в который терминал будет сохранять отчёт после теста. Из этого отчёта (для каждого подчинённого терминала) нам предстоит взять такие данные: название символа и период, на которых тестировался советник, показатели из блока "Бэктест" и график баланса. Из всех подчинённых терминалов будет сформирован такой сравнительный отчёт:
Напомню, что подчинённые терминалы сохраняют отчёты тестирования (в данном случае в формате htm) в корень своих каталогов данных. Значит, нашему советнику нужно запустить подчинённые терминалы, а затем периодически искать в этих каталогах файлы отчётов тестирования. После того, как будут найдены все четыре отчёта, можно будет приступать к формированию общего, сравнительного отчёта.
Для начала введём флаг "find_report", который будет разрешать советнику начинать поиск файлов отчетов:
//---
string arr_path[][2];
bool find_report=false;
//+------------------------------------------------------------------+
//| Enumeration command to start the application |
//+------------------------------------------------------------------+
enum EnSWParam
и добавим функцию OnTimer():
{
//--- create timer
EventSetTimer(9);
ArrayFree(arr_path);
find_report=false; // true - flag allows the search reports
if(!FindDataFolders(arr_path))
return(INIT_SUCCEEDED);
//| Timer function |
//+------------------------------------------------------------------+
void OnTimer()
{
//---
if(!find_report)
return;
}
В OnTimer() будем искать файл "expert_short_name"+".htm". Поиск будет одноуровневый — только в корне каталога данных каждого из подчинённых терминалов. Эту задачу будет выполнять функция ListingFilesDirectory.mqh::FindFile().
Так как поиск проводится за пределами "песочницы", то будем использовать Win API функцию FindFirstFileW. Подробнее про FindFirstFileW см. в предыдущей статье:
- FindFirstFileW, FindNextFileW
- Пример использования FindFirstFileW, FindNextFileW
- Заглядываем внутрь папок терминалов ).
А в данном коде мы сравниваем полученное имя файла, и если оно совпадаем с заданным, то возвращаем true, предварительно закрыв хендл поиска:
//| Find file |
//+------------------------------------------------------------------+
bool FindFile(const string path,const string name)
{
//---
WIN32_FIND_DATA ffd;
long hFirstFind_0;
ArrayInitialize(ffd.cFileName,0);
ArrayInitialize(ffd.cAlternateFileName,0);
//--- stage Search №0.
string filter_0=path+"\\*.*"; // filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\*.*
hFirstFind_0=FindFirstFileW(filter_0,ffd);
//---
string str_handle="";
if(hFirstFind_0==INVALID_HANDLE)
str_handle="INVALID_HANDLE";
else
str_handle=IntegerToString(hFirstFind_0);
//Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",str_handle);
//---
if(hFirstFind_0==INVALID_HANDLE)
{
PrintFormat("Failed FindFirstFile (hFirstFind_0) with error: %x",kernel32::GetLastError());
return(false);
}
//--- list all the files in the directory with some info about them
bool rezult=0;
do
{
string name_0="";
for(int i=0;i<MAX_PATH;i++)
{
name_0+=ShortToString(ffd.cFileName[i]);
}
if(name_0==name)
{
WinAPI_FindClose(hFirstFind_0);
return(true);
}
ArrayInitialize(ffd.cFileName,0);
ArrayInitialize(ffd.cAlternateFileName,0);
ResetLastError();
rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
}
while(rezult!=0); //if(hFirstFind_1==INVALID_HANDLE), we appear here
if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
PrintFormat("Failed FindNextFileW (hFirstFind_0) with error: %x",kernel32::GetLastError());
//else
// Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",hFirstFind_0,", NO_MORE_FILES");
WinAPI_FindClose(hFirstFind_0);
//---
return(false);
}
Советник проверяет факт наличия всех четырёх файлов отчётов в папках подчиненных терминалов: это будет признаком того, что все подчинённые терминалы окончили тестирование.
Теперь нужно обработать эту информацию. Все четыре файла отчётов и их четыре графических файла — графики баланса — копируются в песочницу TERMINAL_COMMONDATA_PATH\Files:
string path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
if(!CopyFileW(slaveTerminalDataPath1+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_1"+".htm",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath1+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_1"+".png",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath2+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_2"+".htm",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath2+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_2"+".png",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath3+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_3"+".htm",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath3+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_3"+".png",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath4+"\\"+expert_short_name+".htm",path+"\\Files\\"+"report_4"+".htm",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
if(!CopyFileW(slaveTerminalDataPath4+"\\"+expert_short_name+".png",path+"\\Files\\"+"report_4"+".png",false))
{
PrintFormat("Failed with error: %x",kernel32::GetLastError());
return;
}
Но в полученных файлах отчетов много избыточной информации, а это сильно затрудняет применение регулярных выражений. Поэтому в функции Compare multiple tests.mq5::ParsingReportToArray проводятся некоторые манипуляции, в результате чего файлы принимают примерно такой вид:
На такой файл уже легко "натравить" регулярное выражение "(>)(.*?)(<)" — то есть, поиск любых символов, которые находятся между символами ">" и "<", при этом количество таких символов начинается от нуля.
Результаты работы регулярных выражений помещаются в четыре массива: arr_report_1, arr_report_2, arr_report_3 и arr_report_4. Информация из этих массивов будет использоваться для генерации кода итогового сравнительного отчёта. После создания итогового отчёта мы вызываем WinAPI функцию ShellExecuteW (подробнее про ShellExecuteW см. здесь ) и запускаем браузер:
Открывается страница браузера, на которой можно сравнить результаты тестов советника сразу на четырёх символах.
Заключение
В статье был описан ещё один вариант того, как можно оценить результаты тестирования советника на четырёх символах. При этом тестирование на выбранных четырёх символах идёт параллельно сразу на четырех терминалах, и в итоге мы имеем сводную таблицу, в которой собраны результаты всех этих тестов.