![交易者的 LifeHack: 四次回测比一次好](https://c.mql5.com/i/articles/overlay.png)
交易者的 LifeHack: 四次回测比一次好
在第一次单独测试之前,每个交易者都面对相同的问题 — "四种模式中使用那一种呢?" 每个提供的模式都有其优点和特点,所以我们将会使用简单的方法 - 使用一个按钮一起运行全部四种模式!本文展示了如何使用 Win API 和一点魔术来同时看到全部四个测试图表。
目录
- 简介
- 1. 一般原则
- 2. 输入参数
- 3. 匹配安装文件夹与从终端(Slave terminals)的AppData文件夹
- 3.1. 秘密 №1
- 3.2. FindFirstFileW, FindNextFileW
- 3.3. 使用 FindFirstFileW, FindNextFileW 的示例
- 3.4. 在终端目录内
- 3.5. CopyFileW
- 3.6. 操作 "origin.txt" 文件
- 3.7. 完成工作
- 4. 选择用于测试的 EA
- 4.1. GetOpenFileName
- 4.2. 使用"打开文件"系统对话框选择一个EA交易
- 4.3. 配置 INI 文件
- 4.4. 秘密 №2
- 4.5. 设置终端大小(宽度, 高度). 在文件中间插入文字行
- 5. 在从终端中运行测试
- 6. 可能的错误
- 结论
简介
本文的主要目的是展示如何从一个终端到同时在四个终端(它们将被称为从终端,并使用#1,#2,#3和#4表示)上运行一个EA交易的单个测试(不是优化,只是测试!)。与此同时,从终端中的策略测试器将以不同的订单生成模式来运行:
- 终端 #1 — "基于真实订单的每一订单";
- 终端 #2 — "每一订单";
- 终端 #3 — "1 分钟 OHLC";
- 终端 #4 — "只使用开盘价".
重要的局限:
- 主终端(Master terminal)一定不能使用/portable参数启动;
- 至少要安装五个 MetaTrader 5 终端;
- 主终端的交易账户 — 让我们称之为主帐户 — 必须在每个从终端上至少激活一次。这是有必要的,因为本文中的EA交易不会使用INI文件把交易账户密码传给从终端,它只是传送运行策略测试器的交易账户的编号,并且这个编号永远对应着主帐户的编号。这样的行为是符合逻辑的,因为应该在同一个交易账户上使用不同的订单生成模式来测试EA交易。
- 在启动EA交易之前,需要使CPU尽可能空闲: 关闭在线游戏,媒体播放器和其他消耗资源较多的程序,否则,一个CPU核心可能会被阻挡,那个核上可能就无法进行测试。
1. 一般原则
过犹不及.
我一直倾向于使用软件的标准功能,关于 MetaTrader 5 交易终端, 它有如下说法: "永远不要使用 /portable 关键字启动终端, 永远不要在操作系统中禁止用户账户控制(UAC)"。在此基础上,所介绍的EA交易将操作位于 AppData 文件夹下的文件。
所有文章中的屏幕截图都是在 Windows 10 中完成的,因为它是最新的许可证完整的系统,所有文中的应用程序代码都会考虑这一点。
相关的EA交易广泛使用了DLL和其他MQL5特性:
图 1. 依赖关系
特别指出,调用了以下的 Windows API 函数:
- CopyFileW — 把文件复制到 "沙盒(sandbox)" 以及复制来自MQL5 "沙盒"中的文件。
- FindClose — 关闭搜索句柄。
- FindFirstFileW — 在文件目录或者子目录中寻找符合指定文件名称的文件。
- FindNextFileW — 在之前的FindFirstFile函数调用之后继续搜索文件。
- GetOpenFileNameW — 调用系统对话框来打开一个文件:
- ShellExecuteW — 用于运行从终端。
图 2. 打开文件
关于 ① 和 ② 图标的详细信息将在章节4.2. 使用"打开文件"系统对话框选择一个EA.中提供。
2. 输入参数
图 3. 输入参数
"folder of the MetaTrader#Х installation(MetaTrader #X 的安装文件夹)"路径就是从终端的安装文件夹,当在mq5代码中指定路径时,需要使用双斜线书写。另外很重要的就是在路径的末尾使用双反斜线:
//--- 输入参数 input string ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\"; // MetaTrader#1 的安装文件夹 input string ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\"; // MetaTrader#2 的安装文件夹 input string ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\"; // MetaTrader#3 的安装文件夹 input string ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\"; // MetaTrader#4 的安装文件夹 input string ExtTerminalName="terminal64.exe"; // 正确的终端文件名称
在64位操作系统中,终端程序的文件名称是"terminal64.exe"。
当终端以普通方式启动或者使用/portable关键字启动时,终端会为 TERMINAL_DATA_PATH 变量生成不同的路径。让我们考虑这种情形,例如主终端安装在"C:\Program Files\MetaTrader 5 1"目录下,
如果主终端以 /portable 关键字启动, MQL 将从终端中生成以下结果:
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
而这里是不使用 /portable 关键字的终端反应:
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
这对于只从当前终端接收参数是有用的。那么将要运行EA测试的从终端还需要怎样处理呢?如何绑定从终端的安装目录和它们的数据目录呢?
这就需要解释为什么知道数据文件夹是在 AppData 文件夹下是很重要的了(引用自帮助):
从 MS Windows Vista 开始, 安装在 Program Files 文件夹下的应用程序,不允许在安装目录下保存数据,默认的所有用户(All)的数据应该保存在独立的Windows用户目录下。
换句话说,EA可以自由创建和修改位于如下文件夹下的文件: C:\Users\user_name\AppData\Roaming\MetaQuotes\Terminal\terminal_identifier\MQL5\Files. 在这里,"terminal_identifier"是主终端的标示符。
3. 匹配安装文件夹与从终端(Slave terminals)的AppData文件夹
EA 根据指定的配置文件在从终端中启动,另外,每个终端使用的是单独的配置文件,每个配置文件都有指示,使终端启动之后就开始测试指定的EA交易。对应的命令位于配置文件的 [Tester] 部分:
... [Tester] Expert=test ...
您可以看到,并没有指定路径,意思是测试的 EA 交易可以独立位于 MQL5 "沙盒"中,以从终端1为例,可以有两个路径:
- C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
- 或者 C:\Program Files\MetaTrader 5 1\MQL5\Experts
位置 №2 被排除掉,因为根据从 Windows Vista 开始的安全策略,禁止在 "Program Files" 文件夹下写入,只剩下位置 №1 — 这就是说所有的从终端都需要进行安装目录和AppData中的数据目录进行匹配。
每个数据目录都包含了一个 "origin.txt" 文件,以从终端1为例:
图 4. "origin.txt" 文件
以及 origin.txt 文件的内容:
C:\Program Files\MetaTrader 5 1
文件中的这个记录指出,"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962" 文件夹是由安装在 "C:\Program Files\MetaTrader 5 1" 目录下的终端创建的。
3.2. FindFirstFileW, FindNextFileW
FindFirstFileW — 在目录或者子目录下搜索符合某名称(或者名称的一部分,假如使用了特定的字符)的文件。
HANDLE FindFirstFileW(
string lpFileName, //
WIN32_FIND_DATA &lpFindFileData //
);
参数
lpFileName
[in] 文件的目录路径和名称,可以包含通配符,例如星号(*)或者问号(?)。
lpFindFileData
[in][out] WIN32_FIND_DATA 结构的指针,用于接收找到文件或目录的信息。
返回值
如果函数成功,返回值将是用于随后调用FindNextFile或者FindClose函数的搜索句柄, 并且lpFindFileData参数包含了找到的第一个文件或者目录。
如果函数失败或者无法找到lpFileName参数中指定的搜索字符串的文件,它就返回INVALID_HANDLE_VALUE而lpFindFileData中的内容将是未定义的,为了取得错误的进一步信息,调用GetLastError函数。
如果函数因为没有找到对应文件而触发,GetLastError函数返回ERROR_FILE_NOT_FOUND。
FindNextFileW — 继续前一次调用FindFirstFile, FindFirstFileEx, 或者 FindFirstFileTransacted 函数之后的搜索。
bool FindNextFileW(
HANDLE FindFile, //
WIN32_FIND_DATA &lpFindFileData //
);
参数
FindFile
[in] 之前调用 FindFirstFile 或 FindFirstFileEx 函数返回的句柄。
lpFindFileData
[in][out] WIN32_FIND_DATA 结构的指针,用于接收找到文件或目录的信息。
返回值
如果函数成功,返回值是非0值,并且lpFindFileData参数包含了找到的文件或者文件夹,
如果函数以出错结束,返回值为0,并且lpFindFileData的内容是未定义的。为了获取错误的额外信息,调用GetLastError函数。
如果函数是因为没有找到任何文件而出错,GetLastError 函数返回 ERROR_NO_MORE_FILES。
声明 Win API 中 FindFirstFileW 和 FindNextFileW 函数的实例 (代码来自所包含的 ListingFilesDirectory.mqh 文件):
#define MAX_PATH 0x00000104 // #define FILE_ATTRIBUTE_DIRECTORY 0x00000010 // #define ERROR_NO_MORE_FILES 0x00000012 //没有更多文件 #define ERROR_FILE_NOT_FOUND 0x00000002 //系统没有找到指定的文件 //+------------------------------------------------------------------+ //| FILETIME 结构 | //+------------------------------------------------------------------+ struct FILETIME { uint dwLowDateTime; uint dwHighDateTime; }; //+------------------------------------------------------------------+ //| WIN32_FIND_DATA 结构 | //+------------------------------------------------------------------+ 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. 使用 FindFirstFileW, FindNextFileW 的实例
"ListingFilesDirectory.mq5"脚本程序既是实例也是EA中的完整工作代码,换句话说,代码会尽可能接近实际使用。
目标: 获得 TERMINAL_COMMONDATA_PATH 中的全部文件夹的名称 - "Common"。
例如, 计算机上的 TERMINAL_COMMONDATA_PATH 返回 "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common",所以,如果从该路径中去掉 "Common", 就能获得所需路径 "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\":
图 5. Find First(找到第一个)
通常, "*.*" 搜索字串用于寻找所有文件,所以,需要使用以下字符串进行两次操作: 消去"Common"单词, 再加上"*.*"字符串:
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);
让我们检查结果路径,为此,设置一个断点再开始调试
图 6. 调试
我们将会有:
mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*
这样一切正常: 准备字符串用于在"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\"目录下搜索全部文件和文件夹。
下一步: 初始化 "hFind" 搜索句柄 (在我的实例中,这主要是因为习惯)并调用Win API中的 FindFirstFileW 函数:
printf("mask_path=%s",mask_path); hFind=-100; hFind=FindFirstFileW(mask_path,ffd); if(hFind==INVALID_HANDLE) { PrintFormat("FindFirstFile (hFind) 失败,错误代码: %x",kernel32::GetLastError()); return; } // 列出目录下的所有文件以及一些相关信息
如果调用 FindFirstFileW 失败, "hFind" 搜索句柄将等于 "INVALID_HANDLE",而脚本程序将会终止。
如果调用 FindFirstFileW 函数成功, 创建一个do while循环, 在其中获得文件或者文件夹的名称, 并且在循环的末尾将会调用Win API 的 FindNextFileW 函数:
// 列出目录下的所有文件以及一些相关信息 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,"\", 文件属性常数 (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("FindNextFileW (hFind) 失败,错误代码: %x",kernel32::GetLastError()); WinAPI_FindClose(hFind);
'do while' 循环在 Win API 的 FindNextFileW 函数返回非零值时会一直继续,如果调用 Win API 的 FindNextFileW 函数返回0,并且错误代码不等于 "ERROR_NO_MORE_FILES" — 意思就是遇到了严重错误。
在脚本程序运行的末尾,搜索句柄会被关闭。
"ListingFilesDirectory.mq5" 脚本程序的运行结果:
mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.* hFind=-847293552 ".", 文件属性常数 (dec): 16 "..", 文件属性常数 (dec): 16 "038C9E8FAFF9EA373522ECC6D5159962", 文件属性常数 (dec): 16 "0C46DDCEB43080B0EC647E0C66170465", 文件属性常数 (dec): 16 "2A6A33B25AA0984C6AB9D7F28665B88E", 文件属性常数 (dec): 16 "50CA3DFB510CC5A8F28B48D1BF2A5702", 文件属性常数 (dec): 16 "BC11041F9347CD71C5F8926F53AA908A", 文件属性常数 (dec): 16 "Common", 文件属性常数 (dec): 16 "Community", 文件属性常数 (dec): 16 "D0E8209F77C8CF37AD8BF550E51FF075", 文件属性常数 (dec): 16 "D3852169A6E781B7F35488A051432620", 文件属性常数 (dec): 16 "EE57F715BA53F2E183D6731C9376293D", 文件属性常数 (dec): 16 "Help", 文件属性常数 (dec): 16
以上描述的实例演示了顶端的工作 — 在 "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" 目录中,但是请注意章节3.1. 秘密 №1, 还有必要深入查看所有的子文件夹。
为此,要组织一次二级搜索,在子目录中搜索需要使用 Win API 中 FineFirstFileW的主搜索字符串:
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" + name of found top-level folder + "origin.txt".
这样 FindFirstFileW 的主搜索将只会在子目录中查找单个文件 — "origin.txt".
这里是 FindDataPath() 函数的完整代码:
//+------------------------------------------------------------------+ //| 找到并读取 origin.txt 文件 | //+------------------------------------------------------------------+ void FindDataPath(string &array[][2]) { //--- WIN32_FIND_DATA ffd; long hFirstFind_0,hFirstFind_1; ArrayInitialize(ffd.cFileName,0); ArrayInitialize(ffd.cAlternateFileName,0); //+------------------------------------------------------------------+ //| 取得计算机中安装的所有终端的通用路径. | //| 我的电脑中的通用路径: | //| 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) { //+------------------------------------------------------------------+ //| 去掉 "Common" ... 我们可以得到: | //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal | //+------------------------------------------------------------------+ common_data_path=StringSubstr(common_data_path,0,pos-1); } else return; //--- 搜索阶段 №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("FindFirstFile (hFirstFind_0) 失败,错误代码: %x",kernel32::GetLastError()); return; } //--- 列出目录下的全部文件以及一些相关信息 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); //--- 搜索阶段 №1. 在文件夹下搜索 origin.txt 文件 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("FindFirstFile (hFirstFind_1) 失败,错误代码: %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 文件 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); //--- 从 origin.txt 文件中得到一个字符串 if(origin!=NULL) { //--- 在数组中写下字符串 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), 我们在这里出现 if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES) PrintFormat("FindNextFileW (hFirstFind_0) 失败,错误代码: %x",kernel32::GetLastError()); else Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",hFirstFind_0,", NO_MORE_FILES"); WinAPI_FindClose(hFirstFind_0); }
FindDataPath() 函数打印出差不多如下信息:
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
打印的第一行的解释: 首先,它创建 "filter_0" 过滤器用于主搜索 (过滤器是 "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*") 并获得 "hFirstFind_0" 的主搜索句柄, 它等于 1901014212592。因为 "hFirstFind_0" 的值不等于 "INVALID_HANDLE" — 那么传给 Win API 中 FindFirstFileW(filter_0,ffd) 的搜索过滤字符串就是正确的。在成功调用了 FindFirstFileW(filter_0,ffd) 之后, 第一个文件夹的名称就得到了: 它是 "038C9E8FAFF9EA373522ECC6D5159962" 文件夹,
下一步,需要在 038C9E8FAFF9EA373522ECC6D5159962 文件夹下搜索 "origin.txt" 文件,为此,要构建过滤字符串。例如,对于 038C9E8FAFF9EA373522ECC6D5159962,过滤字符串将如下: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt",如果 "hFirstFind_1" 句柄不等于 "INVALID_HANDLE" — 那么指定的文件夹(038C9E8FAFF9EA373522ECC6D5159962)就包含指定的文件(origin.txt)。
打印的内容清晰显示了有时候在子目录的搜索返回了 "INVALID_HANDLE",它的意思就是指定的文件夹不包含 "origin.txt" 文件。
让我们继续讨论当在子目录中找到了"origin.txt"后应该怎样做。
CopyFileW — 把已经存在的文件复制成一个新的文件。
bool CopyFileW( string lpExistingFileName, // string lpNewFileName, // bool bFailIfExists // );
参数
lpExistingFileName
[in] 已存在文件的名称。
在此, 名称长度有一个限制 — 最多有MAX_PATH个字符, 这对例子来说肯定是足够的。
如果名称为lpExistingFileName的文件不存在,函数会失败并且GetLastError返回ERROR_FILE_NOT_FOUND。
lpNewFileName
bFailIfExists[in] 新文件的名称。
在此, 名称长度有一个限制 — 最多有MAX_PATH个字符, 这对例子来说肯定是足够的。
[in]如果此参数为TRUE并且在lpNewFileName中指定的新文件已经存在,函数就会失败。如果此参数为FALSE并且新文件存在,函数会覆盖已有文件并且成功结束。
返回值
如果函数成功,返回值不等于0。
如果函数以出错结束,返回值为0。为了取得错误的额外信息,需要调用GetLastError函数。
声明 Win API 中CopyFileW函数的实例 (代码来自所包含的 ListingFilesDirectory.mqh 文件):
#import "kernel32.dll" int GetLastError(); bool CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists); #import
3.6. 操作 "origin.txt" 文件
ListingFilesDirectory.mqh::CopiedAndReadFile(string full_file_name) 函数的用法描述。
在一个子目录下找到的 "origin.txt" 的文件全名会传给这个函数作为输入参数,路径可能看起来像: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". 打开 "origin.txt" 文件并通过 MQL5 方式读取它的内容,这表明文件必须位于“沙盒”之中,所以, "origin.txt" 必须从子目录复制到沙盒中(在这种情况下,复制到所有终端公用文件的“沙盒”之中)。这样的复制是通过 Win API 的 CopyFileW 函数来进行的,
把沙盒中 "origin.txt" 的路径写到 "new_path" 变量中:
//+------------------------------------------------------------------+ //| 复制到通用数据文件夹 | //| 即所有客户终端的 ***\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
并调用 Win API 的 CopyFileW 函数,第三个参数设为 false — 允许覆盖沙盒中的 "origin.txt" 文件:
//--- Win API if(!CopyFileW(full_file_name,new_path,false)) { Print("错误的 CopyFile,从 ",full_file_name," 到 ",new_path); return(NULL); } //--- 使用 MQL5 打开文件
打开 "origin.txt" 文件用于读取,并且不要忘记设置 FILE_COMMON 标志, 因为文件是在通用文件夹中:
//--- 使用 MQL5 打开文件 string str; ResetLastError(); int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON); if(file_handle!=INVALID_HANDLE) { //--- 使用 MQL5 读取一个字符串 str=FileReadString(file_handle,-1)+"\\"; //--- 使用 MQL5 关闭文件 FileClose(file_handle); } else { PrintFormat("文件 %s 打开失败 , MQL5 错误=%d","origin.txt",GetLastError()); return(NULL); } return(str); }
只读取一次 — 一个字符串,在它的末尾加上 "\\" 并返回获得的结果。
四个终端的安装目录在输入参数中设置:
//--- 输入参数 input string ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\"; // MetaTrader#1 安装文件夹 input string ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\"; // MetaTrader#2 安装文件夹 input string ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\"; // MetaTrader#3 安装文件夹 input string ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\"; // MetaTrader#4 安装文件夹
这些路径是固定写成的,它们必须正确指向安装的目录。
另外,在全局还要声明另外四个字符串变量和一个数组:
string slaveTerminalDataPath1=NULL; // 终端 #1 的数据文件夹路径 string slaveTerminalDataPath2=NULL; // 终端 #2 的数据文件夹路径 string slaveTerminalDataPath3=NULL; // 终端 #3 的数据文件夹路径 string slaveTerminalDataPath4=NULL; // 终端 #4 的数据文件夹路径 //--- string arr_path[][2];
在 AppData 中的终端文件夹路径需要保存在那些变量中,二维数组将有所帮助。现在可以大致描绘如何匹配从终端的安装目录和它们在AppData中的文件夹了:
GetStatsFromAccounts_EA.mq5::OnInit() >调用> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path)
>调用> ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) >调用> CopiedAndReadFile(string full_file_name)
string origin=CopiedAndReadFile(filter_1); //--- 从 origin.txt 文件中得到一个字符串 if(origin!=NULL) { //--- 在数组中写下字符串 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);
当"origin.txt"文件在终端的子目录中找到时,ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) 函数调用 CopiedAndReadFile(string full_file_name) 函数,在调用之后就在二维数组中加入一条记录。数组的"0"维包含AppData中的终端路径,而"1"维保存着安装路径(提醒一下,这个路径可以从找到的"origin.txt"文件中获得)。
>把控制权返回给> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path):
在此,slaveTerminalDataPath1, slaveTerminalDataPath2, slaveTerminalDataPath3 和 slaveTerminalDataPath4 变量是在二维数组中使用简单的循环进行填充的:
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); }
如果达到了这一步,EA就匹配了安装路径和它们在AppData文件夹下的路径。如果至少有一个在AppData下的终端路径没有找到 (也就是说等于 NULL), 那么会在最后几行打印所有路径,而 EA 因错误而终止。
4. 选择用于测试的 EA
测试EA的文件应该在运行四个从终端之前选择好,该EA交易必须预先编译好并放在主终端的数据目录中。
GetOpenFileName — 创建"打开文件"对话框, 用户可以指定驱动器, 文件夹和将要打开文件(或一组文件)的名称。"打开文件"对话框的声明和实现都完整位于所包含的 GetOpenFileNameW.mqh 文件中。
"打开文件"系统对话框是在EA的OnInit()中被调用的:
//+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- 在终端的文件夹中编辑和复制 ini 文件
在那里调用了 GetOpenFileNameW.mqh::OpenFileName(void)。
//+------------------------------------------------------------------+ //| 创建打开文件对话框 | //+------------------------------------------------------------------+ string OpenFileName(void) { string path=NULL; string filter=NULL; if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") filter="Компилированный код"; else filter="已编译的代码"; if(GetOpenFileName(path,filter+"\0*.ex5\0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","选择源文件")) return(path); else { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); return(NULL); } }
如果调用 Win API 的 GetOpenFileName 函数成功, "path" 变量将包含所选文件的完整名称,例如: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5".
"filter" 是用于图2中的文字 ① 的。"\0*.ex5\0" 字符串用于过滤文件类型 (图2中的 ②)。"TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"" 字符串指定了 "打开文件"系统对话框的文件夹的路径。
为了从命令行运行(或者使用Win API)测试EA的终端,需要有一个配置INI文件,其中必须包含以下的[Tester]部分以及所需的指令:
[Tester] Expert=test //在测试模式下自动运行的EA交易的名称。 Symbol=EURUSD //用作主测试的交易品种名称 Period=H1 //测试图表的时段 Deposit=10000 //用于测试的初始存款 Model=4 //订单生成模式 Optimization=0 //启用/禁用优化以及设置它的类型 FromDate=2016.01.22 //测试开始日期 ToDate=2016.06.06 //测试结束日期 Report=TesterReport //用于保存测试报告的文件名称 ReplaceReport=1 //启用/禁止覆盖报告文件 UseLocal=1 //启用/禁用测试中的本地代理 Port=3000 //测试代理的端口 Visual=0 //启用/禁止可视化模式测试 ShutdownTerminal=0 //启用/禁止测试完成后平台的关闭
在计划中,我打算在文件中人工加入[Tester]部分,
并且巨顶使用主终端中的INI文件为基础,这个文件 (common.ini) 位于终端的数据目录,在"config"文件夹中。在例子里面,文件路径看起来如下: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini"。
操作INI文件的步骤是:
- 取得主终端中 "common.ini" 的完整路径,完整路径的字符串形式:
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini". (MQL5) - 取得INI文件在 "\Files" 沙盒中的新路径,新路径的字符串形式:
对于主终端,是"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini",(MQL5) - 把"common.ini"文件复制为"myconfiguration.ini",(Win API 的 CopyFileW 函数)。
- 编辑"myconfiguration.ini"文件,(MQL5);
- 取得INI文件在从终端沙盒中的新路径,它的字符串形式(以从终端 №1 为例)
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini". (MQL5) - 把编辑过的"myconfiguration.ini"INI文件从主终端的沙盒中复制到从终端的沙盒之中(Win API 的 CopyFileW 函数);
- 从主终端的沙盒中删除"myconfiguration.ini"文件,(MQL5)。
这个过程必须在每个从终端中重复进行。尽管还有优化的空间,但这些过程不在本文讨论之列。
在选择好了需要测试的EA交易之后,就开始编辑配置INI文件,GetStatsFromAccounts_EA.mq5::OnInit():
//+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- 在终端的文件夹中编辑和复制 ini 文件 if(!CopyCommonIni()) return(INIT_FAILED); if(!CopyTerminalIni()) return(INIT_FAILED); //--- 在终端文件夹下复制EA交易
操作INI文件的过程,以从终端 №1 为例, GetStatsFromAccounts_EA.mq5::CopyCommonIni():
//+------------------------------------------------------------------+ //| 复制 common.ini - 文件位于客户终端的共享 | //| 文件夹下,编辑 ini-文件,并把获得的文件复制 | //| 到文件夹中 | //| ...\AppData\Roaming\MetaQuotes\Terminal\"终端id"\MQL5\Files | //+------------------------------------------------------------------+ bool CopyCommonIni() { //0 — "每一订单", "1 — 1分钟OHLC", 2 — "仅开盘价" //3 — "数学计算", 4 — "基于真实订单的每一订单" //--- 数据文件夹的路径 string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); //--- 通用数据文件夹的路径 string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); //--- string existing_file_name=terminal_data_path+"\\config\\common.ini"; // ini文件的完整路径 string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name; string test=NULL; //--- 终端 #1 if(!CopyFileW(existing_file_name,temp_name_ini,false)) { PrintFormat("失败,错误编号: %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("失败,错误编号: %x",kernel32::GetLastError()); return(false); } ResetLastError(); if(!FileDelete(common_file_name,0)) Print("#1 file ",common_file_name," not deleted, an error ",GetLastError()); //--- 终端 #2
对 EditCommonIniFile(common_file_name,3000,4) 函数的调用传入以下参数:
common_file_name — 将要编辑的 INI 文件;
3000 — 测试代理的端口编号. 每个终端必须由它们自己的测试代理运行,代理编号从 3000 开始,如需查看测试代理的端口编号: 在 MetaTrader 5终端中,进入策略测试器并使用鼠标右键点击策略测试器的 "日志" 页面,测试代理的端口编号可以在下拉菜单中看到:
图 7. 测试代理
4 - 测试的类型:
- 0 — "每一订单"
- 1 — "1 分钟 OHLC",
- 2 — "仅开盘价",
- 3 — "数学计算",
- 4 — "基于真实订单的每一订单"
在 GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model) 函数中编辑 common.ini 配置文件 — 打开文件的操作,文件的读写都是使用 MQL5 的方式执行的:
//+------------------------------------------------------------------+ //| 编辑 common.ini 文件 | //+------------------------------------------------------------------+ bool EditCommonIniFile(string name,const int port,const int model) { bool tester=false; // 如果是 false - 意思是没有找到 [Tester] 部分 int count_tester=0; // 寻找 [Tester] 部分的计数器 //--- 打开文件 ResetLastError(); int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT); if(file_handle!=INVALID_HANDLE) { //--- 辅助变量 string str; //--- 读取数据 while(!FileIsEnding(file_handle)) { //--- 读取一行 str=FileReadString(file_handle,-1); //--- 寻找 [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 — "每一订单", "1 — 1 分钟 OHLC", 2 — "仅开盘价" //3 — "数学计算", 4 — "基于真实订单的每一订单" 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); } //--- 关闭文件 FileClose(file_handle); } else { PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError()); return(false); } return(true); }
在关闭之前,MetaTrader 5终端在terminal.ini文件中保存了窗口和面板的位置以及它们的大小,这个文件本身存储于终端的数据目录,在"config"子目录下。例如,从终端 №1 的"terminal.ini"的完整路径如下:
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\config\terminal.ini".
在"terminal.ini"文件中,我们只对"[Window]"部分有兴趣。恢复 MetaTrader 5 终端的窗口,终端的近似大小如下:
图 8. 恢复终端窗口
如果终端被关闭,terminal.ini文件中的[Window]部分有如下的形式:
Arrange=1 [Window] Fullscreen=0 Type=1 Left=412 Top=65 Right=1212 Bottom=665 LSave=412
也就是说,[Window] 部分保存着终端的坐标和状态。
4.5. 设置终端的大小 (宽度, 高度),在文件中插入文字
修改从终端的 terminal.ini 中的坐标,可以在从终端启动时按以下形式进行排布:
图 9. 终端的分布
上面已经说到,每个从终端的 "terminal.ini" 文件都需要修改,请注意,文字需要被插入,不是在末尾,而是在"terminal.ini"文件的中间。以下是这个过程的特点,
这里是一个例子: 终端的"沙盒"中有一个"test.txt"文件,"test.txt" 文件的内容:
s=0 df=12 asf=3 g=3 n=0 param_f=123
需要修改第二行和第三行以达到以下效果:
s=0 df=1256 asf=5 g=3 n=0 param_f=123
乍一看来, 应该这样做:
- 打开文件用于读写,读取第一行 (这个操作会把文件指针移动到第二行的开始);
- 在第二行写下新的值"df=1256";
- 在第三行写下新的值 "asf=5";
- 关闭文件。
//+------------------------------------------------------------------+ //| 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() { //--- 打开文件 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); //--- 关闭文件 FileClose(file_handle); } else { PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError()); return; } } //+------------------------------------------------------------------+
收到了一个未预料到的结果 — 第四行缺少了"g="的字符:
之前 | 之后 |
---|---|
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 |
为什么会这样呢?把文件看作包含了一系列相互连接的单元,每个单元包含一个字符,所以,当从文件的中间开始写一些内容时,单元会被覆盖,如果加入了比之前多的字符(在上面的例子中: 开始有"df=12", 后来多加了两个字符 - "df=1256"), 多出来的字符就打乱了后来的代码,所以看起来就成了这样:
图 10. 信息被打乱
为了避免打乱信息,当在文件中间插入文字时,要按以下方式做,
- 把从终端的 "terminal.ini" 文件复制到主终端的沙盒中,并命名为 "terminal_ext.ini" (Win API CopyFileW),
- 在主终端的沙盒中创建一个名为 "terminal.ini" 的文件,以写入方式打开它 (MQL5),
- 在主终端沙盒中以读取方式打开 "terminal_ext.ini" 文件 (MQL5),
- 在主终端的沙盒中: 从 "terminal_ext.ini" 读取内容并把它们写到 "terminal.ini"文件中 (MQL5),
- 当读到 "[Window]" 的时候 - 在 "terminal.ini" 文件中写入新的坐标 (6行), 并把 "terminal_ext.ini" 文件的指针也移动6行 (MQL5),
- 在主终端沙盒中: 从 "terminal_ext.ini" 读取内容并把它们写到 "terminal.ini" 文件中,直到 "terminal_ext.ini" 文件的末尾 (MQL5)。
- 在主终端沙盒中: 关闭"terminal.ini"和"terminal_ext.ini"文件 (MQL5),
- 把"terminal.ini"文件从主终端沙盒中复制到从终端的"terminal.ini"文件中(Win API CopyFileW)。
- 在主终端沙盒中: 删除 "terminal.ini" 和 "terminal_ext.ini" 文件(MQL5)。
函数调用顺序:
GetStatsFromAccounts_EA.mq5::OnInit() >调用> GetStatsFromAccounts_EA.mq5::CopyTerminalIni()
//+------------------------------------------------------------------+ //| 编辑 "terminal.ini" 文件 | //+------------------------------------------------------------------+ bool CopyTerminalIni() { //--- 终端数据文件夹路径 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("失败,错误编号: %x",kernel32::GetLastError()); return(false); } if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom)) return(false); if(!CopyFileW(ini,existing_file_name,false)) { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); return(false); } ResetLastError(); if(!FileDelete("terminal.ini",0)) Print("#",i," terminal.ini 文件未被删除, 错误为 ",GetLastError()); ResetLastError(); if(!FileDelete("terminal_ext.ini",0)) Print("#",i," terminal_ext.ini 文件未被删除, 错误为 ",GetLastError()); } //--- return(true); }
>调用> GetStatsFromAccounts_EA.mq5::EditTerminalIniFile
//+------------------------------------------------------------------+ //| 编辑 terminal.ini 文件 | //+------------------------------------------------------------------+ bool EditTerminalIniFile(string ext_name,const int Left=0,const int Top=0,const int Right=1366,const int Bottom=738) { //--- 创建并打开文件 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("无法打开文件 %s, 错误编号 = %d",name,GetLastError()); } if(terminal_ext_ini__handle==INVALID_HANDLE) { PrintFormat("无法打开文件 %s, 错误 = %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); } //--- 辅助变量 string str=NULL; //--- 读取数据 while(!FileIsEnding(terminal_ext_ini__handle)) { //--- 读取文字 str=FileReadString(terminal_ext_ini__handle,-1); FileWriteString(terminal_ini_handle,str+"\r\n",-1); //--- 找到 [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); } } //--- 关闭文件 FileClose(terminal_ext_ini__handle); FileClose(terminal_ini_handle); return(true); }
这样,从终端中的 "terminal.ini" 文件就编辑好了,就可以像图9种那样启动了,就可以观察测试图表,比较不同测试模式下的精确度了。
5. 在从终端中运行测试
在 EA 交易测试模式下运行从终端的准备工作都就绪了:
- 所有从终端的 "myconfiguration.ini" 配置文件都准备好了;
- 所有从终端的 "terminal.ini" 文件都编辑好了;
- 用于测试的 EA 交易名称也知道了。
在OnInit()中复制之前所选的EA交易 (它的名称保存在 "expert_name" 变量中):
//+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) return(INIT_SUCCEEDED); //--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) return(INIT_FAILED); //--- 在终端的文件夹中编辑和复制 ini 文件 if(!CopyCommonIni()) return(INIT_FAILED); if(!CopyTerminalIni()) return(INIT_FAILED); //--- 在终端文件夹下复制EA交易 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("CopyFileW #2 失败,错误代码: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("CopyFileW #3 失败,错误代码: %x",kernel32::GetLastError()); return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",false)) { PrintFormat("CopyFileW #4 失败,错误代码: %x",kernel32::GetLastError()); return(INIT_FAILED); } //--- Sleep(sleeping);
ShellExecuteW — 在指定文件上执行操作。
//--- 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 // );
参数
hwnd
[in] 父窗口的句柄,用于显示用户界面和错误消息,如果操作与窗口无关,这个值必须等于NULL。
lpOperation
[in] 决定将要执行行为的命令名称的字符串,可用的命令集依赖于指定的文件或者目录,作为一项规则,那些命令来自于对象的上下文菜单。以下是常用的命令:
"edit"
启动编辑器并打开文档用于编辑,如果lpFile不是一个文档文件,函数就不会被执行。
"explore"
打开lpFile中指定的文件。
"find"
在由lpDirectory指定的目录中开始搜索。
"open"
打开由lpFile参数定义的元件,这个元件可以是一个文件或者文件夹。
"print"
打印由lpFile指定的文件,如果lpFile不是一个文档文件,函数会出错并终止。
"NULL"
如果有默认命令,就使用默认命令,如果没有,就使用"open"命令,如果两个命令都没有,系统会使用注册表中指定的第一个命令。
lpFile[in] 用于设定执行命令的文件或者对象的字符串,传入的是全名(不仅包含文件名,还包含路径)。请注意,对象可能不支持所有的命令,例如,不是所有的文档都支持"print"命令。如果在lpDirectory参数中使用了相对路径,就不要在lpFile中使用相对路径。
lpParameters[in] 如果lpFile指向了一个可执行文件,这个参数就是一个定义了传给应用程序参数的字符串,字符串的格式由将要执行命令的名称决定。如果lpFile指向一个文档文件,lpParameters必须为NULL。
lpDirectory[in] 定义工作目录的字符串。如果这个值是NULL, 就使用当前的工作目录。如果在lpFile中指定了相对路径, 就不要在lpDirectory中使用相对路径。
nShowCmd[in] 决定应用程序在打开时如何显示的标志,如果lpFile指定了一个文档文件,这个标志就传给对应的应用程序。使用的标志:
//+------------------------------------------------------------------+ //| 启动应用程序的枚举命令 | //+------------------------------------------------------------------+ enum EnSWParam { //+------------------------------------------------------------------+ //| 把窗口显示为最小化窗口,这个值是类似于 | //| SW_SHOWMINIMIZED, 除了窗口没有被激活。 | //+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=7, //+------------------------------------------------------------------+ //| 激活并显示一个窗口,如果窗口被最小化或者 | //| 最大化, 系统会把它恢复到它原来的大小和 | //| 位置。应用程序应该指定这个标志,当 | //| 第一次显示窗口的时候。 | //+------------------------------------------------------------------+ SW_SHOWNORMAL=1, //+------------------------------------------------------------------+ //| 激活窗口并把它显示为最小化窗口 . | //+------------------------------------------------------------------+ SW_SHOWMINIMIZED=2, //+------------------------------------------------------------------+ //| 激活窗口并把它显示为最大化窗口。 | //+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=3, //+------------------------------------------------------------------+ //| 隐藏窗口并激活另一个窗口。 | //+------------------------------------------------------------------+ SW_HIDE=0, //+------------------------------------------------------------------+ //| 激活窗口并以它当前的大小 | //| 和位置显示。 | //+------------------------------------------------------------------+ SW_SHOW=5, };
返回值
如果函数成功,它返回一个大于 32 的值。
调用 Win API 的 ShellExecuteW 的例子:
#import "shell32.dll" int GetLastError(); //+------------------------------------------------------------------+ //| ShellExecute 函数 | //| https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx //| 对指定文件进行某项操作 | //+------------------------------------------------------------------+ //--- 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" //+------------------------------------------------------------------+ //| 启动应用程序的枚举命令 | //+------------------------------------------------------------------+ enum EnSWParam { //+------------------------------------------------------------------+ //| 把窗口显示为最小化窗口,这个值是类似于 | //| SW_SHOWMINIMIZED, 除了窗口没有被激活。 | //+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=7, //+------------------------------------------------------------------+ //| 激活并显示一个窗口,如果窗口被最小化或者 | //| 最大化, 系统会把它恢复到它原来的大小和 | //| 位置。应用程序应该指定这个标志,当 | //| 第一次显示窗口的时候。 | //+------------------------------------------------------------------+ SW_SHOWNORMAL=1, //+------------------------------------------------------------------+ //| 激活窗口并把它显示为最小化窗口 . | //+------------------------------------------------------------------+ SW_SHOWMINIMIZED=2, //+------------------------------------------------------------------+ //| 激活窗口并把它显示为最大化窗口。 | //+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=3, //+------------------------------------------------------------------+ //| 隐藏窗口并激活另一个窗口。 | //+------------------------------------------------------------------+ SW_HIDE=0, //+------------------------------------------------------------------+ //| 激活窗口并以它当前的大小 | //| 和位置显示。 | //+------------------------------------------------------------------+ SW_SHOW=5, };
从终端在 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); }
同时,EA交易在每次运行之间等待"sleeping"毫秒。默认条件下,"sleeping" 参数等于 9000 (也就是 9 秒),如果在从终端中出现代理认证错误,就加大这个参数。
传给这个 Win API 函数的参数(以从终端 №1 为例) 如下:
LaunchSlaveTerminal("C:\Program Files\MetaTrader 5 1\", "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini");
6. 可能的错误
当从终端启动,而测试器没有连接上测试代理的时候可能会出现状况,
测试器将在"日志"页面出现类似如下的记录:
2016.07.15 15:10:48.327 Tester EURUSD: 历史数据开始于 2014.01.14 00:00 2016.07.15 15:10:49.212 Core 1 代理的处理开始 2016.07.15 15:10:49.717 Core 1 连接到 127.0.0.1:3002 2016.07.15 15:11:00.771 Core 1 测试器代理认证错误 2016.07.15 15:11:01.417 Core 1 连接关闭
测试代理记录包含这些内容:
2016.07.15 16:08:45.416 启动 MetaTester 5 x64 build 1368 (13 Jul 2016) 2016.07.15 16:08:45.612 服务器 MetaTester 5 开始于 127.0.0.1:3000 2016.07.15 16:08:45.612 启动初始化结束 2016.07.15 16:09:36.811 服务器 MetaTester 5 停止 2016.07.15 16:09:38.422 Tester 关闭测试机
在这种情况下,推荐增加终端运行的延迟 ("sleeping" 变量), 并且退出所有可能阻止CPU内核使用的消耗资源的应用程序。
结论
同时在四种测试模式下测试所选EA交易的任务就这样结束了,在启动EA交易后,就可以几乎同时在全部四个终端中观察测试。
本文也展示了如何调用 Win API 函数,例如:
- CopyFileW — 把文件复制到"沙盒"以及从MQL5的"沙盒"复制文件。
- FindClose — 关闭搜索句柄
- FindFirstFileW — 在文件目录或者子目录中搜索匹配指定文件名称的文件。
- FindNextFileW — 继续前一次 FindFirstFile 函数调用的搜索。
- GetOpenFileNameW — 调用系统对话框用于打开一个文件。
- ShellExecuteW — 运行应用程序。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/2552
![以横盘和趋势行情为例强化策略测试器的指标优化](https://c.mql5.com/2/22/Optimize-Indicators-Only.png)
![交易者的 LifeHack: 测试中的余额,回撤,负载和订单指标](https://c.mql5.com/2/23/avac18.png)
![EA交易的自我优化: 进化与遗传算法](https://c.mql5.com/2/22/images__2.png)