轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件·进阶篇
(2/3)·当订单、历史事件、行情集合混在一个数组里,靠标志位区分类型只会让代码腐烂,本篇用虚方法ID根治
◍ 订单排序字段与事件状态枚举拆解
在 MT5 的订单容器排序逻辑里,双精度属性排序从 FIRST_ORD_DBL_PROP 基址偏移,止盈价占 +3、盈利占 +4、佣金 +5、隔夜利息 +6、交易量 +7、未执行量 +8、全利润(含佣息)+9、StopLimit 激活限价 +10;字符串属性则从 FIRST_ORD_STR_PROP 起算,品种为基址、注释 +1、外部系统订单 ID +2。 写 EA 时若要用 CTrade 或自定义数组给持仓排序,直接引用这些枚举值比手写字符串比对更稳,也避免不同 broker 点差导致的小数精度误判。 事件状态枚举 ENUM_EVENT_STATUS 把账户动作切成五类:场内仓位(开/加减/逆转)、场内挂单放置、历史挂单删除、历史仓位平仓、余额操作(出金入金及 DEAL 类事件)。回测看,同一毫秒内 EVENT_STATUS_MARKET_POSITION 与 EVENT_STATUS_BALANCE 可能连续触发,监听器里不分流就会重复统计净值。 事件原因枚举 ENUM_EVENT_REASON 目前片断只给到两个:挂单激活=0、挂单部分激活=1,说明部分成交在真实流动性薄的黄金时段(如周日开盘后 30 秒内)概率偏高,排序和事件过滤最好分开处理。
SORT_BY_ORDER_TP = FIRST_ORD_DBL_PROP+class="num">3, class=class="str">"cmt">// 按止盈价排序 SORT_BY_ORDER_PROFIT = FIRST_ORD_DBL_PROP+class="num">4, class=class="str">"cmt">// 按盈利排序 SORT_BY_ORDER_COMMISSION = FIRST_ORD_DBL_PROP+class="num">5, class=class="str">"cmt">// 按佣金排序 SORT_BY_ORDER_SWAP = FIRST_ORD_DBL_PROP+class="num">6, class=class="str">"cmt">// 按隔夜利息排序 SORT_BY_ORDER_VOLUME = FIRST_ORD_DBL_PROP+class="num">7, class=class="str">"cmt">// 按交易量排序 SORT_BY_ORDER_VOLUME_CURRENT = FIRST_ORD_DBL_PROP+class="num">8, class=class="str">"cmt">// 按未执行交易量排序 SORT_BY_ORDER_PROFIT_FULL = FIRST_ORD_DBL_PROP+class="num">9, class=class="str">"cmt">// 按利润+佣金+隔夜利息标准排序 SORT_BY_ORDER_PRICE_STOP_LIMIT= FIRST_ORD_DBL_PROP+class="num">10, class=class="str">"cmt">// 按由 StopLimit 订单激活的限价订单排序 class=class="str">"cmt">//--- 按字符串型属性排序 SORT_BY_ORDER_SYMBOL = FIRST_ORD_STR_PROP, class=class="str">"cmt">// 按品种排序 SORT_BY_ORDER_COMMENT = FIRST_ORD_STR_PROP+class="num">1, class=class="str">"cmt">// 按注释排序 SORT_BY_ORDER_EXT_ID = FIRST_ORD_STR_PROP+class="num">2 class=class="str">"cmt">// 按外部交易系统中的订单 ID 排序 }; enum ENUM_EVENT_STATUS { EVENT_STATUS_MARKET_POSITION, class=class="str">"cmt">// 场内仓位事件(开仓,部分开仓,部分平仓,增加交易量,逆转) EVENT_STATUS_MARKET_PENDING, class=class="str">"cmt">// 场内挂单事件(放置) EVENT_STATUS_HISTORY_PENDING, class=class="str">"cmt">// 历史挂单事件(删除) EVENT_STATUS_HISTORY_POSITION, class=class="str">"cmt">// 历史仓位事件(平仓) EVENT_STATUS_BALANCE, class=class="str">"cmt">// 余额操作事件(累计余额,出金和来自 ENUM_DEAL_TYPE 枚举的事件) }; enum ENUM_EVENT_REASON { EVENT_REASON_ACTIVATED_PENDING = class="num">0, class=class="str">"cmt">// 挂单激活 EVENT_REASON_ACTIVATED_PENDING_PARTIALLY = class="num">1, class=class="str">"cmt">// 挂单部分激活
「成交事件原因枚举的数值分布」
在 MT5 的订单与成交事件回调里,EVENT_REASON 系列常量用整数标记一笔交易动作是怎么发生的。从取消、过期到完整执行、部分执行,再到 SL/TP 触发以及逆向仓位对冲,编号从 2 一路排到 13,中间 1 留空未用。 账户层面的变动另起一套偏移:从充值(14)、出金(15) 到信贷(16)、费用(17)、更正(18)、奖励(19),再到日/月佣金(21/22) 与代理日佣金(23),这批常量相对 ACCOUNT_CREDIT 事件基准做了 +13 偏移。写 EA 时若用 OnTradeTransaction 抓 reason 参数,直接比对这些枚举值就能区分平仓到底是止损打掉还是止盈落袋。 实盘里黄金和外汇品种跳空常见,挂单过期(3) 与部分执行(5/7/9) 出现频率可能高于股票品种;拿历史成交回放验证时,建议先 Print 出 reason 数值分布,确认经纪商是否复写了默认枚举。
EVENT_REASON_CANCEL = class="num">2, class=class="str">"cmt">// 取消 EVENT_REASON_EXPIRED = class="num">3, class=class="str">"cmt">// 挂单过期 EVENT_REASON_DONE = class="num">4, class=class="str">"cmt">// 请求已完整执行 EVENT_REASON_DONE_PARTIALLY = class="num">5, class=class="str">"cmt">// 请求已部分执行 EVENT_REASON_DONE_SL = class="num">6, class=class="str">"cmt">// 由止损平仓 EVENT_REASON_DONE_SL_PARTIALLY = class="num">7, class=class="str">"cmt">// 由止损部分平仓 EVENT_REASON_DONE_TP = class="num">8, class=class="str">"cmt">// 由止盈平仓 EVENT_REASON_DONE_TP_PARTIALLY = class="num">9, class=class="str">"cmt">// 由止盈部分平仓 EVENT_REASON_DONE_BY_POS = class="num">10, class=class="str">"cmt">// 由逆向仓位平仓 EVENT_REASON_DONE_PARTIALLY_BY_POS = class="num">11, class=class="str">"cmt">// 由逆向仓位部分平仓 EVENT_REASON_DONE_BY_POS_PARTIALLY = class="num">12, class=class="str">"cmt">// 由部分交易量将逆向仓位平仓 EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY = class="num">13, class=class="str">"cmt">// 由部分交易量将逆向仓位部分平仓 class=class="str">"cmt">//--- 与 ENUM_DEAL_TYPE 枚举中的 DEAL_TYPE_BALANCE 成交类型相关的常量 EVENT_REASON_BALANCE_REFILL = class="num">14, class=class="str">"cmt">// 充值 EVENT_REASON_BALANCE_WITHDRAWAL = class="num">15, class=class="str">"cmt">// 从账户出金 class=class="str">"cmt">//--- 常量列表与 ENUM_TRADE_EVENT 枚举中的 TRADE_EVENT_ACCOUNT_CREDIT 相关,并相对于 ENUM_DEAL_TYPE(EVENT_REASON_ACCOUNT_CREDIT-class="num">3)偏移了 +class="num">13 EVENT_REASON_ACCOUNT_CREDIT = class="num">16, class=class="str">"cmt">// 积累信贷 EVENT_REASON_ACCOUNT_CHARGE = class="num">17, class=class="str">"cmt">// 额外费用 EVENT_REASON_ACCOUNT_CORRECTION = class="num">18, class=class="str">"cmt">// 更正账目 EVENT_REASON_ACCOUNT_BONUS = class="num">19, class=class="str">"cmt">// 累积奖励 EVENT_REASON_ACCOUNT_COMISSION = class="num">20, class=class="str">"cmt">// 额外佣金 EVENT_REASON_ACCOUNT_COMISSION_DAILY = class="num">21, class=class="str">"cmt">// 在交易日结束时收取的佣金 EVENT_REASON_ACCOUNT_COMISSION_MONTHLY = class="num">22, class=class="str">"cmt">// 在交易月结束时收取的佣金 EVENT_REASON_ACCOUNT_COMISSION_AGENT_DAILY = class="num">23, class=class="str">"cmt">// 在交易日结束时收取的代理佣金
账户事件枚举与整数属性映射
在 MT5 的账户事件体系里,事件原因码从 24 到 30 覆盖了一组非交易类的资金变动:代理月佣金(24)、资金利息(25)、撤买(26)、撤卖(27)、红利(28)、含税红利(29)与税款(30)。写 EA 监听账户流水时,这几个值能帮你把「真成交」和「账务事件」分开处理。 宏 REASON_EVENT_SHIFT 被定义为 EVENT_REASON_ACCOUNT_CREDIT 减 3,作用是将事件原因枚举整体偏移成一个紧凑的整数区间,方便在 OnEvent 里用位运算或数组下标快速查表,而不是写一长串 if-else。 事件自身的整数型属性由 ENUM_EVENT_PROP_INTEGER 枚举给出,从 0 开始:0 是事件类型,1 是毫秒级时间,2 是状态,3 是原因码,4~5 是成交层面的类型与票据,6~11 则细化到订单与仓位的首笔/末笔关联 ID。实盘里抓 EVENT_PROP_REASON_EVENT 配合 24~30 区间,就能在日志里标出「这笔是利息而非滑点」——外汇与贵金属杠杆高,账务事件和交易事件混淆会直接导致风控误判。
EVENT_REASON_ACCOUNT_COMISSION_AGENT_MONTHLY = class="num">24, class=class="str">"cmt">// 在交易月结束时收取的代理佣金 EVENT_REASON_ACCOUNT_INTEREST = class="num">25, class=class="str">"cmt">// 可用资金的累积利息 EVENT_REASON_BUY_CANCELLED = class="num">26, class=class="str">"cmt">// 取消购买成交 EVENT_REASON_SELL_CANCELLED = class="num">27, class=class="str">"cmt">// 取消卖出成交 EVENT_REASON_DIVIDENT = class="num">28, class=class="str">"cmt">// 累积红利 EVENT_REASON_DIVIDENT_FRANKED = class="num">29, class=class="str">"cmt">// 累积分红 EVENT_REASON_TAX = class="num">30 class=class="str">"cmt">// 税款 }; class="macro">#define REASON_EVENT_SHIFT(EVENT_REASON_ACCOUNT_CREDIT-class="num">3) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的整数型属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_EVENT_PROP_INTEGER { EVENT_PROP_TYPE_EVENT = class="num">0, class=class="str">"cmt">// 帐户交易事件类型(来自 ENUM_TRADE_EVENT 枚举) EVENT_PROP_TIME_EVENT, class=class="str">"cmt">// 事件时间(以毫秒为单位) EVENT_PROP_STATUS_EVENT, class=class="str">"cmt">// 事件状态(来自 ENUM_EVENT_STATUS 枚举) EVENT_PROP_REASON_EVENT, class=class="str">"cmt">// 事件原因(来自 ENUM_EVENT_REASON 枚举) EVENT_PROP_TYPE_DEAL_EVENT, class=class="str">"cmt">// 成交事件类型 EVENT_PROP_TICKET_DEAL_EVENT, class=class="str">"cmt">// 成交事件票据 EVENT_PROP_TYPE_ORDER_EVENT, class=class="str">"cmt">// 基于开立成交事件的订单类型(最后的开仓订单) EVENT_PROP_TICKET_ORDER_EVENT, class=class="str">"cmt">// 基于成交开立事件的订单票据(最后的开仓订单) EVENT_PROP_TIME_ORDER_POSITION, class=class="str">"cmt">// 基于成交开立事件的订单时间(首笔开仓订单) EVENT_PROP_TYPE_ORDER_POSITION, class=class="str">"cmt">// 基于成交开立事件的订单类型(首笔开仓订单) EVENT_PROP_TICKET_ORDER_POSITION, class=class="str">"cmt">// 基于成交开立事件的订单票据(首笔开仓订单) EVENT_PROP_POSITION_ID class=class="str">"cmt">// 仓位 ID
◍ 事件属性的三段式枚举拆解
在 MT5 自建事件监控器时,事件属性被硬拆成整数、实数、字符串三类枚举,分别用 ENUM_EVENT_PROP_INTEGER、ENUM_EVENT_PROP_DOUBLE、ENUM_EVENT_PROP_STRING 承载。整数段以 EVENT_PROP_POSITION_BY_ID 收尾,宏 EVENT_PROP_INTEGER_TOTAL 定为 14,意味着前 14 个属性槽全留给整型字段。 实数段从 EVENT_PROP_PRICE_EVENT 起步,其枚举值直接挂接在整数总数 14 上,所以首个双精度属性编号是 14 而非 0。该段覆盖开平价格、SL/TP、三档成交量及盈利,宏 EVENT_PROP_DOUBLE_TOTAL 标为 9,实数属性共占 9 个槽。 字符串段极简,仅 EVENT_PROP_SYMBOL 一项,枚举值 = 14 + 9 = 23,宏 EVENT_PROP_STRING_TOTAL 为 1。排序时可用 FIRST_EVN_DBL_PROP(14) 与 FIRST_EVN_STR_PROP(23) 快速切到对应段,避免手写魔法数字。外汇与贵金属事件回放请留意点差跳变导致 SL/TP 价格属性可能偏离预期,属高风险验证项。
EVENT_PROP_POSITION_BY_ID, class=class="str">"cmt">// 逆向仓位 ID EVENT_PROP_MAGIC_ORDER, class=class="str">"cmt">// 订单/成交/仓位的魔幻数字 }; class="macro">#define EVENT_PROP_INTEGER_TOTAL(class="num">14) class=class="str">"cmt">// 整数型事件属性的总数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的实数型属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_EVENT_PROP_DOUBLE { EVENT_PROP_PRICE_EVENT = (EVENT_PROP_INTEGER_TOTAL), class=class="str">"cmt">// 事件发生的价格所在 EVENT_PROP_PRICE_OPEN, class=class="str">"cmt">// 订单/成交/仓位开立价格 EVENT_PROP_PRICE_CLOSE, class=class="str">"cmt">// 订单/成交/仓位平仓价格 EVENT_PROP_PRICE_SL, class=class="str">"cmt">// 订单/成交/仓位止损价格 EVENT_PROP_PRICE_TP, class=class="str">"cmt">// 订单/成交/仓位止盈价格 EVENT_PROP_VOLUME_INITIAL, class=class="str">"cmt">// 请求的交易量 EVENT_PROP_VOLUME_EXECUTED, class=class="str">"cmt">// 执行的交易量 EVENT_PROP_VOLUME_CURRENT, class=class="str">"cmt">// 剩余的交易量 EVENT_PROP_PROFIT class=class="str">"cmt">// 盈利 }; class="macro">#define EVENT_PROP_DOUBLE_TOTAL(class="num">9) class=class="str">"cmt">// 事件的实数型属性总数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的字符串型属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_EVENT_PROP_STRING { EVENT_PROP_SYMBOL = (EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_DOUBLE_TOTAL), class=class="str">"cmt">// 订单品种 }; class="macro">#define EVENT_PROP_STRING_TOTAL(class="num">1) class=class="str">"cmt">// 事件的字符串型属性总数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 可能的事件排序标准 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define FIRST_EVN_DBL_PROP(EVENT_PROP_INTEGER_TOTAL) class="macro">#define FIRST_EVN_STR_PROP(EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_DOUBLE_TOTAL) enum ENUM_SORT_EVENTS_MODE { class=class="str">"cmt">//--- 按整数型属性排序
「事件排序枚举的字段映射」
在 MT5 的历史事件检索接口里,排序方向由一组枚举常量决定,从 0 到 13 覆盖了事件类型、时间、状态、原因以及订单/成交/仓位的关联标识。比如 SORT_BY_EVENT_TIME_EVENT=1 是按事件时间排,SORT_BY_EVENT_MAGIC_ORDER=12 则是按魔幻数字把 EA 自己的痕迹挑出来。 实数型属性从 FIRST_EVN_DBL_PROP 开始偏移,SORT_BY_EVENT_PRICE_EVENT 对应该基值,之后每加 1 就是一种价格或成交量维度:开仓价、平仓价、SL、TP、初始量、当前量、剩余量,直到 SORT_BY_EVENT_PROFIT 落在 FIRST_EVN_DBL_PROP+8,直接按盈亏给事件排队。 写回测或做账户诊断时,若想快速抽取某魔幻数字下的首笔开仓,可用 SORT_BY_EVENT_TYPE_ORDER_POSITION=7 配合 SORT_BY_EVENT_TICKET_ORDER_POSITION=9;外汇与贵金属杠杆高,这类排序只帮你看清已发生结构,不预示后续波动。
SORT_BY_EVENT_TYPE_EVENT = class="num">0, class=class="str">"cmt">// 按事件类型排序 SORT_BY_EVENT_TIME_EVENT = class="num">1, class=class="str">"cmt">// 按事件时间排序 SORT_BY_EVENT_STATUS_EVENT = class="num">2, class=class="str">"cmt">// 按事件状态排序 (来自 ENUM_EVENT_STATUS 枚举) SORT_BY_EVENT_REASON_EVENT = class="num">3, class=class="str">"cmt">// 按事件原因排序 (来自 ENUM_EVENT_REASON 枚举) SORT_BY_EVENT_TYPE_DEAL_EVENT = class="num">4, class=class="str">"cmt">// 按成交事件类型排序 SORT_BY_EVENT_TICKET_DEAL_EVENT = class="num">5, class=class="str">"cmt">// 按成交事件票据排序 SORT_BY_EVENT_TYPE_ORDER_EVENT = class="num">6, class=class="str">"cmt">// 根据成交开立事件的订单类型排序(最后的开仓订单) SORT_BY_EVENT_TYPE_ORDER_POSITION = class="num">7, class=class="str">"cmt">// 根据成交开立事件的订单类型排序(首笔开仓订单) SORT_BY_EVENT_TICKET_ORDER_EVENT = class="num">8, class=class="str">"cmt">// 根据成交开立事件的订单票据排序(最后的开仓订单) SORT_BY_EVENT_TICKET_ORDER_POSITION = class="num">9, class=class="str">"cmt">// 根据成交开立事件的订单票据排序(首笔开仓订单) SORT_BY_EVENT_POSITION_ID = class="num">10, class=class="str">"cmt">// 按仓位 ID 排序 SORT_BY_EVENT_POSITION_BY_ID = class="num">11, class=class="str">"cmt">// 按逆向仓位 ID 排序 SORT_BY_EVENT_MAGIC_ORDER = class="num">12, class=class="str">"cmt">// 按订单/成交/仓位魔幻数字排序 SORT_BY_EVENT_TIME_ORDER_POSITION = class="num">13, class=class="str">"cmt">// 根据成交开立事件的订单时间排序(首笔开仓订单) class=class="str">"cmt">//--- 按实数型属性排序 SORT_BY_EVENT_PRICE_EVENT = FIRST_EVN_DBL_PROP, class=class="str">"cmt">// 按事件发生时的价格排序 SORT_BY_EVENT_PRICE_OPEN = FIRST_EVN_DBL_PROP+class="num">1, class=class="str">"cmt">// 按开仓价排序 SORT_BY_EVENT_PRICE_CLOSE = FIRST_EVN_DBL_PROP+class="num">2, class=class="str">"cmt">// 按平仓价排序 SORT_BY_EVENT_PRICE_SL = FIRST_EVN_DBL_PROP+class="num">3, class=class="str">"cmt">// 按仓位止损价排序 SORT_BY_EVENT_PRICE_TP = FIRST_EVN_DBL_PROP+class="num">4, class=class="str">"cmt">// 按仓位止盈价排序 SORT_BY_EVENT_VOLUME_INITIAL = FIRST_EVN_DBL_PROP+class="num">5, class=class="str">"cmt">// 按仓位初始交易量排序 SORT_BY_EVENT_VOLUME = FIRST_EVN_DBL_PROP+class="num">6, class=class="str">"cmt">// 按仓位当前交易量排序 SORT_BY_EVENT_VOLUME_CURRENT = FIRST_EVN_DBL_PROP+class="num">7, class=class="str">"cmt">// 按仓位剩余交易量排序 SORT_BY_EVENT_PROFIT = FIRST_EVN_DBL_PROP+class="num">8, class=class="str">"cmt">// 按盈利排序 class=class="str">"cmt">//--- 按字符串型属性排序
给订单类型配上可读的中文标签
在 MT5 的 EA 或脚本里直接打印 ENUM_ORDER_TYPE 数值,日志里只是一串整数,排查挂单异常时很不直观。下面这段函数把 13 种常见订单类型映射成字符串,MQL5 环境下市价单前缀为 "Market order",MQL4 则退化为 "Position"。 函数用预编译宏 __MQL5__ 区分环境:MQL5 多出 BUY_STOP_LIMIT、SELL_STOP_LIMIT、CLOSE_BY 三类,MQL4 则补 BALANCE、CREDIT 操作类型。实际跑起来,若收到一笔 ORDER_TYPE_SELL_STOP_LIMIT,返回文本就是 "Sell Stop Limit",比看枚举值快得多。 外汇与贵金属杠杆高,挂单类型识别错可能引发非预期平仓,建议在回测和实盘前先用 Print(OrderTypeDescription(type)) 自验一遍输出。
class="type">class="kw">string OrderTypeDescription(const ENUM_ORDER_TYPE type) { class="type">class="kw">string pref=(class="macro">#ifdef __MQL5__ "Market order" class="macro">#else "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__ 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" :