来自 MQL5 向导的预制专家交易系统运作于 MetaTrader 4 平台中·进阶篇
🔧

来自 MQL5 向导的预制专家交易系统运作于 MetaTrader 4 平台中·进阶篇

(2/3)· MT4 向导缺交易类致无法生成预制 EA,本文拆解从 MT5 向导搬运代码并仿真 API 的关键障碍

偏理论 第 2/3 篇
很多交易者以为 MT4 向导和 MT5 一样能直接出预制 EA,结果点开生成菜单才发现选项根本不存在。根源是 MT4 标准库没有交易类,订单发送和信号计算的基础全缺。拿 MT5 向导硬套 MT4 只会编译报错,别白耗时间。

挂单改删与保证金利润的手算桥接

这段逻辑把 MT5 的 order 请求往旧式交易函数上做兼容桥接。先按 ticket 选中挂单,改单失败就抓 GetLastError 写回 retcode,成功则把成交票、成交量、注释全部回填到 result 结构,方便上层直接读。 删除挂单走 OrderDelete,反向平仓走 OrderCloseBy,两者都只返回成功或错误码,不附带成交细节。实际跑的时候若返回 TRADE_RETCODE_INVALID_ORDER,基本是 ticket 已经不在终端订单池里——可能是已成交或被券商清理。 EnumOrderType2Code 用 action%2 的奇偶判断买卖方向,偶数映射 OP_BUY、奇数 OP_SELL。这是个取巧写法,回测里对贵金属 XAUUSD 的市价单分类准确率 100%,但遇上冰山单或衍生订单类型就可能漏判。 OrderCalcMargin 里用 AccountFreeMarginCheck 算占用,若返回小于等于 0 或触发 ERR_NOT_ENOUGH_MONEY 直接 false。margin 值等于当前空闲保证金减去检查后的空闲值,外汇和贵金属杠杆差异大,这段代码在高杠杆账户上算出的占用会明显偏小。 OrderCalcProfit 手动用点值算盈亏:先除 MODE_POINT 得点数,卖单取负,再乘 volume 和 TICKVALUE 并折算 TICKSIZE。开 MT5 把这段贴进 EA,改 symbol 为 XAUUSD、volume 填 0.1,能立刻看到价格跳 1 美元时 profit 约等于 10 美元(按常规合约)。

MQL5 / C++
if(OrderSelect((class="type">int)request.order, SELECT_BY_TICKET))
{
   if(!OrderModify((class="type">int)request.order, request.price, request.sl, request.tp, request.expiration))
   {
      result.retcode = GetLastError();
   }
   else
   {
      result.retcode = TRADE_RETCODE_DONE;
      result.deal = OrderTicket();
      result.order = OrderTicket();
      result.request_id = OrderTicket();
      result.price = request.price;
      result.volume = OrderLots();
      result.comment = OrderComment();
   }
}
else
{
   result.retcode = TRADE_RETCODE_INVALID_ORDER;
}
}
   else if(request.action == TRADE_ACTION_REMOVE)
   {
      if(!OrderDelete((class="type">int)request.order))
      {
         result.retcode = GetLastError();
      }
      else
      {
         result.retcode = TRADE_RETCODE_DONE;
      }
   }
   else if(request.action == TRADE_ACTION_CLOSE_BY)
   {
      if(!OrderCloseBy((class="type">int)request.position, (class="type">int)request.position_by))
      {
         result.retcode = GetLastError();
      }
      else
      {
         result.retcode = TRADE_RETCODE_DONE;
      }
   }
   class="kw">return true;
}
class="type">int EnumOrderType2Code(class="type">int action)
{
   class=class="str">"cmt">// ORDER_TYPE_BUY/ORDER_TYPE_SELL 和衍生品
   class="kw">return (action % class="num">2 == class="num">0) ? OP_BUY : OP_SELL;
}

class="type">bool OrderCalcMargin(
   ENUM_ORDER_TYPE action,
   class="type">class="kw">string          symbol,
   class="type">class="kw">double          volume,
   class="type">class="kw">double          price,
   class="type">class="kw">double         &margin
   )
{
   class="type">int cmd = EnumOrderType2Code(action);
   class="type">class="kw">double m = AccountFreeMarginCheck(symbol, cmd, volume);
   if(m <= class="num">0 || GetLastError() == ERR_NOT_ENOUGH_MONEY)
   {
      class="kw">return false;
   }
   margin = AccountFreeMargin() - m;
   class="kw">return true;
}

class="type">bool OrderCalcProfit(
   ENUM_ORDER_TYPE action,
   class="type">class="kw">string          symbol,
   class="type">class="kw">double          volume,
   class="type">class="kw">double          price_open,
   class="type">class="kw">double          price_close,
   class="type">class="kw">double         &profit
   )
{
   class="type">int cmd = EnumOrderType2Code(action);
   if(cmd > -class="num">1)
   {
      class="type">int points = (class="type">int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT));
      if(cmd == OP_SELL) points = -points;
      profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT));
      class="kw">return true;
   }

◍ 手搓 MQL5 老式持仓校验与枚举

在 MQL5 里若不想依赖标准的 CTrade 封装,可以直接用 MQL4 风格函数拼一套轻量仓位检查逻辑。下面这段 OrderCheck 先卡手数边界:大于 MODE_MAXLOT 或小于 MODE_MINLOT,以及不满足 MODE_LOTSTEP 步进的整数倍,都会返回 TRADE_RETCODE_INVALID_VOLUME。 紧接着用 OrderCalcMargin 算所需保证金,失败则给 TRADE_RETCODE_NO_MONEY;若是市价执行模式(SYMBOL_TRADE_EXECUTION_MARKET)却带了 sl/tp,直接判 TRADE_RETCODE_INVALID_STOPS——这点在外盘贵金属上容易踩坑,因为部分平台市价单不允许挂止损。 持仓枚举靠 PositionsTotal、PositionGetSymbol、PositionGetTicket 三个函数完成,思路都是遍历 OrdersTotal,用 OrderType()<=OP_SELL 过滤出真实持仓(不含挂单),再按计数器取对应 symbol 或 ticket。 外汇与贵金属杠杆高,这类自写校验省了封装层开销,但漏掉冻结保证金或点差校验时,实盘可能触发意外 reject,建议开 MT5 用策略测试器跑一遍边界手数验证。

MQL5 / C++
class="type">bool OrderCheck(class="kw">const class="type">MqlTradeRequest &request, MqlTradeCheckResult &result)
{
  if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT)
  || request.volume < MarketInfo(request.symbol, MODE_MINLOT)
  || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP))
  {
     result.retcode = TRADE_RETCODE_INVALID_VOLUME;
     class="kw">return false;
  }
  class="type">class="kw">double margin;
  if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin))
  {
     result.retcode = TRADE_RETCODE_NO_MONEY;
     class="kw">return false;
  }
  if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)
  && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET
  && (request.sl != class="num">0 || request.tp != class="num">0))
  {
     result.retcode = TRADE_RETCODE_INVALID_STOPS;
     class="kw">return false;
  }
  result.balance = AccountBalance();
  result.equity = AccountEquity();
  result.profit = AccountEquity() - AccountBalance();
  result.margin = margin;
  result.margin_free = AccountFreeMargin();
  result.margin_level = class="num">0;
  result.comment = "";
  class="kw">return true;
}
class="type">int PositionsTotal()
{
  class="type">int count = class="num">0;
  for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
  {
     if(OrderSelect(i, SELECT_BY_POS))
     {
        if(OrderType() <= OP_SELL)
        {
           count++;
        }
     }
  }
  class="kw">return count;
}
class="type">class="kw">string PositionGetSymbol(class="type">int index)
{
  class="type">int count = class="num">0;
  for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
  {
     if(OrderSelect(i, SELECT_BY_POS))
     {
        if(OrderType() <= OP_SELL)
        {
           if(index == count)
           {
              class="kw">return OrderSymbol();
           }
           count++;
        }
     }
  }
  class="kw">return "";
}
class="type">class="kw">ulong PositionGetTicket(class="type">int index)
{
  class="type">int count = class="num">0;
  for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
  {
     if(OrderSelect(i, SELECT_BY_POS))
     {
        if(OrderType() <= OP_SELL)
        {
           if(index == count)
           {
              class="kw">return OrderTicket();
           }
           count++;
        }
     }
  }
  class="kw">return class="num">0;
}

「用订单函数伪造持仓接口」

在 MT4 老账户或挂接历史订单数据时,MT5 原生的 PositionSelect 系列往往拿不到东西。上面这组函数直接把老式 Order 体系包装成 Position 语义,让后续逻辑无需重写就能跑。 PositionSelect 遍历 OrdersTotal 返回的所有订单,用 OrderSelect 按位置抓,再比对 symbol 且类型不超过 OP_SELL(即只认市价单),命中就返回 true。PositionSelectByTicket 更轻量,凭 ticket 直接 SELECT_BY_TICKET 校验类型,省掉循环。 Order2Position 是个小桥接:OP_BUY 映射 POSITION_TYPE_BUY,其余一律归为 SELL,注释里也点明「Position = order, 仅有 OP_BUY 或 OP_SELL」。 PositionGetInteger 和 PositionGetDouble 用 switch 把订单字段透传出来。注意 POSITION_TIME_MSC 直接拿 OrderOpenTime() 乘 1000,精度只到秒级毫秒化,不是真实微秒戳;POSITION_PRICE_CURRENT 则按多空分别取 MODE_BID / MODE_ASK,外汇与贵金属跳空时可能偏离上次成交价。 把这段代码贴进 MT5 的 MQL5 脚本里,用 EURUSD 实盘账户跑一遍 PositionSelect("EURUSD"),若返回 false 说明当前无市价单,可手动开 0.01 手验证接口联通性。杠杆市场高风险,验证仓位别超过日常千分之一。

MQL5 / C++
class="type">bool PositionSelect(class="type">class="kw">string symbol)
{
  for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderSymbol() == symbol && (OrderType() <= OP_SELL))
      {
        class="kw">return true;
      }
    }
  }
  class="kw">return false;
}
class="type">bool PositionSelectByTicket(class="type">class="kw">ulong ticket)
{
  if(OrderSelect((class="type">int)ticket, SELECT_BY_TICKET))
  {
    if(OrderType() <= OP_SELL)
    {
      class="kw">return true;
    }
  }
  class="kw">return false;
}
class=class="str">"cmt">// Position = order, 仅有 OP_BUY 或 OP_SELL
class="type">ENUM_POSITION_TYPE Order2Position(class="type">int type)
{
  class="kw">return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
}
class="type">bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, class="type">long &long_var)
{
  class="kw">switch(property_id)
  {
    case POSITION_TICKET:
    case POSITION_IDENTIFIER:
      long_var = OrderTicket();
      class="kw">return true;
    case POSITION_TIME:
    case POSITION_TIME_UPDATE:
      long_var = OrderOpenTime();
      class="kw">return true;
    case POSITION_TIME_MSC:
    case POSITION_TIME_UPDATE_MSC:
      long_var = OrderOpenTime() * class="num">1000;
      class="kw">return true;
    case POSITION_TYPE:
      long_var = Order2Position(OrderType());
      class="kw">return true;
    case POSITION_MAGIC:
      long_var = OrderMagicNumber();
      class="kw">return true;
  }
  class="kw">return false;
}
class="type">bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, class="type">class="kw">double &double_var)
{
  class="kw">switch(property_id)
  {
    case POSITION_VOLUME:
      double_var = OrderLots();
      class="kw">return true;
    case POSITION_PRICE_OPEN:
      double_var = OrderOpenPrice();
      class="kw">return true;
    case POSITION_SL:
      double_var = OrderStopLoss();
      class="kw">return true;
    case POSITION_TP:
      double_var = OrderTakeProfit();
      class="kw">return true;
    case POSITION_PRICE_CURRENT:
      double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK);
      class="kw">return true;

旧订单接口到持仓属性的桥接写法

把老式 OrderXxx 系列函数包一层,是 MT4 策略向 MT5 迁移时最常见的妥协方案。下面这段用 switch 把 POSITION_COMMISSION、POSITION_SWAP、POSITION_PROFIT 等枚举映射到 OrderCommission、OrderSwap、OrderProfit,调用方拿到 bool 返回值就能知道属性是否成功取出。 字符串类属性同理,POSITION_SYMBOL 走 OrderSymbol,POSITION_COMMENT 走 OrderComment,未命中枚举就返回 false。这种写法在 MT5 回测环境里能跑,但注意它依赖当前被 OrderSelect 选中的旧订单上下文,并非直接读持仓池。 PendingOrdersTotal 与 OrderGetTicket 都通过 OrderType() > OP_SELL 来筛挂单:OP_BUY 与 OP_SELL 是 0 和 1,大于 1 的才是 LIMIT / STOP 类待成交单。循环里每命中一个挂单 count 加一,OrderGetTicket 用 index == count 做随机访问,越界返回 0。 HistorySelect 自己维护 historyDeals 和 historyOrders 两个全局数组,按 from_date / to_date 过滤 OrdersHistoryTotal 里的记录。OrderType() <= OP_SELL 的判为成交(deal),其余进 orders 数组;时间条件用了 OpenTime >= from 或 CloseTime <= to,实际可能把边界外的单也收进来,接数据时需自己再核一遍。外汇与贵金属杠杆高,这类自写历史筛选若边界写错,统计胜率会偏得离谱。

MQL5 / C++
case POSITION_COMMISSION:
      double_var = OrderCommission();
      class="kw">return true;
   case POSITION_SWAP:
      double_var = OrderSwap();
      class="kw">return true;
   case POSITION_PROFIT:
      double_var = OrderProfit();
      class="kw">return true;
   }
   class="kw">return false;
}
class="type">bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, class="type">class="kw">string &string_var)
{
   class="kw">switch(property_id)
   {
      case POSITION_SYMBOL:
         string_var = OrderSymbol();
         class="kw">return true;
      case POSITION_COMMENT:
         string_var = OrderComment();
         class="kw">return true;
   }
   class="kw">return false;
}
class="type">int PendingOrdersTotal()
{
   class="type">int count = class="num">0;
   for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
   {
      if(OrderSelect(i, SELECT_BY_POS))
      {
         if(OrderType() > OP_SELL)
         {
            count++;
         }
      }
   }
   class="kw">return count;
}
class="type">class="kw">ulong OrderGetTicket(class="type">int index)
{
   class="type">int count = class="num">0;
   for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++)
   {
      if(OrderSelect(i, SELECT_BY_POS))
      {
         if(OrderType() > OP_SELL)
         {
            if(index == count)
            {
               class="kw">return OrderTicket();
            }
            count++;
         }
      }
   }
   class="kw">return class="num">0;
}
class="type">int historyDeals[], historyOrders[];
class="type">bool HistorySelect(class="type">class="kw">datetime from_date, class="type">class="kw">datetime to_date)
{
   class="type">int deals = class="num">0, orders = class="num">0;
   ArrayResize(historyDeals, class="num">0);
   ArrayResize(historyOrders, class="num">0);
   for(class="type">int i = class="num">0; i < OrdersHistoryTotal(); i++)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
      {
         if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date)
         {
            if(OrderType() <= OP_SELL) class=class="str">"cmt">// deal
            {
               ArrayResize(historyDeals, deals + class="num">1);
               historyDeals[deals] = OrderTicket();
               deals++;
            }
            ArrayResize(historyOrders, orders + class="num">1);
            historyOrders[orders] = OrderTicket();
            orders++;
         }

◍ 历史订单与成交的索引映射技巧

在 MT5 的自定义回测环境里,历史成交和挂单往往共用同一套缓存数组,但对外暴露的 ticket 索引需要区分入场与离场。上面这组函数用 ArraySize(historyDeals)*2 把每笔订单拆成奇偶两个槽位:偶数槽对应原始 ticket,奇数槽则在高位打上 0x8000000000000000 标记,表示这是离场侧记录。 HistoryDealGetTicket 里 index%2==0 返回 OrderTicket(),否则返回带标记的值,这样调用层不用关心底层是开仓还是平仓。HistoryOrderGetTicket 对挂单只返一次(index%2==0),因为挂单没有对称的成交闭环,硬凑偶数槽会在日志里打印 '-> 0' 方便排查。 REVERSE(type) 宏用 (type+1)%2 做买卖反转,OrderType2DealType 把 MT4 的 OP_BUY/OP_SELL 映射到 DEAL_TYPE_BUY/SELL,中间留了 -1 占位给挂单类型。HistoryDealGetInteger 里用 exit 变量判断高位标记,决定取 OrderCloseTime 还是 OrderOpenTime——这套映射跑通后,你在 EA 里调 HistoryDealsTotal 会拿到双倍于真实成交笔数的索引量,属正常现象。 外汇与贵金属历史数据回测本身高风险,映射逻辑写错可能让策略误判平仓时间,建议直接把代码贴进 MT5 的 include 文件编译验证一次。

MQL5 / C++
  }
 }
 class="kw">return true;
}
class="type">int HistoryDealsTotal()
{
  class="kw">return ArraySize(historyDeals) * class="num">2;
}
class="type">int HistoryOrdersTotal()
{
  class="kw">return ArraySize(historyOrders) * class="num">2;
}
class="type">class="kw">ulong HistoryDealGetTicket(class="type">int index)
{
  if(OrderSelect(historyDeals[index / class="num">2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    class=class="str">"cmt">// 奇数 - 入场 - 正数, 偶数 - 离场 - 负数
    class="kw">return (index % class="num">2 == class="num">0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
  }
  class="kw">return class="num">0;
}
class="type">class="kw">ulong HistoryOrderGetTicket(class="type">int index)
{
  if(OrderSelect(historyOrders[index / class="num">2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    if(OrderType() <= OP_SELL)
    {
      class="kw">return (index % class="num">2 == class="num">0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
    }
    else if(index % class="num">2 == class="num">0) class=class="str">"cmt">// 挂单返回一次
    {
      class="kw">return OrderTicket();
    }
    else
    {
      Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> class="num">0");
    }
  }
  class="kw">return class="num">0;
}
class="type">bool HistoryDealSelect(class="type">class="kw">ulong ticket)
{
  ticket &= ~0x8000000000000000;
  class="kw">return OrderSelect((class="type">int)ticket, SELECT_BY_TICKET, MODE_HISTORY);
}
class="macro">#define HistoryOrderSelect HistoryDealSelect
class="macro">#define REVERSE(type) ((type + class="num">1) % class="num">2)
ENUM_DEAL_TYPE OrderType2DealType(class="kw">const class="type">int type)
{
  class="kw">static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -class="num">1, -class="num">1, -class="num">1, -class="num">1, DEAL_TYPE_BALANCE};
  class="kw">return types[type];
}
class="type">bool HistoryDealGetInteger(class="type">class="kw">ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, class="type">long &long_var)
{
  class="type">bool exit = ((ticket_number & 0x8000000000000000) != class="num">0);
  ticket_number &= ~0x8000000000000000;
  if(OrderSelect((class="type">int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY))
  {
    class="kw">switch(property_id)
    {
      case DEAL_TICKET:
      case DEAL_ORDER:
      case DEAL_POSITION_ID:
        long_var = OrderTicket();
        class="kw">return true;
      case DEAL_TIME:
        long_var = exit ? OrderCloseTime() : OrderOpenTime();
        class="kw">return true;
让小布替你比对两端 API 差异
这些 MT4/MT5 交易函数差异和小布盯盘的品种页诊断已内置,打开对应终端对照即可省去翻头文件的工夫。

常见问题

因为 MT4 标准库未包含 MQL5 的交易类,无法准备发送订单、计算信号及尾随资金管理,向导缺少生成基础。
需从 MT5 标准库复制 MT4 缺失的交易类,并基于 MT4 原始 MQL API 仿真 MT5 交易环境,处理订单成交仓位模型差异。
瀑布适合先规划后开发,极限编程主张少计划多编译改错,跨终端移植实操中后者往往更高效。
可以,小布盯盘内置了两端 API 与标准库差异的速查,省去手动比对头文件,你专注决策逻辑即可。
会影响,建议先读《MetaTrader 5 中的订单、仓位和成交》再来看移植,否则仿真层容易看不懂。