基于快速数学计算的自定义策略测试器·综合运用

基于快速数学计算的自定义策略测试器·综合运用

(3/3)·从字节数组加载到分帧保存优化结果,一套可复用的高速回测骨架

含代码示例实战向 第 3/3 篇
很多人把 MT5 标准测试器当唯一回测入口,却忽略了数学计算模式能绕开行情环境直接跑优化。用对模式,简单策略的近似验证速度可以提升一个量级,还不占用真实模拟的交易开销。

手工算双均线交叉的原型思路

双均线交叉是经典策略里最容易落地的版本,但在数学计算模式下没有交易环境,不能直接调 iMA 这类指标函数,只能自己算均值。为了让优化跑得够快,算法要压到 O(1) 复杂度——也就是均值计算速度不随周期变大而变慢,这里用环形缓冲区预制库来实现。 原型里先定义两个输入参数:快速均线周期 PeriodFastMa 默认 9,慢速均线周期 PeriodSlowMa 默认 16,并各挂一个 CRiSMA 环形缓冲区实例。初始化时要先卡一道逻辑:若快速周期大于等于慢速周期,直接返回 INIT_PARAMETERS_INCORRECT 结束测试,避免优化器瞎组合浪费时间。 加载完报价后,真正的信号判断放在 OnTester 里:当 FastMA 缓冲区的均值大于 SlowMA 均值,倾向开多;反之倾向开空。不过这版还没写实际下单模块,只是把交叉判定骨架搭出来了。外汇与贵金属杠杆高,这种原型仅用于验证计算效率,实盘信号胜率未经验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 MExpert.mq5 |
class=class="str">"cmt">//|                    版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include "Mtester.mqh"
class="macro">#include <RingBuffer\RiSma.mqh>
input class="type">int PeriodFastMa = class="num">9;
input class="type">int PeriodSlowMa = class="num">16;
CRiSMA FastMA;    class=class="str">"cmt">// 计算快速移动平均值的环形缓冲区
CRiSMA SlowMA;    class=class="str">"cmt">// 计算慢速移动平均值的环形缓冲区
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 测试中用到的报价                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">MqlRates Rates[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
  class=class="str">"cmt">//-- 验证参数组合的正确性
  class=class="str">"cmt">//-- 快速移动平均值不能小于慢速移动平均值
  if(PeriodFastMa >= PeriodSlowMa)
      class="kw">return INIT_PARAMETERS_INCORRECT;
  class=class="str">"cmt">//-- 初始化环形缓冲区的周期
  FastMA.SetMaxTotal(PeriodFastMa);
  SlowMA.SetMaxTotal(PeriodSlowMa);
  class=class="str">"cmt">//-- 加载指定品种的报价。
  if(LoadRates(Symbol(), Rates)==-class="num">1)
  {
      printf("报价品种 " + Symbol() + " 未发现。创建相应的报价资源。");
      class="kw">return INIT_FAILED;
  }
  printf("已加载 " + (class="type">class="kw">string)ArraySize(Rates) + " 根柱线, 品种为 " + Symbol());
  class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略描述                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
{

◍ 用循环把每根收盘价喂给双均线

这段片段是 EA 初始化里的一段核心遍历:从索引 1 开始扫过 Rates 数组,把每根 K 线的收盘价依次灌进快速均线和慢速均线的计算器。 注意循环起点是 i=1 而非 0,意味着第 0 根(通常是最新未完成或基准根)被刻意跳过,避免用未定型数据污染均线序列。 FastMA.AddValue 与 SlowMA.AddValue 成对调用,保证两条均线在同一根收盘价上同步更新;注释行标出 EA 主逻辑后续会插在这里,也就是金叉死叉判断的真正落点。 在 MT5 里把这段直接塞进 OnInit 或独立预热函数,跑一遍 EURUSD H1 历史,能确认两条均线在 ArraySize(Rates) 根数据后都已具备有效值,否则后续信号会缺前半段。

MQL5 / C++
  for(class="type">int i = class="num">1; i < ArraySize(Rates); i++)
  {
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      class=class="str">"cmt">// EA 逻辑将位于此处
  }
  class="kw">return class="num">0.0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「用数学模式跑回测先造个假仓位」

纯数学计算模式里没有真实账户环境,MetaTrader 的 PositionSelect 一类接口根本用不上,所谓“仓位”在运行时并不存在。要在回测器里模拟开平仓,就得自己写一个轻量仓位类,只保留回测最关心的几个字段:开平时间、开平价格、交易量、开仓点差和买卖方向。 这个类把核心字段用 private 锁死,只允许通过静态方法 CreateOnBarOpen 生成对象。也就是说,跳过这个方法就无法实例化,能挡掉大部分手误改数据的坑。下面这段是类的骨架,字段和接口一目了然。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Mtester.mqh |
class=class="str">"cmt">//|                        版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include "rates.mqh"
class="macro">#include "Type2Char.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 虚拟仓位类, 用于数学计算模式的测试器                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMposition : class="kw">public CObject
{
class="kw">private:
   class="type">class="kw">datetime    m_time_open;
   class="type">class="kw">datetime    m_time_close;
   class="type">class="kw">double      m_price_open;
   class="type">class="kw">double      m_price_close;
   class="type">class="kw">double      m_volume;
   class="type">int         m_spread;
   class="type">ENUM_POSITION_TYPE m_type;
class="kw">public:
                CMposition(class="type">void);
   class="kw">static class="type">int  Sizeof(class="type">void);
   class="type">bool        IsActive(class="type">void);
   class="type">class="kw">datetime    TimeOpen(class="type">void);
   class="type">class="kw">datetime    TimeClose(class="type">void);
   class="type">class="kw">double      PriceOpen(class="type">void);
   class="type">class="kw">double      PriceClose(class="type">void);
   class="type">class="kw">double      Volume(class="type">void);
   class="type">class="kw">double      Profit(class="type">void);
   class="type">ENUM_POSITION_TYPE PositionType(class="type">void);
   class="kw">static CMposition*  CreateOnBarOpen(class="type">MqlRates& bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double vol);
   class="type">void        CloseOnBarOpen(class="type">MqlRates& bar);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 一个 CMposition 仓位占用 class="num">45 个字节的数据                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CMposition::Sizeof(class="type">void)
{
   class="kw">return class="num">48;
}
CMposition::CMposition(class="type">void):m_time_open(class="num">0),
                             m_time_close(class="num">0),
                             m_price_open(class="num">0.0),
代码里 Sizeof 注释写“45 字节”,函数体却 return 48,实际对象含 7 个基础字段,在 64 位 MT5 环境按对齐算下来更靠近 48。开 MT5 新建 mqh 把这段贴进去,编译后看 sizeof(CMposition) 输出就能验证,外汇和贵金属模拟回测本身高风险,数字不对就别急着接策略。 以后若想加滑点、手续费字段,直接往 private 里补,再改 CreateOnBarOpen 的参数表即可,扩展成本很低。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Mtester.mqh |
class=class="str">"cmt">//|                        版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include "rates.mqh"
class="macro">#include "Type2Char.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 虚拟仓位类, 用于数学计算模式的测试器                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMposition : class="kw">public CObject
{
class="kw">private:
   class="type">class="kw">datetime    m_time_open;
   class="type">class="kw">datetime    m_time_close;
   class="type">class="kw">double      m_price_open;
   class="type">class="kw">double      m_price_close;
   class="type">class="kw">double      m_volume;
   class="type">int         m_spread;
   class="type">ENUM_POSITION_TYPE m_type;
class="kw">public:
                CMposition(class="type">void);
   class="kw">static class="type">int  Sizeof(class="type">void);
   class="type">bool        IsActive(class="type">void);
   class="type">class="kw">datetime    TimeOpen(class="type">void);
   class="type">class="kw">datetime    TimeClose(class="type">void);
   class="type">class="kw">double      PriceOpen(class="type">void);
   class="type">class="kw">double      PriceClose(class="type">void);
   class="type">class="kw">double      Volume(class="type">void);
   class="type">class="kw">double      Profit(class="type">void);
   class="type">ENUM_POSITION_TYPE PositionType(class="type">void);
   class="kw">static CMposition*  CreateOnBarOpen(class="type">MqlRates& bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double vol);
   class="type">void        CloseOnBarOpen(class="type">MqlRates& bar);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 一个 CMposition 仓位占用 class="num">45 个字节的数据                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CMposition::Sizeof(class="type">void)
{
   class="kw">return class="num">48;
}
CMposition::CMposition(class="type">void):m_time_open(class="num">0),
                             m_time_close(class="num">0),
                             m_price_open(class="num">0.0),

持仓对象的活性判定与字段读取

在自建的 CMposition 类里,平仓时间字段 m_time_close 被初始化为 0.0,这是区分持仓是否还在场内的关键标记。只要该值等于 0,就说明这笔单子没平过,逻辑上仍处于活动状态。 IsActive() 方法直接返回 m_time_close == 0 的布尔结果。实盘里你可以用它快速过滤出当前挂着的仓位,而不必每次去调 OrderSelect 或 PositionGetInteger,省掉重复查询开销。 类里还暴露了一组只读接口:TimeOpen / TimeClose 取开平时间,PriceOpen / PriceClose 取成交价,Volume 取手数,PositionType 返回 ENUM_POSITION_TYPE 枚举。把这些封装成成员函数后,EA 内部统计浮盈回撤或持仓时长时,直接 pos.PriceOpen() 就行,比全局函数调用更顺手。 外汇与贵金属杠杆高,这类封装只解决代码组织问题,不降低品种波动风险;跑历史回测时建议先用少量 tick 验证字段读写是否与预期一致。

MQL5 / C++
m_price_close(class="num">0.0),
m_volume(class="num">0.0)
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| True, 如果已平仓                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMposition::IsActive()
{
   class="kw">return m_time_close == class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 开仓时间                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">datetime CMposition::TimeOpen(class="type">void)
{
   class="kw">return m_time_open;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 平仓时间                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">datetime CMposition::TimeClose(class="type">void)
{
   class="kw">return m_time_close;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 开仓价格                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMposition::PriceOpen(class="type">void)
{
   class="kw">return m_price_open;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 平仓价格                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMposition::PriceClose(class="type">void)
{
   class="kw">return m_price_close;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 仓位交易量                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMposition::Volume(class="type">void)
{
   class="kw">return m_volume;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回交易仓位的类型                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ENUM_POSITION_TYPE CMposition::PositionType(class="type">void)
{
   class="kw">return m_type;
}

◍ 用柱线开盘价回测仓位盈亏

做历史回测时,把每笔仓位挂在某根 K 线开盘价成交,是最干净的入场假设。下面这段仓位类方法,把「开仓—平仓—算利润」三件事压到了最小可用单元。 Profit() 先判断 IsActive(),若持仓未了结直接返回 0.0,避免拿未实现浮盈当结果。接着用 sign 把多空方向转成 ±1:买仓 pips = 平仓价减开盘价,卖仓反过来。profit = pips × volume,这里 volume 若以「标准手」计,1 手黄金波动 1 美元点值约 100 美元,回测时自己乘合约规格才不会算错量级。 CreateOnBarOpen() 只记 time、open、volume、type 四个字段,不立刻给平仓价——意思很明确:先挂单,后面对照下一根 bar 的开盘来决定退出。CloseOnBarOpen() 就是把传入 bar.open 写进 m_price_close,相当于「信号触发后,下一根开盘滑点成交」。 实盘别直接照抄:外汇和贵金属点差、滑点可能吃掉这根开盘价的理论优势,回测盈利不等于实盘概率占优,拿 MT5 策略测试器跑一遍 EURUSD 15M 才能看见真实分布。

MQL5 / C++
class="type">class="kw">double CMposition::Profit(class="type">void)
{
  if(IsActive())
    class="kw">return class="num">0.0;
  class="type">int sign = m_type == POSITION_TYPE_BUY ? class="num">1 : -class="num">1;
  class="type">class="kw">double pips = (m_price_close - m_price_open)*sign;
  class="type">class="kw">double profit = pips*m_volume;
  class="kw">return profit;
}

class="kw">static CMposition* CMposition::CreateOnBarOpen(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume)
{
  CMposition* position = new CMposition();
  position.m_time_open = bar.time;
  position.m_price_open = bar.open;
  position.m_volume = volume;
  position.m_type = pos_type;
  class="kw">return position;
}

class="type">void CMposition::CloseOnBarOpen(class="type">MqlRates &bar)
{
  m_price_close = bar.open;
  m_time_close = bar.time;
}

「用类把仓位管起来」

写策略测试器时,最好单独建一个交易模块类来托管所有仓位,相当于给 MT5 原生交易函数做一层轻量封装。仓位数据本身也得塞进这个类里:用两个 CArrayObj 集合分流,Active 装当前未平掉的仓位,History 装已经平仓进历史的部分。 这个类只暴露两个动作就够了。EntryAtOpenBar 按指定方向和手数在柱线开盘时开仓;CloseAtOpenBar 按索引把对应仓位平掉。价格统一取所传柱线的开盘价,实现极简单、回测跑得快,但代价是无法杜绝“偷窥未来”——用开盘价模拟成交本身就带这种先天缺陷,外汇和贵金属波动大,这类简化模型的高风险得心里有数。 CMtrade 类的骨架非常薄:构造里把 Active 的 FreeMode 设为 false,析构时改回 true 并 Clear,避免对象残留;开仓无非是调 CMposition::CreateOnBarOpen 拿到新仓位再 Add 进 Active,平仓则是把仓位从 Active 挪去 History。整套组件齐了,EA 各部件就能直接进回测。 别把开盘价成交当真实盘口 用 EntryAtOpenBar 跑出来的胜率会偏乐观,因为实盘点差和滑点在开盘跳空时可能吞掉那根阳线。想逼近真实,至少把点差加进 bar.open 再比一次。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Mtester.mqh |
class=class="str">"cmt">//|                                                                             |
class=class="str">"cmt">//|                                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include "Mposition.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于打开虚拟仓位的交易模块                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMtrade
{
class="kw">public:
             CMtrade(class="type">void);
             ~CMtrade();
  CArrayObj  Active;
  CArrayObj  History;
  class="type">void       EntryAtOpenBar(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume);
  class="type">void       CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 默认构造器                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMtrade::CMtrade(class="type">void)
{
   Active.FreeMode(false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 删除所有剩余仓位                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMtrade::~CMtrade()
{
   Active.FreeMode(true);
   Active.Clear();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建新仓位, 并将其加入                                            |
class=class="str">"cmt">//| 活跃仓位清单。                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMtrade::EntryAtOpenBar(class="type">MqlRates &bar, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double volume)
{
   CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume);
   Active.Add(pos);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按照 pos_index 索引指定柱线的开盘价                               |

用一根K线开盘价了结持仓

在 MT5 的 EA 逻辑里,若想让某个持仓在下一根 K 线一开盘就平掉,可把平仓动作绑定到 Bar 开放事件。下面这段实现把活跃持仓对象取出、触发其开盘平仓方法、再从活跃表移除并转入历史表。

MQL5 / C++
class=class="str">"cmt">//| 将所有仓位平仓                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMtrade::CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index)
{
   CMposition* pos = Active.At(pos_index);
   pos.CloseOnBarOpen(bar);
   Active.Delete(pos_index);
   History.Add(pos);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆解:第一行注释仅为分隔美化,无执行意义。函数 CloseAtOpenBar 接收一根 MqlRates 类型的 bar 引用和持仓在活跃列表里的下标 pos_index。 Active.At(pos_index) 按下标拿回活跃持仓指针;pos.CloseOnBarOpen(bar) 以该根 bar 的开盘价挂平仓指令。随后 Active.Delete 把这条从活跃容器删掉,History.Add 把它塞进历史容器,保证后续统计不漏单。 外汇与贵金属杠杆高,这类市价转开盘价平仓在跳空时可能滑点偏大,实盘前建议在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,观察成交价偏离开盘价的概率。

MQL5 / C++
class=class="str">"cmt">//| 将所有仓位平仓                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMtrade::CloseAtOpenBar(class="type">MqlRates &bar, class="type">int pos_index)
{
   CMposition* pos = Active.At(pos_index);
   pos.CloseOnBarOpen(bar);
   Active.Delete(pos_index);
   History.Add(pos);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把双均线策略丢进优化器跑一遍

组件拼齐后,双均线策略就能直接在数学优化器里跑了。OnTester 的逻辑很直白:用 for 循环逐根扫报价,快线 SMA 高于慢线就认多、否则认空;活跃仓位方向不对就平掉,空仓则按当前方向开新仓。循环结束再统算已平仓位总利润回传测试器。 实跑参数全扫时,我习惯在「慢速完整算法」下做 1000 次通关,每次吃一年的一分钟历史。i7 机器上整轮优化大概 1 分钟出图,并不会卡。但眼下尴尬的是,优化结果只给一个虚拟利润数字,拿来筛参数太单薄。 想要更细的判别维度,就得自己定义一套优化数据格式,并写加载机制。这部分留到下一节拆。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 MExpert.mq5 |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link     "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <RingBuffer\RiSma.mqh>
class="macro">#include "Mtester.mqh"
input class="type">int PeriodFastMa = class="num">9;
input class="type">int PeriodSlowMa = class="num">16;
CRiSMA FastMA;     class=class="str">"cmt">// 计算快速移动平均值的环形缓冲区
CRiSMA SlowMA;     class=class="str">"cmt">// 计算慢速移动平均值的环形缓冲区
CMtrade Trade;     class=class="str">"cmt">// 用于虚拟计算的交易模块
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 测试中用到的报价                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">MqlRates Rates[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
  class=class="str">"cmt">//-- 验证参数组合的正确性
  class=class="str">"cmt">//-- 快速移动平均值不能小于慢速移动平均值
  class=class="str">"cmt">//if(PeriodFastMa >= PeriodSlowMa)
  class=class="str">"cmt">//   class="kw">return INIT_PARAMETERS_INCORRECT;
  class=class="str">"cmt">//-- 初始化环形缓冲区的周期
  FastMA.SetMaxTotal(PeriodFastMa);
  SlowMA.SetMaxTotal(PeriodSlowMa);
  class=class="str">"cmt">//-- 加载指定品种的报价。
  if(LoadRates(Symbol(), Rates)==-class="num">1)
  {
     printf("报价品种 " + Symbol() + " 未发现。创建相应的报价资源。");
     class="kw">return INIT_FAILED;
  }
  printf("已加载 " + (class="type">class="kw">string)ArraySize(Rates) + " 根柱线, 品种为 " + Symbol());
  class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略描述                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
{

「双均线反转信号的逐根平仓与开仓逻辑」

这段循环是策略回测里真正干活的部分:从第二根 K 线开始扫 Rates 数组,每根 bar 都先喂给快慢两条 SMA,再决定多空倾向。FastMA.SMA() 大于 SlowMA.SMA() 时判为买,否则判为卖,信号在两根均线交叉的瞬间翻转。 翻转后第一件事是清掉反向仓:内层循环从持仓列表尾部倒序遍历,只要某仓的 PositionType 和当前 pos_type 不一致,就在下一根 bar 开盘价 CloseAtOpenBar 平掉。倒序遍历是为了避免删除元素时下标错位,这是 MT5 持仓容器操作的常见坑。 若清完之后 Active.Total() 为 0,说明场上无任何仓,直接在 Rates[i+1] 开盘价按 1.0 手开新仓。注意开仓用的是 i+1 而非 i,意味着信号在当根确认、下一根执行,能回避未来函数。 循环结束后另起一段统计:把 History 里每笔平仓的 Profit() 累加进 double profit,最后 return profit 作为该段回测的总盈亏。外汇与贵金属杠杆高,这种反转策略在震荡市可能连续小亏,实盘前务必用 MT5 策略测试器跑足样本。

MQL5 / C++
  for(class="type">int i = class="num">1; i < ArraySize(Rates)-class="num">1; i++)
  {
      class="type">MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      class="type">ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      class=class="str">"cmt">//-- 与当前信号相反的所有仓位平仓
      for(class="type">int k = Trade.Active.Total()-class="num">1; k >= class="num">0 ; k--)
      {
       CMposition* pos = Trade.Active.At(k);
       if(pos.PositionType() != pos_type)
          Trade.CloseAtOpenBar(Rates[i+class="num">1], k);  
      }
      class=class="str">"cmt">//-- 如果没有仓位, 则在指定方向上开新仓。
      if(Trade.Active.Total() == class="num">0)
         Trade.EntryAtOpenBar(Rates[i+class="num">1], pos_type, class="num">1.0);
  }
  class="type">class="kw">double profit = class="num">0.0;
  for(class="type">int i = class="num">0; i < Trade.History.Total(); i++)
  {
      CMposition* pos = Trade.History.At(i);
      profit += pos.Profit();
  }
  class="kw">return profit;
}

用分帧把优化中间数据捞出来

MT5 的策略测试器有一套叫分帧(frame)的机制,专门在优化模式下搬运自定义二进制数据。它只能在 OnTesterInit、OnTesterDeinit、OnTesterPass 三个无参无返回值的钩子里用,靠 FrameAdd 往测试器塞数据、FrameNext 把数据还原出来。 实测跑一段数学计算模式的优化,参数 Param 从 10 到 90、步长 10 共 9 组,选「慢速完整算法」就能在日志里看到每一帧的编号、标识符和对应 Param 值被逐条打印。这些数据原本只在优化进程里,但分帧让它们可被图表上的另一实例接收。 这里有个容易忽略的点:带事件处理函数的 EA 在优化时会启动两次,一次在优化器内、一次在实时图表上。优化器生成的新帧,图表实例能直接拿到,等于同一份源码被两个上下文分别跑。 下面这段示例在 OnTester 里拼了一个 5 字节数组,用 FrameAdd("sample",1,Param,data) 发帧,OnTesterPass 里用 FrameNext 取出来只做了 printf。真实场景里你可以把 data 换成指标矩阵或权重表,在 OnTesterPass 里落盘或喂给小布做聚类,而不只是打印。外汇与贵金属优化存在过拟合高风险,回测漂亮不等于实盘能跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                      OnTesterSample.mq5 |
class=class="str">"cmt">//|            版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
input class="type">int Param = class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTesterInit 函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterInit()
{
   printf("优化开始");      
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略通关启于此处                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
{
   class="type">uchar data[class="num">5] = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5};           class=class="str">"cmt">// 生成分帧数据
   FrameAdd("sample", class="num">1, Param, data);    class=class="str">"cmt">// 用提供的数据创建一个新分帧
   class="kw">return class="num">3.0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 最后添加的优化分帧可以在这里获得                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterPass()
{
   class="type">ulong pass = class="num">0;
   class="type">class="kw">string name = "";
   class="type">ulong id = class="num">0;
   class="type">class="kw">double value = class="num">0.0;
   class="type">uchar data[];
   FrameNext(pass, name, id, value, data);
   class=class="str">"cmt">//-- 创建一个通关文件并将其添加到 zip 压缩文件中
   printf("收到新分帧 # " + (class="type">class="kw">string)pass + ". 名称: " + (class="type">class="kw">string)name + " ID: " + (class="type">class="kw">string)id + " 参数值: " + DoubleToString(value, class="num">0));
}

◍ 优化收尾钩子与分帧日志实测

在 MT5 策略测试器的优化流程里,OnTesterDeinit 是整轮参数扫描跑完后的收尾事件。它不干预交易逻辑,只适合做资源清理或打印一句状态,避免把重计算塞进这里拖慢批次。 下面这段就是最小可用的收尾函数: void OnTesterDeinit() { printf("优化完成"); } 第一行 void 表示无返回值;OnTesterDeinit() 是测试器反初始化时自动调用的函数名;printf("优化完成") 向专家日志输出一行文本,标记本轮优化结束。 实测日志里能看到分帧(frame)是乱序到达的:从 2017.12.19 16:58:08.389 到 .450,Param 值 10~90 对应的分帧 # 按 1、0、4、5、2、3、6、7、8 的顺序回传,说明优化内核并不保证序号连续下发。 整轮 EURUSD M15 的 9 个参数组合从开始到打出「优化完成」只用了约 693 毫秒(.101 到 .794)。外汇与贵金属优化结果受点差和滑点影响大,这类耗时数据仅作环境参考,实盘参数迁移须重测,高风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTesterDeinit 函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterDeinit()
{
   printf("优化完成");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.101 OnTesterSample(EURUSD,M15)    优化开始
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.389 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">1. 名称: sample ID: class="num">1 Param 值: class="num">20
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.396 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">0. 名称: sample ID: class="num">1 Param 值: class="num">10
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.408 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">4. 名称: sample ID: class="num">1 Param 值: class="num">50
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.426 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">5. 名称: sample ID: class="num">1 Param 值: class="num">60
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.426 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">2. 名称: sample ID: class="num">1 Param 值: class="num">30
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.432 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">3. 名称: sample ID: class="num">1 Param 值: class="num">40
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.443 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">6. 名称: sample ID: class="num">1 Param 值: class="num">70
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.444 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">7. 名称: sample ID: class="num">1 Param 值: class="num">80
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.450 OnTesterSample(EURUSD,M15)    收到新分帧 # class="num">8. 名称: sample ID: class="num">1 Param 值: class="num">90
class="num">2017.12.class="num">19 class="num">16:class="num">58:class="num">08.794 OnTesterSample(EURUSD,M15)    优化完成

「把历史仓位压成字节流再塞进 zip」

自定义回测框架跑优化时,每过一关都要把已平仓记录留痕。直接存结构体太占地方,正路是把 CMposition 里的单笔仓位序列化成 uchar 字节数组,再聚合成一帧传给 OnTesterPass(),思路和 MqlRates 把报价拍扁成字节流一致。 序列化靠 TypeToChar 联合做中介,时间用 datetime、价格与仓量用 double、点差与类型用 int/char,逐字段 ArrayCopy 进目标数组。下面这段 ToCharArray 就是单仓转字节的核心,open/close 时间各占 sizeof(datetime),开平价格加仓量各占 sizeof(double),点差占 sizeof(int),类型占 1 字节——算下来一笔仓固定耗约 41 字节(按 8+8+8+8+8+4+1 的常见对齐)。 [CODE] //+------------------------------------------------------------------+

//将仓位转换为字节表示形式的数组

//+------------------------------------------------------------------+ int CMposition::ToCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- 复制开仓时间 type2char.time_value = m_time_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- 复制平仓时间 type2char.time_value = m_time_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- 复制开仓价格 type2char.double_value = m_price_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制平仓价格 type2char.double_value = m_price_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制仓量 type2char.double_value = m_volume; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- 复制品种点差 type2char.int_value = m_spread; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int)); offset += sizeof(int); //-- 复制仓位类型 type2char.int_value = m_type; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char)); offset += sizeof(int); //-- 返回最后的偏移量 return offset; } //+------------------------------------------------------------------+

//从字节数组中加载仓位

//+------------------------------------------------------------------+ int CMposition::FromCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- 复制开仓时间 ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime)); m_time_open = type2char.time_value; 逐行拆解:函数入口记 offset 为写入起点;type2char 联合把 m_time_open 等强类型值映射到 char_array;每次 ArrayCopy 按字段长度搬字节并推进 offset;末尾返回新偏移,调用方据此把下一笔仓接在后面。FromCharArray 方向相反,从字节流按同顺序捞回各字段写进对象成员。 CMTrade 类负责遍历历史集合,双向都能跑:历史仓→uchar 数组,或字节数组→历史仓清单,全程只读全局虚拟仓数组,不额外拷内存,穷举上千笔成交时开销可控。 每关结束拿到字节帧后,别散着放。用 zip 类把帧写成存档内独立文件,OnTesterPass 里往全局 Zip 对象添新文件即可;优化收尾在 OnTesterDeinit 存盘,默认名 Optimization.zip,落在 Files 文件夹。实测千笔成交的帧压完普遍有 3 到 5 倍压缩比,后续拖进分析端读 zip 就行。外汇与贵金属品种点差跳变频繁,回测仓里的 m_spread 若取自实时上下文,跨周期重载时要复核其代表性,这类品种高杠杆波动可能引发与历史帧不符的偏差。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将仓位转换为字节表示形式的数组                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CMposition::ToCharArray(class="type">int dst_start, class="type">uchar &array[])
{
  class="type">int offset = dst_start;
  class=class="str">"cmt">//-- 复制开仓时间
  type2char.time_value = m_time_open;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">datetime));
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制平仓时间
  type2char.time_value = m_time_close;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">datetime));
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制开仓价格
  type2char.double_value = m_price_open;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double));
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制平仓价格
  type2char.double_value = m_price_close;  
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double));
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制仓量
  type2char.double_value = m_volume;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">class="kw">double));
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制品种点差
  type2char.int_value = m_spread;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">int));
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 复制仓位类型
  type2char.int_value = m_type;
  ArrayCopy(array, type2char.char_array, offset, class="num">0, class="kw">sizeof(class="type">char));
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 返回最后的偏移量
  class="kw">return offset;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从字节数组中加载仓位                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CMposition::FromCharArray(class="type">int dst_start, class="type">uchar &array[])
{
  class="type">int offset = dst_start;
  class=class="str">"cmt">//-- 复制开仓时间
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime));
  m_time_open = type2char.time_value;

从字节流里抠出每笔历史仓位的字段

反序列化历史仓位时,靠一个偏移量 offset 在字节数组里逐段切数据。每读一种类型就前进对应的字节数:datetime 占 8 字节,double 占 8 字节,int 占 4 字节,顺序错一位全盘皆乱。 下面这段是拆包的核心循环尾部,平仓时间、开平价格、仓量、点差、仓位类型依次落位。注意 m_type 做了强转,把 int 还原成 ENUM_POSITION_TYPE,否则后续判断多空会失效。

MQL5 / C++
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制平仓时间
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime));
  m_time_close = type2char.time_value;
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制开仓价格
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_price_open = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制平仓价格
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_price_close = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制仓量
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_volume = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制品种点差
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int));
  m_spread = type2char.int_value;
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 复制仓位类型
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int));
  m_type = (class="type">ENUM_POSITION_TYPE)type2char.int_value;
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 返回最后的偏移量
  class="kw">return offset;
}
真正干脏活的是 TypeToChar 这个 union:char_array[128] 与其他基础类型共享内存,写进去的字节直接按类型 reinterpret。外汇贵金属历史回放里,这种零拷贝解析能把单账户上万笔平仓记录的加载耗时压到毫秒级,但内存布局一旦跨平台字节序不同就可能读崩。 ToCharArray 负责反向打包,total_size 按 CMposition::Sizeof()*History.Total() 算。若数组为空或 ArrayResize 对不上大小,直接返回 false 并打印函数名,开 MT5 跑时看日志能立刻定位是历史为空还是尺寸算错。

MQL5 / C++
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制平仓时间
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">datetime));
  m_time_close = type2char.time_value;
  offset += class="kw">sizeof(class="type">class="kw">datetime);
  class=class="str">"cmt">//-- 复制开仓价格
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_price_open = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制平仓价格
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_price_close = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制仓量
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">class="kw">double));
  m_volume = type2char.double_value;
  offset += class="kw">sizeof(class="type">class="kw">double);
  class=class="str">"cmt">//-- 复制品种点差
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int));
  m_spread = type2char.int_value;
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 复制仓位类型
  ArrayCopy(type2char.char_array, array, class="num">0, offset, class="kw">sizeof(class="type">int));
  m_type = (class="type">ENUM_POSITION_TYPE)type2char.int_value;
  offset += class="kw">sizeof(class="type">int);
  class=class="str">"cmt">//-- 返回最后的偏移量
  class="kw">return offset;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 转换简单类型至字节数组                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
union TypeToChar
{
   class="type">uchar    char_array[class="num">128];
   class="type">int      int_value;
   class="type">class="kw">double   double_value;
   class="type">float    float_value;
   class="type">long     long_value;
   class="type">class="kw">short    short_value;
   class="type">bool     bool_value;
   class="type">class="kw">datetime time_value;
   class="type">char     char_value;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将历史仓位清单转换为压缩的 zip 存档                                                  |
class=class="str">"cmt">//| 并形成字节数组。若成功, 返回 true                                                   |
class=class="str">"cmt">//| 否则 false。                                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMtrade::ToCharArray(class="type">uchar &array[])
{
   class="type">int total_size = CMposition::Sizeof()*History.Total();
   if(total_size == class="num">0)
   {
      printf(__FUNCTION__ +  ": 接收的数组为空");
      class="kw">return false;
   }
   if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size)
   {
      printf(__FUNCTION__ +  ": 调整接收数组大小失败");
      class="kw">return false;
   }
   class=class="str">"cmt">//-- 将仓位存储在字节流中
   for(class="type">int offset = class="num">0, i = class="num">0; offset < total_size; i++)
   {
      CMposition* pos = History.At(i);

◍ 把历史仓位塞进回测帧里

OnTester 是 MT5 策略测试器的钩子函数,在每轮优化结束时触发。上面这段逻辑先遍历 Rates 数组(从索引 1 到倒数第二根),用快、慢两条 SMA 的差值决定多空倾向:FastMA.SMA() 大于 SlowMA.SMA() 时偏多,否则偏空,这属于典型的双均线信号还原。 遇到与当前信号反向的活跃仓位,代码在下一根 K 线开盘价直接平仓;若当前无持仓,则按 1.0 手在下一根开盘价同向开仓。注意循环里 i 从 1 开始、平仓和开仓都用 Rates[i+1],意味着信号在当前 bar 收盘确认、执行顺延一根,能规避看未来数据的坑。 真正关键的是末尾三行:Trade.ToCharArray(array) 把全部历史仓位压成 uchar 字节流,再经 FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array) 推到自定义帧。回测里仓位对象存在内存,测试器默认不导出,靠这步才能把成交序列甩给外部做离线统计。外汇与贵金属杠杆高,这套还原仅用于历史验证,实盘信号失效概率不低。 最后返回 Trade.History.Total(),即历史平仓总数,可作为优化目标的粗糙代理指标。想验证就开 MT5 把这段贴进 EA 的 OnTester,跑完在帧监视器里读 array 长度,除以单个仓位字节数就能反推成交笔数。

MQL5 / C++
class="type">class="kw">double OnTester()
{
   for(class="type">int i = class="num">1; i < ArraySize(Rates)-class="num">1; i++)
   {
      class="type">MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      class="type">ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      class=class="str">"cmt">//-- 与当前信号相反的所有仓位平仓
      for(class="type">int k = Trade.Active.Total()-class="num">1; k >= class="num">0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+class="num">1], k);
      }
      class=class="str">"cmt">//-- 如果没有仓位, 则在指定方向上开新仓。
      if(Trade.Active.Total() == class="num">0)
         Trade.EntryAtOpenBar(Rates[i+class="num">1], pos_type, class="num">1.0);
   }
   class="type">uchar array[];
   class=class="str">"cmt">//-- 获取历史仓位的字节表示
   Trade.ToCharArray(array);
   class=class="str">"cmt">//-- 将字节表示加载到分帧中并将其发送给进一步处理
   FrameAdd(MTESTER_STR, MTESTER_ID, class="num">0.0, array);
   class="kw">return Trade.History.Total();
}

「把优化通关直接压进本地 zip」

MT5 的遗传优化跑完一轮,每过一个通关参数组都会触发 OnTesterPass,这时不去手点报告,而是把帧数据直接封包进内存里的 zip 对象,能省掉大量人工导出的时间。上面这段代码里,FrameNext 把当前通关的 name、id、value 和二进制 data 取出来,文件名拼成「名称_编号_轮次_目标值.hps」,再 new 一个 CZipFile 丢进 Zip.AddFile,通关就进了存档。 落到硬盘只发生在 OnTesterDeinit:Zip.SaveZipToFile(OptimizationFile, FILE_COMMON) 一口气把整包写进终端公共目录,随后 printf 打出 Zip.TotalElements() 的总数。你在跑 EA 批量优化时,若看到「保存的优化通关总数」明显少于预期,优先查 FILE_COMMON 路径权限,而不是怀疑模型。 外汇与贵金属品种点差跳动大,遗传优化通关结果仅代表历史样本拟合,实盘复制存在滑点侵蚀概率,开 MT5 把这段挂到自己的优化脚本里跑一遍最直观。

MQL5 / C++
class="type">void OnTesterPass()
{
  class="type">ulong pass = class="num">0;
  class="type">class="kw">string name = "";
  class="type">ulong id = class="num">0;
  class="type">class="kw">double value = class="num">0.0;
  class="type">uchar data[];
  FrameNext(pass, name, id, value, data);
  class=class="str">"cmt">//-- 创建一个通关文件并将其添加到 zip 压缩文件中
  printf("接收新存档大小 " + (class="type">class="kw">string)ArraySize(data));
  class="type">class="kw">string file_name = name + "_" + (class="type">class="kw">string)id + "_" + (class="type">class="kw">string)pass + "_" + DoubleToString(value, class="num">5)+".hps";
  CZipFile* zip_file = new CZipFile(file_name, data);
  Zip.AddFile(zip_file);
}
CZip    Zip;    class=class="str">"cmt">// Zip 存档由优化通关填充
class="type">void OnTesterDeinit()
{
  Zip.SaveZipToFile(OptimizationFile, FILE_COMMON);
  class="type">class="kw">string f_totals = (class="type">class="kw">string)Zip.TotalElements();
  printf("优化完成。保存的优化通关总数: " + f_totals);
}

把优化存档变成可读的余额图

做一次 MT5 优化往往产出成百上千组通关参数,传统做法只能反复跑测试器看结果。这里思路换成:优化时把所有通关信息打包进一个 zip 存档,之后用独立的分析器程序加载它,单次优化、多次回看,不再消耗重跑时间。 分析器以智能系统形式挂载在图表上,基于 CPanel 图形引擎绘制虚拟余额曲线,并输出成交数、总利润、总亏损、盈利因子、收益预期等汇总统计。存档里的数据始终以压缩格式驻留内存,只有被点名的那一组通关才解压成对象,内存占用可控。 主入口代码极薄:建一个 CAnalyzePanel 对象,设好 800×630 的窗口与坐标,调 LoadFilePasses 读入默认 Optimization.zip,再 Show 挂到图上。图表事件只截获文本输入结束和图形点击,转成 CEvent 喂给面板。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  mAnalizer.mq5 |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "mAnalizerPanel.mqh"
CAnalyzePanel Panel;            class=class="str">"cmt">// 声明分析器面板对象,负责所有界面与计算
input class="type">class="kw">string FilePasses = "Optimization.zip";  class=class="str">"cmt">// 输入项:待加载的优化存档名,默认放共有目录
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   Panel.Width(class="num">800);            class=class="str">"cmt">// 面板宽 class="num">800 像素
   Panel.Height(class="num">630);           class=class="str">"cmt">// 面板高 class="num">630 像素
   Panel.XCoord(class="num">10);            class=class="str">"cmt">// 左上角 X 坐标 class="num">10
   Panel.YCoord(class="num">20);            class=class="str">"cmt">// 左上角 Y 坐标 class="num">20
   Panel.LoadFilePasses(FilePasses);  class=class="str">"cmt">// 从共有目录载入指定 zip 存档
   Panel.Show();                class=class="str">"cmt">// 将面板显示在当前图表上
   ChartRedraw();               class=class="str">"cmt">// 强制重绘图表使面板立即可见
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);      class=class="str">"cmt">// 初始化成功
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逆初函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
面板内部用四个控件数组分别装统计名称、全部成交、多头成交、空头成交的数值,十项参数乘三组方向共 40 个文本标签,靠 CreateStatElements 一次性自动生成。随附的示例 MTester.zip 里带了约 160 次通关的 Optimization.zip,不用自己跑优化也能直接验面板。 外汇与贵金属市场杠杆高、滑点跳空频繁,这类离线分析仅基于历史优化数据,对实盘走势只具参考倾向,不构成任何方向暗示。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  mAnalizer.mq5 |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "mAnalizerPanel.mqh"
CAnalyzePanel Panel;
input class="type">class="kw">string FilePasses = "Optimization.zip";
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   Panel.Width(class="num">800);
   Panel.Height(class="num">630);
   Panel.XCoord(class="num">10);
   Panel.YCoord(class="num">20);
   Panel.LoadFilePasses(FilePasses);
   Panel.Show();
   ChartRedraw();
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逆初函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 面板退出与图表事件的分发逻辑

指标或 EA 卸载时,OnDeinit 里只做一件事:调用 Panel.Hide() 把自定义面板从图表上撤掉,避免残留对象。reason 参数虽传入但未参与判断,说明无论手动移除还是重编译,清理动作一致。 图表交互靠 OnChartEvent 接住两类事件:CHARTEVENT_OBJECT_ENDEDIT(编辑框结束输入)和 CHARTEVENT_OBJECT_CLICK(点击控件)。各自构造 CEventChartEndEdit / CEventChartObjClick 后丢给 Panel.Event() 处理,末尾 ChartRedraw() 强制重绘,保证界面状态即时刷新。 下面的 CAnalizePanel 类继承自 CElChart,私有成员用四组 CArrayObj 分别装统计说明、全部成交、多头、空头的结果,另挂一个 CTradeAnalize 做计算。图形侧有 m_name_analyze 主窗名与 m_np 的“Pass #”文本对象,结构上把数据层和展示层绑在同一面板类里。 在 MT5 里开个空指标把这段 OnChartEvent 抄进去,挂一个可编辑文本对象,改完内容松开鼠标,若面板数值跟着变,就说明事件链路通了。外汇与贵金属品种波动剧烈,这类面板仅作统计呈现,不代表任何方向确定性。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
{
  Panel.Hide();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent 函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                  const class="type">long &lparam,
                  const class="type">class="kw">double &dparam,
                  const class="type">class="kw">string &sparam)
{
  class="kw">switch(id)
  {
    case CHARTEVENT_OBJECT_ENDEDIT:
    {
      CEventChartEndEdit event(sparam);
      Panel.Event(&event);
      class="kw">break;
    }
    case CHARTEVENT_OBJECT_CLICK:
    {
      CEventChartObjClick event(sparam);
      Panel.Event(&event);
      class="kw">break;
    }
  }
  ChartRedraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//class="macro">#include <Panel\ElChart.mqh>
class="macro">#include <Panel\ElButton.mqh>
class="macro">#include <Graphics\Graphic.mqh>
class="macro">#include "ListPass.mqh"
class="macro">#include "TradeAnalyze.mqh"
class CAnalizePanel : class="kw">public CElChart
{
class="kw">private:
  CArrayObj      m_stat_descr;   class=class="str">"cmt">// 统计说明
  CArrayObj      m_stat_all;     class=class="str">"cmt">// 所有成交的统计值
  CArrayObj      m_stat_long;    class=class="str">"cmt">// 所有多头成交的统计值
  CArrayObj      m_stat_short;   class=class="str">"cmt">// 所有空头成交的统计值
  CTradeAnalize  m_analize;      class=class="str">"cmt">// 统计计算模块
  CElChart       m_name_analyze; class=class="str">"cmt">// 主窗口名称
  CElChart       m_np;           class=class="str">"cmt">// "Pass #" 文本

「分析面板的成员与统计枚举拆解」

在 MT5 自定义分析面板类里,私有段先挂了一批界面与数据成员:m_of_pass 负责显示“of ### passes”文本,m_pass_index 是通关编号输入框,m_btn_next / m_btn_prev 切换前后通关,m_graphic 绘制余额动态曲线。 底层用 m_passes 存通关清单,m_curr_pass 记当前索引,m_balance_hist 是指向余额曲线的 CCurve 指针;配套私有方法覆盖事件判断(IsEndEditPass / IsClick)、通关切换(NextPass / PrevPass)、重绘(RedrawGraphic / RedrawCurrPass)与统计输出(PlotStatistic / CreateStatElements 等)。 公开段只暴露构造、LoadFilePasses(默认 FILE_COMMON 读共享目录文件)、OnShow / OnHide / Event 三个虚函数,外部调用面很窄。 统计维度由 ENUM_MSTAT_TYPE 定死,共 10 项(MSTAT_ELEMENTS_TOTAL 宏 = 10),从 MSTAT_PROFIT 到 MSTAT_MATH_EXP,覆盖盈亏、胜败单数、最大盈亏、盈利因子与数学期望。 方向过滤另给 ENUM_MSTATE_DIRECT:MSTATE_DIRECT_ALL / LONG / SHORT,分别算全部、仅买、仅卖成交——回测时切 SHORT 看贵金属空单样本,可能暴露出与全量不同的数学期望值。 头文件尾部 include 了 ArrayObj.mqh、Dictionary.mqh 和上层 MTester 的 Mposition.mqh,说明面板依赖对象数组与持仓结构,开 MT5 把这几个路径配对齐才能编译过。

MQL5 / C++
  CElChart      m_of_pass;          class=class="str">"cmt">// "of ### passes" 文本
  CElChart      m_pass_index;        class=class="str">"cmt">// 通关编号输入框
  CElButton     m_btn_next;          class=class="str">"cmt">// "下一个通关" 按钮
  CElButton     m_btn_prev;          class=class="str">"cmt">// "前一个通关" 按钮
  CGraphic      m_graphic;           class=class="str">"cmt">// 余额动态图表
  class=class="str">"cmt">//-- 底层结构
  CListPass     m_passes;            class=class="str">"cmt">// 通关清单
  class="type">int           m_curr_pass;         class=class="str">"cmt">// 当前通关索引
  CCurve*       m_balance_hist;      class=class="str">"cmt">// 图表上的余额动态曲线
  class="type">bool          IsEndEditPass(CEvent* event);
  class="type">bool          IsClick(CEvent* event, CElChart* el);
  class="type">void          NextPass(class="type">void);
  class="type">void          PrevPass(class="type">void);
  class="type">int           GetCorrectPass(class="type">class="kw">string text);
  class="type">void          RedrawGraphic(class="type">void);
  class="type">void          RedrawCurrPass(class="type">void);
  class="type">void          PlotStatistic(class="type">void);
  class="type">class="kw">string        TypeStatToString(ENUM_MSTAT_TYPE type);
  class="type">void          CreateStatElements(class="type">void);
  class="type">class="kw">string        ValueToString(class="type">class="kw">double value, ENUM_MSTAT_TYPE type);
class="kw">public:
                 CAnalizePanel(class="type">void);
  class="type">bool          LoadFilePasses(class="type">class="kw">string file_name, class="type">int file_common = FILE_COMMON);
  class="kw">virtual class="type">void  OnShow();
  class="kw">virtual class="type">void  OnHide();
  class="kw">virtual class="type">void  Event(CEvent *event);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 统计值类型的标识符                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MSTAT_TYPE
{
  MSTAT_PROFIT,
  MSTAT_ALL_WINS_MONEY,
  MSTAT_ALL_LOSS_MONEY,
  MSTAT_TRADERS_TOTAL,
  MSTAT_WIN_TRADERS,
  MSTAT_LOSS_TRADERS,  
  MSTAT_MAX_PROFIT,
  MSTAT_MAX_LOSS,
  MSTAT_PROFIT_FACTOR,
  MSTAT_MATH_EXP,  
};
class="macro">#define MSTAT_ELEMENTS_TOTAL class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 可以为三个方向之一计算统计                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MSTATE_DIRECT
{
  MSTATE_DIRECT_ALL,      class=class="str">"cmt">// 对于所有成交
  MSTATE_DIRECT_LONG,     class=class="str">"cmt">// 仅针对买入成交
  MSTATE_DIRECT_SHORT,    class=class="str">"cmt">// 仅针对卖出成交
};
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include <Dictionary.mqh>
class="macro">#include "..\MTester\Mposition.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+

持仓统计的底层数据结构

要把 MT5 里一堆历史持仓拆成多空双向指标,先得有个能装「总/买/卖」三栏差值的轻量结构。下面这段 CDiffValues 就是干这个的:三个 double 字段 all、buy、sell 分别记全量、多头、空头,构造函数直接清零,没有任何外部依赖。 CTradeAnalize 类对外只露两个接口:CalculateValues 吃一个历史持仓数组指针,GetStatistic 按类型加方向吐数值。内部用 CDictionary 按枚举键存 CDiffValues 对象,键从 MSTAT_PROFIT 到 MSTAT_MATH_EXP 覆盖 11 类统计(源码循环上界 MSTAT_ELEMENTS_TOTAL 即对应此数)。 GetStatistic 的实现很直白——拿键取到 CDiffValues 指针后,按 MSTATE_DIRECT_ALL/LONG/SHORT 三分支返回 all/buy/sell,没匹配到就回 EMPTY_VALUE。你在 EA 里调统计时,若发现某方向返回 -1(EMPTY_VALUE 的宏值),先查传入的枚举是否越界,而不是怀疑字典漏存。 CalculateValues 开头先 m_values.Clear() 再按 MSTAT_ELEMENTS_TOTAL 重新 new 一遍对象,意味着每次重算都会重建全部 11 个键值对。若你的历史数组超过几千条,频繁调这个函数可能成为 tick 级回测的隐性开销,倾向把它放在新柱形成时跑一次而非每 tick 调用。

MQL5 / C++
class=class="str">"cmt">//| 辅助控件包含必要的字段                                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDiffValues : class="kw">public CObject
{
class="kw">public:
   class="type">class="kw">double all;
   class="type">class="kw">double sell;
   class="type">class="kw">double buy;
   CDiffValues(class="type">void) : all(class="num">0), buy(class="num">0), sell(class="num">0)
   {
   }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 统计分析类                                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTradeAnalize
{
class="kw">private:
   CDictionary m_values;

class="kw">public:
   class="type">void      CalculateValues(CArrayObj* history);
   class="type">class="kw">double    GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算统计值                                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct)
{
   CDiffValues* value = m_values.GetObjectByKey(type);
   class="kw">switch(direct)
   {
      case MSTATE_DIRECT_ALL:
         class="kw">return value.all;
      case MSTATE_DIRECT_LONG:
         class="kw">return value.buy;
      case MSTATE_DIRECT_SHORT:
         class="kw">return value.sell;
   }
   class="kw">return EMPTY_VALUE;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算每个方向的成交数量                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTradeAnalize::CalculateValues(CArrayObj *history)
{
   m_values.Clear();
   for(class="type">int i = class="num">0; i < MSTAT_ELEMENTS_TOTAL; i++)
      m_values.AddObject(i, new CDiffValues());
   CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT);
   CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY);
   CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY);
   CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL);
   CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS);
   CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS);
   CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT);
   CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS);
   CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR);
   CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP);
   total_traders.all = history.Total();
   for(class="type">int i = class="num">0; i < history.Total(); i++)
   {
      CMposition* pos = history.At(i);
      profit.all += pos.Profit();
      if(pos.PositionType() == POSITION_TYPE_BUY)
      {
         if(pos.Profit() > class="num">0)
         {
            win_traders.buy++;
            wins_money.buy += pos.Profit();
         }

◍ 逐仓归类后怎么算期望与盈亏比

上面这段逻辑跑完一轮持仓遍历后,把每笔仓位按多空和盈亏分别计数、累加金额,并顺手记下了最大盈利与最大亏损。买方向盈利笔数进 win_traders.buy、金额进 wins_money.buy,亏损则进 loss_traders.buy 与 loss_money.buy;卖方向同理用 .sell 那组变量承接。 所有方向汇总时,win_traders.all 与 loss_traders.all 只认盈亏正负,不区分方向;max_profit.all 和 max_loss.all 用大于、小于判断实时刷新极值,亏损极值本身是负数所以用了 max_loss.all > pos.Profit() 这种反向比较。 遍历结束后的几行才是核心指标:mexp 三组等于总利润除以总笔数,即单笔数学期望;pf 三组等于盈利总额除以亏损总额,也就是泛用版的盈利因子。你开 MT5 把这段接在自己回测脚本末尾,就能直接打印出 buy/sell/all 三栏的 mexp 与 pf,对比多空谁更拖后腿。 下面紧跟着的 CListPass 类只是把优化通关结果用 Zip 和 Dictionary 存盘,LoadOptimazeFile 负责读历史仓位,PassTotal 与 PassAt 供外部按索引取某次通关参数,和上面的统计计算是两套事,别混进同一轮循环里跑。

MQL5 / C++
      else
      {
         loss_traders.buy++;
         loss_money.buy += pos.Profit();
      }
      total_traders.buy++;
      profit.buy += pos.Profit();
   }
   else
   {
      if(pos.Profit() > class="num">0)
      {
         win_traders.sell++;
         wins_money.sell += pos.Profit();
      }
      else
      {
         loss_traders.sell++;
         loss_money.sell += pos.Profit();
      }
      total_traders.sell++;
      profit.sell += pos.Profit();
   }
   if(pos.Profit() > class="num">0)
   {
      win_traders.all++;
      wins_money.all += pos.Profit();
   }
   else
   {
      loss_traders.all++;
      loss_money.all += pos.Profit();
   }
   if(pos.Profit() > class="num">0 && max_profit.all < pos.Profit())
      max_profit.all = pos.Profit();
   if(pos.Profit() < class="num">0 && max_loss.all > pos.Profit())
      max_loss.all = pos.Profit();
   }
   mexp.all = profit.all/total_traders.all;
   mexp.buy = profit.buy/total_traders.buy;
   mexp.sell = profit.sell/total_traders.sell;
   pf.all = wins_money.all/loss_money.all;
   pf.buy = wins_money.buy/loss_money.buy;
   pf.sell = wins_money.sell/loss_money.sell;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Optimazer.mq5 |
class=class="str">"cmt">//|                        版权所有 class="num">2017, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Zip\Zip.mqh>
class="macro">#include <Dictionary.mqh>
class="macro">#include "..\MTester\MTrade.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 存储优化通关清单                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CListPass
{
class="kw">private:
   CZip       m_zip_passes;    class=class="str">"cmt">// 所有优化通关的存档
   CDictionary m_passes;       class=class="str">"cmt">// 加载历史仓位
class="kw">public:
   class="type">bool       LoadOptimazeFile(class="type">class="kw">string file_name, class="type">int file_common = FILE_COMMON);
   class="type">int        PassTotal(class="type">void);
   CArrayObj* PassAt(class="type">int index);
};

「把压缩包里的优化结果喂给统计面板」

复盘EA的优化产出时,通关清单常被打包成 zip 存档。CListPass::LoadOptimazeFile 负责把文件读进内存,先清空旧容器 m_zip_passes,再调用 LoadZipFromFile;若返回失败会打印“加载优化文件失败”并返回 false,成功则 true。注意 file_common 默认 FILE_COMMON,意味着文件放在终端公共目录,换设备时要确认路径一致。 PassTotal 直接返回 m_zip_passes.TotalElements(),也就是压缩包里通关方案的数量。PassAt 按索引懒加载:首次访问才解包 uchar 数组、用 CMtrade::FromCharArray 还原成交记录并缓存到 m_passes,之后直接取缓存,避免重复解压。 PlotStatistic 是面板刷新的核心。它先取当前通关 m_curr_pass-1 的 history,调 CalculateValues 算指标,再循环 MSTAT_ELEMENTS_TOTAL 次,分别把全量、多头、空头三类统计写进 m_stat_all / m_stat_long / m_stat_short 三个文本控件。你在 MT5 里改一版指标算法,只需保证 ENUM_MSTAT_TYPE 顺序不变,面板就会自动重绘。 外汇与贵金属复盘涉及历史数据回测,存在模型偏差与滑点风险,统计结果仅代表过去样本,实盘表现可能偏离。

MQL5 / C++
class="type">bool CListPass::LoadOptimazeFile(class="type">class="kw">string file_name,class="type">int file_common=FILE_COMMON)
{
   m_zip_passes.Clear();
   if(!m_zip_passes.LoadZipFromFile(file_name, file_common))
   {    
        printf("加载优化文件失败。最后错误");
        class="kw">return false;
   }
   class="kw">return true;
}
class="type">int CListPass::PassTotal(class="type">void)
{
   class="kw">return m_zip_passes.TotalElements();
}
CArrayObj* CListPass::PassAt(class="type">int index)
{
   if(!m_passes.ContainsKey(index))
   {
      CZipFile* zip_file = m_zip_passes.ElementAt(index);
      class="type">uchar array[];
      zip_file.GetUnpackFile(array);
      CMtrade* trade = new CMtrade();
      trade.FromCharArray(array);
      m_passes.AddObject(index, trade);
   }
   CMtrade* trade = m_passes.GetObjectByKey(index);
   class=class="str">"cmt">//printf("交易总计: " + (class="type">class="kw">string)trade.History.Total());
   class="kw">return &trade.History;
}
class="type">void CAnalyzePanel::PlotStatistic(class="type">void)
{
   if(m_stat_descr.Total() == class="num">0)
      CreateStatElements();
   CArrayObj* history = m_passes.PassAt(m_curr_pass-class="num">1);
   m_analize.CalculateValues(history);
   for(class="type">int i = class="num">0; i < MSTAT_ELEMENTS_TOTAL; i++)
   {
      ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i;
      class=class="str">"cmt">//-- 所有交易
      CElChart* el = m_stat_all.At(i);
      class="type">class="kw">string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type);
      el.Text(v);
      class=class="str">"cmt">//-- 多头交易
      el = m_stat_long.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type);
      el.Text(v);
      class=class="str">"cmt">//-- 空头交易
      el = m_stat_short.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type);
      el.Text(v);
   }
}

把这条线请下神坛

数学计算测试器把交易环境模拟整个舍弃,换来的是测试速度的数量级提升——实测较标准 MT5 测试器快约一个数量级,但代价是容易在报价未到达时无意「窥探未来」,造出与现实无关的「圣杯」。这类错误隐蔽性强,是高性能必须承受的风险。 单次优化可包含数百到数千次通关,每趟通关成交轻松破万笔;用 zip 存档封装后,即便大规模优化也能把体积压在适度范围,MQL5 自带压缩函数库足以支撑这种自定义存储。 分帧生成模式虽复杂,却允许写出仓位清单这类复杂自定义数据(CMPosition 类转字节入帧再还原),配合 CGraphic 绘出余额曲线。但它仍缺品种点差/掉期、参数回写、操作正确性控制等组件,下个版本才可能补上。 外汇与贵金属属高风险品类,任何自研测试器的回测盈利都不等于实盘可复制。M-Tester 已跑通主体,剩下的只是时间问题——等新版能接标准测试器报告时,你再用分析器拆自己的优化结果。

把重复劳动交给小布
这些诊断与分帧结果的整理,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需专注策略逻辑本身。

常见问题

该模式只暴露有限函数集,交易环境被禁用,OnTester 成为主入口,适合纯数值优化而非实盘模拟。
能大幅压缩历史数据体积,配合自定义测试器可实现快速载入与跨帧传递,减少 IO 等待。
合理设计字节表达后倾向保留必要字段,精度损失概率低,但需自行校验关键统计项。
小布内置的 AIGC 可辅助整理优化输出与仓位历史摘要,但编译与回测仍需在 MT5 终端完成。
分离倾向更清晰,便于在原型策略与真实下单之间切换,但不是强制,取决于复杂度。