TradeTransaction 事件处理:从基础机制读懂 MQL5 交易事件流(基础篇)
⚙️

TradeTransaction 事件处理:从基础机制读懂 MQL5 交易事件流(基础篇)

(1/3)· 多数 EA 作者只知 OnTrade 却忽略 OnTradeTransaction,本文补齐事件驱动编程的认知缺口

含代码示例 第 1/3 篇
写 EA 时你或许习惯用 OnTrade 捕捉账户变化,却没留意市价开仓实际会触发五次底层事务回调。直接监听订单状态而不分清事务阶段,容易漏掉成交与历史转移的关键节点。先把事件机制理顺,后续诊断与告警才不会建立在错误假设上。

◍ 在 MT5 里捕获每一笔成交回执

MT5 的 OnTradeTransaction 是少数能让你在订单状态真正变更时拿到第一手数据的钩子,而不是靠轮询账户历史去猜。它把成交、挂单改动、持仓变动都统一成 TradeTransaction 事件推给 EA,延迟远低于人工刷新。 事件结构体里 Type 字段决定了你该干什么:TRADE_TRANSACTION_DEAL_ADD 代表真实成交落地,TRADE_TRANSACTION_ORDER_ADD 是挂单进场,TRADE_TRANSACTION_POSITION 系列则对应持仓微调。只监听你关心的类型,能省掉大量无效分支。 外汇与贵金属属高杠杆品种,这类事件回调里的价格与成交量只反映当时盘口,不代表后续方向,任何基于回执的自动跟随都存在滑点与流动性断裂风险。

「MQL5 里被忽略的事件驱动本质」

MQL5 当前版本客户端内置了 14 个正式事件处理器,程序员还能用 EventChartCustom() 抛自定义事件、交给 OnChartEvent() 消化。但官方文档从头到尾不写“事件驱动编程(EDP)”这个词,可实际上你建的每一个 EA 模板,第一步“EA 事件处理器”就是在选事件入口,底层全是 EDP 原则。 本文不重复别人写过的 OnTrade(),改用 OnTradeTransaction() 来控制交易事件。对客户端原生事件,开发者只能写后半段——事件处理器;定时器与自定义事件例外,从触发到处理都捏在你手里。 做外汇、贵金属自动化前先认清这点:MQL5 程序块本质就两部分,事件筛选加事件处理。高频处理交易事务时,选错处理器可能导致事件漏捕,这类策略回测与实盘偏差风险偏高。

一次市价开仓为何触发五次回调

在 MT5 里,一次看似简单的市价开仓,终端与服务器之间其实走了制定请求、验证、发送、收响应、落历史这一串流程。从 EA 的视角看,市场动作会映射成交易事务事件,市价开仓这一种操作就会调用 OnTradeTransaction() 处理器整整 5 次。 这 5 次分别对应:程序收到服务器返回的请求结果、订单拿到唯一单号进入开仓列表、订单执行后从列表移除、订单转入账户历史、历史中写入成交记录。也就是说,你写 EA 时不能假设‘开仓=1 次事件’,否则计数或风控逻辑会漏掉中间状态。 MQL5 用 ENUM_TRADE_TRANSACTION_TYPE 这个枚举区分事务类型。想在当前回调里判断是哪类事务,要去读 MqlTradeTransaction 结构体的第四个字段 type,而不是凭次数硬猜。 下面这段是处理器的标准头与结构体定义,直接贴进 MT5 就能编译验证:

MQL5 / C++
class="type">void  OnTradeTransaction(
  class="kw">const MqlTradeTransaction&    trans,      class=class="str">"cmt">// structure of the trade transaction
  class="kw">const class="type">MqlTradeRequest&       request,    class=class="str">"cmt">// structure of the request
  class="kw">const class="type">MqlTradeResult&        result      class=class="str">"cmt">// structure of the response
  );
class="kw">struct MqlTradeTransaction
  {
   class="type">class="kw">ulong                       deal;       class=class="str">"cmt">// Ticket of the deal
   class="type">class="kw">ulong                       order;      class=class="str">"cmt">// Ticket of the order
   class="type">class="kw">string                      symbol;     class=class="str">"cmt">// Trade symbol
   ENUM_TRADE_TRANSACTION_TYPE type;       class=class="str">"cmt">// Type of the trade transaction
   ENUM_ORDER_TYPE             order_type; class=class="str">"cmt">// Type of the order
   ENUM_ORDER_STATE            order_state;class=class="str">"cmt">// Status of the order
   ENUM_DEAL_TYPE              deal_type;  class=class="str">"cmt">// Type of the deal
   ENUM_ORDER_TYPE_TIME        time_type;  class=class="str">"cmt">// Order expiration type
   class="type">class="kw">datetime                    time_expiration; class=class="str">"cmt">// Order expiration time
   class="type">class="kw">double                      price;      class=class="str">"cmt">// Price 
   class="type">class="kw">double                      price_trigger; class=class="str">"cmt">// Price that triggers the Stop Limit order
   class="type">class="kw">double                      price_sl;   class=class="str">"cmt">// Level of Stop Loss
   class="type">class="kw">double                      price_tp;   class=class="str">"cmt">// Level of Take Profit
   class="type">class="kw">double                      volume;     class=class="str">"cmt">// Volume in lots
  };

◍ 开仓动作在 OnTradeTransaction 里的五次回调链路

在 MT5 里手动开一单 EURUSD,EA 的 OnTradeTransaction 会被连续触发五次,而不是一次。这五次的类型字段依次是 TRADE_TRANSACTION_REQUEST、TRADE_TRANSACTION_ORDER_ADD、TRADE_TRANSACTION_ORDER_DELETE、TRADE_TRANSACTION_HISTORY_ADD、TRADE_TRANSACTION_DEAL_ADD,分别对应「请求发出 → 订单进活跃列表 → 订单移出活跃列表 → 订单进历史 → 成交单进历史」。 只有「修改仓位」(比如改 SL/TP)是例外,它只触发两次回调,且不会在成交和订单历史里留下 TRADE_TRANSACTION_HISTORY_ADD 或 DEAL_ADD 痕迹,靠历史流是跟踪不到的。 想自己验证,就在终端用调试模式挂一个带事件日志的 EA(模板名随意,比如 TradeProcessor.mq5),手工开仓后看日志。第一次回调里 Request 块是发单详情,Response 块是执行结果;后面几次真正有用的只有 Transaction 块。下面这段日志是第五次 DEAL_ADD 时的 Transaction 输出示例,成交单号、买卖方向、订单状态都在里面,但价格字段此时还是 0.0000——说明这一帧只确认「成交已落历史」,具体成交价要去别处取。 外汇和贵金属杠杆高,这类底层事件跟踪只解决「信号怎么来」,不解决「该不该做」,验证时请用模拟盘。

MQL5 / C++
IO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;---===Transaction===---
NK&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Ticket of the deal: <span class="number">class="num">0</span>
RR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Type of the deal: <span class="macro">DEAL_TYPE_BUY</span>
DE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Ticket of the order: <span class="number">class="num">0</span>
JS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Status of the order: <span class="macro">ORDER_STATE_STARTED</span>
JN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Type of the order: <span class="macro">ORDER_TYPE_BUY</span>
FD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Price: <span class="number">class="num">0.0000</span>
FN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level of Stop Loss: <span class="number">class="num">0.0000</span>
HF&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level of Take Profit: <span class="number">class="num">0.0000</span>
FQ&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Price that triggers the Stop Limit order: <span class="number">class="num">0.0000</span>
RR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Trade symbol:
HD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Pending order expiration time: <span class="number">class="num">1970.01</span>.<span class="number">class="num">01</span> <span class="number">class="num">00</span>:<span class="number">class="num">00</span>
GS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span>&nbsp;&nbsp;&nbsp;&nbsp;TradeProcessor(EURUSD,H1)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order expiration type: <span class="macro">ORDER_TIME_GTC</span>

「从日志拆开一笔市价买单的请求结构」

在 MT5 的 EA 调试里,一笔交易从发单到成交会先打出 Request 日志。上面这段是 EURUSD H1 周期下 TradeProcessor 在 17:37:53.233 输出的典型买单请求快照,所有字段同一毫秒生成,说明是同步构造后一次性提交。 交易事务类型显示为 TRADE_TRANSACTION_REQUEST,具体操作为 TRADE_ACTION_DEAL,即不挂单、直接以市价成交。订单方向是 ORDER_TYPE_BUY,目标品种 EURUSD,申报价格 1.3137,订单票号 22535869。 注意几个零值:Volume in lots 为 0.00,SL/TP/StopLimit 均为 0.0000,Magic number 也是 0,Deviation 为 0,过期时间落在 1970.01.01 00:00。这往往意味着 EA 在打日志时手数尚未赋值,或该笔为纯结构探查请求,并未真正带仓位参数去执行。 外汇与贵金属杠杆高,这类零手数请求若误传进真实账户可能直接被服务器拒单。开 MT5 按 F5 跑一遍自己的 EA,对照 Experts 标签里的同格式日志,先确认 Volume 和 SL/TP 在 Request 阶段是否已被正确填入,再放实盘。

从日志看 EURUSD 买单的成交与挂单状态

下面这段 MT5 专家日志来自 EURUSD H1 图表上的 TradeProcessor 脚本,记录了一次实际成交与后续挂单状态。时间锚点在 17:37:53.233,订单号 22535869 以成交价 1.3137 买入 0.11 手,成交单号 15258202,返回码 10009 表示交易请求已被服务器接受并执行。 同一订单在 17:41:12.280 再次出现,类型标记为 DEAL_TYPE_BUY,订单状态为 ORDER_STATE_STARTED,说明这张买单已进入启动态但尚未完全闭环。此时 Price 仍挂在 1.3137,Stop Loss 显示为 0.0000,即未设止损。 日志里 Bid 1.3135、Ask 1.3137 的瞬时价差仅 2 点,结合 0.11 手的微型仓位,可推断脚本在窄价差时段优先撮合。外汇与贵金属属高杠杆品种,未设 SL 的启动单在跳空时可能直接放大回撤,开 MT5 按订单号 22535869 自查自己的成交回放能验证这套状态机。

把事件流交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到账户事务的实时分层,你专注策略逻辑而非埋点调试。

常见问题

OnTrade 在交易账户状态变动后触发一次汇总通知,而 OnTradeTransaction 在每一个交易事务阶段(如请求发送、订单开仓、成交转移历史)分别回调,市价开仓可能连续触发五次。
因为开仓涉及请求完成响应、订单列表写入、订单删除、转入历史、成交数据记录等数个事务阶段,每个阶段都作为独立事务抛出一次事件。
它是描述交易事务类型的枚举,处理器通过结构参数里的该字段判断当前处于下单、成交、删单还是历史转移等具体阶段,从而分流逻辑。
可以,小布盯盘的品种页已内置账户事务的实时分层视图,不需要你自己写 EA 去埋 OnTradeTransaction 就能看到各阶段触发情况。
尽管文档未明说 EDP 术语,但所有 MQL5 程序都由事件选择加处理两块构成,客户端事件程序员控制处理器部分,定时器和自定义事件则完全自主,实质基于事件驱动原则。