轻松快捷开发 MetaTrader 程序的函数库(第十部分):与 MQL4 的兼容性 - 开仓和激活挂单的事件·综合运用
🔧

轻松快捷开发 MetaTrader 程序的函数库(第十部分):与 MQL4 的兼容性 - 开仓和激活挂单的事件·综合运用

(3/3)·从 34 个编译错误到跨终端事件统一,手把手拆掉 MT4/MT5 代码鸿沟的最后砖块

新手友好 第 3/3 篇
直接把 MQL5 测试 EA 丢进 MT4 编译,蹦出 34 个错误就慌着改逻辑的人不在少数。其实绝大多数报错只是标准库交易类缺失,用条件编译圈一下就能过。先跑通再谈改进,比闷头读文档省三倍时间。

「回测与实盘下的定时器分叉处理」

在 MT5 的 CEngine 初始化里,毫秒级定时器靠 EventSetMillisecondTimer(TIMER_FREQUENCY) 拉起。MQL4 分支不判断环境直接建;MQL5 分支则加了 !this.IsTester() guard,只在非回测实盘才建定时器,避免测试环境下无谓占用。 OnTimer 是定时器回调入口。先按 COLLECTION_COUNTER_ID 取计数器索引,index>WRONG_VALUE 才继续,否则直接跳过。 实盘时若计数器 IsTimeDone() 为真,才调用 TradeEventsControl() 处理历史订单、成交、持仓等集合事件;回测中则跳过时间判断,每个 tick 都跑 TradeEventsControl()。这意味着同段逻辑在策略测试器里事件触发频率显著高于实盘,验证 EA 时需注意两者行为差。 外汇与贵金属波动剧烈,这类定时器差异可能影响信号滞后,上线前务必在 MT5 实盘模拟账户核对频率。

MQL5 / C++
  if(!::EventSetMillisecondTimer(TIMER_FREQUENCY))
     ::Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError());
  class=class="str">"cmt">//---__MQL4__
  class="macro">#else
     if(!this.IsTester() && !::EventSetMillisecondTimer(TIMER_FREQUENCY))
       ::Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError());
  class="macro">#endif
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CEngine timer                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEngine::OnTimer(class="type">void)
  {
class=class="str">"cmt">//--- Timer of historical orders, deals, market orders and positions collections
   class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         class=class="str">"cmt">//--- If this is not a tester
         if(!this.IsTester())
           {
            class=class="str">"cmt">//--- If unpaused, work with the collections events
            if(counter.IsTimeDone())
               this.TradeEventsControl();
           }
         class=class="str">"cmt">//--- If this is a tester, work with collection events by tick
         else
           {
            this.TradeEventsControl();
           }
        }
     }
}

让库能识别挂单删除与MQL4开仓

函数库早期版本在 MQL4 下看不到挂单删除,根子在 CEventsCollection::Refresh() 里读历史事件时,抽象订单平仓构造器把 PositionID 在 MQL4 中直接填成了订单票证。MQL5 里挂单删除时 PositionID 应为 0,激活时才等于新开仓位 ID;MQL4 错填导致无法区分「删单」和「激活」。把 OrderPositionID() 在删单场景下强制置 0 后重新编译 EA,在策略测试器里挂单再删除,事件就能被跟踪到。 另一个盲点是 MQL4 下激活挂单开仓对库不可见。原因是 CEngine::OnTimer() 调用 TradeEventsControl(),后者把「新在场订单数」误当作「新持仓数」传给 Refresh();MQL5 靠成交数推新持仓,MQL4 没有这层映射。修复方式是把控制订单列表(已跟踪订单属性变化、未清理)公开给 CMarketCollection,并在 TradeEventsControl() 里改传新持仓数加控制订单列表。 Refresh() 内部要补两块:一是接收持仓列表的方法(按持仓状态排序后回传),二是从控制订单列表比对票证、返回导致开仓的订单类型(找不到回 -1)。CEventsCollection 私有段加两个成员存开仓订单类型与持仓 ID,CreateNewEvent() 里给 MQL4 开仓事件补上原因、订单与仓位 ID。改完之后,MQL4 的市价开仓与挂单触发都能进事件流。外汇与贵金属挂单测试请先在 MT5 策略测试器用历史数据验证,实盘存在滑点与被跳过风险,事件捕获可能延迟。

MQL5 / C++
  }
class=class="str">"cmt">//--- If the event is in the account history
  if(is_history_event)
    {
      class=class="str">"cmt">//--- If the number of historical orders increased
      if(new_history_orders>class="num">0)
        {
          class=class="str">"cmt">//--- Receive the list of removed pending orders only
          CArrayObj* list=this.GetListHistoryPendings(list_history);
          if(list!=NULL)
            {
              Print(DFUN);
              class=class="str">"cmt">//--- Sort the new list by order removal time
              list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC);
              class=class="str">"cmt">//--- Take the number of orders equal to the number of newly removed ones from the end of the list in a loop(the last N events)
              class="type">int total=list.Total(), n=new_history_orders;
              for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--)
                {
                  class=class="str">"cmt">//--- Receive an order from the list. If this is a removed pending order without a position ID, 
                  class=class="str">"cmt">//--- this is an order removal - set a trading event
                  COrder* order=list.At(i);
                  if(order!=NULL && order.Status()==ORDER_STATUS_HISTORY_PENDING && order.PositionID()==class="num">0)
                    this.CreateNewEvent(order,list_history,list_market);
                }
          }
      }
      class=class="str">"cmt">//--- If the number of deals increased
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Closed parametric constructor                                          |

◍ 订单对象构造时的整型属性落盘

在 COrder 的构造函数里,整型订单属性被一次性刷进 m_long_prop 数组,靠枚举键定位。这样后续读状态不必反复调 Order* 系列函数,直接从内存取,回测大样本时可能明显省掉重复获取的开销。 注意 ORDER_PROP_POSITION_ID 这行,它调的是 OrderPositionID() 而不是 OrderTicket(),二者在部分成交或对冲账户里可能返回不同值,复制代码时别顺手改成 ticket。 时间类字段如 ORDER_PROP_TIME_OPEN 做了 (long)(ulong) 强转,是因为 MQL5 的 datetime 本质是无符号,直接赋给有符号 long 在部分编译告警下可能越界,这种写法更稳。

MQL5 / C++
COrder::COrder(ENUM_ORDER_STATUS order_status,const class="type">ulong ticket)
  {
class=class="str">"cmt">//--- Save integer properties
   this.m_ticket=ticket;
   this.m_long_prop[ORDER_PROP_STATUS]                                = order_status;
   this.m_long_prop[ORDER_PROP_MAGIC]                                 = this.OrderMagicNumber();
   this.m_long_prop[ORDER_PROP_TICKET]                                = this.OrderTicket();
   this.m_long_prop[ORDER_PROP_TIME_OPEN]                             = (class="type">long)(class="type">ulong)this.OrderOpenTime();
   this.m_long_prop[ORDER_PROP_TIME_CLOSE]                            = (class="type">long)(class="type">ulong)this.OrderCloseTime();
   this.m_long_prop[ORDER_PROP_TIME_EXP]                              = (class="type">long)(class="type">ulong)this.OrderExpiration();
   this.m_long_prop[ORDER_PROP_TYPE]                                  = this.OrderType();
   this.m_long_prop[ORDER_PROP_STATE]                                 = this.OrderState();
   this.m_long_prop[ORDER_PROP_DIRECTION]                             = this.OrderTypeByDirection();
   this.m_long_prop[ORDER_PROP_POSITION_ID]                           = this.OrderPositionID();
   this.m_long_prop[ORDER_PROP_REASON]                                = this.OrderReason();
   this.m_long_prop[ORDER_PROP_DEAL_ORDER_TICKET]                     = this.DealOrderTicket();
   this.m_long_prop[ORDER_PROP_DEAL_ENTRY]                            = this.DealEntry();
   this.m_long_prop[ORDER_PROP_POSITION_BY_ID]                        = this.OrderPositionByID();
   this.m_long_prop[ORDER_PROP_TIME_OPEN_MSC]                         = this.OrderOpenTimeMSC();
   this.m_long_prop[ORDER_PROP_TIME_CLOSE_MSC]                        = this.OrderCloseTimeMSC();
   this.m_long_prop[ORDER_PROP_TIME_UPDATE]                           = (class="type">long)(class="type">ulong)this.PositionTimeUpdate();

「订单历史快照的字段落盘」

在回测或实盘复盘里,把一笔已平订单的完整属性固化到本地对象,是后续做盈亏归因的前提。下面这段逻辑出现在订单对象刷新时,把 MT5 交易类接口取出的原始值逐一写进内部数组。 时间字段用毫秒精度落盘:this.m_long_prop[ORDER_PROP_TIME_UPDATE_MSC] 先把 PositionTimeUpdateMSC() 的 ulong 结果强转 long 存储,避免跨平台整型宽度差异导致溢出。 浮点类属性覆盖开平价格、利润、佣金、库存费、成交量、止损止盈等 11 个键,全部通过 IndexProp() 动态取索引再赋值。例如 ORDER_PROP_PROFIT 对应 OrderProfit(),ORDER_PROP_VOLUME_CURRENT 对应 OrderVolumeCurrent(),StopLimit 单的触发价则由 OrderPriceStopLimit() 填入。 字符串与扩展整型分开存:品种名、注释、外部 ID 进 m_string_prop;点数盈利 ProfitInPoints()、来源与去向票号、是否被 SL/TP 关闭等进 m_long_prop,其中 GROUP_ID 硬编码为 0 表示未分组。最后 ProfitFull() 算出的含费总盈利用作补充浮点字段。 开 MT5 用这些接口写个脚本,把最近 100 笔平仓单的 ORDER_PROP_PROFIT 与 ORDER_PROP_PROFIT_PT 打印出来,就能验证你的点值计算和券商佣金口径是否一致。外汇与贵金属杠杆高,复盘结论只代表历史样本,实盘仍可能偏离。

MQL5 / C++
this.m_long_prop[ORDER_PROP_TIME_UPDATE_MSC] = (class="type">long)(class="type">ulong)this.PositionTimeUpdateMSC();

class=class="str">"cmt">//--- Save real properties
this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_OPEN)] = this.OrderOpenPrice();
this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_CLOSE)] = this.OrderClosePrice();
this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT)] = this.OrderProfit();
this.m_double_prop[this.IndexProp(ORDER_PROP_COMMISSION)] = this.OrderCommission();
this.m_double_prop[this.IndexProp(ORDER_PROP_SWAP)] = this.OrderSwap();
this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME)] = this.OrderVolume();
this.m_double_prop[this.IndexProp(ORDER_PROP_SL)] = this.OrderStopLoss();
this.m_double_prop[this.IndexProp(ORDER_PROP_TP)] = this.OrderTakeProfit();
this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME_CURRENT)] = this.OrderVolumeCurrent();
this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_STOP_LIMIT)] = this.OrderPriceStopLimit();

class=class="str">"cmt">//--- Save class="type">class="kw">string properties
this.m_string_prop[this.IndexProp(ORDER_PROP_SYMBOL)] = this.OrderSymbol();
this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT)] = this.OrderComment();
this.m_string_prop[this.IndexProp(ORDER_PROP_EXT_ID)] = this.OrderExternalID();

class=class="str">"cmt">//--- Save additional integer properties
this.m_long_prop[ORDER_PROP_PROFIT_PT] = this.ProfitInPoints();
this.m_long_prop[ORDER_PROP_TICKET_FROM] = this.OrderTicketFrom();
this.m_long_prop[ORDER_PROP_TICKET_TO] = this.OrderTicketTo();
this.m_long_prop[ORDER_PROP_CLOSE_BY_SL] = this.OrderCloseByStopLoss();
this.m_long_prop[ORDER_PROP_CLOSE_BY_TP] = this.OrderCloseByTakeProfit();
this.m_long_prop[ORDER_PROP_GROUP_ID] = class="num">0;

class=class="str">"cmt">//--- Save additional real properties
this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT_FULL)] = this.ProfitFull();
}

从订单状态反查持仓 ID 的写法

在 MT5 的封装类里,想拿到一笔订单对应的持仓 ID,不能无脑调一个函数,得先看它处在什么生命周期。上面这段 COrder::OrderPositionID 就按 ORDER_PROP_STATUS 做了分路:市价持仓直接取 POSITION_IDENTIFIER,活动订单或挂单走 ORDER_POSITION_ID,历史订单和成交记录则分别从 HistoryOrderGetInteger 与 HistoryDealGetInteger 里捞。 MQL4 分支里早期版本返回 ::OrderTicket(),后来改成返回 0,说明在四系环境里这个映射本身不可靠,别拿它当持仓主键用。 实盘里如果你要按持仓归并挂单、统计某笔头寸衍生了多少次加仓成交,这套 switch 逻辑可以直接抄进 EA。外汇和贵金属杠杆高,持仓 ID 串错可能误平不该平的仓,先在策略测试器用历史数据跑一遍再上真金。

MQL5 / C++
class="type">long COrder::OrderPositionID(class="type">void) const
  {
class="macro">#ifdef __MQL4__
   class="kw">return ::OrderTicket();
class="macro">#else
   class="type">long res=class="num">0;
   class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
     {
      case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_IDENTIFIER);                    class="kw">break;
      case ORDER_STATUS_MARKET_ORDER      :
      case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_POSITION_ID);                         class="kw">break;
      case ORDER_STATUS_HISTORY_PENDING   :
      case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_POSITION_ID); class="kw">break;
      case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_POSITION_ID);     class="kw">break;
      class="kw">default                             : res=class="num">0;                                                          class="kw">break;
     }
   class="kw">return res;
class="macro">#endif
  }

◍ 引擎定时器如何分流实盘与回测的事件采集

CEngine::OnTimer 是交易引擎的周期性入口,实盘下靠计数器判断是否到时,回测中则每 tick 直接触发 TradeEventsControl,两种模式的分流就在这里定调。 看 OnTimer 里的关键分支:非测试环境用 counter.IsTimeDone() 拦一道,避免无谓刷新;测试环境跳过计时直接走事件控制,保证回测节奏和 tick 同步。实盘若误用回测逻辑,历史订单采集频率会失控,EA 占用可能翻倍。 TradeEventsControl 先清零市场与历史事件标志,再 Refresh 两个列表。首次启动只置 NO_EVENT 并 return,防止启动瞬间把缓存差异当交易信号。 change_total 取自 m_market.GetListChanges 的 Total,若列表非空则记录变动条数,这一步是后续推事件给收集器的基数。外汇与贵金属波动快,这类采集若卡顿可能漏掉滑点较大的瞬时成交,属高风险场景。

MQL5 / C++
class="type">void CEngine::OnTimer(class="type">void)
  {
class=class="str">"cmt">//--- Timer of historical orders, deals, market orders and positions collections
   class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         class=class="str">"cmt">//--- If this is not a tester
         if(!this.IsTester())
           {
            class=class="str">"cmt">//--- If unpaused, work with the collections events
            if(counter.IsTimeDone())
               this.TradeEventsControl();
           }
         class=class="str">"cmt">//--- If this is a tester, work with collection events by tick
         else 
           {
            this.TradeEventsControl();
           }
        }
     }
  }

class="type">void CEngine::TradeEventsControl(class="type">void)
  {
class=class="str">"cmt">//--- Initialize the trading events code and flags
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
class=class="str">"cmt">//--- Update the lists 
   this.m_market.Refresh();
   this.m_history.Refresh();
class=class="str">"cmt">//--- First launch actions
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      class="kw">return;
     }
class=class="str">"cmt">//--- Check the changes in the market status and account history 
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
class=class="str">"cmt">//--- If there is any event, send the lists, the flags and the number of new orders and deals to the event collection, and update it
   class="type">int change_total=class="num">0;
   CArrayObj* list_changes=this.m_market.GetListChanges();
   if(list_changes!=NULL)
      change_total=list_changes.Total();

「事件触发与三类订单列表的取数口子」

账户交易事件不是每次 tick 都重算。只有当历史成交事件、市场成交事件标志置位,或者订单总数变化量 change_total 大于 0 时,才进 if 块刷新事件容器并抓取最近一次账户交易事件。这样能把无谓的 Refresh 调用压下去,MT5 实盘里日志打印频率可能下降一个数量级。 刷新时把四个来源的新增对象一并喂给 m_events.Refresh:历史订单、市场挂单、市场市价单、历史成交。其中 m_market.NewMarketOrders() 抓的是本次事件新开的市价单,和 NewPendingOrders 的挂单要分开看,混在一起会让你在回测里把成交与挂单的触发时序搞反。 对外取数靠三个裸指针接口:GetList 拿全量挂单与持仓,GetListChanges 拿本次变更对象,GetListControl 拿控单对象。CArrayObj* 直接返回成员地址,调用方改了里面元素会影响原容器,EA 里多线程或异步事件处理时要留神。 按属性过滤另有一组重载:传入 ENUM_ORDER_PROP_DOUBLE 或 INTEGER 加比较模式 EQUAL 等,底层走 CSelect::ByOrderProperty 在 GetList() 结果上二次筛。写策略时若只要「盈利超过 N 点的持仓」,直接调这个比自己遍历省事,但注意它返回的是新数组,原 m_list_all_orders 不被改动。

MQL5 / C++
if(this.m_is_history_trade_event || this.m_is_market_trade_event || change_total>class="num">0)
  {
    this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(),list_changes,
                          this.m_is_history_trade_event,this.m_is_market_trade_event,
                          this.m_history.NewOrders(),this.m_market.NewPendingOrders(),
                          this.m_market.NewMarketOrders(),this.m_history.NewDeals());
    class=class="str">"cmt">//--- Get the account&class="macro">#x27;s last trading event
    this.m_acc_trade_event=this.m_events.GetLastTradeEvent();
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Return the list(class="num">1) of all pending orders and open positions, (class="num">2) control orders and positions
  CArrayObj*       GetList(class="type">void)                                            { class="kw">return &this.m_list_all_orders;              }
  CArrayObj*       GetListChanges(class="type">void)                                     { class="kw">return &this.m_list_changed;                }
  CArrayObj*       GetListControl(class="type">void)                                     { class="kw">return &this.m_list_control;                }
class=class="str">"cmt">//--- Return the list of orders and positions with an open time from begin_time to end_time
  CArrayObj*       GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0);
class=class="str">"cmt">//--- Return the list of orders and positions by selected(class="num">1) class="type">class="kw">double, (class="num">2) integer and(class="num">3) class="type">class="kw">string class="kw">property fitting a compared condition
  CArrayObj*       GetList(ENUM_ORDER_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode);  }
  CArrayObj*       GetList(ENUM_ORDER_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL)  { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode);  }

从订单事件里抠出增量数据

做日内波段的外汇和贵金属交易者,最怕的是事件轮询时把旧单和新单混在一起算,导致重复触发。下面这段 CMarketCollection 的接口把「本周期新增了什么」直接拆成只读方法,调用一次就能拿到五类增量。 GetList 按字符串属性过滤订单集合,比如按订单注释或符号筛,mode 默认 EQUAL,也可换包含或不等。真正有用的是后面五个 const 方法:NewMarketOrders 返回新市价单数,NewPendingOrders 返回新挂单数,NewPositions 返回新持仓数,IsTradeEvent 返回是否发生了交易事件布尔值,ChangedVolumeValue 返回变动的成交量。 这些方法背后是成员 m_new_market_orders、m_new_pendings、m_new_positions、m_is_trade_event、m_change_volume_value,由 Refresh 在每次更新挂单与持仓列表时重算。实盘里黄金 XAUUSD 在美盘初段常在一根 5 分钟 K 内新增 3~5 个市价单,用 NewMarketOrders 抓到 >0 即可判定有主动流入,但外汇保证金波动剧烈,仍属高风险,信号仅作概率参考。 让小布替你跑这套 把 Refresh 挂到 OnTradeTransaction,再用 NewPositions>0 做开仓后二次确认,比每 tick 全量扫持仓省 CPU,回测里重复下单率可能降一半。

MQL5 / C++
CArrayObj* GetList(ENUM_ORDER_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); }
class=class="str">"cmt">//--- Return the number of(class="num">1) new market orders, (class="num">2) new pending orders, (class="num">3) new positions, (class="num">4) occurred trading event flag, (class="num">5) changed volume
  class="type">int NewMarketOrders(class="type">void) const { class="kw">return this.m_new_market_orders; }
  class="type">int NewPendingOrders(class="type">void) const { class="kw">return this.m_new_pendings; }
  class="type">int NewPositions(class="type">void) const { class="kw">return this.m_new_positions; }
  class="type">bool IsTradeEvent(class="type">void) const { class="kw">return this.m_is_trade_event; }
  class="type">class="kw">double ChangedVolumeValue(class="type">void) const { class="kw">return this.m_change_volume_value; }
class=class="str">"cmt">//--- Constructor
 CMarketCollection(class="type">void);
class=class="str">"cmt">//--- Update the list of pending orders and positions
  class="type">void Refresh(class="type">void);
};

◍ 交易事件的分发与刷新逻辑

在 MT5 的 EA 框架里,账户层面的交易事件(挂单成交、持仓开平、历史订单变动)不能靠轮询裸数据,得用统一的刷新入口收敛。下面这段引擎方法就是干这件事的:先把市场与历史两个列表 Refresh,再比对是否出现事件标志。 首次启动时要跳过事件判定,直接把账户事件码置为 TRADE_EVENT_NO_EVENT 并返回,否则容易把初始化时的存量订单误判成新信号。实盘里这个细节没处理好,可能在重启 EA 的瞬间触发重复下单。 当市场或历史任一侧出现交易事件,或市场变更列表总数大于 0 时,才调用 m_events.Refresh 把最新的挂单、持仓、成交塞进事件集合,并回写 m_acc_trade_event。外汇与贵金属杠杆高,事件漏判或重复判都会放大滑点风险,建议你在策略测试器里用 2023 年 10 月 EURUSD 的 M1 数据跑一遍,观察 change_total 在非农前后的跳变。 别把正态当圣经 上面代码里 m_market.GetListChanges() 返回的指针可能为 NULL,必须先判空再取 Total(),很多自行改写的引擎省掉这步,在账户无变更时直接崩在零指针上。

MQL5 / C++
class="type">void CEngine::TradeEventsControl(class="type">void)
  {
class=class="str">"cmt">//--- Initialize the trading events code and flags
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
class=class="str">"cmt">//--- Update the lists 
   this.m_market.Refresh();
   this.m_history.Refresh();
class=class="str">"cmt">//--- First launch actions
   if(this.IsFirstStart())
     {
       this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
       class="kw">return;
     }
class=class="str">"cmt">//--- Check the changes in the market status and account history 
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
class=class="str">"cmt">//--- If there is any event, send the lists, the flags and the number of new orders and deals to the event collection, and update it
   class="type">int change_total=class="num">0;
   CArrayObj* list_changes=this.m_market.GetListChanges();
   if(list_changes!=NULL)
       change_total=list_changes.Total();
   if(this.m_is_history_trade_event || this.m_is_market_trade_event || change_total>class="num">0)
     {
       this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(),list_changes,this.m_market.GetListControl(),
                             this.m_is_history_trade_event,this.m_is_market_trade_event,
                             this.m_history.NewOrders(),this.m_market.NewPendingOrders(),
                             this.m_market.NewPositions(),this.m_history.NewDeals());
       class=class="str">"cmt">//--- Get the account&class="macro">#x27;s last trading event
       this.m_acc_trade_event=this.m_events.GetLastTradeEvent();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//--- Select events from the collection with time within the range from begin_time to end_time
   CArrayObj        *GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0);
class=class="str">"cmt">//--- Return the full event collection list "as is"
   CArrayObj        *GetList(class="type">void)                                                                  { class="kw">return &this.m_list_events;                                                                  }
class=class="str">"cmt">//--- Return the list by selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion

「事件集合类的筛选与刷新接口」

CEventsCollection 提供了三组按属性取事件的重载方法,分别针对整型、双精度、字符串三种属性类型。调用时只需传 ENUM_EVENT_PROP_XXX 枚举、对应数值和比较模式(默认 EQUAL),底层转交 CSelect::ByEventProperty 处理当前列表,返回的是 CArrayObj 指针,方便继续链式筛选。 Refresh 是事件系统的核心刷新入口,参数列了 4 个事件列表指针:历史、市场、变更、控制程序;后跟 3 个 bool 开关标明本次是否含历史/市场事件,再跟 new_history_orders、new_market_pendings、new_market_positions、new_deals 四个 int 计数,告知各类别新增数量。实盘里若发现某类事件没触发,先打印这四个计数确认推送是否到位。 SetChartID 直接写 m_chart_id,控制程序靠它绑定图表;GetLastTradeEvent 返回 ENUM_TRADE_EVENT 标记最近一次账户交易事件,ResetLastTradeEvent 将其归零为 TRADE_EVENT_NO_EVENT。建议在 EA 的 OnTradeTransaction 里读完后立即 reset,避免同事件被重复处理。 下面这段是类声明里的接口骨架,注意 list_control 在原文中被高亮,说明它是控制事件分离的关键载体。

MQL5 / C++
  CArrayObj       *GetList(ENUM_EVENT_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL)  { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode);  }
  CArrayObj       *GetList(ENUM_EVENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode);  }
  CArrayObj       *GetList(ENUM_EVENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode);  }
class=class="str">"cmt">//--- Update the list of events
   class="type">void             Refresh(CArrayObj* list_history,
                            CArrayObj* list_market,
                            CArrayObj* list_changes,
                            CArrayObj* list_control,
                            const class="type">bool is_history_event,
                            const class="type">bool is_market_event,
                            const class="type">int  new_history_orders,
                            const class="type">int  new_market_pendings,
                            const class="type">int  new_market_positions,
                            const class="type">int  new_deals);
class=class="str">"cmt">//--- Set the control program chart ID
   class="type">void             SetChartID(const class="type">long id)        { this.m_chart_id=id;     }
class=class="str">"cmt">//--- Return the last trading event on the account
   ENUM_TRADE_EVENT  GetLastTradeEvent(class="type">void)      const { class="kw">return this.m_trade_event;  }
class=class="str">"cmt">//--- Reset the last trading event
   class="type">void             ResetLastTradeEvent(class="type">void)         { this.m_trade_event=TRADE_EVENT_NO_EVENT;  }
class=class="str">"cmt">//--- Constructor
                     CEventsCollection(class="type">void);
   };
class="type">void CEventsCollection::Refresh(CArrayObj* list_history,
                                CArrayObj* list_market,

账户事件收集类的私有成员布局

在 MT5 的 EA 事件框架里,账户状态变更不会自动归类,需要自己用一个继承 CListObj 的容器把每次成交、挂单、持仓变动都收拢起来。下面这段类声明把后续判断所需的全部上下文都塞进了私有成员,少一个都会在回放历史时丢事件。 m_list_events 是事件链表本体,m_is_hedge 标记账户是净位还是对冲模式——这两者在持仓 ID 处理上完全不同,外汇与贵金属账户尤需区分,杠杆放大下误判可能直接错过大行情。 m_chart_id 绑定控制程序的图表 ID,m_trade_event_code 与 m_trade_event 记录本次交易事件代码与枚举,m_event_instance 用作按属性检索的临时对象,m_tick 存最近一个 tick 结构。 被高亮的两行是 MQL4 兼容字段:m_position_id 存仓位 ID,m_type_first 存开仓订单类型,做跨版本迁移时不能删。最后一行声明了 CreateNewEvent 方法,靠订单指针加历史/市场两个数组来生成新事件对象。

MQL5 / C++
class CEventsCollection : class="kw">public CListObj
  {
class="kw">private:
   CListObj       m_list_events;            class=class="str">"cmt">// List of events
   class="type">bool           m_is_hedge;              class=class="str">"cmt">// Hedge account flag
   class="type">long           m_chart_id;              class=class="str">"cmt">// Control program chart ID
   class="type">int            m_trade_event_code;      class=class="str">"cmt">// Trading event code
   ENUM_TRADE_EVENT m_trade_event;         class=class="str">"cmt">// Account trading event
   CEvent         m_event_instance;        class=class="str">"cmt">// Event object for searching by class="kw">property
   class="type">MqlTick        m_tick;                  class=class="str">"cmt">// Last tick structure
   class="type">ulong          m_position_id;           class=class="str">"cmt">// Position ID(MQL4)  
   ENUM_ORDER_TYPE m_type_first;           class=class="str">"cmt">// Opening order type(MQL4)

class=class="str">"cmt">//--- Create a trading event depending on the order(class="num">1) status and(class="num">2) change type
   class="type">void           CreateNewEvent(COrder* order,CArrayObj* list_history,CArrayObj* list_market);

◍ 拆账户事件与持仓筛选的接口层

在 MT5 的 EA 事件框架里,账户类型直接决定成交回调的走向。上面这组声明把对冲账户(hedge)与净仓账户(netto)的成交事件拆成两个独立入口:NewDealEventHedge 与 NewDealEventNetto,调用时按 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 的返回值分流即可,避免在一套逻辑里反复判断持仓模式。 持仓与挂单的提取靠 GetListPositions 与 GetListMarketPendings 两个方法,入参是完整历史/市场对象数组,返回的是筛选后的 CArrayObj* 指针。实盘里若每 tick 都重建全量列表,CPU 占用可能偏高;更稳的做法是仅在 OnTradeTransaction 触发后调用一次,而非塞进 OnTick。 按持仓 ID 回溯成交是统计盈亏来源的关键。GetListAllDealsInByPosID 取该仓所有进场成交,GetListAllDealsOutByPosID 取所有出场成交,再用 SummaryVolumeDealsInByPosID / OutByPosID 汇总双边手数——若 In 手数明显大于 Out,说明部分平仓后仓位仍开着,净仓账户下这种不对称要单独标记。 GetTypeFirst 接收 MQL4 风格 ticket 返回首单 ENUM_ORDER_TYPE,是老代码迁移时的兼容桥。IsPresentEventInList 则用来去重:同一笔成交推送可能触发多次事件对象入列,靠它拦掉重复处理。

MQL5 / C++
class="type">void                 CreateNewEvent(COrderControl* order);
class=class="str">"cmt">//--- Create an event for a(class="num">1) hedging account, (class="num">2) netting account
class="type">void                 NewDealEventHedge(COrder* deal,CArrayObj* list_history,CArrayObj* list_market);
class="type">void                 NewDealEventNetto(COrder* deal,CArrayObj* list_history,CArrayObj* list_market);
class=class="str">"cmt">//--- Select from the list and class="kw">return the list of(class="num">1) market pending orders, (class="num">2) open positions
CArrayObj*           GetListMarketPendings(CArrayObj* list);
CArrayObj*           GetListPositions(CArrayObj* list);
class=class="str">"cmt">//--- Select from the list and class="kw">return the list of historical(class="num">1) removed pending orders, (class="num">2) deals, (class="num">3) all closing orders 
CArrayObj*           GetListHistoryPendings(CArrayObj* list);
CArrayObj*           GetListDeals(CArrayObj* list);
CArrayObj*           GetListCloseByOrders(CArrayObj* list);
class=class="str">"cmt">//--- Return the list of(class="num">1) all position orders by its ID, (class="num">2) all deal positions by its ID
class=class="str">"cmt">//--- (class="num">3) all market entry deals by position ID, (class="num">4) all market exit deals by position ID,
class=class="str">"cmt">//--- (class="num">5) all position reversal deals by position ID
CArrayObj*           GetListAllOrdersByPosID(CArrayObj* list,const class="type">ulong position_id);
CArrayObj*           GetListAllDealsByPosID(CArrayObj* list,const class="type">ulong position_id);
CArrayObj*           GetListAllDealsInByPosID(CArrayObj* list,const class="type">ulong position_id);
CArrayObj*           GetListAllDealsOutByPosID(CArrayObj* list,const class="type">ulong position_id);
CArrayObj*           GetListAllDealsInOutByPosID(CArrayObj* list,const class="type">ulong position_id);
class=class="str">"cmt">//--- Return the total volume of all deals(class="num">1) IN, (class="num">2) OUT of the position by its ID
class="type">class="kw">double               SummaryVolumeDealsInByPosID(CArrayObj* list,const class="type">ulong position_id);
class="type">class="kw">double               SummaryVolumeDealsOutByPosID(CArrayObj* list,const class="type">ulong position_id);
class=class="str">"cmt">//--- Return the(class="num">1) first, (class="num">2) last and(class="num">3) closing order from the list of all position orders,
class=class="str">"cmt">//--- (class="num">4) an order by ticket, (class="num">5) market position by ID,
class=class="str">"cmt">//--- (class="num">6) the last and(class="num">7) penultimate InOut deal by position ID
COrder*              GetFirstOrderFromList(CArrayObj* list,const class="type">ulong position_id);
COrder*              GetLastOrderFromList(CArrayObj* list,const class="type">ulong position_id);
COrder*              GetCloseByOrderFromList(CArrayObj* list,const class="type">ulong position_id);
COrder*              GetHistoryOrderByTicket(CArrayObj* list,const class="type">ulong order_ticket);
COrder*              GetPositionByID(CArrayObj* list,const class="type">ulong position_id);
class=class="str">"cmt">//--- Return the type of the opening order by the position ticket(MQL4)
ENUM_ORDER_TYPE      GetTypeFirst(CArrayObj* list,const class="type">ulong ticket);
class=class="str">"cmt">//--- Return the flag of the event object presence in the event list
class="type">bool                 IsPresentEventInList(CEvent* compared_event);
class=class="str">"cmt">//--- Existing order/position change event handler
class="type">void                 OnChangeEvent(CArrayObj* list_changes,const class="type">int index);
class="kw">public:

「从混合列表里抠出市价持仓」

做事件聚合时,往往拿到的是包含挂单、历史单和市价单的混合集合。下面这段逻辑先卡一道类型校验,非市场集合直接返回 NULL,避免后续遍历错对象。 CArrayObj* list_positions=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL); 这一行才是核心:按订单状态属性过滤,只保留 STATUS 为 MARKET_POSITION 的实体。回测里若列表含 120 个对象、其中 37 个为市价持仓,该函数返回的新数组长度就是 37。 另一个实用函数是 GetTypeFirst:传入持仓 ticket,在列表里线性查找(list.Total() 决定循环上限),命中即返回当时开仓的订单类型枚举。注意它返回的是 WRONG_VALUE 而不是抛异常,调用方必须判这个值。 Refresh 方法把历史、市场、变更、控制四张表一并接进来,配合 is_history_event / is_market_event 两个布尔开关和 new_history_orders / new_market_pendings 两个增量计数,决定本次刷新要重算哪一部分。外汇与贵金属波动大、滑点随机,这类增量刷新逻辑能降低 CPU 占用,但高频重算仍可能漏掉极端跳空瞬间的状态。

MQL5 / C++
CArrayObj* CEventsCollection::GetListPositions(CArrayObj *list)
  {
   if(list.Type()!=COLLECTION_MARKET_ID)
     {
      Print(DFUN,TextByLanguage("Ошибка. Список не является списком рыночной коллекции","Error. The list is not a list of the market collection"));
      class="kw">return NULL;
     }
   CArrayObj* list_positions=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   class="kw">return list_positions;
  }
ENUM_ORDER_TYPE CEventsCollection::GetTypeFirst(CArrayObj* list,const class="type">ulong ticket)
  {
   if(list==NULL)
      class="kw">return WRONG_VALUE;
   class="type">int total=list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      COrderControl* ctrl=list.At(i);
      if(ctrl==NULL)
         class="kw">continue;
      if(ctrl.Ticket()==ticket)
         class="kw">return (ENUM_ORDER_TYPE)ctrl.TypeOrder();
     }
   class="kw">return WRONG_VALUE;
  }
class="type">void CEventsCollection::Refresh(CArrayObj* list_history,
                                CArrayObj* list_market,
                                CArrayObj* list_changes,
                                CArrayObj* list_control,
                                const class="type">bool is_history_event,
                                const class="type">bool is_market_event,
                                const class="type">int  new_history_orders,
                                const class="type">int  new_market_pendings,

区分挂单与持仓的事件回调

在 MT5 的实时交易事件处理里,市场环境事件(is_market_event)需要拆成两条支线:一是挂单属性变更或新挂单入场,二是 MQL4 兼容层下的持仓增加。若不做区分,把挂单和持仓混在同一循环里扫,会重复触发交易事件,导致小布盯盘的提醒逻辑误报。 代码里先用 list_changes.Total() 拿到变更总数,大于 0 才倒序回放 OnChangeEvent;随后判断 new_market_pendings>0,只从已排序的挂单列表尾部取最后 N 条(N=new_market_pendings),用 ORDER_STATUS_MARKET_PENDING 状态过滤,确保只有真实挂单才进 CreateNewEvent。 #ifdef __MQL4__ 那段只在 MQL4 编译环境下生效,用 GetListPositions 取持仓并按开仓时间排序,逻辑和挂单对称但对象不同。外汇与贵金属杠杆高,事件回调若漏掉状态判断,可能在跳空时连发多条预警,实盘前务必在策略测试器用 2023 年 EURUSD tick 数据跑一遍确认只触发一次。

MQL5 / C++
const class="type">int  new_market_positions,
const class="type">int  new_deals)
  {
class=class="str">"cmt">//--- Exit if the lists are empty
   if(list_history==NULL || list_market==NULL)
      class="kw">return;
class=class="str">"cmt">//--- If the event is in the market environment
   if(is_market_event)
     {
      class=class="str">"cmt">//--- if the order properties were changed
      class="type">int total_changes=list_changes.Total();
      if(total_changes>class="num">0)
        {
         for(class="type">int i=total_changes-class="num">1;i>=class="num">0;i--)
           {
            this.OnChangeEvent(list_changes,i);
           }
        }
      class=class="str">"cmt">//--- if the number of placed pending orders increased
      if(new_market_pendings>class="num">0)
        {
         class=class="str">"cmt">//--- Receive the list of the newly placed pending orders
         CArrayObj* list=this.GetListMarketPendings(list_market);
         if(list!=NULL)
           {
            class=class="str">"cmt">//--- Sort the new list by order placement time
            list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
            class=class="str">"cmt">//--- Take the number of orders equal to the number of newly placed ones from the end of the list in a loop(the last N events)
            class="type">int total=list.Total(), n=new_market_pendings;
            for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--)
              {
               class=class="str">"cmt">//--- Receive an order from the list, if this is a pending order, set a trading event
               COrder* order=list.At(i);
               if(order!=NULL && order.Status()==ORDER_STATUS_MARKET_PENDING)
                  this.CreateNewEvent(order,list_history,list_market);
              }
           }
        }
      class="macro">#ifdef __MQL4__
      class=class="str">"cmt">//--- If the number of positions increased
      if(new_market_positions>class="num">0)
        {
         class=class="str">"cmt">//--- Get the list of open positions
         CArrayObj* list=this.GetListPositions(list_market);
         if(list!=NULL)
           {
            class=class="str">"cmt">//--- Sort the new list by a position open time
            list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);

◍ 从列表尾部抓取最新N笔成交事件

在账户事件监控里,新开市价仓位和已撤挂单都只关心「最近发生的几笔」,而不是全量扫描。代码用 list.Total() 拿到总长度,再用 new_market_positions 或 new_history_orders 作为计数器 n,从 i=total-1 倒序循环,每处理一笔 n 减 1,直到 n 耗尽——这样只触及列表末尾的 N 个节点,时间复杂度稳定在 O(N)。 市价仓位分支里,position.Status()==ORDER_STATUS_MARKET_POSITION 是过滤关键。命中后调 GetTypeFirst() 反查导致开仓的首笔订单类型,把 position.Ticket() 同时存进 m_position_id,再 CreateNewEvent() 落一条交易事件。外汇与贵金属杠杆高,这类事件捕获逻辑若漏掉状态判断,可能在快速反转行情里重复记账。 历史挂单分支更苛刻:必须是 ORDER_STATUS_HISTORY_PENDING 且 PositionID()==0,才认定为「纯撤单」而非「挂单转持仓」。list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC) 先按撤单时间毫秒排序,保证尾部取出的就是最新撤单。直接把这段拷进 MT5 的 CEVENT 派生类,用 Print() 打 new_market_positions 和 new_history_orders,能直观看到每次回调只跑末尾几笔。

MQL5 / C++
class="type">int total=list.Total(), n=new_market_positions;
for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--)
  {
   COrder* position=list.At(i);
   if(position!=NULL && position.Status()==ORDER_STATUS_MARKET_POSITION)
     {
      this.m_type_first=this.GetTypeFirst(list_control,position.Ticket());
      this.m_position_id=position.Ticket();
      this.CreateNewEvent(position,list_history,list_market);
     }
  }

CArrayObj* list=this.GetListHistoryPendings(list_history);
if(list!=NULL)
  {
   list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC);
   class="type">int total=list.Total(), n=new_history_orders;
   for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--)
     {
      COrder* order=list.At(i);
      if(order!=NULL && order.Status()==ORDER_STATUS_HISTORY_PENDING && order.PositionID()==class="num">0)
        this.CreateNewEvent(order,list_history,list_market);
     }
  }

「从成交流水里抓新开仓事件」

在 EA 的历史回放或实盘监控里,成交流水(deal history)往往混杂了平仓、部分成交和手续费记录。上面这段逻辑先判断 new_deals 是否大于 0,只有确实出现新增成交时才去拉取列表,避免无谓的对象创建和排序开销。 拿到列表后按 SORT_BY_ORDER_TIME_OPEN_MSC 排序,再从尾部倒序取最近 n 笔(n = new_deals)。这种「取末尾 N 个」的写法保证了只处理增量事件,而不是每次全量重扫——在 MT5 跑高频策略时,全量扫描几千条成交会让 OnTrade 回调明显掉帧。 下面这段则专门处理市价持仓开通:当 status 等于 ORDER_STATUS_MARKET_POSITION,把交易事件码置为 TRADE_EVENT_FLAG_POSITION_OPENED。若首笔订单类型落在挂单区间(m_type_first 大于 ORDER_TYPE_SELL 且小于 ORDER_TYPE_BALANCE),则把原因改为 EVENT_REASON_ACTIVATED_PENDING,并叠加 TRADE_EVENT_FLAG_ORDER_ACTIVATED 标记——这意味着你能在事件层区分「市价直接吃单」和「挂单触发成仓」两种开仓路径。 外汇与贵金属杠杆高、滑点跳空频繁,挂单激活和实际成仓之间可能有数毫秒到数十毫秒偏差,事件原因字段就是用来回溯这种延迟来源的。开 MT5 用这些标记跑一遍回测,你能直接看出自己策略里有多少仓是挂单触发的。

MQL5 / C++
if(new_deals>class="num">0)
  {
   class=class="str">"cmt">//--- Receive the list of deals only
   CArrayObj* list=this.GetListDeals(list_history);
   if(list!=NULL)
     {
      class=class="str">"cmt">//--- Sort the new list by deal time
      list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
      class=class="str">"cmt">//--- Take the number of deals equal to the number of new ones from the end of the list in a loop(the last N events)
      class="type">int total=list.Total(), n=new_deals;
      for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--)
        {
         class=class="str">"cmt">//--- Receive a deal from the list and set a trading event
         COrder* order=list.At(i);
         if(order!=NULL)
           this.CreateNewEvent(order,list_history,list_market);
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Position opened(__MQL4__)
  if(status==ORDER_STATUS_MARKET_POSITION)
   {
    class=class="str">"cmt">//--- Set the "position opened" trading event code
    this.m_trade_event_code=TRADE_EVENT_FLAG_POSITION_OPENED;
    class=class="str">"cmt">//--- Set the "request executed partially" reason
    ENUM_EVENT_REASON reason=EVENT_REASON_DONE;
    class=class="str">"cmt">//--- If an opening order is a pending one
    if(this.m_type_first>ORDER_TYPE_SELL && this.m_type_first<ORDER_TYPE_BALANCE)
      {
       class=class="str">"cmt">//--- set the "pending order activated" reason
       reason=EVENT_REASON_ACTIVATED_PENDING;
       class=class="str">"cmt">//--- add a pending order activation flag to the event code
       this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
      }
    CEvent* event=new CEventPositionOpen(this.m_trade_event_code,order.Ticket());
    if(event!=NULL)
      {
       event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC());                                                     class=class="str">"cmt">// Event time
       event.SetProperty(EVENT_PROP_REASON_EVENT,reason);                                                                class=class="str">"cmt">// Event reason(from the ENUM_EVENT_REASON enumeration)
       event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,this.m_type_first);                                                  class=class="str">"cmt">// Event deal type

给持仓事件挂上可追溯的票据属性

在 MT5 的自定义事件对象里,用 SetProperty 把成交与持仓的关键票据写进去,是后续做持仓翻转分析的第一步。下面这段代码把成交票号、触发订单类型、持仓 ID 以及反向持仓 ID 全部塞进 event,相当于给这次方向切换留了案底。 event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); // 成交事件票号 event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,this.m_type_first); // 触发成交的订单类型(末仓单) event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,this.m_type_first); // 触发持仓的订单类型(首仓单) event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket()); // 成交事件对应的订单票号 event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); // 持仓事件对应的订单票号 event.SetProperty(EVENT_PROP_POSITION_ID,this.m_position_id); // 持仓 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID()); // 反向持仓 ID event.SetProperty(EVENT_PROP_MAGIC_BY_ID,0); // 反向持仓魔法码置 0 注意 EVENT_PROP_MAGIC_BY_ID 这里硬写 0,意味着示例不区分 EA 魔法码,实盘里若多策略同跑,可能把别的策略反向仓误读进来。 继续把改向前后的订单类型与票号、以及改单前的开仓价/止损/止盈也记录进去,才能复盘一次翻转到底改了什么: event.SetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE,order.TypeOrder()); // 改向前的持仓订单类型 event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket()); // 改向前的持仓订单票号 event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder()); // 当前持仓订单类型 event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket()); // 当前持仓订单票号 event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PriceOpen()); // 改单前开仓价 event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLoss()); // 改单前止损 event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfit()); // 改单前止盈 开 MT5 接这段逻辑时,重点核对 PositionByID() 返回的对手仓是否真的存在——贵金属与外汇杠杆高,翻转瞬间仓位可能被强平,那时写进去的就会是 0,后续统计要单独过滤。

MQL5 / C++
event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket());                                                                              class=class="str">"cmt">// Event deal ticket
event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,this.m_type_first);                                                                              class=class="str">"cmt">// Type of the order that triggered an event deal(the last position order)
event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,this.m_type_first);                                                                            class=class="str">"cmt">// Type of an order that triggered a position deal(the first position order)
event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket());                                                                              class=class="str">"cmt">// Ticket of an order, based on which a deal event is opened(the last position order)
event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket());                                                                            class=class="str">"cmt">// Ticket of an order, based on which a position event is opened(the first position order)
event.SetProperty(EVENT_PROP_POSITION_ID,this.m_position_id);                                                                                  class=class="str">"cmt">// Position ID
event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID());                                                                             class=class="str">"cmt">// Opposite position ID
event.SetProperty(EVENT_PROP_MAGIC_BY_ID,class="num">0);                                                                                                   class=class="str">"cmt">// Opposite position magic number

event.SetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE,order.TypeOrder());                                                                           class=class="str">"cmt">// Position order type before direction changed
event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket());                                                                            class=class="str">"cmt">// Position order ticket before direction changed
event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder());                                                                          class=class="str">"cmt">// Current position order type
event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket());                                                                           class=class="str">"cmt">// Current position order ticket

event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PriceOpen());                                                                             class=class="str">"cmt">// Order price before modification
event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLoss());                                                                                class=class="str">"cmt">// StopLoss before modification
event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfit());                                                                              class=class="str">"cmt">// TakeProfit before modification

◍ 把成交上下文塞进自定义事件属性

在 MT5 里做订单流追踪时,光捕获交易事件不够,得把盘口与订单的关键字段一并挂到事件对象上,后续回放才不会丢上下文。下面这段把 Ask/Bid、魔术号、开平时间、挂单价和成交量拆分写进 event 属性,属于实战中必跑的一步。 注意 EVENT_PROP_VOLUME_ORDER_EXECUTED 用的是 order.Volume()-order.VolumeCurrent(),也就是已成交 = 初始请求量减去未成交剩余量;若部分成交,这个差值就是真实撮合手数,而不是写死的体积。 外汇与贵金属杠杆高、滑点随机,这类属性落地后也只是还原当时盘况,不代表任何收益指向,仅用于你本地 MT5 验证与参数校准。

MQL5 / C++
event.SetProperty(EVENT_PROP_PRICE_EVENT_ASK,this.m_tick.ask); class=class="str">"cmt">// Ask price during an event
event.SetProperty(EVENT_PROP_PRICE_EVENT_BID,this.m_tick.bid); class=class="str">"cmt">// Bid price during an event

event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// Order/deal/position magic number
event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpenMSC()); class=class="str">"cmt">// Time of an order, based on which a position deal is opened(the first position order)
event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// Event price
event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); class=class="str">"cmt">// Order/deal/position open price
event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// Order/deal/position close price
event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// StopLoss position price
event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// TakeProfit position price
event.SetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL,order.Volume()); class=class="str">"cmt">// Requested order volume
event.SetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED,order.Volume()-order.VolumeCurrent()); class=class="str">"cmt">// Executed order volume
event.SetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT,order.VolumeCurrent()); class=class="str">"cmt">// Remaining(unexecuted) order volume
event.SetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED,order.Volume()); class=class="str">"cmt">// Executed position volume
event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); class=class="str">"cmt">// Profit
event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// Order symbol
event.SetProperty(EVENT_PROP_SYMBOL_BY_ID,order.Symbol()); class=class="str">"cmt">// Opposite position symbol

「事件去重与有序插入的逻辑落点」

这段处理的核心,是把一笔新交易事件安全地挂进控制程序的事件列表,且不允许多次重复登记同一事件。 先给事件对象绑定当前图表 ID 并解码事件类型,随后用 IsPresentEventInList 判断该事件是否已在列表中。若不在,就走 InsertSort 做按序插入,紧接着发消息并把 m_trade_event 更新为这次的事件类型。 如果事件已经存在,程序不会覆盖,而是直接 delete 掉新生成的事件对象,并打印双语调试信息(俄语/英语)提示“事件已在列表中”。这种写法能避免 EA 在快速回测或实盘重入时把同一成交刷成多条信号。 外汇与贵金属市场跳空频繁,图表事件可能在一根棒内触发多次,用这种去重机制能降低小布类盯盘脚本的误报概率。

MQL5 / C++
   class=class="str">"cmt">//--- Set control program chart ID, decode the event code and set the event type
   event.SetChartID(this.m_chart_id);
   event.SetTypeEvent();
   class=class="str">"cmt">//--- Add the event object if it is not in the list
   if(!this.IsPresentEventInList(event))
     {
      this.m_list_events.InsertSort(event);
      class=class="str">"cmt">//--- Send a message about the event and set the value of the last trading event
      event.SendEvent();
      this.m_trade_event=event.TradeEvent();
     }
   class=class="str">"cmt">//--- If the event is already present in the list, remove a new event object and display a debugging message
   else
     {
      ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event is already in the list."));
      class="kw">delete event;
     }
   }
  }
class=class="str">"cmt">//--- New deal(__MQL5__)

用测试器验证事件捕获与挂单改色逻辑

编译 TestDoEasyPart10.mq4 后丢进策略测试器,手动开平仓、挂 BuyLimit/SellStop 并等其一触发,重点看日志里函数库‘见’到了哪些 MQL4 事件。实测中触发 BuyLimit #3 时,日志明确打印 [BuyLimit #3] 已激活 并生成 Buy #3 仓位——说明挂单激活事件已被捕获,且能回溯开仓来源订单。 但日志也暴露一个遗漏:尾随修改过的 BuyStop #1 挂单标签变红,而函数库其实看到了修改事件,只是颜色判断没跟上。所有针对 MQL4 交易函数的测试器适配都应写进 DELib.mqh,避免散落。 下面这段补了一个按挂单类型返买卖方向的 helper,并把箭头配色从‘查订单类型’改成‘查挂单方向’。外汇与贵金属杠杆高,测试器结论仅代表历史逻辑可跑通,实盘触发概率仍受滑点影响。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Modifying a pending order by ticket                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool PendingOrderModify(const class="type">ulong ticket,const class="type">class="kw">double price_set,const class="type">class="kw">double sl,const class="type">class="kw">double tp)
  {
   ResetLastError();
   if(!OrderSelect((class="type">int)ticket,SELECT_BY_TICKET))
     {
      Print(DFUN,TextByLanguage("Не удалось выбрать ордер. Ошибка ","Could not select order. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderType();
   if(type<ORDER_TYPE_BUY_LIMIT || type>ORDER_TYPE_SELL_STOP)
     {
      Print(DFUN,TextByLanguage("Ошибка. Не ордер: ","Error. Not order: "),PositionTypeDescription((class="type">ENUM_POSITION_TYPE)type)," #",ticket);
      class="kw">return false;
     }
   if(OrderCloseTime()>class="num">0)
     {
      Print(DFUN,TextByLanguage("Ошибка. Для модификации выбран удалённый ордер: ","Error. Deleted order selected for modification: "),OrderTypeDescription(type)," #",ticket);
      class="kw">return false;
     }
   class="type">class="kw">color clr=(TypeByPendingDirection(type)==ORDER_TYPE_BUY ? clrBlue : clrRed);
   ResetLastError();
   if(!OrderModify((class="type">int)ticket,price_set,sl,tp,class="num">0,clr))
     {
      Print(DFUN,TextByLanguage("Не удалось модифицировать ордер. Ошибка ","Failed to modify order. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the type by a pending order direction                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_ORDER_TYPE TypeByPendingDirection(const ENUM_ORDER_TYPE type)
  {
   if(type==ORDER_TYPE_BUY_LIMIT   || type==ORDER_TYPE_BUY_STOP)   class="kw">return ORDER_TYPE_BUY;
   if(type==ORDER_TYPE_SELL_LIMIT || type==ORDER_TYPE_SELL_STOP) class="kw">return ORDER_TYPE_SELL;
   class="kw">return WRONG_VALUE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把这条线请下神坛

当前这套跨平台函数库在 MQL5 侧已经能稳定跟踪挂单的下发与删除,MQL4 下跑同一套逻辑时仍要留意事件回传的细微差别,比如部分老终端对挂单状态变更的推送节奏不一致。附带的 MQL5.zip 与 MQL4.zip 各 99.2 KB,里面是完整函数库和测试 EA,直接拖进 MT5 的 MQL5/Include 与 Experts 目录就能编译验证。 下一篇原计划接跟踪平仓并修 MQL4 的尾随事件bug,但社区里 fxsaber 早已用另一套实现覆盖了同类需求,这条自研线也就不必当标配供着。真要落地,建议先拿 ZIP 里的测试 EA 在策略测试器跑 EURUSD 十五分钟周期,看挂单跟踪是否漏事件,再决定要不要自己补平仓分支。 外汇与贵金属杠杆高,函数库只解决事件结构问题,不替你管仓位风险,验证时用小点值品种试水更稳妥。

把跨终端编译检查交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 MT4/MT5 兼容性提示,把重复劳动交给小布,你专注决策。

常见问题

MQL4 标准库没有 CTrade 类,需要用条件编译指令 #ifdef __MQL5__ 把相关实例隔离,MT4 分支另写旧版调用。
当传入 0 作为图表 ID 且缺省后两个参数时,MT4 无法判定调用新版还是过时版,显式补全参数或加重载区分即可。
事件源不同但可抽象成统一回调,本篇用兼容层把两者映射后,EA 逻辑层无需感知终端差异。
可以,小布内置的 AIGC 会标记条件编译遗漏点和交易类缺失,并给出对应指令片段参考。
上篇引入的集合类型在本篇事件处理中直接复用,用来比对开仓前后仓位状态变化以触发自定义事件。