
LifeHack for trader: four backtests are better than one
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
- 1. General Principles
- 2. Inputs
- 3. Matching the installation folder and the AppData folder of the Slave terminals
- 3.1. Secret №1
- 3.2. FindFirstFileW, FindNextFileW
- 3.3. An example of using FindFirstFileW, FindNextFileW
- 3.4. Inside the terminal directories
- 3.5. CopyFileW
- 3.6. Working with the "origin.txt" file
- 3.7. Finishing Stroke
- 4. Selecting an EA for testing
- 4.1. GetOpenFileName
- 4.2. Selecting an EA with the "Open file" system dialog
- 4.3. Configuration INI file
- 4.4. Secret №2
- 4.5. Setting the terminal size (width, height). Inserting lines into the middle of the file
- 5. Launching tests on the Slave terminals
- 5.1. Copying the EA to folders of the Slave terminals
- 5.2. ShellExecuteW
- 5.3. Launching the terminals
- 6. Possible errors
- Conclusion
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:
- terminal #1 — "Every tick based on real ticks";
- terminal #2 — "Every tick";
- terminal #3 — "1 Minute OHLC";
- terminal #4 — "Opening prices only".
Important limitations:
- The Master terminal must be started without the /portable key.
- At least five MetaTrader 5 terminals must be installed.
- 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. - 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:
- CopyFileW — copies files into the "sandbox" and from the "sandbox" of the MQL5.
- FindClose — closes the search handles.
- FindFirstFileW — searches for the file directory or subdirectory with the name that matches the specified file name.
- FindNextFileW — continues the search for the file from the previous call to the FindFirstFile function.
- GetOpenFileNameW — calls the system dialog for opening a file:
- ShellExecuteW — used for launching the Slave terminals.
Fig. 2. Opening file
More details about the ① and ② icons will be provided in the chapter 4.2. Selecting an EA with the "Open file" system dialog.
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:
- C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
- 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.
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.
FindNextFileW — continues the search for the file from the previous call to the FindFirstFile, FindFirstFileEx, 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.
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
bFailIfExists[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.
[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.
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.
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.
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:
- 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) - 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) - Copy the "common.ini" file to "myconfiguration.ini". (CopyFileW function of the Win API).
- Edit the "myconfiguration.ini" file. (MQL5).
- 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) - 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).
- 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:
- 0 — "Every tick"
- 1 — "1 Minute OHLC",
- 2 — "Opening prices only",
- 3 — "Mathematical calculations",
- 4 — "Every tick based on real ticks"
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); }
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:
- open the file for reading and writing, read the first line (this operation moves the file pointer at the beginning of the second line);
- write the new value "df=1256" to the second line;
- write the new value "asf=5" to the third line;
- close the file.
//+------------------------------------------------------------------+ //| 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:
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.
- Copy the "terminal.ini" file from the Slave terminal into the sandbox of the Master terminal to the file named "terminal_ext.ini" (Win API CopyFileW).
- Create a file named "terminal.ini" in the Master terminal sandbox, open it for writing (MQL5).
- Open the "terminal_ext.ini" file in the Master terminal sandbox for writing (MQL5).
- In the Master terminal sandbox: read lines from "terminal_ext.ini" and write them to the "terminal.ini" file (MQL5).
- Once the read is "[Window]" - write the new coordinates (six lines) to the "terminal.ini" file, and move the file pointer in the "terminal_ext.ini" to six lines as well (MQL5).
- In the Master terminal sandbox: read lines from "terminal_ext.ini" and write them to the "terminal.ini" file, until the end of the "terminal_ext.ini" file is found (MQL5).
- In the Master terminal sandbox: close the "terminal.ini" and "terminal_ext.ini" files (MQL5).
- Copy the "terminal.ini" from the Master terminal sandbox to the Slave terminal, into the "terminal.ini" file (Win API CopyFileW).
- In the Master terminal sandbox: delete the "terminal.ini" and "terminal_ext.ini" files (MQL5).
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:
- the "myconfiguration.ini" configuration files have been prepared for all the Slave terminals;
- the "terminal.ini" files of all the Slave terminals have been edited;
- the name of the Expert Advisor to be tested is known.
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);
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, };
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:
- CopyFileW — copies files into the "sandbox" and from the "sandbox" of the MQL5.
- FindClose — closes the search handles.
- FindFirstFileW — searches for the file directory or subdirectory with the name that matches the specified file name.
- FindNextFileW — continues the search for the file from the previous call to the FindFirstFile function.
- GetOpenFileNameW — calls the system dialog for opening a file
- ShellExecuteW — runs application
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/2552





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