轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件(基础篇)
「把交易事件塞进一个集合类」
在 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。这在事件去重和状态变更检测里直接用得上。
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 冲突会导致持仓统计错乱,上真实账户前务必在策略测试器用历史数据跑一遍。
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,只是个魔数占位,方便运行时区分不同列表。
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; } };
ListObj.mqh 把基类贴进去,再引到自己的 EA 里,就能直接 new CHistoryCollection 验证排序行为。
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 完毕就已经是空且按时间排好序的状态。外汇与贵金属杠杆交易风险高,此类封装只解决代码结构,不预示任何收益。
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 验证毫秒字段是否如预期递进。
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">// 按止损价排序