继续漫步优化(第二部分):为任意机器人创建优化报告的机制(基础篇)
📘

继续漫步优化(第二部分):为任意机器人创建优化报告的机制(基础篇)

第 1/3 篇

「给任意EA挂上优化报告生成器」

在 MT5 里做策略优化,最头疼的不是跑参数,而是跑完一堆结果却没法快速横向比对。本文要落地的机制,是给任意已有机器人(EA)套一层优化报告采集逻辑,让每一组传入参数在回测结束时自动吐出结构化结果。 实现思路不依赖重写 EA 本身,而是在 OnTester() 里接管返回逻辑。MT5 每次优化遍历会调用这个函数,我们往里塞自定义指标计算与字符串拼接,就能把夏普、回撤、交易次数等随优化任务一起导出。 一个可验证的基准现象:在 2020 年 2 月社区示例里,同样机制在 24 小时内被实测访问 2463 次,说明这种「不碰原 EA 源码、只加报告层」的做法对多品种回测有普遍需求。外汇与贵金属品种波动跳空频繁,用此机制做跨品种优化时须意识到过拟合概率偏高,属高风险操作。

为什么要在 EA 里自己造优化报告

策略测试器对 EA 是黑盒:它不让智能交易系统直接读取自身跑出来的优化数据,而且默认导出的结果缺少逐参数细节,复盘时很难定位哪一组走得好。 为了解决这个问题,我们沿用之前文章里实现的优化报告下载能力,把测试器的产出拉回程序内部自行解析。因为其中几个独立模块后来被改过、且旧文没讲全,这里要把它们重新拆开看——它们是整个自动优化器的关键构件。 本篇新切入的一个点是自定义佣金写入。所有涉及的类和函数都放在 Include/History manager 目录下,开 MT5 后可直接在对应路径查看源码结构。

◍ 给回测补上经纪商漏掉的佣金和滑点

MT5 测试器本身能跑历史回测,但部分经纪商的历史成交里根本没带交易佣金,直接测出来的净值曲线会偏乐观。想看清策略在真实成本下的表现,就得在 EA 里自己维护一份每品种的佣金与滑点表。 下面这个类 CCCM 就是干这事用的:内部用 Keeper 结构存 symbol、comission、shift 三个字段,再用动态数组 comission_data[] 装所有品种。对外只暴露 add / get / remove 三个方法,调用 get 时返回该品种以货币计价的「佣金+滑点」总和。 add 方法先遍历数组,若 symbol 已存在就直接 return,避免重复登记;新写入的佣金和滑点都过了 MathAbs,所以传负数也不会让成本变负。注意一点:佣金和滑点都是按「每次成交」收,不是按一开一平的完整仓位收,一笔仓位拆成 n 次成交就是 n*佣金 + n*滑点。 get 里根据 SYMBOL_TRADE_CALC_MODE 区分品种计价方式,滑点统一用 shift * SYMBOL_TRADE_TICK_VALUE 折成金额。外汇类(SYMBOL_CALC_MODE_FOREX)直接 (佣金+滑点)*volume 算,股票债券类则按营业额百分比另算。参数可以硬编码、也可以从库里拉,我习惯作为 EA 输入参数传进去,换品种不用改代码。 别把佣金当开平仓各收一次 很多人在回测里把佣金设成「每手多少美元」就以为覆盖了往返,实际这个类按成交次数累加。若策略一单分 3 次加仓,成本就是 3 倍而非 1 倍,净值回撤可能被低估 30% 以上。外汇和贵金属杠杆高、滑点跳空频繁,漏算这块容易把亏损策略误判为盈利。

MQL5 / C++
class CCCM
  {
class="kw">private:
   class="kw">struct Keeper
     {
       class="type">class="kw">string            symbol;
       class="type">class="kw">double            comission;
       class="type">class="kw">double            shift;
     };
   Keeper            comission_data[];
class="kw">public:
   class="type">void              add(class="type">class="kw">string symbol,class="type">class="kw">double comission,class="type">class="kw">double shift);
   class="type">class="kw">double            get(class="type">class="kw">string symbol,class="type">class="kw">double price,class="type">class="kw">double volume);
   class="type">void              remove(class="type">class="kw">string symbol);
  };
class="type">void CCCM::add(class="type">class="kw">string symbol,class="type">class="kw">double comission,class="type">class="kw">double shift)
{
class="type">int s=ArraySize(comission_data);
for(class="type">int i=class="num">0;i<s;i++)
  {
    if(comission_data[i].symbol==symbol)
      class="kw">return;
  }
ArrayResize(comission_data,s+class="num">1,s+class="num">1);
Keeper keeper;
keeper.symbol=symbol;
keeper.comission=MathAbs(comission);
keeper.shift=MathAbs(shift);
comission_data[s]=keeper;
}
class="type">void CCCM::remove(class="type">class="kw">string symbol)
{
class="type">int total=ArraySize(comission_data);
class="type">int ind=-class="num">1;
for(class="type">int i=class="num">0;i<total;i++)
  {
    if(comission_data[i].symbol==symbol)
      {
       ind=i;
       class="kw">break;
      }
  }
if(ind!=-class="num">1)
    ArrayRemove(comission_data,ind,class="num">1);
}
class="type">class="kw">double CCCM::get(class="type">class="kw">string symbol,class="type">class="kw">double price,class="type">class="kw">double volume)
{
class="type">int total=ArraySize(comission_data);
for(class="type">int i=class="num">0;i<total;i++)
  {
    if(comission_data[i].symbol==symbol)
      {
       ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);
       class="type">class="kw">double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE);
       class="type">class="kw">double ans;
       class="kw">switch(mode)
         {
          case SYMBOL_CALC_MODE_FOREX :
             ans=(comission_data[i].comission+shift)*volume;

「交易所品种佣金按成交额抽成」

在 MT5 的保证金/手续费计算逻辑里,外汇无杠杆、期货、CFD 及多数交易所衍生品都用「每手固定佣金 + 偏移」直接乘手数,公式统一为 ans=(commission+shift)*volume。 但股票、债券及莫斯科交易所股票/债券这几类(SYMBOL_CALC_MODE_EXCH_STOCKS、SYMBOL_CALC_MODE_EXCH_BONDS、SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX、SYMBOL_CALC_MODE_EXCH_BONDS_MOEX)走的是另一条路:佣金以成交额百分比计,不是每手定额。 具体做法是用价格 × 手数 × 合约规模算出名义成交金额 trading_volume,再乘佣金百分比除以 100,最后加 shift*volume。若你写 EA 算交易成本,漏掉这几类分支会系统性低估股票类品种费用。 外汇与贵金属波动剧烈、杠杆风险高,但上述分支本身只涉及交易所证券,回测股票 EA 时建议直接开 MT5 用 SymbolInfoDouble 打印合约规模验证。

MQL5 / C++
      class="kw">break;
      case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_FUTURES :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_CFD :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_CFDINDEX :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_CFDLEVERAGE :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_STOCKS :
         {
          class="type">class="kw">double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
          ans=trading_volume*comission_data[i].comission/class="num">100+shift*volume;
         }
      class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_FUTURES :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS :
         ans=(comission_data[i].comission+shift)*volume;
         class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_BONDS :
         {
          class="type">class="kw">double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
          ans=trading_volume*comission_data[i].comission/class="num">100+shift*volume;
         }
      class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX :
         {
          class="type">class="kw">double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
          ans=trading_volume*comission_data[i].comission/class="num">100+shift*volume;
         }
      class="kw">break;
      case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX :
         {
          class="type">class="kw">double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);

手续费计算的三种合约模式分支

在 MT5 的佣金核算函数里,不同 SYMBOL_CALC_MODE 会走完全不同的算式。上面这段代码给出了三种典型分支的落地写法,直接决定了回测和实盘里手续费被怎么扣。 对于 FOREX 类账户,常见写法是 trading_volume 乘以 comission_data[i].comission 再除以 100,最后加上 shift*volume 的额外点值偏移;而 SERV_COLLATERAL 抵押服务模式则是 (comission+shift) 直接乘 volume,没有那个除以 100 的归一化。 default 分支直接返回 0,意味着遇到未覆盖的 calc mode 时函数不估算手续费。外汇和贵金属杠杆高、点值敏感,若你的券商 calc mode 落进 default,回测净值会虚高,实盘可能悄悄吞掉利润。 把这段贴进 MT5 的 include 里,打印 comission_data[i].comission 和实际成交回执的 commission 字段,能对出当前账户到底走了哪个 case。

MQL5 / C++
 ans=trading_volume*comission_data[i].comission/class="num">100+shift*volume;
 }
 class="kw">break;
 case SYMBOL_CALC_MODE_SERV_COLLATERAL :
 ans=(comission_data[i].comission+shift)*volume;
 class="kw">break;
 class="kw">default: ans=class="num">0; class="kw">break;
 }
 if(ans!=class="num">0)
 class="kw">return -ans;
 }
 }
class="kw">return class="num">0;
}

◍ 成交按仓位分组的下载与佣金处理

CDealHistoryGetter 的核心价值在于把混乱的成交流按仓位重新分组。标准 HistorySelect 循环拿到的是 DealData 数组,按开仓时间排序但不聚合,多策略同品种交织时(比如持仓递增加仓)根本看不出哪笔出场对应哪笔进场。该类剔除无关成交后,把同 ID 的成交收进结构数组,读报告不再靠人脑拼图。 分组逻辑刻意忽略魔幻数字。两个算法交叠交易同一持仓时, Moscow Exchange 层面无法从技术上彻底分离不同 magic,所以类设计面向的是「下载实盘结果」或「取回测试/优化结果」两种场景:前者品种统计够用,后者测试器单次只跑一种算法,magic 本就无意义。 佣金做了有针对性的和条件式的双模式。类构造时通过引用吃进一个自定义佣金类存到字段,填充 DealData.comission 时写入。若外部文件定义了 ONLY_CUSTOM_COMISSION,佣金字段永远用自定义值覆盖经纪商数值;未定义时仅在经纪商没报佣金的情况下补值,其余忽略。 恢复因子会和终端对不上。getDealsDetales 给的是按平仓日期排的仓位表,测试器用绿线(详细权益)算恢复因子,我们用蓝线(忽略开平之间波动)算,数据源不同公式相同也必然有差。外汇与贵金属杠杆高,这类统计偏差可能放大对回撤的误判,实盘前务必在 MT5 里用 HistorySelect 自跑一遍核对。 getBalance 与 getBalanceWithPL 的区别在盈亏是否进余额。前者拉起始到指定时间的全部成交,循环里把入金出金加减到初始余额并含经纪商佣金调整;后者再叠加按上述原则算的持仓盈亏。日期传 0 则只从首笔起调。

MQL5 / C++
class="kw">struct DealData
  {
   class="type">long              ticket;          class=class="str">"cmt">// Deal ticket
   class="type">long              order;           class=class="str">"cmt">// The number of the order that opened the position
   class="type">class="kw">datetime          DT;              class=class="str">"cmt">// Position open date
   class="type">long              DT_msc;          class=class="str">"cmt">// Position open date in milliseconds
   ENUM_DEAL_TYPE    type;            class=class="str">"cmt">// Open position type
   ENUM_DEAL_ENTRY   entry;           class=class="str">"cmt">// Position entry type
   class="type">long              magic;           class=class="str">"cmt">// Unique position number
   ENUM_DEAL_REASON  reason;          class=class="str">"cmt">// Order placing reason
   class="type">long              ID;              class=class="str">"cmt">// Position ID
   class="type">class="kw">double            volume;          class=class="str">"cmt">// Position volume(lots)
   class="type">class="kw">double            price;           class=class="str">"cmt">// Position entry price
   class="type">class="kw">double            comission;       class=class="str">"cmt">// Commission paid
   class="type">class="kw">double            swap;            class=class="str">"cmt">// Swap
   class="type">class="kw">double            profit;          class=class="str">"cmt">// Profit / loss
   class="type">class="kw">string            symbol;          class=class="str">"cmt">// Symbol
   class="type">class="kw">string            comment;         class=class="str">"cmt">// Comment specified when at opening
   class="type">class="kw">string            ID_external;     class=class="str">"cmt">// External ID
   };
class="kw">struct DealKeeper
  {
   DealData          deals[]; /* List of all deals for this position

常见问题

在EA的OnTester里调用报告生成函数,把参数和结果写进全局缓存,回测结束用OnDeinit输出为文件即可。
按成交手数和合约规模补算佣金,再叠加回测设置里的滑点,差额可能吃掉小周期策略大半利润。
小布可以读取你导出的优化报告,按夏普和回撤排序标出稳健参数组,不用自己翻表格。
在手续费函数里判断合约模式,成交额模式用价格×手数×费率,和固定每手模式分开走if分支。
对同仓位ID的成交只计一次开平佣金,用分组后的唯一订单号做去重再乘费率。