轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件(基础篇)
「给交易事件做个统一收口」
写 EA 最烦的就是 OnTradeTransaction 里那堆分支:订单成交、改挂单、删仓位、账户资金变,全都挤在一个回调里,稍不留神就漏处理。把这类事件从核心逻辑里剥出来,单独做成函数库的一层,后续维护会轻松很多。 MetaTrader 5 的 ENUM_TRADE_TRANSACTION_TYPE 一共定义了 15 种交易事务类型,从 TRADE_TRANSACTION_ORDER_ADD 到 TRADE_TRANSACTION_HISTORY_ADD 都在列。2019 年 5 月那篇示例里,作者用一组 switch-case 把这 15 类收进一个事件分发器,读者在 MT5 里按 F4 打开 MetaEditor 搜 ENUM_TRADE_TRANSACTION_TYPE 就能看到完整枚举。 实际落地时,建议先在库里写个 CTradeEvent 类,只负责识别事务类型并抛出对应虚函数,具体策略逻辑继承后重写。这样换品种或加风控规则时,不用动底层事件解析。外汇和贵金属杠杆高、滑点随机,事件回调顺序不保证严格时序,回测和实盘表现可能有偏差,需自行在策略层加保护。
◍ 让基础对象嗅到账户交易事件
上一篇文章里我们已经搭起跨 MT4/MT5 的通用函数库骨架,并把引擎层的基础对象和场内订单、持仓集合跑通。这一节要补的缺口很具体:让基础对象能识别账户上发生的交易事件,而不是只盯着静态持仓。 所谓交易事件,指开仓、平仓、改挂单、成交回执这类账户状态变动。基础对象若不能主动感知,后续的风控与信号触发就只能靠轮询,延迟和漏判都会上来。 本节的落地动作很明确:扩展基础对象,把事件接收与初步分发接进已有引擎。做完这一步,你开 MT5 加载函数库,在账户里手动下一单,基础对象应当能在日志里捕获到对应事件类型,而不再是哑巴。外汇与贵金属品种波动剧烈,事件处理链路任何一处阻塞都可能放大滑点风险,验证时建议先用模拟账户跑通。
把账户动作拆成可识别的交易事件
想让 EA 真正“看懂”账户里发生了什么,不能只笼统收到一个交易通知。要把动作按类型切开:挂单增删、激活成持仓、开平半平、逆向平、出入金、余额操作,以及尚未跟踪的修改挂单/持仓止损止盈。 对冲与净持账户的判定逻辑不同。对冲下,挂单数量减少且仓位数量增加 = 挂单激活;挂单减少但仓位没增加 = 删单;仓位不变但持仓量持续减少 = 部分平仓。净持下,仓位数量不变但伴随持仓时间修改和持仓量变化,要区分是加仓还是部分平仓。这两套规则必须按账户类型分开写,否则事件会误判。 事件代码用一组标志位封包,存在 CEngine 私有变量里,直到下一个事件到来前都不变。比如“平仓”可细分为全平、部分平、逆向平、止损平、止盈平,每个子集都是同一事件下的标志。定义时建议直接对齐 MQL5 的 ENUM_DEAL_TYPE 值,除买入卖出外,信用、红利等类型沿用原枚举值,再把余额操作拆成“入金”和“出金”两个事件。 MQL5 有个坑:逆向平仓会生成 ORDER_TYPE_CLOSE_BY 特殊订单,止损平仓则用平仓单替代原在场单。接收整数属性的方法里必须补上这个状态,否则库的基础对象认不出一笔仓位被逆向关掉。历史集合在 MQL5 中默认按放置时间排序,MQL4 按平仓时间排序,构造时要分开处理。 调用层用法很直接:GetList() 拿完整在场列表,再用 CSelect 按“position”状态筛出持仓;若需最后一笔,按开单时间(毫秒)排序取末位。返回可能为空,调用程序务必判 NULL。当前 CheckTradeEvent() 仅实现了 MQL5 对冲账户,净持与 MQL4 后续再补。外汇和贵金属杠杆高,事件误判会直接引发错单,上线前用策略测试器跑真实账户镜像验证。
class CEngine : class="kw">public CObject { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// 历史订单和成交集合 CMarketCollection m_market; class=class="str">"cmt">// 在场订单和成交集合 CArrayObj m_list_counters; class=class="str">"cmt">// 计时器列表计数器 class="type">bool m_first_start; class=class="str">"cmt">// 首次启动标志
「引擎构造里先认账户是单边还是对冲」
CEngine 构造函数一上来就干了三件实在事:用 EventSetMillisecondTimer 按 TIMER_FREQUENCY 挂毫秒级定时器,给计数器列表做 Sort,再 CreateCounter 建一个 ID 为 COLLECTION_COUNTER_ID 的采集计数器。这三步决定了后续行情与交易事件的轮询节奏。 关键在最后一行:m_is_hedge 通过 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 来判定。返回 true 即当前是零售对冲账户,同一品种可多向持仓并存;false 则多为净额结算账户,加仓会合并仓位。外汇与贵金属杠杆高,账户模式不同会直接改变平仓逻辑与保证金占用,开 MT5 在「账户历史」右侧看保证金模式最直观。 下方 ENUM_TRADE_EVENT_FLAGS 用 2 的幂做位标志:挂单放置=1、删除=2、激活=4、开仓=8、平仓=16、余额操作=32、部分执行=64。用位或就能一次标记同 tick 内的多类事件,比堆布尔变量省内存也更好判重。
class="type">bool m_is_hedge; class=class="str">"cmt">// 对冲账户标志 class=class="str">"cmt">//--- 按 id 返回计数器索引 class="type">int CounterIndex(const class="type">int id) const; class=class="str">"cmt">//--- 返回首次启动标志 class="type">bool IsFirstStart(class="type">void); class="kw">public: class=class="str">"cmt">//--- 创建计时器计数器 class="type">void CreateCounter(const class="type">int id,const class="type">ulong frequency,const class="type">ulong pause); class=class="str">"cmt">//--- 计时器 class="type">void OnTimer(class="type">void); CEngine(); ~CEngine(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEngine::CEngine() : m_first_start(true) { ::EventSetMillisecondTimer(TIMER_FREQUENCY); this.m_list_counters.Sort(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE); this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 帐户上的交易事件标志列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_TRADE_EVENT_FLAGS { TRADE_EVENT_FLAG_NO_EVENT = class="num">0, class=class="str">"cmt">// 无事件 TRADE_EVENT_FLAG_ORDER_PLASED = class="num">1, class=class="str">"cmt">// 挂单已放置 TRADE_EVENT_FLAG_ORDER_REMOVED = class="num">2, class=class="str">"cmt">// 挂单已删除 TRADE_EVENT_FLAG_ORDER_ACTIVATED = class="num">4, class=class="str">"cmt">// 挂单已激活 TRADE_EVENT_FLAG_POSITION_OPENED = class="num">8, class=class="str">"cmt">// 已开仓 TRADE_EVENT_FLAG_POSITION_CLOSED = class="num">16, class=class="str">"cmt">// 已平仓 TRADE_EVENT_FLAG_ACCOUNT_BALANCE = class="num">32, class=class="str">"cmt">// 余额操作 (按交易类型声明) TRADE_EVENT_FLAG_PARTIAL = class="num">64, class=class="str">"cmt">// 部分执行
◍ 成交标志位与交易事件枚举的底层定义
在 MT5 的底层交易事件体系里,成交来源靠一组 2 的幂次标志位区分。TRADE_EVENT_FLAG_BY_POS 取值 128,代表由逆向仓位执行;TRADE_EVENT_FLAG_SL 为 256,对应止损触发;TRADE_EVENT_FLAG_TP 为 512,对应止盈触发。这三个值互不重叠,按位或运算即可同时标记多个来源。 账户层面的事件则由 ENUM_TRADE_EVENT 枚举统一管理,从 TRADE_EVENT_NO_EVENT(无事件)到挂单放置、删除,再到与 ENUM_DEAL_TYPE 对齐的账户类动作:信贷充值、佣金(含日结/月结及代理变体)、利息、取消的买卖成交、除权与税款等。写 EA 时直接引用这些枚举成员,比手写字符串判断更不容易漏掉经纪商特有的事件类型。 外汇与贵金属品种上,这类事件回调的高频触发和滑点、点差扩大叠加,属于典型高风险场景;用 OnTradeTransaction 监听时,建议先按标志位过滤再处理仓位,避免每笔账户利息都重算净值。
TRADE_EVENT_FLAG_BY_POS = class="num">128, class=class="str">"cmt">// 由逆向仓位执行 TRADE_EVENT_FLAG_SL = class="num">256, class=class="str">"cmt">// 由止损执行 TRADE_EVENT_FLAG_TP = class="num">512 class=class="str">"cmt">// 由止盈执行 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 帐户上的仓位交易事件列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_TRADE_EVENT { TRADE_EVENT_NO_EVENT, class=class="str">"cmt">// 无交易事件 TRADE_EVENT_PENDING_ORDER_PLASED, class=class="str">"cmt">// 挂单已放置 TRADE_EVENT_PENDING_ORDER_REMOVED, class=class="str">"cmt">// 挂单已删除 class=class="str">"cmt">//--- 与 ENUM_DEAL_TYPE 枚举匹配的枚举成员 TRADE_EVENT_ACCOUNT_CREDIT, class=class="str">"cmt">// 信贷充值 TRADE_EVENT_ACCOUNT_CHARGE, class=class="str">"cmt">// 充值 TRADE_EVENT_ACCOUNT_CORRECTION, class=class="str">"cmt">// 调整 TRADE_EVENT_ACCOUNT_BONUS, class=class="str">"cmt">// 奖金 TRADE_EVENT_ACCOUNT_COMISSION, class=class="str">"cmt">// 佣金 TRADE_EVENT_ACCOUNT_COMISSION_DAILY, class=class="str">"cmt">// 日结佣金 TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY, class=class="str">"cmt">// 月结佣金 TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY, class=class="str">"cmt">// 日结代理佣金 TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY, class=class="str">"cmt">// 月结代理佣金 TRADE_EVENT_ACCOUNT_INTEREST, class=class="str">"cmt">// 可用资金的应计利息 TRADE_EVENT_BUY_CANCELLED, class=class="str">"cmt">// 取消的买入成交 TRADE_EVENT_SELL_CANCELLED, class=class="str">"cmt">// 取消的卖出成交 TRADE_EVENT_DIVIDENT, class=class="str">"cmt">// 应计除权 TRADE_EVENT_DIVIDENT_FRANKED, class=class="str">"cmt">// 应计派发除权 TRADE_EVENT_TAX, class=class="str">"cmt">// 应计税款
把账户动作拆成可监听的事件枚举
在 MT5 的自定义交易事件体系里,账户余额变动和挂单/持仓的生命周期被拆成了一组枚举成员。充值和出金分别对应 TRADE_EVENT_ACCOUNT_BALANCE_REFILL 与 TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL,这两类与 DEAL_TYPE_BALANCE 成交类型挂钩,是资金流水监控的入口。 挂单激活分整单与部分两种:TRADE_EVENT_PENDING_ORDER_ACTIVATED 和 TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL。持仓侧更细,开仓、平仓、被反向仓平、被 SL/TP 平,全都带完整与部分两个分支,比如 TRADE_EVENT_POSITION_CLOSED_BY_TP 与 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP。净持仓模式下还有逆转 TRADE_EVENT_POSITION_REVERSED 和加仓 TRADE_EVENT_POSITION_VOLUME_ADD。 CMarketCollection 类用两个 MqlDataCollection 结构体分别存当前和前一次的快照:hash_sum_acc 是账户所有订单与仓位的哈希,total_pending 和 total_positions 是挂单与持仓计数,total_volumes 是总交易量。把前后两次快照做差,就能定位是哪一种 TRADE_EVENT 被触发。 开 MT5 新建一个 EA,把上面这组枚举抄进头文件,再对比 m_struct_curr_market 与 m_struct_prev_market 的 total_positions 差值,大概率能抓出平仓或半平事件。外汇和贵金属杠杆高,快照对比只反映账户状态,不预示行情方向。
class=class="str">"cmt">//--- 与 ENUM_DEAL_TYPE 枚举中的 DEAL_TYPE_BALANCE 交易类型相关的枚举成员 TRADE_EVENT_ACCOUNT_BALANCE_REFILL, class=class="str">"cmt">// 补充账户余额 TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL, class=class="str">"cmt">// 从账户中出金 class=class="str">"cmt">//--- TRADE_EVENT_PENDING_ORDER_ACTIVATED, class=class="str">"cmt">// 挂单由价格激活 TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL, class=class="str">"cmt">// 挂单由价格部分激活 TRADE_EVENT_POSITION_OPENED, class=class="str">"cmt">// 已开仓 TRADE_EVENT_POSITION_OPENED_PARTIAL, class=class="str">"cmt">// 部分开仓 TRADE_EVENT_POSITION_CLOSED, class=class="str">"cmt">// 已平仓 TRADE_EVENT_POSITION_CLOSED_PARTIAL, class=class="str">"cmt">// 部分平仓 TRADE_EVENT_POSITION_CLOSED_BY_POS, class=class="str">"cmt">// 由逆向平仓 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS, class=class="str">"cmt">// 由逆向部分平仓 TRADE_EVENT_POSITION_CLOSED_BY_SL, class=class="str">"cmt">// 由止损平仓 TRADE_EVENT_POSITION_CLOSED_BY_TP, class=class="str">"cmt">// 由止盈平仓 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL, class=class="str">"cmt">// 由止损部分平仓 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP, class=class="str">"cmt">// 由止盈部分平仓 TRADE_EVENT_POSITION_REVERSED, class=class="str">"cmt">// 持仓逆转 (净持) TRADE_EVENT_POSITION_VOLUME_ADD class=class="str">"cmt">// 加仓 (净持) }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在场订单和仓位集合 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketCollection { class="kw">private: class="kw">struct MqlDataCollection { class="type">long hash_sum_acc; class=class="str">"cmt">// 账户上所有订单和仓位的哈希值 class="type">int total_pending; class=class="str">"cmt">// 账户上的挂单数量 class="type">int total_positions; class=class="str">"cmt">// 账户上的仓位数量 class="type">class="kw">double total_volumes; class=class="str">"cmt">// 帐户上订单和仓位的总交易量 }; MqlDataCollection m_struct_curr_market; class=class="str">"cmt">// 账户内在场订单和仓位的当前数据 MqlDataCollection m_struct_prev_market; class=class="str">"cmt">// 账户内在场订单和仓位的之前数据 CArrayObj m_list_all_orders; class=class="str">"cmt">// 账户上的挂单和仓位列表