交易系统的评估 - 有关进入、退出与交易效率的概述·进阶篇
📊

交易系统的评估 - 有关进入、退出与交易效率的概述·进阶篇

(2/3)·从订单到仓位的术语迁移,正悄悄让你的历史回测数据失真

含代码示例实战向 第 2/3 篇
还在用建仓即平仓的旧账本算胜率?MT5里部分平仓和加仓早把这套逻辑拆了。顺着老书里的公式跑评估,出来的效率指标可能连你自己都不敢信。

「理清 deal、order、position 与 trade 的边界」

在 MT5 客户端里,Order(订单)、Deal(成交)、Position(仓位)是开发者定死的三个词,但 Bulachev 的统计体系只用 deal 和 order 混指‘已发生的交易’,未平仓的 deal 他叫 position。为了避免回测时概念串味,我们把 Bulachev 说的‘交易’统一记作 trade,即 trade = deal;MT5 原生三术语保持不变,于是手头有 Position / Deal / Order / Trade 四个词要分清。 Order 是发往服务器建仓或修改仓位的指令,它不进统计直接样本,因为发单不保证按量价成交;统计必须落在 Deal 上,订单只通过成交间接参与。这一点直接决定你写 EA 的成交统计模块时,不能拿 OrderSend 的返回值当交易笔数。 Bulachev 体系里 type 和 direction 被拆开:type 指 buy/sell(买卖属性),direction 指 in/out(进出场),还包括加仓(非首笔 buy)和部分平仓(非末笔 sell)。当反向成交数量大于当前仓位,就触发反转,旧 trade 被平、新 trade 建立。 部分平仓是老解释系统的盲区。例子里建仓 0.3 手、平仓 0.2 手,若每笔 trade 等量平仓,就变成一笔 deal 拆成多笔 trade,各自相同进场点、不同出场点,缺量靠复制原 trade 并改 volume 补齐。这种重算可能不符合某人‘想平另一笔’的主观预期,但不影响系统评估——它只会戳破‘MT4 交易零损耗’的错觉。 下面这段原始成交与 Bulachev 重解释对照,可直接贴进 MT5 看盘比对:deal[0] 0.1 sell @1.22218 建仓,deal[1] 0.2 buy @1.22261 反转并部分平,deal[2] 0.1 buy @1.22337 再建,deal[3] 0.2 sell @1.22310 平剩余。重解释后拆成 trade[0](sell 进 buy 出)、trade[1] 与 trade[2](两笔 buy 同价出)。外汇贵金属高风险,重算仅用于复盘概率倾向,不代表实盘必然复现。

MQL5 / C++
МТ 中的解释-<span class="number">class="num">5</span>
deal[ <span class="number">class="num">0</span> ]&nbsp;&nbsp;<span class="keyword">in</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0.1</span>&nbsp;&nbsp; sell&nbsp;&nbsp; <span class="number">class="num">1.22218</span>&nbsp;&nbsp; <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">33</span>
deal[ <span class="number">class="num">1</span> ]&nbsp;&nbsp;<span class="keyword">in</span>/<span class="keyword">out</span>&nbsp;&nbsp;<span class="number">class="num">0.2</span>&nbsp;&nbsp; buy&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1.22261</span>&nbsp;&nbsp; <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">36</span>
deal[ <span class="number">class="num">2</span> ]&nbsp;&nbsp;<span class="keyword">in</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0.1</span>&nbsp;&nbsp; buy&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1.22337</span>&nbsp;&nbsp; <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">39</span>
deal[ <span class="number">class="num">3</span> ]&nbsp;&nbsp;<span class="keyword">out</span>&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">0.2</span>&nbsp;&nbsp; sell&nbsp;&nbsp; <span class="number">class="num">1.22310</span>&nbsp;&nbsp; <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">41</span>
Bulachev 的解释
trade[ <span class="number">class="num">0</span> ]&nbsp;&nbsp;<span class="keyword">in</span> <span class="number">class="num">0.1</span>&nbsp;&nbsp;sell <span class="number">class="num">1.22218</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">33</span>&nbsp;&nbsp; <span class="keyword">out</span> <span class="number">class="num">1.22261</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">36</span>
trade[ <span class="number">class="num">1</span> ]&nbsp;&nbsp;<span class="keyword">in</span> <span class="number">class="num">0.1</span>&nbsp;&nbsp;buy&nbsp;&nbsp;<span class="number">class="num">1.22261</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">36</span>&nbsp;&nbsp; <span class="keyword">out</span> <span class="number">class="num">1.22310</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">41</span>
trade[ <span class="number">class="num">2</span> ]&nbsp;&nbsp;<span class="keyword">in</span> <span class="number">class="num">0.1</span>&nbsp;&nbsp;buy&nbsp;&nbsp;<span class="number">class="num">1.22337</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">39</span>&nbsp;&nbsp; <span class="keyword">out</span> <span class="number">class="num">1.22310</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">41</span>
trade[<span class="number">class="num">0</span>]&nbsp;&nbsp;in <span class="number">class="num">0.1</span>&nbsp;&nbsp;sell <span class="number">class="num">1.22218</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">33</span>
trade[<span class="number">class="num">0</span>]&nbsp;&nbsp;in <span class="number">class="num">0.1</span>&nbsp;&nbsp;sell <span class="number">class="num">1.22218</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">33</span> out <span class="number">class="num">1.22261</span> <span class="number">class="num">2010.06</span>.<span class="number">class="num">14</span> <span class="number">class="num">13</span>:<span class="number">class="num">36</span>

◍ 同一笔成交在不同口径下的拆法

看 2010.06.14 那两组买单:trade[1] 在 13:36 以 1.22261 开 0.1 手多单,trade[2] 在 13:39 以 1.22337 加 0.1 手,两笔都在 13:41 以 1.22310 平仓。按 MT 终端的历史标签,这就是两条独立平仓记录,单看容易以为平得干脆。 但把同一时段的 deal 流水拉出来就变样了:2010.06.15 08:00 先卖 0.3 手于 1.22133,紧接着 08:01 买回 0.2 手于 1.22145 平掉其中一部分,08:02 又买 0.4 手、08:03 卖 0.4 手在 1.22122,最后 08:06 买 0.1 手于 1.2206 收尾。这里 deal 级别把开平拆成了五条逐笔,和上面的 trade 视角完全不是一套数。 Bulachev 那种归并解释更极端:把 08:00 的 0.3 手卖单拆成 trade[0] 的 0.2 和 trade[1] 的 0.1,分别配对 08:01、08:02 的买平;再把 08:02 的 0.4 买、08:03 的 0.4 卖合成 trade[2],尾单 trade[3] 则是 08:03 卖 0.1 于 1.22122、08:06 买 0.1 于 1.2206 平。三种口径下,同一段行情的「交易笔数」从 2 到 5 再到 4,外汇和贵金属自带高杠杆,复盘前先确认你盯的是哪一层的记录,否则胜率算出来会偏得离谱。

同一毫秒的两笔卖单为何一平一扛

下面这段日志来自一次 EURUSD 的实盘回放:在 2010.06.15 08:00,价格 1.22133 同时挂出三笔 sell,但只有一笔在 08:01 以 1.22145 平仓,另外两笔仍留着。 trade[0] in 0.3 sell 1.22133 2010.06.15 08:00 trade[0] in 0.2 sell 1.22133 2010.06.15 08:00 out 1.22145 2010.06.15 08:01 trade[1] in 0.1 sell 1.22133 2010.06.15 08:00 注意第一笔 0.3 手始终没有 out 记录,第二笔 0.2 手在 1 分钟后微亏 1.2 点离场,第三笔 0.1 手也无平仓痕迹。外汇与贵金属杠杆交易高风险,这种同价多单不同退出的现象,往往意味着仓位管理或平仓条件在代码里被分桶处理了。 打开 MT5 的 Experts 日志对照上面三行,重点看 trade 数组索引是否跨 bar 复用——若索引 0 被两条记录占用,说明你的订单池没有在平仓后及时清槽,可能造成重复统计或漏平。

MQL5 / C++
trade[class="num">0]  in class="num">0.3  sell class="num">1.22133 class="num">2010.06.class="num">15 class="num">08:class="num">00
trade[class="num">0]  in class="num">0.2  sell class="num">1.22133 class="num">2010.06.class="num">15 class="num">08:class="num">00   out class="num">1.22145 class="num">2010.06.class="num">15 class="num">08:class="num">01  trade[class="num">1]  in class="num">0.1  sell class="num">1.22133 class="num">2010.06.class="num">15 class="num">08:class="num">00

「把成交流重写成交易循环的 C_Pos 类」

MQL5 里策略测试器自带的 Bulachev 统计只覆盖一部分,要把「逐笔成交」重解释成「完整交易」,得靠 OOP 继承自己补方法。先撘两个结构:S_Stat_Deals 装单笔成交主字段(订单号、类型、方向、量、价、时间),S_Stat_Trades 装一笔交易的建/平两端字段。派生指标(如持仓时长)不用存,用时再算,结构更干净。 核心类 C_Pos 声明 public 的成交与交易结构指针数组——因为历史里成交和交易都不止一条,用数组比单变量方便,且继承函数也要读,故放 public。三个 public 方法管历史:OnHistory() 把每笔成交拷进 S_Stat_Deals 副本;OnHistoryTransform() 按仓位重排成新解释系统。 转换时先用 DigitMinLots() 定最小可计成交量精度,再把 count_trades 与 trades_ends 清零,按成交数预估重分配交易结构内存。实测大多数仓位交易数 ≤ 成交数,先给近似值比每步重分配整块数组省机器时间。 筛选循环嵌套三层:in(开仓)、in/out(反手)、out(平仓),前一筛返回 false 才进下一筛,少跑无用分支。in 里调 open_pos() 把指针数组 +1、成交字段灌进交易建仓端,此时平仓端空,count_trades 涨而 trades_ends 不涨,二者差即未平交易数。 out 最麻烦:若是仓位末笔成交,平掉所有未平;否则比量——等量全平,量不等则循环平下一笔直至够数,不够就部分平仓并拷一份缺量新交易,且后续交易整体偏移一位保时间序。下面两段结构定义是整套重写的地基。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 成交的结构                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct S_Stat_Deals
  {
class="kw">public:
   class="type">ulong                DTicket;       class=class="str">"cmt">// 成交的订单号  
   ENUM_DEAL_TYPE       deals_type;    class=class="str">"cmt">// 成交的类型
   ENUM_DEAL_ENTRY      deals_entry;   class=class="str">"cmt">// 成交的方向 
   class="type">class="kw">double               deals_volume;  class=class="str">"cmt">// 成交量    
   class="type">class="kw">double               deals_price;   class=class="str">"cmt">// 成交建仓价  
   class="type">class="kw">datetime             deals_date;    class=class="str">"cmt">// 成交建仓时间  
                     S_Stat_Deals(){};
                    ~S_Stat_Deals(){};
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 交易结构                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct S_Stat_Trades
  {
class="kw">public:
   class="type">ulong                OTicket;       class=class="str">"cmt">// 建仓交易订单号
   class="type">ulong                CTicket;       class=class="str">"cmt">// 平仓交易订单号    
   ENUM_DEAL_TYPE       trade_type;    class=class="str">"cmt">// 交易类型

◍ 用结构体把每笔交易拆开量

想在 MT5 里复盘自己的真实表现,第一步是把券商流水里的 deal(成交)重新聚合成 trade(交易)。下面这段结构定义就是干这个的:它给每笔交易留了建仓价、平仓价、最高/最低触及价,以及按 Bulachev 思路算的进入效率与退出效率字段。 进入效率衡量你建仓后价格朝有利方向跑了多少,退出效率看平仓位置相对整段行情有多准。两者都是 double,回测时直接打印就能对比‘进场好但出场烂’还是反过来。 C_Pos 类则负责持有某个 pos_id 下的全部成交与聚合后的交易数组。注意它初始化时把 count_deals、count_trades、trades_ends 全置 0,说明统计是增量填充的,别在循环外误用旧值。外汇与贵金属杠杆高,这类统计只帮你看偏差,不预示下一笔盈亏。

MQL5 / C++
class="type">class="kw">double trade_volume;      class=class="str">"cmt">// 交易量
class="type">class="kw">double max_price_trade;    class=class="str">"cmt">// 交易最高价位
class="type">class="kw">double min_price_trade;    class=class="str">"cmt">// 交易最低价位
class="type">class="kw">double enter_price;        class=class="str">"cmt">// 交易建仓价位
class="type">class="kw">datetime enter_date;       class=class="str">"cmt">// 交易建仓时间
class="type">class="kw">double exit_price;         class=class="str">"cmt">// 交易平仓价位
class="type">class="kw">datetime exit_date;        class=class="str">"cmt">// 交易平仓时间
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_Stat_Trades(){};
~S_Stat_Trades(){};
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把成交转换为交易的类                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Pos
{
class="kw">public:
 S_Stat_Deals    m_deals_stats[];   class=class="str">"cmt">// 成交结构
 S_Stat_Trades   m_trades_stats[];  class=class="str">"cmt">// 交易结构
 class="type">long            pos_id;            class=class="str">"cmt">// 仓位编号
 class="type">class="kw">string          symbol;            class=class="str">"cmt">// 仓位交易品种
 class="type">int             count_deals;       class=class="str">"cmt">// 成交数量
 class="type">int             count_trades;      class=class="str">"cmt">// 交易数量
 class="type">int             trades_ends;       class=class="str">"cmt">// 完成的交易数量
 class="type">int             DIGITS;            class=class="str">"cmt">// 仓位交易品种最小交易量的精确度
 C_Pos()
  {
   count_deals=class="num">0;
   count_trades=class="num">0;
   trades_ends=class="num">0;
  };
 ~C_Pos(){};
 class="type">void            OnHistory();       class=class="str">"cmt">// 建立仓位历史
 class="type">void            OnHistoryTransform(); class=class="str">"cmt">// 把仓位历史转换为新的系统解释
 class="type">void            efficiency();      class=class="str">"cmt">// 根据 Bulachev&class="macro">#x27;s 方法计算的效率值
class="kw">private:
 class="type">void            open_pos(class="type">int c);
 class="type">void            copy_pos(class="type">int x);

把历史成交塞进自定义结构

做持仓统计前,得先把账户历史里的每笔成交捞出来,填进自己的结构体数组。下面这段直接挂在 C_Pos 类里,用 HistoryDealGetTicket 按索引取成交单号,再逐个读类型、方向、成交量、价格和时间为字段赋值。 ArrayResize(m_deals_stats,count_deals) 先按总成交数 count_deals 扩好数组,避免循环里反复重分配。外汇和贵金属杠杆高,历史里可能混着对冲单和爆仓平仓,DEAL_ENTRY 方向字段一定要读,不然后面算净敞口会偏。 循环里每一行对应一个 HistoryDealGetInteger / GetDouble 调用:DEAL_TYPE 区分买卖、DEAL_VOLUME 是手数、DEAL_PRICE 是成交价、DEAL_TIME 转成 datetime。开 MT5 按 F4 把这段贴进 EA 的 OnHistory,跑完在观测窗口看 m_deals_stats[0].deals_price 是不是和账户历史第一笔对得上。

MQL5 / C++
class="type">void C_Pos::OnHistory()
  {
   ArrayResize(m_deals_stats,count_deals);
   for(class="type">int i=class="num">0;i<count_deals;i++)
     {
      m_deals_stats[i].DTicket=HistoryDealGetTicket(i);
      m_deals_stats[i].deals_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_TYPE);  class=class="str">"cmt">// 成交类型
      m_deals_stats[i].deals_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_ENTRY);class=class="str">"cmt">// 成交方向
      m_deals_stats[i].deals_volume=HistoryDealGetDouble(m_deals_stats[i].DTicket,DEAL_VOLUME);                class=class="str">"cmt">// 成交量
      m_deals_stats[i].deals_price=HistoryDealGetDouble(m_deals_stats[i].DTicket,DEAL_PRICE);                 class=class="str">"cmt">// 建仓价格
      m_deals_stats[i].deals_date=(class="type">class="kw">datetime)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_TIME);         class=class="str">"cmt">// 建仓时间
     }
  };
让小布替你跑这套
把重复劳动交给小布盯盘,你专注决策——这些仓位级的进出场诊断小布已内置,打开对应品种页即可看到实时重构后的交易效率分布。

常见问题

原书基于每手建仓必伴等量反向平仓的假设,而 MT5 支持部分平仓与加仓,解释系统须改为依据平仓事实而非建仓事实,否则样本划分错位。
可以,小布盯盘在品种页内置了基于仓位解释的诊断模块,自动把历史成交按现代交易条件重算进入与退出效率,省去手动改代码的麻烦。
Order 只是发往经纪商的交易请求记录,Position 才是借入资金存续期间的真实敞口;统计效率应以仓位生命周期为单元,而非订单流水。
部分平仓使单次建仓对应多次退出,传统配对法失效;需按平仓占比切分收益与成本,再分别计入退出分布,概率上才接近真实表现。