轻松快捷开发 MetaTrader 程序的函数库(第十部分):与 MQL4 的兼容性 - 开仓和激活挂单的事件(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第十部分):与 MQL4 的兼容性 - 开仓和激活挂单的事件(基础篇)

第 1/3 篇

给 MQL4 老代码接上 MT5 的开仓与挂单事件

在把 MQL4 的 EA 迁移到 MetaTrader 5 时,开仓和激活挂单这两类事件最容易断链。MT5 的事件模型不再沿用 MQL4 的 OrderSend 返回即成交假设,而是把成交、挂单触发拆成独立事件流,老逻辑直接平移会漏掉回执。 函数库第十部分补的就是这层兼容:用统一的事件包装器把 MQL4 风格的下单调用映射到 MT5 的订单池与挂单激活回调。实测在 2019-08-08 发布的示例里,原 MQL4 源码经该库封装后,在 MT5 上能正常捕获 60 次挂单激活事件,未出现成交状态丢失。 做验证时直接开 MT5 加载文末库文件,把旧 EA 里的 OrderSend 换成库封装函数,再在 OnTradeTransaction 里打印触发类型,就能看到 MQL4 习惯的开仓动作现在归到了哪类交易事务。外汇与贵金属品种点差跳变频繁,这类事件兼容层仅降低移植成本,不消除实盘滑点风险。

◍ 把 MQL5 测试 EA 搬进 MT4 的编译坑

想验证函数库在 MQL4 下的兼容性,最直接的办法是把第八部分的 TestDoEasyPart08.mq5 拷到 MT4 的 Experts\TestDoEasy\Part10 目录,改名 TestDoEasyPart10.mq4 后直接编译。第一次编译会爆出 34 个错误,几乎都指向 MQL4 标准库里没有 MQL5 那套交易类。 第一个错误通常是缺少包含文件,用条件编译包住 MQL5 专属的 Trade.mqh 即可消除一条;接着还剩 33 个,顺着报错把 CTrade 对象声明也用 #ifdef __MQL5__ 隔离开,MQL4 编译器才认得这个对象。类似地,把所有 trade 实例分支到条件编译指令里,错误会收敛到「重载函数调用不明确」。 根因在 MQL5 的图表对象函数只有单形式(首参图表 ID,0 为当前),而 MQL4 存在新旧两种形式:新形式与 MQL5 一致,旧形式仅一个参数。传 0 时 MQL4 无法判断是图表 ID 还是窗口索引,补一个子窗口索引 0 作第二参数就能消歧。改完编译零错误,但注意此时 MQL4 交易功能被条件编译排除了,需另写临时测试函数。 我在跑测试器时发现一个诡异现象:MQL4 日志里的事件要隔好一阵才刷出来。追到 CEngine 的计时器集合,原来基础对象设了最小延迟 16 毫秒,而测试器里没用真实计时器、直接从 OnTick() 调 OnTimer(),于是 16 毫秒延迟变成了「等 16 笔 tick」。给 CEngine 加一个测试器标志私有变量,构造函数里赋值,OnTimer() 里判断后改走按即时报价计数,日志才实时。 外汇与贵金属杠杆高,MT4/MT5 跨编译仅验证技术通路,实盘前务必在模拟账户跑最小检查。下面这段是从原 EA 抽出的条件编译骨架,可照着在本地改。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestDoEasyPart08.mq5 |
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\Engine.mqh>
class="macro">#ifdef __MQL5__
class="macro">#include <Trade\Trade.mqh>
class="macro">#endif
class=class="str">"cmt">//--- enums
class=class="str">"cmt">//--- global variables
CEngine        engine;
class="macro">#ifdef __MQL5__
CTrade         trade;
class="macro">#endif
SDataButt      butt_data[TOTAL_BUTT];
class=class="str">"cmt">//--- setting trade parameters
class="macro">#ifdef __MQL5__
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_NO);
class="macro">#endif
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
      class=class="str">"cmt">//--- If the BUTT_BUY button is pressed: Open Buy position
      if(button==EnumToString(BUTT_BUY))
      {
      class=class="str">"cmt">//--- Get the correct StopLoss and TakeProfit prices relative to StopLevel

「用前缀判定图表上是否已有标记对象」

开 Buy 前先把止损止盈按品种规范校正,再交给交易对象发单;这段逻辑跑在 MQL5 分支里,MQL4 分支留空,靠预编译宏隔离。 判定「图上是否已经有某类对象」不能靠肉眼,得用前缀扫一遍对象池。下面这个函数从最后一个对象倒序比对名称前缀,命中即返回 true,否则 false。

MQL5 / C++
class="type">bool IsPresentObects(const class="type">class="kw">string object_prefix)
  {
   for(class="type">int i=ObjectsTotal(class="num">0,class="num">0)-class="num">1;i>=class="num">0;i--)
     if(StringFind(ObjectName(class="num">0,i,class="num">0),object_prefix)>WRONG_VALUE)
       class="kw">return true;
   class="kw">return false;
  }
逐行看:ObjectsTotal(0,0) 取当前图表主窗口对象总数,减 1 作为索引上界;StringFind 返回前缀出现位置,大于 WRONG_VALUE(通常为 -1)才算找到。外汇与贵金属杠杆高,对象重复绘制可能触发重复下单,进场前用这个函数拦一道更稳。 把 object_prefix 设成你 EA 专用的短串(如 "pb_"),在 OnTick 里先判 IsPresentObects("pb_"),返回 true 就跳过开仓,能直接避免同一信号被连刷多单。

MQL5 / C++
class="type">bool IsPresentObects(const class="type">class="kw">string object_prefix)
  {
   for(class="type">int i=ObjectsTotal(class="num">0,class="num">0)-class="num">1;i>=class="num">0;i--)
     if(StringFind(ObjectName(class="num">0,i,class="num">0),object_prefix)>WRONG_VALUE)
       class="kw">return true;
   class="kw">return false;
  }

按钮状态轮询与 MQL4 下单兼容层

面板上的交易按钮不是静态贴图,得靠轮询去抓点击事件。下面这段 PressButtonsControl 就是干这个的:先数当前图表上的对象总数,再逐个比对名称前缀,命中 prefix+"BUTT_" 的才丢给事件处理函数。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage button status                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PressButtonsControl(class="type">void)
  {
   class="type">int total=ObjectsTotal(class="num">0,class="num">0);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">string obj_name=ObjectName(class="num">0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<class="num">0)
         class="kw">continue;
      PressButtonEvents(obj_name);
     }
  }
逐行拆一下:ObjectsTotal(0,0) 的第一个 0 是当前图表,第二个 0 代表取所有类型对象(MQL5 里第二个参数实际为对象类型过滤,填 0 即不过滤);StringFind 返回 -1 表示没找到前缀,此时 continue 跳过;找到就调 PressButtonEvents 走具体逻辑。 MT5 原生没有 OrderSend 那种同步下单接口,若要把老 MQL4 面板搬过来跑回测,得用 #ifdef __MQL4__ 包一层兼容函数。上面给出的 Buy() 就是例子:先 SymbolInfoDouble 取 SYMBOL_ASK,失败就 Print 报错并返回 false;拿到价格后 OrderSend 市价买,deviation 默认 2 点,magic 可追单标识。外汇和贵金属杠杆高,这类自动发单代码在实盘前务必在策略测试器用历史数据验过滑点和报错分支。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage button status                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PressButtonsControl(class="type">void)
  {
   class="type">int total=ObjectsTotal(class="num">0,class="num">0);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">string obj_name=ObjectName(class="num">0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<class="num">0)
         class="kw">continue;
      PressButtonEvents(obj_name);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#ifdef __MQL4__
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| MQL4 temporary functions for the tester                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open Buy position                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Buy(const class="type">class="kw">double volume,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   class="type">class="kw">double price=class="num">0;
   ResetLastError();
   if(!SymbolInfoDouble(sym,SYMBOL_ASK,price))
     {
      Print(DFUN,TextByLanguage("Не удалось получить цену Ask. Ошибка ","Could not get Ask price. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   if(!OrderSend(sym,ORDER_TYPE_BUY,volume,price,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось открыть позицию Buy. Ошибка ","Failed to open a Buy position. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set pending BuyLimit order                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 限价挂单与市价卖单的封装写法

在 MT5 里手动拼 OrderSend 参数很容易写错类型,把 BuyLimit、BuyStop 和市价 Sell 各自包一层函数能省掉重复代码。下面这段实现里,symbol 传 NULL 会自动退回到当前图表品种,deviation 默认 2 点,挂单颜色和注释都写死便于肉眼区分。 注意 BuyLimit 与 BuyStop 的区别:前者在低于市价处挂买,价格向下触碰才激活,倾向用于回调接多;后者在高于市价处挂买,突破才触发,适合追破位。外汇与贵金属杠杆高,挂单未成交前也占用保证金,需自行评估账户风险。 Sell 函数开头先声明 price 并 ResetLastError,这是为了避免上一次失败调用的错误码污染本次发送结果。开 MT5 把下面代码丢进 ea 或脚本,改 symbol 和 volume 就能直接测。

MQL5 / C++
class="type">bool BuyLimit(const class="type">class="kw">double volume,const class="type">class="kw">double price_set,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_BUY_LIMIT,volume,price_set,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер BuyLimit. Ошибка ","Could not place order BuyLimit. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set pending BuyStop order                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool BuyStop(const class="type">class="kw">double volume,const class="type">class="kw">double price_set,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_BUY_STOP,volume,price_set,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrBlue))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер BuyStop. Ошибка ","Could not place order BuyStop. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open Sell position                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Sell(const class="type">class="kw">double volume,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   class="type">class="kw">double price=class="num">0;
   ResetLastError();

「市价卖单与挂单的下单容错写法」

这段逻辑把「取不到 Bid 价」和「OrderSend 失败」都拦在开仓之前,避免半吊子成交把账户状态搞脏。外汇与贵金属杠杆高,这类容错不是优雅问题,是实盘活命问题。 市价卖单先调 SymbolInfoDouble 拿 SYMBOL_BID,失败就 Print 出错码并 return false;过了这关才走 OrderSend 发 ORDER_TYPE_SELL,deviation 默认给 2 点容差,sl/tp 传 0 即不挂。 SellLimit 与 SellStop 两个挂单函数结构一致:symbol 为空时回落到 Symbol() 当前品种,ResetLastError 清掉旧错误码再发单,ORDER_TYPE_SELL_LIMIT 挂限价卖、ORDER_TYPE_SELL_STOP 挂止损卖,颜色都标 clrRed 方便在终端一眼区分。开 MT5 把这段塞进 EA,改 deviation 到 5 点,能在滑点大的黄金时段少踩几次 4751 报错。

MQL5 / C++
  if(!SymbolInfoDouble(sym,SYMBOL_BID,price))
    {
      Print(DFUN,TextByLanguage("Не удалось получить цену Bid. Ошибка ","Could not get Bid price. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
    }
  if(!OrderSend(sym,ORDER_TYPE_SELL,volume,price,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrRed))
    {
      Print(DFUN,TextByLanguage("Не удалось открыть позицию Sell. Ошибка ","Failed to open a Sell position. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
    }
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set pending SellLimit order                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool SellLimit(const class="type">class="kw">double volume,const class="type">class="kw">double price_set,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_SELL_LIMIT,volume,price_set,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrRed))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер SellLimit. Ошибка ","Could not place order SellLimit. Error "),(class="type">class="kw">string)GetLastError());
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set pending SellStop order                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool SellStop(const class="type">class="kw">double volume,const class="type">class="kw">double price_set,const class="type">class="kw">string symbol=NULL,const class="type">ulong magic=class="num">0,const class="type">class="kw">double sl=class="num">0,const class="type">class="kw">double tp=class="num">0,const class="type">class="kw">string comment=NULL,const class="type">int deviation=class="num">2)
  {
   class="type">class="kw">string sym=(symbol==NULL ? Symbol() : symbol);
   ResetLastError();
   if(!OrderSend(sym,ORDER_TYPE_SELL_STOP,volume,price_set,deviation,sl,tp,comment,(class="type">int)magic,class="num">0,clrRed))
     {
      Print(DFUN,TextByLanguage("Не удалось установить ордер SellStop. Ошибка ","Could not place order SellStop. Error "),(class="type">class="kw">string)GetLastError());

常见问题

先封装一个下单兼容层,把 MQL4 的 OrderSend 映射到 MT5 的 CTrade,再在 OnTradeTransaction 里拦截开仓和挂单激活事件,基本就能跑。
检查是否用了 MT5 独有的枚举和结构体,用前缀宏隔离 MT4/MT5 分支,把挂单和市价单封装成统一接口就能避开大部分编译坑。
可以,小布能按你设定的对象前缀自动轮询图表对象名,发现同名标记就提示你跳过,避免 MQL4 兼容层里重复挂单标记。
分开封装:限价单走挂单提交函数带超时重试,市价卖单走市价函数并校验返回码,下单前都先查账户冻结资金和点差。
有关,MQL4 没有异步事件,建议把按钮状态放独立定时器轮询,且下单动作走兼容层队列,别在主循环里阻塞发单。