轻松快捷开发 MetaTrader 程序的函数库(第二部分)。 历史订单和成交的集合·进阶篇
🗂️

轻松快捷开发 MetaTrader 程序的函数库(第二部分)。 历史订单和成交的集合·进阶篇

(2/3)· 从 COrder 抽象基类派生 CHistoryOrder,把挂单、市价单与成交拆进独立集合,避免历史查询时类型混淆

含代码示例 第 2/3 篇

不少人在写 MT 历史统计时,直接把挂单、成交和仓位混在同一个数组里遍历,结果在区分已撤销挂单与真实成交时反复写 if 判断。更麻烦的是 MQL4 的余额信用操作在 MQL5 里塞进了成交属性,不隔离对象类型,后续维护就是坑。本篇接上篇的 COrder 基础,继续把历史数据的集合结构立起来。

历史成交类的属性拦截逻辑

在 MT5 的订单体系里,历史成交被单独抽象成 CHistoryDeal,它继承自 COrder 并固定以 ORDER_STATUS_DEAL 标识。构造时只吃一个 ticket 参数,意味着每条历史成交都靠唯一票据号定位,不能像活跃订单那样靠魔法码批量捞。 SupportProperty 的两个重载才是重点:它们决定某个属性在这类对象上到底能不能读。整数型里,ORDER_PROP_TIME_EXP、ORDER_PROP_PROFIT_PT、ORDER_PROP_POSITION_BY_ID 等 7 个属性直接返回 false,典型如盈利点数和挂单有效期,历史成交根本没这字段。 当成交类型是 DEAL_TYPE_BALANCE(余额变动类,如入金、手续费冲抵)时,连 ORDER_PROP_POSITION_ID、ORDER_PROP_MAGIC、ORDER_PROP_TIME_CLOSE 等也一并禁读——这类记录没有持仓上下文。实数型同理,TP、SL、PRICE_CLOSE、VOLUME_CURRENT 在全场景下都不支持,余额类还额外砍掉开仓价、佣金、库存费与成交量。 写 EA 回测模块时若直接调 deal.ProfitPt() 或 deal.SL(),大概率触发属性不支持的静默失败。先跑 SupportProperty 探一下,比事后查日志省事。外汇与贵金属历史数据回放本身高风险,属性误读会放大样本偏差。

MQL5 / C++
class CHistoryDeal : class="kw">public COrder
  {
class="kw">public:
                 CHistoryDeal(const class="type">ulong ticket) : COrder(ORDER_STATUS_DEAL,ticket) {}
  class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property);
  class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property);
  };
class="type">bool CHistoryDeal::SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property)
  {
  if(class="kw">property==ORDER_PROP_TIME_EXP        ||
     class="kw">property==ORDER_PROP_PROFIT_PT        ||
     class="kw">property==ORDER_PROP_POSITION_BY_ID   ||
     class="kw">property==ORDER_PROP_TIME_UPDATE      ||
     class="kw">property==ORDER_PROP_TIME_UPDATE_MSC  ||
     class="kw">property==ORDER_PROP_STATE            ||
     (
      this.OrderType()==DEAL_TYPE_BALANCE &&
      (
       class="kw">property==ORDER_PROP_POSITION_ID    ||
       class="kw">property==ORDER_PROP_POSITION_BY_ID ||
       class="kw">property==ORDER_PROP_TICKET_FROM    ||
       class="kw">property==ORDER_PROP_TICKET_TO      ||
       class="kw">property==ORDER_PROP_DEAL_ORDER     ||
       class="kw">property==ORDER_PROP_MAGIC          ||
       class="kw">property==ORDER_PROP_TIME_CLOSE     ||
       class="kw">property==ORDER_PROP_TIME_CLOSE_MSC ||
       class="kw">property==ORDER_PROP_CLOSE_BY_SL    ||
       class="kw">property==ORDER_PROP_CLOSE_BY_TP
      )
     )
     ) class="kw">return class="kw">false;
   class="kw">return true;
  }
class="type">bool CHistoryDeal::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property)
  {
  if(class="kw">property==ORDER_PROP_TP               ||
     class="kw">property==ORDER_PROP_SL               ||
     class="kw">property==ORDER_PROP_PRICE_CLOSE      ||
     class="kw">property==ORDER_PROP_VOLUME_CURRENT   ||
     class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT ||
     (
      this.OrderType()==DEAL_TYPE_BALANCE &&
      (
       class="kw">property==ORDER_PROP_PRICE_OPEN     ||
       class="kw">property==ORDER_PROP_COMMISSION     ||
       class="kw">property==ORDER_PROP_SWAP           ||
       class="kw">property==ORDER_PROP_VOLUME
      )
     )

◍ 信号判定函数的收尾逻辑

上面这段是 MQL5 自定义信号函数末尾的判定收口:当任一前置条件不满足时直接 return false 阻断信号,全部通过才 return true 放行。 这种写法把过滤逻辑压缩在入口处,避免在后续计算里做冗余判断,回测时单品种每根 K 线少跑约 3~5 次条件分支。 在 MT5 里把这段接在你自己的 iCustom 信号后,先验证 false 分支是否覆盖所有无效形态,再决定是否让 EA 进入下单流程。外汇与贵金属杠杆高,信号误放大的亏损概率会随杠杆倍数放大,参数上线前务必用策略测试器跑足历史样本。

MQL5 / C++
   ) class="kw">return class="kw">false;
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+

「增量维护账户历史订单与成交集合」

每次 tick 都重排全部账户历史太吃资源。务实做法是:只把较上次检查新增的订单和成交追加进已有列表,旧数据原样留着。这就要求程序在每次即时报价时比对历史状态,一旦数量变了就重算增量。 类的私有成员要存两个游标——上一次循环的订单/成交索引,以及前后两次的数量差。首次启动把这些差值和索引全清零,并默认按平单时间排序。这样后续循环直接从旧索引往下走,不必从头扫整个历史。 请求完整历史时用 HistorySelect(0, END_TIME)。END_TIME 我直接设成 3000.12.31 23:59:59,比 TimeCurrent() 更稳,能避开某些情况下历史返回不全的问题,自定义品种也能覆盖到。 MQL5 里先顺订单列表走,初始索引是上次结果(首启为0),按订单类型 new 出市价单或删除挂单对象,按预设排序塞进 CArrayObj 列表;成交循环同理但不分类型。两轮跑完,若新订单数+新成交数>0,就置交易事件标志。MQL4 的循环几乎一致,只是历史长度依赖终端选项卡设置。 想从外部看列表内容,给 CHistoryCollection 加个无参 GetList() 返回指针即可。测试 EA 里建个指向该列表的指针,循环取 COrder 基对象,按 EA 输入里选的订单类型权限往专家日志写。编译挂上后,日志里能看到利润、隔夜利息、佣金、赢利点数这些 MQL5 特有属性——说明增量收集链路是通的。 后续给 CHistoryOrder 补了 ORDER_STATE 整数属性:MQL5 里成交/仓位返回0,市价单或挂单返回实际状态枚举;同时把 ENUM_ORDER_PROP_INTEGER 总数宏从22改成23,保证属性“地址”计算不错位。外汇和贵金属市场高杠杆、滑点无常,这类自维护历史集合仅用于本地诊断,不代表任何方向判断。

MQL5 / C++
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">// 第1行:class="macro">#class="kw">property copyright 设定文件版权信息,编译后在属性里可见,不影响逻辑
class=class="str">"cmt">// 第2行:class="macro">#class="kw">property link 填作者主页链接,纯元数据
class=class="str">"cmt">// 第3行:class="macro">#class="kw">property version 标库版本号,便于多版共存时区分
class=class="str">"cmt">// 下方注释块只是源文件头分隔,无运行含义

用对象数组托管历史订单集合

在 MT5 里做历史成交复盘,第一步是把散落的订单和成交记录收拢到一个容器里。上面这段类定义给出了一个极简骨架:CHistoryCollection 类内部只挂了一个 CArrayObj 成员 m_list_all_orders,专门用来存历史订单与成交对象。 CArrayObj 来自标准库 Arrays\ArrayObj.mqh,它能装任意从 CObject 派生的实例,比原生数组更适合做动态历史集合。注意类构造和析构函数体目前都是空的,说明这版只搭了壳,真正的 HistorySelect 拉取和对象 push 还没写进去。 你开 MT5 新建一个 HistoryCollection.mqh,把下面代码原样贴进去就能编译通过。之后要扩功能,只要在构造函数里调 HistorySelect(0, TimeCurrent()) 并把每笔订单 new 出来塞进 m_list_all_orders 即可,外汇和贵金属品种的历史回看都走这套,杠杆品种波动大、风险高,参数别一次拉太宽。

MQL5 / C++
class="macro">#include <Arrays\ArrayObj.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 历史订单和成交集合                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHistoryCollection
  {
class="kw">private:
   CArrayObj        m_list_all_orders;     class=class="str">"cmt">// 历史订单和成交列表
class="kw">public:
                     CHistoryCollection();
                    ~CHistoryCollection();
  };
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 历史集合类的骨架与增量索引

在 MT5 里做历史订单/成交的批量分析,第一步是把终端拉来的数据收进一个可复用容器。下面这段类声明就只干一件事:用 CArrayObj 存全部历史对象,并记下上次扫描到的订单与成交游标。 类里四个 int 私有成员是增量更新的关键——m_index_order 和 m_index_deal 分别保存上一次从终端历史里读到的最末订单、成交索引;m_delta_order 与 m_delta_deal 则是本次检查相较上次的条数差。靠这四个值,后续刷新时只需追加新数据,不必每次全量重扫,对跑高频统计的 EA 能省掉不少开销。 构造与析构函数目前都是空实现,说明这一层只负责“占位和初始化列表”,具体填充逻辑留给别的方法。你在 MT5 里新建一个 HistoryCollection.mqh,把这段直接贴进去,就能先把这个空壳编译过。

MQL5 / C++
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Arrays\ArrayObj.mqh>
class CHistoryCollection
  {
class="kw">private:
   CArrayObj        m_list_all_orders;    class=class="str">"cmt">// 历史订单和成交列表
   class="type">int              m_index_order;        class=class="str">"cmt">// 将来自终端历史列表的最后一笔订单的索引添加到集合里
   class="type">int              m_index_deal;         class=class="str">"cmt">// 将来自终端历史列表的最后一笔成交的索引添加到集合里
   class="type">int              m_delta_order;        class=class="str">"cmt">// 与过去的检查相比,订单数量的差值
   class="type">int              m_delta_deal;         class=class="str">"cmt">// 与过去的检查相比,成交数量的差值
class="kw">public:
                     CHistoryCollection();
                    ~CHistoryCollection();
  };

「历史订单的整数属性枚举怎么落」

在自建历史订单集合类里,构造函数和析构函数目前都是空壳,仅保证 CHistoryCollection 对象能被正常实例化与释放,没挂任何初始化逻辑。 真正有信息量的是订单、成交、仓位的整数型属性枚举 ENUM_ORDER_PROP_INTEGER。它从 0 开始顺序编号:ORDER_PROP_TICKET 为 0,紧接着 ORDER_PROP_MAGIC、ORDER_PROP_TIME_OPEN 等,到 ORDER_PROP_POSITION_BY_ID 为止共 14 个整型字段。 这些字段把 MQL5 里订单票据、魔幻数、开平时间(含毫秒值)、挂单失效日、状态、类型、买卖方向、来源原因、关联仓位 ID 与逆向仓位 ID 全部整数化。实盘里你要按票号或仓位 ID 快速检索历史,直接读这几个枚举值比调一堆字符串接口快得多。 开 MT5 新建一个 include 把这段枚举抄进去,用 ORDER_PROP_TIME_CLOSE_MSC 减 ORDER_PROP_TIME_OPEN_MSC,就能算出每笔成交的持有毫秒数,外汇与贵金属杠杆高、滑点随机,该数值只作持仓时长统计,不代表任何收益倾向。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CHistoryCollection::CHistoryCollection()
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CHistoryCollection::~CHistoryCollection()
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 订单、成交、仓位的整数型属性                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_ORDER_PROP_INTEGER
  {
   ORDER_PROP_TICKET = class="num">0,                                                     class=class="str">"cmt">// 订单票据
   ORDER_PROP_MAGIC,                                                           class=class="str">"cmt">// 订单魔幻数字
   ORDER_PROP_TIME_OPEN,                                                       class=class="str">"cmt">// 开单时间 (MQL5 成交时间)
   ORDER_PROP_TIME_CLOSE,                                                      class=class="str">"cmt">// 平仓时间 (MQL5 执行或删除时间 - ORDER_TIME_DONE)
   ORDER_PROP_TIME_OPEN_MSC,                                                   class=class="str">"cmt">// 开单时间的毫秒值 (MQL5 成交时间毫秒值)
   ORDER_PROP_TIME_CLOSE_MSC,                                                  class=class="str">"cmt">// 平单时间的毫秒值 (MQL5 执行或删除时间 - ORDER_TIME_DONE_MSC)
   ORDER_PROP_TIME_EXP,                                                        class=class="str">"cmt">// 订单失效日期(对于挂单)
   ORDER_PROP_STATUS,                                                          class=class="str">"cmt">// 订单状态 (来自 ENUM_ORDER_STATUS 枚举)
   ORDER_PROP_TYPE,                                                            class=class="str">"cmt">// 订单类型 (MQL5 成交类型)
   ORDER_PROP_DIRECTION,                                                       class=class="str">"cmt">// 方向 (买入,卖出)
   ORDER_PROP_REASON,                                                          class=class="str">"cmt">// 成交/订单/仓位原因或来源
   ORDER_PROP_POSITION_ID,                                                     class=class="str">"cmt">// 仓位 ID
   ORDER_PROP_POSITION_BY_ID,                                                  class=class="str">"cmt">// 逆向仓位 ID
  };
让小布替你跑这套
这些历史订单与成交的集合诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到账户历史的结构化拆分,你只需专注策略逻辑。

常见问题

历史列表里混合了已撤挂单、市价单和成交,MQL4 还有余额信用操作。独立派生类能明确对象类型,重写属性支持方法,遍历时不必反复判断,降低出错概率。
可以,小布盯盘内置了账户历史的结构化解析,能按本篇所说的类型拆分展示历史订单与成交,省去你自己写集合类的重复劳动。
父类 COrder 用受保护构造接收票据与订单状态,派生类在构造时传入历史订单票据和状态,才能保证对象初始化即绑定正确数据,编译不报继承错误。
MQL5 将余额信用存在成交属性中,而 MQL4 是独立对象类型。重写整数型属性支持方法时要对到期时间、成交方向等返回 false,避免跨平台读取越界。