轻松快捷开发 MetaTrader 程序的函数库(第十部分):与 MQL4 的兼容性 - 开仓和激活挂单的事件·综合运用
(3/3)·从 34 个编译错误到跨终端事件统一,手把手拆掉 MT4/MT5 代码鸿沟的最后砖块
- 回测与实盘下的定时器分叉处理
- 让库能识别挂单删除与MQL4开仓
- 订单对象构造时的整型属性落盘
- 订单历史快照的字段落盘
- 从订单状态反查持仓 ID 的写法
- 引擎定时器如何分流实盘与回测的事件采集
- 事件触发与三类订单列表的取数口子
- 从订单事件里抠出增量数据
- 交易事件的分发与刷新逻辑
- 事件集合类的筛选与刷新接口
- 账户事件收集类的私有成员布局
- 拆账户事件与持仓筛选的接口层
- 从混合列表里抠出市价持仓
- 区分挂单与持仓的事件回调
- 从列表尾部抓取最新N笔成交事件
- 从成交流水里抓新开仓事件
- 给持仓事件挂上可追溯的票据属性
- 把成交上下文塞进自定义事件属性
- 事件去重与有序插入的逻辑落点
- 用测试器验证事件捕获与挂单改色逻辑
- 把这条线请下神坛
「回测与实盘下的定时器分叉处理」
在 MT5 的 CEngine 初始化里,毫秒级定时器靠 EventSetMillisecondTimer(TIMER_FREQUENCY) 拉起。MQL4 分支不判断环境直接建;MQL5 分支则加了 !this.IsTester() guard,只在非回测实盘才建定时器,避免测试环境下无谓占用。 OnTimer 是定时器回调入口。先按 COLLECTION_COUNTER_ID 取计数器索引,index>WRONG_VALUE 才继续,否则直接跳过。 实盘时若计数器 IsTimeDone() 为真,才调用 TradeEventsControl() 处理历史订单、成交、持仓等集合事件;回测中则跳过时间判断,每个 tick 都跑 TradeEventsControl()。这意味着同段逻辑在策略测试器里事件触发频率显著高于实盘,验证 EA 时需注意两者行为差。 外汇与贵金属波动剧烈,这类定时器差异可能影响信号滞后,上线前务必在 MT5 实盘模拟账户核对频率。
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 策略测试器用历史数据验证,实盘存在滑点与被跳过风险,事件捕获可能延迟。
} 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 在部分编译告警下可能越界,这种写法更稳。
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 打印出来,就能验证你的点值计算和券商佣金口径是否一致。外汇与贵金属杠杆高,复盘结论只代表历史样本,实盘仍可能偏离。
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 串错可能误平不该平的仓,先在策略测试器用历史数据跑一遍再上真金。
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,若列表非空则记录变动条数,这一步是后续推事件给收集器的基数。外汇与贵金属波动快,这类采集若卡顿可能漏掉滑点较大的瞬时成交,属高风险场景。
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 不被改动。
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,回测里重复下单率可能降一半。
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(),很多自行改写的引擎省掉这步,在账户无变更时直接崩在零指针上。
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 在原文中被高亮,说明它是控制事件分离的关键载体。
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 方法,靠订单指针加历史/市场两个数组来生成新事件对象。
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 则用来去重:同一笔成交推送可能触发多次事件对象入列,靠它拦掉重复处理。
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 占用,但高频重算仍可能漏掉极端跳空瞬间的状态。
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 数据跑一遍确认只触发一次。
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,能直观看到每次回调只跑末尾几笔。
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 用这些标记跑一遍回测,你能直接看出自己策略里有多少仓是挂单触发的。
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,后续统计要单独过滤。
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 验证与参数校准。
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 在快速回测或实盘重入时把同一成交刷成多条信号。 外汇与贵金属市场跳空频繁,图表事件可能在一根棒内触发多次,用这种去重机制能降低小布类盯盘脚本的误报概率。
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,并把箭头配色从‘查订单类型’改成‘查挂单方向’。外汇与贵金属杠杆高,测试器结论仅代表历史逻辑可跑通,实盘触发概率仍受滑点影响。
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 十五分钟周期,看挂单跟踪是否漏事件,再决定要不要自己补平仓分支。 外汇与贵金属杠杆高,函数库只解决事件结构问题,不替你管仓位风险,验证时用小点值品种试水更稳妥。