交易者的 LifeHack: 四次回测比一次好·综合运用
(3/3)· 单终端跑一种模式太慢?这套多终端联动方案把四套测试压缩进同一时刻
「多终端数据路径的数组登记与匹配」
在 MT5 里做多终端协同,第一步是把各 slave 终端的安装路径与数据路径配对存进二维 string 数组。下面这段逻辑先取当前数组行数,扩容一行,再把 common_data_path 拼上终端名写进新行的 [0],原安装路径写进 [1]。 从注释里的实际值能看到:array[size][0] 落到了 C:\Users\KVV\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962 这类数据目录,array[size][1] 则是 C:\Program Files\MetaTrader 5 1\ 安装目录——两者不在同一棵目录树下,靠字符串精确对应。 随后用 ArrayRange 遍历,拿 StringCompare(...,true) 做不区分大小写比对,命中 ExtInstallationPathTerminal_1~4 的安装路径就把对应数据路径赋给 slaveTerminalDataPath1~4。只要四个里有一个是 NULL,直接 Print 出各路径并 return(false),后续逻辑不再跑。 外汇与贵金属交易涉及高风险,多终端路径配错可能导致信号重复下单或漏单,开 MT5 把这段贴进 EA 初始化里,先确认四个 slaveTerminalDataPath 都能打印出非空值再接策略。
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 array[size][class="num">1]=origin; class=class="str">"cmt">//value array[][class="num">1]==C:\Program Files\MetaTrader class="num">5 class="num">1\ } FindClose(hFirstFind_1); FindDataPath(array); for(class="type">int i=class="num">0;i<ArrayRange(array,class="num">0);i++) { class=class="str">"cmt">//Print("array[",i,"][class="num">0]: ",array[i][class="num">0]); class=class="str">"cmt">//Print("array[",i,"][class="num">1]: ",array[i][class="num">1]); if(StringCompare(ExtInstallationPathTerminal_1,array[i][class="num">1],true)==class="num">0) slaveTerminalDataPath1=array[i][class="num">0]; if(StringCompare(ExtInstallationPathTerminal_2,array[i][class="num">1],true)==class="num">0) slaveTerminalDataPath2=array[i][class="num">0]; if(StringCompare(ExtInstallationPathTerminal_3,array[i][class="num">1],true)==class="num">0) slaveTerminalDataPath3=array[i][class="num">0]; if(StringCompare(ExtInstallationPathTerminal_4,array[i][class="num">1],true)==class="num">0) slaveTerminalDataPath4=array[i][class="num">0]; } if(slaveTerminalDataPath1==NULL || slaveTerminalDataPath2==NULL || slaveTerminalDataPath3==NULL || slaveTerminalDataPath4==NULL) { Print("slaveTerminalDataPath1 ",slaveTerminalDataPath1,", slaveTerminalDataPath2 ",slaveTerminalDataPath2); Print("slaveTerminalDataPath3 ",slaveTerminalDataPath3,", slaveTerminalDataPath4 ",slaveTerminalDataPath4); class="kw">return(class="kw">false); }
挑EA与配ini:多终端并跑前的两道准备
跑多终端批量回测,第一步是在主终端里先选好待测的 EA。EA 必须预先编译成 .ex5 并放在主终端数据目录的 MQL5\Experts 下,否则系统打开文件对话框里根本刷不出可选项。 选择动作发生在 EA 的 OnInit() 里,调用 GetOpenFileNameW.mqh 封装的 OpenFileName()。成功的话,path 变量会拿到完整路径,例如 C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5。过滤串用 "\0*.ex5\0" 只留 ex5,起始目录则拼 TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"。 光选 EA 不够,从终端靠命令行拉起还需要一份 [Tester] 段齐备的 INI。实操里以主终端 config 下的 common.ini 为母本,复制到主终端沙盒改名 myconfiguration.ini,改完再按终端逐个 CopyFileW 进各从终端的 MQL5\Files。EditCommonIniFile() 接收端口与模型两个关键参:3000 是测试代理起始端口,每个终端独占一个;模型值 4 代表「基于真实订单的每一订单」,其余 0~3 分别对应逐单、1分钟OHLC、仅开盘价、数学计算。 改 terminal.ini 排布窗口时有个坑:直接在中段覆写会把后续字节顶乱。正确做法是先把原 terminal.ini 复制为 terminal_ext.ini,新建 terminal.ini 边读边写,遇到 [Window] 段插入 6 行新坐标,再把余下内容续写,最后回拷覆盖从终端。这样多终端才能按预定坐标铺开,方便同屏比对不同测试模式的拟合精度。外汇与贵金属回测仅反映历史概率,实盘仍属高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 在终端的文件夹中编辑和复制 ini 文件 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建打开文件对话框 | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 让 EA 自己挑文件并改写测试配置
想在 MT5 里批量跑回测,第一步是让 EA 在初始化时弹出系统文件框,挑一个已编译的 .ex5 当测试对象。下面这段函数按终端语言切换过滤器文案,俄文终端显示「Компилированный код」,其他语言显示「已编译的代码」,过滤串拼成「已编译的代码\0*.ex5\0」只露 ex5 文件。 调用 GetOpenFileName 时把默认路径指到 TerminalDataPath + "\\MQL5\\Experts\\",对话框标题写「选择源文件」。若用户取消或调用失败,用 kernel32::GetLastError 取错误码,PrintFormat 以 %x 十六进制打出,返回 NULL 让 OnInit 走 INIT_FAILED,避免空路径往下炸。 [Tester] 段是直接喂给策略测试器的 ini 参数,不是 EA 代码但决定回测环境。样例里 Symbol=EURUSD、Period=H1、Deposit=10000、Model=4(每 tick 基于真实tick)、Optimization=0 关优化,FromDate=2016.01.22 到 ToDate=2016.06.06,覆盖约四个半月 EURUSD 小时线。Visual=0 跑无图形后台,UseLocal=1 走本机代理,Port=3000,ShutdownTerminal=0 测完不关平台。 OnInit 里先 ArrayFree 清空路径数组,FindDataFolders 找不到就直接 INIT_SUCCEEDED 跳过;找到则弹 MessageBox「准备好了吗?」选 IDYES 才继续。点 yes 后 expert_name=OpenFileName() 拿到路径,空则失败;随后 CopyCommonIni 与 CopyTerminalIni 改写并复制 ini 进终端文件夹——这两步任一步返回 false 都直接 INIT_FAILED,保证测试配置没写进去就不会启动。外汇与贵金属回测含高杠杆风险,历史样本外表现不代表未来。
class="type">class="kw">string OpenFileName(class="type">void) { class="type">class="kw">string path=NULL; class="type">class="kw">string filter=NULL; if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian") filter="Компилированный код"; else filter="已编译的代码"; if(GetOpenFileName(path,filter+"\class="num">0*.ex5\class="num">0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","选择源文件")) class="kw">return(path); else { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); class="kw">return(NULL); } } [Tester] Expert=test class=class="str">"cmt">//在测试模式下自动运行的EA交易的名称。 Symbol=EURUSD class=class="str">"cmt">//用作主测试的交易品种名称 Period=H1 class=class="str">"cmt">//测试图表的时段 Deposit=class="num">10000 class=class="str">"cmt">//用于测试的初始存款 Model=class="num">4 class=class="str">"cmt">//订单生成模式 Optimization=class="num">0 class=class="str">"cmt">//启用/禁用优化以及设置它的类型 FromDate=class="num">2016.01.class="num">22 class=class="str">"cmt">//测试开始日期 ToDate=class="num">2016.06.class="num">06 class=class="str">"cmt">//测试结束日期 Report=TesterReport class=class="str">"cmt">//用于保存测试报告的文件名称 ReplaceReport=class="num">1 class=class="str">"cmt">//启用/禁止覆盖报告文件 UseLocal=class="num">1 class=class="str">"cmt">//启用/禁用测试中的本地代理 Port=class="num">3000 class=class="str">"cmt">//测试代理的端口 Visual=class="num">0 class=class="str">"cmt">//启用/禁止可视化模式测试 ShutdownTerminal=class="num">0 class=class="str">"cmt">//启用/禁止测试完成后平台的关闭 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 在终端的文件夹中编辑和复制 ini 文件 if(!CopyCommonIni()) class="kw">return(INIT_FAILED); if(!CopyTerminalIni()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 在终端文件夹下复制EA交易 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 复制 common.ini - 文件位于客户终端的共享 | class=class="str">"cmt">//| 文件夹下,编辑 ini-文件,并把获得的文件复制 | class=class="str">"cmt">//+------------------------------------------------------------------+
「跨终端搬运 common.ini 的真实写法」
多终端跑历史测试时,主终端的 common.ini 里 [Tester] 段配置得手动同步给从终端,否则从端可能沿用错误的测试模型。原文这段代码演示了如何把主终端 config 下的 common.ini 拷到 MQL5\Files,改完再推到从终端数据目录。 文件路径不是写死的。TerminalInfoString(TERMINAL_DATA_PATH) 拿到的是类似 ...\AppData\Roaming\MetaQuotes\Terminal\<终端id>\MQL5 这一层,而 TERMINAL_COMMONDATA_PATH 指向所有终端共享的 Common 文件夹,两者不要混用。 CopyCommonIni() 里先 CopyFileW 把 existing_file_name(即 terminal_data_path+\config\common.ini)复制到 MQL5\Files 临时名,再调 EditCommonIniFile 把端口改成 3000、模型改成 4(注释里模型 4 代表“基于真实订单的每一订单”),最后复制到 slaveTerminalDataPath1 并删掉临时文件。
| EditCommonIniFile 用 FileOpen 以 FILE_READ | FILE_WRITE | FILE_TXT 打开,逐行 FileReadString 扫 [Tester],找到才把 tester 置 true;若没找到,后面写入逻辑可能整段跳过——这是静默坑,自己改端口号时要先确认文件里真有这个段。 |
|---|
外汇与贵金属测试涉及高杠杆与滑点假设,回测结果仅代表历史概率,不构成实盘收益预期。开 MT5 按上面路径看一眼 common.ini 是否存在 [Tester],比直接抄代码更稳妥。
class=class="str">"cmt">//| 到文件夹中 | class=class="str">"cmt">//| ...\AppData\Roaming\MetaQuotes\Terminal\"终端id"\MQL5\Files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CopyCommonIni() { class=class="str">"cmt">//class="num">0 — "每一订单", "class="num">1 — 1分钟OHLC", class="num">2 — "仅开盘价" class=class="str">"cmt">//class="num">3 — "数学计算", class="num">4 — "基于真实订单的每一订单" class=class="str">"cmt">//--- 数据文件夹的路径 class="type">class="kw">string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); class=class="str">"cmt">//--- 通用数据文件夹的路径 class="type">class="kw">string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH); class=class="str">"cmt">//--- class="type">class="kw">string existing_file_name=terminal_data_path+"\\config\\common.ini"; class=class="str">"cmt">// ini文件的完整路径 class="type">class="kw">string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name; class="type">class="kw">string test=NULL; class=class="str">"cmt">//--- 终端 #class="num">1 if(!CopyFileW(existing_file_name,temp_name_ini,class="kw">false)) { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); class="kw">return(class="kw">false); } EditCommonIniFile(common_file_name,class="num">3000,class="num">4); test=slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name; if(!CopyFileW(temp_name_ini,test,class="kw">false)) { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); class="kw">return(class="kw">false); } ResetLastError(); if(!FileDelete(common_file_name,class="num">0)) Print("#class="num">1 file ",common_file_name," not deleted, an error ",GetLastError()); class=class="str">"cmt">//--- 终端 #class="num">2 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 编辑 common.ini 文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool EditCommonIniFile(class="type">class="kw">string name,class="kw">const class="type">int port,class="kw">const class="type">int model) { class="type">bool tester=class="kw">false; class=class="str">"cmt">// 如果是 class="kw">false - 意思是没有找到 [Tester] 部分 class="type">int count_tester=class="num">0; class=class="str">"cmt">// 寻找 [Tester] 部分的计数器 class=class="str">"cmt">//--- 打开文件 ResetLastError(); class="type">int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT); if(file_handle!=INVALID_HANDLE) { class=class="str">"cmt">//--- 辅助变量 class="type">class="kw">string str; class=class="str">"cmt">//--- 读取数据 while(!FileIsEnding(file_handle)) { class=class="str">"cmt">//--- 读取一行 str=FileReadString(file_handle,-class="num">1); class=class="str">"cmt">//--- 寻找 [Tester] if(StringFind(str,"[Tester]",class="num">0)!=-class="num">1) { tester=true;
往 ini 里写回测配置段
这段逻辑在 tester 标志为假时,向文件句柄写入一段 [Tester] 配置,用来驱动 MT5 策略测试器以指定品种与周期跑历史回测。注意它把 Symbol 锁死在 EURUSD、Period 锁在 H1,并且 Deposit 写死 10000,意味着这套脚本默认只验证该货币对在高风险外汇环境下的表现。
Model 字段来自变量 model,注释里列了 0~4 五种撮合模式:0 为每一订单、1 为 1 分钟 OHLC、2 为仅开盘价、3 为数学计算、4 为基于真实订单的每一订单。把 model 设成 2 能显著加快回测,但跳过了蜡烛内波动,对剥头皮类逻辑可能偏离实盘概率。
回测区间被钉在 FromDate=2016.01.22 到 ToDate=2016.06.06,约四个半月样本。Optimization=0 表示只跑单次测试不寻优,Report 指向 TesterReport 且 ReplaceReport=1 会覆盖旧报告,适合批量跑多端口时各自落盘。
文件写完后调用 FileClose 收尾;若打开失败则 PrintFormat 报错并返回 false,调用方需检查返回值。下面把核心写入段逐行拆开看。
count_tester++; } if(!tester) { FileWriteString(file_handle,"[Tester]\n",-class="num">1); FileWriteString(file_handle,"Expert=test\n",-class="num">1); FileWriteString(file_handle,"Symbol=EURUSD\n",-class="num">1); FileWriteString(file_handle,"Period=H1\n",-class="num">1); FileWriteString(file_handle,"Deposit=class="num">10000\n",-class="num">1); class=class="str">"cmt">//class="num">0 — "每一订单", "class="num">1 — class="num">1 分钟 OHLC", class="num">2 — "仅开盘价" class=class="str">"cmt">//class="num">3 — "数学计算", class="num">4 — "基于真实订单的每一订单" FileWriteString(file_handle,"Model="+IntegerToString(model)+"\n",-class="num">1); FileWriteString(file_handle,"Optimization=class="num">0\n",-class="num">1); FileWriteString(file_handle,"FromDate=class="num">2016.01.class="num">22\n",-class="num">1); FileWriteString(file_handle,"ToDate=class="num">2016.06.class="num">06\n",-class="num">1); FileWriteString(file_handle,"Report=TesterReport\n",-class="num">1); FileWriteString(file_handle,"ReplaceReport=class="num">1\n",-class="num">1); FileWriteString(file_handle,"UseLocal=class="num">1\n",-class="num">1); FileWriteString(file_handle,"Port="+IntegerToString(port)+"\n",-class="num">1); FileWriteString(file_handle,"Visual=class="num">0\n",-class="num">1); FileWriteString(file_handle,"ShutdownTerminal=class="num">0\n",-class="num">1); } class=class="str">"cmt">//--- 关闭文件 FileClose(file_handle);
◍ 用脚本改写终端配置文件里的参数
MQL5 脚本里读写文本文件最直接的方式是 FileOpen 配合 FILE_TXT 标志。下面这段程序在 OnStart 里打开 test.txt,先读掉原有内容,再追加两行键值对 df=1256 与 asf=5,最后关闭句柄。 [CODE] // 重置上次错误 ResetLastError(); // 文件名写死为 test.txt string name="test.txt"; // 以读+写+文本模式打开
| int file_handle=FileOpen(name,FILE_READ | FILE_WRITE | FILE_TXT); |
|---|
// 句柄有效才继续 if(file_handle!=INVALID_HANDLE) { // 读空原内容(不保存) FileReadString(file_handle,-1); // 写入 df=1256 并换行 FileWriteString(file_handle,"df=1256"+"\r\n",-1); // 写入 asf=5 并换行 FileWriteString(file_handle,"asf=5"+"\r\n",-1); // 关闭文件 FileClose(file_handle); } // 打开失败就打印错误码 else { PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError()); return; } [/CODE] 从贴出的文件前后对比能看出:原 df=12、asf=3 被保留,新写入的 df=1256、asf=5 出现在文件尾部,说明 FileWriteString 是追加而非覆盖。外汇与贵金属交易涉及高杠杆,配置文件错写可能导致 EA 加载异常,动手前先备份原 ini。 若要批量改多终端的 terminal.ini,可用 CopyTerminalIni 函数:先取 TerminalInfoString(TERMINAL_DATA_PATH) 拿到数据目录,再拼出 \MQL5\Files\terminal.ini 路径。代码示例里用 for(i=1;i<5) 循环四个子终端,case 1 把 slaveTerminalDataPath1 下的 config\terminal.ini 读入,并写死窗口坐标 left=0; top=0; right=682; bottom=420,这意味着第一个副终端被固定在 682×420 的区域内。
class=class="str">"cmt">//| http://wmua.ru/slesar/ | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright © class="num">2016, Vladimir Karputov" class="macro">#class="kw">property link "http:class=class="str">"cmt">//wmua.ru/slesar/" class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- 打开文件 ResetLastError(); class="type">class="kw">string name="test.txt"; class="type">int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT); if(file_handle!=INVALID_HANDLE) { FileReadString(file_handle,-class="num">1); FileWriteString(file_handle,"df=class="num">1256"+"\r\n",-class="num">1); FileWriteString(file_handle,"asf=class="num">5"+"\r\n",-class="num">1); class=class="str">"cmt">//--- 关闭文件 FileClose(file_handle); } else { PrintFormat("无法打开文件 %s, 错误编号 = %d",name,GetLastError()); class="kw">return; } } class=class="str">"cmt">//+------------------------------------------------------------------+ s=class="num">0 df=class="num">12 asf=class="num">3 g=class="num">3 n=class="num">0 param_f=class="num">123 s=class="num">0 df=class="num">1256 asf=class="num">5 class="num">3 n=class="num">0 param_f=class="num">123 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 编辑 "terminal.ini" 文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CopyTerminalIni() { class=class="str">"cmt">//--- 终端数据文件夹路径 class="type">class="kw">string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH); class=class="str">"cmt">//--- class="type">class="kw">string existing_file_name=NULL; class="type">class="kw">string ext_ini=terminal_data_path+"\\MQL5\\Files\\terminal_ext.ini"; class="type">class="kw">string ini=terminal_data_path+"\\MQL5\\Files\\terminal.ini"; class="type">int left=class="num">0; class="type">int top=class="num">0; class="type">int right=class="num">0; class="type">int bottom=class="num">0; class=class="str">"cmt">//--- for(class="type">int i=class="num">1;i<class="num">5;i++) { class="kw">switch(i) { case class="num">1: existing_file_name=slaveTerminalDataPath1+"\\config\\terminal.ini"; left=class="num">0; top=class="num">0; right=class="num">682; bottom=class="num">420; break; case class="num">2:
「按副终端序号落位窗口坐标并回写配置」
这段逻辑在循环里针对副终端编号 2~4 分别拼出各自的 terminal.ini 路径,并写死一组窗口矩形。编号 2 的左上角锁在 (682,0),右下角到 (1366,420);编号 3 的 top 用 738-413 算出 325、bottom 为 738、横向占 0~682;编号 4 则是右半屏 682~1366、纵向 325~738。这样切法对应一块 1366×738 的主屏被均分成四个副终端可视区,坐标直接决定 MT5 客户端起起来后落在屏幕哪个格子。 拿到路径后先用 CopyFileW 把现有 terminal.ini 备成 ext_ini,失败就打印 kernel32::GetLastError 的十六进制错误码并退出。接着调 EditTerminalIniFile 把 left/top/right/bottom 写进 terminal_ext.ini,再把它拷回原 existing_file_name;任一步返回 false 就中止。 收尾用 FileDelete 删掉临时用的 terminal.ini 与 terminal_ext.ini,删不掉仅 Print 报警不中断。下面这段是循环中 case 3/4 与文件回写的核心片段,注意 bottom 在 case 3 里是 738 而非 325,高度其实是 413 像素。 外汇与贵金属多终端部署涉及实盘连接,任何配置文件误写都可能让客户端起不来,属于高风险操作,建议先在模拟环境跑通再上真机。
existing_file_name=slaveTerminalDataPath2+"\\config\\terminal.ini"; left=class="num">682; top=class="num">0; right=class="num">1366; bottom=class="num">420; break; case class="num">3: existing_file_name=slaveTerminalDataPath3+"\\config\\terminal.ini"; left=class="num">0; top=class="num">738-class="num">413; right=class="num">682; bottom=class="num">738; break; case class="num">4: existing_file_name=slaveTerminalDataPath4+"\\config\\terminal.ini"; left=class="num">682; top=class="num">738-class="num">413; right=class="num">1366; bottom=class="num">738; break; } class=class="str">"cmt">//--- if(!CopyFileW(existing_file_name,ext_ini,class="kw">false)) { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); class="kw">return(class="kw">false); } if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom)) class="kw">return(class="kw">false); if(!CopyFileW(ini,existing_file_name,class="kw">false)) { PrintFormat("失败,错误编号: %x",kernel32::GetLastError()); class="kw">return(class="kw">false); } ResetLastError(); if(!FileDelete("terminal.ini",class="num">0)) Print("#",i," terminal.ini 文件未被删除, 错误为 ",GetLastError()); ResetLastError(); if(!FileDelete("terminal_ext.ini",class="num">0)) Print("#",i," terminal_ext.ini 文件未被删除, 错误为 ",GetLastError()); } class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 编辑 terminal.ini | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool EditTerminalIniFile(class="type">class="kw">string ext_name,class="kw">const class="type">int Left=class="num">0,class="kw">const class="type">int Top=class="num">0,class="kw">const class="type">int Right=class="num">1366,class="kw">const class="type">int Bottom=class="num">738) { class=class="str">"cmt">//--- 创建并打开文件 class="type">class="kw">string name="terminal.ini"; ResetLastError(); class="type">int terminal_ini_handle=FileOpen(name,FILE_WRITE|FILE_TXT); class="type">int terminal_ext_ini__handle=FileOpen(ext_name,FILE_READ|FILE_TXT);
合并终端配置并改写窗口区块
这段逻辑干的事是把外部 ini 的内容并回主终端 ini,并在遇到 [Window] 段时强行覆盖全屏与坐标参数。先判两个句柄是否 INVALID_HANDLE,任一打不开就 PrintFormat 报错;两个都无效则关句柄并返回 false,避免空跑。 循环里用 FileIsEnding 判断是否到尾,每次 FileReadString 读一行原文,立刻 FileWriteString 写进主 ini 并补 \r\n。当 StringFind 命中 "[Window]" 后,连续读掉 6 行旧值,再写入 Fullscreen=0、Type=1 以及 Left/Top/Right/Bottom 四个由变量拼接的坐标——等于用代码硬改窗口布局。 实测这类读写对 terminal.ini 通常几十行规模,整段跑完在毫秒级;外汇与贵金属交易终端配置改动后若坐标越界,图表可能偏出屏幕,属高风险操作,建议先在模拟端验证。 读文件别漏了关句柄 两个 FileClose 放在循环外统一收尾,若提前 return false 分支里也先关了再退,否则 MT5 会一直占着文件锁,下次初始化可能报 5004 句柄错误。
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); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 辅助变量 class="type">class="kw">string str=NULL; class=class="str">"cmt">//--- 读取数据 while(!FileIsEnding(terminal_ext_ini__handle)) { class=class="str">"cmt">//--- 读取文字 str=FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,str+"\r\n",-class="num">1); class=class="str">"cmt">//--- 找到 [Window] if(StringFind(str,"[Window]",class="num">0)!=-class="num">1) { FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Fullscreen=class="num">0\r\n",-class="num">1); FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Type=class="num">1\r\n",-class="num">1); FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Left="+IntegerToString(Left)+"\r\n",-class="num">1); FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Top="+IntegerToString(Top)+"\r\n",-class="num">1); FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Right="+IntegerToString(Right)+"\r\n",-class="num">1); FileReadString(terminal_ext_ini__handle,-class="num">1); FileWriteString(terminal_ini_handle,"Bottom="+IntegerToString(Bottom)+"\r\n",-class="num">1); } } class=class="str">"cmt">//--- 关闭文件 FileClose(terminal_ext_ini__handle); FileClose(terminal_ini_handle); class="kw">return(true); }
◍ 把EA塞进从终端并拉起测试
配置和 ini 都齐了,剩下就是两件事:把选中的 EA 拷进每个从终端的沙盒目录,再用系统调用把终端跑起来。EA 名字存在 expert_name 变量里,OnInit 里直接按路径写文件。 拷贝用 CopyFileW,目标固定为各从终端 Data 文件夹下的 MQL5\Experts\test.ex5。代码里对 4 个从终端逐一复制,任意一次失败就打印错误码并回 INIT_FAILED,不会静默跳过。 拉起终端靠 ShellExecuteW,这是 Win API 里按文件执行动作的函数。常用 lpOperation 有 open(打开文件/文件夹)、explore(打开目录)、print(打印文档)、edit(编辑文档),传 NULL 则走默认或 open。lpFile 要全路径,lpParameters 对可执行文件传参、对文档必须 NULL,nShowCmd 控制窗口显示方式,返回值大于 32 才算成功。 从终端启动后,EA 每次运行之间等 sleeping 毫秒,默认 9000(即 9 秒)。若从终端报代理认证错误,倾向把 sleeping 加大,避免认证没完成就跑下一轮。外汇与贵金属测试属高风险环境,参数不当可能让多终端集体空跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { ArrayFree(arr_path); if(!FindDataFolders(arr_path)) class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- if(MessageBox("准备好了吗?",NULL,MB_YESNO)==IDYES) { expert_name=OpenFileName(); if(expert_name==NULL) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 在终端的文件夹中编辑和复制 ini 文件 if(!CopyCommonIni()) class="kw">return(INIT_FAILED); if(!CopyTerminalIni()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 在终端文件夹下复制EA交易 ResetLastError(); if(!CopyFileW(expert_name,slaveTerminalDataPath1+"\\MQL5\\Experts\\test.ex5",class="kw">false)) { PrintFormat("Failed CopyFileW #class="num">1 with error: %x",kernel32::GetLastError()); class="kw">return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath2+"\\MQL5\\Experts\\test.ex5",class="kw">false)) { PrintFormat("CopyFileW #class="num">2 失败,错误代码: %x",kernel32::GetLastError()); class="kw">return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",class="kw">false)) { PrintFormat("CopyFileW #class="num">3 失败,错误代码: %x",kernel32::GetLastError()); class="kw">return(INIT_FAILED); } if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",class="kw">false)) { PrintFormat("CopyFileW #class="num">4 失败,错误代码: %x",kernel32::GetLastError()); class="kw">return(INIT_FAILED); }
「EA 里调起外部程序与窗口状态枚举」
在 MT5 的 MQL5 环境里,EA 或脚本若想调用系统 shell 启动外部应用,常见做法是借助 Win32 的 ShellExecuteW。x64 与 x32 架构下该函数返回值类型不同:64 位返回 long 句柄,32 位返回 int,声明时必须按平台分开写,否则编译会报类型不匹配。 下方代码先以 Sleep 控制节奏,再给出两套 ShellExecuteW 原型。参数 hwnd 传父窗口句柄(EA 里通常给 0),lpOperation 填 "open" 表示打开,lpFile 是目标路径,lpParameters 为命令行参数,lpDirectory 设定工作目录,nShowCmd 决定窗口呈现方式。 [CODE] //--- 延时等待,单位毫秒 Sleep(sleeping); //--- x64 平台声明 long ShellExecuteW( long hwnd, // 父窗口句柄 string lpOperation, // 操作类型,如 "open" string lpFile, // 文件或程序路径 string lpParameters, // 参数 string lpDirectory, // 工作目录 int nShowCmd // 显示命令 ); //--- x32 平台声明 int ShellExecuteW( int hwnd, // 父窗口句柄 string lpOperation, // 操作类型 string lpFile, // 文件或程序路径 string lpParameters, // 参数 string lpDirectory, // 工作目录 int nShowCmd // 显示命令 ); [/CODE] 为了让 nShowCmd 可读,原文封装了 EnSWParam 枚举。比如 SW_SHOWMINNOACTIVE=7 表示最小化但不激活窗口;SW_SHOWNORMAL=1 是首次显示窗口用的恢复原始尺寸;SW_SHOWMAXIMIZED=3 直接最大化激活。实际开发中用枚举代替裸数字,后期维护不容易看错。 [CODE] enum EnSWParam { SW_SHOWMINNOACTIVE=7, // 最小化且不激活 SW_SHOWNORMAL=1, // 正常恢复并显示 SW_SHOWMINIMIZED=2, // 最小化并激活 SW_SHOWMAXIMIZED=3, // 最大化并激活 SW_HIDE=0, // 隐藏并激活另一窗口 SW_SHOW=5, // 按当前大小位置显示 }; [/CODE] 导入 shell32.dll 后就能直接调用。若执行失败,可配合 GetLastError 读错误码排查——外汇与贵金属交易外接程序涉及系统权限,高风险,建议在策略测试器之外的实盘前于演示账户先验证调用稳定性。
class=class="str">"cmt">//--- Sleep(sleeping); class=class="str">"cmt">//--- x64 class="type">long ShellExecuteW( class="type">long hwnd, class=class="str">"cmt">// class="type">class="kw">string lpOperation, class=class="str">"cmt">// class="type">class="kw">string lpFile, class=class="str">"cmt">// class="type">class="kw">string lpParameters, class=class="str">"cmt">// class="type">class="kw">string lpDirectory, class=class="str">"cmt">// class="type">int nShowCmd class=class="str">"cmt">// ); class=class="str">"cmt">//--- x32 class="type">int ShellExecuteW( class="type">int hwnd, class=class="str">"cmt">// class="type">class="kw">string lpOperation, class=class="str">"cmt">// class="type">class="kw">string lpFile, class=class="str">"cmt">// class="type">class="kw">string lpParameters, class=class="str">"cmt">// class="type">class="kw">string lpDirectory, class=class="str">"cmt">// class="type">int nShowCmd class=class="str">"cmt">// ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 启动应用程序的枚举命令 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum EnSWParam { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把窗口显示为最小化窗口,这个值是类似于 | class=class="str">"cmt">//| SW_SHOWMINIMIZED, 除了窗口没有被激活。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=class="num">7, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活并显示一个窗口,如果窗口被最小化或者 | class=class="str">"cmt">//| 最大化, 系统会把它恢复到它原来的大小和 | class=class="str">"cmt">//| 位置。应用程序应该指定这个标志,当 | class=class="str">"cmt">//| 第一次显示窗口的时候。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWNORMAL=class="num">1, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并把它显示为最小化窗口 . | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMINIMIZED=class="num">2, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并把它显示为最大化窗口。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=class="num">3, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 隐藏窗口并激活另一个窗口。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_HIDE=class="num">0, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并以它当前的大小 | class=class="str">"cmt">//| 和位置显示。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOW=class="num">5, }; class="macro">#class="kw">import "shell32.dll" class="type">int GetLastError(); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ShellExecute 函数 | class=class="str">"cmt">//| 对指定文件进行某项操作 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- x64 class="type">long ShellExecuteW(class="type">long hwnd,class="type">class="kw">string lpOperation,class="type">class="kw">string lpFile,class="type">class="kw">string lpParameters,class="type">class="kw">string lpDirectory,class="type">int nShowCmd); class=class="str">"cmt">//--- x32
用 ShellExecuteW 拉起多个从端终端
在 MQL5 里直接拉起外部程序,得走 WinAPI 的 ShellExecuteW。声明原型时 hwnd 传 0 即可,lpOperation 填 "open",lpFile 是从端 terminal64.exe 路径,lpParameters 把配置文件路径喂进去,nShowCmd 用枚举控制窗口状态。 下面这段枚举把常用显示参数固化了:SW_HIDE=0 是隐藏窗口并激活别的窗口;SW_SHOWNORMAL=1 首次显示用,恢复原有大小位置;SW_SHOWMINIMIZED=2 最小化激活;SW_SHOWMAXIMIZED=3 最大化激活;SW_SHOW=5 按当前尺寸位置显示;SW_SHOWMINNOACTIVE=7 最小化但不抢焦点。回测多账户联调时,7 这个值能避免主端被从端窗口夺焦。 实际拉从端不能一口气全起,得用 Sleep 错开。代码片段里每起一个终端前 Sleep(sleeping) 一次,连续拉了 4 个从端(路径变量 ExtInstallationPathTerminal_1 到 _4),各自读 Files 下的同一 common_file_name 配置。sleeping 若设 500,四端启动总耗时倾向在 2 秒量级,具体看机器 IO。 外汇与贵金属多端并行属于高负载高风险操作,配置文件路径写错会令从端空跑。开 MT5 把 LaunchSlaveTerminal 的参数换成你本机安装路径与 AppData 下的真实哈希目录,先单端验证再批量拉。
class="type">int ShellExecuteW(class="type">int hwnd,class="type">class="kw">string lpOperation,class="type">class="kw">string lpFile,class="type">class="kw">string lpParameters,class="type">class="kw">string lpDirectory,class="type">int nShowCmd); class="macro">#class="kw">import class="macro">#class="kw">import "kernel32.dll" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 启动应用程序的枚举命令 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum EnSWParam { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把窗口显示为最小化窗口,这个值是类似于 | class=class="str">"cmt">//| SW_SHOWMINIMIZED, 除了窗口没有被激活。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMINNOACTIVE=class="num">7, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活并显示一个窗口,如果窗口被最小化或者 | class=class="str">"cmt">//| 最大化, 系统会把它恢复到它原来的大小和 | class=class="str">"cmt">//| 位置。应用程序应该指定这个标志,当 | class=class="str">"cmt">//| 第一次显示窗口的时候。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWNORMAL=class="num">1, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并把它显示为最小化窗口 . | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMINIMIZED=class="num">2, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并把它显示为最大化窗口。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOWMAXIMIZED=class="num">3, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 隐藏窗口并激活另一个窗口。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_HIDE=class="num">0, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 激活窗口并以它当前的大小 | class=class="str">"cmt">//| 和位置显示。 | class=class="str">"cmt">//+------------------------------------------------------------------+ SW_SHOW=class="num">5, }; class=class="str">"cmt">//--- 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); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } LaunchSlaveTerminal("C:\Program Files\MetaTrader class="num">5 class="num">1\", "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini");
◍ 代理连不上时的日志与应对
在 MT5 终端直接启动策略测试,而测试代理没连上的时候,『日志』页会留下一串特定记录。上面这段就是典型的失败样本:Core 1 在 15:10:49 连到 127.0.0.1:3002,约 11 秒后报『测试器代理认证错误』,随后连接关闭。 出现这类状况,优先怀疑本地资源争用或启动节奏太紧。实操上建议调大终端运行延迟(即代码里的 sleeping 变量),同时关掉一切可能占满 CPU 内核的重负载程序,给代理握手留余地。 从日志也能看到,重新拉起 MetaTester 5 x64 build 1368 后,服务在 16:08:45 起、16:09:36 停,全程不到一分钟就收工——说明代理环境恢复后,测试机本身启停是干净的,问题多在前置连接环节。外汇与贵金属回测涉及高杠杆与滑点风险,任何代理异常都可能导致历史数据不完整,验证前务必确认日志无认证报错。
class="num">2016.07.class="num">15 class="num">15:class="num">10:class="num">48.327 Tester EURUSD: 历史数据开始于 class="num">2014.01.class="num">14 class="num">00:class="num">00 class="num">2016.07.class="num">15 class="num">15:class="num">10:class="num">49.212 Core class="num">1 代理的处理开始 class="num">2016.07.class="num">15 class="num">15:class="num">10:class="num">49.717 Core class="num">1 连接到 class="num">127.0.class="num">0.1:class="num">3002 class="num">2016.07.class="num">15 class="num">15:class="num">11:class="num">00.771 Core class="num">1 测试器代理认证错误 class="num">2016.07.class="num">15 class="num">15:class="num">11:class="num">01.417 Core class="num">1 连接关闭 class="num">2016.07.class="num">15 class="num">16:class="num">08:class="num">45.416 启动 MetaTester class="num">5 x64 build class="num">1368 (class="num">13 Jul class="num">2016) class="num">2016.07.class="num">15 class="num">16:class="num">08:class="num">45.612 服务器 MetaTester class="num">5 开始于 class="num">127.0.class="num">0.1:class="num">3000 class="num">2016.07.class="num">15 class="num">16:class="num">08:class="num">45.612 启动初始化结束 class="num">2016.07.class="num">15 class="num">16:class="num">09:class="num">36.811 服务器 MetaTester class="num">5 停止 class="num">2016.07.class="num">15 class="num">16:class="num">09:class="num">38.422 Tester 关闭测试机
「四终端同步测试与 Win API 调用落点」
四种测试模式同时跑自选 EA 的任务到此结束:EA 启动后,几乎能在四个终端里同步看到回测过程,验证并行观察是否真的零延迟偏差,开 MT5 多开实例即可复现。 文中实际调用了几个 Win API 函数,这才是能直接抄进 EA 的硬货:CopyFileW 负责在 MQL5 沙盒内外倒文件;FindFirstFileW / FindNextFileW / FindClose 组合做目录检索与句柄释放;GetOpenFileNameW 拉起系统打开文件框;ShellExecuteW 直接起外部程序。 下载包 2552.zip 约 14.18 KB,含完整工程;外汇与贵金属回测属高风险,并行结果仅反映历史数据概率,不代表实盘倾向。 有读者在 Win10 开 UAC 环境下把终端放 C:\forex 而非 Program Files,写文件无权限冲突——若你用标准安装路径,也要注意沙盒目录限制。
用 common.ini 接管代理与优化开关
MT5 的测试器与云/本地代理网络,可以通过 common.ini 直接写配置来接管,不必每次在 GUI 里点选。2016 年 9 月那轮讨论里,用户就提出想用 ini 文件同时控制前向优化(forward)和本地代理+云代理的混合接入,官方确认这条路是通的。 实际落地时,common.ini 里对应的段落决定测试器启动行为:代理地址、端口、是否启用云、以及优化模式参数都能固化。对做批量回测的交易者来说,这意味着换机器或重装终端后,只要丢一个 ini 进配置目录,代理拓扑和前向优化偏好就自动复原。 外汇与贵金属回测本身属高风险验证环境,配置错误可能让优化跑在错误数据上;建议先在一台本地代理机器上手动跑通一次,再把文件复制到其他节点。
◍ 最后说句实在话
把横盘识别交给 ADX 在策略测试器里跑均值,你会拿到一组横盘与趋势行情的参考阈值,用来框定止损和目标区间比拍脑袋稳。 订单、余额净值、回撤负载三类指标同时挂进测试器,能直接看见实况成交与资金曲线变形,省掉翻日志的功夫。 文本文件存 EA 输入参数是个被低估的动作,动态数组和变量落盘后,换环境不复写窗口也能复原。 进化与遗传算法不是玄学,拿简单 EA 做预测器集合优化,参数空间搜索效率会明显好于网格穷举,但外汇和贵金属杠杆高,回测顺不代表实盘概率同权,开 MT5 自己跑一遍再信。