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

Stanislav Korotky | 22 五月, 2017


MetaTrader 4 和 MetaTrader 5 客户端允许用户使用内置 MQL 向导 来创建 MQL 程序原型。两个终端的向导版本类似, 但仍有一个重要的区别。MetaTrader 5 向导提供了一个预制专家交易系统生成的选项, 而在 MetaTrader 4 向导中则未提供。它与以下事实相关联, 即这些 EA 基于标准 MQL 函数库中的类, 意即终端中提供的一组头文件。MetaTrader 4 也包括这样的函数库, 但没有 MQL5 的交易类。特别是, 没有类负责准备和发送交易订单, 基于指标值或价格行为计算信号, 尾随, 资金管理, 而所有这些功能均是构建自动生成的专家交易系统的必要基础。

这种情况源自于 MQL5 的逐渐发展。新语言最初出现在 MetaTrader 5 中, 且标准函数库中的类是针对该终端开发的。只有后来的 MQL5 被集成到 MetaTrader 4 中。但两个终端的 API 中, 交易函数差异巨大, 因此标准库中仅有部分转移到 MT 4, 绕过了交易类。结果导致, MetaTrader 4 向导无法提供生成预制专家交易系统的可能性。

与此同时, MetaTrader 4 依旧很流行, 那么为其生成预制专家交易系统的能力将会非常实用。由于不会再为 MetaTrader 4 中添加新函数, 只在新版本中修正错误, 所以仅在其向导中有很少的机会完成进一步改善。不过, 我们可以自由使用 MetaTrader 5 向导, 然后将结果代码转移到 MetaTrader 4。为代码运做需要将来自标准库中的交易类集合改编为 MetaTrader 4 的原始 MQL API。换言之, 我们需要从 MetaTrader 5 标准库类中复制 MetaTrader 4 中未提供的部分, 并为它们实现 MT5 交易环境的仿真。

若要理解当前的原材料, 您需要了解 MetaTrader 5 中交易操作的原则, 订单、成交和仓位的含义。如果您还不熟悉此版本的终端, 强烈建议您阅读文章 "MetaTrader 5 中的订单、仓位、和成交"。

计划

任何工作的最佳完成均应坚持某种预定计划。在开发中运用的方法示例, 是所谓的 瀑布模型。这个模型十分适合我们的情况, 在开发之余, 它的论述已添加到本文中。然而, 在实践中, 使用一种诸如 极限编程 之类的灵活方法将 MQL5 代码移植到 MQL4 (或反之亦然) 更加有效。其座右铭是: 少计划, 多行动。从字面上来说, 这意味着您可以取得源代码, 尝试编译它, 然后纠正发生的错误。我在本文中提出的规划没有立即出现。它是基于编译器生成的 "提示" 渐次形成的。

当比较两个终端的函数库时, 我们可以看到 MT4 版本没有 Trade, ExpertModels 文件夹。因此, 主要任务是将所有类从这些文件夹中移植到 MT4 版本。除此之外, 我们显然还需要调整 Indicators 文件夹里的一些东西。它在 MT4 版本的函数库中存在, 但在这些终端中指标的操作原则是不同的。然而, 无论如何, 您应该遵守最少编辑原则, 避免对函数库文件进行修改, 因为它会不时更新, 在此情况下, 我们需要自行定制与官方版本相对应的版本。

所有复制的文件在某种程度上参考了 MT5 版本的交易 MQL API。因此, 我们需要开发一套或多或少完整定义的函数, 保留相同的编程接口, 并将所有调用转换为 MT4 版继承的 MQL API。我们来详细考虑在仿真交易环境中应该包括哪些内容。我们从类型开始, 因为它们是用于建造建筑物的砖, 即算法和程序本身。

最简单的类型是枚举。通过结构, 它们被直接或间接地在许多函数里使用。因此, 与之相适的顺序如下: 枚举, 结构, 常数, 函数。

枚举

一些必要的枚举已被移植到 MetaTrader 4 当中。例如, 订单属性: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE, ENUM_ORDER_PROPERTY_STRING。另一方面, 这似乎很方便, 但另一方面, 并不是所有这些枚举的定义均与 MetaTrader 5 中的定义完全一样, 这也会造成困惑。

例如, ENUM_ORDER_TYPE 在 MetaTrader 5 中比之 MetaTrader 4 包含了更多的订单类型。如果我们原样保留 ENUM_ORDER_TYPE, 我们将收到编译错误, 因为复制的代码将引用缺失的元素。枚举不能重新定义。因此, 最简单的解决方案利用预处理器的宏定义, 就像这样:

// ENUM_ORDER_TYPE 扩展
#define ORDER_TYPE_BUY_STOP_LIMIT ((ENUM_ORDER_TYPE)6)
#define ORDER_TYPE_SELL_STOP_LIMIT ((ENUM_ORDER_TYPE)7)
#define ORDER_TYPE_CLOSE_BY ((ENUM_ORDER_TYPE)8)


其它 MetaTrader 4 中未提供的枚举可以通过与 MT5 类推来定义, 例如:

enum ENUM_ORDER_TYPE_FILLING
{
  ORDER_FILLING_FOK,
  ORDER_FILLING_IOC,
  ORDER_FILLING_RETURN
};


因此, 我们应该定义 (或添加常量) 以下枚举。乍一看, 似乎有太多的枚举。但此过程很繁琐, 我们只需要从文档中复制它们 (对应部分的链接可在下面找到; 星号标记的枚举为已存在但需要进行一些调整)。

  • 订单
    • ENUM_ORDER_TYPE_TIME
    • ENUM_ORDER_STATE
    • ENUM_ORDER_TYPE_FILLING
    • ENUM_ORDER_TYPE (*)
    • ENUM_ORDER_PROPERTY_INTEGER (*)
    • ENUM_ORDER_PROPERTY_STRING (*)
  • 仓位
    • ENUM_POSITION_TYPE
    • ENUM_POSITION_PROPERTY_INTEGER
    • ENUM_POSITION_PROPERTY_DOUBLE
    • ENUM_POSITION_PROPERTY_STRING
  • 成交
    • ENUM_DEAL_ENTRY
    • ENUM_DEAL_TYPE
    • ENUM_DEAL_PROPERTY_INTEGER
    • ENUM_DEAL_PROPERTY_DOUBLE
    • ENUM_DEAL_PROPERTY_STRING
  • 交易操作类型
    • ENUM_TRADE_REQUEST_ACTIONS

MetaTrader 4 已经包含描述品种的枚举定义, 例如 ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING。它们中的某些元素予以保留, 但不能操作 (如 文档 中所写)。与 MetaTrader 5 相比, 这些是 MetaTrader 4 平台的局限性, 我们必须接受这一点。对于我们来说, 重要的是不需要在项目中定义这些枚举。

结构

除了枚举之外, MetaTrader 5 函数中还使用了结构。它们的定义也可以从文档中获取 (对应部分的链接给出如下)。

宏定义

除了上述类型, MT5 的源代码使用了很多常数。在项目中定义这些常量的最简单方式是使用预处理器的 #define 指令。

交易函数

我们计划中的最后的要点是包括交易函数。我们只有在定义所有上述类型和常量之后才能开始实现它们。

交易函数列表 令人印象深刻。它们可以分为 4 组:

  • 订单
  • 仓位
  • 订单历史
  • 成交历史

最后, 我们将使用以下简单的替换:

#define MQL5InfoInteger MQLInfoInteger
#define MQL5InfoString  MQLInfoString


事实上, 以上是终端内核的相同函数, 但它们的名称在 MQL5 和 MQL4 中略有不同。

在直接实现之前, 我们需要定义如何通过 MetaTrader 4 的交易模型来反射 MetaTrader 5 交易模型。

反射

我们尝试并行绘制 MetaTrader 5 和 MetaTrader 4 之间的实体。从 MT4 开始更容易, 因其使用了 "订单" 的通用概念。这个概念实际上是指所有一切, 包括市场订单, 挂单和交易历史。在所有这些情况下, 订单可有不同的状态。在 MT5 中市场订单是仓位, 挂单是订单, 历史记录为成交。

在最简单的情况下, MetaTrader 5 操作如下。为了生成一笔仓位, 入场订单被发送到交易服务器。若要平仓, 发送另一笔订单, 这是一笔离场的订单。每笔订单都是在相应成交的框架内执行的, 这笔成交被添加到交易历史中。因此, 一笔 MT4 市场订单在模拟的 MT5 交易环境中应显示如下:

  • 入场订单
  • 入场成交
  • 仓位
  • 离场订单
  • 离场成交

MetaTrader 5 最初是一个纯粹的净额平台, 也就是说, 同一时间一个品种只能存在一笔仓位。相同品种的所有订单将会在该品种的总交易量上增加、减少或完全抵消, 且修改止损、止盈是针对该品种整体。MetaTrader 4 中不提供此模式, 如果 MetaTrader 5 尚未添加对冲支持, 则实现该项目将非常困难。MetaTrader 4 中采用的是这种模式: 每笔订单的执行会形成一笔单独的 "仓位" (以 MetaTrader 5 的术语), 以至于在相同的品种上存在若干笔未平仓订单, 包括反向订单。

注意!如果您想比较 MetaTrader 5 和 MetaTrader 4 所生成的专家交易系统的操作, 请记住, 您的 MetaTrader 5 终端必须激活对冲模式。为了比较, 最好使用同一经纪商的服务器。

实现

MetaTrader 5 环境的模拟

为了简单起见, 我们将把整个模拟环境, 包括类型、常量和函数置于单个头文件 MT5Bridge.mqh 中。良好的编程风格可能需要将它们安配为分开的单独文件中。这种结构对于大型项目以及团队项目尤其重要。不过, 单一文件在分发和安装方面更为方便。

所以, 根据我们的计划, 我们需要定义所有的枚举, 常量和结构。这是一个常规的复印作业, 并不任何复杂之处。没有必要更详细地解释, 在规划阶段提供的注解就足够了。我们再次检查文档中关于 交易函数 的信息, 并进一步了解更多的知性部分, 其中包括编写所有这些函数的代码。

我们从当前的操作开始, 包括市价单和挂单的处理, 以及仓位。

为此目的, 我们需要超级通用 MT5 函数 OrderSend

bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
{


根据请求类型, 我们需要在此函数中使用 MT4 类型之一。

  int cmd;   
  result.retcode = 0;
  switch(request.type)
  {
    case ORDER_TYPE_BUY:
      cmd = OP_BUY;
      break;
    case ORDER_TYPE_SELL:
      cmd = OP_SELL;
      break;
    case ORDER_TYPE_BUY_LIMIT:
      cmd = OP_BUYLIMIT;
      break;
    case ORDER_TYPE_SELL_LIMIT:
      cmd = OP_SELLLIMIT;
      break;
    case ORDER_TYPE_BUY_STOP:
      cmd = OP_BUYSTOP;
      break;
    case ORDER_TYPE_SELL_STOP:
      cmd = OP_SELLSTOP;
      break;
    default:
      Print("Unsupported request type:", request.type);
      return false;
  }


传递的操作代码来自 action 字段, 允许以不同的方式处理订单的放置, 删除和修改。例如, 可以如下实现打开市价单或创建挂单。

  ResetLastError();
  if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)
  {
    if(request.price == 0)
    {
      if(cmd == OP_BUY)
      {
        request.price = MarketInfo(request.symbol, MODE_ASK);
      }
      else
      if(cmd == OP_SELL)
      {
        request.price = MarketInfo(request.symbol, MODE_BID);
      }
    }
    if(request.position > 0)
    {
      if(!OrderClose((int)request.position, request.volume, request.price, (int)request.deviation))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = request.position | 0x8000000000000000;
        result.order = request.position | 0x8000000000000000;
        result.volume = request.volume;
        result.price = request.price;
      }
    }
    else
    {
      int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (int)request.deviation, request.sl, request.tp, request.comment, (int)request.magic, request.expiration);
      if(ticket == -1)
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = ticket;
        result.order = ticket;
        result.request_id = ticket;
        if(OrderSelect(ticket, SELECT_BY_TICKET))
        {
          result.volume = OrderLots();
          result.price = OrderOpenPrice() > 0 ? OrderOpenPrice() : request.price;
          result.comment = OrderComment();
          result.ask = MarketInfo(OrderSymbol(), MODE_ASK);
          result.bid = MarketInfo(OrderSymbol(), MODE_BID);
        }
        else
        {
          result.volume = request.volume;
          result.price = request.price;
          result.comment = "";
        }
      }
    }
  }


基本操作是通过众多参数组合的常用 MT4 函数 OrderSend 来执行。调用之后, 将操作结果相应地写入输出结构。

请注意, 在 MetaTrader 5 中, 已存在市价单是通过打开相反方向的另一笔订单来平仓, 并在 position 字段中传递已平仓的标识符。在此情况下, 即当 position 字段不为空时, 以上代码尝试使用 OrderClose 函数平仓。在此, 订单的票号被用作仓位标识符。这是合乎逻辑的, 因为在 MT4 中, 每笔订单都创建自己的仓位。成交得到相同的单号。

对于虚拟订单 (实际上不存在于此) 的平仓, 人为地将其原始编号的高数位 (high-order bit) 置 1 作为单号。以后在列举订单和成交时将会用到它。

现在, 我们来看看如何才能实现对已有持仓的修改。

  else if(request.action == TRADE_ACTION_SLTP) // 修改已开持仓
  {
    if(OrderSelect((int)request.position, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.position, OrderOpenPrice(), request.sl, request.tp, 0))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_POSITION_CLOSED;
    }
  }


很明显, OrderModify 即用于此目的。

同样的函数也用于修改挂单。

  else if(request.action == TRADE_ACTION_MODIFY) // 修改挂单
  {
    if(OrderSelect((int)request.order, SELECT_BY_TICKET))
    {
      if(!OrderModify((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;
    }
  }


通过标准 OrderDelete 函数执行挂单的删除。

  else if(request.action == TRADE_ACTION_REMOVE)
  {
    if(!OrderDelete((int)request.order))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }


最后, 平仓操作 (使用反向单平仓) 等同于 MetaTrader 4 背景下的反向订单平仓。

  else if(request.action == TRADE_ACTION_CLOSE_BY)
  {
    if(!OrderCloseBy((int)request.position, (int)request.position_by))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }
  return true;
}


除了 OrderSend 之外, MetaTrader 5 提供了一个异步函数 OrderSendAsync。我们不会实现它, 并将在函数库中禁用异步模式的所有情况, 即我们实际上将其替换为同步版本。

操作订单的放置通常伴随着另外三个函数的调用: OrderCalcMargin, OrderCalcProfit, OrderCheck

以下是使用 MetaTrader 4 中提供的工具实现的版本之一。

int EnumOrderType2Code(int action)

{   // ORDER_TYPE_BUY/ORDER_TYPE_SELL 和衍生品   return (action % 2 == 0) ? OP_BUY : OP_SELL; }

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

bool OrderCalcProfit(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price_open,   double          price_close,   double         &profit   ) {   int cmd = EnumOrderType2Code(action);   if(cmd > -1)   {     int points = (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));     return true;   }   return false; } bool OrderCheck(const 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;     return false;   }   double margin;   if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin))   {     result.retcode = TRADE_RETCODE_NO_MONEY;     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 != 0 || request.tp != 0))   {     result.retcode = TRADE_RETCODE_INVALID_STOPS;     return false;   }   result.balance = AccountBalance();   result.equity = AccountEquity();   result.profit = AccountEquity() - AccountBalance();   result.margin = margin;   result.margin_free = AccountFreeMargin();   result.margin_level = 0;   result.comment = "";   return true; }

此处主动使用以下内置函数: AccountEquity, AccountFreeMargin, AccountFreeMarginCheck, 以及通过调用 MarketInfo 获得的品种点值和其它设置。

若要获得仓位总数, 返回已开市价单的数量即可。

int PositionsTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}


若要通过其号码获得仓位的品种, 必须循环遍历所有订单, 且仅计算市价单。

string PositionGetSymbol(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderSymbol();
        }
        count++;
      }
    }
  }
  return "";
}


通过其号码获取仓位单号的函数以相同的方式构造。

ulong PositionGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}


通过品名选择一笔仓位, 我们也需循环遍历市价单, 并在第一次品名匹配的情况下停止。

bool PositionSelect(string symbol)
{
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderSymbol() == symbol && (OrderType() <= OP_SELL))
      {
        return true;
      }
    }
  }
  return false;
}


通过单号选择仓位可以无需循环实现。

bool PositionSelectByTicket(ulong ticket)
{
  if(OrderSelect((int)ticket, SELECT_BY_TICKET))
  {
    if(OrderType() <= OP_SELL)
    {
      return true;
    }
  }
  return false;
}


所选仓位的属性应通过 MetaTrader 5 中使用的三个函数返回: _GetDouble, _GetInteger, _GetString。在此我们介绍它们的实现。它们看起来非常相似于订单和成交, 所以我们不会在这里分析它们。不过, 在附件文件中提供了这些函数的代码。

// Position = order, 仅有 OP_BUY 或 OP_SELL
ENUM_POSITION_TYPE Order2Position(int type)
{
  return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
}

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, long &long_var)
{
  switch(property_id)
  {
    case POSITION_TICKET:
    case POSITION_IDENTIFIER:
      long_var = OrderTicket();
      return true;
    case POSITION_TIME:
    case POSITION_TIME_UPDATE:
      long_var = OrderOpenTime();
      return true;
    case POSITION_TIME_MSC:
    case POSITION_TIME_UPDATE_MSC:
      long_var = OrderOpenTime() * 1000;
      return true;
    case POSITION_TYPE:
      long_var = Order2Position(OrderType());
      return true;
    case POSITION_MAGIC:
      long_var = OrderMagicNumber();
      return true;
  }
  return false;
}

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, double &double_var)
{
  switch(property_id)
  {
    case POSITION_VOLUME:
      double_var = OrderLots();
      return true;
    case POSITION_PRICE_OPEN:
      double_var = OrderOpenPrice();
      return true;
    case POSITION_SL:
      double_var = OrderStopLoss();
      return true;
    case POSITION_TP:
      double_var = OrderTakeProfit();
      return true;
    case POSITION_PRICE_CURRENT:
      double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK);
      return true;
    case POSITION_COMMISSION:
      double_var = OrderCommission();
      return true;
    case POSITION_SWAP:
      double_var = OrderSwap();
      return true;
    case POSITION_PROFIT:
      double_var = OrderProfit();
      return true;
  }
  return false;
}

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case POSITION_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case POSITION_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}


仓位类似于实际的市价单, 我们需要实现一组处理挂单的函数。不过, 这里有一个难点。我们无法实现 OrdersTotal 函数和其它的 OrderGet_ 函数, 因为它们已经在内核中定义, 并且内置函数不能被覆盖。编译器将返回以下错误:

'OrderGetString' - 覆盖系统函数 MT5Bridge.mqh

因此, 我们必须为所有函数设置其它名称, 这些名称将以 Order_ 开头。由于它们只处理挂单, 所以它们的名字使用 PendingOrder_ 开头是合乎逻辑的。例如:

int PendingOrdersTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}


之后, 在标准函数库代码中, 我们需要从 MT5Bridge.mqh 中替换所有调用到这些新函数。

通过号码返回单号的 OrderGetTicket 函数在 MetaTrader 4 里不存在, 所以我们无需改变其名称, 并根据 MetaTrader 5 API 使用它。

ulong OrderGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}


MetaTrader 4 里存在的 OrderSelect 函数比之 MetaTrader 5 的版本拥有许多扩展参数, 所以我们保留其调用并加入所需的 SELECT_BY_TICKET 参数。

在附带的头文件中提供了读取挂单属性函数的完整实现。

现在让我们来看看操纵订单和成交历史的函数。它们的实现需要一些别出心裁。选择以下变体 (其是许多可能的变体之一) 是因为它的简单性。

在 MetaTrader 4 里的每笔市价单在历史中显示为两个 MT5 风格的订单: 入场和离场。此外, 历史必须包含一对相应的成交。挂单按原样显示。历史记录将存储在两个带有单号的数组中。

int historyDeals[], historyOrders[];

它们将由来自 MQL5 API 中的 HistorySelect 函数填充。

bool HistorySelect(datetime from_date, datetime to_date)
{
  int deals = 0, orders = 0;
  ArrayResize(historyDeals, 0);
  ArrayResize(historyOrders, 0);
  for(int i = 0; i < OrdersHistoryTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
    {
      if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date)
      {
        if(OrderType() <= OP_SELL) // deal
        {
          ArrayResize(historyDeals, deals + 1);
          historyDeals[deals] = OrderTicket();
          deals++;
        }
        ArrayResize(historyOrders, orders + 1);
        historyOrders[orders] = OrderTicket();
        orders++;
      }
    }
  }
  return true;
}


一旦数组被填充, 我们可以获得历史的大小。

int HistoryDealsTotal()
{
  return ArraySize(historyDeals) * 2;
}

int HistoryOrdersTotal()
{
  return ArraySize(historyOrders) * 2;
}


数组的大小要乘以 2, 因为 MetaTrader 4 的每笔订单在 MetaTrader 5 中均被表示为两笔订单或两笔成交。挂单无此必要, 但为了保持这种方式的通用性, 我们仍然保留 2 个单号, 其中一个不会使用 (参阅以下的 HistoryOrderGetTicket 函数)。市价入场成交将具有 MetaTrader 4 里生成此笔成交的订单的相同单号。对于离场成交, 这个单号将补充一个高数位 (high-order bit)。

ulong HistoryDealGetTicket(int index)
{
  if(OrderSelect(historyDeals[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    // 奇数 - 入场 - 正数, 偶数 - 离场 - 负数
    return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
  }
  return 0;
}


即使历史编号总是包含入场单号 (实际), 奇数是离场单号 (虚拟)。

这种情况对于订单来说有点复杂, 因为它们之间可能有挂单的情况, 它们是按原样显示的。在此情况下, 偶数将返回挂单的正确单号, 而下一个奇数将返回 0。

ulong HistoryOrderGetTicket(int index)
{
  if(OrderSelect(historyOrders[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    if(OrderType() <= OP_SELL)
    {
      return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
    }
    else if(index % 2 == 0) // 挂单返回一次
    {
      return OrderTicket();
    }
    else
    {
      Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> 0");
    }
  }
  return 0;
}


通过单号选择成交的实现已考虑到此功能所需的附加高数位 (此处需要跳过)。

bool HistoryDealSelect(ulong ticket)
{
  ticket &= ~0x8000000000000000;
  return OrderSelect((int)ticket, SELECT_BY_TICKET, MODE_HISTORY);
}


对于订单, 一切都是完全相似的。

#define HistoryOrderSelect HistoryDealSelect

使用 HistoryDealSelect 或者 HistoryDealGetTicket 选择成交, 我们可以编写访问成交属性的函数实现。

#define REVERSE(type) ((type + 1) % 2)

ENUM_DEAL_TYPE OrderType2DealType(const int type)
{
  static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -1, -1, -1, -1, DEAL_TYPE_BALANCE};
  return types[type];
}

bool HistoryDealGetInteger(ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, long &long_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  if(OrderSelect((int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY))
  {
    switch(property_id)
    {
      case DEAL_TICKET:
      case DEAL_ORDER:
      case DEAL_POSITION_ID:
        long_var = OrderTicket();
        return true;
      case DEAL_TIME:
        long_var = exit ? OrderCloseTime() : OrderOpenTime();
        return true;
      case DEAL_TIME_MSC:
        long_var = (exit ? OrderCloseTime() : OrderOpenTime()) * 1000;
        return true;
      case DEAL_TYPE:
        long_var = OrderType2DealType(exit ? REVERSE(OrderType()) : OrderType());
        return true;
      case DEAL_ENTRY:
        long_var = exit ? DEAL_ENTRY_OUT : DEAL_ENTRY_IN;
        return true;
      case DEAL_MAGIC:
        long_var = OrderMagicNumber();
        return true;
    }
  }
  return false;
}
  
bool HistoryDealGetDouble(ulong ticket_number, ENUM_DEAL_PROPERTY_DOUBLE property_id, double &double_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  switch(property_id)
  {
    case DEAL_VOLUME:
      double_var = OrderLots();
      return true;
    case DEAL_PRICE:
      double_var = exit ? OrderClosePrice() : OrderOpenPrice();
      return true;
    case DEAL_COMMISSION:
      double_var = exit? 0 : OrderCommission();
      return true;
    case DEAL_SWAP:
      double_var = exit ? OrderSwap() : 0;
      return true;
    case DEAL_PROFIT:
      double_var = exit ? OrderProfit() : 0;
      return true;
  }
  return false;
}

bool HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case DEAL_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case DEAL_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}


我希望这个思路很清楚。在历史中操作订单的函数组。

标准函数库的修改

在函数实现期间, 函数库的一些必要编辑已进行了讨论。您可以比较 MetaTrader 5 交付文件和本项目中创建的文件, 以便获取完整的更改列表。更进一步, 只考虑最重要的关键点, 而忽略次要修正的注释。许多文件加入了连接到 MT5Bridge.mqh 的新 #include 语句。

标准库文件的主要更改列表


文件/方法 更改
Trade.mqh SetAsyncMode 异步模式的一行已被删除, 因为不支持此模式
SetMarginMode 模式 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 被明确指定
OrderOpen 设置到期模式的组合标志被明确指定为 SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED
OrderTypeCheck 处理不存在类型的情况 ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT 被排除
OrderSend 调用不存在的 OrderSendAsync 函数被删除
 
OrderInfo.mqh 所有调用 OrderGetInteger, OrderGetDouble, OrderGetString 语句替换为 PendingOrder 前缀加原函数名。
所有调用 OrderSelect(m_ticket) 替换为 OrderSelect((int)m_ticket, SELECT_BY_TICKET)
 
PositionInfo.mqh FormatPosition
SelectByIndex
设置了保证金模式 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
 
SymbolInfo.mqh Refresh 许多 MetaTrader 4 中不支持的检查被删除
 
AccountInfo.mqh MarginMode 返回 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 常量
 
Expert.mqh TimeframeAdd
TimeframesFlags
移除不支持时间帧
 
ExpertBase.mqh 添加 #include <Indicators\IndicatorsExt.mqh>
SetMarginMode 无条件设置为 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING


需要 IndicatorsExt.mqh 文件来纠正标准 Indicators.mqh 文件里的小错误。此外, 它包含另一个指标必须的文件 TimeSeriesExt.mqh

TimeSeriesExt.mqh 文件包含 MetaTrader 4 标准 TimeSeries.mqh 文件中未提供的 MetaTrader 5 交易风格所需的类描述。

特别地, 这些是以下类: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume。其中许多只作为存根, 而毫无作为 (且不能做任何事情, 因为相应的函数在 MetaTrader 4 中未提供)。

测试

将与标准库适配的交易类保存到 MetaTrader 4 中的 Include 目录 (保留子目录的层次结构), 并将 MT5Bridge.mqh 复制到Include/Trade 文件夹, 我们即能直接在 MetaTrader 4 中编译并运行 MetaTrader 5 向导生成的专家交易系统。

MetaTrader 5 软件包里包含生成的专家交易系统的示例 (在 Experts/Advisors 文件夹中)。我们将选用这些 EA 当中的一个, ExpertMACD.mq5。我们将其复制到 MQL4/Experts 文件夹, 并重命名为 ExpertMACD.mq4。在编辑器中编译应得到以下结果:

在 MetaTrader 4 中编译来自 MetaTrader 5 的专家交易系统

在 MetaTrader 4 中编译来自 MetaTrader 5 的专家交易系统

如您所见, 函数库文件已连接并被处理, 没有错误和警告。当然, 缺少编译错误并不能保证程序逻辑中没有问题, 但这是进一步实际测试的任务。

让我们在 MetaTrader 4 测试器中使用省缺设置运行已编译的专家交易系统。

自 MetaTrader 5 中生成的专家交易系统在 MetaTrader 4 上的测试报告

自 MetaTrader 5 中生成的专家交易系统在 MetaTrader 4 上的测试报告

您可以检查日志, 并确保它不包含明显的订单处理错误。

在 EURUSD M15 图表上, 专家交易系统看来交易良好, 包括设定止损位和止盈位的方式。

图表窗口, 展示来自 MetaTrader 5 向导的专家交易系统在 MetaTrader 4 里的操作

图表窗口, 展示来自 MetaTrader 5 向导的专家交易系统在 MetaTrader 4 里的操作

我们来比较 MetaTrader 5 策略测试器的结果。

MetaTrader 5 生成的专家交易系统的测试报告

MetaTrader 5 生成的专家交易系统的测试报告

显然这里有些差异。它们可以用报价的差异来解释 (例如, MetaTrader 5 使用浮动点数) 以及测试器使用不同算法。一般来说, 测试是类似的: 交易数量和余额曲线的整体性质几乎相同。

当然, 用户可以在 MetaTrader 5 向导中使用完全任意的模块集合来生成自己的专家交易系统, 然后可以轻松地将其转移到 MetaTrader 4。特别地, 在项目测试期间, 我们测试了尾随停止功能和可变手数大小的专家交易系统。

结论

我们已经研究了将 MetaTrader 5 向导生成的专家交易系统迁移到 MetaTrader 4 平台的一种可能方式。其主要优点是实现相对容易, 这是基于尽可能多地使用 MetaTrader 5 标准库的现有交易类代码。主要缺点是需要在计算机上有两个终端: 其中一个用于生成 EA, 而在第二个终端中, 执行这些 EA。

以下是 2 个文件:

  • 一个文档是标准库的修改文件, 在 MetaTrader 4 的 Include 目录中解压缩并保留子目录的层次结构。存档仅包含终端软件包中未提供的文件, 因此不可能覆盖现有文件;
  • MT5Bridge.mqh 文件应复制到 Include/Trade

该版本的函数库是从 MetaTrader 5 build 1545 中获取的。未来的版本可能包含标准库的更改, 这可能很有用 (您也许需要利用仿真器编辑重新合并文件)。一个完美的解决方案是从 MetaQuotes 获得标准库版本, 其中 MetaTrader 5 和 MetaTrader 4 两个版本实现的交易类应使用条件编译指令进行最初的合并。

应当注意的是, 不可能在 MetaTrader 4 中实现 MetaTrader 5 交易环境的完全仿真。新终端在内核级别提供了旧终端中未提供的新功能。因此, 有可能出现生成的 EA 中所用的某些模块无法正常工作的情况。

而且, 不要忘记, 提供的仿真器实现是以 beta 状态分发的, 并可能包含隐藏的错误。只有经过长期和多方面的测试才能有助于我们得到适合实际交易的最终软件产品。所提供的源代码可令我们为此合作。