
LifeHack for Trader: A comparative report of several tests
Contents
- Introduction
- Necessary Actions
- 1. Input parameters Selecting an Expert Advisor to test
- 2. Again about common.ini
- 2.1. common.ini -> original.ini
- 2.2. Search for the [Common] section using regular expressions
- 2.3. Creating four files: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini and myconfiguration4.ini
- 2.4. Editing ini files (copying the [Common] section and individual [Tester] sections to it)
- 3. Parsing and editing the mq5 file of the selected Expert Advisor
- 3.1 Secrete #3
- 3.2. Including "#include"
- 3.3. Including "double OnTester()"
- 3.4. The complex case: the code already contains DistributionOfProfits.mqh and/or OnTester()
- 4. Copying the Expert Advisor to the folders of slave terminals
- 5. Lunching slave terminals
- 6. A comparative report
- Conclusion
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:
- LifeHack for trader: Four backtests are better than one
- LifeHack for trader: "Quiet" optimization or Plotting trade distributions
- Regular expressions for traders
Testing scenario:
- Choosing an Expert Advisor for testing (Win API)
- Parsing the Expert Advisor code and adding the call of the graphical report library to it (Win API, MQL5, and regular expressions)
- Parsing common.ini of the main terminal and preparing individual common.ini for each terminal (Win API, MQL5, and regular expressions)
- Copying individual common.ini to the folders of the terminals (Win API)
- Copying individual common.ini to the folders of the terminals (Win API)
- Parsing reports of slave terminals
- 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.
- The master and slave terminals should be connected with the same trading account.
- 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".
- 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:
Parameters:
- 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 — a pause between the starts of slave terminals
- date of beginning testing (only year, month and day)
- dates of end testing (only year, month and day)
- initial deposit
- leverage
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:
- TERMINAL_PATH — the folder from which the terminal is running
- TERMINAL_DATA_PATH — the folder in which the terminal data are stored
- TERMINAL_COMMONDATA_PATH — common folder for all client terminals installed on the computer
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:
- Binding the installation folder and the data directory in the AppData folder
- Matching the installation folder and the AppData folder of the Slave terminals
An Expert Advisor is selected using the "Open File" system dialog (the GetOpenFileNameW function):
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:
- Copy common.ini to the folder TERMINAL_COMMONDATA_PATH\Files\original.ini (WinAPI CopyFileW)
- In the original.ini file, find section [Common] (MQL5 + regular expressions).
This is how the section looks like for my main terminal (the terminal is not signed in to mql5.community):
[Common] Login=5116256 ProxyEnable=0 ProxyType=0 ProxyAddress= ProxyAuth= CertInstall=0 NewsEnable=0 NewsLanguages=
- Create four files: myconfiguration1.ini, myconfiguration2.ini, myconfiguration3.ini и myconfiguration4.ini (MQL5)
- Edit these four files (copy [Common] and individual [Tester] sections to it) (MQL5)
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 time | Reading the entire file into one 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:
- Reading one line at a time and searching in each line
- Or reading the entire file into one variable
Several examples comparing the two methods:
Reading one line at a time | Reading 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 output | This 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 here | In 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:
- searching for "[Common]" (MQL5);
- once the line is found, writing it into an array;
- 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:
- Adding the call of the include graphical analytics file (for details see Running analytics charts from the strategy tester);
- Integrating into the Expert Advisor the OnTester() function with the graphical analysis call.
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:
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:
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:
//--- 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:
//--- с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
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:
Variants | Good/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); }
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:
Earlier, we have added the "Report" parameter into the configuration ini file, in the [Tester] block:
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:
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 arr_path[][2];
bool find_report=false;
//+------------------------------------------------------------------+
//| Enumeration command to start the application |
//+------------------------------------------------------------------+
enum EnSWParam
also, we add the OnTimer() function:
{
//--- 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:
- FindFirstFileW, FindNextFileW
- An example of using FindFirstFileW, FindNextFileW
- Inside the terminal directories ).
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:
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:
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.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/2731





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use