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

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

(3/3)· 复制进 MT4 编译即爆 2000+ 错误?本篇用 ToMQL4 适配层彻底填平 MQL5 常量与订单系统的鸿沟

新手友好 第 3/3 篇

把为 MT5 写好的 DoEasy 函数库直接丢进 MT4 的 Include 目录编译,编辑器瞬间抛出两千多个错误,绝大多数来自 MQL4 根本不认识的 MQL5 常量和枚举。订单系统差异更狠:MT4 历史订单列表里既没有成交数据,字段也更少,原先的事件跟踪逻辑完全跑不起来。本篇是系列最后一篇,专治这种跨平台移植的硬骨头。

「从订单状态反推成交原因」

在 MT5 的订单封装类里,想拿到一笔单子为什么平掉,得先看它处在哪种状态。上面这段条件编译把 MQL4 和 MQL5 两套取法分开了:MQL5 下用 ORDER_PROP_STATUS 判别,再分别调 PositionGetIntegerOrderGetIntegerHistoryOrderGetIntegerHistoryDealGetIntegerREASON 字段。 如果单子是被止盈扫掉的,直接给 ORDER_REASON_TP(宏值 5);若魔术码非零则归到 EA 自发单 ORDER_REASON_EXPERT(宏值 3),其余情况落 WRONG_VALUE。这套分支能让你在回测里区分「系统止盈」和「策略主动平仓」两类退出,外汇与贵金属杠杆高,样本归因错了会直接误导仓位模型。 下面这组宏把平台没暴露全的枚举补上了:ORDER_TYPE_CLOSE_BY 为 8、BUY_STOP_LIMIT 为 9、SELL_STOP_LIMIT 为 10,成交策略里 ORDER_FILLING_RETURN 是 2,ORDER_TIME_GTC 为 0。开 MT5 把 CHistoryCollection::OrderSearch 里的 for(int i=start-1;i>=0;i--) 打日志,就能验证历史遍历是不是从尾向前扫。

MQL5 / C++
this.OrderCloseByTakeProfit() ? ORDER_REASON_TP :
this.OrderMagicNumber()!=class="num">0 ? ORDER_REASON_EXPERT : WRONG_VALUE
 );
class="macro">#else
  class="type">long res=WRONG_VALUE;
  class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
   {
    case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_REASON);                class="kw">break;
    case ORDER_STATUS_MARKET_ORDER      :
    case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_REASON);                      class="kw">break;
    case ORDER_STATUS_HISTORY_PENDING   :
    case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_REASON);class="kw">break;
    case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_REASON);  class="kw">break;
    class="kw">default                             : res=WRONG_VALUE;                                            class="kw">break;
   }
  class="kw">return res;
class="macro">#endif
}
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">#define ORDER_FILLING_RETURN(class="num">2)
class="macro">#define ORDER_TIME_GTC(class="num">0)
class="macro">#define ORDER_REASON_EXPERT(class="num">3)
class="macro">#define ORDER_REASON_SL(class="num">4)
class="macro">#define ORDER_REASON_TP(class="num">5)
class="type">ulong CHistoryCollection::OrderSearch(class="kw">const class="type">int start,ENUM_ORDER_TYPE &order_type)
 {
   class="type">ulong order_ticket=class="num">0;
class="macro">#ifdef __MQL5__
   for(class="type">int i=start-class="num">1;i>=class="num">0;i--)

◍ 历史订单遍历与对冲账户识别的兼容写法

在 MT5 历史订单回查里,MQL5 用 HistoryOrderGetTicket 按索引取票,MQL4 则靠 OrderSelect(i,SELECT_BY_POS,MODE_HISTORY) 按位置选单。两边差异不只是函数名,订单类型枚举范围也不同:MQL4 分支里用 type<ORDER_TYPE_BUY_LIMITtype>ORDER_TYPE_SELL_STOP 过滤挂单,把非挂单类直接 continue 掉,避免把已成交 deal 误当事件源。

CEvent 构造函数里那行 m_is_hedge 判断很关键:MQL5 下用 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 来确认零售对冲账户,MQL4 则直接写死 true。外汇和贵金属品种在对冲账户上同 symbol 多向持仓并存是常态,这套识别逻辑跑错,事件去重(IsPresentOrderInList)就会漏单,属于高杠杆环境下的隐性风险。 NewDealEventHedge 开头取 SYMBOL_ASK 用的是 deal.Symbol() 动态品种,而不是写死 _Symbol。如果你在 MT5 测试器里用多品种回测,这种写法能保证事件对象绑定到真实成交品种,否则用错卖价会让你在日志里看到一堆价格偏差告警。

MQL5 / C++
  {
      class="type">ulong ticket=::HistoryOrderGetTicket(i);
      if(ticket==class="num">0)
         class="kw">continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(ticket,ORDER_TYPE);
      if(this.IsPresentOrderInList(ticket,type))
         class="kw">continue;
      order_ticket=ticket;
      order_type=type;
   }
class="macro">#else
   for(class="type">int i=start-class="num">1;i>=class="num">0;i--)
   {
      if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY))
         class="kw">continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType();
      class="type">ulong ticket=::OrderTicket();
      if(ticket==class="num">0 || type<ORDER_TYPE_BUY_LIMIT || type>ORDER_TYPE_SELL_STOP)
         class="kw">continue;
      if(this.IsPresentOrderInList(ticket,type))
         class="kw">continue;
      order_ticket=ticket;
      order_type=type;
   }
class="macro">#endif
   class="kw">return order_ticket;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEvent::CEvent(class="kw">const ENUM_EVENT_STATUS event_status,class="kw">const class="type">int event_code,class="kw">const class="type">ulong ticket) : m_event_code(event_code),m_digits(class="num">0)
  {
   this.m_long_prop[EVENT_PROP_STATUS_EVENT]       =  event_status;
   this.m_long_prop[EVENT_PROP_TICKET_ORDER_EVENT] =  (class="type">long)ticket;
   this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif;
   this.m_digits_acc=class="macro">#ifdef __MQL4__ class="num">2 class="macro">#else (class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) class="macro">#endif;
   this.m_chart_id=::ChartID();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a hedging account event                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEventsCollection::NewDealEventHedge(COrder* deal,CArrayObj* list_history,CArrayObj* list_market)
  {
class="macro">#ifdef __MQL5__
   class="type">class="kw">double ask=::SymbolInfoDouble(deal.Symbol(),SYMBOL_ASK);

成交类型与持仓方向的枚举拆解

在 MT5 的成交事件回放里,先把底层枚举认清楚,才不会把余额变动和真实开仓混为一谈。下面这段枚举把账户里可能发生的 18 种 deal 类型一次性列全,其中 DEAL_TYPE_BUY 与 DEAL_TYPE_SELL 才是真正进场,其余如 DEAL_TYPE_COMMISSION、DEAL_TYPE_INTEREST 都只是资金流水。 除了成交类型,持仓方向只有两个值:POSITION_TYPE_BUY 和 POSITION_TYPE_SELL。而 DEAL_ENTRY 枚举更细一层,区分了 IN(开仓)、OUT(平仓)、INOUT(同笔反手)、OUT_BY(被另一单平掉),对冲账户里 OUT_BY 出现频率会明显偏高。 构造函数里有一行值得盯:m_is_hedge 在 MQL5 下通过 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 判定,说明同一品种能否多向持仓完全由账户保证金模式决定。外汇与贵金属杠杆高,零售对冲模式虽允许锁仓,但占用保证金与裸单边不同,回测前务必在 MT5 终端确认账户类型。

MQL5 / C++
class="type">class="kw">double bid=::SymbolInfoDouble(deal.Symbol(),SYMBOL_BID);
class=class="str">"cmt">//--- Market entry
class="macro">#endif
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| MQL5 deal types                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
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
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Position change method                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_DEAL_ENTRY
  {
   DEAL_ENTRY_IN,
   DEAL_ENTRY_OUT,
   DEAL_ENTRY_INOUT,
   DEAL_ENTRY_OUT_BY
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open position direction                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum class="type">ENUM_POSITION_TYPE
  {
   POSITION_TYPE_BUY,
   POSITION_TYPE_SELL
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEventsCollection::CEventsCollection(class="type">void) : m_trade_event(TRADE_EVENT_NO_EVENT),m_trade_event_code(TRADE_EVENT_FLAG_NO_EVENT)
  {
   this.m_list_events.Clear();
   this.m_list_events.Sort(SORT_BY_EVENT_TIME_EVENT);
   this.m_list_events.Type(COLLECTION_EVENTS_ID);
   this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif;
   this.m_chart_id=::ChartID();
   ::ZeroMemory(this.m_tick);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+

「引擎构造与历史余额类的初始化细节」

CEngine 的构造函数里先把 m_first_start 置为 true、交易事件标记设为无事件,随后用 EventSetMillisecondTimer(TIMER_FREQUENCY) 拉起毫秒级定时器;若返回失败会打印错误码,但程序不中断。 列表 m_list_counters 先 Sort 再 Clear,然后以 COLLECTION_COUNTER_ID / STEP / PAUSE 三个常量创建计数器对象,这部分决定了后续统计采集的节拍。 对冲账户判定用编译期与运行期双路径:MQL4 下直接认 true,MQL5 下则读 ACCOUNT_MARGIN_MODE 是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING,把结果塞进 m_is_hedge,外汇与贵金属账户须留意柜台模式差异带来的持仓逻辑分歧。 CHistoryBalance 继承 COrder,构造时强制传入 ticket 并把状态标为 ORDER_STATUS_BALANCE;它只声明了三组 SupportProperty 虚函数(整型、双精、字符串),用于运行时筛掉历史余额不支持的字段读写。

MQL5 / C++
CEngine::CEngine() : m_first_start(true),m_acc_trade_event(TRADE_EVENT_NO_EVENT)
  {
   ::ResetLastError();
   if(!::EventSetMillisecondTimer(TIMER_FREQUENCY))
      Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError());
   this.m_list_counters.Sort();
   this.m_list_counters.Clear();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                     HistoryBalance.mqh |
class=class="str">"cmt">//|                                                                  Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Order.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Historical balance operation                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHistoryBalance : class="kw">public COrder
  {
class="kw">public:
   class=class="str">"cmt">//--- Constructor
                     CHistoryBalance(class="kw">const class="type">ulong ticket) : COrder(ORDER_STATUS_BALANCE,ticket) {}
   class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property);
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property);
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+

MQL5 / C++
CEngine::CEngine() : m_first_start(true),m_acc_trade_event(TRADE_EVENT_NO_EVENT)
  {
   ::ResetLastError();
   if(!::EventSetMillisecondTimer(TIMER_FREQUENCY))
      Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError());
   this.m_list_counters.Sort();
   this.m_list_counters.Clear();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                     HistoryBalance.mqh |
class=class="str">"cmt">//|                                                                  Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Order.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Historical balance operation                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHistoryBalance : class="kw">public COrder
  {
class="kw">public:
   class=class="str">"cmt">//--- Constructor
                     CHistoryBalance(class="kw">const class="type">ulong ticket) : COrder(ORDER_STATUS_BALANCE,ticket) {}
   class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property);
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property);
   class="kw">virtual class="type">bool      SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 历史余额类怎么筛订单属性

在写 MT5 历史持仓统计类时,CHistoryBalance 用三组重载的 SupportProperty 去判断某笔订单是否支持指定的整型、双精度或字符串属性。整型里只放行票号、开仓时间、状态、类型、触发原因这 5 个字段,其余一律 return false,避免对历史余额记录误读不存在的字段。 双精度版本极简:仅 ORDER_PROP_PROFIT 返回 true,说明历史余额对象只认盈亏这一个浮点属性。字符串版本则反过来,symbol 与 ext_id 返回 false,其余字符串属性都放行,因为余额类订单本就不挂交易品种。 下方宏把几个易漏的枚举值钉死:ORDER_TYPE_CLOSE_BY=8、BUY_STOP_LIMIT=9、SELL_STOP_LIMIT=10,填充策略 RETURN=2,挂单时长 GTC=0,而原因里 SL=4、TP=5、BALANCE=6、CREDIT=7 都是回测分组时要单独捞出来的。 COrder::OrderPositionID 在 MQL4 分支直接取 MagicNumber 顶替持仓 ID;MT5 分支另有实现。复制这段代码到本地编译器,把 BALANCE / CREDIT 原因过滤出来,就能单独算入金出金对净值曲线的扰动。外汇与贵金属杠杆高,这类统计仅作风险复盘,不代表未来盈亏。

MQL5 / C++
class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property)
  {
   if(class="kw">property==ORDER_PROP_TICKET     ||
      class="kw">property==ORDER_PROP_TIME_OPEN  ||
      class="kw">property==ORDER_PROP_STATUS     ||
      class="kw">property==ORDER_PROP_TYPE       ||
      class="kw">property==ORDER_PROP_REASON
     ) class="kw">return true;
   class="kw">return false;
  }
class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property)
  {
   class="kw">return(class="kw">property==ORDER_PROP_PROFIT ? true : false);
  }
class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property)
  {
   if(class="kw">property==ORDER_PROP_SYMBOL || class="kw">property==ORDER_PROP_EXT_ID)
      class="kw">return false;
   class="kw">return true;
  }
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">#define ORDER_FILLING_RETURN(class="num">2)
class="macro">#define ORDER_TIME_GTC(class="num">0)
class="macro">#define ORDER_REASON_EXPERT(class="num">3)
class="macro">#define ORDER_REASON_SL(class="num">4)
class="macro">#define ORDER_REASON_TP(class="num">5)
class="macro">#define ORDER_REASON_BALANCE(class="num">6)
class="macro">#define ORDER_REASON_CREDIT(class="num">7)
class="type">long COrder::OrderPositionID(class="type">void) class="kw">const
  {
class="macro">#ifdef __MQL4__
   class="kw">return ::OrderMagicNumber();
class="macro">#else
class=class="str">"cmt">//+------------------------------------------------------------------+

订单状态与来源在 MQL5 下的取法

在 MQL5 里,COrder 类把历史订单和当前订单的状态查询拆成了两套接口。若订单处于历史挂单或历史市价单状态,要用 HistoryOrderGetInteger(m_ticket, ORDER_STATE) 取状态;若是当前市场的挂单或市价单,则走 OrderGetInteger(ORDER_STATE)。 下面这段是 OrderState() 在 MQL5 分支的核心逻辑,按内部状态枚举分流: long res=0; switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_STATE); break; case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_STATE); break; case ORDER_STATUS_MARKET_POSITION : case ORDER_STATUS_DEAL : default : res=0; break; } return res; 注意市场持仓和成交记录会落入 default 返回 0,说明这两类并不通过订单对象暴露 ORDER_STATE。 OrderReason() 在 MQL4 下靠余额、信用、SL/TP、魔术码反推来源;MQL5 分支原文在此截断,实际应直接读 ORDER_PROP_REASON 属性。开 MT5 新建脚本打印 HistoryOrderGetInteger 与 OrderGetInteger 的返回值差异,能验证历史与实时订单的状态字段并非同源。

MQL5 / C++
class="type">long 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=::HistoryOrderGetInteger(m_ticket,ORDER_STATE); class="kw">break;
   case ORDER_STATUS_MARKET_ORDER      :
   case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_STATE);                 class="kw">break;
   case ORDER_STATUS_MARKET_POSITION   :
   case ORDER_STATUS_DEAL              :
   class="kw">default                             : res=class="num">0;                                              class="kw">break;
  }
class="kw">return res;

「挂单残量与平仓缘由的文本化」

在封装订单类时,有两个细节容易在回测日志里被忽略:一是历史挂单的未成交手数,二是平仓动作背后的触发原因。把这两类信息用代码直接暴露出来,能省掉大量肉眼比对成交回执的时间。 MQL4 环境下,若订单状态为历史挂单(ORDER_STATUS_HISTORY_PENDING),OrderVolumeCurrent 直接返回 ::OrderLots();否则返回 0,意味着非挂单历史记录没有‘未执行_volume’概念。切换到 MQL5 分支后该函数体为空,说明标准库在五版里改走交易对象的自有字段,迁移脚本时要留意这套差异。 GetReasonDescription 用一串三元表达式把俄语/英语双语文本映射到具体缘由:止损触发、止盈触发、EA 下单单、被取消、余额存取、信用增减。其中余额与信用项还嵌套了 Profit()>0 的判断——盈利侧显示‘入金/授信’,亏损侧显示‘出金/扣减’。 别把正态当圣经:这些文本只是给日志和人类读屏用的,真要做统计分析请直接读 Reason() 枚举值,字符串比对在跨语言 MT5 终端上可能漏判。 开 MT5 把这段塞进你自己的 COrder 派生类,跑一轮 EURUSD 回测,看历史选项卡里‘Due to StopLoss’出现次数是否和预期止损单总数一致,就能验证封装有没有漏掉分支。

MQL5 / C++
class="type">class="kw">double COrder::OrderVolumeCurrent(class="type">void) class="kw">const
  {
class="macro">#ifdef __MQL4__
   class="kw">return(this.Status()==ORDER_STATUS_HISTORY_PENDING ? ::OrderLots() : class="num">0);
class="macro">#else

class="type">class="kw">string COrder::GetReasonDescription(class="kw">const class="type">long reason) class="kw">const
  {
class="macro">#ifdef __MQL4__
   class="kw">return
     (
       this.IsCloseByStopLoss()          ? TextByLanguage("Срабатывание StopLoss","Due to StopLoss")                :
       this.IsCloseByTakeProfit()         ? TextByLanguage("Срабатывание TakeProfit","Due to TakeProfit")            :
       this.Reason()==ORDER_REASON_EXPERT ? TextByLanguage("Выставлен из mql4-программы","Placed from mql4 program")  :
       this.Comment()=="cancelled"        ? TextByLanguage("Отменён","Cancelled")                                    :
       this.Reason()==ORDER_REASON_BALANCE ? (
                                            this.Profit()>class="num">0 ? TextByLanguage("Пополнение баланса","Deposit of funds on the account balance") :
                                                               TextByLanguage("Снятие средств с баланса","Withdrawal from the balance")
                                          ) :
       this.Reason()==ORDER_REASON_CREDIT  ? (
                                            this.Profit()>class="num">0 ? TextByLanguage("Начисление кредитных средств","Received credit funds") :

◍ 历史订单刷新里的 MQL4 分支处理

在跨 MQL4/MQL5 的订单历史集合类里,刷新逻辑用预编译宏做了硬切分。MQL4 下走 OrdersHistoryTotal 配合 OrderSelect 按位遍历,MQL5 则靠独立对象类接管,二者头文件引入也不同。 MQL4 分支中,Refreshm_index_order 游标开始扫历史池,只处理 order_type < ORDER_TYPE_BUY_LIMIT(已平持仓)与 order_type > ORDER_TYPE_SELL_STOP(余额/信用操作)两类。前者塞进 m_list_all_orders 排序表,后者单独建 CHistoryBalance 对象。 注意那段被高亮的 #include "..\Objects\Orders\HistoryBalance.mqh" 仅在 __MQL4__ 定义时编译,MQL5 无此文件——直接抄这套 include 到 MT5 会报找不到文件。外汇与贵金属交易自带高杠杆风险,回测历史订单结构前先确认终端版本,避免编译期就卡死。

MQL5 / C++
class="macro">#ifdef __MQL4__
class="macro">#include "..\Objects\Orders\HistoryBalance.mqh"
class="macro">#endif
class="type">void CHistoryCollection::Refresh(class="type">void)
  {
class="macro">#ifdef __MQL4__
   class="type">int total=::OrdersHistoryTotal(),i=m_index_order;
   for(; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)) class="kw">continue;
      ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)::OrderType();
      class=class="str">"cmt">//--- Closed positions
      if(order_type<ORDER_TYPE_BUY_LIMIT)
        {
         CHistoryOrder *order=new CHistoryOrder(::OrderTicket());
         if(order==NULL) class="kw">continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the list"));
            class="kw">delete order;
           }
        }
      class=class="str">"cmt">//--- Balance/credit operations
      else if(order_type>ORDER_TYPE_SELL_STOP)
        {
         CHistoryBalance *order=new CHistoryBalance(::OrderTicket());

历史订单属性支持判断与列表插入逻辑

在 MQL5 历史订单封装里,往全量订单列表塞对象时先判空:若 new 出来的 CHistoryPending 或普通 order 为 NULL 就直接 continue,避免后续对空指针做 InsertSort 导致运行时崩。 InsertSort 失败的分支里会打印双语报错(俄/英“Could not add order to the list”)并 delete order 释放内存,这一步在高频刷新历史池时很关键,否则悬空指针会慢慢吃掉终端内存。 计算 delta_order = i - m_index_order 后,用 m_delta_order!=0 来置 m_is_trade_event 布尔位;这意味着只有订单索引真正偏移时才算交易事件,单纯重绘不触发。 SupportProperty 函数对 ORDER_PROP_PRICE_STOP_LIMIT 的处理有版本差异:MQL5 下仅当订单类型不在 BUY_STOP_LIMIT~SELL_STOP_LIMIT 区间才返回 false,而 MQL4 分支要求状态必须是 HISTORY_ORDER 才允许该属性,否则一律 false。开 MT5 把这段贴进 EA 调试,改一下 ORDER_TYPE 边界就能看到属性可用性变化。

MQL5 / C++
if(order==NULL) class="kw">continue;
if(!this.m_list_all_orders.InsertSort(order))
  {
  ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the list"));
  class="kw">delete order;
  }
}
else
  {
  class=class="str">"cmt">//--- Removed pending orders
  CHistoryPending *order=new CHistoryPending(::OrderTicket());
  if(order==NULL) class="kw">continue;
  if(!this.m_list_all_orders.InsertSort(order))
    {
    ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the list"));
    class="kw">delete order;
    }
  }
class=class="str">"cmt">//---
  class="type">int delta_order=i-m_index_order;
  this.m_index_order=i;
  this.m_delta_order=delta_order;
  this.m_is_trade_event=(this.m_delta_order!=class="num">0 ? true : false);
class=class="str">"cmt">//--- __MQL5__
class="macro">#else
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return &class="macro">#x27;true&class="macro">#x27; if an order supports the passed class="kw">property, otherwise, class="kw">return &class="macro">#x27;false&class="macro">#x27;                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CHistoryOrder::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property)
  {
  if(
  class="macro">#ifdef __MQL5__
    class="kw">property==ORDER_PROP_PROFIT              ||
    class="kw">property==ORDER_PROP_PROFIT_FULL         ||
    class="kw">property==ORDER_PROP_SWAP                ||
    class="kw">property==ORDER_PROP_COMMISSION          ||
    class="kw">property==ORDER_PROP_PRICE_CLOSE         ||
    (
     class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT   &&
     (
      this.TypeOrder()<ORDER_TYPE_BUY_STOP_LIMIT ||
      this.TypeOrder()>ORDER_TYPE_SELL_STOP_LIMIT  
     )
    )
  class="macro">#else
    class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT    &&
    this.Status()==ORDER_STATUS_HISTORY_ORDER
  class="macro">#endif
   ) class="kw">return false;
  class="kw">return true;
  }

「在 MT4 里跑通历史集合列表的验证」

把第三篇文章里用的测试 EA(原路径 \MQL5\Experts\TestDoEasy\Part03 下的 TestDoEasyPart03_1.mq5)直接塞进 \MQL4\Experts\TestDoEasy\Part09,改名 TestDoEasyPart09.mq4 即可编译。EA 本身逻辑没动,但 MQL4 没有 MQL5 的成交(deal)概念,原文里用条件编译把成交硬替换成余额操作——这种做法只适合内部函数库验证,正式发给用户的产品必须把语言差异藏干净。 启动前先在终端“帐户历史记录”右键选“所有历史记录”。MT4 给 EA 开放的历史深度完全取决于这里选的范围,选小了 list.Total() 可能直接返回 0。EA 只在 OnInit() 跑一次,所以改完设置列表后得重新加载才能看到刷新。 实测时我新开的 MT4 账户原本空仓,挂了卖单带止损止盈,离开一会回来被损掉、行情随后朝空头走——典型止损就被扫的尴尬。但正好留下一笔平仓,设置里选“在场订单”就能在日志里核对平仓搜索是否正常;再删几组挂单、选“挂单”就能列出已删除挂单。外汇和贵金属杠杆高,这类验证请在模拟账户先跑。 代码里 TYPE_ORDER_DEAL 分支(标红处)在 MQL4 实际不会命中,因为 ORDER_STATUS_DEAL 状态来自 MQL5 成交,MT4 里要改用余额操作状态判断。OnInit 中 history.Refresh() 后按时间区间取集合,循环里按 InpOrderType 分流打印,这就是你复制去改的最小骨架。

MQL5 / C++
class=class="str">"cmt">//--- enums
enum ENUM_TYPE_ORDERS
  {
   TYPE_ORDER_MARKET,   class=class="str">"cmt">// Market orders
   TYPE_ORDER_PENDING,  class=class="str">"cmt">// Pending orders
   TYPE_ORDER_DEAL      class=class="str">"cmt">// Deals
   };
class=class="str">"cmt">//--- class="kw">input parameters
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- update history
   history.Refresh();
class=class="str">"cmt">//--- get the collection list within the date range
   CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE);
   if(list==NULL)
     {
      Print("Could not get collection list");
      class="kw">return INIT_FAILED;
     }
   class="type">int total=list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the order from the list
      COrder* order=list.At(i);
      if(order==NULL) class="kw">continue;
      class=class="str">"cmt">//--- if this is a deal
      if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL)
         order.Print();
      class=class="str">"cmt">//--- if this is a historical market order
      if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET)
         order.Print();
      class=class="str">"cmt">//--- if this is a removed pending order
      if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING)
         order.Print();
     }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestDoEasyPart03_1.mq4 |
class=class="str">"cmt">//|    Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//--- includes
class="macro">#include <DoEasy\Collections\HistoryCollection.mqh>
class=class="str">"cmt">//--- enums
enum ENUM_TYPE_ORDERS
  {

◍ 按时间区间抽取历史订单并分类打印

这段逻辑用 CHistoryCollection 把账户历史按时间闭区间捞出来,再按订单状态分流打印,省去手动翻终端历史标签的麻烦。 枚举里区分了市价单、挂单,以及在 MQL5 环境下用 TYPE_ORDER_DEAL 代表成交、MQL4 回退到 TYPE_ORDER_BALANCE 指代余额与信用操作,编译期由 __MQL5__ 宏切换。 OnInit 中先 history.Refresh() 强制刷新,再用 GetListByTime(InpTimeBegin, InpTimeEnd, SELECT_BY_TIME_CLOSE) 取按平仓时间筛选的列表;若返回 NULL 直接 INIT_FAILED,说明区间参数或历史读取异常。 遍历时用 order.Status() 比对:ORDER_STATUS_DEAL 对应成交、ORDER_STATUS_HISTORY_ORDER 对应历史市价单、ORDER_STATUS_HISTORY_PENDING 对应已删除挂单,匹配 InpOrderType 才调用 order.Print()。 实盘接这段时把 InpTimeBegin 设成 D'2024.01.01',InpTimeEnd 留 END_TIME,能在 MT5 日志里直接看清该年所有市价单盈亏分布;外汇与贵金属杠杆高,历史统计仅作概率参考,不代表未来走向。

MQL5 / C++
  TYPE_ORDER_MARKET,   class=class="str">"cmt">// Market orders
  TYPE_ORDER_PENDING,  class=class="str">"cmt">// Pending orders
class="macro">#ifdef __MQL5__
  TYPE_ORDER_DEAL       class=class="str">"cmt">// Deals
class="macro">#else 
  TYPE_ORDER_BALANCE   class=class="str">"cmt">// Balance/Credit
class="macro">#endif
};
class=class="str">"cmt">//--- class="kw">input parameters
class="kw">input ENUM_TYPE_ORDERS   InpOrderType   =  TYPE_ORDER_MARKET;   class=class="str">"cmt">// Show type:
class="kw">input class="type">class="kw">datetime           InpTimeBegin   =  class="num">0;                   class=class="str">"cmt">// Start date of required range
class="kw">input class="type">class="kw">datetime           InpTimeEnd     =  END_TIME;             class=class="str">"cmt">// End date of required range
class=class="str">"cmt">//--- global variables
CHistoryCollection history;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- update history
   history.Refresh();
class=class="str">"cmt">//--- get the collection list within the date range
   CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE);
   if(list==NULL)
     {
       Print("Could not get collection list");
       class="kw">return INIT_FAILED;
     }
   class="type">int total=list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
       class=class="str">"cmt">//--- get the order from the list
       COrder* order=list.At(i);
       if(order==NULL) class="kw">continue;
  class=class="str">"cmt">//--- if this is a deal
  class="macro">#ifdef __MQL5__
       if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL)
          order.Print();
  class="macro">#else 
  class=class="str">"cmt">//--- if this is a balance/credit operation
       if(order.Status()==ORDER_STATUS_BALANCE && InpOrderType==TYPE_ORDER_BALANCE)
          order.Print();
  class="macro">#endif 
  class=class="str">"cmt">//--- if this is a historical market order
       if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET)
          order.Print();
   class=class="str">"cmt">//--- if this is a removed pending order
       if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING)
          order.Print();
     }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---

  }

先给 EA 留个空壳 OnTick

MQL5 里任何自动交易逻辑的入口都绕不开 OnTick()。系统每来一个报价就调一次这个函数,你后面要挂的均线穿越、ATR 过滤全得塞进它的花括号里。 下面这段是 MT5 新建 EA 时最朴素的骨架:只有函数声明和一对空大括号,编译能通过,但什么都不做。它的价值在于让你先确认编译器环境没问题,再往里填策略。 外汇与贵金属杠杆高、滑点随机,空壳 EA 不会下单也就没有即时风险,但一旦你往里写持仓逻辑,就必须自己兜底平仓与风控。

MQL5 / C++
class=class="str">"cmt">//| Expert tick function                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「记住这一条就够了」

这套跨平台函数库目前只暴露一个入口类 CEngine,在 EA 里写 CEngine lib; 然后敲 lib. 就能拉出全部可用方法列表,绝大多数方法名自带语义,跟着前八篇的测试 EA 跑一遍就能摸清调用路径。 库文件分 MQL5.zip 与 MQL4.zip 两个包,体积均为 89.98 KB,评论区有人实测从旧构建到新构建没出现过强制修补断点,跨版本稳定性是它最实在的优势。 外汇和贵金属波动剧烈、杠杆风险高,直接把库搬进实盘前先在策略测试器用演示账户验证每处调用;下一篇才会补齐持仓与挂单激活的处理,现在缺的那块得自己用 CTrade 类先顶上。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到编译错误聚类与跨平台常量差异提示,你只需专注改适配层逻辑。

常见问题

MT4 不认识 MQL5 的常量和枚举,且缺少部分 MQL5 函数,需在 ToMQL4.mqh 中补齐定义并用 MQL4 函数重写逻辑。
需在逻辑上比较在场与历史订单列表的变化,依据差异推导下单、平仓、修改等事件,单独写一套 MQL4 事件处理。
可以,小布内置的 AIGC 能标记跨平台常量缺失和订单系统差异点,减少手动翻错误列表的时间。
在 Defines.mqh 最开头用 #include 指向 ToMQL4.mqh,这样整个库在 MQL4 编译时都能读到兼容定义。
它们已是跨平台函数库的基础,本篇在此基础上加 MQL4 兼容层,使交易类在 MT4 和 MT5 都能跑。