LifeHack for trader: four backtests are better than one

Vladimir Karputov | 23 August, 2016

Before the first single test, every trader faces the same question — "Which of the four modes to use?" Each of the provided modes has its advantages and features, so we will do it the easy way — run all four modes at once with a single button! The article shows how to use the Win API and a little magic to see all four testing chart at the same time.

Table of Contents


Introduction

The main purpose of this article is to show how to run a single test (not optimization, but exactly testing!) of an expert from one terminal on four terminals simultaneously (they will be called Slave terminals and referred to as #1, #2, #3 and #4). At the same time the strategy testers in the Slave terminals will be run in different tick generation modes:

Important limitations:

  1. The Master terminal must be started without the /portable key.
  2. At least five MetaTrader 5 terminals must be installed.
  3. Trade account in the Master terminal — let us call it Master account — must be activated at least once on each Slave terminal This is necessary, because the EA in this article does not pass the trade account password to the Slave terminals using the INI files. Instead, it passes only the trade account number, on which the strategy tester is to be launched, and this number always matches the Master account number.
    Such behavior seems logical, as testing the EA in different tick generation modes should be carried out on the same trading account.
  4. Before starting the EA, unload the CPU as much as possible: shut down online games, media player and other resource-intensive programs. Otherwise, one of the CPU cores may be blocked, and testing will not be started on that core.


1. General Principles

Too much water drowned the miller

I always prefer to use the standard features of the software. Regarding the MetaTrader 5 trading terminals, it sounds as follows: "never start terminals with the /portable key, never disable the User Account Control (UAC) in the operating system". On this basis, the described EA will work with the files within the AppData folder.

All the screenshots provided in the article demonstrate the work in Windows 10, as it is the latest and fully licensed system. All the code described in this publication will be considered in application to it.

The considered EA widely uses DLL along with the MQL5 features:


Fig. 1. Dependencies 

In particular, the following Windows API functions are called:


2. Inputs


Fig. 3. Inputs 

The "folder of the MetaTrader#Х installation" paths are the paths to the Slave terminal installation folders. When specifying paths in mq5 code, it is necessary to write double slashes. It is also very important to place double backslash at the end of the path:

//--- input parameters                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // folder of the MetaTrader#1 installation
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // folder of the MetaTrader#2 installation
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // folder of the MetaTrader#3 installation
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // folder of the MetaTrader#4 installation
input string   ExtTerminalName="terminal64.exe";                                       // correct name of the file of the terminal

The terminal name of "terminal64.exe" is given for a 64-bit operating system.

 

Binding the installation folder and the data directory in the AppData folder 

When the terminal is started the conventional way or with the /portable key, the terminal will generate different paths for the TERMINAL_DATA_PATH variable. Let us consider this situation by the example of a Master terminal installed in the "C:\Program Files\MetaTrader 5 1" directory.

If the master terminal is started with the /portable key, MQL will generate the following results from that terminal:

TERMINAL_PATH = C:\Program Files\MetaTrader 5
TERMINAL_DATA_PATH = C:\Program Files\MetaTrader 5
TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common

And here is the response of a terminal without the /portable key:

TERMINAL_PATH = C:\Program Files\MetaTrader 5
TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common

This will be useful only if the parameters are received from the current terminal. And what to do with the Slave terminals that will run the EA testing? How to bind the installation directories of the Slave terminals with their data catalogs?

Here it is necessary to explain why it is so important to know the path to data catalogs in the AppData folder (quote from the help):

Starting from MS Windows Vista, applications installed to Program Files are not allowed to store their data in the installation folder on default All data should be stored in a separate Windows user directory.

In other words, the EA is free to create and modify files in a folder like this: C:\Users\user_name\AppData\Roaming\MetaQuotes\Terminal\terminal_identifier\MQL5\Files. Here, the "terminal_identifier" is the Master terminal identifier.


3. Matching the installation folder and the AppData folder of the Slave terminals

The EA launches the Slave terminals by specifying the configuration file. In addition, individual configuration file is used for each terminal. Each configuration file has an indication to start testing of the given EA right after the terminal is launched. The corresponding commands are located in the [Tester] section of the configuration file:

...
[Tester]
Expert=test
...

As you can see, the path is not specified, which means that the tested EA can be located exclusively in the MQL5 "sandbox". In the example of the Slave terminal 1 those can be two paths:

  1. C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
  2. or C:\Program Files\MetaTrader 5 1\MQL5\Experts

Variant №2 is dismissed, as according to the security policy starting from Windows Vista, writing in the "Program Files" is forbidden. The variant №1 is left — and this means that for all Slave terminals it is necessary to perform a matching of the installation directory and the folder in AppData. 

3.1. Secret №1

Every data catalog contains an "origin.txt" file. In the example of the Slave terminal 1:


 

Fig. 4. "origin.txt" file 

and content of the origin.txt file:

C:\Program Files\MetaTrader 5 1

This record in the file indicates that the "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962" folder had been created by the terminal installed in "C:\Program Files\MetaTrader 5 1".

3.2. FindFirstFileW, FindNextFileW

FindFirstFileW — searches for a directory for the file or subdirectory with the name that matches a certain name (or part of the name, if the special characters are used).

HANDLE  FindFirstFileW(
   string           lpFileName,         //
   WIN32_FIND_DATA  &lpFindFileData     //
   ); 

Parameters

lpFileName

[in]  The directory or path and name of the file, that can include wildcard characters such as asterisk (*) or question mark (?).

lpFindFileData

[in][out]  Pointer to the WIN32_FIND_DATA structure, which receives information about the found file or directory. 

Returned value

If the function succeeds, the returned value will be the search handle used in the subsequent call to the FindNextFile or FindClose, and the lpFindFileData parameter contains the information on the first file or folder found.

If the function fails or is unable to find files from the search string in the lpFileName parameter, it returns the INVALID_HANDLE_VALUE and the content of the lpFindFileData will be undefined. To get additional information about an error, call the GetLastError function.

If the function does not trigger because the corresponding files cannot be found, the GetLastError function returns ERROR_FILE_NOT_FOUND.


FindNextFileWcontinues the search for the file from the previous call to the FindFirstFileFindFirstFileEx, or FindFirstFileTransacted function.

bool  FindNextFileW(
   HANDLE           FindFile,           //
   WIN32_FIND_DATA  &lpFindFileData     //
   );

Parameters

FindFile

[in] Search handle returned by the previous call to the FindFirstFile or FindFirstFileEx function.

lpFindFileData

[in][out]  Pointer to the WIN32_FIND_DATA structure, which receives information about the found file or directory. 

Returned value

If the function succeeds, then the returned value is not zero, and the lpFindFileData parameter will contain the information about the next file or folder found.

If the function completes with an error, the returned value is zero, and the content of the lpFindFileData will be undefined. To get additional information about an error, call the GetLastError function.

If the function fails because it cannot find any more files, the GetLastError function returns ERROR_NO_MORE_FILES.

Example of declaring the FindFirstFileW and FindNextFileW functions of the Win API (code is taken from the included ListingFilesDirectory.mqh file):

#define MAX_PATH                 0x00000104  //
#define FILE_ATTRIBUTE_DIRECTORY 0x00000010  //
#define ERROR_NO_MORE_FILES      0x00000012  //there are no more files
#define ERROR_FILE_NOT_FOUND     0x00000002  //the system cannot find the file specified
//+------------------------------------------------------------------+
//| FILETIME structure                                               |
//+------------------------------------------------------------------+
struct FILETIME
  {
   uint              dwLowDateTime;
   uint              dwHighDateTime;
  };
//+------------------------------------------------------------------+
//| WIN32_FIND_DATA structure                                        |
//+------------------------------------------------------------------+
struct WIN32_FIND_DATA
  {
   uint              dwFileAttributes;
   FILETIME          ftCreationTime;
   FILETIME          ftLastAccessTime;
   FILETIME          ftLastWriteTime;
   uint              nFileSizeHigh;
   uint              nFileSizeLow;
   uint              dwReserved0;
   uint              dwReserved1;
   ushort            cFileName[MAX_PATH];
   ushort            cAlternateFileName[14];
  };

#import "kernel32.dll"
int      GetLastError();
long     FindFirstFileW(string lpFileName,WIN32_FIND_DATA  &lpFindFileData);
int      FindNextFileW(long FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(long hFindFile);
int      FindNextFileW(int FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(int hFindFile);
int      CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists);
#import

bool WinAPI_FindClose(long hFindFile)
  {
   bool res;
   if(_IsX64)
      res=FindClose(hFindFile)!=0;      
   else
      res=FindClose((int)hFindFile)!=0;      
//---
   return(res);
  }
  
bool WinAPI_FindNextFile(long hFindFile,WIN32_FIND_DATA &lpFindFileData)
  {
   bool res;
   if(_IsX64)
      res=FindNextFileW(hFindFile,lpFindFileData)!=0;      
   else
      res=FindNextFileW((int)hFindFile,lpFindFileData)!=0;      
//---
   return(res);
  }

3.3. An example of using FindFirstFileW, FindNextFileW

The "ListingFilesDirectory.mq5" script is at the same time the example and practically the full copy of the working code for the EA. In other words, this code is as close to reality as possible.

Objective: obtain the names of all folders for the path TERMINAL_COMMONDATA_PATH - "Common". 

For example, the path of TERMINAL_COMMONDATA_PATH on the computer returns "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common". So, if the "Common" is trimmed from this path, the required path can be obtained "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\":


Fig. 5. Find First 

Usually, the "*.*" search mask is used to find all files. So, it is necessary to perform two operations with the following strings: trim the "Common" word, and the add the "*.*" mask:

   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
   int pos=StringFind(common_data_path,"Common",0);
   if(pos!=-1)
     {
      common_data_path=StringSubstr(common_data_path,0,pos-1);
     }
   else
      return;

   string path_addition="\\*.*";
   string mask_path=common_data_path+path_addition;
   printf("mask_path=%s",mask_path);

Let us check the resulting path. To do this, set a breakpoint and start debugging


 

Fig. 6. Debugging 

We will have:

mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*

So far everything is correct: prepared a mask to search for ALL files and folders in the "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" directory.

Next: initialize the "hFind" search handle (in my case this is mainly due to habit) and call the FindFirstFileW function of the Win API:

   printf("mask_path=%s",mask_path);
   hFind=-100;
   hFind=FindFirstFileW(mask_path,ffd);
   if(hFind==INVALID_HANDLE)
     {
      PrintFormat("Failed FindFirstFile (hFind) with error: %x",kernel32::GetLastError());
      return;
     }

// List all the files in the directory with some info about them

If a call to the FindFirstFileW fails, the "hFind" search handle will be equal to "INVALID_HANDLE" and the script will be terminated.

In case of a successful call to the FindFirstFileW function, create a do while loop, which obtains the file or folder name, and at the end of the loop the FindNextFileW function of the Win API will be called:

// List all the files in the directory with some info about them
   PrintFormat("hFind=%d",hFind);
   bool rezult=0;
   do
     {
      string name="";
      for(int i=0;i<MAX_PATH;i++)
        {
         name+=ShortToString(ffd.cFileName[i]);
        }
      
      Print("\"",name,"\", File Attribute Constants (dec): ",ffd.dwFileAttributes);
      //---
      ArrayInitialize(ffd.cFileName,0);
      ArrayInitialize(ffd.cAlternateFileName,0);
      ffd.dwFileAttributes=-100;
      ResetLastError();
      rezult=WinAPI_FindNextFile(hFind,ffd);
     }
   while(rezult!=0);
   if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
      PrintFormat("Failed FindNextFileW (hFind) with error: %x",kernel32::GetLastError());
   WinAPI_FindClose(hFind);

The 'do while' loop will continue as long as the FindNextFileW function of the Win API returns a non-zero value. If a call to the FindNextFileW function of the Win API returns zero and the error is not equal to "ERROR_NO_MORE_FILES" — it means that there was a critical error.

The search handle is closed at the end of the script operation. 

Result of the "ListingFilesDirectory.mq5" script operation:

mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*
hFind=-847293552
".", File Attribute Constants (dec): 16
"..", File Attribute Constants (dec): 16
"038C9E8FAFF9EA373522ECC6D5159962", File Attribute Constants (dec): 16
"0C46DDCEB43080B0EC647E0C66170465", File Attribute Constants (dec): 16
"2A6A33B25AA0984C6AB9D7F28665B88E", File Attribute Constants (dec): 16
"50CA3DFB510CC5A8F28B48D1BF2A5702", File Attribute Constants (dec): 16
"BC11041F9347CD71C5F8926F53AA908A", File Attribute Constants (dec): 16
"Common", File Attribute Constants (dec): 16
"Community", File Attribute Constants (dec): 16
"D0E8209F77C8CF37AD8BF550E51FF075", File Attribute Constants (dec): 16
"D3852169A6E781B7F35488A051432620", File Attribute Constants (dec): 16
"EE57F715BA53F2E183D6731C9376293D", File Attribute Constants (dec): 16
"Help", File Attribute Constants (dec): 16

3.4. Inside the terminal directories

The example described above demonstrated the work at the top level — in the "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" directory. But be mindful of the section 3.1. Secret №1, according to which it is necessary to look inside all the subfolders.

To do this, organize a two-level search, and search in subfolders requires using this primary search mask for the FindFirstFileW function of the Win API:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" + name of found top-level folder + "origin.txt".

Thus, the primary search of the FindFirstFileW will only look for a single file in the subfolder — "origin.txt".

Here is the full listing of the FindDataPath() function:

//+------------------------------------------------------------------+
//| Find and read the origin.txt                                     |
//+------------------------------------------------------------------+
void FindDataPath(string &array[][2])
  {
//---
   WIN32_FIND_DATA ffd;
   long            hFirstFind_0,hFirstFind_1;

   ArrayInitialize(ffd.cFileName,0);
   ArrayInitialize(ffd.cAlternateFileName,0);
//+------------------------------------------------------------------+
//| Get common path for all of the terminals installed on a computer.|
//| The common path on my computer:                                  |
//| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common          |
//+------------------------------------------------------------------+
   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
   int pos=StringFind(common_data_path,"Common",0);
   if(pos!=-1)
     {
      //+------------------------------------------------------------------+
      //| Cuts "Common" ... and we get:                                    |
      //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal                 |
      //+------------------------------------------------------------------+
      common_data_path=StringSubstr(common_data_path,0,pos-1);
     }
   else
      return;

//--- stage Search №0. 
   string filter_0=common_data_path+"\\*.*"; // filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*

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

//--- list all the files in the directory with some info about them
   bool rezult=0;
   do
     {
      if((ffd.dwFileAttributes  &FILE_ATTRIBUTE_DIRECTORY)==FILE_ATTRIBUTE_DIRECTORY)
        {
         string name_0="";
         for(int i=0;i<MAX_PATH;i++)
           {
            name_0+=ShortToString(ffd.cFileName[i]);
           }
         if(name_0!="." && name_0!="..")
           {
            ArrayInitialize(ffd.cFileName,0);
            ArrayInitialize(ffd.cAlternateFileName,0);
            //--- stage Search №1. search origin.txt file in the folder
            string filter_1=common_data_path+"\\"+name_0+"\\origin.txt";
            ResetLastError();
            hFirstFind_1=FindFirstFileW(filter_1,ffd);
            //---
            if(hFirstFind_1==INVALID_HANDLE)
               str_handle="INVALID_HANDLE";
            else
               str_handle=IntegerToString(hFirstFind_1);
            Print("   filter_1: \"",filter_1,"\", handle hFirstFind_1: ",str_handle);
            //---
            if(hFirstFind_1==INVALID_HANDLE)
              {
               if(kernel32::GetLastError()!=ERROR_FILE_NOT_FOUND)
                 {
                  PrintFormat("Failed FindFirstFile (hFirstFind_1) with error: %x",kernel32::GetLastError());
                  break;
                 }
               WinAPI_FindClose(hFirstFind_1);
               ArrayInitialize(ffd.cFileName,0);
               ArrayInitialize(ffd.cAlternateFileName,0);
               ResetLastError();
               rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
               continue;
              }
            //--- origin.txt file in this folder is found
            bool rezultTwo=0;
            string name_1="";
            for(int i=0;i<MAX_PATH;i++)
              {
               name_1+=ShortToString(ffd.cFileName[i]);
              }
            string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt
            if(origin!=NULL)
              {
               //--- write a string into an array
               int size=ArrayRange(array,0);
               ArrayResize(array,size+1,0);
               array[size][0]=common_data_path+"\\"+name_0;
               //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
               array[size][1]=origin;
               //value array[][1]==C:\Program Files\MetaTrader 5 1\
              }
            WinAPI_FindClose(hFirstFind_1);
           }
        }
      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);
  }

The FindDataPath() function prints nearly the following information:

filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt", handle hFirstFind_1: 1901014213744
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\0C46DDCEB43080B0EC647E0C66170465\origin.txt", handle hFirstFind_1: 1901014213840
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\2A6A33B25AA0984C6AB9D7F28665B88E\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\50CA3DFB510CC5A8F28B48D1BF2A5702\origin.txt", handle hFirstFind_1: 1901014218448
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\BC11041F9347CD71C5F8926F53AA908A\origin.txt", handle hFirstFind_1: 1901014213936
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Community\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\origin.txt", handle hFirstFind_1: 1901014216720
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D3852169A6E781B7F35488A051432620\origin.txt", handle hFirstFind_1: 1901014217104
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\EE57F715BA53F2E183D6731C9376293D\origin.txt", handle hFirstFind_1: 1901014218640
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Help\origin.txt", handle hFirstFind_1: INVALID_HANDLE
filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592, NO_MORE_FILES 

Explanation of the first print lines: first it creates the "filter_0" filter of the primary search (filter is "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*") and obtains the "hFirstFind_0" handle of the primary search, which equals 1901014212592. As the value of "hFirstFind_0" is not "INVALID_HANDLE" — then the "filter_0" filter of the primary search passed to the FindFirstFileW(filter_0,ffd) function of the Win API is correct. After a successful call to the FindFirstFileW(filter_0,ffd), the name of first folder is received: it is the "038C9E8FAFF9EA373522ECC6D5159962" folder. 

Next, it is necessary to search for the "origin.txt" file within the 038C9E8FAFF9EA373522ECC6D5159962 folder. To do this, form the filter mask. For example, for the 038C9E8FAFF9EA373522ECC6D5159962 the mask will look as follows: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". If the "hFirstFind_1" handle is not equal to "INVALID_HANDLE" — then the specified folder(038C9E8FAFF9EA373522ECC6D5159962) contains the specified file (origin.txt). 

The printing clearly shows that the primary search in the subfolders sometimes returns "INVALID_HANDLE". This means that the specified folders have no "origin.txt" file. 

Let us dwell on what should be done when the "origin.txt" is found in a subfolder.

3.5. CopyFileW

CopyFileW — copies the existing file to a new file.

bool  CopyFileW(
   string lpExistingFileName,     //
   string lpNewFileName,          //
   bool bFailIfExists             //
   );

Parameters

lpExistingFileName

[in] Name of the existing file.

Here, a limitation on the name length — MAX_PATH characters is enforced, this is always sufficient for the example.

If a file with the name lpExistingFileName does not exist, the function fails and the GetLastError returns ERROR_FILE_NOT_FOUND.

lpNewFileName

[in]  Name of the new file. 

Here, a limitation on the name length — MAX_PATH characters is enforced, this is always sufficient for the example.

bFailIfExists
[in] 
If this parameter is TRUE and the new file specified in lpNewFileName exists, the function fails. If this parameter is FALSE and the new file exists, the function overwrites the existing file and successfully completes.

Returned value

IF the function succeeds, the returned value is not equal to zero.

If the function completes with an error, the returned value is zero. To get additional information about an error, call the GetLastError function.

Example of declaring the CopyFileW function of the Win API (code is taken from the included ListingFilesDirectory.mqh file):

#import "kernel32.dll"
int      GetLastError();
bool     CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists);
#import

3.6. Working with the "origin.txt" file

Description of working with the ListingFilesDirectory.mqh::CopiedAndReadFile(string full_file_name) function.

The full name of the "origin.txt" file found in one of the subfolders is passed to the function as input. The path may look like: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". Open the "origin.txt" file and read its content by means of MQL5, this implies that the file must be located in the "sandbox". Therefore, the "origin.txt" must be copied from the subfolder to the sandbox (in this case, to the "sandbox" in the common files of all terminals). Such copying is performed by calling the CopyFileW function of the Win API.

Write the path to the "origin.txt" file in the sandbox into the "new_path" variable:

//+------------------------------------------------------------------+
//| Copying to the Common Data Folder                                |
//| for all client terminals ***\Terminal\Common\Files               |
//+------------------------------------------------------------------+
string CopiedAndReadFile(string full_file_name)
  {
   string new_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files\\origin.txt";
// => new_path==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\Files\origin.txt
//--- Win API

and call the CopyFileW function of the Win API with the third parameter set to false — allow overwriting the "origin.txt" file in the sandbox:

//--- Win API
   if(!CopyFileW(full_file_name,new_path,false))
     {
      Print("Error CopyFile ",full_file_name," to ",new_path);
      return(NULL);
     }
//--- open the file using MQL5

Open the "origin.txt" file for reading, and do not forget to set the FILE_COMMON flag, because the file is in the common files folder:

//--- open the file using MQL5
   string str;
   ResetLastError();
   int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- read a string using the MQL5 
      str=FileReadString(file_handle,-1)+"\\";
      //--- close the file using the MQL5
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("File %s open failed , MQL5 error=%d","origin.txt",GetLastError());
      return(NULL);
     }
   return(str);
  }

Read only once — one string, append "\\" to its end and return the obtained result.

3.7. Finishing Stroke

The paths to installation directories for the four terminals are set in the input parameters:

//--- input parameters                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // folder of the MetaTrader#1 installation
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // folder of the MetaTrader#2 installation
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // folder of the MetaTrader#3 installation
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // folder of the MetaTrader#4 installation

These paths are hardcoded and they have to indicate the installation directories correctly.

Also, four more string variables and one array are declared on the global scope:

string         slaveTerminalDataPath1=NULL;                                // the path to the Data Folder of the terminal #1
string         slaveTerminalDataPath2=NULL;                                // the path to the Data Folder of the terminal #2
string         slaveTerminalDataPath3=NULL;                                // the path to the Data Folder of the terminal #3
string         slaveTerminalDataPath4=NULL;                                // the path to the Data Folder of the terminal #4
//---
string         arr_path[][2];

The paths to the terminals folders in AppData need to be stored in those variables, and the two-dimensional array can help with that. Now it is possible to draw a general outline of how to match the installation directories of the Slave terminals with their folders in AppData:

GetStatsFromAccounts_EA.mq5::OnInit() >call> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path) 
>call> ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) >call> CopiedAndReadFile(string full_file_name) 

            string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt
            if(origin!=NULL)
              {
               //--- write a string into an array
               int size=ArrayRange(array,0);
               ArrayResize(array,size+1,0);
               array[size][0]=common_data_path+"\\"+name_0;
               //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
               array[size][1]=origin;
               //value array[][1]==C:\Program Files\MetaTrader 5 1\
              }
            FindClose(hFirstFind_1);

The ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) function calls the CopiedAndReadFile(string full_file_name) function when the "origin.txt" file is found in subfolders of the terminals, and after the call an entry is made to the two-dimensional array. The "0" dimension of the array contains the path to terminal in AppData and the "1" dimension stores the installation path (as a reminder, this path is obtained from the found "origin.txt" file).

>return control to> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path): 

here, the slaveTerminalDataPath1, slaveTerminalDataPath2, slaveTerminalDataPath3 and slaveTerminalDataPath4 variables are filled by simply iterating over the two-dimensional array:

   FindDataPath(array);
   for(int i=0;i<ArrayRange(array,0);i++)
     {
      //Print("array[",i,"][0]: ",array[i][0]);
      //Print("array[",i,"][1]: ",array[i][1]);
      if(StringCompare(ExtInstallationPathTerminal_1,array[i][1],true)==0)
         slaveTerminalDataPath1=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_2,array[i][1],true)==0)
         slaveTerminalDataPath2=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_3,array[i][1],true)==0)
         slaveTerminalDataPath3=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_4,array[i][1],true)==0)
         slaveTerminalDataPath4=array[i][0];
     }
   if(slaveTerminalDataPath1==NULL || slaveTerminalDataPath2==NULL ||
      slaveTerminalDataPath3==NULL || slaveTerminalDataPath4==NULL)
     {
      Print("slaveTerminalDataPath1 ",slaveTerminalDataPath1,", slaveTerminalDataPath2 ",slaveTerminalDataPath2);
      Print("slaveTerminalDataPath3 ",slaveTerminalDataPath3,", slaveTerminalDataPath4 ",slaveTerminalDataPath4);
      return(false);
     }

If this stage is reached, then the EA has matched the installation directories and their paths in the AppData folder. In case at least one of the terminal paths in the AppData is not found (i.e. equal to NULL), then all paths are printed in the last lines and the EA terminates with an error.


4. Selecting an EA for testing

The file of the tested EA should be selected before launching the four Slave terminals. This expert must be precompiled and placed too the data catalog of the Master terminal.

4.1. GetOpenFileName

GetOpenFileName — creates the "open" dialog, which allows the user to specify the drive, folder and name of the file (or a set of files) to be opened. Declaration and implementation of the "Open" dialog is fully present in the included GetOpenFileNameW.mqh file.

4.2. Selecting an EA with the "Open file" system dialog

The "Open" system dialog is called from within the OnInit() of the EA:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals

where the GetOpenFileNameW.mqh::OpenFileName(void) is called

//+------------------------------------------------------------------+
//| Creates an Open dialog box                                       |
//+------------------------------------------------------------------+
string OpenFileName(void)
  {
   string path=NULL;
   string filter=NULL;
   if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
      filter="Компилированный код";
   else
      filter="Compiled code";
   if(GetOpenFileName(path,filter+"\0*.ex5\0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","Select source file"))
      return(path);
   else
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(NULL);
     }
  }

If a call to the GetOpenFileName function of the Win API succeeds, the "path" variable will contain the full name of the selected file, such as: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5".

The "filter" is responsible for text ① in fig. 2. The "\0*.ex5\0" string is responsible for the filter by file types (② in fig. 2). The "TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"" string specifies the path to the folder that will be opened in the "Open" system dialog.

4.3. Configuration INI file

To launch the terminal for testing the EA from the command line (or using the Win API), it is necessary to have a configuration INI file which must contain the following [Tester] section and the required instructions:

[Tester]
Expert=test             //the file name of the Expert Advisor that will automatically run in the testing mode.
Symbol=EURUSD           //the name of the symbol that will be used as the main testing symbol
Period=H1               //the period of the testing chart
Deposit=10000           //amount of the initial deposit for testing
Model=4                 //tick generation mode
Optimization=0          //enable/disable optimization and set its type
FromDate=2016.01.22     //testing start date
ToDate=2016.06.06       //testing end date
Report=TesterReport     //the name of the file to save the report on testing
ReplaceReport=1         //enable/disable overwriting of the report file 
UseLocal=1              //enable/disable the used of local agents for testing
Port=3000               //port of the testing agent
Visual=0                //enable/disable testing in the visual mode
ShutdownTerminal=0      //enable/disable platform shutdown after completion of testing 

Looking ahead, I will say that the [Tester] section will be added to the file manually.

It was decided to take the INI file of the Master terminal as the basis. This file (common.ini) is located in the terminal data catalog, in the "config" folder. In the example, the file path looks as follows: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini".

The procedure for working with the INI file is:

  1. Get the full path to the "common.ini" of the Master terminal. The full path is a string of the form:
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini". (MQL5)
  2. Get the new path to the INI file in the "\Files" sandbox. The new path is a string of the form:
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini" for the Master terminal. (MQL5)
  3. Copy the "common.ini" file to "myconfiguration.ini". (CopyFileW function of the Win API).
  4. Edit the "myconfiguration.ini" file. (MQL5).
  5. Get the new path to the INI file in the sandbox of the Slave terminal. It is a string of the form (in the example of the Slave terminal №1)
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini". (MQL5)
  6. Copy the edited "myconfiguration.ini" INI file from the sandbox of the Master terminal to the sandbox of the Slave terminal. (CopyFileW function of the Win API).
  7. Delete the "myconfiguration.ini" file from the sandbox of the Master terminal. (MQL5)

This procedure must be repeated for each Slave terminal. Although there is room for optimization, the description of the process was not intended in this article. 

Editing the configuration INI files starts after the Expert Advisor for testing has been selected, GetStatsFromAccounts_EA.mq5::OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- copying an expert in the terminal folders

The procedure for working with the INI file, in the example of the Slave terminal №1, GetStatsFromAccounts_EA.mq5::CopyCommonIni():

//+------------------------------------------------------------------+
//| Copying common.ini - file in a shared folder of client           |
//| terminals. Edit the ini-file and copy obtained                   |
//| ini-files into folders                                           |
//| ...\AppData\Roaming\MetaQuotes\Terminal\"id terminal"\MQL5\Files |
//+------------------------------------------------------------------+
bool CopyCommonIni()
  {
//0 — "Every tick", "1 — 1 minute OHLC", 2 — "Open price only"
//3 — "Math calculations", 4 — "Every tick based on real ticks" 
//--- path to Data Folder
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
//--- path to Commomm Data Folder
   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
//---
   string existing_file_name=terminal_data_path+"\\config\\common.ini"; // full path to the ini-file                                                        
   string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name;
   string test=NULL;
//--- terminal #1
   if(!CopyFileW(existing_file_name,temp_name_ini,false))
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(false);
     }
   EditCommonIniFile(common_file_name,3000,4);
   test=slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name;
   if(!CopyFileW(temp_name_ini,test,false))
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(false);
     }
   ResetLastError();
   if(!FileDelete(common_file_name,0))
      Print("#1 file ",common_file_name," not deleted, an error ",GetLastError());
//--- terminal #2 

The call to the EditCommonIniFile(common_file_name,3000,4) function is passed the following:

common_file_name — name if the INI file to be edited;

3000 — port number of the testing agent. Each terminal must be launched on its own testing agent. Agent numbering starts from 3000. To see the port number of testing agents: in the MetaTrader 5 terminal go to the strategy tester and right click in the "Journal" tab of the strategy tester. The numbering of the testing agent ports can be seen in the drop-down menu:


 

Fig. 7. Testing agents 

4 - type of testing: 

Editing the commom.ini configuration file is carried out in the function GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model) — operations of file opening, reading from file and writing to file are executed by means of MQL5:

//+------------------------------------------------------------------+
//| Editing common.ini file                                          |
//+------------------------------------------------------------------+
bool EditCommonIniFile(string name,const int port,const int model)
  {
   bool tester=false;      // if false - means the section [Tester] not found
   int  count_tester=0;    // counter discoveries section [Tester]
//--- open file 
   ResetLastError();
   int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- auxiliary variable
      string str;
      //--- read data
      while(!FileIsEnding(file_handle))
        {
         //--- read line 
         str=FileReadString(file_handle,-1);
         //--- find [Tester]
         if(StringFind(str,"[Tester]",0)!=-1)
           {
            tester=true;
            count_tester++;
           }
        }
      if(!tester)
        {
         FileWriteString(file_handle,"[Tester]\n",-1);
         FileWriteString(file_handle,"Expert=test\n",-1);
         FileWriteString(file_handle,"Symbol=EURUSD\n",-1);
         FileWriteString(file_handle,"Period=H1\n",-1);
         FileWriteString(file_handle,"Deposit=10000\n",-1);
         //0 — "Every tick", "1 — 1 minute OHLC", 2 — "Open price only"
         //3 — "Math calculations", 4 — "Every tick based on real ticks" 
         FileWriteString(file_handle,"Model="+IntegerToString(model)+"\n",-1);
         FileWriteString(file_handle,"Optimization=0\n",-1);
         FileWriteString(file_handle,"FromDate=2016.01.22\n",-1);
         FileWriteString(file_handle,"ToDate=2016.06.06\n",-1);
         FileWriteString(file_handle,"Report=TesterReport\n",-1);
         FileWriteString(file_handle,"ReplaceReport=1\n",-1);
         FileWriteString(file_handle,"UseLocal=1\n",-1);
         FileWriteString(file_handle,"Port="+IntegerToString(port)+"\n",-1);
         FileWriteString(file_handle,"Visual=0\n",-1);
         FileWriteString(file_handle,"ShutdownTerminal=0\n",-1);
        }
      //--- close file
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
      return(false);
     }
   return(true);
  }

4.4. Secret №2

Before shutting down, the MetaTrader 5 terminal stores the location of windows and panels, as well as their sizes in the "terminal.ini" file. The file itself is stored in the terminal data catalog, in the "config" subfolder. For example, the full path to the "terminal.ini" of the Slave terminal №1 is the following:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\config\terminal.ini".

In the "terminal.ini" file itself, only the "[Window]" block is of interest. Restore the window of the MetaTrader 5 terminal. The terminal will have approximately these sizes:


Fig. 8. Restored terminal window

If the terminal is closed, the [Window] block in the terminal.ini file will have the following form:

Arrange=1
[Window]
Fullscreen=0
Type=1
Left=412
Top=65
Right=1212
Bottom=665
LSave=412

That is, the [Window] block stores coordinates and status of the terminal. 


4.5. Setting the terminal size (width, height). Inserting lines into the middle of the file 

Changing the coordinates in the terminal.ini files of the Slave terminals is required to arrange all four Slave terminals the following way at startup:

 

Fig. 9. Arrangement of terminals

As mentioned above, the "terminal.ini" file needs to be edited for each Slave terminal. Please note that the lines need to be inserted not in the end, but in the middle of the "terminal.ini" file. Below are the features of this procedure.

Here is an example: there is a "test.txt" file located in the terminal "sandbox". Contents of the "test.txt" file:

s=0
df=12
asf=3
g=3
n=0
param_f=123

It is necessary to modify information in the second and third lines to receive the following:

s=0
df=1256
asf=5
g=3
n=0
param_f=123

At first glance, this should be done:

Let us consider this in the code example of the "InsertRowsMistakenly.mq5" script:

//+------------------------------------------------------------------+
//|                                         InsertRowsMistakenly.mq5 |
//|                              Copyright © 2016, Vladimir Karputov |
//|                                           http://wmua.ru/slesar/ |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2016, Vladimir Karputov"
#property link      "http://wmua.ru/slesar/"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- open file
   ResetLastError();
   string name="test.txt";
   int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT);
   if(file_handle!=INVALID_HANDLE)
     {
      FileReadString(file_handle,-1);
      FileWriteString(file_handle,"df=1256"+"\r\n",-1);
      FileWriteString(file_handle,"asf=5"+"\r\n",-1);
      //--- close file
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
      return;
     }
  }
//+------------------------------------------------------------------+

Receive an unexpected result — in the fourth line the "g=" characters are missing:

Before After 
s=0
df=12
asf=3
g=3
n=0
param_f=123
s=0
df=1256
asf=5
3
n=0
param_f=123

Why did it happen? Imagine that the file consists of a set of cells that are following each other. Each cell contains one character. So, when something is written into the file staring from its middle, the cells are in fact overwritten. If more characters are added than there were originally (as in the example above: originally there was "df=12", and then two more characters were written - "df=1256"), then the additional characters simply corrupt the further code. That's how it looks like:

write string

Fig. 10. Information corruption.

In order to prevent corrupting the information when inserting lines into the middle of the file, proceed in the following manner.

The order of the function calls:

GetStatsFromAccounts_EA.mq5::OnInit() >call> GetStatsFromAccounts_EA.mq5::CopyTerminalIni()

//+------------------------------------------------------------------+
//| Editing Files "terminal.ini"                                     |
//+------------------------------------------------------------------+
bool CopyTerminalIni()
  {
//--- path to the terminal data folder 
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
//---
   string existing_file_name=NULL;
   string ext_ini=terminal_data_path+"\\MQL5\\Files\\terminal_ext.ini";
   string ini=terminal_data_path+"\\MQL5\\Files\\terminal.ini";
   int left=0;
   int top=0;
   int right=0;
   int bottom=0;
//---
   for(int i=1;i<5;i++)
     {
      switch(i)
        {
         case 1:
            existing_file_name=slaveTerminalDataPath1+"\\config\\terminal.ini";
            left=0; top=0; right=682; bottom=420;
            break;
         case 2:
            existing_file_name=slaveTerminalDataPath2+"\\config\\terminal.ini";
            left=682; top=0; right=1366; bottom=420;
            break;
         case 3:
            existing_file_name=slaveTerminalDataPath3+"\\config\\terminal.ini";
            left=0; top=738-413; right=682; bottom=738;
            break;
         case 4:
            existing_file_name=slaveTerminalDataPath4+"\\config\\terminal.ini";
            left=682; top=738-413; right=1366; bottom=738;
            break;
        }
      //---
      if(!CopyFileW(existing_file_name,ext_ini,false))
        {
         PrintFormat("Failed with error: %x",kernel32::GetLastError());
         return(false);
        }
      if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom))
         return(false);
      if(!CopyFileW(ini,existing_file_name,false))
        {
         PrintFormat("Failed with error: %x",kernel32::GetLastError());
         return(false);
        }
      ResetLastError();
      if(!FileDelete("terminal.ini",0))
         Print("#",i," file terminal.ini not deleted, an error ",GetLastError());
      ResetLastError();
      if(!FileDelete("terminal_ext.ini",0))
         Print("#",i," file terminal_ext.ini not deleted, an error ",GetLastError());
     }
//---
   return(true);
  }

 >call> GetStatsFromAccounts_EA.mq5::EditTerminalIniFile

//+------------------------------------------------------------------+
//| Editing terminal.ini file                                        |
//+------------------------------------------------------------------+
bool EditTerminalIniFile(string ext_name,const int Left=0,const int Top=0,const int Right=1366,const int Bottom=738)
  {
//--- creates and opens files
   string name="terminal.ini";
   ResetLastError();
   int terminal_ini_handle=FileOpen(name,FILE_WRITE|FILE_TXT);
   int terminal_ext_ini__handle=FileOpen(ext_name,FILE_READ|FILE_TXT);
   if(terminal_ini_handle==INVALID_HANDLE)
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
     }
   if(terminal_ext_ini__handle==INVALID_HANDLE)
     {
      PrintFormat("Unable to open file %s, error = %d",ext_name,GetLastError());
     }
   if(terminal_ini_handle==INVALID_HANDLE && terminal_ext_ini__handle==INVALID_HANDLE)
     {
      FileClose(terminal_ext_ini__handle);
      FileClose(terminal_ini_handle);
      return(false);
     }

//--- auxiliary variable
   string str=NULL;
//--- read data
   while(!FileIsEnding(terminal_ext_ini__handle))
     {
      //--- read line
      str=FileReadString(terminal_ext_ini__handle,-1);
      FileWriteString(terminal_ini_handle,str+"\r\n",-1);
      //--- find [Window]
      if(StringFind(str,"[Window]",0)!=-1)
        {
         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Fullscreen=0\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Type=1\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Left="+IntegerToString(Left)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Top="+IntegerToString(Top)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Right="+IntegerToString(Right)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Bottom="+IntegerToString(Bottom)+"\r\n",-1);
        }
     }
//--- close files
   FileClose(terminal_ext_ini__handle);
   FileClose(terminal_ini_handle);
   return(true);
  }

Thus, the "terminal.ini" files are edited in the Slave terminals, which allows to launch them as in Fig. 9. It is possible to observe the test charts to compare the accuracy of the testing in different modes. 


5. Launching tests on the Slave terminals

Everything is now ready to launch the Slave terminals in the EA testing mode:

Only two tasks remain: copy the selected EA to sandboxes of the Slave terminals and run those terminals.

5.1. Copying the EA to folders of the Slave terminals

Copying the previously selected expert (its name is stored in the "expert_name" variable) occurs in the OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- copying an expert in the terminal folders
      ResetLastError();
      if(!CopyFileW(expert_name,slaveTerminalDataPath1+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #1 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath2+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #2 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #3 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #4 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }
      //---
      Sleep(sleeping);

5.2. ShellExecuteW

ShellExecuteW — executes operation on the specified file.

//--- x64
long ShellExecuteW(
   long hwnd,               //
   string lpOperation,      //
   string lpFile,           //
   string lpParameters,     //
   string lpDirectory,      //
   int nShowCmd             //
   );
//--- x32
int ShellExecuteW(
   int hwnd,                //
   string lpOperation,      //
   string lpFile,           //
   string lpParameters,     //
   string lpDirectory,      //
   int nShowCmd             //
   );

Parameters

hwnd

[in] Handle of the parent window used for displaying the user interface and error messages. This value must be NULL, if the operation is not related to windows.

lpOperation

[in] String with the name of command that determines the action to be executed. The set of the available commands depends on a specific file or directory. As a rule, those are the actions available from the context menu of the object. The following commands are commonly used:

"edit"

Starts the editor and opens the document for editing. If lpFile is not a document file, the function will not be executed.

"explore"

Opens the file specified in the lpFile.

"find"

Initiates search starting in the directory specified in the lpDirectory.

"open"

Opens the element defined by the lpFile parameter. This element may either a file or folder.

"print"

Prints the file specified by the lpFile. I the lpFile is not a document file, the function terminates with an error.

"NULL"

The default command name is used, if any. If none, the "open" command will be used. If neither of the commands is used, the system uses the first command specified in the registry.

lpFile 

[in] String that sets the file or object on which to execute the command. The full name (including not only the file name, but also the to it) is passed. Note that the object may not support all commands. For example, not all documents support the "print" command. If a relative path is used for the lpDirectory parameter, do not use a relative path for the lpFile.

lpParameters

[in] If the lpFile points to an executable file, this parameter is a string that defines the parameters to be passed to the application. Format of this string is determined by the name of the command to be executed. If the lpFile points to a document file, the lpParameters must be NULL.

lpDirectory

[in] String that defines the working directory. If this value is NULL, the current working directory is used. If a relative path is specified in the lpFile, then do not use a relative path for the lpDirectory.

nShowCmd

[in] Flags that determine how the application should be displayed when opened. If the lpFile specified a document file, the flag is simply passed to the corresponding application. Used flags:

//+------------------------------------------------------------------+
//| Enumeration command to start the application                     |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| Displays the window as a minimized window. This value is similar |
   //| to SW_SHOWMINIMIZED, except the window is not activated.         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| Activates and displays a window. If the window is minimized or   |
   //| maximized, the system restores it to its original size and       |
   //| position. An application should specify this flag when           |
   //| displaying the window for the first time.                        |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a minimized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a maximized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| Hides the window and activates another window.                   |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it in its current size         |
   //| and position.                                                    |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

Returned value

If the function succeeds, it returns a value greater than 32.

Example of calling the ShellExecuteW function of the Win API:

#import  "shell32.dll"
int  GetLastError();
//+------------------------------------------------------------------+
//| ShellExecute function                                            |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
//| Performs an operation on a specified file                        |
//+------------------------------------------------------------------+
//--- x64
long ShellExecuteW(long hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd);
//--- x32
int ShellExecuteW(int hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd);
#import
#import "kernel32.dll"

//+------------------------------------------------------------------+
//| Enumeration command to start the application                     |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| Displays the window as a minimized window. This value is similar |
   //| to SW_SHOWMINIMIZED, except the window is not activated.         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| Activates and displays a window. If the window is minimized or   |
   //| maximized, the system restores it to its original size and       |
   //| position. An application should specify this flag when           |
   //| displaying the window for the first time.                        |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a minimized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a maximized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| Hides the window and activates another window.                   |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it in its current size         |
   //| and position.                                                    |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

5.3. Launching the terminals

The slave terminals are launched from the OnInit():

      //---
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_1,slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_2,slaveTerminalDataPath2+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_3,slaveTerminalDataPath3+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_4,slaveTerminalDataPath4+"\\MQL5\\Files\\"+common_file_name);
     }
//---
   return(INIT_SUCCEEDED);
  }

at the same time, the EA waits for "sleeping" milliseconds between each launch. By default, the "sleeping" parameter is equal to 9000 (i.e., 9 seconds). If agent authorization errors occur in the Slave terminals, increase this parameter. 

Parameters passed to the Win API function (in the example of the Slave terminal №1) are as follows:

LaunchSlaveTerminal("C:\Program Files\MetaTrader 5 1\",
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini");


6. Possible errors

A situation may arise when one of the Slave terminal starts, but the tester cannot connect to the testing agent.

The tester will have a record in the "Journal" tab similar to this:

2016.07.15 15:10:48.327 Tester  EURUSD: history data begins from 2014.01.14 00:00
2016.07.15 15:10:49.212 Core 1  agent process started
2016.07.15 15:10:49.717 Core 1  connecting to 127.0.0.1:3002
2016.07.15 15:11:00.771 Core 1  tester agent authorization error
2016.07.15 15:11:01.417 Core 1  connection closed

The agent logs contain these records:

2016.07.15 16:08:45.416 Startup MetaTester 5 x64 build 1368 (13 Jul 2016)
2016.07.15 16:08:45.612 Server  MetaTester 5 started on 127.0.0.1:3000
2016.07.15 16:08:45.612 Startup initialization finished
2016.07.15 16:09:36.811 Server  MetaTester 5 stopped
2016.07.15 16:09:38.422 Tester  shutdown tester machine

In such cases, it is recommended to increase the delay between the terminal launches (the "sleeping" variable), and also unload all resource-intensive applications that can block the use of the CPU core.


Conclusion

The task of starting the testing of a selected EA in four testing modes simultaneously has been completed. After starting the EA, it is possible to observe the testing in all four terminals almost at the same time.

The article also showed how to call the Win API functions, such as: