LifeHack for Trader: A comparative report of several tests

Vladimir Karputov | 21 December, 2016

Contents

 

Introduction

Testing an Expert Advisor multiple times on different symbols would be not very demonstrative, since you would need to save testing results for each symbols to separate files and then to compare the results. I suggest changing this approach and running Expert Advisor tests on several symbols simultaneously. In this case, the test results can be saved into one location and can be visually compared.

Some solutions have already been discussed in previous articles:

Testing scenario:

  1. Choosing an Expert Advisor for testing (Win API)
  2. Parsing the Expert Advisor code and adding the call of the graphical report library to it (Win API, MQL5, and regular expressions)
  3. Parsing common.ini of the main terminal and preparing individual common.ini for each terminal (Win API, MQL5, and regular expressions)
  4. Copying individual common.ini to the folders of the terminals (Win API)
  5. Copying individual common.ini to the folders of the terminals (Win API)
  6. Parsing reports of slave terminals
  7. Adding results of the slave terminals into one common report

 

Necessary Actions

Before launching the Expert Advisor, we need to "synchronize" the master and slave terminals.

  1. The master and slave terminals should be connected with the same trading account.
  2. In the settings of all slave terminals, allow the use of DLLs. If you launch terminals with the \Portable key, enter the terminal installation directory (using the explorer or any other file manager), launch the terminal "terminal64.exe" and set "Allow DLL import".
  3. The "DistributionOfProfits.mqh" library should be added to all data folders (data folder\MQL5\Include\DistributionOfProfits.mqh) of slave terminals.

1. Input parameters. Selecting an Expert Advisor to test

Since my computer has four cores, I can only run four testing agents. Hence, simultaneously (or with a small delay of a few seconds), I can only run four terminals, i.e. one terminal per agent. That is why four groups of settings are available in the input parameters:

inputs

Parameters:


Before the start of the basic algorithms, we need to link the installation folders of the slave terminals and data directories in the AppData folder. Here is an example of a simple script 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));
  }
//+------------------------------------------------------------------+

The script prints three parameters:

An example for three terminals (one of them is running with the /Portable key):

// The terminal is launched in the main mode
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

// The terminal is launched in the main mode
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

// The terminal is launched in the Portable mode
TERMINAL_PATH 			= D:\MetaTrader 5 5
TERMINAL_DATA_PATH 			= D:\MetaTrader 5 5
TERMINAL_COMMONDATA_PATH 			= C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common

You can read more about the correspondence of terminal folders and the folders in AppData in the following sections of one of my previous articles:

An Expert Advisor is selected using the "Open File" system dialog (the  GetOpenFileNameW function):

open file 

Details about the call of the Open File dialog were discussed in my previous article "LifeHack for trader: Four backtests are better than one": 4.2. Selecting an EA with the "Open file" system dialog. 

The current version (file GetOpenFileNameW.mqh, version 1.003) features changes in the OpenFileName function:

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

Now it has become more convenient to set the file search filter. Also please note that now the filter searches for files in the recommended *.mq5 format (in the previous article compiled *ex5 files were searched). 


2. Again about common.ini

Now it's time to describe the operation of the CopyCommonIni() function in 'Compare multiple tests.mq5'.

The slave terminals are launched by specifying a configuration file. We have four slave terminals, so we need to create four *.ini files: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini, myconfiguration4.ini. The myconfigurationХ.ini file is created based on the common.ini file of the terminal, from which our Expert Advisor is launched. The path to the common.ini file:

TERMINAL_DATA_PATH\config\common.ini

The algorithm for creating and editing myconfiguration.ini files looks like this:

2.1. common.ini -> original.ini

This is probably the easiest code: receiving paths to "Data Folder" and "Common Data Folder" to variables, and initializing a variable with the "original.ini" value

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

Copying the "common.ini" configuration file to "original.ini" using the Win API of the CopyFileW function.

2.2. Search for the [Common] section using regular expressions

We use regular expressions in order to find and copy the [Common] section. This is not a normal task, since the common.ini file consists of very short LINES, at the end of which newline characters are always used (invisible characters). There are two ways:

Reading one line at a timeReading the entire file into one variable
  • Reading one file and searching for a match with "[Common]" (no need to type the entire name, you can set a kind of a template "[Com" a_few_characters "]")
    • If found, write the found lines into an array
    • Searching for the next match with "["
      • If found, stop writing to the array, because the array will contain the entire "[Common]" section by that moment
  • Reading all lines into one string variable
  • Searching for the template "[Common]" a few characters "]" (can also be replaced by something like "[Com" a few characters "]")
  • If a match is found, write it into the string variable

Test file "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

The "test_original.ini" file can be used for training the use of regular expressions using the "Receiving lines.mq5" script. Two operation modes can be selected in the script settings:

Several examples comparing the two methods:

Reading one line at a timeReading the entire file into one variable
Query: "Prox(.*)0"
- search for "Prox"
- followed by any symbol except for a newline character or another separator of a Unicode string, found zero or more times (greedy) "(.*)"
- the search must end once the "0" number is found
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, 
As you can see, two results are outputThis one contains one results, which has a lot of unnecessary items (the result of the greedy request)
  
Query: "Prox(.*?)0"
- search for "Prox"
- followed by any symbol except for a newline character or another separator of a Unicode string, found zero or more times (greedy) "(.*)"
- the search must end once the "0" number is found
12: 0: ProxyEnable=0,
13: 0: ProxyType=0,
: 0: ProxyEnable=0, 1: ProxyType=0, 2: ProxyAddress=ProxyAuth=CertInstall=0,
Again we have two results hereIn this case, we have three results, and the third result is not the one we expected.

What method should we use for extracting the entire "[Common]" block — reading one line at a time, or reading into one variable? I have chosen reading one line at a time and the following algorithm:

  1. searching for "[Common]" (MQL5);
  2. once the line is found, writing it into an array;
  3. then we continue to write lines into the array until the regular expression finds the "[" symbol.

An example of this approach is implemented in the "Receiving lines v.2.mq5" script:

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

Script execution results:

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

As you can see, the script has accurately identified the "[Common]" block of parameters from the "test_original.ini" file. I use an almost unchanged algorithm of the "Receiving lines v.2.mq5" script in the SearchBlock() function. If the "[Common]" block is successively found, the SearchBlock() function writes this block into the service array arr_common[].

2.3. Creating four files: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini and myconfiguration4.ini

The four files are created by a sequential call of the following code (note the flags used when opening files):

//+------------------------------------------------------------------+
//| 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. Editing ini files (copying the [Common] section and individual [Tester] sections to it)

Earlier, the "[Common]" block of parameters has been written into the service array arr_common[]. Now this array is written to all the four files:

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

Then the [Tester] block of parameters is formed: unique parameters (symbol and timeframe) and common parameters (testing start and end dates, initial deposit, leverage) are prepared for each terminal. 

The created files myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini, and myconfiguration4.ini are saved in the common data folder (TERMINAL_COMMONDATA_PATH\Files\). Handles of these files should be closed.


3. Parsing and editing the mq5 file of the selected Expert Advisor

Problems to be solved:

3.1. Secrete #3 

Why the secret number three? Secrete #1 and Secrete #2 were published earlier in LifeHack for trader: Four backtests are better than one.

Consider the following scenario: the terminal is started from the command line, and at the same time the configuration ini file is specified. In the ini file we specify the name of the Expert Advisor that will be launched in the tester at terminal start. In this case we keep in mind that we specify the name of the Expert Advisor that has not yet been compiled.

Secrete #3.

The name of the Expert Advisor MUST be written without the extension. That's how it looks like within this article:

NewsEnable=0
[Tester]
Expert=D0E820_test
Symbol=GBPAUD

At start, the terminal searches for a COMPILED FILE first (within this article, the terminal will search for Expert=D0E820_test.ex5). And only in case the terminal cannot find the compiled file, the terminal will start the compilation of the Expert Advisor specified in the ini file.

For this reason, before starting to edit the selected Expert Advisor, we need to go through the folders of slave terminals and remove the compiled versions of the selected EA (in our case we need to delete files 'D0E820_test.ex5'). We will delete the files using the DeleteFileW Win API function:

      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

And now it is necessary to delete *.set files. The reason is that if you edit input parameters of the selected Expert Advisor, the tester will still start with the parameters used during the previous run. So let's delete *.set files:

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

Also, remove the tester report file from the folders of slave terminals:

      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))

Why do we need to delete the report files? By deleting the reports, we will be able to spot the moment when new report files are created in all slave terminals, after which we can parse these files for creating a page with the comparison of testing results on multiple symbols.

Only after we delete the compiled files, we can copy the selected Expert Advisor file to the TERMINAL_COMMONDATA_PATH folder for further work with the files using MQL5 tools:

      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. Including "#include"

Description of Compare multiple tests.mq5::ParsingEA().

In general, we need to determine if the Expert Advisor file already contains "#include <DistributionOfProfits.mqh>". If not, we need to add this line to the EA. However, there can be different variants:

VariantsGood/bad
"#include <DistributionOfProfits.mqh>"a good variant (ideal)
"#include <DistributionOfProfits.mqh>"good (in this variant "#include" is followed by a tab, not a space)
" #include <DistributionOfProfits.mqh>"good (in this variant a tab is used before "#include" instead of a space)
"//#include <DistributionOfProfits.mqh>"a bad variant (it's just a comment)

Also variants are possible, when "#include" is followed not by a space, but by a tab character or multiple spaces. So the following regular expression has been created for search: 

"(\\s+?#include|^#include)(.*?)(<DistributionOfProfits.mqh)"

That's how the expression is interpreted (\\s+?#include|^#include): (one or more spaces, not greedy, then "#include") or (the line starts with "#include"). The search is performed using the NumberRegulars() function. A new variable is introduced: "name_Object_CDistributionOfProfits", in which we will save the name of the CDistributionOfProfits object. This can be useful later if we need to perform a complex search.

//+------------------------------------------------------------------+
//| 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");

If the string is not found, then we need to insert it into our Expert Advisor (the InsertLine() function). The principle of operation is as follows: read the Expert Advisor line by line into a temporary array. When the line number matches the set "position", the appropriate piece of code is inserted into the array (the code piece is taken from the "text" array). As soon as reading is complete, the Expert Advisor file is deleted, and a new file with the same name is created. Information from the temporary array is written to the file:

//+------------------------------------------------------------------+
//| 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. Insert "OnTester()"

Now the task becomes much more difficult, since the "OnTester" word can occur in the program code in a many different variations. For example, the simplest case is when no "OnTester" is used in the code. The classic version is the following:

double OnTester()
  {

It is not very difficult. But developers are different, and sometimes we can meet the following programming style:

double OnTester() {

And perhaps one of the most difficult cases:

/*
//+-------------------------------+
//|                               |
//+-------------------------------+
double OnTester()
  {
...
  }
...
*/

So, in order to find out whether a code contains declaration of the OnTetster function, let's use the following regular expression:

"(\\s+?double|^double)(.+?)(OnTester\\(\\))(.*)"
"(\\s+?double" 
 \\s a space, \\s+ a space occurring at least once, \\s+? a space occurring at least once, non-greedy operator, \\s+?double a space occurring at least once, non-greedy operator, and the "double" word.
"|"
 | or
     "^double)" the line starts with the  double word
"(.+?)"
 . any symbol except a newline character or any other separator of a Unicode string, .+ any symbol except a newline character or any other separator of a Unicode string, occurring one or more times, .+? any symbol except a newline character or any other separator of a Unicode string, occurring one or more times, non-greedy
"(OnTester\\(\\))"
 OnTester\\(\\) the OnTester() word
"(.*)"
 . any symbol except a newline character or any other separator of a Unicode string, .* any symbol except a newline character or any other separator of a Unicode string occurring zero or more times

In the simplest case when regular expressions return zero after search, insert the OnTester() function call:

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

So, if the code had neither "#include <DistributionOfProfits.mqh>" nor "OnTester()", the source code will be the following (for example, if we choose 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"

The code looks not very aesthetically pleasing, nevertheless it performs its task. In paragraphs 3.1 and 3.2, we have discussed simple cases (easiest ones) — when the Expert Advisor code initially contains neither the graphical analysis library declaration nor the OnTester() function. Next, we consider more complicated cases, where the code originally contains the declaration of the graphical analysis library and/or the OnTester() function. 

3.4. The complex case: the code already contains DistributionOfProfits.mqh and/or OnTester()

The complex search is performed in the AdvancedSearch() function:

//+------------------------------------------------------------------+
//| 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)

Parameters:

  • name_file — the name of the Expert Advisor file
  • name_object — the name of the CDistributionOfProfits class object
  • only_ontester — search flag, if only_ontester=true we only search for OnTester().

In the beginning, the entire file is read into a temporary array

string arr_temp[];

— so it will be easier to work with.

Then several service codes are called sequentially:

RemovalMultiLineComments() — in this code, all multi-line comments are removed from the array;

RemovalComments() — single-line comments are deleted here;

DeleteZeroLine() — all zero-length lines are removed from the array.

If only_ontester==false, we search for "#include <DistributionOfProfits.mqh> ", which is done using the FindInclude() function:

FindInclude() searches for the occurrences of "#include <DistributionOfProfits.mqh>" and saves the number of the line into the "function_position" variable (in p. 3.1. Including "#include" we used regular expressions to determine that the code already contains "#include <DistributionOfProfits.mqh>"). Then an attempt is made to find "CDistributionOfProfits". If such a line is found, we obtain the variable name for the "CDistributionOfProfits" class from that line. If the line is not found, we will need to insert it to the position next to "function_position".

If only_ontester==true, then we start searching for OnTester(). Once found, we search for the graphical analysis library call in that line using the FindFunctionOnTester() function.


4. Copying the Expert Advisor to the folders of slave terminals

Expert Advisors are copied in 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. Lunching slave terminals

Before launching slave terminals, please check the following: a trading account in the main terminal, in which our Expert Advisor is started, must be used in all slave terminals. Also, you should allow dll in all slave terminals:

 

If you do not allow DLLs, the slave terminal will not be able to run the EA (remember, our EA actively uses Win API calls), and the following message will be printed in the "Journal" tab of the tester:

2016.10.13 11:28:57     Core 1  2016.02.03 00:00:00   DLL loading is not allowed

More about the system function ShellExecuteW:  ShellExecuteW. A pause is made between terminal launches, and the launch is performed by the "LaunchSlaveTerminal" function. 

      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. A comparative report

We have done so much work parsing the code of the selected Expert Advisor, the purpose of which was to insert into its code the call of a library for a graphical analysis of positions in relation to the position opening time (this library was described in article LifeHack for trader: "Quiet" optimization or Plotting trade distributions"). This inserted code enables every Expert Advisor in the slave terminal to create and automatically open the following html page after testing:

scheme

Earlier, we have added the "Report" parameter into the configuration ini file, in the [Tester] block:

[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

This is the name of the file (D0E820_test.htm), into which the terminal will save the report after test completion. From this report (for each of the slave terminals) we will need to extract the following data: symbol name and period, on which the Expert Advisor was tested, values from the "Backtest" block and the balance graph. The following comparative report is generate based on the results of all slave terminals:

report

The slave terminals save testing reports (in this case in the htm format) in the root folder of their data directories. It means that our Expert Advisor needs to run the slave terminals, and then to periodically look for testing report files in these directories. Once all the four reports are found, we can proceed to generating a common comparative report. 

First we introduce the "find_report" flag allowing the EA to start searching for the report files:

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

also, we add the OnTimer() function:

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

We will search for the "expert_short_name"+".htm" file in OnTimer(). This is a single-level search, only performed in the root folder of data directory of each slave terminal. For this purpose we will use the ListingFilesDirectory.mqh::FindFile() function.

Since the search is performed outside the "sandbox", we will use the Win API function FindFirstFileW. Read more about FindFirstFileW in the previous article: 

In this code, we compare the resulting file name, and if it matches the specified name, true will be returned; and the search handle should be closed before that: 

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

The Expert Advisor checks if all four report files are available in the folders of slave terminals: this is an indication that all terminals have completed testing.

Now we need to handle this information. These four report files and their four graphical files (balance charts) are copied to the sandbox 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;
     }

But the resulting report files contain a lot of unnecessary information, which greatly complicates the use of regular expressions. That is why some manipulations are performed in the 'Compare multiple tests.mq5::ParsingReportToArray' function, after which the files will look something like this:

 

This file is more convenient for using the regular expression "(>)(.*?)(<)", i.e. for searching for any symbols between ">" and "<", the number of such symbols starts with zero.

The results of use of regular expressions are added to four arrays: arr_report_1, arr_report_2, arr_report_3 and arr_report_4. Information from these arrays will be used for generating the code of the final comparative report. After creating the final report, we call the WinAPI function 'ShellExecuteW' (read more about ShellExecuteW here) and launch the browser:

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

This will open a browser page, in which we can compare the results of Expert Advisor testing on four different symbols. 

 

Conclusion

In this article we have discussed one more method of assessing the results of Expert Advisor testing on four different symbols. In this case, parallel testing on four symbols is performed simultaneously in four terminals, after which we receive a summary table with the results of these tests.