轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件(基础篇)

第 1/3 篇

「给交易事件做个统一收口」

写 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 后续再补。外汇和贵金属杠杆高,事件误判会直接引发错单,上线前用策略测试器跑真实账户镜像验证。

MQL5 / C++
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 内的多类事件,比堆布尔变量省内存也更好判重。

MQL5 / C++
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 监听时,建议先按标志位过滤再处理仓位,避免每笔账户利息都重算净值。

MQL5 / C++
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 差值,大概率能抓出平仓或半平事件。外汇和贵金属杠杆高,快照对比只反映账户状态,不预示行情方向。

MQL5 / C++
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">// 账户上的挂单和仓位列表

常见问题

把交易事件做统一收口,定义一套事件枚举和监听接口,基础对象直接订阅即可,避免重复代码。
在引擎构造阶段读取账户属性认单边或对冲,后续拆成交和持仓事件时按对应规则处理。
小布可自动识别账户动作并拆成可读事件推送到品种页,你直接看结论不用管底层枚举。
在底层先定义清楚成交标志位,再映射到交易事件枚举,覆盖开仓、平仓、删单等动作。
让基础对象持有事件处理器引用,账户动作触发时广播枚举事件,对象内做条件响应。