跨平台专家交易系统: 订单管理器

Enrico Lambino | 5 六月, 2017



内容目录

概述

正如本系列之前的文章中所论述的那样, MetaTrader 4 和 MetaTrader 5 有某些差别, 导致很难简单地复制 MQL4 源文件并使用 MQL5 编译器进行编译。两者之间最明显的区别之一就是两个平台在执行交易操作方面的差别。本文将处理 COrderManager 类的创建。上述 COrderManager 类, 及其它辅助类将主要负责在专家交易系统中执行交易, 以及维护系统建立的交易。


目标

本文中的订单管理器将会执行以下操作:

  1. 手数计算
  2. 止损和止盈
  3. 入场所需的各种参数 (过期, 注释, 魔幻数字)
  4. 发送订单前的一些预设条件
  5. 订单和历史订单的管理

涉及手数计算的部分通常最好委托给对象成员, 因为有很多方法可以计算下一笔交易的最佳手数 (取决于涉及的交易策略)。止损和止盈价位的计算同样如此。

订单入场所需的其它参数, 如过期, 订单注释和魔幻数字, 所需的复杂性较低, 因此最好由订单管理器对象本身处理。

专家交易系统入场交易之前, 需要某些预设条件。这些包括基于市场状况的交易信号, 时间限制, 以及任意给定时间的最大活跃交易数量, 和整个生存周期内 (OnInit 和 OnDeinit 之间) 的最大交易数量。对于订单管理器, 在订单最终发送到市场之前, 将包含最后两个预设条件将作为条件。这样的实现, 可令专家交易系统在操作期间不会发生重复交易。另一方面, 其它预设条件可以委托给订单管理器外部的某些组件。


基类实现

类似于本系列文章中讨论的其它类对象, 必须寻求 MQL4 和 MQL5 之间的共同点, 并在基类中实现, 而有分歧的部分在特定语言的基类继承类中实现。在开发基类时, 我们必须了解以下有关交易请求处理流程的领域:

  1. 交易请求如何发送是不同的
  2. 交易行为如何备录是不同的
  3. 在 MQL5 中有一些特性在 MQL4 中没有等效功能

在此领域有一些组件在 MQL4 和 MQL5 之间是不同的。参考 OrderSend 函数 (mql4, mql5), 如在两个平台的文档中所示:

(MQL4)

int  OrderSend(
   string   symbol,              // 品种
   int      cmd,                 // 操作
   double   volume,              // 交易量
   double   price,               // 价位
   int      slippage,            // 滑点
   double   stoploss,            // 止损
   double   takeprofit,          // 止盈
   string   comment=NULL,        // 注释
   int      magic=0,             // 魔幻数字
   datetime expiration=0,        // 挂单过期
   color    arrow_color=clrNONE  // 颜色
       );

(MQL5)

bool  OrderSend(
   MqlTradeRequest&  request,      // 查询结构
   MqlTradeResult&   result        // 应答结构
       );

MQL4 函数有一个更直接的方式。而另一方面, MQL5 函数更复杂一些, 但将参数的数量减少到两个, 包含请求和结果的数据 (struct)。有关将 MQL5 标准库的某些组件导入 MQL4, 特别是 CExpertTrade 和 CExpertTradeX 类这个障碍, 已经在前一篇文章中很大程度上得以解决。因此, 订单管理器将会简单地利用这些类来确保在发出交易请求时两种语言之间的兼容性。

另一方面是 MetaTrader 4 和 MetaTrader 5 中交易离场或订单删除的处理方式。尽管删除订单的方式没有太大差别, 但市价单 (MQL4) 或持仓 (MQL5) 的离场方式存在巨大差异。在 MQL4 中, 一笔市价单的离场通过调用 OrderClose 函数来实现。在 MetaTrader 5 中, 实现相同的效果则通过调用 PositionClose 函数, 或是发出与当前持仓交易量相同但方向相反的交易请求。

在 MetaTrader 5 中, 每一笔交易动作均备录在案。一个动作是否是交易入场, 修改, 或离场, 此动作将留下足迹, 有关这些动作的数据均可由专家交易系统访问。而在 MetaTrader 4 中并非如此。例如, 当为一笔挂单分配一个单号 ID 时, 在该订单的整个生命周期中通常使用相同的 ID, 即使它已经达到其触发价格, 并在它已退场成为市价单之前。为了看到某一笔交易的完整进程, 必须查看专家和流水账日志文件, 这可能是一个耗时的任务。再者, 日志文件本意是由人类阅读, 且没有内置的 MQL4 函数能令专家交易系统轻松地访问这些信息。

MQL5 中的某些功能在 MQL4 中根本不存在。一个例子是订单的 填充类型。MQL5 的一笔订单拥有以下交易量填充选项:

  • ORDER_FILLING_FOK – 当请求的交易量未能填充时, 取消此订单
  • ORDER_FILLING_IOC – 当请求的交易量未能填充时, 使用最大可用交易量, 并取消剩余交易量。
  • ORDER_FILLING_RETURN – 当请求的交易量未能填充时, 使用最大可用交易量。剩余交易量的订单保留在市场上。

在 MetaTrader 4 中, 一笔交易请求简单地被填充或取消, 实质上这等同于 ORDER_FILLING_FOK, 而其它两个选项则不可用。不过, 这些填充政策仅当请求的交易量超过市场上的可用交易量时才会实施, 这对于设置为低风险 和/或 低余额的帐户来说并不常见。ORDER_FILLING_IOC 和 ORDER_FILLING_RETURN 可能很困难, 但如果在 MQL4 中实现并非不可能或不切实际, 主要是因为专家交易系统没有任何方法来确定某笔交易请求的交易量能够满足 (即便如此, 这些信息也可能是巨幅波动地, 并且会经常受到改变)。因此, 为了确保 MQL4 和 MQL5 之间的兼容性, ORDER_FILLING_FOK 将是唯一使用的填充选项 (MetaTrader 5 中也是省缺填充选项)。同时, 当专家系统计算出交易请求的手数大小超过 SYMBOL_VOLUME_MAX (是由经纪商设定的任何交易/成交的最大允许成交量) 时会产生事件。MetaTrader 5 通过自动将交易切分成多笔成交来解决此问题, 但 MetaTrader 4 中不提供此功能 (导致交易请求取消)。因此, 对于跨平台专家交易系统来说, 在使用订单管理器发送入场交易请求之前, 最好提前检查 (首选是在获取或计算出交易的交易量之后)。

以下图例大体描述了订单管理器是如何实施交易入场的:

入场流程图

如图例中所示, 该方法从准备操作所需的数据开始。如果该位置允许入场并满足预设条件, 则该方法将处理订单入场。否则, 处理结束。在发送交易请求之前, 必要的数值需要先行计算。如果请求成功, 则检查结果, 并创建一个新的 COrder 实例, 然后将其添加到当前订单/仓位列表 (m_orders) 中。

仅执行纯计算的方法可以在基类中找到。由于两种语言调用函数的方法不同, 所以将基类方法扩展到各自特定语言的类中。然而, 在这种方法上, 操作如何执行差别甚微。因此, 基类方法是纯虚拟的, 并且可以在两个版本之间分别找到相应实现。我们将在下面找到基类方法的实现:

COrder* COrderManagerBase::TradeOpen(const string,ENUM_ORDER_TYPE)
  {
   return NULL;
      }

交易量的计算

如前所述, 下一笔交易的交易量计算最好委托给另一个类对象, 这将是订单管理器的类成员。这种方法也在 MQL5 标准库的专家系统库里运用。下面显示了 LotSizeCalculate 方法的代码, 此方法负责计算下一笔交易的交易量:

double COrderManagerBase::LotSizeCalculate(const double price,const ENUM_ORDER_TYPE type,const double stoploss)
  {
   if(CheckPointer(m_moneys))
      return m_moneys.Volume(m_symbol.Name(),0,type,stoploss);
   return m_lotsize;
      }

方法检查指向 CMoneys 实例的指针, 它只是订单管理器所用的货币管理对象的容器 (与 COrders 是 COrder 实例的容器相同)。这个资金管理对象将在另一篇文章中讨论。至少在这一点上, 知道有一个单独组件处理手数计算就足以了, 并且计算的手数将是验证过的。如果没有向订单管理器提供货币管理实例, 则订单管理器将简单地通过其类成员 m_lotsize 使用省缺手数。

计算止损和止盈

止损和止盈的计算分别通过 StopLossCalculate 和 TakeProfitCalculate 方法来实现。以下代码片段显示了在订单管理器中如何实现这些方法:

double COrderManagerBase::StopLossCalculate(const ENUM_ORDER_TYPE type,const double price)
  {
   if(CheckPointer(m_main_stop))
      return m_main_stop.StopLossTicks(type,price);
   return 0;
      }
double COrderManagerBase::TakeProfitCalculate(const ENUM_ORDER_TYPE type,const double price)
  {
   if(CheckPointer(m_main_stop))
      return m_main_stop.TakeProfitTicks(type,price);
   return 0;
      }

停止级别的计算被委派给一个单独的类对象, 它将是订单管理器的成员 (将在另一篇文章中讨论)。停止对象还会有其单独的 MetaTrader 4 和 MetaTrader 5 实现。不过, 如果不能为订单管理器提供指向停止对象的指针, 则计算出的止损或止盈将默认为零 (无止损/止盈)。

订单或仓位的平仓

以下图例大体描述了订单管理器是如何平仓和删除订单的:

离场流程图

如图例中所示, 该方法首先检查指向 COrder 实例的指针是否有效。然后, 进程获取需要处理的离场请求的品种和交易对象的正确实例。然后依据其类型删除订单或关仓。成功平仓/删除订单之后, COrder 实例随后从活动订单列表移动到订单管理器的历史订单列表 (已存档)。方法还将设置 COrder 对象的标志, 将其标记为已关闭。

设置验证

订单管理器上的设置验证将通过调用该类的 Validate 方法来实现。所述方法的代码显示在以下代码段中:

bool COrderManagerBase::Validate(void) const
  {
   if(CheckPointer(m_moneys)==POINTER_DYNAMIC)
     {
      if(!m_moneys.Validate())
         return false;
     }
   if(CheckPointer(m_stops)==POINTER_DYNAMIC)
     {
      if(!m_stops.Validate())
         return false;
     }
   return true;
      }

此代码与 MQL5 标准库中某些类中经常出现的 ValidationSettings 方法类似。它简单地调用其对象成员的 Validate 方法, 并且当这些对象之一失败验证时返回 false (最终导致专家系统的 OnInit 函数失败或返回 INIT_FAILED)。此方法原本是在执行专家交易系统的初始化函数期间被调用。

交易数量的计数

交易总数是订单管理器迄今为止建立的交易总数, 包括自专家交易系统或脚本启动以来已在历史中的交易。订单总数是指账户内的当前交易数量, 订单历史总数是指在订单管理器自己的历史订单记录中找到的。所以:

int COrderManagerBase::OrdersTotal(void) const
  {
   return m_orders.Total();
      }
int COrderManagerBase::OrdersHistoryTotal(void) const
  {
   return m_orders_history.Total();
      }
int COrderManagerBase::TradesTotal(void) const
  {
   return m_orders.Total()+m_orders_history.Total()+m_history_count;
      }
在这两种情况下, 我们进行订单计数时使用 MetaTrader 4 的标准概念 (每笔订单是一个仓位, 无论为市价还是挂单)。例如, 在 MQL4 的实现中, 我们将看到 TradeOpen 方法开始的两行如下:
int trades_total =TradesTotal();
  int orders_total = OrdersTotal();

此处要注意的重要是, OrdersTotal 引用了类的本地方法, 而非 MQL4 和 MQL5 中找到的 OrdersTotal 函数。如果我们用该语言的本地函数替代, 则调用 OrdersTotal 时应在函数名称之前使用作用域解析运算符:

int orders_total = ::OrdersTotal();

COrder 实例的存档

由于订单管理器将针对 MetaTrader 4 和 MetaTrader 5 相符的交易行为保持其自身的独立性 (但兼容两者), 因此必须有一种方式来标记 COrder 实例已属于历史。订单存储在 m_orders 和 m_orders_history 中, 这些是 COrders 的实例, 取决于它们是否已经是历史记录, 或者仍然活跃在市场上。后果即是, 对于这两个版本, 跨平台专家系统将需要检查给定的订单或交易是否已经离场。

由于两个平台记录交易入场的方式不同, 订单管理器需要保留自己独立的交易记录。一笔订单成功打开后, 将创建一个 COrder 实例, 最终将添加到 m_orders 中。一旦订单或持仓离场, 订单管理器就必须将 COrder 实例移动到 m_orders_history 之中。用于两个版本的类方法 ArchiveOrder 如下所示:

bool COrderManagerBase::ArchiveOrder(COrder *order)
  {
   return m_orders_history.Add(order);
      }

MQL4-规范的实现

订单或仓位的开仓

以下代码片段显示了 MQL4-规范 CorderManagerBase 类的继承类 TradeOpen 方法:

COrder* COrderManager::TradeOpen(const string symbol,ENUM_ORDER_TYPE type)
  {
   int trades_total = TradesTotal();
   int orders_total = OrdersTotal();
   m_symbol = m_symbol_man.Get(symbol);
   if (!CheckPointer(m_symbol))
      return NULL;
   if(!IsPositionAllowed(type))
      return NULL;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=0))
     {
      ENUM_ORDER_TYPE ordertype = type;
      double price=PriceCalculate(ordertype);
      double sl=0,tp=0;
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
        {
         sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price);
         tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price);
        }
      double lotsize=LotSizeCalculate(price,type,sl);
      ulong ticket = SendOrder(type,lotsize,price,sl,tp);
      if (ticket>0)
      {
         if (OrderSelect((int)ticket,SELECT_BY_TICKET))
            return m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice());
      }            
     }
   return NULL;
      }

函数接收两个参数, 品种或金融工具的名称, 以及开单类型。它首先获得处理请求所需的数值: 交易总数, 订单总数, 和订单的品种对象( 如方法的第一个参数所示)。

若满足 2 个预设条件 (最大订单和最大交易) 后, 方法采用特定对象成员计算主要的止损和止盈价位, 以及交易量。最后, 它发送订单, 操作成功后, 创建一个新的 COrder 实例, 并将其保存在活动订单列表 (在订单管理器) 中。

订单或仓位的平仓

以下代码片段显示了 MQL4-规范 COrderManagerBase 类的继承类 CloseOrder 方法:

bool COrderManager::CloseOrder(COrder *order,const int index=-1)
  {
   bool closed=true;
   if(CheckPointer(order)==POINTER_DYNAMIC)
     {
      if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),order.Symbol())!=0)
         m_symbol=m_symbol_man.Get(order.Symbol());
      if(CheckPointer(m_symbol))
         m_trade=m_trade_man.Get(order.Symbol());
      if(order.Volume()>0)
        {
         if(order.OrderType()==ORDER_TYPE_BUY || order.OrderType()==ORDER_TYPE_SELL)
            closed=m_trade.OrderClose((ulong)order.Ticket());
         else
            closed=m_trade.OrderDelete((ulong)order.Ticket());
        }
      if(closed)
        {
         int idx = index>=0?index:FindOrderIndex(GetPointer(order));
         if(ArchiveOrder(m_orders.Detach(idx)))
           {
            order.Close();
            order.Volume(0);
           }
        }
     }
   return closed;
  }

我们稍后将会看到, MQL4 版本比 MQL5 版本简单得多, 主要是因为它只有一个保证金模式 (对冲)。即使 MetaTrader 4 经纪商禁用对冲, 订单平仓的过程将保持不变: 如果是挂单则删除, 如果为市价单则平仓。

函数接收两个参数, 即订单对象及其在活动订单/持仓列表上的索引。如果指向 COrder 对象的指针有效, 我们随后会得到 CExpertTradeX 和 CSymbolInfo 的正确实例来平仓, 之后通过调用相应的函数将之放入交易终端的历史当中。

一旦订单或持仓出场后, 需要更新 COrder 对象。首先, 从活动订单列表中删除, 然后转移到历史订单列表的末尾。然后, 对象被标记为关闭, 并将其交易量清零。

类方法的第二个参数接收一个可选参数 (索引)。这是为了加快处理请求, 如果 COrder 实例在订单数组中的索引已预知 (这很平常, 因为订单经常需要迭代)。在索引未知的情况下, 只能使用一个参数来调用方法, 方法将调用另一个类方法 FindOrderIndex, 该方法将负责 COrder 实例在数组中的定位。


MQL5-规范的实现

订单或仓位的开仓

以下代码片段显示了 MQL5-规范 CorderManagerBase 类的继承类 TradeOpen 方法:

COrder* COrderManager::TradeOpen(const string symbol,ENUM_ORDER_TYPE type)
  {
   double lotsize=0.0,price=0.0;
   int trades_total =TradesTotal();
   int orders_total = OrdersTotal();
   m_symbol=m_symbol_man.Get(symbol);
   if(!IsPositionAllowed(type))
      return NULL;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=0))
     {
      price=PriceCalculate(type);
      lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?0:m_main_stop.StopLossCalculate(symbol,type,price));
      if (SendOrder(type,lotsize,price,0,0))
         return m_orders.NewOrder((int)m_trade.ResultOrder(),m_trade.RequestSymbol(),    (int)m_trade.RequestMagic(),m_trade.RequestType(),m_trade.ResultVolume(),m_trade.ResultPrice());
     }      
   return NULL;
      }

正如我们所见, 实现与 MQL4 的实现没有太大差异。然而, 关键的区别之一是在本代码中, 我们不需要获取止损和止盈价位的数值。原因是 MetaTrader 5 中的停止级别的行为与其前身不同。拥有 MQL5 专家交易系统开发经验的程序员会知道, 在这个函数库中, 我们将使用挂单作为 MQL4 中经纪商级别停止位的对手。

订单或仓位的平仓

以下代码片段显示了 MQL5-规范 COrderManagerBase 类的继承类 CloseOrder 方法:

bool COrderManager::CloseOrder(COrder *order,const int index=-1)   {    bool closed=true;    COrderInfo ord;    if(!CheckPointer(order))       return true;    if(order.Volume()<=0)       return true;    if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),order.Symbol())!=0)       m_symbol=m_symbol_man.Get(order.Symbol());    if(CheckPointer(m_symbol))       m_trade=m_trade_man.Get(order.Symbol());    if(ord.Select(order.Ticket()))    {       closed=m_trade.OrderDelete(order.Ticket());    }      else      {       ResetLastError();       if(IsHedging())       {          closed=m_trade.PositionClose(order.Ticket());       }         else         {          if(COrder::IsOrderTypeLong(order.OrderType()))             closed=m_trade.Sell(order.Volume(),0,0,0);          else if(COrder::IsOrderTypeShort(order.OrderType()))             closed=m_trade.Buy(order.Volume(),0,0,0);         }      }    if(closed)      {

      if(ArchiveOrder(m_orders.Detach(index)))         {          order.Close();          order.Volume(0);         }      }    return closed;   }

在 MQL5 中平单或平仓时, 我们必须考虑保证金模式 (净持或对冲)。但首先我们需要确定将要关闭的项目是否为 MQL5 的订单或持仓。我们可以使用 OrderSelect 和 HistoryOrderSelect 函数来完成此操作, 但是为了缩短此方法所需的代码, 并使此过程更容易, 我们将简单地使用 MQL5 标准库中的 COrderInfo 类来完成此操作。

MQL5中 的一笔订单是来自交易请求的结果, 通常会导致一笔或一组成交 (大致相当于 MetaTrader 4 市价订单)。然而, 如果请求无需即时执行, 那么它引用一个挂单 (而非在 MetaTrader 4 中, 订单可以是市价或待决)。现在, 要区分该项目的退出, 此方法首先使用 COrderInfo 检查该项目是否为挂单, 如果是, 则删除该挂单。如果通过 COrderInfo 选择失败, 那么我们确定它是一笔市价单或一笔持仓。对于对冲模式, 使用 PositionClose 函数简单地平仓。否则, 在净持模式下, 我们简单地通过一笔同等交易量的相反仓位来抵消持仓。

创建 COrder 的实例

我们已经看到订单管理器是如何处理交易的打开和关闭。在之前的文章中, 我们还展示了如何更改 CExpertTrade 类以便令其与两个交易平台兼容的方式。目前, 在两个交易平台中对于如何实施止损和止盈价位有所不同, 而在订单管理器中这个罕有地未做处理。此过程的其余部分是在 COrder 实例的初始化时设置的, 其在 COrders 的 NewOrder 方法中被调用。以下代码显示了 COrdersBase 的 Init 方法:

COrder* COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
      {  
         order.Magic(magic);
         order.Init(GetPointer(this),m_stops);
         return order;
      }  
   return NULL;
      }

正如我们所见, COrder 类的 Init 方法接收一个自定义对象 (CStops) 为其第二个参数。这是一个容纳停止对象的容器 (如前面所示的 m_main_stop 对象)。这个类对象将在另一篇文章中讨论。

订单或仓位的修改

我们还未展示任何允许订单管理器修改现有持仓的代码。这将被委托给另一个停止对象 (CStop 和 CorderStop), 其将在另一篇文章中讨论。这些对象将负责更新或修改持仓的停止级别, 以及与它们所属的 COrder 对象的协调。

在 MetaTrader 4 中, 挂单入场价格可以修改任意次数。在 MetaTrader 5 中则并非如此。这一次, MQL5 版本是受限组件, 所以我们将采用 MQL5 标准。修改挂单将需要删除现有的挂单, 并用更新后的属性创建一笔新订单。

示例

例如, 我们将使用本系列文章中讨论的类对象来实现专家交易系统。在 MetaEditor 上创建专家交易系统源文件之后, 我们将首先参考该函数库:

#include "MQLx\Base\OrderManager\OrderManagerBase.mqh"

请注意, 在本语句中, 我们使用引号而非 "<" 和 ">"。我们将函数库放在与专家交易系统源代码文件相同的目录下。

对于此 EA, 我们将要求至少三个指针, 它们必须在程序中声明为全局 (COrderManager, CSymbolInfo 和 CSymbolManager):

COrderManager *order_manager;
CSymbolManager *symbol_manager;
CSymbolInfo *symbol_info;

在 OnInit 函数之下, 我们必须初始化这三个指针, 特别是对于 CSymbolInfo 的实例, 需要在初始化期间分配一个特定的金融工具名称。

int OnInit()
  {
//---
   order_manager = new COrderManager();
   symbol_manager = new CSymbolManager();
   symbol_info = new CSymbolInfo();
   if (!symbol_info.Name(Symbol()))
   {
      Print("symbol not set");
      return (INIT_FAILED);
   }   
   symbol_manager.Add(GetPointer(symbol_info));   
   order_manager.Init(symbol_manager,NULL);
//---
   return(INIT_SUCCEEDED);
  }

在 OnDeinit 之下, 我们必须删除这三个指针, 这样它们就不会导致内存泄漏 (至少在交易平台内):

void OnDeinit(const int reason)
  {
//---
   delete symbol_info;
   delete symbol_manager;
   delete order_manager;
  }

在 OnTick 之下, 我们必须实现实际的策略。本示例中的专家系统将使用一种检测新柱线的简单方法 (通过检查图表上的柱线数)。之前的柱线数必须保存在静态变量 (或全局变量) 中。方向变量也是如此, 它将用来保存专家系统之前采用的方向 (如果是第一次交易, 则为零)。然而, 由于用来计算图表上柱线数的函数在两个平台之间有所不同, 因此我们不得不为此分开实现:

static int bars = 0;
static int direction = 0;
int current_bars = 0;
#ifdef __MQL5__
   current_bars = Bars(NULL,PERIOD_CURRENT);
#else 
   current_bars = Bars;
#endif

对于 MQL4 版本, 我们简单地使用名为 Bars 的预定义变量 (也可调用函数 iBars)。另一方面, 对于 MQL5 版本, 我们则调用 Bars 函数。

下一个代码片段可观察到专家系统实际行为的实现。如果检测到前一柱线和当前柱线之间的差异, 专家系统会首先初始化品种 (CSymbolInfo) 的汇率, 以便用于进一步的操作。然后检查是否之前的交易需要平仓。如果有所发现, 专家系统将之平仓, 并根据以前的方向继续处理另一笔交易的入场。代码通过更新 EA 上的柱线数来结束。 

if (bars<current_bars)
   {   
      symbol_info.RefreshRates();
      COrder *last = order_manager.LatestOrder();
      if (CheckPointer(last) && !last.IsClosed())
         order_manager.CloseOrder(last);
      if (direction<=0)
      {
         Print("买入交易入场..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
         direction = 1;
      }
      else
      {
         Print("卖出交易入场..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
         direction = -1;
      }   
      bars = current_bars;
   }

我们为了确保在初始版本和所有未来版本的专家交易系统中使用相同的代码, 我们将目前为止所完成的代码移到头文件中, 然后在主要的源文件中引用它 (MQL4 和 MQL5 版本分别有一个)。两个源文件 (test_ordermanager.mq4 或 test_ordermanager.mq5, 取决于目标平台) 将有一行代码引用主要的头文件:

#include "test_ordermanager.mqh"

下列表格显示了在 MetaTrader 4 以及 MetaTrader 5 的净持和对冲模式下运行专家交易系统的结果, 它们会出现在各自的策略测试报告中。出于简洁起见, 在本文中只包含前 10 笔交易 (完整报告可以在本文末尾的 zip 包中找到)。

MT4:

# Time Type Order Size Price S / L T / P Profit Balance
1 2017.01.02 00:00 buy 1 0.10 1.05102 0.00000 0.00000
2 2017.01.02 01:00 close 1 0.10 1.05172 0.00000 0.00000 7.00 10007.00
3 2017.01.02 01:00 sell 2 0.10 1.05172 0.00000 0.00000
4 2017.01.02 02:00 close 2 0.10 1.05225 0.00000 0.00000 -5.30 10001.70
5 2017.01.02 02:00 buy 3 0.10 1.05225 0.00000 0.00000
6 2017.01.02 03:00 close 3 0.10 1.05192 0.00000 0.00000 -3.30 9998.40
7 2017.01.02 03:00 sell 4 0.10 1.05192 0.00000 0.00000
8 2017.01.02 04:00 close 4 0.10 1.05191 0.00000 0.00000 0.10 9998.50
9 2017.01.02 04:00 buy 5 0.10 1.05191 0.00000 0.00000
10 2017.01.02 05:00 close 5 0.10 1.05151 0.00000 0.00000 -4.00 9994.50
11 2017.01.02 05:00 sell 6 0.10 1.05151 0.00000 0.00000
12 2017.01.02 06:00 close 6 0.10 1.05186 0.00000 0.00000 -3.50 9991.00
13 2017.01.02 06:00 buy 7 0.10 1.05186 0.00000 0.00000
14 2017.01.02 07:00 close 7 0.10 1.05142 0.00000 0.00000 -4.40 9986.60
15 2017.01.02 07:00 sell 8 0.10 1.05142 0.00000 0.00000
16 2017.01.02 08:00 close 8 0.10 1.05110 0.00000 0.00000 3.20 9989.80
17 2017.01.02 08:00 buy 9 0.10 1.05110 0.00000 0.00000
18 2017.01.02 09:00 close 9 0.10 1.05131 0.00000 0.00000 2.10 9991.90
19 2017.01.02 09:00 sell 10 0.10 1.05131 0.00000 0.00000
20 2017.01.02 10:00 close 10 0.10 1.05155 0.00000 0.00000 -2.40 9989.50


MT5 (净持):

Open Time Order Symbol Type Volume Price S / L T / P Time State Comment
2017.01.02 00:00:00 2 EURUSD buy 0.10 / 0.10 1.05140 2017.01.02 00:00:00 filled
2017.01.02 01:00:00 3 EURUSD sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 filled
2017.01.02 01:00:00 4 EURUSD sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 filled
2017.01.02 02:00:00 5 EURUSD buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 filled
2017.01.02 02:00:00 6 EURUSD buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 filled
2017.01.02 03:00:00 7 EURUSD sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 filled
2017.01.02 03:00:00 8 EURUSD sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 filled
2017.01.02 04:00:00 9 EURUSD buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 filled
2017.01.02 04:00:00 10 EURUSD buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 filled
2017.01.02 05:00:00 11 EURUSD sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 filled
2017.01.02 05:00:00 12 EURUSD sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 filled
2017.01.02 06:00:00 13 EURUSD buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 filled
2017.01.02 06:00:00 14 EURUSD buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 filled
2017.01.02 07:00:00 15 EURUSD sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 filled
2017.01.02 07:00:00 16 EURUSD sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 filled
2017.01.02 08:00:00 17 EURUSD buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 filled
2017.01.02 08:00:00 18 EURUSD buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 filled
2017.01.02 09:00:00 19 EURUSD sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 filled
2017.01.02 09:00:00 20 EURUSD sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 filled
2017.01.02 10:00:00 21 EURUSD buy 0.10 / 0.10 1.05164 2017.01.02 10:00:00 filled


MT5 (对冲):














Open Time Order Symbol Type Volume Price S / L T / P Time State Comment
2017.01.02 00:00:00 2 EURUSD buy 0.10 / 0.10 1.05140 2017.01.02 00:00:00 filled
2017.01.02 01:00:00 3 EURUSD sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 filled
2017.01.02 01:00:00 4 EURUSD sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 filled
2017.01.02 02:00:00 5 EURUSD buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 filled
2017.01.02 02:00:00 6 EURUSD buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 filled
2017.01.02 03:00:00 7 EURUSD sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 filled
2017.01.02 03:00:00 8 EURUSD sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 filled
2017.01.02 04:00:00 9 EURUSD buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 filled
2017.01.02 04:00:00 10 EURUSD buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 filled
2017.01.02 05:00:00 11 EURUSD sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 filled
2017.01.02 05:00:00 12 EURUSD sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 filled
2017.01.02 06:00:00 13 EURUSD buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 filled
2017.01.02 06:00:00 14 EURUSD buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 filled
2017.01.02 07:00:00 15 EURUSD sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 filled
2017.01.02 07:00:00 16 EURUSD sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 filled
2017.01.02 08:00:00 17 EURUSD buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 filled
2017.01.02 08:00:00 18 EURUSD buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 filled
2017.01.02 09:00:00 19 EURUSD sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 filled
2017.01.02 09:00:00 20 EURUSD sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 filled
2017.01.02 10:00:00 21 EURUSD buy 0.10 / 0.10 1.05164 2017.01.02 10:00:00 filled


请注意, 对于 MT5 的对冲和净持模式, 结果是一致的。虽然基本的实现是相同的, 但区别在于, 在净持模式中, 通过一笔等量交易来抵消一笔持仓, 而在对冲模式下, 持仓与许多交易者习惯的 MetaTrader 4 类似。在对冲模式下, 我们可以看到以下消息:

PE      0       16:19:15.747    Trade   2017.01.02 01:00:00   instant sell 0.10 EURUSD at 1.05172, close #2 (1.05172 / 1.05237 / 1.05172)
GP      0       16:19:15.747    Trades  2017.01.02 01:00:00   deal #3 sell 0.10 EURUSD at 1.05172 done (based on order #3)
DS      0       16:19:15.747    Trade   2017.01.02 01:00:00   deal performed [#3 sell 0.10 EURUSD at 1.05172]

请注意第一行右侧所说的 "close #2"。对冲模式表明哪笔特定交易将被抵消 (平仓)。另一方面, 在净持模式下, 我们仅能看到以下消息:

PG      0       16:20:51.958    Trade   2017.01.02 01:00:00   instant sell 0.10 EURUSD at 1.05172 (1.05172 / 1.05237 / 1.05172)
MQ      0       16:20:51.958    Trades  2017.01.02 01:00:00   deal #3 sell 0.10 EURUSD at 1.05172 done (based on order #3)
KN      0       16:20:51.958    Trade   2017.01.02 01:00:00   deal performed [#3 sell 0.10 EURUSD at 1.05172]

在此模式下, 如果已执行成交仅是为了新订单的入场, 或者简单地抵消现有的持仓, 那就不太明显了。

结构概览

COrderManager 类是本系列文章中所讨论的最复杂的类对象之一。为了给出有关最终订单管理器与其对象成员的外貌的思路, 请参考以下流程图:

订单管理器结构概览

简言之, 订单管理器将包含两个 COrders (当前和历史) 实例, 它们将作为订单管理器 (COrder) 已入场订单的容器。这些订单中的每一笔都可有相应的停止价位 (可以没有, 但也可能有多个), 并且这些价位中的每一个都可以有自己的尾随方法 (可以没有, 但也可能有多个)。虽然, 也许没发现大多数的专家交易系统的使用如此复杂, 但一些策略, 特别是处理多重支持和阻力级别的策略, 将会发现这种结构很有用。这些类成员将在今后的文章中讨论。

结论

在本文中, 我们讨论了 COrderManager 类, 它负责管理 EA 的交易操作。以这种方式设计 COrderManager 类即可处理 MQL4 和 MQL5, 因此交易者和程序员所编写的专家交易系统将能确保主要源文件或头文件代码级别上的跨平台兼容性。