轻松快捷开发 MetaTrader 程序的函数库(第九部分):与 MQL4 的兼容性 - 准备数据·进阶篇
(2/3)· 两千多个编译报错背后,其实是 MQL4 对常量和订单系统的认知空白
「MT5 成交流水与挂单状态的枚举骨架」
在 MT5 的底层交易结构里,成交流水、持仓方向与订单状态分别由三组枚举定义。直接读标准头文件比查文档更快,也能避免自己造轮子时命名冲突。 成交类型 ENUM_DEAL_TYPE 共列出 18 个成员,从 DEAL_TYPE_BUY、DEAL_TYPE_SELL 到 DEAL_TAX,覆盖了真实成交、余额调整、佣金计提、分红扣税等全口径账目动作。写 EA 做成交回放时,若只筛 BUY/SELL 会漏掉 DEAL_TYPE_COMMISSION_DAILY 这类每日佣金流,导致净值曲线对不上账户历史。 持仓方向只有两个值:POSITION_TYPE_BUY 与 POSITION_TYPE_SELL。看起来简单,但在跨品种对冲逻辑里,必须显式判断持仓枚举而非用 OrderType() 的旧习惯,否则 MQL5 编译直接报错。 订单状态 ENUM_ORDER_STATE 有 10 个成员,从 STARTED、PLACED 到 REQUEST_CANCEL。其中 PARTIAL 代表部分成交,FILLED 才是完全成交;很多自制订单管理器漏掉 PARTIAL 分支,造成「已成交却没计数」的幽灵仓位。 下面这段是从标准兼容头里摘出的原生定义,开 MT5 按 F4 进 stdlib 对照即可验证。
enum ENUM_DEAL_TYPE { DEAL_TYPE_BUY, DEAL_TYPE_SELL, DEAL_TYPE_BALANCE, DEAL_TYPE_CREDIT, DEAL_TYPE_CHARGE, DEAL_TYPE_CORRECTION, DEAL_TYPE_BONUS, DEAL_TYPE_COMMISSION, DEAL_TYPE_COMMISSION_DAILY, DEAL_TYPE_COMMISSION_MONTHLY, DEAL_TYPE_COMMISSION_AGENT_DAILY, DEAL_TYPE_COMMISSION_AGENT_MONTHLY, DEAL_TYPE_INTEREST, DEAL_TYPE_BUY_CANCELED, DEAL_TYPE_SELL_CANCELED, DEAL_DIVIDEND, DEAL_DIVIDEND_FRANKED, DEAL_TAX }; enum class="type">ENUM_POSITION_TYPE { POSITION_TYPE_BUY, POSITION_TYPE_SELL }; enum ENUM_ORDER_STATE { ORDER_STATE_STARTED, ORDER_STATE_PLACED, ORDER_STATE_CANCELED, ORDER_STATE_PARTIAL, ORDER_STATE_FILLED, ORDER_STATE_REJECTED, ORDER_STATE_EXPIRED, ORDER_STATE_REQUEST_ADD, ORDER_STATE_REQUEST_MODIFY, ORDER_STATE_REQUEST_CANCEL };
成交与挂单枚举的底层定义
在 MT5 的底层交易结构里,一笔成交(deal)并不只有买卖两种性质。系统用 ENUM_DEAL_TYPE 枚举区分了余额调整、信用、手续费、利息、分红、税务等非交易性流水,共 17 个成员,写策略时若只过滤 buy/sell 会漏掉佣金和库存费对净值的真实影响。 持仓方向被压缩到极简:ENUM_POSITION_TYPE 只有 POSITION_TYPE_BUY 与 POSITION_TYPE_SELL 两个值,对应多空敞口,没有第三种状态。 订单生命周期由 ENUM_ORDER_STATE 描述,从 STARTED、PLACED 到 PARTIAL、FILLED,再到 REJECTED、EXPIRED,共 10 个状态。回测或实盘监控时,必须按状态机判断,不能假设挂单只有「成交 / 取消」两种结局。 订单类型在原生枚举之外,用宏补了三个:ORDER_TYPE_CLOSE_BY=8、ORDER_TYPE_BUY_STOP_LIMIT=9、ORDER_TYPE_SELL_STOP_LIMIT=10。这代表 MT5 支持平仓反向单和止损限价单,但老版本头文件未必包含,自己补定义才能编译过。 CorrectPricePending 这个函数负责把挂单价相对 StopLevel 做合规化。它先取 SYMBOL_POINT 和点数位数,再调 StopLevel(symbol_name, spread_multiplier) 算出最小允许距离——外汇和贵金属杠杆高,StopLevel 随播价跳动,忽略它会直接报『成交价离市价太近』的错。
enum ENUM_DEAL_TYPE { DEAL_TYPE_BALANCE, DEAL_TYPE_CREDIT, DEAL_TYPE_CHARGE, DEAL_TYPE_CORRECTION, DEAL_TYPE_BONUS, DEAL_TYPE_COMMISSION, DEAL_TYPE_COMMISSION_DAILY, DEAL_TYPE_COMMISSION_MONTHLY, DEAL_TYPE_COMMISSION_AGENT_DAILY, DEAL_TYPE_COMMISSION_AGENT_MONTHLY, DEAL_TYPE_INTEREST, DEAL_TYPE_BUY_CANCELED, DEAL_TYPE_SELL_CANCELED, DEAL_DIVIDEND, DEAL_DIVIDEND_FRANKED, DEAL_TAX }; enum class="type">ENUM_POSITION_TYPE { POSITION_TYPE_BUY, POSITION_TYPE_SELL }; enum ENUM_ORDER_STATE { ORDER_STATE_STARTED, ORDER_STATE_PLACED, ORDER_STATE_CANCELED, ORDER_STATE_PARTIAL, ORDER_STATE_FILLED, ORDER_STATE_REJECTED, ORDER_STATE_EXPIRED, ORDER_STATE_REQUEST_ADD, ORDER_STATE_REQUEST_MODIFY, ORDER_STATE_REQUEST_CANCEL }; class="macro">#define ORDER_TYPE_CLOSE_BY(class="num">8) class="macro">#define ORDER_TYPE_BUY_STOP_LIMIT(class="num">9) class="macro">#define ORDER_TYPE_SELL_STOP_LIMIT(class="num">10) class="macro">#ifdef class="macro">#endif class="type">class="kw">double CorrectPricePending(class="kw">const class="type">class="kw">string symbol_name,class="kw">const ENUM_ORDER_TYPE order_type,class="kw">const class="type">class="kw">double price_set,class="kw">const class="type">class="kw">double price=class="num">0,class="kw">const class="type">int spread_multiplier=class="num">2) { class="type">class="kw">double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=class="num">0; class="type">int lv=StopLevel(symbol_name,spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
◍ 挂单价如何卡在 StopLevel 之外
MT5 的 StopLevel 是经纪商对挂单与市价最小距离的硬限制,直接用用户填的距离可能报 ERR_INVALID_STOPS。上面这段逻辑把 StopLevel 乘上 spread_multiplier(默认 2)作为底线,再和手动距离比大小,确保返回价一定在合法区。 BUY_LIMIT 取 fmin(pp-lv*pt, pp-distance_set*pt),也就是「比市价低 StopLevel 距离」和「比参考价低手动距离」里更靠下的那个;SELL_LIMIT 反之用 fmax 取更靠上的。BUY_STOP / BUY_STOP_LIMIT 共用 fmax(pp+lv*pt, pp+distance_set*pt),向下破位挂单不会贴在止损禁区内。 默认 spread_multiplier=2 意味着底线是两倍 StopLevel,对黄金这类点差跳变频繁的品种,能在数据行情减少拒单概率;若你的券商 StopLevel 已含点差,可传 1 回到单倍。 price 参数为 0 时自动取 SYMBOL_ASK(买类)或 SYMBOL_BID(卖类)作为基准,写 EA 时不用每次手动塞实时报价。
class="type">class="kw">double CorrectPricePending(class="kw">const class="type">class="kw">string symbol_name,class="kw">const ENUM_ORDER_TYPE order_type,class="kw">const class="type">int distance_set,class="kw">const class="type">class="kw">double price=class="num">0,class="kw">const class="type">int spread_multiplier=class="num">2) { class="type">class="kw">double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=class="num">0; class="type">int lv=StopLevel(symbol_name,spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS); class="kw">switch(order_type) { case ORDER_TYPE_BUY_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg); case ORDER_TYPE_BUY_STOP : case ORDER_TYPE_BUY_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg); case ORDER_TYPE_SELL_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg); case ORDER_TYPE_SELL_STOP : case ORDER_TYPE_SELL_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg); class="kw">default : Print(DFUN,TextByLanguage("Неправильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); class="kw">return class="num">0; } }
「挂单价格如何避让止损级别」
MT5 里 broker 给的 StopLevel 不是摆设,它规定了挂单与市场现价之间的最小距离。若你直接把 BUY_LIMIT 挂在比 ASK - lv*pt 更近的位置,订单可能被拒或瞬间异常成交。 CorrectPricePending 这个函数就是用来把用户设定的 price_set 往合规方向推。以买限价为例:当 price 参数为 0 时取 SYMBOL_ASK 作基准,最终返回 fmin(pp-lv*pt, price_set),也就是「止损级别外沿」和「用户意图价」里更靠下方的那个,再 NormalizeDouble 到小数位。 卖Stop类订单反过来用 fmax(pp+lv*pt, price_set) 处理,确保挂单价不会侵入 broker 禁挂区。spread_multiplier 默认 2,意味着 StopLevel 返回值本身已乘过 spread 倍数,实盘里黄金 XAUUSD 在波动时段 lv 可能跳到 30~50 点,挂单前不校这个就容易吃闷亏。 外汇与贵金属杠杆高、滑点随机,任何挂单距离计算都只是概率上的合规,不保证成交于预期价。开 MT5 把这段塞进 EA 的订单发送前一步,比手动填价稳得多。
class="type">class="kw">double CorrectPricePending(class="kw">const class="type">class="kw">string symbol_name,class="kw">const ENUM_ORDER_TYPE order_type,class="kw">const class="type">class="kw">double price_set,class="kw">const class="type">class="kw">double price=class="num">0,class="kw">const class="type">int spread_multiplier=class="num">2) { class="type">class="kw">double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=class="num">0; class="type">int lv=StopLevel(symbol_name,spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS); class="kw">switch((class="type">int)order_type) { case ORDER_TYPE_BUY_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,price_set),dg); case ORDER_TYPE_BUY_STOP : case ORDER_TYPE_BUY_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,price_set),dg); case ORDER_TYPE_SELL_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,price_set),dg); case ORDER_TYPE_SELL_STOP : case ORDER_TYPE_SELL_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,price_set),dg);
挂单价格如何卡在 StopLevel 之外
MT5 里下挂单最容易被拒的原因,是价格落在了经纪商要求的 StopLevel 之内。CorrectPricePending 这个函数就是专门把你要的挂单位置,按 StopLevel 和自定义距离双重约束重新算一遍,返回合规价格。 它先取品种点值 pt 和小数位 dg,再用 StopLevel(symbol_name, spread_multiplier) 拿到以点为单位的缓冲距离 lv,默认 spread_multiplier=2 意味着缓冲按 2 倍点差估算。BUY_LIMIT 和 SELL_STOP 类空单方向,取较小值:fmin(pp-lv*pt, pp-distance_set*pt),保证价格离市价足够远;BUY_STOP 与 SELL_LIMIT 类则取 fmax 向上推。 如果 order_type 不在六种挂单枚举内,函数直接 Print 报错并返回 0,调用方须判断返回值非零才继续下单。外汇与贵金属杠杆高,StopLevel 在重大数据前可能临时扩大,回测里跑通不等于实盘能成交。
class="type">class="kw">double CorrectPricePending(class="kw">const class="type">class="kw">string symbol_name,class="kw">const ENUM_ORDER_TYPE order_type,class="kw">const class="type">int distance_set,class="kw">const class="type">class="kw">double price=class="num">0,class="kw">const class="type">int spread_multiplier=class="num">2) { class="type">class="kw">double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=class="num">0; class="type">int lv=StopLevel(symbol_name,spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS); class="kw">switch((class="type">int)order_type) { case ORDER_TYPE_BUY_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg); case ORDER_TYPE_BUY_STOP : case ORDER_TYPE_BUY_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg); case ORDER_TYPE_SELL_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg); case ORDER_TYPE_SELL_STOP : case ORDER_TYPE_SELL_STOP_LIMIT : pp=(price==class="num">0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); class="kw">return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg); class="kw">default : Print(DFUN,TextByLanguage("Неправильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); class="kw">return class="num">0; } }
◍ 从订单状态反查成交属性
封装类里取成交填充方式与有效期,核心是按订单所处状态分流:活动单走 OrderGetInteger,历史单走 HistoryOrderGetInteger,拿不到就回 0。 MQL4 分支直接写死 ORDER_FILLING_RETURN 与 ORDER_TIME_GTC,因为老平台没有这些运行时字段,硬查会编译不过。 订单了结原因在 MQL4 下只能靠止损失败标记反推,命中就返回 ORDER_REASON_SL,否则落到默认分支;这种写法在跨版本移植时最容易漏改,建议开 MT5 把三个方法各跑一遍活动单与历史单对照返回值。 外汇与贵金属杠杆高,订单属性读错可能让你误判经纪商成交模式,实盘前务必用策略测试器验证。
class="kw">return (class="type">long)ORDER_FILLING_RETURN; 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_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_TYPE_FILLING); class="kw">break; case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_TYPE_FILLING);class="kw">break; class="kw">default : res=class="num">0; class="kw">break; } class="kw">return res; class="macro">#endif } class="type">long COrder::OrderTypeTime(class="type">void) class="kw">const { class="macro">#ifdef __MQL4__ class="kw">return (class="type">long)ORDER_TIME_GTC; 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_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_TYPE_TIME); class="kw">break; case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_TYPE_TIME);class="kw">break; class="kw">default : res=class="num">0; class="kw">break; } class="kw">return res; class="macro">#endif } class="type">long COrder::OrderReason(class="type">void) class="kw">const { class="macro">#ifdef __MQL4__ class="kw">return ( this.OrderCloseByStopLoss() ? ORDER_REASON_SL