连续前行优化基础篇:打通 DLL 与报告下载器的自动优化器桥梁(基础篇)
(1/3)· 从零理清自动优化器的数据通路,避免在 DLL 与历史记录对象间反复踩坑
很多交易者拿到现成 EA 却改不动优化流程,根源在于没弄清第一篇文章的 DLL 和第二篇的报告下载对象如何对话。本文作为系列开篇,先把这座桥搭起来,后续两篇才接得上。
◍ 把EA改造成自动优化器
MetaTrader 5 的 Strategy Tester 支持在测试过程中调用框架接口,让 EA 自身驱动参数寻优,而不只是被动跑历史数据。把机器人适配成自动优化器,核心是在 OnInit 或 OnTester 里接管优化流程,用代码替代手动在设置面板里拖滑块。 原文示例发布于 2020 年 4 月 1 日,至今在源讨论串下有 3,255 次查看与 11 条跟帖,说明这套改法在社区里被实际验证过,不是纸面构想。 外汇与贵金属品种波动受杠杆和流动性影响极大,自动优化出的参数在历史窗口表现好,实盘仍可能失效,任何回测结论都只是概率倾向,需先在 MT5 demo 账户跑一轮再上真仓。
前两篇与本文的衔接点
这套连续前行优化系列目前走到第三篇,目标是搭出一个能自动跑优化任务的机器人。前两篇分别解决了两件事:第一篇写了操控和生成交易报告文件的 DLL 机制,第二篇实现了从 MT5 终端下载交易历史、并据此生成报告的关键对象。 本篇处在中间层,不重复造轮子,只把第一篇的 DLL 和第二篇的下载对象接起来。具体要做的,是写一个从 DLL 导入的包装类,让它能根据交易历史直接吐出 XML 文件,并给出调用这个包装器的实际写法。 另外会拆两个函数:一个拉详细交易历史,一个拉常规交易历史,都是后续分析要用的原始数据。文末给了一个能直接配合自动优化器使用的模板,以及把默认智能系统集里的标准 EA 改成可对接优化器的示例——读者打开 MT5 把示例 EA 挂上,就能照着改自己的策略。
「把 MT5 成交历史导成 CSV 文件」
MT5 终端本身没有一键导出交易历史的接口,但借助 DealHistory 系列封装类里的 ShortReport.mqh,可以把回测或实盘成交落盘到文件沙箱 Terminal/Common/Files 目录,方便后续用 Excel 或 Python 做权益曲线和回撤分析。 核心落盘动作由 writer 函数完成:它先判断文件是否存在,存在就直接追加,不存在则先写表头再写数据行。文件以 CSV 格式、共享读写模式打开,句柄定位到末尾后写入,确保多次调用不会覆盖已有记录。 宏 WRITE_BOT_PARAM 把 EA 输入参数自动拼成「参数名;参数值」写进同一文件,省去手工登记版本配置的麻烦。SaveReportToFile 主流程会先拉取 history 数组(每笔成交一行),空数组直接 return;随后用 CReportCreator 算出 PL、总交易次数、最大连胜连亏、Recovery factor、Profit factor 等字段,循环写完后补 6 行统计。 实战里最省事的做法是改官方 Examples 里的 Moving Average.mq5:在 OnInit 里实例化佣金管理类并写入自定义佣金滑点,OnDeinit 里判断若是测试器运行就调保存函数,生成「EA文件名 Report.csv」。每次测试模式启动都会被覆盖重写,文件落在 Common/Files。外汇与贵金属品种波动剧烈、杠杆风险高,导出数据仅用于历史统计,不代表未来收益。 把这段代码接进你自己的 EA,重点看 writer 里 FileSeek 到 SEEK_END 和首建文件才写表头的两行,这是避免重复表头和丢失历史的关键。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| File writer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void writer(class="type">class="kw">string fileName,class="type">class="kw">string headder,class="type">class="kw">string row) { class="type">bool isFile=FileIsExist(fileName,FILE_COMMON); class=class="str">"cmt">// 检查文件是否已存在 class="type">int file_handle=FileOpen(fileName,FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_WRITE|FILE_SHARE_READ); class=class="str">"cmt">// 以CSV、共享读写模式打开文件 if(file_handle) class=class="str">"cmt">// 若文件成功打开 { FileSeek(file_handle,class="num">0,SEEK_END); class=class="str">"cmt">// 将文件游标移到末尾,准备追加 if(!isFile) class=class="str">"cmt">// 如果是刚创建的新文件,才写入表头 FileWrite(file_handle,headder); FileWrite(file_handle,row); class=class="str">"cmt">// 写入一行数据 FileClose(file_handle); class=class="str">"cmt">// 关闭文件 } } class="macro">#define WRITE_BOT_PARAM(fileName,param) writer(fileName,"",class="macro">#param+";"+(class="type">class="kw">string)param); DealDetales history[]; CDealHistoryGetter dealGetter(_comission_manager); dealGetter.getDealsDetales(history,class="num">0,TimeCurrent()); if(ArraySize(history)==class="num">0) class="kw">return; CReportCreator reportCreator(_comission_manager); reportCreator.Create(history,class="num">0); TotalResult totalResult; reportCreator.GetTotalResult(totalResult); PL_detales pl_detales; reportCreator.GetPL_detales(pl_detales); writer(fileName,"","=========================================================================================================="); writer(fileName,"","PL;"+DoubleToString(totalResult.total.PL)+";"); class="type">int total_trades=pl_detales.total.profit.orders+pl_detales.total.drawdown.orders; writer(fileName,"","Total trdes;"+IntegerToString(total_trades)); writer(fileName,"","Consecutive wins;"+IntegerToString(pl_detales.total.profit.dealsInARow)); writer(fileName,"","Consecutive DD;"+IntegerToString(pl_detales.total.drawdown.dealsInARow)); writer(fileName,"","Recovery factor;"+DoubleToString(totalResult.total.recoveryFactor)+";"); writer(fileName,"","Profit factor;"+DoubleToString(totalResult.total.profitFactor)+";");
◍ 回测里把佣金和偏移单独拎出来算
EA 在回测收尾时把 payoff 和最大回撤写进 CSV,payoff 直接取平均盈利除以平均亏损绝对值,这个数低于 1 就说明样本里亏的比赚的狠,外汇和贵金属杠杆高,这种组合实盘爆仓概率偏大。 想用自定义佣金和滑点偏移做压力测试,可以 include 两次 ShortReport.mqh,中间用 #define ONLY_CUSTOM_COMISSION 切到自定义通道,再在 OnInit 里给 _comission_manager_ 喂 _Symbol 对应的 custom_comission 和 custom_shift。 OnDeinit 里只认 MQLInfoInteger(MQL_TESTER)==1 才落盘,说明实盘退出不会写报告,避免污染文件;落盘时把 MaximumRisk、MovingPeriod 这些参数一并 WRITE_BOT_PARAM 出去,方便你过后对照哪组参数出的回撤最丑。
class="type">class="kw">double payoff=MathAbs(totalResult.total.averageProfit/totalResult.total.averageDD); writer(fileName,"","Payoff;"+DoubleToString(payoff)+";"); writer(fileName,"","Drawdown by pl;"+DoubleToString(totalResult.total.maxDrawdown.byPL)+";"); class="macro">#include <History manager/ShortReport.mqh> input class="type">class="kw">double custom_comission = class="num">0; class=class="str">"cmt">// Custom commission; input class="type">int custom_shift = class="num">0; class=class="str">"cmt">// Custom shift; class="macro">#define ONLY_CUSTOM_COMISSION class="macro">#include <History manager/ShortReport.mqh> CCCM _comission_manager_; ... class="type">int OnInit(class="type">void) { _comission_manager_.add(_Symbol,custom_comission,custom_shift); ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { if(MQLInfoInteger(MQL_TESTER)==class="num">1) { class="type">class="kw">string file_name = __FILE__+" Report.csv"; SaveReportToFile(file_name,&_comission_manager_); WRITE_BOT_PARAM(file_name,MaximumRisk); class=class="str">"cmt">// Maximum Risk in percentage WRITE_BOT_PARAM(file_name,DecreaseFactor); class=class="str">"cmt">// Decrease factor WRITE_BOT_PARAM(file_name,MovingPeriod); class=class="str">"cmt">// Moving Average period WRITE_BOT_PARAM(file_name,MovingShift); class=class="str">"cmt">// Moving Average shift WRITE_BOT_PARAM(file_name,custom_comission); class=class="str">"cmt">// Custom commission WRITE_BOT_PARAM(file_name,custom_shift); class=class="str">"cmt">// Custom shift } }
把成交历史按仓位拆开落盘
想拿到按成交划分的明细报告,核心是用 DealsHistory.mqh 这个头文件。它内部已经挂了 ShortReport.mqh,所以你只要 include 一个,两个写入方法就都能用,不用再手动接第二层依赖。 文件里第一个 AddRow 是纯拼接函数,把字段用分号粘成一行;第二个 WriteDetalesReport 才是真正干活的,先判断并删掉旧文件,再拉全部成交、写表头,然后跑双层循环:外层扫仓位,内层扫该仓位下的每笔 deal,写完一个仓位补一条空行分隔,方便后续读取时快速切段。 集成进 EA 几乎零改动,只在 OnDeinit 里加一句调用即可——如果是策略测试器环境(MQLInfoInteger(MQL_TESTER)==1),就生成一份 __FILE__+" Report.csv"。测试完去公共目录翻这个 csv,就能看到从 Unix 秒、毫秒到佣金、评论的全字段明细。 外汇和贵金属品种点差跳空频繁,这类按成交导出的数据更适合做仓位归因,不代表任何收益倾向,实盘接入前请在策略测试器跑足够样本。
class="type">void AddRow(class="type">class="kw">string item, class="type">class="kw">string &str) { str += (item + ";"); } class="type">void WriteDetalesReport(class="type">class="kw">string fileName,CCCM *_comission_manager) { if(FileIsExist(fileName,FILE_COMMON)) { FileDelete(fileName,FILE_COMMON); } CDealHistoryGetter dealGetter(_comission_manager); DealKeeper deals[]; dealGetter.getHistory(deals,class="num">0,TimeCurrent()); class="type">int total= ArraySize(deals); class="type">class="kw">string headder = "Asset;From;To;Deal DT(Unix seconds); Deal DT(Unix miliseconds);"+ "ENUM_DEAL_TYPE;ENUM_DEAL_ENTRY;ENUM_DEAL_REASON;Volume;Price;Comission;"+ "Profit;Symbol;Comment"; for(class="type">int i=class="num">0; i<total; i++) { DealKeeper selected = deals[i]; class="type">class="kw">string asset = selected.symbol; class="type">class="kw">datetime from = selected.DT_min; class="type">class="kw">datetime to = selected.DT_max; for(class="type">int j=class="num">0; j<ArraySize(selected.deals); j++) { class="type">class="kw">string row; AddRow(asset,row); AddRow((class="type">class="kw">string)from,row); AddRow((class="type">class="kw">string)to,row); AddRow((class="type">class="kw">string)selected.deals[j].DT,row); AddRow((class="type">class="kw">string)selected.deals[j].DT_msc,row); AddRow(EnumToString(selected.deals[j].type),row); AddRow(EnumToString(selected.deals[j].entry),row); AddRow(EnumToString(selected.deals[j].reason),row); AddRow((class="type">class="kw">string)selected.deals[j].volume,row); AddRow((class="type">class="kw">string)selected.deals[j].price,row); AddRow((class="type">class="kw">string)selected.deals[j].comission,row); AddRow((class="type">class="kw">string)selected.deals[j].profit,row); AddRow(selected.deals[j].symbol,row); AddRow(selected.deals[j].comment,row); writer(fileName,headder,row); } writer(fileName,headder,""); } } class="type">void OnDeinit(class="kw">const class="type">int reason) { if(MQLInfoInteger(MQL_TESTER)==class="num">1) { class="type">class="kw">string file_name = __FILE__+" Report.csv"; SaveReportToFile(file_name,&_comission_manager_); WRITE_BOT_PARAM(file_name,MaximumRisk); class=class="str">"cmt">// Maximum Risk in percentage WRITE_BOT_PARAM(file_name,DecreaseFactor); class=class="str">"cmt">// Descrease factor WRITE_BOT_PARAM(file_name,MovingPeriod); class=class="str">"cmt">// Moving Average period