轻松快捷开发 MetaTrader 程序的函数库(第十一部分)。 与 MQL4 的兼容性 - 平仓事件·综合运用
(3/3)·从删冗余属性到实现平仓事件,把 MQL5 函数库最后一块 MQL4 拼图补齐
- 持仓与订单属性落库的逻辑差异
- 从订单状态反查对冲单与开仓时间
- 订单时间字段的跨状态取法
- 订单状态与整型属性的双语描述逻辑
- 订单属性本地化展示的嵌套三元写法
- 订单时间属性的毫秒级文本化
- 挂单与成交单的字段描述差异
- 订单注释与类型的文本化拼装
- 挂单与持仓归类的代码落点
- 把订单塞进控单列表并按时间捞历史
- 返回当前持仓与挂单的合并列表
- MT4 里区分平仓与删挂单的坑
- 账户事件集合的刷新与筛选接口
- 账户事件容器的接口与刷新骨架
- 交易事件类的内部状态与分拣接口
- 从持仓事件集合里抠出历史单与控制单
- 从持仓 ticket 反查开仓订单类型
- 抓新增挂单与仓位的事件分发逻辑
- 从持仓缩减里抓平仓事件
- 账户历史事件里的挂单撤销与成交捕获
- MQL5里按成交增量回捞尾部事件
- 反方向平仓时的事件标记与残仓计算
- 平仓事件对象的属性注入细节
- 把持仓上下文塞进事件对象
- 给交易事件对象灌入成交细节
- 从持仓订单堆里捞最后那笔 Close By
- 把挂单删除改成按挂单时间排序
- 跨平台库的下一批对象与毫秒时间坑
- 画得少,看得清
「持仓与订单属性落库的逻辑差异」
在封装订单对象时,把长整型属性一次性写进 m_long_prop 数组是常规做法。上面这段代码把盈利点数、来源 ticket、目标 ticket、是否被 SL/TP 平掉、分组 ID 全部固化到对象内部,其中 ORDER_PROP_GROUP_ID 直接赋 0,说明分组逻辑留待外部处理。 双精度与字符串属性分开存:m_double_prop 里只放 ProfitFull() 的完整盈亏,m_string_prop 的扩展注释先置空串,避免构造时做无谓字符串分配。 OrderPositionID() 的跨版本实现值得注意:MQL4 下只有市价持仓才返回 Ticket,其余一律 0;MQL5 则按订单状态分流,市价持仓取 PositionGetInteger(POSITION_IDENTIFIER),挂单或活跃订单取 ORDER_POSITION_ID,历史类取 HistoryOrderGetInteger,成交记录取 DEAL_POSITION_ID。这种分流在回测与实盘切换时可能影响关联统计,开 MT5 跑一段 EURUSD 历史,对比 ORDER_POSITION_ID 与 POSITION_IDENTIFIER 的一致性就能验证。 外汇与贵金属杠杆高,持仓 ID 错配会导致平仓信号误触发,验证前先用模拟账户跑通。
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(); class=class="str">"cmt">//--- Save additional class="type">class="kw">string properties this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT_EXT)] = ""; } class="type">long COrder::OrderPositionID(class="type">void) const { class="macro">#ifdef __MQL4__ class="kw">return(this.Status()==ORDER_STATUS_MARKET_POSITION ? this.Ticket() : class="num">0); class="macro">#else class="type">long id=class="num">0; class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_MARKET_POSITION : id=::PositionGetInteger(POSITION_IDENTIFIER); break; case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : id=::OrderGetInteger(ORDER_POSITION_ID); break; case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : id=::HistoryOrderGetInteger(m_ticket,ORDER_POSITION_ID); break; case ORDER_STATUS_DEAL : id=::HistoryDealGetInteger(m_ticket,DEAL_POSITION_ID); break;
◍ 从订单状态反查对冲单与开仓时间
在跨 MQL4/MQL5 的订单封装里,想拿到「这笔订单对冲掉了哪张仓位」,得先判断订单状态再取属性。MQL5 环境下用 ORDER_POSITION_BY_ID 这个属性,市场单和挂单走 OrderGetInteger,已成交历史单走 HistoryOrderGetInteger,其余状态直接返回 0。 具体看 switch 分支:ORDER_STATUS_MARKET_ORDER 与 MARKET_PENDING 对应在途订单,ORDER_STATUS_HISTORY_PENDING 与 HISTORY_ORDER 对应历史池,default 分支 ticket 强制为 0 避免脏值。 开仓时间也按状态分流:持仓用 PositionGetInteger(POSITION_TIME) 强转 datetime,市场单与历史单再各自走 OrderGetInteger / HistoryOrderGetInteger。实盘里若你写 EA 需要对冲记录做统计,直接复用这套状态机即可,外汇与贵金属杠杆高,回测前先在小周期验证字段非空。
class="type">long COrder::OrderPositionByID(class="type">void) const { class="type">long ticket=class="num">0; class="macro">#ifdef __MQL4__ class="type">class="kw">string order_comment=::OrderComment(); if(::StringFind(order_comment,"close hedge by #")>WRONG_VALUE) ticket=::StringToInteger(::StringSubstr(order_comment,class="num">16)); class="macro">#else class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : ticket=::OrderGetInteger(ORDER_POSITION_BY_ID); break; case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : ticket=::HistoryOrderGetInteger(m_ticket,ORDER_POSITION_BY_ID); break; class="kw">default : ticket=class="num">0; break; } class="macro">#endif class="kw">return ticket; } class="type">class="kw">datetime COrder::OrderOpenTime(class="type">void) const { class="macro">#ifdef __MQL4__ class="kw">return ::OrderOpenTime(); class="macro">#else class="type">class="kw">datetime res=class="num">0; class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_MARKET_POSITION : res=(class="type">class="kw">datetime)::PositionGetInteger(POSITION_TIME); break; case ORDER_STATUS_MARKET_ORDER :
订单时间字段的跨状态取法
在 MQL5 里,一笔订单挂在那儿还是已经成交,取「建立时间」和「关闭时间」要走的接口完全不同。市价挂单用 OrderGetInteger(ORDER_TIME_SETUP),历史挂单与历史订单走 HistoryOrderGetInteger(ticket, ORDER_TIME_SETUP),已成交易(deal)则调 HistoryDealGetInteger(ticket, DEAL_TIME)。 关闭时间更挑状态:只有历史挂单、历史订单才能取 ORDER_TIME_DONE,deal 取 DEAL_TIME,其余情况返回 0。写 EA 时若不分状态直接调,很容易拿到 1970 年零点这种脏值。 下面这段是 OrderCloseTime 的 MQL5 分支实现,注意 default 分支直接给 0,意味着活跃中的市价单在这里拿不到关闭时间——这是符合预期的。 别把正态当圣经 MT5 历史池只在终端开启期间缓存,重启后首次调 HistoryOrderGetInteger 可能返回 0,建议在 OnTick 里先跑一次 HistorySelect 再取时间字段。
class="type">class="kw">datetime COrder::OrderCloseTime(class="type">void) const { class="macro">#ifdef __MQL4__ class="kw">return ::OrderCloseTime(); class="macro">#else class="type">class="kw">datetime res=class="num">0; class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=(class="type">class="kw">datetime)::HistoryOrderGetInteger(m_ticket,ORDER_TIME_DONE); break; case ORDER_STATUS_DEAL : res=(class="type">class="kw">datetime)::HistoryDealGetInteger(m_ticket,DEAL_TIME); break; class="kw">default : res=class="num">0; break; } class="kw">return res; class="macro">#endif }
「订单状态与整型属性的双语描述逻辑」
在 MT5 的 COrder 封装里,订单状态常需要同时给俄语和英语界面输出可读文本。下面这段三元表达式链用 TextByLanguage 做了状态映射:余额操作、市价单、历史单、成交、持仓、活跃挂单、历史挂单各自对应不同字符串,兜底则返回 EnumToString(status)。 MQL5 与 MQL4 的差别在这里直接体现:__MQL5__ 宏下,市价单和历史单会进一步细分,比如 ORDER_TYPE_CLOSE_BY 被单独标成“Order for closing by”,而 MQL4 分支只把历史单统称“History order”。如果你在 MT4/MT5 双平台编译同一套代码,这个 #ifdef 块就是必须保留的兼容点。 整型属性描述函数 GetPropertyDescription 则把 MAGIC、TICKET、TICKET_FROM 等字段转成“Magic number: 12345”这种带冒号的句子。注意它先调 SupportProperty 判断该属性在当前订单类型上是否可用,不支持就追加“Property not supported”,避免直接读取值抛错。 开 MT5 新建 EA,把下面代码贴进自定义 COrder 类,编译后用一个真实账户的历史订单打印 GetPropertyDescription(ORDER_PROP_TICKET),能立刻看到“Ticket #xxxxxxx”的输出格式。外汇与贵金属杠杆交易高风险,代码仅用于接口验证,不代表任何方向判断。
status==ORDER_STATUS_BALANCE ? TextByLanguage("Балансовая операция","Balance operation") : class="macro">#ifdef __MQL5__ status==ORDER_STATUS_MARKET_ORDER || status==ORDER_STATUS_HISTORY_ORDER ? ( type==ORDER_TYPE_CLOSE_BY ? TextByLanguage("Закрывающий ордер","Order for closing by") : TextByLanguage("Ордер на ","The order to ")+(type==ORDER_TYPE_BUY ? TextByLanguage("покупку","buy") : TextByLanguage("продажу","sell")) ) : class="macro">#else status==ORDER_STATUS_HISTORY_ORDER ? TextByLanguage("Исторический ордер","History order") : class="macro">#endif status==ORDER_STATUS_DEAL ? TextByLanguage("Сделка","Deal") : status==ORDER_STATUS_MARKET_POSITION ? TextByLanguage("Позиция","Active position") : status==ORDER_STATUS_MARKET_PENDING ? TextByLanguage("Установленный отложенный ордер","Active pending order") : status==ORDER_STATUS_HISTORY_PENDING ? TextByLanguage("Отложенный ордер","Pending order") : EnumToString(status) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return description of an order&class="macro">#x27;s integer class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string COrder::GetPropertyDescription(ENUM_ORDER_PROP_INTEGER class="kw">property) { class="kw">return ( class=class="str">"cmt">//--- General properties class="kw">property==ORDER_PROP_MAGIC ? TextByLanguage("Магик","Magic number")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+ (class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_TICKET ? TextByLanguage("Тикет","Ticket")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : " #"+ (class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_TICKET_FROM ? TextByLanguage("Тикет родительского ордера","Ticket of parent order")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
◍ 订单属性本地化展示的嵌套三元写法
在 MT5 自定义订单类里,把枚举属性转成可读文本通常用一长串嵌套三元运算符。下面这段覆盖了从继承订单票据号、过期时间到成交方向等字段,俄语与英语双语文案由 TextByLanguage 按终端语言切换。
| 关键点在 ORDER_PROP_TIME_EXP:当 GetProperty 返回 0 时判定为未设置过期,否则用 TimeToString 配合 TIME_DATE | TIME_MINUTES | TIME_SECONDS 输出到秒。外汇与贵金属挂单的过期字段若 broker 不支持,SupportProperty 会返回 false,此时文本追加 ': Property not supported'。 |
|---|
复制以下片段到你的 order 描述方法里,开 MT5 用一款带过期属性的限价单测试,能直接看到 ': Not set' 与具体时间两种分支。
class="kw">property==ORDER_PROP_TICKET_TO ? TextByLanguage("Тикет наследуемого ордера","Inherited order ticket")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : " #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_TIME_EXP ? TextByLanguage("Дата экспирации","Date of expiration")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : (this.GetProperty(class="kw">property)==class="num">0 ? TextByLanguage(": Не задана",": Not set") : ": "+::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)) ) : class="kw">property==ORDER_PROP_TYPE ? TextByLanguage("Тип","Type")+": "+this.TypeDescription() : class="kw">property==ORDER_PROP_DIRECTION ? TextByLanguage("Тип по направлению","Type by direction")+": "+this.DirectionDescription() : class="kw">property==ORDER_PROP_REASON ? TextByLanguage("Причина","Reason")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+this.GetReasonDescription(this.GetProperty(class="kw">property)) ) : class="kw">property==ORDER_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position identifier")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_DEAL_ORDER_TICKET ? TextByLanguage("Сделка на основании ордера с тикетом","Deal by order ticket")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_DEAL_ENTRY ? TextByLanguage("Направление сделки","Deal entry")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
订单时间属性的毫秒级文本化
在封装订单对象的描述方法时,时间类属性需要单独处理。ORDER_PROP_TIME_OPEN、ORDER_PROP_TIME_CLOSE、ORDER_PROP_TIME_UPDATE 三者都走毫秒时间戳,但 UPDATE 允许为 0,代表持仓从未被修改过。 如果对应属性不被当前订单类型支持,代码统一返回「Property not supported」类提示,避免空值崩溃。实际在 MT5 里跑,未成交挂单的 TIME_UPDATE 大概率返回 0。 下面这段三元嵌套是直接从类方法里摘出的核心分支,注意 TimeMSCtoString 负责把毫秒转可读时间,外层再拼上原始数值方便核对。外汇与贵金属杠杆高,回测时时间字段错位可能导致信号误判,建议开 MT5 用脚本打印几笔真实持仓验证。
class="kw">property==ORDER_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position identifier")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_TIME_OPEN ? TextByLanguage("Время открытия в милисекундах","Opening time in milliseconds")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+TimeMSCtoString(this.GetProperty(class="kw">property))+" ("+(class="type">class="kw">string)this.GetProperty(class="kw">property)+")" ) : class="kw">property==ORDER_PROP_TIME_CLOSE ? TextByLanguage("Время закрытия в милисекундах","Closing time in milliseconds")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+TimeMSCtoString(this.GetProperty(class="kw">property))+" ("+(class="type">class="kw">string)this.GetProperty(class="kw">property)+")" ) : class="kw">property==ORDER_PROP_TIME_UPDATE ? TextByLanguage("Время изменения позиции в милисекундах","Time to change the position in milliseconds")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+(this.GetProperty(class="kw">property)!=class="num">0 ? TimeMSCtoString(this.GetProperty(class="kw">property))+" ("+(class="type">class="kw">string)this.GetProperty(class="kw">property)+")" : "class="num">0") ) : class="kw">property==ORDER_PROP_STATE ? TextByLanguage("Состояние","Statе")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": \""+this.StateDescription()+"\"" ) : class="kw">property==ORDER_PROP_STATUS ? TextByLanguage("Статус","Status")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": \""+this.StatusDescription()+"\"" ) :
「挂单与成交单的字段描述差异」
在封装订单对象的 COrder 类里,字符串类属性的可读化输出靠 GetPropertyDescription 做分支映射。市场挂单(ORDER_STATUS_MARKET_PENDING)状态下,距离字段显示的是「Distance from price in points」,一旦成交就切换成「Profit in points」,这个切换直接由 Status() 决定。 下面这段是该类方法的核心三元嵌套,注意 SupportProperty 先拦一道——经纪商不支持的属性会回「Property not supported」,避免 MT5 上读黄金 XAUUSD 某些字段时直接抛错。 ORDER_PROP_CLOSE_BY_SL 与 ORDER_PROP_CLOSE_BY_TP 不返回数值,而是把 bool 转成 Yes/No,方便面板直接读「是否由止损/止盈触发平仓」。GROUP_ID 则原样转 string 拼接,用于多单分组管理。 开 MT5 自建 EA 时,把这段直接塞进你的订单类,能省掉一半日志格式化的脏活;外汇与贵金属杠杆高,字段不支持时宁可打不支持也别硬读。
this.Status()==ORDER_STATUS_MARKET_PENDING ? TextByLanguage("Дистанция от цены в пунктах","Distance from price in points") : TextByLanguage("Прибыль в пунктах","Profit in points") )+ (! this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+( class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==ORDER_PROP_CLOSE_BY_SL ? TextByLanguage("Закрытие по StopLoss","Close by StopLoss")+ (! this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+( this.GetProperty(class="kw">property) ? TextByLanguage("Да","Yes") : TextByLanguage("Нет","No")) ) : class="kw">property==ORDER_PROP_CLOSE_BY_TP ? TextByLanguage("Закрытие по TakeProfit","Close by TakeProfit")+ (! this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+( this.GetProperty(class="kw">property) ? TextByLanguage("Да","Yes") : TextByLanguage("Нет","No")) ) : class="kw">property==ORDER_PROP_GROUP_ID ? TextByLanguage("Идентификатор группы","Group identifier")+ (! this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : ": "+( class="type">class="kw">string)this.GetProperty(class="kw">property) ) : "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return description of the order&class="macro">#x27;s class="type">class="kw">string class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string COrder::GetPropertyDescription(ENUM_ORDER_PROP_STRING class="kw">property) { class="kw">return ( class="kw">property==ORDER_PROP_SYMBOL ? TextByLanguage("Символ","Symbol")+": \""+this.GetProperty(class="kw">property)+"\""
◍ 订单注释与类型的文本化拼装
在封装订单对象展示逻辑时,常需要把内部属性转成交易员能读的文本。下面这段三元嵌套针对 ORDER_PROP_COMMENT、ORDER_PROP_COMMENT_EXT、ORDER_PROP_EXT_ID 三类属性做本地化拼接:若属性为空则回显“Not set”,否则把取值包在引号里输出。 ORDER_PROP_EXT_ID 还多一层判断——先查 SupportProperty(),不支持就直接返回“Property not supported”,避免 MT5 某些账户类型下硬读交易所 ID 导致空串误判。 另一个函数是 OrderTypeDescription(),用 as_order 参数区分“市价单”语境与“持仓”语境:MQL5 编译下固定返回 "Market order",而 MQL4 分支里会根据 as_order 在 "Market order" 与 "Position" 之间切换。 挂单类型映射很直白,BUY_LIMIT/BUY_STOP/SELL_LIMIT/SELL_STOP 直接回字面量。你在写自定义订单面板时,可照搬这套文本映射,省掉重复写多语言判断的功夫。
class="kw">property==ORDER_PROP_COMMENT ? TextByLanguage("Комментарий","Comment")+ (this.GetProperty(class="kw">property)=="" ? TextByLanguage(": Отсутствует",": Not set"):": \""+this.GetProperty(class="kw">property)+"\"") : class="kw">property==ORDER_PROP_COMMENT_EXT ? TextByLanguage("Пользовательский комментарий","Custom comment")+ (this.GetProperty(class="kw">property)=="" ? TextByLanguage(": Не задан",": Not set"):": \""+this.GetProperty(class="kw">property)+"\"") : class="kw">property==ORDER_PROP_EXT_ID ? TextByLanguage("Идентификатор на бирже","Exchange identifier")+ (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") : (this.GetProperty(class="kw">property)=="" ? TextByLanguage(": Отсутствует",": Not set"):": \""+this.GetProperty(class="kw">property)+"\"")): "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return order name | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string OrderTypeDescription(const ENUM_ORDER_TYPE type,class="type">bool as_order=true) { class="type">class="kw">string pref=(class="macro">#ifdef __MQL5__ "Market order" class="macro">#else (as_order ? "Market order" : "Position") class="macro">#endif ); class="kw">return ( type==ORDER_TYPE_BUY_LIMIT ? "Buy Limit" : type==ORDER_TYPE_BUY_STOP ? "Buy Stop" : type==ORDER_TYPE_SELL_LIMIT ? "Sell Limit" : type==ORDER_TYPE_SELL_STOP ? "Sell Stop" : class="macro">#ifdef __MQL5__
挂单与持仓归类的代码落点
这段逻辑解决一个很实际的问题:MT5 里订单类型从 Buy Stop Limit 到 Balance、Credit 一大堆,面板要显示成人类能读的字串,就得先做一次类型映射。 上面那段三元表达式把 ORDER_TYPE_BUY_STOP_LIMIT 映射成 "Buy Stop Limit",SELL_STOP_LIMIT 同理;旧版终端(#else 分支)才处理 BALANCE / CREDIT 这类历史操作类型,新 build 已废弃,所以编译时靠宏隔离。 真正把订单塞进集合的是 AddToListMarket()。它先做空指针拦截,再按 ORDER_STATUS_MARKET_POSITION 和 ORDER_STATUS_MARKET_PENDING 分流:持仓累加 time_update 哈希与成交量、position 计数 +1;挂单只加哈希与成交量、pending 计数 +1。 InsertSort 失败时会 Print 出错票据并 delete 对象,避免野指针。你在 MT5 里接这套,只需盯 total_positions 与 total_pending 两个字段,就能在每次 tick 后立刻知道账户敞口结构变化。外汇与贵金属杠杆高,这类统计仅作风险监控参考,不预示方向。
type==ORDER_TYPE_BUY_STOP_LIMIT ? "Buy Stop Limit" : type==ORDER_TYPE_SELL_STOP_LIMIT ? "Sell Stop Limit" : type==ORDER_TYPE_CLOSE_BY ? TextByLanguage("Закрывающий ордер","Order for closing by") : class="macro">#else type==ORDER_TYPE_BALANCE ? TextByLanguage("Балансовая операция","Balance operation") : type==ORDER_TYPE_CREDIT ? TextByLanguage("Кредитная операция","Credit operation") : class="macro">#endif type==ORDER_TYPE_BUY ? pref+" Buy" : type==ORDER_TYPE_SELL ? pref+" Sell" : TextByLanguage("Неизвестный тип ордера","Unknown order type") ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+--------------------------------------------------------------------------------+ class=class="str">"cmt">//| Add an order or a position to the list of orders and positions on the account | class=class="str">"cmt">//+--------------------------------------------------------------------------------+ class="type">bool CMarketCollection::AddToListMarket(COrder *order) { if(order==NULL) class="kw">return false; ENUM_ORDER_STATUS status=order.Status(); if(this.m_list_all_orders.InsertSort(order)) { if(status==ORDER_STATUS_MARKET_POSITION) { this.m_struct_curr_market.hash_sum_acc+=order.GetProperty(ORDER_PROP_TIME_UPDATE)+this.ConvertToHS(order); this.m_struct_curr_market.total_volumes+=order.Volume(); this.m_struct_curr_market.total_positions++; class="kw">return true; } if(status==ORDER_STATUS_MARKET_PENDING) { this.m_struct_curr_market.hash_sum_acc+=this.ConvertToHS(order); this.m_struct_curr_market.total_volumes+=order.Volume(); this.m_struct_curr_market.total_pending++; class="kw">return true; } } else { ::Print(DFUN,order.TypeDescription()," #",order.Ticket()," ",TextByLanguage("не удалось добавить в список","failed to add to the list")); class="kw">delete order; }
「把订单塞进控单列表并按时间捞历史」
控单对象入库前要先做空指针拦截:传入的 COrder 若为 NULL 直接返回 false,避免后续调用 PositionID() 这类方法时崩在 MT5 运行时。 新建 COrderControl 时把持仓 ID、ticket、magic、交易品种四个字段一次性灌进去,随后用 order.TimeOpen() 连续三次写进 SetTime 与 SetTimePrev——这里代码里重复调了两次 SetTime,实盘复制时建议删掉冗余行,否则只是覆盖同一个成员。 历史订单按时间区间抽取时,SELECT_BY_TIME_CLOSE 与 SELECT_BY_TIME_OPEN 决定用 ORDER_PROP_TIME_CLOSE 还是 ORDER_PROP_TIME_OPEN 做二分查找的键。begin_time 大于 end_time 会被强制归零,end_time 传 0 则落到 END_TIME 常量,回测脚本里若想取全量历史别误传反了区间。 ListStorage.Add(list) 之后 FreeMode(false) 保证临时数组不被自动析构,跑多品种扫描时这块若设成 true,可能在 OnDeinit 前就把指针释放掉,引发随机性访问违规。
class="type">bool CMarketCollection::AddToListControl(COrder *order) { if(order==NULL) class="kw">return false; COrderControl* order_control=new COrderControl(order.PositionID(),order.Ticket(),order.Magic(),order.Symbol()); if(order_control==NULL) class="kw">return false; order_control.SetTime(order.TimeOpen()); order_control.SetTimePrev(order.TimeOpen()); order_control.SetVolume(order.Volume()); order_control.SetTime(order.TimeOpen()); order_control.SetTypeOrder(order.TypeOrder()); order_control.SetTypeOrderPrev(order.TypeOrder()); order_control.SetPrice(order.PriceOpen()); order_control.SetPricePrev(order.PriceOpen()); order_control.SetStopLoss(order.StopLoss()); order_control.SetStopLossPrev(order.StopLoss()); order_control.SetTakeProfit(order.TakeProfit()); order_control.SetTakeProfitPrev(order.TakeProfit()); if(!this.m_list_control.Add(order_control)) { class="kw">delete order_control; class="kw">return false; } class="kw">return true; } CArrayObj *CHistoryCollection::GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE) { ENUM_ORDER_PROP_INTEGER class="kw">property=(select_time_mode==SELECT_BY_TIME_CLOSE ? ORDER_PROP_TIME_CLOSE : ORDER_PROP_TIME_OPEN); CArrayObj *list=new CArrayObj(); if(list==NULL) { ::Print(DFUN+TextByLanguage("Ошибка создания временного списка","Error creating temporary list")); class="kw">return NULL; } class="type">class="kw">datetime begin=begin_time,end=(end_time==class="num">0 ? END_TIME : end_time); if(begin_time>end_time) begin=class="num">0; list.FreeMode(false); ListStorage.Add(list); this.m_order_instance.SetProperty(class="kw">property,begin); class="type">int index_begin=this.m_list_all_orders.SearchGreatOrEqual(&m_order_instance); if(index_begin==WRONG_VALUE) class="kw">return list; this.m_order_instance.SetProperty(class="kw">property,end); class="type">int index_end=this.m_list_all_orders.SearchLessOrEqual(&m_order_instance); if(index_end==WRONG_VALUE) class="kw">return list; for(class="type">int i=index_begin; i<=index_end; i++)
◍ 返回当前持仓与挂单的合并列表
这段片段是某个订单管理类方法的收尾部分,作用是把遍历得到的有效订单指针塞进一个动态数组再抛出给调用方。 代码先通过 list.Add(this.m_list_all_orders.At(i)) 把第 i 个订单对象地址追加进 list;循环结束后用 return list 把整合好的清单返回。调用方拿到后可直接遍历做批量平仓或风控扫描。 在 MT5 实盘里,m_list_all_orders 通常缓存了 PositionSelectByTicket 与 OrderSelect 两类结果,合并返回能少写一遍筛选逻辑,但需注意 list 生命周期由调用者释放,避免重复 Add 造成野指针。
list.Add(this.m_list_all_orders.At(i)); class="kw">return list; }
MT4 里区分平仓与删挂单的坑
在 MT4 里想精准识别平仓或删挂单事件,比 MT5 棘手得多。MT4 把持仓和挂单都叫“订单”,部分平仓和删除挂单在账户数据上表现一致:历史订单数增加、总交易量减少、在场订单数不变。实测在策略测试器里开仓→部分平仓→挂单→删挂单,日志会同时吐出“部分平仓”和“挂单删除”两条消息,说明旧逻辑把两类事件混为一谈了。 破局的线索是盯住在场挂单数量。部分平仓时挂单总数不动,删挂单时它会变。所以判断部分平仓要额外比对在场挂单变化:没变就是部分平仓,变了才是删挂单。但这就给 MT4 埋了限制——同一根 K 线里既不能删挂单又做部分平仓,否则事件判定逻辑直接打架。 当前函数库用“在场挂单数量控制”绕开这个坑,终端用户无感,调用封装函数即可。要实现部分平仓识别,得把账户总交易量变化值传进 CEventsCollection::Refresh()。下面这段是 CEngine::TradeEventsControl 里补传控制订单列表的调用点,注意高亮行把 GetListControl() 也喂给了 Refresh。 [CODE] 里只展示了事件控制主框架,真正区分平仓的改动在 Refresh 内部:用控制订单列表比对票证,GetOrderControlByTicket() 按持仓票证抓控制订单,GetTypeFirst() 借它返类型,删掉原先的搜索循环。MQL4 没有 ORDER_TYPE_CLOSE_BY,改抓逆向仓位 ID 非 0 的订单来列平仓单。 外汇与贵金属交易本身高风险,MT4 这套事件限制不影响下单成交,只牵动 EA 逻辑判定,复盘时容易误读自己日志。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Check trading events |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);">CEngine::TradeEventsControl</span>(<span class="keyword">class="type">void</span>) { <span class="comment">class=class="str">"cmt">//--- Initialize the trading events code and flags</span> <span class="keyword">this</span>.m_is_market_trade_event=<span class="macro">false</span>; <span class="keyword">this</span>.m_is_history_trade_event=<span class="macro">false</span>; <span class="comment">class=class="str">"cmt">//--- Update the lists </span> <span class="keyword">this</span>.m_market.Refresh(); <span class="keyword">this</span>.m_history.Refresh(); <span class="comment">class=class="str">"cmt">//--- First launch actions</span> <span class="keyword">if</span>(<span class="keyword">this</span>.IsFirstStart()) { <span class="keyword">this</span>.m_acc_trade_event=TRADE_EVENT_NO_EVENT; <span class="keyword">class="kw">return</span>; } <span class="comment">class=class="str">"cmt">//--- Check the changes in the market status and account history </span> <span class="keyword">this</span>.m_is_market_trade_event=<span class="keyword">this</span>.m_market.IsTradeEvent(); <span class="keyword">this</span>.m_is_history_trade_event=<span class="keyword">this</span>.m_history.IsTradeEvent(); <span class="comment">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</span> <span class="keyword">class="type">int</span> change_total=<span class="number">class="num">0</span>; CArrayObj* list_changes=<span class="keyword">this</span>.m_market.GetListChanges(); <span class="keyword">if</span>(list_changes!=<span class="macro">NULL</span>) change_total=list_changes.Total(); <span class="keyword">if</span>(<span class="keyword">this</span>.m_is_history_trade_event || <span class="keyword">this</span>.m_is_market_trade_event || change_total><span class="number">class="num">0</span>) { <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">this</span>.m_events.Refresh</span>(<span class="keyword">this</span>.m_history.GetList(),<span class="keyword">this</span>.m_market.GetList(),list_changes,<span class="keyword">this</span>.m_market.GetListControl(),
「账户事件集合的刷新与筛选接口」
在交易事件管理器里,Refresh 方法负责把历史、市场、变更、控制四类事件数组合并刷新,同时记录最近一次账户交易事件。代码里 this.m_acc_trade_event=this.m_events.GetLastTradeEvent(); 这行直接抓取事件池中的末位交易动作,供后续逻辑判断持仓异动。
对外暴露的查询接口分三层:按时间区间取子集的 GetListByTime(begin_time, end_time),默认参数均为 0 即不限制;直接返回完整事件池的 GetList(void);以及按整型、双精度、字符串属性做等值或比较筛选的重载 GetList(...)。三者都返回 CArrayObj* 指针,调用方拿到的就是同一批事件对象的不同视图。
注意 GetList 的属性筛选重载默认比较模式是 EQUAL,若你想抓「成交量大于某值」的市场事件,需显式传 ENUM_COMPARER_TYPE 的其它模式。外汇与贵金属行情下这类事件回调可能高频触发,实盘使用前建议在 MT5 策略测试器里用历史 tick 跑一遍确认对象生命周期无泄漏。
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(), this.m_market.ChangedVolumeValue()); 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 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,
◍ 账户事件容器的接口与刷新骨架
在自研 EA 里把账户事件统一收口,先得有一个继承自 CListObj 的容器类。下面这段声明给出了它对外暴露的几个关键方法:绑定图表 ID、读取并清空最近一次交易事件。 SetChartID() 只做一件事——把控制程序所在的图表句柄写进成员变量 m_chart_id,后续弹窗或画线都限定在这个图表上,避免多图表 EA 互相串台。GetLastTradeEvent() 返回枚举类型 ENUM_TRADE_EVENT,让你在 OnTradeTransaction 里快速判断刚才发生的是开仓、平仓还是改单;ResetLastTradeEvent() 则把它复位成 TRADE_EVENT_NO_EVENT,防止同一事件被重复处理。 Refresh() 是真正干活的函数。它接收四个对象数组指针:list_history、list_market、list_changes、list_control,分别对应历史订单、市场挂单/持仓、账户变动、控制事件四类来源;再用 is_history_event / is_market_event 两个布尔量标记本次触发类型,以及 new_history_orders、new_market_pendings、new_market_positions、new_deals 四个整型计数,告诉调用者各类新增了几条。 被高亮的那行 const double changed_volume 是容易被忽略的字段——它记录本次账户成交量净变动(含部分平仓或加仓),精度到手数小数点后。外汇与贵金属杠杆高,成交量异常跳动往往先于价格破位,把这个值接出来做阈值监控,比单纯数订单数更敏感。
const class="type">int new_market_pendings, const class="type">int new_market_positions, const class="type">int new_deals, const class="type">class="kw">double changed_volume); 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update the event list | class=class="str">"cmt">//+------------------------------------------------------------------+ 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, const class="type">int new_market_positions, const class="type">int new_deals, const class="type">class="kw">double changed_volume) { class CEventsCollection : class="kw">public CListObj { class="kw">private:
交易事件类的内部状态与分拣接口
在 MT5 的 EA 架构里,把账户事件抽象成一个独立类,能避免 OnTradeTransaction 里堆满 if-else。下面这段声明给出了该类持有的核心成员:事件链表、账户模式标记、图表 ID、最新 tick 结构,以及 MQL4 兼容用的持仓 ID 与首单类型。 m_is_hedge 这个布尔值很关键——它决定后面走 Hedge 还是 Netto 分支。MT5 同一套代码若要兼顾对冲账户与净仓账户,必须在事件创建前就判断清楚,否则持仓统计会串号。 类里暴露的并不是一堆散函数,而是一组按「来源」分拣的 getter:GetListMarketPendings 取活跃挂单,GetListPositions 取持仓,GetListHistoryPositions / GetListHistoryPendings / GetListDeals 分别取已平持仓、已删挂单、成交明细。 更细的维度是按持仓 ID 反查:GetListAllOrdersByPosID 拿某持仓的全部订单,GetListAllDealsInByPosID / OutByPosID 分别拿进场与出场成交。实盘里若想算单笔持仓的加权平均开仓价,直接调 In 列表求和即可,不用自己遍历全部历史。 外汇与贵金属杠杆高,回测和实盘的事件触发顺序可能有差异,上述接口逻辑建议在策略测试器用真实点差重跑验证。
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,CArrayObj* list_control); 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) closed orders, class=class="str">"cmt">//--- (class="num">2) removed pending orders, (class="num">3) deals, (class="num">4) all closing orders CArrayObj* GetListHistoryPositions(CArrayObj* list); 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 position deals 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);
「从持仓事件集合里抠出历史单与控制单」
做持仓复盘时,经常要把一个 position_id 下的所有成交、订单、控制对象从事件集合里单独捞出来。CEventsCollection 提供了一组按 ID 过滤的取数接口:GetListAllDealsInOutByPosID 返回该持仓的 IN/OUT 全部成交对象数组,SummaryVolumeDealsInByPosID 与 SummaryVolumeDealsOutByPosID 则分别汇总建仓与平仓成交量,便于算净敞口。 GetOrderControlByTicket 是典型的按 ticket 线性查找——代码里先判 list 空指针,再走 list.Total() 拿总数、for 循环用 list.At(i) 取 COrderControl*,比对 ctrl.Ticket()==ticket 即返回。这种 O(n) 遍历在持仓事件少于几百条时延迟可忽略,但若你把它挂到每秒触发的 OnChangeEvent 里扫全量历史,就可能拖慢 EA tick。 另一个实用函数是 GetListHistoryPositions:它先检查传入 list 的 Type() 是否等于 COLLECTION_HISTORY_ID,不是就 Print 报错并返回 NULL;通过后用 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS + ORDER_STATUS_HISTORY_ORDER 筛出已历史化订单。外汇与贵金属杠杆高,历史持仓筛选错了会直接误导仓位统计,跑之前建议在策略测试器用真实账户历史样本验一遍类型判断分支。
CArrayObj* CEventsCollection::GetListHistoryPositions(CArrayObj *list) { if(list.Type()!=COLLECTION_HISTORY_ID) { Print(DFUN,TextByLanguage("Ошибка. Список не является списком исторической коллекции","Error. The list is not a list of the history collection")); class="kw">return NULL; } CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_ORDER,EQUAL); class="kw">return list_orders; } COrderControl* CEventsCollection::GetOrderControlByTicket(CArrayObj *list,const class="type">ulong ticket) { if(list==NULL) class="kw">return NULL; 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 ctrl;
◍ 从持仓 ticket 反查开仓订单类型
在 MT5 的 EA 事件框架里,往往只拿到一个持仓 ticket,却需要回溯它最初是由哪种订单开出来的。下面这段 MQL5 类方法就是干这个的:传入一个对象数组和 ticket,从控制对象里把对应订单捞出来,再返回它的枚举类型。 方法先判空,list 为 NULL 直接给 WRONG_VALUE,避免后续访问崩在终端上。接着用 GetOrderControlByTicket 按 ticket 定位 COrderControl 指针,拿不到同样返回 WRONG_VALUE,最后把 ctrl.TypeOrder() 强转成 ENUM_ORDER_TYPE 交出去。 真正跑事件刷新的是 Refresh。它接收历史、市场、变更、控制四张对象表,加上若干 bool 和 int 计数(如 new_history_orders、new_market_positions、new_deals),任一核心表为空就直接 return。is_market_event 为真时,先读 list_changes.Total(),大于 0 才从尾到头倒序扫变更项——这个倒序很关键,能保证后发生的属性修改覆盖在先发生的上面。 外汇和贵金属杠杆高,这类订单追踪逻辑若漏掉判空,可能在跳空时把脏指针当有效订单处理,引发误判。开 MT5 把这段塞进自己的 CEventsCollection 派生类,挂个 EURUSD 五分钟图验证 ticket 回查是否对得上。
ENUM_ORDER_TYPE CEventsCollection::GetTypeFirst(CArrayObj* list,const class="type">ulong ticket) { if(list==NULL) class="kw">return WRONG_VALUE; COrderControl* ctrl=this.GetOrderControlByTicket(list,ticket); if(ctrl==NULL) class="kw">return WRONG_VALUE; class="kw">return (ENUM_ORDER_TYPE)ctrl.TypeOrder(); } 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, const class="type">int new_market_positions, const class="type">int new_deals, const class="type">class="kw">double changed_volume) { 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--) {
抓新增挂单与仓位的事件分发逻辑
在交易事件引擎里,区分「挂单变多」和「持仓变多」是两套独立分支。MQL5 与 MQL4 都走挂单增量判断,但 MQL4 额外要处理净持仓增加的情况,因为 MQL4 的 position 本质是一条特殊 order。 当 new_market_pendings 大于 0 时,先通过 GetListMarketPendings 拿到最新挂单列表,再用 Sort(SORT_BY_ORDER_TIME_OPEN) 按挂单时间排序。循环从列表尾部倒取 n=new_market_pendings 条,只挑 STATUS 为 ORDER_STATUS_MARKET_PENDING 的调用 CreateNewEvent,保证事件不重不漏。 MQL4 下若 new_market_positions>0,走 GetListPositions 取持仓列表,同样按开仓时间排序后倒序取新增条数。此时判断条件是 STATUS 为 ORDER_STATUS_MARKET_POSITION,再去找对应开仓订单数据并写事件。外汇与贵金属市场跳空频繁,挂单转持仓的边界秒级变化,这类增量捕获逻辑能降低漏事件概率,但高频环境下仍可能丢帧,实盘前务必在 MT5 策略测试器用真实 tick 回放验证。
if(new_market_pendings>class="num">0) { CArrayObj* list=this.GetListMarketPendings(list_market); if(list!=NULL) { list.Sort(SORT_BY_ORDER_TIME_OPEN); 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--) { COrder* order=list.At(i); if(order!=NULL && order.Status()==ORDER_STATUS_MARKET_PENDING) this.CreateNewEvent(order,list_history,list_market,list_control); } } } class="macro">#ifdef __MQL4__ if(new_market_positions>class="num">0) { CArrayObj* list=this.GetListPositions(list_market); if(list!=NULL) { list.Sort(SORT_BY_ORDER_TIME_OPEN); 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) { class=class="str">"cmt">// Find an order and set type and position ID } } } } class="macro">#endif
「从持仓缩减里抓平仓事件」
当持仓数量下降(new_market_positions<0),或在 MQL4 兼容逻辑下出现零持仓变动但成交量缩减且历史订单增加(new_market_positions==0 && changed_volume<0 && new_history_orders>0 && new_market_pendings>WRONG_VALUE)时,系统判定为有仓位被平掉或部分平仓。 先通过 GetListHistoryPositions 拉出已平仓仓位列表,若非空则按平仓时间排序:list.Sort(SORT_BY_ORDER_TIME_CLOSE)。这样列表尾端就是最近关闭的仓位。 用 total=list.Total() 与 n=new_history_orders 锁定需处理的末尾 N 条。循环从 i=total-1 向前取,直到 n 耗尽,确保只处理本次新增的平仓记录,不会重复触发旧事件。 对每条记录,若状态为 ORDER_STATUS_HISTORY_ORDER,就用 GetOrderControlByTicket 找原建仓控制单。找到后把 m_type_first 设为控制单的开仓类型,m_position_id 记为该仓位 ticket,再调 CreateNewEvent 生成平仓事件。外汇与贵金属杠杆高,这类事件回调若用于自动风控,务必在 MT5 策略测试器里先验证触发逻辑。
class=class="str">"cmt">//--- If the number of positions decreased or a position is closed partially(MQL4) else if(new_market_positions<class="num">0 || (new_market_positions==class="num">0 && changed_volume<class="num">0 && new_history_orders>class="num">0 && new_market_pendings>WRONG_VALUE)) { class=class="str">"cmt">//--- Get the list of closed positions CArrayObj* list=this.GetListHistoryPositions(list_history); if(list!=NULL) { class=class="str">"cmt">//--- Sort the new list by position close time list.Sort(SORT_BY_ORDER_TIME_CLOSE); class=class="str">"cmt">//--- Take the number of positions equal to the number of newly closed positions 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 position, look for data of an opening order and set a trading event COrder* position=list.At(i); if(position!=NULL && position.Status()==ORDER_STATUS_HISTORY_ORDER) { class=class="str">"cmt">//--- If there is a control order of a closed position COrderControl* ctrl=this.GetOrderControlByTicket(list_control,position.Ticket()); if(ctrl!=NULL) { class=class="str">"cmt">//--- Set an(class="num">1) order type that led to a position opening, (class="num">2) position ID and create a position closure event this.m_type_first=(ENUM_ORDER_TYPE)ctrl.TypeOrder(); this.m_position_id=position.Ticket(); this.CreateNewEvent(position,list_history,list_market,list_control); }
◍ 账户历史事件里的挂单撤销与成交捕获
当 is_history_event 为真,说明账户历史有变动,此时要区分是挂单撤销还是实际成交。MQL5 与 MQL4 都可能在历史订单数增加时产生挂单移除,而只有 MQL5 会在成交数增加时触发 deal 事件。
处理挂单撤销时,先通过 GetListHistoryPendings 取出新移除的挂单列表,按平仓时间排序后从尾部取最近 new_history_orders 条。若某条订单状态为 ORDER_STATUS_HISTORY_PENDING 且 PositionID()==0,说明是纯挂单撤销而非转持仓,这时才调用 CreateNewEvent 写入事件。
MQL5 专属分支里,用 #ifdef __MQL5__ 包裹 deal 处理:从 GetListDeals 拿纯成交列表,按开单时间排序,同样倒序取 new_deals 条并逐条建事件。这样账户历史里的最后一笔动作(无论是撤单还是成交)都能被精准捕获,方便后续做复盘统计。
外汇与贵金属市场杠杆高、滑点随机,历史事件回调可能滞后于实时行情,验证时建议在策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 new_history_orders 与 new_deals 的触发次数是否和日志一致。
class="macro">#endif } class=class="str">"cmt">//--- If an event in an account history if(is_history_event) { class=class="str">"cmt">//--- If the number of historical orders increased(MQL5, MQL4) if(new_history_orders>class="num">0) { class=class="str">"cmt">//--- Get the list of newly removed pending orders CArrayObj* list=this.GetListHistoryPendings(list_history); if(list!=NULL) { class=class="str">"cmt">//--- Sort the new list by order removal time list.Sort(SORT_BY_ORDER_TIME_CLOSE); 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,list_control); } } } class=class="str">"cmt">//--- If the number of deals increased(MQL5) class="macro">#ifdef __MQL5__ 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); 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);
MQL5里按成交增量回捞尾部事件
在 MQL5 环境下,订单与成交是两套对象。当监控线程发现 new_deals>0 即历史成交数增加时,不能像 MQL4 那样直接读持仓闭环,而要先单独拉取成交列表再做切片。
下方代码演示了如何只取最近 N 笔成交来生成交易事件,避免每次全量扫描。注意 list.Sort(SORT_BY_ORDER_TIME_OPEN) 保证尾部就是最新成交,循环从 total-1 倒序取 new_deals 条即可。
[CODE]
#ifdef __MQL5__
if(new_deals>0)
{
//--- Receive the list of deals only
CArrayObj* list=this.GetListDeals(list_history);
if(list!=NULL)
{
//--- Sort the new list by deal time
list.Sort(SORT_BY_ORDER_TIME_OPEN);
//--- Take the number of deals equal to the number of new ones from the end of the list
int total=list.Total(), n=new_deals;
for(int i=total-1; i>=0 && n>0; i--,n--)
{
COrder* order=list.At(i);
if(order!=NULL)
this.CreateNewEvent(order,list_history,list_market,list_control);
}
}
}
#endif
[/CODE]
逐行拆解:#ifdef __MQL5__ 仅在 MQL5 编译期生效;GetListDeals 返回历史成交容器;Sort 按开单时间升序,尾部即新成交;total-1 是末位索引,n 递减控制只处理增量部分;CreateNewEvent 把每笔成交推入事件总线的三个列表。
对比 MQL4 分支,平仓走 ORDER_STATUS_HISTORY_ORDER 后直接打 TRADE_EVENT_FLAG_POSITION_CLOSED 标记,并用 IsCloseByStopLoss()/IsCloseByTakeProfit() 追加 SL/TP 原因码。外汇与贵金属杠杆高,这类事件标记误差会放大风控延迟,建议开 MT5 用策略测试器跑一遍成交回放验证标记命中率。
class="macro">#ifdef __MQL5__ 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); class=class="str">"cmt">//--- Take the number of deals equal to the number of new ones from the end of the list 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--) { COrder* order=list.At(i); if(order!=NULL) this.CreateNewEvent(order,list_history,list_market,list_control); } } } class="macro">#endif
「反方向平仓时的事件标记与残仓计算」
在 MT5 的 EA 交易事件解析里,当一笔历史订单带有继承订单的成交票号(TicketTo()>0),说明当前平仓属于部分平仓。此时会把 EVENT_REASON_DONE_PARTIALLY 写入 reason,并给 m_trade_event_code 累加 TRADE_EVENT_FLAG_PARTIAL,方便上层逻辑区分全平与减仓。 若从历史订单列表中能取到与当前平仓票号对应的反向关闭单(GetCloseByOrderFromList 返回非空),则判定为「以反向仓位平仓」(close by)。这时要用反向单自身的类型、票号、魔术码、品种与成交量覆盖初始变量,并将 reason 置为 EVENT_REASON_DONE_BY_POS、事件码追加 TRADE_EVENT_FLAG_BY_POS。 真正麻烦的是残仓体积推算。代码从控制订单列表(list_control)里分别取出已平与反向两笔仓位的控制对象,用 ctrl_closed.Volume()-order.Volume() 得到已平仓位的剩余体积 vol_closed,再用 vol_closed-close_by_volume 算出反向仓位的剩余体积 vol_close_by。若 ctrl_closed.Volume() 仍大于本次 order.Volume(),说明原仓位没平干净,会再补一个 TRADE_EVENT_FLAG_PARTIAL 到事件码——这意味着一笔 close by 也可能同时是部分平仓,回测里遇到这种组合要单独计数。 开 MT5 把这段逻辑塞进你自己的 COrder 派生类,跑一组带反向平仓的 EURUSD 历史,看 m_trade_event_code 的位或结果是否同时含 BY_POS 与 PARTIAL,就能验证上述分支有没有漏。
class=class="str">"cmt">//--- add the TakeProfit closure flag to the event code this.m_trade_event_code+=TRADE_EVENT_FLAG_TP; } class=class="str">"cmt">//--- If an order has the class="kw">property with an inherited order filled, a position is closed partially if(order.TicketTo()>class="num">0) { class=class="str">"cmt">//--- set the "partial closure" reason reason=EVENT_REASON_DONE_PARTIALLY; class=class="str">"cmt">//--- add the partial closure flag to the event code this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL; } class=class="str">"cmt">//--- Check closure by an opposite position COrder* order_close_by=this.GetCloseByOrderFromList(list_history,order.Ticket()); class=class="str">"cmt">//--- Declare the variables of the opposite order properties and initialize them with the values of the current closed position ENUM_ORDER_TYPE close_by_type=this.m_type_first; class="type">class="kw">double close_by_volume=order.Volume(); class="type">ulong close_by_ticket=order.Ticket(); class="type">long close_by_magic=order.Magic(); class="type">class="kw">string close_by_symbol=order.Symbol(); class=class="str">"cmt">//--- If the list of historical orders features an order with a closed position ID, the position is closed by an opposite one if(order_close_by!=NULL) { class=class="str">"cmt">//--- Fill in the properties of an opposite closing order using data on the opposite position properties close_by_type=(ENUM_ORDER_TYPE)order_close_by.TypeOrder(); close_by_ticket=order_close_by.Ticket(); close_by_magic=order_close_by.Magic(); close_by_symbol=order_close_by.Symbol(); close_by_volume=order_close_by.Volume(); class=class="str">"cmt">//--- set the "close by" reason reason=EVENT_REASON_DONE_BY_POS; class=class="str">"cmt">//--- add the close by flag to the event code this.m_trade_event_code+=TRADE_EVENT_FLAG_BY_POS; class=class="str">"cmt">//--- Take data on(class="num">1) closed and(class="num">2) opposite positions from the list of control orders class=class="str">"cmt">//--- (in this list, the properties of two opposite positions remain the same as before the closure) COrderControl* ctrl_closed=this.GetOrderControlByTicket(list_control,order.Ticket()); COrderControl* ctrl_close_by=this.GetOrderControlByTicket(list_control,close_by_ticket); class="type">class="kw">double vol_closed=class="num">0; class="type">class="kw">double vol_close_by=class="num">0; class=class="str">"cmt">//--- If no errors detected when receiving these two opposite orders if(ctrl_closed!=NULL && ctrl_close_by!=NULL) { class=class="str">"cmt">//--- Calculate closed volumes of a(class="num">1) closed and(class="num">2) an opposite positions vol_closed=ctrl_closed.Volume()-order.Volume(); vol_close_by=vol_closed-close_by_volume; class=class="str">"cmt">//--- If a position is closed partially(the previous volume exceeds the currently closed one) if(ctrl_closed.Volume()>order.Volume()) { class=class="str">"cmt">//--- add the partial closure flag to an event code this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
◍ 平仓事件对象的属性注入细节
在持仓被部分平仓或全部平仓的判定分支里,先给 reason 打上 EVENT_REASON_DONE_PARTIALLY_BY_POS 标记,说明这笔事件属于「部分平仓」类别,后续统计和回放时可直接按原因过滤。 随后用 new CEventPositionClose 构造一个平仓事件对象,仅当 event 非空且 order.PositionByID()==0(即当前订单已不挂在任何持仓下)才继续写属性,避免对仍开着的仓位误发闭合事件。 下面这段把订单的闭合时间、触发原因、成交与挂单类型、各类 ticket 和持仓 ID 全部塞进 event 的 property 表。注意 EVENT_PROP_TICKET_ORDER_POSITION 取的是 order.Ticket()(首笔建仓单),而 EVENT_PROP_TICKET_ORDER_EVENT 取 close_by_ticket(末笔平仓单),两者混淆会导致复盘时把平仓单错认成开仓单。 外汇与贵金属杠杆高,这类事件映射若写错,回测里的胜率与回撤数字可能完全失真,上 MT5 用样例单逐步打印 property 最稳妥。
class=class="str">"cmt">//--- set the "partial closure" reason reason=EVENT_REASON_DONE_PARTIALLY_BY_POS; } } } class=class="str">"cmt">//--- Create the position closure event CEvent* event=new CEventPositionClose(this.m_trade_event_code,order.Ticket()); if(event!=NULL && order.PositionByID()==class="num">0) { event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeClose()); 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,close_by_type); class=class="str">"cmt">// Event deal type event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// Event deal ticket event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,close_by_type); 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,close_by_ticket); class=class="str">"cmt">// Ticket of an order, based on which an event deal 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 deal 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,close_by_ticket); class=class="str">"cmt">// Opposite position ID event.SetProperty(EVENT_PROP_MAGIC_BY_ID,close_by_magic); 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
把持仓上下文塞进事件对象
在 MT5 里做持仓事件追踪时,光记录“发生了什么”不够,还得把改向前后的订单票据、开仓价、SL/TP 以及当时的买卖报价一并挂到事件实例上,后续回放才不会丢失上下文。 下面这段把方向变更前的持仓订单票、当前订单类型与票号写进事件属性,相当于给事件打上“前世今生”的标签。 event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket()); // Position order ticket before direction changed event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder()); // Current position order type event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket()); // Current position order ticket 紧接着把修改前的开仓价、止损、止盈,以及触发时刻的 Ask/Bid 也存进去,这样复盘时能直接看到事件发生时市场处于什么报价环境。 event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PriceOpen()); // Order price before modification event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLoss()); // StopLoss before modification event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfit()); // TakeProfit before modification event.SetProperty(EVENT_PROP_PRICE_EVENT_ASK,this.m_tick.ask); // Ask price during an event event.SetProperty(EVENT_PROP_PRICE_EVENT_BID,this.m_tick.bid); // Bid price during an event 最后补上魔术码、首单时间、事件价与最新开平仓价、SL/TP、初始手数,一个事件节点就携带了 15 个以上可检索字段。外汇与贵金属波动剧烈,这类结构化事件能帮你用小布类工具批量筛出“改向瞬间滑点超 2 点”的疑似异常单。 event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); // Order/deal/position magic number event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpen()); // Time of an order, based on which a position deal is opened (the first position order) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); // Event price event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); // Order/deal/position open price event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); // Order/deal/position close price event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); // StopLoss position price event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); // TakeProfit position price event.SetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL,order.Volume()); // Requested order volume
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 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.TimeOpen()); 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,MT5 上跑起来你能直接看到结构里每个属性被赋值。 event.SetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED,order.Volume()-order.VolumeCurrent()); // 已执行部分 = 总申报量减剩余量 event.SetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT,order.VolumeCurrent()); // 还没吃掉的挂单量 event.SetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED,order.Volume()); // 该笔对应的持仓执行量 event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); // 浮动或已实现利润 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); // 本单品种 event.SetProperty(EVENT_PROP_SYMBOL_BY_ID,close_by_symbol); // 被 CloseBy 反向平掉的那边品种 灌完属性后调 SetChartID 和 SetTypeEvent,再把对象丢进有序列表:若列表中查无同事件才 InsertSort 并 SendEvent,重复事件直接 delete 并打印调试语。外汇与贵金属杠杆高,这类事件去重逻辑能避免回测里重复计平仓信号。 取 CloseBy 历史单也有坑:MQL5 下用 ORDER_TYPE_CLOSE_BY 等值筛选,老环境只能按 POSITION_BY_ID 不等于 0 反选,两种编译分支结果可能差出几十条记录,开 MT5 切历史集合亲自数一遍最实在。
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,close_by_symbol); class=class="str">"cmt">// Opposite position symbol 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="macro">#ifdef __MQL5__ CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,ORDER_TYPE_CLOSE_BY,EQUAL); class="macro">#else CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_BY_ID,class="num">0,NO_EQUAL); class="macro">#endif
◍ 从持仓订单堆里捞最后那笔 Close By
做多平台兼容的事件回放时,Close By 订单的提取逻辑在 MQL5 和 MQL4 下走的是两条路。MQL5 环境先按持仓 ID 取出全部订单,再用 ORDER_PROP_TYPE 过滤出 ORDER_TYPE_CLOSE_BY;MQL4 没有这个类型枚举,只能直接按 ORDER_PROP_POSITION_BY_ID 去捞关联订单。 两种分支最后都做了一件事:把列表按时间排序后取最后一个元素。MQL5 用 SORT_BY_ORDER_TIME_OPEN,MQL4 用 SORT_BY_ORDER_TIME_CLOSE,差异来自两边订单时间字段的语义不同。排序后 At(Total()-1) 拿到的就是该持仓最近一笔平仓关联单。 实盘验证时可注意:若 list_orders 为空或总数为 0,函数直接返 NULL,调用方必须判空,否则 At() 会越界抛错。外汇与贵金属杠杆高,这类订单回溯若漏掉空指针判断,回测脚本可能在真实 tick 下崩。
COrder* CEventsCollection::GetCloseByOrderFromList(CArrayObj *list,const class="type">ulong position_id) { class="macro">#ifdef __MQL5__ CArrayObj* list_orders=this.GetListAllOrdersByPosID(list,position_id); list_orders=CSelect::ByOrderProperty(list_orders,ORDER_PROP_TYPE,ORDER_TYPE_CLOSE_BY,EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; list_orders.Sort(SORT_BY_ORDER_TIME_OPEN); class="macro">#else CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_BY_ID,position_id,EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; list_orders.Sort(SORT_BY_ORDER_TIME_CLOSE); class="macro">#endif COrder* order=list_orders.At(list_orders.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); }
把挂单删除改成按挂单时间排序
上一版里订单列表用的是 SORT_BY_ORDER_TIME_OPEN_MSC(毫秒级开盘时间)来排,这一轮把枚举里的时间常数拿掉后,EA 里按下「删除挂单」按钮的处理逻辑得换成 SORT_BY_ORDER_TIME_OPEN,否则编译会报未知标识符。 实际验证做法很直接:把 TestDoEasyPart10 的 EA 复制到 Experts\TestDoEasy\Part11 重命名为 TestDoEasyPart11.mq4,止损点数和止盈点数都填 0,这样平仓不会带止损止盈单。进测试器开仓、部分平仓,再下一笔挂单然后删掉,日志里能看到部分平仓和挂单删除已经拆成了两个独立事件。 再跑一次点「观察事件定义」按钮,平仓事件、止损价修改、挂单价修改都能被正确追踪。外汇和贵金属品种波动大,这类事件拆分在实盘里也可能因滑点导致顺序和测试略有出入,建议先在策略测试器用历史数据跑通再上模拟盘。 下面这段是挂单删除按钮处理里改完后的核心代码,注意排序调用和 MQL4/MQL5 删除函数的条件编译分支:
<span style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);">list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);</span> <span class="comment">class=class="str">"cmt">//--- If the BUTT_DELETE_PENDING button is pressed: Remove the first pending order</span> <span class="keyword">else</span> <span class="keyword">if</span>(button==<span class="functions">EnumToString</span>(BUTT_DELETE_PENDING)) { <span class="comment">class=class="str">"cmt">//--- Get the list of all orders</span> CArrayObj* list=engine.GetListMarketPendings(); <span class="keyword">if</span>(list!=<span class="macro">NULL</span>) { <span class="comment">class=class="str">"cmt">//--- Sort the list by placement time</span> <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);">list.Sort(SORT_BY_ORDER_TIME_OPEN)</span>; <span class="keyword">class="type">int</span> total=list.Total(); <span class="comment">class=class="str">"cmt">//--- In the loop from the position with the most amount of time</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=total-<span class="number">class="num">1</span>;i>=<span class="number">class="num">0</span>;i--) { COrder* order=list.At(i); <span class="keyword">if</span>(order==<span class="macro">NULL</span>) <span class="keyword">class="kw">continue</span>; <span class="comment">class=class="str">"cmt">//--- class="kw">delete the order by its ticket</span> <span class="preprocessor">class="macro">#ifdef </span><span class="macro">__MQL5__</span> trade.OrderDelete(order.Ticket()); <span class="preprocessor">class="macro">#else </span>PendingOrderDelete(order.Ticket()); <span class="preprocessor">class="macro">#endif </span>} } }
「跨平台库的下一批对象与毫秒时间坑」
这一节把 MQL4 兼容的收尾工作交代清楚:现有函数库的类已能跑在 MT4/MT5 双端,下一篇会落地“帐户”和“品种”两个新对象,以及它们的集合与事件跟踪骨架。想提前验证当前进度,直接把文末的 MQL5.zip(100.78 KB)和 MQL4.zip(100.79 KB)拖进对应终端编译即可。 评论区里有人点出一个实打实的雷:MQL5 的 datetime 以秒计,库内部不少地方用 long 存毫秒,两者差 1000 倍,直接赋值会让 GetListByTime(...) 类函数失效。下面这段就是社区补的转换函数,解决因子错位。 //+------------------------------------------------------------------+ //} 将毫秒时间转换为日期时间。 //+------------------------------------------------------------------+ datetime TimeMSCtoDate(const long time_msc) { return datetime(time_msc/1000); } //+------------------------------------------------------------------+ //} 将日期时间转换为以毫秒为单位的时间。 //+------------------------------------------------------------------+ long DatetoTimeMSC(const datetime time_sec) { return long(time_sec * 1000); } public: ....... //--- 从时间范围为 begin_time 到 end_time 的集合中选择订单 逐行看:TimeMSCtoDate 收一个 long 毫秒值,除以 1000 强转 datetime 返回;DatetoTimeMSC 反向,秒乘 1000 变毫秒 long;public 之后是集合按时间区间筛选订单的接口占位。 另一个容易被忽略的点:历史集合自身知道是否按 ORDER_PROP_TIME_OPEN / CLOSE 排过序,但 GetListByTime 默认假定已排好,OOP 上应在调用前用 SortMode() 自检。TestDoEasyPart03_1.mq5 默认按开仓时间排,却传 SELECT_BY_TIME_CLOSE 去取,就可能漏单。外汇与贵金属杠杆高,这类集合边界 bug 在回测里不一定显形,实盘可能错失触发。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//} 将毫秒时间转换为日期时间。 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime TimeMSCtoDate(const class="type">long time_msc) { class="kw">return class="type">class="kw">datetime(time_msc/class="num">1000); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//} 将日期时间转换为以毫秒为单位的时间。 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long DatetoTimeMSC(const class="type">class="kw">datetime time_sec) { class="kw">return class="type">long(time_sec * class="num">1000); } class="kw">public: ....... class=class="str">"cmt">//--- 从时间范围为 begin_time 到 end_time 的集合中选择订单
◍ 画得少,看得清
GetListByTime 把历史订单按时间窗切片,默认以开仓时间(SELECT_BY_TIME_OPEN)检索,也可切到平仓时间。若底层 m_list_all_orders 的排序模式跟所选时间属性不一致,函数直接 Print 报错并返回 NULL,这一步能拦掉大部分误用导致的空指针崩溃。 时间边界处理上,end_time 传 0 会被替换成 END_TIME 常量,begin_time 大于 end_time 时强制回退到 0,等于不限制起点。检索靠 SearchGreatOrEqual 和 SearchLessOrEqual 两个二分定位,再挨个 Add 进新列表,复杂度为对数级而非全表扫。 实盘里接小布盯盘做历史胜率回看时,只框最近 200 根 K 对应的订单窗就够了,别把全量历史都拉出来画。外汇与贵金属杠杆高、滑点跳空频繁,历史切片结论只代表过去概率,不构成方向暗示。
CArrayObj *CHistoryCollection::GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_OPEN) { ENUM_ORDER_PROP_INTEGER class="kw">property=(select_time_mode==SELECT_BY_TIME_CLOSE ? ORDER_PROP_TIME_CLOSE : ORDER_PROP_TIME_OPEN); if(class="kw">property != (ENUM_ORDER_PROP_INTEGER)m_list_all_orders.SortMode()) { ::Print(DFUN+"History list not prpperly sorted"); class="kw">return NULL; } CArrayObj *list=new CArrayObj(); if(list==NULL) { ::Print(DFUN+CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_TEMP_LIST)); class="kw">return NULL; } class="type">class="kw">datetime begin=begin_time,end=(end_time==class="num">0 ? END_TIME : end_time); if(begin_time>end_time) begin=class="num">0; list.FreeMode(false); ListStorage.Add(list); class=class="str">"cmt">//--- this.m_order_instance.SetProperty(class="kw">property,DatetoTimeMSC(begin)); class="type">int index_begin=this.m_list_all_orders.SearchGreatOrEqual(&m_order_instance); if(index_begin==WRONG_VALUE) class="kw">return list; this.m_order_instance.SetProperty(class="kw">property,DatetoTimeMSC(end)); class="type">int index_end=this.m_list_all_orders.SearchLessOrEqual(&m_order_instance); if(index_end==WRONG_VALUE) class="kw">return list; for(class="type">int i=index_begin; i<=index_end; i++) list.Add(this.m_list_all_orders.At(i)); class="kw">return list; }