English 中文 Español Deutsch 日本語 Português
LifeHack для трейдера: Сравнительный отчет нескольких тестирований

LifeHack для трейдера: Сравнительный отчет нескольких тестирований

MetaTrader 5Примеры | 27 октября 2016, 13:13
6 024 0
Vladimir Karputov
Vladimir Karputov

Содержание

 

Введение

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

Некоторые решения были уже частично рассмотрены в статьях:

Схема работы будет такая:

  1. Определяемся с советником, который будет тестироваться (Win API)
  2. Парсим код советника и дописываем в него вызов библиотеки графического отчета (Win API, MQL5 и регулярные выражения)
  3. Парсим common.ini основного терминала и подготавливаем индивидуальные common.ini для каждого терминала (Win API, MQL5 и регулярные выражения)
  4. копируем индивидуальные common.ini по папкам терминалов (Win API)
  5. Копируем индивидуальные common.ini по папкам терминалов (Win API)
  6. Парсим отчёты подчинённых терминалов
  7. Сводим отчеты подчиненных терминалов в один общий отчет

 

Необходимые действия

Перед запуском советника нужно провести "синхронизацию" главного и подчинённых терминалов.

  1. И в главном, и в подчиненных терминалах должен быть запущен один и тот же торговый счет.
  2. В настройках всех подчинённых терминалов необходимо включить опцию "Разрешить использование dll". Если вы запускаете терминалы с ключом \Portable — зайдите в каталог установки терминала (при помощи проводника или другого файлового менеджера), запустите терминал "terminal64.exe" и выставьте в настройках "Разрешить использование dll".
  3. Библиотека "DistributionOfProfits.mqh" должна быть во всех каталогах данных (каталог данных\MQL5\Include\DistributionOfProfits.mqh) подчинённых терминалов.

1. Входные параметры. Выбор советника для тестирования

Так как на моем компьютере четыре ядра, то я могу запустить только четыре агента тестирования. Значит, одновременно (или с небольшой задержкой в несколько секунд) я могу запустить только четыре терминала — по одному на каждый агент. Именно поэтому во входных параметрах отображены четыре группы настроек:

inputs

Параметры:

  • 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 можно прочитать в этих разделах одной из моих предыдущих статей:

Выбор советника осуществляется при помощи системного диалога "Открыть файл" (функция GetOpenFileNameW):

open file 

Подробнее про вызов системного диалога "Открыть файл" я уже говорил в статье "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 состоит из очень коротких СТРОК, а в конце строк всегда ставятся символы конца строки (невидимые символы). Можно пойти двумя путями:

Построчное считываниеСчитывание всего файла в одну переменную
  • считывать по одной строке и искать совпадение с "[Common]" (не обязательно искать "в лоб": можно задать в поиск что-то вроде шаблона "[Com" несколько_символов "]")
    • при нахождении записывать в массив найденные строки
    • искать следующее совпадение с "["
      • при нахождении прекратить запись в массив, поскольку в массиве на этот момент уже будет весь раздел "[Common]"
  • считать все строки в одну строковую переменную
  • искать шаблон "[Common]" несколько символов "]" (здесь тоже не обязательно искать в лоб: можно искать что-то вроде шаблона "[Com" несколько символов "]")
  • при нахождении записать совпадение в строковую переменную

Тестовый файл "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]" — построчное считывание или считывание в одну переменную? Я выбрал построчное считывание и такой алгоритм:

  1. поиск строки "[Common]" (MQL5);
  2. после нахождения — запись найденной строки в массив;
  3. дальше продолжаем записывать в массив строки, пока регулярное выражение не обнаружит символ "[".

Пример этого подхода реализован в скрипте "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-файла выбранного советника

Задачи, которые нужно решить:

3.1. Секрет №3 

Почему секрет номер три? Дело в том, что  Секрет №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:

      if(!CopyCommonIni())
         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 файлы:

      //--- delete expert_short_name+".set" files
      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)

Также удалим файл отчёта тестера из папок подчинённых терминалов:

      DeleteFileW(slaveTerminalDataPath4+"\\MQL5\\Experts\\"+compiled_expert);
      //--- 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:

      DeleteFileW(slaveTerminalDataPath4+"\\"+file_report);
      //--- с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

3.2. Встраиваем "#include"

Описание функции 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);
  }

3.3. Встраиваем "OnTester()"

Теперь задача становится намного сложнее, так как слово "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:

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-страницу:

scheme

Ранее в конфигурационном ini-файле, в блоке [Tester], мы записывали такой параметр "Report":

[Tester]
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), в который терминал будет сохранять отчёт после теста. Из этого отчёта (для каждого подчинённого терминала) нам предстоит взять такие данные: название символа и период, на которых тестировался советник,  показатели из блока "Бэктест" и график баланса. Из всех подчинённых терминалов будет сформирован такой сравнительный отчёт:

report

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

Для начала введём флаг "find_report", который будет разрешать советнику начинать поиск файлов отчетов:

string         slaveTerminalDataPath4=NULL;                                // the path to the Data Folder of the terminal #4
//---
string         arr_path[][2];
bool           find_report=false;
//+------------------------------------------------------------------+
//| Enumeration command to start the application                     |
//+------------------------------------------------------------------+
enum EnSWParam

и добавим функцию OnTimer():

int OnInit()
  {
//--- 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 см. в предыдущей статье: 

А в данном коде мы сравниваем полученное имя файла, и если оно совпадаем с заданным, то возвращаем 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:

//--- reports -> 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 проводятся некоторые манипуляции, в результате чего файлы принимают примерно такой вид:

simplified page 

На такой файл уже легко "натравить" регулярное выражение "(>)(.*?)(<)" — то есть, поиск любых символов, которые находятся между символами ">" и "<", при этом количество таких символов начинается от нуля.

Результаты работы регулярных выражений помещаются в четыре массива: arr_report_1, arr_report_2, arr_report_3 и arr_report_4. Информация из этих массивов будет использоваться для генерации кода итогового сравнительного отчёта. После создания итогового отчёта мы вызываем WinAPI функцию ShellExecuteW (подробнее про ShellExecuteW см. здесь ) и запускаем браузер:

ShellExecuteW(hwnd,"open",path,NULL,NULL,SW_SHOWNORMAL);

Открывается страница браузера, на которой можно сравнить результаты тестов советника сразу на четырёх символах. 

 

Заключение

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


Прикрепленные файлы |
Статистические распределения в виде гистограмм без индикаторных буферов и массивов Статистические распределения в виде гистограмм без индикаторных буферов и массивов
В статье рассматривается возможность создания гистограмм статистических распределений характеристик рынка с использованием графической памяти, то есть без использования индикаторных буферов и массивов. Приведены подробные примеры построения таких гистограмм и показан так называемый "скрытый" функционал графических объектов языка MQL5.
Торговая стратегия '80-20' Торговая стратегия '80-20'
В статье описывается создание инструментов (индикатора и советника) для исследования торговой стратегии '80-20'. Правила ТС взяты из книги Линды Рашке и Лоуренса Коннорса "Биржевые секреты. Высокоэффективные стратегии краткосрочной торговли". На языке MQL5 формализованы правила этой стратегии, а созданные на ее основе индикатор и советник протестированы на современной истории рынка.
Графические интерфейсы X: Текстовое поле ввода, слайдер картинок и простые элементы управления (build 5) Графические интерфейсы X: Текстовое поле ввода, слайдер картинок и простые элементы управления (build 5)
В этой статье будут рассматриваться новые элементы управления: «Текстовое поле ввода», «Слайдер картинок», а также простые дополнительные элементы: «Текстовая метка» и «Картинка». Библиотека продолжает развиваться, и кроме добавления новых элементов управления, дорабатываются и ранее созданные.
Универсальный Зигзаг Универсальный Зигзаг
Зигзаг — один из самых популярных индикаторов среди пользователей MetaTrader 5. В статье были проанализированы возможности создания различных вариантов Зигзага. В результате мы получаем универсальный индикатор с широкими возможностями для расширения функциональности, который удобно использовать при разработке торговых советников и других индикаторов.