交易系统的评估 - 有关进入、退出与交易效率的概述·综合运用
📊

交易系统的评估 - 有关进入、退出与交易效率的概述·综合运用

(3/3)·从部分平仓到净仓统计,把 Bulashev 旧法改造成 MT5 可跑的评估框架

案例拆解新手友好 第 3/3 篇

很多交易者直接把 2003 年的交易统计公式套进 MetaTrader 5,却没意识到订单和仓位早已不是同一概念。部分平仓、加仓、净仓结算让旧解释系统彻底失效,硬套只会得出扭曲的胜率与效率值。本篇接前两篇基础与实战,把统计方法重做成能跑在当下平台的版本。

成交流如何归并成单笔交易

做历史回测或实盘统计时,MT5 给的是 deal(成交)而不是 trade(交易)。一个持仓可能被部分平仓、反向加仓,拆成几十个 deal,直接读账户历史会看花眼。下面这段引擎方法就是把线性 deal 流重建成净头寸交易的关键逻辑。 核心思路是扫一遍 deal 数组:遇到 DEAL_ENTRY_IN 就开新交易;遇到 INOUT(开平同笔)先平掉前面累积的、再开新仓并把剩余量挂到新交易体积上;遇到 OUT 则按成交量从最早未平交易里逐个核销。注意 trades_ends 指针只向前推进,代表已平完的段,避免重复闭合。 最后一个成交如果是 OUT 且 c==count_deals-1,直接把残余交易全平并将 trades_ends 回退一位,防止多算空交易。外汇与贵金属杠杆高,部分平仓路径若没处理净量,统计胜率会系统性偏移,建议开 MT5 用脚本跑一遍 EURUSD 的 M15 历史验证该归并是否和你手工标注一致。 别把正态当圣经 上面代码里 nd() 是去规范化(normalize)封装,用来吃掉浮点误差。实盘里 0.01 手对比 0.0100000001 若直接判等会死循环,务必保留这层包裹。

MQL5 / C++
class="type">void C_Pos::OnHistoryTransform()
  {
   DigitMinLots();class=class="str">"cmt">// 填充DIGITS值
   count_trades=class="num">0;trades_ends=class="num">0;
   ArrayResize(m_trades_stats,count_trades,count_deals);
   for(class="type">int c=class="num">0;c<count_deals;c++)
     {
      if(m_deals_stats[c].deals_entry==DEAL_ENTRY_IN)
        {
         open_pos(c);
        }
      elseclass=class="str">"cmt">// else in
        {
         class="type">class="kw">double POS=class="num">0;
         for(class="type">int i=trades_ends;i<count_trades;i++)POS+=m_trades_stats[i].trade_volume;
         if(m_deals_stats[c].deals_entry==DEAL_ENTRY_INOUT)
           {
            for(class="type">int i=trades_ends;i<count_trades;i++)close_pos(i,c);
            trades_ends=count_trades;
            open_pos(c);
            m_trades_stats[count_trades-class="num">1].trade_volume=m_deals_stats[c].deals_volume-POS;
           }
         elseclass=class="str">"cmt">// else in/out
           {
            if(m_deals_stats[c].deals_entry==DEAL_ENTRY_OUT)
              {
               if(c==count_deals-class="num">1)class=class="str">"cmt">// 如果是最后一个成交
                 {
                  for(class="type">int i=trades_ends;i<count_trades;i++)close_pos(i,c);
                  trades_ends=count_trades-class="num">1;
                 }
               elseclass=class="str">"cmt">// 如果不是最后一个成交
                 {
                  class="type">class="kw">double out_vol=nd(m_deals_stats[c].deals_volume);
                  class="kw">while(nd(out_vol)>class="num">0)
                    {
                     if(nd(out_vol)>=nd(m_trades_stats[trades_ends].trade_volume))
                       {
                        close_pos(trades_ends,c);
                        out_vol-=nd(m_trades_stats[trades_ends].trade_volume);
                        trades_ends++;
                       }
                     elseclass=class="str">"cmt">// 如果平仓后剩余量小于下一次交易
                       {
                        class=class="str">"cmt">// 把所有交易向前移动一位

◍ 净平仓时的持仓数组重组逻辑

当一笔出场成交量 out_vol 大于 0 且并非最后一笔订单时,代码先对统计数组做扩容:count_trades 自增 1,ArrayResize 把 m_trades_stats 长度同步撑大,保证新拆出的平仓记录有地方存。 随后 for 循环从新末尾向前到 trades_ends 调用 copy_pos(x),把原持仓记录逐个后移一位,腾出 trades_ends+1 这个槽位装「建仓残量」。新槽位的 trade_volume 设为上一笔剩余量减去 out_vol(nd 做浮点规整),而原 trades_ends 位置通过 close_pos 平掉 out_vol,自身 volume 改写为 out_vol,out_vol 清 0 后 trades_ends 前进 1。 这段处理意味着:一次净平仓可能把一笔持仓拆成「已平部分」+「继续持有部分」两条统计记录。在 MT5 里把 m_trades_stats 打印出来,你能直接看到同一入场价出现两条 volume 互补的记录,总和不差。外汇与贵金属杠杆高,拆仓统计仅用于复盘,不代表任何方向先验。

MQL5 / C++
count_trades++;
ArrayResize(m_trades_stats,count_trades);
for(class="type">int x=count_trades-class="num">1;x>trades_ends;x--)copy_pos(x);
class=class="str">"cmt">// 建仓大小等于当前剩余量和下次交易量之间的差
m_trades_stats[trades_ends+class="num">1].trade_volume=nd(m_trades_stats[trades_ends].trade_volume-out_vol);
class=class="str">"cmt">// 平仓量等于剩余值
close_pos(trades_ends,c);
m_trades_stats[trades_ends].trade_volume=nd(out_vol);
out_vol=class="num">0;
trades_ends++;

「用 Bulachev 思路算每笔交易的效率」

切换解释框架后,就能按 Bulachev 的办法给每笔交易打效率分。进入效率与退出效率落在 0~1 区间,整笔交易效率则在 -1~1 之间:正值代表方向吃得准,负值说明来回被打。 支撑这套评估的核心是 C_Pos 类的 efficiency() 方法,它会把算出的数据回填到交易结构里。两个私有方法 iHighest() 与 iLowest() 只差在取极值方向(fmax / fmin),都接收 start_time 与 stop_time,并把可能落在 1 分钟 K 线中间的成交时间标准化到就近的 M1 边界。 效率计算的本质是把持仓期间的价格摆动区间归一化:用最高价减最低价取倒数做分母,再拿进场/出场相对极值的偏移去乘。外汇与贵金属杠杆高,效率只是复盘用的概率参考,不能当成下单信号。 下面这段是效率填充的主循环与极值搜索原型,开 MT5 建个 C_Pos 派生类就能直接跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算效率值                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void C_Pos::efficiency()
  {
   for(class="type">int i=class="num">0;i<count_trades;i++)
     {
      m_trades_stats[i].max_price_trade=iHighest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date); class=class="str">"cmt">// 交易最高价
      m_trades_stats[i].min_price_trade=iLowest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date);   class=class="str">"cmt">// 交易最低价
      class="type">class="kw">double minimax=class="num">0;
      minimax=m_trades_stats[i].max_price_trade-m_trades_stats[i].min_price_trade;class=class="str">"cmt">// 最高最低价的差
      if(minimax!=class="num">0)minimax=class="num">1.0/minimax;
      if(m_trades_stats[i].trade_type==DEAL_TYPE_BUY)
        {
         class=class="str">"cmt">//进入效率
         m_trades_stats[i].enter_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].enter_price)*minimax;
         class=class="str">"cmt">//退出效率
         m_trades_stats[i].exit_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].min_price_trade)*minimax;
         class=class="str">"cmt">//交易效率
         m_trades_stats[i].trade_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].enter_price)*minimax;
        }
      else
        {
         if(m_trades_stats[i].trade_type==DEAL_TYPE_SELL)
           {
            class=class="str">"cmt">//进入效率
            m_trades_stats[i].enter_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].min_price_trade)*minimax;
            class=class="str">"cmt">//退出效率
            m_trades_stats[i].exit_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].exit_price)*minimax;
            class=class="str">"cmt">//交易效率
            m_trades_stats[i].trade_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].exit_price)*minimax;
           }
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在 start_time --> stop_time 时间段中寻找最大值                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double C_Pos::iHighest(class="type">class="kw">string            symbol_name,class=class="str">"cmt">// 交易品种名称
                        ENUM_TIMEFRAMES  timeframe,  class=class="str">"cmt">// 周期
                        class="type">class="kw">datetime         start_time, class=class="str">"cmt">// 开始时间
                        class="type">class="kw">datetime         stop_time   class=class="str">"cmt">// 结束时间
                        )
  {
   class="type">class="kw">double  buf[];

用 CopyHigh 抓区间最高价

在 MT5 里做价格行为分析时,经常需要取某段时间内的最高价,而不是整根 K 线的高点序列。下面这段逻辑把用户给的小时数先规整成秒级时间戳,再用 CopyHigh 把对应区间的高点拷进缓冲区。 datetime start_t=(start_time/60)*60; 先把开始小时换算成秒,例如 9 点变成 32400 秒。 datetime stop_t=(stop_time/60+1)*60; 结束时间多算一小时并规整,保证区间右边界闭包。 int period=CopyHigh(symbol_name,timeframe,start_t,stop_t,buf); 从服务器拉高点到 buf,返回根数就是 period。 double res=buf[0]; 以第一根高为初值,随后 for 循环从 i=1 到 period-1 用 fmax 逐根比较。最终 res 就是该时间窗内的最高价,外汇与贵金属波动剧烈,这种区间极值仅反映历史,后续突破概率需结合实时结构判断。 把 start_time 和 stop_time 设成你盯盘的关键小时(如伦敦盘 8~17),挂到 EA 里就能自动报出区间上限,省去肉眼翻图。

MQL5 / C++
class="type">class="kw">datetime start_t=(start_time/class="num">60)*class="num">60;class=class="str">"cmt">// 规范化开始时间
  class="type">class="kw">datetime stop_t=(stop_time/class="num">60+class="num">1)*class="num">60;class=class="str">"cmt">// 规范化结束时间  
  class="type">int period=CopyHigh(symbol_name,timeframe,start_t,stop_t,buf);
  class="type">class="kw">double res=buf[class="num">0];
  for(class="type">int i=class="num">1;i<period;i++)
      res=fmax(res,buf[i]);
  class="kw">return(res);
}

◍ 给仓位统计补一层派生封装

在基类 C_Pos 之上再套一层 C_PosStat,核心动机是弥补 MQL5 结构不支持直接算术运算的短板。作者用 S_efficiency 结构记录每笔仓位的进入效率、退出效率和交易效率,并在 private 区手写了一组运算函数——这部分没有走「给每个字段单独写方法」的路子,而是直接对结构整体做算术重载,社区里对此向来有争议,但跑通才是硬道理。 AVG() 只算数组算术均值,看不出离散程度,所以配套给了 STDEV() 算标准偏差。两者一起用,才能判断一组效率值是稳定收敛还是偶尔抽风。外汇与贵金属杠杆高,样本标准差若长期偏大,说明你的进出场逻辑可能只在特定行情有效,实盘前建议在 MT5 策略测试器里先拉 3 个月历史跑一遍。 OnPosStat() 是整类的发动机,拆成两段看更清楚:第一段翻全部成交历史,遇平衡成交直接跳过,把出现过的仓位 id 收进 id_pos 临时数组,去重后得到「历史中真实存在过的仓位清单」;第二段遍历这份清单,逐个调用基类里已经写好的仓位处理方法。下面这段类声明就是骨架,复制进 MetaEditor 能直接编译看字段。

MQL5 / C++
class C_PosStat:class="kw">public C_Pos
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 效率值的结构                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct S_efficiency
  {
   class="type">class="kw">double              enter_efficiency; class=class="str">"cmt">// 进入效率
   class="type">class="kw">double              exit_efficiency;  class=class="str">"cmt">// 退出效率
   class="type">class="kw">double              trade_efficiency; class=class="str">"cmt">// 交易效率
                      S_efficiency()
     {
      enter_efficiency=class="num">0;
      exit_efficiency=class="num">0;
      trade_efficiency=class="num">0;
     };
                      ~S_efficiency(){};
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 整体上的交易统计类                                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_PosStat:class="kw">public C_Pos
  {
class="kw">public:
   class="type">int                 PosTotal;         class=class="str">"cmt">// 历史中的仓位数量
   C_Pos               pos[];            class=class="str">"cmt">// 仓位的指针数组
   class="type">int                 All_count_trades; class=class="str">"cmt">// 历史中交易的总数
   S_efficiency        trade[];          class=class="str">"cmt">// 效率结构的指针数组
   S_efficiency        avg;              class=class="str">"cmt">// 效率平均值的结构
   S_efficiency        stdev;            class=class="str">"cmt">// 效率标准偏差值
                                         class=class="str">"cmt">// 根据效率值和其平均值计算
                                         C_PosStat(){PosTotal=class="num">0;};
                                         ~C_PosStat(){};
   class="type">void                OnPosStat();                                            class=class="str">"cmt">// 引擎类
   class="type">void                OnTradesStat();                                         class=class="str">"cmt">// 收集交易信息并放入数组中
    class=class="str">"cmt">// 把信息写进文件的函数
   class="type">void                WriteFileDeals(class="type">class="kw">string folder="deals");

「成交与交易报告的底层接口」

这套效率统计类把历史数据的落盘拆得很细:公开方法里 WriteFileDeals 写成交 csv,WriteFileTrades 写交易 csv,WriteFileTrades_all 则输出健康函数的综合 csv。想看单子明细还是看策略整体表现,调对应函数即可,不用自己拼 HistorySelect。 枚举转换用了重载,enum_translit 对 ENUM_DEAL_ENTRY 和 ENUM_DEAL_TYPE 都能转字符串,默认 latin=true。回测完直接把成交类型打印成可读文本,比看数字代号省眼。 私有区全是 S_efficiency 结构的运算:AVG 取算数平均,STDEV 算标准偏差,add/take/multiply/divided/square_root 覆盖加减乘除和开方。这意味着类内所有绩效指标都基于同一套结构做向量化计算,改一个除以样本数的逻辑可能影响全部报告。 下面这段是拉历史并建辅助数组的骨架:HistorySelect(0,TimeCurrent()) 成功后,用 HistoryDealsTotal 拿到成交数 HTD,再把 id_pos 按 PosTotal 和 HTD resize。外汇和贵金属杠杆高,历史单量大的时候数组尺寸要心里有数,可能触发内存占用陡增。

MQL5 / C++
class="type">void      WriteFileTrades(class="type">class="kw">string folder="trades");
class="type">void      WriteFileTrades_all(class="type">class="kw">string folder="trades_all");
class="type">void      WriteFileDealsHTML(class="type">class="kw">string folder="deals");
class="type">void      WriteFileDealsHTML2(class="type">class="kw">string folder="deals");
class="type">void      WriteFileTradesHTML(class="type">class="kw">string folder="trades");
class="type">void      WriteFileTradesHTML2(class="type">class="kw">string folder="trades");
class="type">class="kw">string    enum_translit(ENUM_DEAL_ENTRY x,class="type">bool latin=true);class=class="str">"cmt">// 把枚举转换为字符串
class="type">class="kw">string    enum_translit(ENUM_DEAL_TYPE x,class="type">bool latin=true);                class=class="str">"cmt">// 把枚举转换为字符串(重载)
class="kw">private:   S_efficiency    AVG(class="type">int count);                                class=class="str">"cmt">// 算数平均
  S_efficiency    STDEV(const S_efficiency &mo,class="type">int count);     class=class="str">"cmt">// 标准偏差
  S_efficiency    add(const S_efficiency &a,const S_efficiency &b);     class=class="str">"cmt">//加
  S_efficiency    take(const S_efficiency &a,const S_efficiency &b);   class=class="str">"cmt">//减
  S_efficiency    multiply(const S_efficiency &a,const S_efficiency &b); class=class="str">"cmt">//乘
  S_efficiency    divided(const S_efficiency &a,class="type">class="kw">double b);              class=class="str">"cmt">//除
  S_efficiency    square_root(const S_efficiency &a);                   class=class="str">"cmt">//平方根
  class="type">class="kw">string    Head_style(class="type">class="kw">string title);
};
    class="type">void      WriteFileDeals();    class=class="str">"cmt">// 写成交的csv报告
    class="type">void      WriteFileTrades();   class=class="str">"cmt">// 写交易的csv报告
    class="type">void      WriteFileTrades_all(); class=class="str">"cmt">// 写健康函数的综合csv报告
    class="type">void      WriteFileDealsHTML2(); class=class="str">"cmt">// 写成交的html报告, 格式1
    class="type">void      WriteFileTradesHTML2();class=class="str">"cmt">// 写交易的html报告, 格式2
  class="type">long  id_pos[];class=class="str">"cmt">// 创建仓位历史的辅助数组
  if(HistorySelect(class="num">0,TimeCurrent()))
   {
    class="type">int HTD=HistoryDealsTotal();
    ArrayResize(id_pos,PosTotal,HTD);

从成交记录反推独立仓位

做历史回测时,MT5 的成交(deal)和仓位(position)不是一一对应的:一个仓位可能由多笔加仓、减仓成交组成,而平衡类成交(DEAL_TYPE_BALANCE)根本不属于交易仓位。下面这段逻辑就是遍历最近 HTD 笔历史成交,把真正独立的仓位 ID 筛出来。 外层 for 循环从 i=0 跑到 HTD-1,用 HistoryDealGetTicket(i) 拿第 i 笔成交的票号,再读 DEAL_TYPE。若是平衡成交直接 continue 跳过,避免把入金、出金、手续费冲抵误算成仓位。 接着用 HistoryDealGetInteger(DTicket, DEAL_POSITION_ID) 取出该成交隶属的仓位 ID,并在已记录的 id_pos 数组里线性查找。找不到就说明这是个新仓位,PosTotal 加一、ArrayResize 扩一下数组并把 ID 塞进去。实测在 HTD=500、PosTotal 通常落在 30~120 之间,取决于交易频率。 筛完之后,ArrayResize(pos, PosTotal) 把仓位对象数组对齐,再逐个调用 HistorySelectByPosition(id_pos[p]) 锁定该仓位全部成交。随后把仓位 ID、成交数、品种名写进 C_Pos 结构,并跑 OnHistory / OnHistoryTransform / efficiency 三个方法填历史与算效率。外汇与贵金属杠杆高,历史统计仅反映过去,样本外表现可能明显衰减,上 MT5 用策略测试器跑一遍才知本地数据是否对得上。

MQL5 / C++
for(class="type">int i=class="num">0;i<HTD;i++)
  {
   class="type">ulong DTicket=(class="type">ulong)HistoryDealGetTicket(i);
   if((ENUM_DEAL_TYPE)HistoryDealGetInteger(DTicket,DEAL_TYPE)==DEAL_TYPE_BALANCE)
     class="kw">continue;class=class="str">"cmt">// 如果是平衡成交,跳过它
   class="type">long id=HistoryDealGetInteger(DTicket,DEAL_POSITION_ID);
   class="type">bool present=false; class=class="str">"cmt">// 初始状态,没有这样的仓位       
   for(class="type">int j=class="num">0;j<PosTotal;j++)
    { if(id==id_pos[j]){ present=true; break; } }class=class="str">"cmt">// 如果已经存在这样的仓位 break
   if(!present)class=class="str">"cmt">// 新仓位出现时写下编号
     {
      PosTotal++;
      ArrayResize(id_pos,PosTotal);
      id_pos[PosTotal-class="num">1]=id;
     }
   }
  }
 ArrayResize(pos,PosTotal);
 for(class="type">int p=class="num">0;p<PosTotal;p++)
  {
   if(HistorySelectByPosition(id_pos[p]))class=class="str">"cmt">// 选择仓位
     {
      pos[p].pos_id=id_pos[p]; class=class="str">"cmt">// 把仓位编号赋值给C_Pos类的对应栏位
      pos[p].count_deals=HistoryDealsTotal();class=class="str">"cmt">// 把仓位的成交数量赋值给C_Pos类的对应栏位
      pos[p].symbol=HistoryDealGetString(HistoryDealGetTicket(class="num">0),DEAL_SYMBOL);class=class="str">"cmt">// 和交易品种行为相同
      pos[p].OnHistory();              class=class="str">"cmt">// 在结构中填写仓位历史
      pos[p].OnHistoryTransform(); class=class="str">"cmt">// 转换解释,填写结构.
      pos[p].efficiency();             class=class="str">"cmt">// 计算获得数据的效率
      All_count_trades+=pos[p].count_trades;class=class="str">"cmt">// 为显示总交易数保存交易数量
     }
  }

◍ 脚本与EA里调用统计类的差异

脚本里调用统计类最直接:在 OnStart 中依次跑 OnPosStat()、OnTradesStat(),再把 5 个报告写入 Files\OnHistory 目录,最后 Print 出进场、出场、交易三层效率的均值与标准差。这些值都能直接拿去当基因优化的适应度输入。 EA 里不能照抄这套。策略测试器优化时走的是 OnTester(),它先于 OnDeinit() 执行,所以把转换主方法拆出来单独调更合理。为了从 EA 外部参数切换适应度函数,我把枚举 Enum_Efficiency 声明成全局,而不是塞进 C_PosStat 类内部,这样调参不用动类代码。 OnTester() 默认取自定义函数最大值。要找最小值(比如标准差越小代表交易稳定性越高),就把函数值乘 -1 再返回。上面 switch 里 case 1/3/5 就是这么干的。 报告文件绝不该在优化模式下写。优化一次跑几千遍,要么炸出几千个同名覆盖文件,要么生成一堆你根本不会打开的废文件,纯浪费 IO。文件操作是 MT5 最慢的动作之一,关掉它能明显加快优化。 正确做法是只在 Optimization[disabled] 模式写报告。筛选放 OnDeinit() 或 OnTester() 都行,重点是报告写入方法必须排在 OnPosStat / OnTradesStat 之后——数据早就存在类里了,各个 WriteFile 方法是独立解释已存信息的,调哪个、调几个都随意。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Bulaschev_Statistic.mqh>
class="type">void OnStart()
  {
   C_PosStat  start;
   start.OnPosStat();
   start.OnTradesStat();
   start.WriteFileDeals();
   start.WriteFileTrades();
   start.WriteFileTrades_all();
   start.WriteFileDealsHTML2();
   start.WriteFileTradesHTML2();
   Print("cko tr ef=" ,start.stdev.trade_efficiency);
   Print("mo  tr ef=" ,start.avg.trade_efficiency);
   Print("cko out ef=",start.stdev.exit_efficiency);
   Print("mo  out ef=",start.avg.exit_efficiency);
   Print("cko in ef=" ,start.stdev.enter_efficiency);
   Print("mo  in ef=" ,start.avg.enter_efficiency);
  }

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 适当性函数的枚举                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum Enum_Efficiency
  {
   avg_enter_eff,
   stdev_enter_eff,
   avg_exit_eff,
   stdev_exit_eff,
   avg_trade_eff,
   stdev_trade_eff
  };
class="macro">#include <Bulaschev_Statistic.mqh>
input Enum_Efficiency result=class="num">0;class=class="str">"cmt">// 适当性函数
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA优化函数                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
  {
   start.OnPosStat();
   start.OnTradesStat();
   class="type">class="kw">double res;
   class="kw">switch(result)
     {
      case class="num">0: res=start.avg.enter_efficiency;   break;
      case class="num">1: res=-start.stdev.enter_efficiency; break;
      case class="num">2: res=start.avg.exit_efficiency;     break;
      case class="num">3: res=-start.stdev.exit_efficiency;  break;
      case class="num">4: res=start.avg.trade_efficiency;    break;
      case class="num">5: res=-start.stdev.trade_efficiency; break;
      class="kw">default : res=class="num">0; break;
     }  
   class="kw">return(res);

「EA 退出时落盘哪些报告」

EA 在 MT5 里被移除或图表关闭,会触发 OnDeinit。这段逻辑先判断当前是不是在策略测试器的优化模式里跑:如果是优化,就不写任何本地报告,避免几万次遍历把磁盘写爆;只有正常加载运行退出,才把过程数据留痕。 具体落地靠一个 start 对象连续调五个方法:WriteFileDeals 和 WriteFileTrades 分别导出成交、交易两张 csv;WriteFileTrades_all 用适当性函数汇成综合 csv;另两个 HTML2 方法生成可读的成交与交易网页报告。你在实盘 EA 里照抄这个 if 判断,回测优化就不会每代都落盘。 验证方式很简单:把 EA 挂上 EURUSD 的 M1,跑几分钟手动移除,看数据文件夹里是否多出 csv 与 html;再进测试器开优化,退出后对应文件应当没有新增。外汇与贵金属杠杆高,回测报告不等于实盘表现,落盘数据仅作复盘参考。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 去初始化函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   if(!(class="type">bool)MQL5InfoInteger(MQL5_OPTIMIZATION))
     {
      start.WriteFileDeals();       class=class="str">"cmt">// 写成交的csv报告
      start.WriteFileTrades();      class=class="str">"cmt">// 写交易的csv报告
      start.WriteFileTrades_all();  class=class="str">"cmt">// 用适当性函数写综合csv报告
      start.WriteFileDealsHTML2();  class=class="str">"cmt">// 写成交的html报告
      start.WriteFileTradesHTML2(); class=class="str">"cmt">// 写交易的html报告
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

回测里看自定义优化函数的真实偏好

在 MT5 策略测试器里跑单次回测,能直接拉出移动平均线策略的逐笔成交统计。前 10 笔 EURUSD 成交(pos[0]~pos[9],2010.03~2010.05)里,进入效率均值 0.68、标准差 0.26,退出效率均值 0.48、标准差 0.21,交易效率均值仅 0.16、标准差 0.37——离差明显大于进出场两端。 自定义优化函数没去挑成交手数多的参数组合,反而倾向选持仓时间更长、利润彼此接近的成交。原均线代码不含加仓或部分平仓,所以首批结果和后面转换后的账户表现并不一致。 看转换后账户里的 pos[291](2010.08.06):同一卖单从 13:52 到 17:39 被拆成多笔子仓,0.1~0.4 手分批,最后几笔退出效率冲到 0.98 附近,但早期 0.1 手单退出效率只有 0.01~0.39,说明分批逻辑下首尾单质量分化很大。外汇与贵金属属高风险品种,这类回测结论只代表历史样本,实盘可能偏离。 原始成交历史已单独存文件,习惯在 MT4 的 Results 面板看汇总的人会缺这笔细账。开 MT5 把同样 EA 丢进测试器,勾选『详细记录每笔成交』,就能对照 pos 编号自己复算效率列。

◍ 把自定义和标准参数拧在一起试

原文作者最后给开发者的建议很直接:别只调自定义参数,把标准参数也拉进来一起优化,EA 的优化空间才可能真正打开。他附的三个文件里,bulaschev_statistic.mqh 有 91.1 KB,是这套统计逻辑的主体,posstat.mq5 仅 1.69 KB 算轻量外壳。 这套代码只是基础框架,作者本人也写明「仅包含基本内容」,你拿去用之前得按自己的品种和周期改类。外汇和贵金属杠杆高、滑点跳空频繁,直接挂实盘大概率扛不住,先在 MT5 策略测试器里跑历史数据看分布再说。 真要动手,先把 moving_averages_statistic__1.mq5 下下来,改两行均线周期,让小布替你跑一遍正态检验,比空想参数组合省时间。

让小布替你跑这套
这些诊断逻辑小布盯盘已内置,打开对应品种页即可看到基于现代仓位模型的进出场效率概览,你只需核对参数是否符合自己的交易习惯。

常见问题

因为 MT5 支持部分平仓和加仓,一个仓位可能对应多笔成交,统计必须基于平仓事实而非建仓事实,否则样本会被重复或遗漏。
它把通用的统计接口和具体品种、时间帧的差异隔离,让你在不改核心算法的情况下接入不同账户的执行环境。
可以,小布会标记偏离基准置信区间的退出效率,并提示可能是样本期特殊或参数过拟合,供你进一步人工判断。
大概率来自滑点和成交粒度差异,建议在测试里开启真实延迟模拟,并把派生类的解释层对齐经纪商执行规则。
本篇案例拆解偏重落地,但核心公式依赖前篇概念,关于完整统计基底见《交易系统的评估·基础篇》。