
交易者的 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



应始终启用 UAC。
以上和以下所有内容都适用于 Windows 10,以前的操作系统甚至不在讨论或考虑之列。
由于与操作系统安全性冲突,用户(或在其下运行的程序)无权写入 "程序文件",但 AppData 中却没有禁止。如果终端安装时没有使用 \Portable 密钥,并且启用了 UAC,会发生什么情况?没错,这是标准安装,写入文件不会有问题。
一般来说,你应该尽量使用标准程序和安装,这样问题和冲突会少很多。
我的系统是 Win 10 x64,UAC 已启用。我写道,我不把终端放在程序文件中,所有东西都放在 c:\forex 及其子文件夹中。你可以安全地在那里写。
总的来说,这是一个宗教问题,我试图按照更方便的方式生活,经常违反规定,而你--按照写下来的规定。这既不好也不坏,只是性格不同而已 ))
这篇文章很棒。
我认为可以用它来使用 valking forward 方法(通过移动优化 日期)自动启动优化。
有没有可能在某个地方看到所有变量的变体及其在配置 ini 文件中的应用值?
我对使用前向法进行优化很感兴趣。还需要将本地代理与本地网络和云代理连接起来。我希望所有这些都能通过 ini 文件进行控制?
这篇文章很棒。
我认为可以用它来使用 valking forward 方法(通过移动优化 日期)自动启动优化。
有没有可能在某个地方看到所有变量的变体及其在配置 ini 文件中的应用值?
我对使用前向法进行优化很感兴趣。还需要将本地代理与本地网络和云代理连接起来。希望所有这些都能通过 ini 文件进行控制?
使用自己的配置文件运行- 以下是 common.ini 文件的说明
有兴趣使用前向功能进行优化。还需要将本地代理与来自本地网络和云的代理连接起来。我希望所有这些都能通过 ini 文件控制?
使用自己的配置文件运行- 这里是 common.ini 文件的说明