MetaTrader 5 中的交易事件·进阶篇
📘

MetaTrader 5 中的交易事件·进阶篇

第 2/2 篇

「挂单为何终将离开活跃队列」

所有等待执行的订单,不管最终有没有触发、有没有被撤,最终都会被推到历史记录里。从成交到被拒,从手动撤单到 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 有没有变,变了再深挖;没变但订单属性改了的情形本文不覆盖。 别把正态当圣经:这套只侦测「数量增减」,若你要在订单改挂止损但单数不变时触发逻辑,得换更细的事件比对,不能直接抄上面计数器法。

MQL5 / C++
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 终端可能卡顿甚至断连。

MQL5 / C++
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 验证。

MQL5 / C++
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 跑一遍,改掉那行赋值再观察日志,比看十篇说明都实在。

MQL5 / C++
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();
     }
  }

常见问题

挂单触发或到期后会自动离开活跃队列,可用账户订单计数器比对前后数量变化来确认,而不是一直盯着列表。
单证到达代表请求已提交,不等于订单已在市场交易中落地,需等成交回执或查交易历史确认状态。
可以,小布能基于账户状态变动提醒你挂单离队或成交落地,不用手动反复刷新终端看盘口。
用动态矫正方式以最近一次已知成交时间为起点向后载入,避免固定起点导致早期订单未被比对到。
会,重连或数据重建也可能触发状态回调,建议用计数器差值和单证号过滤,只对有实际增减的变动告警。