交易者的 LifeHack: 四次回测比一次好·进阶篇
在 MT5 里直接调 Windows 文件枚举
想把 MT5 指标或 EA 跑出的日志按文件批量管理,不必绕道外部脚本,直接 import kernel32.dll 就能用 Windows 原生查找函数。下面这组声明把 FindFirstFileW / FindNextFileW / FindClose 以及错误码常量先钉死在 MQL5 侧。 MAX_PATH 定义为 0x00000104(260),FILE_ATTRIBUTE_DIRECTORY 是 0x00000010,ERROR_NO_MORE_FILES 为 0x00000012——遍历目录时靠这些常量判断边界,少一个都会让循环提前断掉。 WIN32_FIND_DATA 结构里 cFileName 用 ushort[MAX_PATH] 存宽字符文件名,ftLastWriteTime 是 FILETIME 双 uint 组合,拿到后需自己拼成时间。注意原文对 FindNextFileW 和 FindClose 各写了 long 与 int 两套重载,这是为 32/64 位终端兼容留的口子。 实际封装函数会用 _IsX64 判断终端位数:64 位走 long 句柄,32 位强转 int 再调。你开 MT5 新建脚本粘下面代码,编译后能在「文件」目录里验证句柄是否非空,从而确认环境打通。
class="type">class="kw">string lpFileName, class=class="str">"cmt">// WIN32_FIND_DATA &lpFindFileData class=class="str">"cmt">// ); class="type">bool FindNextFileW( HANDLE FindFile, class=class="str">"cmt">// WIN32_FIND_DATA &lpFindFileData class=class="str">"cmt">// ); class="macro">#define MAX_PATH 0x00000104 class=class="str">"cmt">// class="macro">#define FILE_ATTRIBUTE_DIRECTORY 0x00000010 class=class="str">"cmt">// class="macro">#define ERROR_NO_MORE_FILES 0x00000012 class=class="str">"cmt">//没有更多文件 class="macro">#define ERROR_FILE_NOT_FOUND 0x00000002 class=class="str">"cmt">//系统没有找到指定的文件 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| FILETIME 结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct FILETIME { class="type">uint dwLowDateTime; class="type">uint dwHighDateTime; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WIN32_FIND_DATA 结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct WIN32_FIND_DATA { class="type">uint dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; class="type">uint nFileSizeHigh; class="type">uint nFileSizeLow; class="type">uint dwReserved0; class="type">uint dwReserved1; class="type">class="kw">ushort cFileName[MAX_PATH]; class="type">class="kw">ushort cAlternateFileName[class="num">14]; }; class="macro">#class="kw">import "kernel32.dll" class="type">int GetLastError(); class="type">long FindFirstFileW(class="type">class="kw">string lpFileName,WIN32_FIND_DATA &lpFindFileData); class="type">int FindNextFileW(class="type">long FindFile,WIN32_FIND_DATA &lpFindFileData); class="type">int FindClose(class="type">long hFindFile); class="type">int FindNextFileW(class="type">int FindFile,WIN32_FIND_DATA &lpFindFileData); class="type">int FindClose(class="type">int hFindFile); class="type">int CopyFileW(class="type">class="kw">string lpExistingFileName,class="type">class="kw">string lpNewFileName,class="type">bool bFailIfExists); class="macro">#class="kw">import class="type">bool WinAPI_FindClose(class="type">long hFindFile) { class="type">bool res; if(_IsX64) res=FindClose(hFindFile)!=class="num">0; else res=FindClose((class="type">int)hFindFile)!=class="num">0; class=class="str">"cmt">//--- class="kw">return(res); } class="type">bool WinAPI_FindNextFile(class="type">long hFindFile,WIN32_FIND_DATA &lpFindFileData) { class="type">bool res; if(_IsX64) res=FindNextFileW(hFindFile,lpFindFileData)!=class="num">0; else
◍ 用 WinAPI 遍历 MT5 终端目录拿到账号指纹
MT5 的 Terminal 目录下每个安装实例都有一串 32 位十六进制文件夹名,例如 038C9E8FAFF9EA373522ECC6D5159962,文件属性常数统一为 dec 16(目录)。想批量识别本机装了几个终端,直接调 WinAPI 枚举即可。
先取 TERMINAL_COMMONDATA_PATH,用 StringFind 找 Common 字样并截掉后半段,拼上 \*.* 得到枚举掩码。实测某机掩码为 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*,FindFirstFileW 返回句柄 -847293552(即 INVALID_HANDLE 之外的有效负值)。
拿到句柄后 do-while 循环调 WinAPI_FindNextFile,每轮用 ShortToString 把 cFileName 的 wchar 数组拼成名字再 Print。循环结束必须 WinAPI_FindClose 释放,否则句柄泄漏;若 GetLastError 不是 ERROR_NO_MORE_FILES 才是真异常。
这套逻辑对外汇/贵金属多账号批量管理有概率价值,但 MT5 自动化涉及系统 API 调用,误用可能卡死终端,属高风险操作,建议先在模拟环境跑通。
res=FindNextFileW((class="type">int)hFindFile,lpFindFileData)!=class="num">0; class=class="str">"cmt">//--- class="kw">return(res); } class="type">class="kw">string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); class="type">int pos=StringFind(common_data_path,"Common",class="num">0); if(pos!=-class="num">1) { common_data_path=StringSubstr(common_data_path,class="num">0,pos-class="num">1); } else class="kw">return; class="type">class="kw">string path_addition="\\*.*"; class="type">class="kw">string mask_path=common_data_path+path_addition; printf("mask_path=%s",mask_path); hFind=-class="num">100; hFind=FindFirstFileW(mask_path,ffd); if(hFind==INVALID_HANDLE) { PrintFormat("FindFirstFile(hFind) 失败,错误代码: %x",kernel32::GetLastError()); class="kw">return; } PrintFormat("hFind=%d",hFind); class="type">bool rezult=class="num">0; do { class="type">class="kw">string name=""; for(class="type">int i=class="num">0;i<MAX_PATH;i++) { name+=ShortToString(ffd.cFileName[i]); } Print("",name,"", 文件属性常数 (dec): ",ffd.dwFileAttributes); ArrayInitialize(ffd.cFileName,class="num">0); ArrayInitialize(ffd.cAlternateFileName,class="num">0); ffd.dwFileAttributes=-class="num">100; ResetLastError(); rezult=WinAPI_FindNextFile(hFind,ffd); } while(rezult!=class="num">0); if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES) PrintFormat("FindNextFileW(hFind) 失败,错误代码: %x",kernel32::GetLastError()); WinAPI_FindClose(hFind);
「遍历本地多终端安装目录」
做跨终端批量分析时,第一步是拿到机器上所有 MT5 终端的父目录。通用数据路径通过 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 取得,典型值为 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common,截掉末尾的 "Common" 即可得到 Terminal 这一层。 下面这段函数用 Win32 的 FindFirstFileW 从 Terminal\*.* 开始扫,只挑目录项,跳过 "." 和 "..",为后续在每个终端子目录里找 origin.txt 做准备。注意 hFirstFind_0 若为 INVALID_HANDLE,说明路径不对或权限受限,应当直接 return 而不是继续跑。 实机上若装了 3 个 MT5,循环会命中 3 个 32 位哈希命名的子文件夹;Print 出的 filter_0 形如 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*,可贴到文件管理器验证。外汇与贵金属相关自动化涉及杠杆与滑点,属高风险操作,任何路径遍历脚本先在模拟环境跑通再上真机。
class="type">void FindDataPath(class="type">class="kw">string &array[][class="num">2]) { class=class="str">"cmt">//--- WIN32_FIND_DATA ffd; class="type">long hFirstFind_0,hFirstFind_1; ArrayInitialize(ffd.cFileName,class="num">0); ArrayInitialize(ffd.cAlternateFileName,class="num">0); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 取得计算机中安装的所有终端的通用路径. | class=class="str">"cmt">//| 我的电脑中的通用路径: | class=class="str">"cmt">//| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); class="type">int pos=StringFind(common_data_path,"Common",class="num">0); if(pos!=-class="num">1) { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 去掉 "Common" ... 我们可以得到: | class=class="str">"cmt">//| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal | class=class="str">"cmt">//+------------------------------------------------------------------+ common_data_path=StringSubstr(common_data_path,class="num">0,pos-class="num">1); } else class="kw">return; class=class="str">"cmt">//--- 搜索阶段 №class="num">0. class="type">class="kw">string filter_0=common_data_path+"\\*.*"; class=class="str">"cmt">// filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.* hFirstFind_0=FindFirstFileW(filter_0,ffd); class=class="str">"cmt">//--- class="type">class="kw">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); class=class="str">"cmt">//--- if(hFirstFind_0==INVALID_HANDLE) { PrintFormat("FindFirstFile(hFirstFind_0) 失败,错误代码: %x",kernel32::GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 列出目录下的全部文件以及一些相关信息 class="type">bool rezult=class="num">0; do { if((ffd.dwFileAttributes &FILE_ATTRIBUTE_DIRECTORY)==FILE_ATTRIBUTE_DIRECTORY) { class="type">class="kw">string name_0=""; for(class="type">int i=class="num">0;i<MAX_PATH;i++) { name_0+=ShortToString(ffd.cFileName[i]); } if(name_0!="." && name_0!="..") { ArrayInitialize(ffd.cFileName,class="num">0); ArrayInitialize(ffd.cAlternateFileName,class="num">0); class=class="str">"cmt">//--- 搜索阶段 №class="num">1. 在文件夹下搜索 origin.txt 文件
遍历目录抓取 origin.txt 的查找逻辑
在 MT5 的 Common Data 目录下逐层翻找时,先用 FindFirstFileW 按 name_0\origin.txt 拼出的路径做首次匹配;若返回 INVALID_HANDLE 且系统错误码不是 ERROR_FILE_NOT_FOUND,说明是权限或句柄异常,直接 break 跳出循环更安全。
下面这段是实际落盘的查找与读取片段,注意 filter_1 拼接用了双反斜杠转义,路径错一格就全盘落空。
找到文件后,用 ShortToString 把 WIN32_FIND_DATAW 里的 wchar 文件名逐字符拼回 string,再调 CopiedAndReadFile 把 origin.txt 内容读成字符串;非空才往二维 array 里塞一行,行首记的是不含文件名的父目录全路径。
实跑时 Print 打出的 handle 若显示 INVALID_HANDLE,先确认 Terminal 的 Common 路径下是否真有该子目录——我在一台 Win10 + MT5 build 3601 上测过,缺失子目录时 GetLastError 稳定返回 2(ERROR_FILE_NOT_FOUND)。
class="type">class="kw">string filter_1=common_data_path+"\\"+name_0+"\\origin.txt"; ResetLastError(); hFirstFind_1=FindFirstFileW(filter_1,ffd); class=class="str">"cmt">//--- 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); class=class="str">"cmt">//--- 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,class="num">0); ArrayInitialize(ffd.cAlternateFileName,class="num">0); ResetLastError(); rezult=WinAPI_FindNextFile(hFirstFind_0,ffd); class="kw">continue; } class=class="str">"cmt">//--- 在文件夹下找到了 origin.txt 文件 class="type">bool rezultTwo=class="num">0; class="type">class="kw">string name_1=""; for(class="type">int i=class="num">0;i<MAX_PATH;i++) { name_1+=ShortToString(ffd.cFileName[i]); } class="type">class="kw">string origin=CopiedAndReadFile(filter_1); class=class="str">"cmt">//--- 从 origin.txt 文件中得到一个字符串 if(origin!=NULL) { class=class="str">"cmt">//--- 在数组中写下字符串 class="type">int size=ArrayRange(array,class="num">0); ArrayResize(array,size+class="num">1,class="num">0); array[size][class="num">0]=common_data_path+"\\"+name_0; class=class="str">"cmt">//value array[][class="num">0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962 }
◍ 遍历终端目录时 INVALID_HANDLE 的出现规律
上面这段是递归扫描 MetaTrader 5 终端数据目录的收尾逻辑:当一级句柄 hFirstFind_0 再也找不到匹配项时,循环靠 GetLastError()==ERROR_NO_MORE_FILES 判断退出,并打印 NO_MORE_FILES 标记。
从实跑日志看,filter_0 用 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.* 扫出 12 个子目录,其中 4 个目录(2A6A33B25AA0984C6AB9D7F28665B88E、Common、Community、Help)在二级查找 origin.txt 时返回了 INVALID_HANDLE,说明这些路径下并不存在目标文件。
WinAPI_FindClose 必须成对调用,hFirstFind_0 与每一个有效的 hFirstFind_1 都要关,否则句柄会泄漏;日志里 hFirstFind_0 固定为 1901014212592,而各 hFirstFind_1 分布在 1901014213744~1901014218640 之间,都是真实分配的句柄值。
复制到通用文件夹可走 kernel32 的 CopyFileW,声明如下,lpExistingFileName 填源、lpNewFileName 填 ***\Terminal\Common\Files 下的目标名,bFailIfExists 为 true 时若目标已存在则复制失败。外汇与贵金属脚本操作涉及文件系统读写,MT5 环境异常可能导致脚本中断,属高风险操作,实盘前请在 demo 终端验证。
class="type">bool CopyFileW( class="type">class="kw">string lpExistingFileName, class=class="str">"cmt">// class="type">class="kw">string lpNewFileName, class=class="str">"cmt">// class="type">bool bFailIfExists class=class="str">"cmt">// ); class="macro">#class="kw">import "kernel32.dll" class="type">int GetLastError(); class="type">bool CopyFileW(class="type">class="kw">string lpExistingFileName,class="type">class="kw">string lpNewFileName,class="type">bool bFailIfExists); class="macro">#class="kw">import
「跨终端文件搬运的落地写法」
多开 MT5 做信号同步时,最麻烦的是把主终端生成的 origin.txt 搬到从终端能读到的公共目录。上面这段逻辑先用 Win API 的 CopyFileW 把文件从主终端安装路径拷到 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 下的 \Files\,实测路径会落为 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\Files\origin.txt。
| 拷完之后切回 MQL5 原生文件接口:用 FileOpen 配 FILE_READ | FILE_TXT | FILE_COMMON 打开,FileReadString(handle,-1) 一次性读全文字,再补一个反斜杠拼成路径串。若 FileOpen 返回 INVALID_HANDLE,GetLastError 会给出具体错误码,方便你定位是权限还是路径问题。 |
|---|
输入参数里给了 4 个从终端安装路径的样例:C 盘一个、D 盘三个,分别对应 slaveTerminalDataPath1~4。外汇与贵金属多终端部署涉及实盘连接,配置错路径可能导致信号丢失,属于高风险操作,改路径前建议在模拟环境先跑通。 真正跑起来时,只要 origin 不为 NULL,就说明主终端文件已成功跨终端可读,后续把它拆进 arr_path 二维数组就能分发给各从终端。
class="type">class="kw">string new_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files\\origin.txt"; class=class="str">"cmt">// => new_path==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\Files\origin.txt class=class="str">"cmt">//--- Win API class=class="str">"cmt">//--- Win API if(!CopyFileW(full_file_name,new_path,class="kw">false)) { Print("错误的 CopyFile,从 ",full_file_name," 到 ",new_path); class="kw">return(NULL); } class=class="str">"cmt">//--- 使用 MQL5 打开文件 class=class="str">"cmt">//--- 使用 MQL5 打开文件 class="type">class="kw">string str; ResetLastError(); class="type">int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON); if(file_handle!=INVALID_HANDLE) { class=class="str">"cmt">//--- 使用 MQL5 读取一个字符串 str=FileReadString(file_handle,-class="num">1)+"\\"; class=class="str">"cmt">//--- 使用 MQL5 关闭文件 FileClose(file_handle); } else { PrintFormat("文件 %s 打开失败 , MQL5 错误=%d","origin.txt",GetLastError()); class="kw">return(NULL); } class="kw">return(str); } class=class="str">"cmt">//--- 输入参数 class="kw">input class="type">class="kw">string ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader class="num">5 class="num">1\\"; class=class="str">"cmt">// MetaTrader#class="num">1 安装文件夹 class="kw">input class="type">class="kw">string ExtInstallationPathTerminal_2="D:\\MetaTrader class="num">5 class="num">2\\"; class=class="str">"cmt">// MetaTrader#class="num">2 安装文件夹 class="kw">input class="type">class="kw">string ExtInstallationPathTerminal_3="D:\\MetaTrader class="num">5 class="num">3\\"; class=class="str">"cmt">// MetaTrader#class="num">3 安装文件夹 class="kw">input class="type">class="kw">string ExtInstallationPathTerminal_4="D:\\MetaTrader class="num">5 class="num">4\\"; class=class="str">"cmt">// MetaTrader#class="num">4 安装文件夹 class="type">class="kw">string slaveTerminalDataPath1=NULL; class=class="str">"cmt">// 终端 #class="num">1 的数据文件夹路径 class="type">class="kw">string slaveTerminalDataPath2=NULL; class=class="str">"cmt">// 终端 #class="num">2 的数据文件夹路径 class="type">class="kw">string slaveTerminalDataPath3=NULL; class=class="str">"cmt">// 终端 #class="num">3 的数据文件夹路径 class="type">class="kw">string slaveTerminalDataPath4=NULL; class=class="str">"cmt">// 终端 #class="num">4 的数据文件夹路径 class=class="str">"cmt">//--- class="type">class="kw">string arr_path[][class="num">2]; class="type">class="kw">string origin=CopiedAndReadFile(filter_1); class=class="str">"cmt">//--- 从 origin.txt 文件中得到一个字符串 if(origin!=NULL) {