轻松快捷开发 MetaTrader 程序的函数库(第九部分):与 MQL4 的兼容性 - 准备数据·进阶篇
🔧

轻松快捷开发 MetaTrader 程序的函数库(第九部分):与 MQL4 的兼容性 - 准备数据·进阶篇

(2/3)· 两千多个编译报错背后,其实是 MQL4 对常量和订单系统的认知空白

新手友好 第 2/3 篇
把为 MQL5 写好的函数库直接丢进 MQL4 目录编译,弹出的两千多条错误常让人以为代码全废了。其实绝大多数报错只是 MQL4 不认识 MQL5 的常量和枚举,并非逻辑崩溃。先看清错误来源,再谈兼容改造,能省掉大量无效重写。

「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 对照即可验证。

MQL5 / C++
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 随播价跳动,忽略它会直接报『成交价离市价太近』的错。

MQL5 / C++
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 时不用每次手动塞实时报价。

MQL5 / C++
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 的订单发送前一步,比手动填价稳得多。

MQL5 / C++
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 在重大数据前可能临时扩大,回测里跑通不等于实盘能成交。

MQL5 / C++
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 把三个方法各跑一遍活动单与历史单对照返回值。 外汇与贵金属杠杆高,订单属性读错可能让你误判经纪商成交模式,实盘前务必用策略测试器验证。

MQL5 / C++
  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
交给小布盯盘整理跨平台差异
这些 MQL4 与 MQL5 的常量映射和订单系统差异点,小布盯盘的 AIGC 已内置对照摘要,打开对应品种页即可快速回看,不必每次翻编译日志。

常见问题

主要因为 MQL4 编译器不认识 MQL5 专有的常量、枚举以及部分函数接口,并非代码逻辑全错,补齐常量和适配层即可消除大部分报错。
MQL4 历史订单列表提供的订单字段更少且没有成交数据,无法直接获取订单和成交明细,只能在场单与历史单之间做逻辑比对来推断事件。
可以,小布盯盘内置了两类终端的常量和订单系统差异摘要,能帮你快速定位需要补写的兼容代码位置,减少翻错误列表的时间。
在 DoEasy 根目录新建 ToMQL4.mqh,并在 Defines.mqh 最开头用 include 引入,这样 MQL4 编译时就能优先读到补写的常量与枚举。
先在本篇完成常量补齐和事件处理适配,下一篇才能基于干净编译的函数库去写跨平台交易类,关于交易类的完整讨论见《轻松快捷开发 MetaTrader 程序的函数库(第九部分):与 MQL4 的兼容性 - 准备数据·实战篇》。