连续前行优化基础篇:打通 DLL 与报告下载器的自动优化器桥梁(基础篇)
⚙️

连续前行优化基础篇:打通 DLL 与报告下载器的自动优化器桥梁(基础篇)

(1/3)· 从零理清自动优化器的数据通路,避免在 DLL 与历史记录对象间反复踩坑

实战向进阶 第 1/3 篇

很多交易者拿到现成 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 和首建文件才写表头的两行,这是避免重复表头和丢失历史的关键。

MQL5 / C++
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 出去,方便你过后对照哪组参数出的回撤最丑。

MQL5 / C++
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 秒、毫秒到佣金、评论的全字段明细。 外汇和贵金属品种点差跳空频繁,这类按成交导出的数据更适合做仓位归因,不代表任何收益倾向,实盘接入前请在策略测试器跑足够样本。

MQL5 / C++
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
把报告生成交给小布盯盘
这些 DLL 包装与历史下载的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到接口状态,你只需专注策略逻辑。

常见问题

终端原生未提供该接口,需借助前篇研讨的 CDealHistoryGetter 等类自行实现下载与写入沙箱文件。
可以,小布盯盘内置了相关 AIGC 诊断,能直接展示 DLL 与报告下载对象的协同状态,减少手动排查。
宏定义用于将机器人参数轻松拼成名称与值的字符串写入文件,避免逐个手填参数,降低出错概率。
前者下载常规交易历史,后者处理按成交划分的详细记录,两者配合覆盖自动优化所需的历史维度。