自动优化 MetaTrader 5 专用 EA·进阶篇
(2/3)·手工每周调参只是徒劳,用 DLL 与命令行让 MT5 自己跑优化
◍ 把测试参数写进 ini 文件
在 MQL5 里做批量回测前,常需要把交易品种、杠杆和运行机制固化到一个配置里。下面这段片段演示了如何把 EURUSD、1:100 杠杆以及本地执行、关闭远程与云代理、开启可视化这些设定,以 INI 格式字符串写入文件。 注意 Leverage=1:100 与 UseLocal=1 的组合:它意味着回测只在本地以百倍杠杆跑,不开云端能避免agent资源占用,但单机算力会成为瓶颈。外汇与贵金属杠杆交易本身高风险,参数写错可能让回测结果严重偏离实盘。 文件句柄用 FileClose 收尾,打开失败则 Print 出错码并 return false,这是 MT5 里很基础的防御写法,直接复制到脚本里就能验证。
"Currency=EURUSD\n", "Leverage=class="num">1:class="num">100\n", "UseLocal=class="num">1\n", "UseRemote=class="num">0\n", "UseCloud=class="num">0\n", "Visual=class="num">1\n"); FileClose(filehandle); } else { Print("FileOpen, error ",GetLastError()); class="kw">return false; } class="kw">return true; }
把参数写进 .set 并丢进 tester 目录
做批量优化前,得先生成一份参数文件(例如 BuddyIlanTester.set),把被优化 EA 的每个 input 值落盘。上面那段声明就是该 EA 的真实默认值:固定/动态交易量切换、手数 0.1、加仓指数 1.4、止损 1000 点、止盈 40 点、尾随开启且距离 5 点,最大成交 10 笔。 CreateAndCopyParametersFile 这个函数吃 8 个参数:前 4 个(SL、TP、STOFilter、STOTimeFrameFilter)是开关,决定对应参数行尾是否标 Y 进入本轮优化;后 4 个是上一轮已跑出的参考值。函数顺手把文件复制到 MQL5\Profiles\Tester,MT5 测试器才能读到。 实盘前请在策略测试器里加载这份 .set 跑一次单轮回测,确认 _EA_MAGIC_NUMBER=1111 与实例数匹配;外汇与贵金属杠杆高,参数文件仅决定测试边界,不代表任何收益可能。
class="kw">input _TradingMode TradingMode = Dynamic; class=class="str">"cmt">// 固定或动态交易量 class="kw">input class="type">class="kw">double InpIlanFixedVolume = class="num">0.1; class=class="str">"cmt">// 固定交易量大小 (如果已设置) class="kw">input class="type">int InpNCurrencies=class="num">1; class=class="str">"cmt">// 此帐户上 Buddy Ilan 实例数量 class="kw">input class="type">class="kw">double LotExponent = class="num">1.4; class="kw">input class="type">bool DynamicPips = true; class="kw">input class="type">int DefaultPips = class="num">15; class="kw">input class="type">int Glubina=class="num">24; class=class="str">"cmt">// 计算波动率所需的最后柱线数量 class="kw">input class="type">int DEL=class="num">3; class="kw">input class="type">int TakeProfit = class="num">40.0; class=class="str">"cmt">// 止盈 (点数) class="kw">input class="type">int Stoploss = class="num">1000.0; class=class="str">"cmt">// 止损 (点数) class="kw">input class="type">bool InpIlanTrailingStop = true; class=class="str">"cmt">// 启用尾随停止 class="kw">input class="type">int InpIlanDistanceTS = class="num">5; class=class="str">"cmt">// 尾随停止距离 (点数) class="kw">input class="type">int MaxTrades=class="num">10; class="kw">input class="type">int InpDeviation=class="num">10; class=class="str">"cmt">// 最大允许的价格偏离 (点数) class="kw">input class="type">bool bSTOFilter = true; class=class="str">"cmt">// 动态趋势过滤器 class="kw">input class="type">bool bSTOTimeFrameFilter = false; class=class="str">"cmt">// 动态时间帧过滤器 class="kw">input class="type">int InpMaxTf = class="num">60; class=class="str">"cmt">// 最大时间帧 class="type">bool CreateAndCopyParametersFile( class="type">bool SL, class="type">bool TP, class="type">bool STOFilter, class="type">bool STOTimeFrameFilter, class="type">int SLValue, class="type">int TPValue, class="type">bool STOFilterValue, class="type">bool STOTimeFrameFilterValue ) { class="type">int filehandle=FileOpen("Optimiser\BuddyIlanTester.set",FILE_WRITE|FILE_TXT); if(filehandle!=INVALID_HANDLE) { FileWrite(filehandle, "_EA_IDENTIFIER=Buddy Ilan\n", "_EA_MAGIC_NUMBER=class="num">1111||class="num">0||class="num">1||class="num">10||N\n", StringFormat("TradingMode=%d||class="num">0||class="num">0||class="num">0||N\n",TradingMode),
「把参数落盘成可回测的 set 文件」
| 这段逻辑做的是一件事:把 EA 的输入参数用 StringFormat 拼成 MT5 策略测试器能识别的 .set 文本行,再写进终端的 Files\Optimiser 目录。每一行格式都是「参数名=值 | 默认 | 最小 | 最大 | 是否启用」,例如 TakeProfit 那行在 TPValue 为 0 时强制填 30,启用标记由 TP 布尔量决定填 Y 或 N;Stoploss 行在 SLValue 为 0 时落 1000,区间锁在 250~1500。 |
|---|
写文件用 FileClose 收尾,若 FileOpen 失败则 Print 错误码并 return false,说明这套流程对路径权限敏感。随后 Sleep(1500) 留出磁盘缓冲,再用 ShellExecuteW 调 xcopy 把 BuddyIlanTester.set 从 Files\Optimiser 拷到 Profiles\Tester,让测试器直接加载。 开 MT5 验证时,可在脚本里照抄下面这段,把 PathTester 和 PathProfile 打印出来,确认 xcopy 返回 ret>0 才算复制成功。外汇与贵金属回测属高风险,参数落盘仅是工程环节,不代表任何盈利倾向。
StringFormat("InpIlanFixedVolume=%lf||class="num">0.0||class="num">0.000000||class="num">0.000000||N\n",InpIlanFixedVolume), StringFormat("InpNCurrencies=%d||class="num">0||class="num">1||class="num">10||N\n",InpNCurrencies), StringFormat("LotExponent=%lf||class="num">0.0||class="num">0.000000||class="num">0.000000||N\n",LotExponent), StringFormat("DynamicPips=%s||false||class="num">0||true||N\n",(DynamicPips==true)?"true":"false"), StringFormat("DefaultPips=%d||class="num">0||class="num">1||class="num">10||N\n",DefaultPips), StringFormat("Glubina=%d||class="num">0||class="num">1||class="num">10||N\n",Glubina), StringFormat("DEL=%d||class="num">0||class="num">1||class="num">10||N\n",DEL), StringFormat("TakeProfit=%d||class="num">30||class="num">10||class="num">70||%s\n",(TPValue==class="num">0)?class="num">30:TPValue,(TP==true)?"Y":"N"), StringFormat("Stoploss=%d||class="num">500||class="num">250||class="num">1500||%s\n",(SLValue==class="num">0)?class="num">1000:SLValue,(SL==true)?"Y":"N"), StringFormat("InpIlanTrailingStop=%s||false||class="num">0||true||N\n",(InpIlanTrailingStop==true)?"true":"false"), StringFormat("InpIlanDistanceTS=%d||class="num">0||class="num">1||class="num">10||N\n",InpIlanDistanceTS), StringFormat("MaxTrades=%d||class="num">0||class="num">1||class="num">10||N\n",MaxTrades), StringFormat("InpDeviation=%d||class="num">0||class="num">1||class="num">10||N\n",InpDeviation), StringFormat("bSTOFilter=%s||false||class="num">0||true||%s\n",(STOFilterValue==true)?"true":"false",(STOFilter==true)?"Y":"N"), StringFormat("bSTOTimeFrameFilter=%s||false||class="num">0||true||%s\n",(STOTimeFrameFilterValue==true)?"true":"false",(STOTimeFrameFilter==true)?"Y":"N"), StringFormat("InpMaxTf=%d||class="num">0||class="num">1||class="num">10||N\n",InpMaxTf)); FileClose(filehandle); } else { Print("FileOpen BuddyIlanTester.set, error ",GetLastError()); class="kw">return false; } Sleep(class="num">1500); class="type">class="kw">string PathTester=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\Optimiser\\BuddyIlanTester.set"; class="type">class="kw">string PathProfile=sTerminalTesterDataPath+"\\MQL5\\Profiles\\Tester\\"; class=class="str">"cmt">// 将 ini 文件复制到 tester 文件夹中 class="type">int ret=ShellExecuteW(class="num">0,"Open","xcopy","\""+PathTester+"\" \""+PathProfile+"\" /y","",class="num">0); class=class="str">"cmt">// 等到文件复制完成
◍ 参数文件拷贝失败的兜底处理
在 EA 初始化阶段远程拉取配置时,网络抖动或 broker 端限流都可能让拷贝中断。上面这段逻辑先挂起 2500 毫秒(2.5 秒)给系统缓冲,再检查返回值是否小于 32。 MQL5 里 FileCopy 系列函数返回实际写入字节数,低于 32 基本意味着参数文件没拉全。此时直接 Print 报错并 return false,可避免后续用残缺参数开仓。 你在 MT5 里跑这类自动载参的 EA,可以把 2500 这个值调到 5000 试一次,观察日志里 Failed copying parameters file 的出现频率是否下降。外汇与贵金属杠杆品种波动剧烈,参数缺失状态下交易属于高风险操作。
Sleep(class="num">2500); if(ret<class="num">32) { Print("Failed copying parameters file"); class="kw">return false; } class="kw">return true; }
用第二个终端跑无人优化
想让主终端不被卡死,可以起一个独立的 MetaTrader 5 测试器实例专门跑优化。它按预设的 optimise.ini 自动跑完,生成 BuddyIlanReport.xml 后自行退出,主终端只管隔空看报告落没落盘。 判断优化是否结束,最省事的办法就是轮询那个 XML 报告文件。文件一出现,就说明第二实例已经把活干完,此时用 xcopy 拽回工作目录即可。 下面这段逻辑里,StartOptimizer 先清掉旧报告再拉起 terminal64.exe,失败返回 false;CopyReport 最多轮 500 次、每次睡 15 秒,理论超时约 2 小时。外汇与贵金属品种波动剧烈,这种无人值守优化仍可能因点差跳变导致结果漂移,仅作技术验证用。 别把 2 小时超时当够用 若你的参数空间大、品种流动性差,500 次轮询可能提前耗尽。建议先在 EURUSD 15M 上用小样本试跑,确认报告生成节奏再放大。
class="type">bool StartOptimizer() { class=class="str">"cmt">// 删除以前的报告 FileDelete("Optimiser\BuddyIlanReport.xml"); class=class="str">"cmt">// 删除以前的报告(第二个 MetaTrader class="num">5 实例) class="type">class="kw">string PathReport=sTerminalTesterDataPath+"\MQL5\Files\Reports\BuddyIlanReport.xml"; ShellExecuteW(class="num">0,"Open","cmd.exe"," /C del "+PathReport,"",class="num">0); Sleep(class="num">2500); class="type">class="kw">string sTerminalPath=TerminalInfoString(TERMINAL_PATH); class=class="str">"cmt">// 开始优化处理 class="type">int start=ShellExecuteW(class="num">0,"Open",sTerminalTesterPath+"\terminal64.exe","/config:"+TerminalInfoString(TERMINAL_DATA_PATH)+"\MQL5\Files\Optimiser\optimise.ini","",class="num">0); if(start<class="num">32) { Print("Failed starting Tester"); class="kw">return false; } Sleep(class="num">15000); class="kw">return true; } class="type">bool CopyReport() { class="type">int nTry=class="num">0; class=class="str">"cmt">// 等待并复制报告文件 class="kw">while(nTry++<class="num">500) class=class="str">"cmt">// 超时 : class="num">2 小时 { class="type">class="kw">string PathReport = sTerminalTesterDataPath + "\MQL5\Files\Reports\BuddyIlanReport.xml"; class="type">class="kw">string PathTarget = TerminalInfoString(TERMINAL_DATA_PATH) + "\MQL5\Files\Optimiser\"; class="type">int ret=ShellExecuteW(class="num">0,"Open","xcopy","\""+PathReport+"\" \""+PathTarget+"\" /y","",class="num">0); if(ret<class="num">32) { PrintFormat("Waiting generation report(%d) ...",nTry); Sleep(class="num">15000); } else { if(FileIsExist("Optimiser\BuddyIlanReport.xml")==true) { PrintFormat("Report found(ret=%d) ...",ret); Sleep(class="num">2500); class="kw">return true; } else { PrintFormat("Waiting report(%d) ...",nTry); Sleep(class="num">15000); } } } class="kw">return false; }
「把优化报告 XML 拆成可用数组」
MT5 跑完多参数优化导出的报告是 XML 格式,直接读很麻烦。社区里有人写过一个轻量 XML 解析函数库,我们把它挂进 EA,并改了几处去适配自己的报告字段,访问节点就只剩体力活。 因为这次要同时优化 SL、TP、STO 三组参数,解析逻辑得按类型分流,对应函数写成递归去逐层剥节点。以 SL 为例,代码会遍历 Worksheet 下的每一行和单元格,遇到没有子节点的末端节点就认定是数值,从行尾切出来塞进 res[] 数组,后面挑最大利润那组 SL 即可。 递归解析时若节点无子节点,说明已到叶子层,此时把该行从末尾拆分、存成字符串,每列结果就躺在 res[] 里等你挑。外汇与贵金属优化存在过拟合高风险,回测利润高不代表实盘概率占优,参数需谨慎外推。 下面这段是载入 XML 与按类型读取结果的核心片段,注意 loadXmlFromFullPathFile 用 FILE_ANSI|FILE_READ 且编码 CP_UTF8,路径写死在 Optimiser\\BuddyIlanReport.xml,开 MT5 把报告丢进对应目录就能直接跑。
class="macro">#include <EasyXML\EasyXml.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 加载给定的 XML 文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEasyXml::loadXmlFromFullPathFile(class="type">class="kw">string pFilename) { class="type">class="kw">string sStream; class="type">int iStringSize; Print("Loading XML File ",pFilename); class="type">int hFile=FileOpen(pFilename,FILE_ANSI|FILE_READ,class="num">0,CP_UTF8); if(hFile==INVALID_HANDLE) { Err=EASYXML_ERR_CONNECTION_FILEOPEN; PrintFormat("[%s] Err=%d",pFilename,GetLastError()); class="kw">return(Error()); } class="kw">while(!FileIsEnding(hFile)) { iStringSize = FileReadInteger(hFile, INT_VALUE); sStream += FileReadString(hFile, iStringSize); } FileClose(hFile); class="kw">return(loadXmlFromString(sStream)); } class="type">bool LoadResults( OptimisationType eType ) { class=class="str">"cmt">// 初始化变量 BetterProfit=class="num">0.0; class=class="str">"cmt">// 加载结果 CEasyXml EasyXmlDocument; EasyXmlDocument.setDebugging(false); if(EasyXmlDocument.loadXmlFromFullPathFile("Optimiser\\BuddyIlanReport.xml")==true) { str=""; CEasyXmlNode *RootNode=EasyXmlDocument.getDocumentRoot(); for(class="type">int j=class="num">0; j<RootNode.Children().Total(); j++) { CEasyXmlNode *ChildNode=RootNode.Children().At(j); for(class="type">int i=class="num">0; i<ChildNode.Children().Total(); i++) { CEasyXmlNode *cNode=ChildNode.Children().At(i); if(cNode.getName() == "Worksheet" ) { class="kw">switch(eType) { case _SL : DisplayNodesSL(cNode); PrintFormat("-> SL=%d(Profit=%.2lf)",BetterSL,BetterProfit); break; case _TP : DisplayNodesTP(cNode); PrintFormat("-> TP=%d(Profit=%.2lf DD=%lf)",BetterTP,BetterProfit,BetterDD); break;