MetaTrader 5 中的交易事件·进阶篇
「挂单为何终将离开活跃队列」
所有等待执行的订单,不管最终有没有触发、有没有被撤,最终都会被推到历史记录里。从成交到被拒,从手动撤单到 MQL5 程序自动撤单,再到过期、保证金不足、部分平仓策略干预,撤消路径至少有六种。 订单状态变动不会群发,只回传给登录对应账户的客户端。这一点决定了多终端同步时,你看到的历史不一定和队友同秒一致。 关键误区:交易服务器收下请求不等于会执行。OrderSend() 返回 true 只代表请求过了基础校验,真正的成交结果要看 MqlTradeResult 结构。外汇和贵金属杠杆高,这种“假成功”可能让你误判敞口,开 MT5 跑一段日志比对就能验证。
◍ 订单单证到了不等于订单已落地
用 OrderSend() 发买入请求,只要校验通过就会立刻拿到订单单证(ticket),但此时订单未必已经出现在客户端活动列表里。如果马上用 OrderSelect() 去选,很可能返回失败——因为 Trade 事件消息还在路上。 MQL5 程序收到单证和收到「新订单出现」的 Trade 消息是两条独立通道,前者来自服务器对请求的应答,后者来自交易事件推送。网络传输延迟使得这两者之间存在时间差,活动订单列表的更新消息也可能滞后。 更极端的情况是:当 Trade 消息终于到达时,该订单对应的成交已经执行完毕,订单直接从活动列表跳进了历史记录。这种「跳过活动态」的现象真实存在,根源在于券商端处理请求的速度远快于消息经网络回传的速度,外汇与贵金属品种在高波动时该延迟会被放大,属高风险场景,验证时请用模拟账户抓包观察。
用计数器比对抓交易账户变动
MT5 交易服务器推送的成交与订单消息是异步且不对称的,想确切知道账户发生了什么,唯一稳妥的办法是记住上一次的订单、持仓、成交状态,再和当前状态做差分。本文给出的轻量算法思路是:全局声明几类计数器,限定历史缓存深度,初始化后在新事件里比数量。 历史深度不是越大越好。示例里用 input int days=7 只载最近 7 天,载得越多客户端和本机资源吃得越狠;在 OnInit 里按 days*PeriodSeconds(PERIOD_D1) 算出起止时间再 HistorySelect,成功才把 started 置 true。 检查入口放在 OnTick 和 OnTrade 都可以:started 为 true 就跑 SimpleTradeProcessor 比 orders/positions/deals/history_orders 有没有变,变了再深挖;没变但订单属性改了的情形本文不覆盖。 别把正态当圣经:这套只侦测「数量增减」,若你要在订单改挂止损但单数不变时触发逻辑,得换更细的事件比对,不能直接抄上面计数器法。
class="type">int orders; class=class="str">"cmt">// 活动订单数量 class="type">int positions; class=class="str">"cmt">// 开启仓位数量 class="type">int deals; class=class="str">"cmt">// 交易历史缓存中的交易数量 class="type">int history_orders; class=class="str">"cmt">// 交易历史缓存中的订单数量 class="type">bool started=false; class=class="str">"cmt">// 计数器初始化的标志 input class="type">int days=class="num">7; class=class="str">"cmt">// 交易历史深度,以天数为单位 class=class="str">"cmt">//--- 设置全局范围内交易历史的限制 class="type">class="kw">datetime start; class=class="str">"cmt">// 缓存中交易历史的起始日期 class="type">class="kw">datetime end; class=class="str">"cmt">// 缓存中交易历史的结束日期 class="type">int OnInit() { class=class="str">"cmt">//--- end=TimeCurrent(); start=end-days*PeriodSeconds(PERIOD_D1); PrintFormat("将要载入的交易历史限制为: 起始日期 - %s, 结束日期 - %s", TimeToString(start),TimeToString(end)); InitCounters(); class=class="str">"cmt">//--- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化仓位,订单和交易的计数器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InitCounters() { ResetLastError(); class=class="str">"cmt">//--- 载入历史 class="type">bool selected=HistorySelect(start,end); if(!selected) { PrintFormat("%s. 从缓存中载入历史失败,时间从 %s 到 %s. 错误代码: %d", __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 读取当前数值 orders=OrdersTotal(); positions=PositionsTotal(); deals=HistoryDealsTotal(); history_orders=HistoryOrdersTotal(); started=true; Print("订单,仓位和交易计数器初始化成功"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() {
「用 OnTrade 钩子盯住账户状态变动」
EA 里想实时感知下单、平仓、历史刷新,最干净的做法是挂 OnTrade 事件。启动标志 started 为真就跑 SimpleTradeProcessor(),否则先 InitCounters() 把基线数读出来,避免一上来就把初始状态误判成“变动”。 SimpleTradeProcessor() 先用 TimeCurrent() 取结束时间,再 HistorySelect(start,end) 拉指定区间的历史。若返回 false,PrintFormat 会把错误码甩进日志并直接 return——这一步漏掉的话,后面读到的 OrdersTotal / HistoryDealsTotal 可能是脏数据。 四个计数器是核心:curr_orders、curr_positions、curr_deals、curr_history_orders 分别和上次存的 orders、positions、deals、history_orders 比。只要不等就打印“之前数量 X,目前数量 Y”,并把旧值覆盖成新值。比如仓位从 0 变 1,日志会明确写出“仓位数量改变了. 之前数量为 0, 目前数量为 1”,你据此挂告警或触发二次逻辑都行。 末尾的 CheckStartDateInTradeHistory() 负责按需往前推历史请求起点。外汇和贵金属杠杆高,这类账户变动监听最好只在必要时扩大 HistorySelect 区间,否则代理端历史缓存被频繁拉满,MT5 终端可能卡顿甚至断连。
if(started) SimpleTradeProcessor(); else InitCounters(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 当 Trade 事件发生时调用 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTrade() { if(started) SimpleTradeProcessor(); else InitCounters(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理交易历史改变的简单实例 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SimpleTradeProcessor() { end=TimeCurrent(); ResetLastError(); class=class="str">"cmt">//--- 载入历史 class="type">bool selected=HistorySelect(start,end); if(!selected) { PrintFormat("%s. 从缓存中载入历史失败,时间从 %s 到 %s. 错误代码: %d", __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 读取当前数值 class="type">int curr_orders=OrdersTotal(); class="type">int curr_positions=PositionsTotal(); class="type">int curr_deals=HistoryDealsTotal(); class="type">int curr_history_orders=HistoryOrdersTotal(); class=class="str">"cmt">//--- 检查活动订单数是否改变 if(curr_orders!=orders) { class=class="str">"cmt">//--- 活动订单数改变了 PrintFormat("订单数改变了. 之前数量为 %d, 目前数量为 %d", orders,curr_orders); class=class="str">"cmt">/* 其他与订单改变关联的行为 */ class=class="str">"cmt">//--- 更新数值 orders=curr_orders; } class=class="str">"cmt">//--- 开启仓位数量改变 if(curr_positions!=positions) { class=class="str">"cmt">//--- number of open positions has been changed PrintFormat("仓位数量改变了. 之前数量为 %d, 目前数量为 %d", positions,curr_positions); class=class="str">"cmt">/* 其他与仓位改变关联的行为 */ class=class="str">"cmt">//--- 更新数值 positions=curr_positions; } class=class="str">"cmt">//--- 交易历史缓存中交易数量的改变 if(curr_deals!=deals) { class=class="str">"cmt">//--- 交易历史缓存中交易数量已经改变 PrintFormat("交易数量改变了. 之前数量为 %d, 目前数量为 %d", deals,curr_deals); class=class="str">"cmt">/* 其他与交易数量改变关联的行为 */ class=class="str">"cmt">//--- 更新数值 deals=curr_deals; } class=class="str">"cmt">//--- 交易历史缓存中订单数量的改变 if(curr_history_orders!=history_orders) { class=class="str">"cmt">//--- 交易历史缓存中订单数量已经改变 PrintFormat("历史中订单数量已经改变. 之前数量为 %d, 目前数量为 %d", history_orders,curr_history_orders); class=class="str">"cmt">/* 其他与交易历史缓存中订单数量改变关联的行为 */ class=class="str">"cmt">//--- 更新数值 history_orders=curr_history_orders; } class=class="str">"cmt">//--- 检查是否有必要改变在缓存中请求交易历史的限制 CheckStartDateInTradeHistory(); }
◍ 动态矫正交易历史载入起点
EA 在 MT5 里跑实时统计时,常遇到一个问题:终端预载的交易历史起始日比策略需要的窗口更老或更新,导致后续比对新成交订单时基数错位。下面这段函数就是用来在运行时把历史起点拉回合理位置的。 核心逻辑是先按当前时间减去自定义天数(days*日线秒数)算出期望起点 curr_start,再判断已有 start 与它的差距是否超过一整天。若超过,说明缓存区间偏离计划,就把 start 重写为 curr_start,并重载 HistorySelect(start,end)。 重载后必须同步刷新两个计数:history_orders=HistoryOrdersTotal() 与 deals=HistoryDealsTotal(),否则下一轮差异比对会拿旧总量当基准,漏掉刚补进来的历史记录。外汇与贵金属品种跳空频繁,这种起点漂移在高波动周初更可能出现,实操中建议把 days 设为略大于实盘观察窗的整数(如 30 或 90)再开 MT5 验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改请求交易历史的起始日期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckStartDateInTradeHistory() { class=class="str">"cmt">//--- 初始时间段,就像我们马上开始工作 class="type">class="kw">datetime curr_start=TimeCurrent()-days*PeriodSeconds(PERIOD_D1); class=class="str">"cmt">//--- 确认交易历史起始限制的时间离 class=class="str">"cmt">//--- 计划时间多于一天 if(curr_start-start>PeriodSeconds(PERIOD_D1)) { class=class="str">"cmt">//--- 我们需要改正载入历史缓存的起始日期 start=curr_start; PrintFormat("将要载入交易历史的新的起始限制为: 起始日期 => %s", TimeToString(start)); class=class="str">"cmt">//--- 现在再次根据改正过的时间间隔载入交易历史 HistorySelect(start,end); class=class="str">"cmt">//--- 改正历史中交易和订单的数量,以便用于将来的比较 history_orders=HistoryOrdersTotal(); deals=HistoryDealsTotal(); } }
一点提醒
MT5 里的交易操作全是异步的,账户任何变动都是独立推送,不能指望“发一个请求就对应一个交易事件”去追踪。想在 OnTrade 里精确知道变了什么,只能每次把成交、持仓、订单的当前状态和上一次存的状态做 diff。 上面那段代码就是个典型坑:加载历史时若起始限位偏移超过 1 天(PeriodSeconds(PERIOD_D1) 约 86400 秒),就重设 start 并 HistorySelect 重载;但末尾 deals 被错赋成 HistoryOrdersTotal(),正确该用 HistoryDealsTotal(),否则成交计数永远和订单数一样。 外汇和贵金属杠杆高、滑点随机,这种状态不同步若没处理好,EA 可能重复开仓或漏平。开 MT5 把这段抄进 EA 跑一遍,改掉那行赋值再观察日志,比看十篇说明都实在。
class="type">void CheckStartDateInTradeHistory() { class=class="str">"cmt">//--- 初始间隔,就像我们现在开始工作一样 class="type">class="kw">datetime curr_start=TimeCurrent()-days*PeriodSeconds(PERIOD_D1); class=class="str">"cmt">//--- make sure that the start limit of the trade history has not gone class=class="str">"cmt">//--- 超出预定日期 class="num">1 天以上 if(curr_start-start>PeriodSeconds(PERIOD_D1)) { class=class="str">"cmt">//--- 我们应该更正缓存中要加载的历史记录的开始日期 start=curr_start; PrintFormat("New start limit of the trade history to be loaded: start => %s", TimeToString(start)); class=class="str">"cmt">//--- 现在再次加载已更正时段的交易历史记录 HistorySelect(start,end); class=class="str">"cmt">//--- 更正历史记录中的交易和订单数量,以便进一步比较 history_orders=HistoryOrdersTotal(); deals=HistoryOrdersTotal(); } }