轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件(基础篇)

第 1/3 篇

「把交易事件塞进一个集合类」

在 MT5 里做自动化程序,最烦的是散落各处的订单回执、成交推送和自定义事件。把这类信号收拢到一个集合类里,主逻辑只跟集合要状态,耦合会低很多。 原文给出的落地路径是先重排函数库目录,再单独建事件类,最后用「交易事件的集合」统一接管定义、处理与接收。这样你改事件字段时,不用翻遍 EA 的 OnTradeTransaction。 实测中这套结构在 2019 年 6 月公开的示例里跑通,基础工程被下载查看 3643 次、评论 13 条,说明社区对「事件集中管理」的接受度不低。外汇与贵金属品种波动大,事件丢失可能造成重复下单,集成前务必在策略测试器里压一遍。 下一步是让集合类主动向外发事件,而不是被动等 OnTick 来查——这部分留到后面小节拆。

把事件类塞进分层目录并给集合打身份标签

做跨平台 EA 函数库时,对象一多就不能全堆在 Objects 根目录。实际做法是新建 Objects/Orders 与 Objects/Events 两个子文件夹,把历史订单、挂单、成交等已有类迁进 Orders,Events 留给我们马上要写的账户事件对象;同时把 Select.mqh 从 Collections 挪到 Services,因为它提供跨集合属性快速访问,本质是个服务类。 迁移后编译地址全变,Order.mqh、HistoryCollection.mqh、MarketCollection.mqh 里的 include 路径要跟着改,否则 MT5 编译器直接报找不到文件。改完地址再编译,应该零错误——这是验证目录重构是否干净的最快办法。 集合将来会有十几个,靠传 flag 标示列表类型太笨。标准库 CObject::Type() 虚方法返回对象 ID,CArrayObj 的 ID 是 0x7778,我们派生 CListObj 重载 Type() 返回自定义常量,在 Defines.mqh 里先定义 COLLECTION_HISTORY_ID、COLLECTION_MARKET_ID 等。这样 HistoryCollection 和 MarketCollection 继承 CListObj 后,在构造函数里置好自身 ID,任意方法收到列表指针都能用 Type() 识别归属,不用额外参数。 终端历史有个坑:HistorySelect() 拉出来的订单和成交是两套独立历史,不按事件时间排。实测顺序可能是 开仓→下挂单→挂单激活→部分平仓,但列表里挂单激活排在了部分平仓前面。为此给历史集合类加了三个私有方法:IsPresentOrderInList() 按票据+类型查缺失,GetMissingOrder() 从末端向前找终端有但集合没有的订单,CreateOrderObj() 统一建对象入列。Refresh() 也改了,遇到乱序就补回“丢失”的订单,保证事件链从开仓到平仓连贯可追。 COrder 公共部分新增相似订单比对方法,遍历自身全部属性与传入指针逐字段比较,任一不等即返回 false。这在事件去重和状态变更检测里直接用得上。

MQL5 / C++
class="macro">#include "..\Services\DELib.mqh"
class="macro">#include "..\..\Services\DELib.mqh"
class="macro">#include "Select.mqh"
class="macro">#include "..\Objects\HistoryOrder.mqh"
class="macro">#include "..\Objects\HistoryPending.mqh"
class="macro">#include "..\Objects\HistoryDeal.mqh"
class="macro">#include "..\Services\Select.mqh"
class="macro">#include "..\Objects\Orders\HistoryOrder.mqh"
class="macro">#include "..\Objects\Orders\HistoryPending.mqh"
class="macro">#include "..\Objects\Orders\HistoryDeal.mqh"
class="macro">#include "Select.mqh"
class="macro">#include "..\Objects\MarketOrder.mqh"
class="macro">#include "..\Objects\MarketPending.mqh"
class="macro">#include "..\Objects\MarketPosition.mqh"
class="macro">#include "..\Services\Select.mqh"
class="macro">#include "..\Objects\Orders\MarketOrder.mqh"
class="macro">#include "..\Objects\Orders\MarketPending.mqh"

◍ 库内宏定义与集合 ID 的底层约定

在 MT5 自建交易函数库时,用宏把调试信息和集合标识固化下来,能省掉大量重复字符串拼接。下面这段头文件片段把错误定位、定时器节奏、三类订单集合的 ID 全部用 #define 锁死,复制进你的 Include 目录即可直接编译。 DFUN_ERR_LINE 会根据终端语言切换提示词:俄语环境输出 ', Page ',其他语言输出 ', Line ',再拼上 __FUNCTION__ 和 __LINE__。这样在 Journal 里点开报错,能直接看到是哪个函数第几行,不用手动翻代码。 定时器相关三个值值得留意:TIMER_FREQUENCY 设为 16 毫秒,是库内定时器最小触发间隔;COLLECTION_PAUSE 为 250 毫秒,控制订单/成交集合的轮询暂停;COLLECTION_COUNTER_STEP 每次加 16,配合 ID 为 1 的计数器做分批加载,避免一次性拉历史把主线程卡死。 三个集合 ID 采用 0x7778 偏移分配:历史集合用 0x7778+1,入场集合用 0x7778+2,事件集合用 0x7778+3。在 CList 派生类里用这几个常量做 ListID,后续按 ID 取集合比用字符串名快且不易拼错。外汇与贵金属杠杆高、滑点跳空频繁,这类底层集合若 ID 冲突会导致持仓统计错乱,上真实账户前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
class="macro">#include "..\Objects\Orders\MarketPosition.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 宏定义替换                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 描述函数的错误行号
class="macro">#define DFUN_ERR_LINE(__FUNCTION__+(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian" ? ", Page " : ", Line ")+(class="type">class="kw">string)__LINE__+": ")
class="macro">#define DFUN(__FUNCTION__+": ")      class=class="str">"cmt">// "函数描述"
class="macro">#define COUNTRY_LANG("Russian")              class=class="str">"cmt">// 国家语言
class="macro">#define END_TIME(D&class="macro">#x27;class="num">31.12.class="num">3000 class="num">23:class="num">59:class="num">59&class="macro">#x27;) class=class="str">"cmt">// 请求帐户历史记录数据的结束日期
class="macro">#define TIMER_FREQUENCY(class="num">16)                     class=class="str">"cmt">// 函数库定时器的最小频率(以毫秒为单位)
class="macro">#define COLLECTION_PAUSE(class="num">250)                    class=class="str">"cmt">// 订单和成交集合的计时器暂停,以毫秒为单位
class="macro">#define COLLECTION_COUNTER_STEP(class="num">16)                class=class="str">"cmt">// 订单和成交集合计时器计数器的增量
class="macro">#define COLLECTION_COUNTER_ID(class="num">1)                  class=class="str">"cmt">// 订单和成交集合计时器计数器 ID
class="macro">#define COLLECTION_HISTORY_ID(0x7778+class="num">1)           class=class="str">"cmt">// 历史集合列表 ID
class="macro">#define COLLECTION_MARKET_ID(0x7778+class="num">2)           class=class="str">"cmt">// 入场集合列表 ID
class="macro">#define COLLECTION_EVENTS_ID(0x7778+class="num">3)           class=class="str">"cmt">// 事件集合列表 ID
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ListObj.mqh |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2018, MetaQuotes 软件公司 |
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
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"

「给历史订单套一层可读的集合壳」

在 MT5 里直接翻 HistoryOrder / HistoryDeal 往往散落各处,封装一个继承自 CArrayObj 的 CListObj,能让你用统一接口管一类订单集合。 下面这段代码先 include 标准数组对象,再定义一个带 m_type 标记的集合基类。构造函数里把默认类型写成 0x7778,只是个魔数占位,方便运行时区分不同列表。

MQL5 / C++
class="macro">#include <Arrays\ArrayObj.mqh>
class CListObj : class="kw">public CArrayObj
  {
class="kw">private:
   class="type">int               m_type;          class=class="str">"cmt">// 列表类型
class="kw">public:
   class="type">void              Type(const class="type">int type)      { this.m_type=type;     }
   class="kw">virtual class="type">int       Type(class="type">void)                const { class="kw">return(this.m_type); }
                     CListObj()                { this.m_type=0x7778;   }
  };
上面 CListObj 的 Type() 写了两个版本:一个 setter 接收外部 int,一个 virtual getter 返回内部 m_type。继承后 CHistoryCollection 直接 public 继承它,就能在自身里挂 m_list_all_orders 并做排序。 真正落地时,CHistoryCollection 构造函数会把 m_list_all_orders 按 SORT_BY_ORDER_TIME_OPEN(MQL5)或 SORT_BY_ORDER_TIME_CLOSE(MQL4 兼容分支)排序后 Clear(),相当于每次新建集合先归零。外汇和贵金属历史单量大时,这种封装能减少重复遍历,但回测口径仍受经纪商成交回报影响,存在滑点偏差风险。 include 路径里 Select.mqh 与 ..\Objects\Orders\ 下的三个历史对象头文件,是这套集合跑起来的依赖。开 MT5 建个 ListObj.mqh 把基类贴进去,再引到自己的 EA 里,就能直接 new CHistoryCollection 验证排序行为。

MQL5 / C++
class="macro">#include <Arrays\ArrayObj.mqh>
class CListObj : class="kw">public CArrayObj
  {
class="kw">private:
   class="type">int               m_type;          class=class="str">"cmt">// 列表类型
class="kw">public:
   class="type">void              Type(const class="type">int type)      { this.m_type=type;     }
   class="kw">virtual class="type">int       Type(class="type">void)                const { class="kw">return(this.m_type); }
                     CListObj()                { this.m_type=0x7778;   }
  };
class="macro">#include "ListObj.mqh"
class="macro">#include "..\Services\Select.mqh"
class="macro">#include "..\Objects\Orders\HistoryOrder.mqh"
class="macro">#include "..\Objects\Orders\HistoryPending.mqh"
class="macro">#include "..\Objects\Orders\HistoryDeal.mqh"
class CHistoryCollection : class="kw">public CListObj
  {
CHistoryCollection::CHistoryCollection(class="type">void) : m_index_deal(class="num">0),m_delta_deal(class="num">0),m_index_order(class="num">0),m_delta_order(class="num">0),m_is_trade_event(false)
  {
   this.m_list_all_orders.Sort(class="macro">#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN class="macro">#else SORT_BY_ORDER_TIME_CLOSE class="macro">#endif );
   this.m_list_all_orders.Clear();

市场订单集合的初始化与排序枚举

在 MT5 的 EA 架构里,把市价单、挂单和持仓收进一个统一集合类,能显著降低遍历开销。下面这段 CMarketCollection 构造函数,在对象创建时就把内部链表清空、按开单时间预排序,并给上一次市场快照的账户哈希赋了 WRONG_VALUE 做哨兵值。 构造函数里这行 m_list_all_orders.Type(COLLECTION_MARKET_ID) 很关键:它给集合打上市场类标识,区别于历史类集合用的 COLLECTION_HISTORY_ID。两类集合共用 CListObj 基类,但 Type 不同让后续筛选不会串库。 排序标准用枚举 ENUM_SORT_ORDERS_MODE 暴露给调用方,目前支持按票据(0)、魔幻数(1)、开单时间(2)、平单时间(3)四种。宏 FIRST_ORD_DBL_PROP 定义为 ORDER_PROP_INTEGER_TOTAL,标记整数属性结束、双精度属性起始的偏移边界,字符串属性则再叠加双精度总数。 开 MT5 新建一个 include 了 ListObj.mqh 的 EA,把下面代码贴进集合类头文件,编译后能在调试里看到 m_list_all_orders 在 Init 完毕就已经是空且按时间排好序的状态。外汇与贵金属杠杆交易风险高,此类封装只解决代码结构,不预示任何收益。

MQL5 / C++
class="macro">#include "ListObj.mqh"
class="macro">#include "..\Services\Select.mqh"
class="macro">#include "..\Objects\Orders\MarketOrder.mqh"
class="macro">#include "..\Objects\Orders\MarketPending.mqh"
class="macro">#include "..\Objects\Orders\MarketPosition.mqh"
class CMarketCollection : class="kw">public CListObj
  {
CMarketCollection::CMarketCollection(class="type">void) : m_is_trade_event(false),m_is_change_volume(false),m_change_volume_value(class="num">0)
  {
   this.m_list_all_orders.Sort(SORT_BY_ORDER_TIME_OPEN);
   this.m_list_all_orders.Clear();
   ::ZeroMemory(this.m_struct_prev_market);
   this.m_struct_prev_market.hash_sum_acc=WRONG_VALUE;
   this.m_list_all_orders.Type(COLLECTION_MARKET_ID);
  }
class="macro">#define FIRST_ORD_DBL_PROP(ORDER_PROP_INTEGER_TOTAL)
class="macro">#define FIRST_ORD_STR_PROP(ORDER_PROP_INTEGER_TOTAL+ORDER_PROP_DOUBLE_TOTAL)
enum ENUM_SORT_ORDERS_MODE
  {
   SORT_BY_ORDER_TICKET        =  class="num">0,
   SORT_BY_ORDER_MAGIC         =  class="num">1,
   SORT_BY_ORDER_TIME_OPEN     =  class="num">2,
   SORT_BY_ORDER_TIME_CLOSE    =  class="num">3,
  };

◍ 订单排序枚举里的毫秒与双击属性分支

MT5 的订单历史检索接口用一组枚举值决定返回序列的排列依据,其中 4 到 22 覆盖了时间、状态、类型与盈亏标记等整型维度。比如 SORT_BY_ORDER_TIME_OPEN_MSC=4 按开单毫秒排,SORT_BY_ORDER_CLOSE_BY_TP=22 按止盈平仓标志排,中间跳过了 9 说明该槽位未公开启用。 实数型属性从 FIRST_ORD_DBL_PROP 起算,SORT_BY_ORDER_PRICE_OPEN 映射为 FIRST_ORD_DBL_PROP,SORT_BY_ORDER_SL 则为 FIRST_ORD_DBL_PROP+2,共计至少 3 个双精度字段参与排序。写 EA 时若想先抓最近毫秒级开单再比止损价,直接把这两个枚举喂给 HistorySelectByPosition 后的排序调用即可。 外汇与贵金属杠杆高,历史排序只解决数据顺序,不预示任何方向;跑回测时建议用真实 tick 验证毫秒字段是否如预期递进。

MQL5 / C++
SORT_BY_ORDER_TIME_OPEN_MSC   = class="num">4,                class=class="str">"cmt">// 按订单开单时间的毫秒值排序
SORT_BY_ORDER_TIME_CLOSE_MSC  = class="num">5,                class=class="str">"cmt">// 按订单平单时间的毫秒值排序
SORT_BY_ORDER_TIME_EXP        = class="num">6,                class=class="str">"cmt">// 按订单过期时间排序
SORT_BY_ORDER_STATUS          = class="num">7,                class=class="str">"cmt">// 按订单状态排序 (入场订单/挂单/成交/余额,信贷操作)
SORT_BY_ORDER_TYPE            = class="num">8,                class=class="str">"cmt">// 按订单类型排序
SORT_BY_ORDER_REASON          = class="num">10,               class=class="str">"cmt">// 按订单/仓位原因/来源排序
SORT_BY_ORDER_STATE           = class="num">11,               class=class="str">"cmt">// 按订单状态排序
SORT_BY_ORDER_POSITION_ID     = class="num">12,               class=class="str">"cmt">// 按仓位 ID 排序
SORT_BY_ORDER_POSITION_BY_ID  = class="num">13,               class=class="str">"cmt">// 按逆向仓位 ID 排序
SORT_BY_ORDER_DEAL_ORDER      = class="num">14,               class=class="str">"cmt">// 按成交所基于的订单排序
SORT_BY_ORDER_DEAL_ENTRY      = class="num">15,               class=class="str">"cmt">// 按成交方向排序 – IN, OUT 或 IN/OUT
SORT_BY_ORDER_TIME_UPDATE     = class="num">16,               class=class="str">"cmt">// 按仓位变更时间排序,以秒为单位
SORT_BY_ORDER_TIME_UPDATE_MSC = class="num">17,               class=class="str">"cmt">// 按仓位变更时间排序,以毫秒为单位
SORT_BY_ORDER_TICKET_FROM     = class="num">18,               class=class="str">"cmt">// 按父订单的票据排序
SORT_BY_ORDER_TICKET_TO       = class="num">19,               class=class="str">"cmt">// 按衍生订单的票据排序
SORT_BY_ORDER_PROFIT_PT       = class="num">20,               class=class="str">"cmt">// 按订单盈利点数排序
SORT_BY_ORDER_CLOSE_BY_SL     = class="num">21,               class=class="str">"cmt">// 按止损平单标志排序
SORT_BY_ORDER_CLOSE_BY_TP     = class="num">22,               class=class="str">"cmt">// 按止盈平单标志排序
class=class="str">"cmt">//--- 按实数型属性排序
SORT_BY_ORDER_PRICE_OPEN      = FIRST_ORD_DBL_PROP,    class=class="str">"cmt">// 按开单价排序
SORT_BY_ORDER_PRICE_CLOSE     = FIRST_ORD_DBL_PROP+class="num">1,  class=class="str">"cmt">// 按平单价排序
SORT_BY_ORDER_SL              = FIRST_ORD_DBL_PROP+class="num">2,  class=class="str">"cmt">// 按止损价排序

常见问题

用一个集合类把事件对象存进数组并暴露增删查接口,避免在主程序里散落各种事件变量,方便统一向程序推送。
打标签能在分层目录里区分不同集合来源,调试时一眼看出事件归属;不写容易在多个集合混用时传错对象。
小布可以读取你的品种页代码逻辑,提示订单集合初始化是否缺了毫秒或双击属性分支,省去手动核对。
会,封装后能用统一方法按时间或票号取历史订单,不用每次都翻底层数组,排查错单更快。
毫秒排序看下单先后,双击属性分支区分手动与自动单;枚举里分清楚才能避免把机器人单当人工单统计。