TradeTransaction 事件处理:从基础机制读懂 MQL5 交易事件流(基础篇)
(1/3)· 多数 EA 作者只知 OnTrade 却忽略 OnTradeTransaction,本文补齐事件驱动编程的认知缺口
◍ 在 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 就能编译验证:
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——说明这一帧只确认「成交已落历史」,具体成交价要去别处取。 外汇和贵金属杠杆高,这类底层事件跟踪只解决「信号怎么来」,不解决「该不该做」,验证时请用模拟盘。
IO <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) ---===Transaction===--- NK <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Ticket of the deal: <span class="number">class="num">0</span> RR <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Type of the deal: <span class="macro">DEAL_TYPE_BUY</span> DE <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Ticket of the order: <span class="number">class="num">0</span> JS <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Status of the order: <span class="macro">ORDER_STATE_STARTED</span> JN <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Type of the order: <span class="macro">ORDER_TYPE_BUY</span> FD <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Price: <span class="number">class="num">0.0000</span> FN <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Level of Stop Loss: <span class="number">class="num">0.0000</span> HF <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Level of Take Profit: <span class="number">class="num">0.0000</span> FQ <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Price that triggers the Stop Limit order: <span class="number">class="num">0.0000</span> RR <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) Trade symbol: HD <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) 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 <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">37</span>:<span class="number">class="num">53.233</span> TradeProcessor(EURUSD,H1) 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 自查自己的成交回放能验证这套状态机。