连续前行优化 (第三部分): 将机器人适配为自动优化器·进阶篇
⚙️

连续前行优化 (第三部分): 将机器人适配为自动优化器·进阶篇

(2/3)·前两篇铺好了零件,本篇把它们焊成一条能自跑的自动优化流水线

偏理论 第 2/3 篇
很多人在写完交易历史下载类和报告 DLL 后就卡住了,以为零件齐了就能自动跑。实际上两者之间没有桥接层,机器人依然只是孤立脚本,优化器永远起不来。

◍ 把自定义手续费写进回测报告

在 MT5 回测里, broker 默认点值往往不含你实盘那档加点或返佣结构,直接用默认设置跑出的净值曲线会偏乐观。下面这段 EA 骨架演示了如何把 custom_comission 与 custom_shift 两个输入参数落盘到 CSV,方便后续和真实账户对表。 核心逻辑放在 OnDeinit:只有当 MQLInfoInteger(MQL_TESTER)==1 即处于测试环境时才写文件,避免实盘误触发 IO。文件命名用 __FILE__ 去掉扩展名再拼 ' Report.csv',保证每次编译产物对应独立报告。 WRITE_BOT_PARAM 宏负责把数值参数写进同一份 CSV 表头区,WriteDetalesReport 则输出逐笔成交明细到 ' Deals Report.csv'。实盘验证时,把 custom_comission 设成你经纪商每手单边费用(比如 7.0 美元),custom_shift 设成滑点偏移点数,跑一遍 EURUSD 的 2023 年 H1 数据,就能看到手续费对 Sharpe 的侵蚀幅度。外汇与贵金属杠杆高,回测未含极端点差扩张,实盘偏差可能进一步扩大。

MQL5 / C++
class="macro">#define ONLY_CUSTOM_COMISSION
class="macro">#include <History manager/DealsHistory.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;
CCCM _comission_manager_;
class="type">int OnInit()
  {
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   if(MQLInfoInteger(MQL_TESTER)==class="num">1)
     {
      class="type">class="kw">string arr[];
      StringSplit(__FILE__,&class="macro">#x27;.&class="macro">#x27;,arr);
      class="type">class="kw">string file_name = arr[class="num">0]+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);
      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;
      WriteDetalesReport(arr[class="num">0]+" Deals Report.csv", &_comission_manager_);
     }
  }

正文

<span class="keyword">void</span> <span class="functions">OnTick</span>() &nbsp;&nbsp;{ <span class="comment">//---</span> &nbsp;&nbsp;} <span class="comment">//+------------------------------------------------------------------+</span> [/CODE]

「用 DLL 包装器把优化结果落盘成 XML」

连续前行优化最顺手的载体是 XML,但纯函数调用在 EA 里写起来别扭。把第一篇文章里的 C# DLL 用对象包一层,就得到 XmlHistoryWriter.mqh 里的 CXmlHistoryWriter,EA 端只管把参数丢进去,写入逻辑全藏 behind 对象里。 导入 ReportManager.dll 后,先声明 BotParams 结构存 name/value 键值对,再用两个宏降低动态数组操作成本:ADD_TO_ARR 负责扩数组并塞元素,APPEND_BOT_PARAM 则按变量名自动取 #Var 当键、把值强转 string 当值。枚举必须转 int 再转 string,否则和 .set 文件对不上。 多进程并行优化时,两个机器人可能同时写同一个 XML。CXmlHistoryWriter 里写死了 _mutex_name 字段,靠 C# DLL 的命名互斥锁走操作系统内核做同步,谁拿到锁谁写,避免报告互相覆盖。 文件名协议硬编码为『EA名_Report.xml』,且强制落在 MT5 的 Common 沙箱目录,不让你写去 PC 任意路径。Write() 是总入口,失败就吐日志,成功则依次调 append_bot_params、append_days_pl 等包装器,最后调 DLL 落盘。 想更懒一点就用 AutoLoader.mqh 的 CAutoUpLoader:它在 OnTick 里记起止时间,析构时自动委托 CXmlHistoryWriter 写报告。继承它还是全局实例化都行——继承时重载 OnTick 必须显式调基类,否则自动优化器拿不到时间基准。外汇与贵金属 EA 回测涉及高杠杆与滑点风险,并行优化结果仅作概率参考。

MQL5 / C++
class="macro">#include "ReportCreator.mqh"
class="macro">#class="kw">import "ReportManager.dll"
class="kw">struct BotParams
  {
   class="type">class="kw">string            name,value;
   };
class="macro">#define ADD_TO_ARR(arr, value) \
{
   class="type">int s = ArraySize(arr);
   ArrayResize(arr,s+class="num">1,s+class="num">1);
   arr[s] = value;
}
class="macro">#define APPEND_BOT_PARAM(Var,BotParamArr) \
{
   BotParams param;
   param.name = class="macro">#Var;\
   param.value = (class="type">class="kw">string)Var;\
   
   ADD_TO_ARR(BotParamArr,param);
}
class="type">void CopyBotParams(BotParams &dest[], class="kw">const BotParams &src[])
  {
   class="type">int total = ArraySize(src);
   for(class="type">int i=class="num">0; i<total; i++)
     {
      ADD_TO_ARR(dest,src[i]);
     }
   }
class CXmlHistoryWriter
  {
class="kw">private:
   class="kw">const class="type">class="kw">string      _path_to_file,_mutex_name;
   CReportCreator    _report_manager;
   class="type">class="kw">string            get_path_to_expert();class=class="str">"cmt">//

◍ 把回测结果落盘到公共目录的写入器

做 EA 批量回测时,最麻烦的是把每套参数的盈亏、回撤、星期分布写进同一个文件供后续比对。下面这个类把这件事封死了:构造时传入文件名和互斥名,落盘路径直接拼到 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 下,也就是所有终端账号共享的 Common 数据目录,不会散落在某个品种子目录里。 无参文件名的重载更省事——路径自动变成 MQL_PROGRAM_NAME + "_Report.xml",EA 跑完在自己程序名下生成报告,多品种并行也不会互相覆盖。 get_path_to_expert() 里用 StringSplit 按 '\\' 拆 MQL_PROGRAM_PATH,从命中 "Experts" 那段开始拼相对目录。这意味着你若把脚本挪到 Scripts 或 Indicators 下,这段逻辑会拿不到预期路径,调用前最好先打印 arr 数组确认层级。外汇与贵金属回测含高杠杆风险,落盘数据仅用于策略对比,不代表实盘倾向。

MQL5 / C++
class="type">void                append_bot_params(class="kw">const BotParams  &params[]);class=class="str">"cmt">//
class="type">void                append_main_coef(PL_detales &pl_detales,
                                     TotalResult &totalResult);class=class="str">"cmt">//
class="type">class="kw">double              get_average_coef(CoefChartType type);
class="type">void                insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);class=class="str">"cmt">//
class="type">void                append_days_pl();class=class="str">"cmt">//
class="kw">public:
                      CXmlHistoryWriter(class="type">class="kw">string file_name,class="type">class="kw">string mutex_name,
                      CCCM *_comission_manager);class=class="str">"cmt">//
                      CXmlHistoryWriter(class="type">class="kw">string mutex_name,CCCM *_comission_manager);
                      ~CXmlHistoryWriter(class="type">void) {_report_manager.Clear();} class=class="str">"cmt">//
class="type">void                Write(class="kw">const BotParams &params[],class="type">class="kw">datetime start_test,class="type">class="kw">datetime end_test);class=class="str">"cmt">//
};
CXmlHistoryWriter::CXmlHistoryWriter(class="type">class="kw">string file_name,
                                     class="type">class="kw">string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
  _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name),
  _report_manager(_comission_manager)
  {
  }
CXmlHistoryWriter::CXmlHistoryWriter(class="type">class="kw">string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
  _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+MQLInfoString(MQL_PROGRAM_NAME)+"_"+"Report.xml"),
  _report_manager(_comission_manager)
  {
  }
class="type">class="kw">string CXmlHistoryWriter::get_path_to_expert(class="type">void)
  {
   class="type">class="kw">string arr[];
   StringSplit(MQLInfoString(MQL_PROGRAM_PATH),&class="macro">#x27;\\&class="macro">#x27;,arr);
   class="type">class="kw">string relative_dir=NULL;
   class="type">int total= ArraySize(arr);
   class="type">bool save= class="kw">false;
   for(class="type">int i=class="num">0; i<total; i++)
     {
      if(save)
        {
         if(relative_dir== NULL)
            relative_dir=arr[i];
         else
            relative_dir+="\\"+arr[i];
        }
      if(StringCompare("Experts",arr[i])==class="num">0)

把回测结果落盘成 XML 的实现落点

这段 CXmlHistoryWriter::Write 负责把策略回测的汇总指标写进 XML 报告。它先尝试创建报告管理器,失败就直接 Print 报错并返回,不会继续污染文件。 拿到 totalResult 与 pl_detales 后,依次追加机器人参数、核心系数、VaR 三档分位(90/95/99)以及均值和标准差,再写最大盈亏与回撤细节。VaR_99 这类绝对风险值对贵金属跨周期回测尤其敏感,杠杆放大后尾部亏损可能远超正态假设。 最后用 MutexWriter 加锁写文件,传入账户币种、杠杆(AccountInfoInteger(ACCOUNT_LEVERAGE))、品种、周期与起止时间;若返回的 error_msg 非空,则打印错误阻断。外汇与贵金属回测高风险,实盘前务必在 MT5 用真实点差重跑验证。

MQL5 / C++
save=true;
  }
  class="kw">return relative_dir;
}
class="type">void CXmlHistoryWriter::append_bot_params(class="kw">const BotParams &params[])
  {
  class="type">int total= ArraySize(params);
  for(class="type">int i=class="num">0; i<total; i++)
    {
    ReportWriter::AppendBotParam(params[i].name,params[i].value);
    }
  }
class="type">void CXmlHistoryWriter::Write(class="kw">const BotParams &params[],class="type">class="kw">datetime start_test,class="type">class="kw">datetime end_test)
  {
  if(!_report_manager.Create())
    {
    Print("##################################");
    Print("Can`t create report:");
    Print("###################################");
    class="kw">return;
    }
  TotalResult totalResult;
  _report_manager.GetTotalResult(totalResult);
  PL_detales pl_detales;
  _report_manager.GetPL_detales(pl_detales);
  append_bot_params(params);
  append_main_coef(pl_detales,totalResult);
  ReportWriter::AppendVaR(totalResult.total.VaR_absolute.VAR_90,
                          totalResult.total.VaR_absolute.VAR_95,
                          totalResult.total.VaR_absolute.VAR_99,
                          totalResult.total.VaR_absolute.Mx,
                          totalResult.total.VaR_absolute.Std);
  ReportWriter::AppendMaxPLDD(pl_detales.total.profit.totalResult,
                              pl_detales.total.drawdown.totalResult,
                              pl_detales.total.profit.orders,
                              pl_detales.total.drawdown.orders,
                              pl_detales.total.profit.dealsInARow,
                              pl_detales.total.drawdown.dealsInARow);
  append_days_pl();
  class="type">class="kw">string error_msg=ReportWriter::MutexWriter(_mutex_name,get_path_to_expert(),AccountInfoString(ACCOUNT_CURRENCY),
                              _report_manager.GetBalance(),
                              (class="type">int)AccountInfoInteger(ACCOUNT_LEVERAGE),
                              _path_to_file,
                              _Symbol,(class="type">int)Period(),
                              start_test,
                              end_test);
  if(StringCompare(error_msg,"")!=class="num">0)
    {
    Print("##################################");
    Print("Error class="kw">while creating(*.xml) report file:");
    Print("_________________________________________");
    Print(error_msg);
把桥接逻辑交给小布盯盘托管
这些 DLL 与报告对象的衔接诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到调用链路是否断点,你只需判断要不要改参数。

常见问题

终端原生未开放此类文件写出接口,需借助前篇研讨的 CDealHistoryGetter 等类在沙箱目录自行实现写入。
它把机器人参数名和值自动拼成字符串写入文件,避免手动罗列每个输入变量,减少遗漏和拼写错误。
可以,小布盯盘的品种页会呈现 DLL 与报告下载对象之间的调用状态,断点处会标出,便于你定位哪一层没接上。
数据写入文件沙箱 Terminal/Common/Files,打开文件时移向末尾追加,新建时才写入表头。
文末给出标准算法示例,展示在原有 EA 里挂载报告接收结构,并把历史获取实例传入循环保存,从而接入自动优化器。