通用智能交易系统:支持挂单和对冲(第五章)

Vasiliy Sokolov | 15 六月, 2016

内容简介表


简介

本文致力于开发一个通用的智能交易系统,给出交易引擎相关功能的进一步描述,用它你可以轻松地开发一个具有复杂逻辑的自动交易系统。CStrategy类在持续的优化中。新的算法正不断的被加入到交易引擎中,以使得交易更加简单高效。进一步的算法开发,其想法主要源自这一系列文章读者的反馈以及在文章讨论板块的提问或者给作者发送的私信。最经常被问到的问题是关于如何使用挂单。挂单在之前的文章中未被提及,CStrategy交易引擎没有提供一种便利的工具来处理挂单。本文向CStrategy交易引擎中引入一个重要的新功能。在这些改变之后,CStrategy现在能够提供操作挂单的新工具了。改变的细节我们之后详细讨论。

另外,新版MetaTrader 5现在支持使用对冲选项的帐户进行双边交易(参阅“MetaTrader 5的头寸对冲帐户系统”)。CStrategy的代码经过改变以囊括这些创新,并在新的帐户类型上正确的执行交易。仅改动了一小部分代码来增加对对冲的支持,也就是说我们一开始选择的方法是对的:不管新增还是变更,都不妨碍交易引擎的运作。另外,新的MetaTrader 5工具例如支持对冲,为构建策略提供了很多有趣的可能性,可以在新版CStrategy中实现。

此外,CStrategy现在支持直观且流行的获取当前Ask,Bid和最新价格的方法。这些方法在第一章中已经描述过了。

文章包含大量的信息,以覆盖CStrategy 和 MetaTrader 5中的所有创新和改变。本文包含在先前的系列文章中没有涉及的许多主题。我希望能够引起读者的兴趣。


通过Ask,Bid 和 Last方法访问最新价格。重写Digits函数

交易者经常需要访问当前价格。之前版本的CStrategy不包括访问这类数据的特殊方法。相反,它假设用户将使用标准函数来获取当前价格。例如,为了找到Ask价格,用户需要编写如下的代码:

double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
int digits = SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
ask = NormalizeDouble(ask, digits);

虽然你只需要一个函数 — SymbolInfoDouble 来获取Ask价格,但接收到的值得根据当前货币对的精度来进行标准化。因此,实际接收到的Ask价格需要执行更多的操作。对于Bid和Last价格也一样。

为了简化策略的使用方法:我们在CStrategy中添加三个方法:Ask(),Bid() 和 Last()。每一个方法都接收一个对应的价格,并将其根据当前货币对属性做相应的标准化。 

//+------------------------------------------------------------------+
//| 返回Ask价格
//+------------------------------------------------------------------+
double CStrategy::Ask(void)
  {
   double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   ask = NormalizeDouble(ask, digits);
   return ask;
  }
//+------------------------------------------------------------------+
//| 返回Bid价格
//+------------------------------------------------------------------+
double CStrategy::Bid(void)
  {
   double bid = SymbolInfoDouble(ExpertSymbol(), SYMBOL_BID);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   bid = NormalizeDouble(bid, digits);
   return bid;
  }
//+------------------------------------------------------------------+
//| 返回Last价格
//+------------------------------------------------------------------+
double CStrategy::Last(void)
  {
   double last = SymbolInfoDouble(ExpertSymbol(), SYMBOL_LAST);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   last = NormalizeDouble(last, digits);
   return last;
  }

这些方法在新版CStrategy类中定义,并且现在可以直接在其派生策略类中使用。我们将在策略发开的例子中使用它们。

除了通过重组方法来获取Ask,Bid和 Last价格外,CStrategy还重写了Digits函数。该函数返回当前货币对的小数点位数。看上去重写这类函数没有意义,但事实并非如此。EA所运行的货币对可能和含有策略的执行模块所运行的货币对不同。这是调用Digits函数会产生混淆。它会返回EA所运行货币对的小数位数而非执行策略所在货币对的小数位数。为了避免这种混淆,CStartegy中的Digits函数被同名函数重写。任何提及对该方法的地方,实际上调用的是这个方法。它返回EA运行所在货币对报价的位数。下面是该方法的源代码:

//+------------------------------------------------------------------+
//| 返回运行标的的小数位数
//| 
//+------------------------------------------------------------------+
int CStrategy::Digits(void)
  {
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   return digits;
  }

你必须意识到这一特性并理解重写该函数的意义。

 

支持带有对冲选项的帐户。

最近支持对冲的帐户已经被加到了最新版的MetaTrader 5中。在这类账户中,一个交易者会同时持有多种头寸,反向(buy或sell)或者同向。在CStrategy中,所有关于持仓的操作都在特殊的处理函数SupportBuy和SupportSell中进行。当前策略的头寸被逐个传递到这些方法中。仅有一个还是多个头寸并不重要。可以再同一个货币对上或者不同的货币对上开仓。处理和传输这些头寸的机制是一样的。因此,要增加对对冲的支持仅需小小的改动。首先,我们要改变RebuildPosition方法。当交易环境变化(新的交易被执行),该方法将重新组织持仓头寸列表。重组方式根据使用的模式的不同而不同。对于网络账户,将使用选择某货币对的一个头寸的算法。对于支持对冲的账户,将使用选择表中头寸索引的方法。

这里是RebuildPosition方法原先的版本:

//+------------------------------------------------------------------+
//| 重组持仓头寸列表
//+------------------------------------------------------------------+
void CStrategy::RebuildPositions(void)
{
   ActivePositions.Clear();
   for(int i = 0; i < PositionsTotal(); i++)
   {
      string symbol = PositionGetSymbol(i);
      PositionSelect(symbol);
      CPosition* pos = new CPosition();
      ActivePositions.Add(pos);
   }
}

在新版RebuildPosition中,取决于账户类型,使用两种持仓头寸选择算法:

//+------------------------------------------------------------------+
//| 重组持仓头寸列表
//+------------------------------------------------------------------+
void CStrategy::RebuildPositions(void)
  {
   ActivePositions.Clear();
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
      for(int i=0; i<PositionsTotal(); i++)
        {
         string symbol=PositionGetSymbol(i);
         PositionSelect(symbol);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
     }
   else
     {
      for(int i=0; i<PositionsTotal(); i++)
        {
         ulong ticket=PositionGetTicket(i);
         PositionSelectByTicket(ticket);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
     }
  }

注意同样的头寸类CPosition被用于对冲和净额结算账户。一但持仓头寸被选中,其属性可以通过PositionGetInteger,PositionGetDouble 和 PositionGetString方法来访问。选中的是对冲头寸还是一般头寸没有关系。在两种情况下对于持仓头寸的访问是一样的。这就是为何可以在不同的帐户类型中使用同样的头寸类CPosition。 

在CStrategy类内部没有其它需被重写的方法。CStrategy被设计为,基于该引擎的策略的操作取决于上下文。也就是说如果策略在具有对冲选项的账户上运行并且持有同一方向的多个头寸,它将并行管理这些头寸,将每个头寸当做一个独立的CPosition类来对待。相反,如果一个帐户中同一货币对上仅允许持有一个头寸,策略将同样以CPosition对象的形式来管理这一个头寸。  

除了RebuildPosition方法中的改变外,我们还需要修改一些CPosition的内部方法。类位于PositionMT5.mqh文件中,并且它包含基于系统功能的方法。同时,CPosition还使用标准交易类CTrade。最新版的CTrade类已经被修改过,因此可以支持使用对冲头寸的属性。例如,通过调用CTrade::PositionCloseBy方法,一个用作对冲的头寸可以被一个反向头寸所平仓。下面是CPosition的方法,其中的内容有所变化:

//+------------------------------------------------------------------+
//|                                                  PositionMT5.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include "Logs.mqh"
#include <Trade\Trade.mqh>
#include "Trailing.mqh"
//+------------------------------------------------------------------+
//| 典型策略的持仓头寸类
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
   ...
  };
...
//+------------------------------------------------------------------+
//| 返回当前头寸的止赢价格
//| 如果止损未设置,返回0.0
//+------------------------------------------------------------------+
double CPosition::StopLossValue(void)
{
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_SL);
}
//+------------------------------------------------------------------+
//| 设置止赢价格
//+------------------------------------------------------------------+
bool CPosition::StopLossValue(double sl)
{
   if(!IsActive())
      return false;
   return m_trade.PositionModify(m_id, sl, TakeProfitValue());
}
//+------------------------------------------------------------------+
//| 返回当前头寸的止赢价格
//| 如果止损未设置,返回0.0
//+------------------------------------------------------------------+
double CPosition::TakeProfitValue(void)
{
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_TP);
}
//+------------------------------------------------------------------+
//| 设置止赢价格
//+------------------------------------------------------------------+
bool CPosition::TakeProfitValue(double tp)
  {
   if(!IsActive())
      return false;
   return m_trade.PositionModify(m_id, StopLossValue(), tp);
  }
//+------------------------------------------------------------------+
//| 按当前市价平仓并设置一个平仓备注
//| 'comment'                         
//+------------------------------------------------------------------+
bool CPosition::CloseAtMarket(string comment="")
  {
   if(!IsActive())
      return false;
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      return m_trade.PositionClose(m_symbol);
   return m_trade.PositionClose(m_id);
  }
//+------------------------------------------------------------------+
//| 返回当前头寸大小                        
//+------------------------------------------------------------------+
double CPosition::Volume(void)
  {
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_VOLUME);
  }
//+------------------------------------------------------------------+
//| 返回以存款货币计算的当前持仓头寸的获利
//+------------------------------------------------------------------+
double CPosition::Profit(void)
  {
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_PROFIT);
  }
//+------------------------------------------------------------------+
//| 返回true如果头寸被激活。否则返回false     
//||
//+------------------------------------------------------------------+
bool CPosition::IsActive(void)
{
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      return PositionSelect(m_symbol);
   else
      return PositionSelectByTicket(m_id);
}
//+------------------------------------------------------------------+


你可以看到,这些方法的基础是对另一个方法IsActive的调用。如果由CPosition对象表示的有效头寸在系统中存在,则方法返回true。该方法实际上根据帐户类型使用两种方法之一选择一个头寸。对于传统的净额结算帐户,使用PositionSelect函数来选择一个头寸,你要为其确定货币对。对于具有对冲选项的帐户,基于订单编号使用PositionSelectByTicket函数来选择一个头寸。选择的结果(true或false)返回调用该方法的处理函数。这个方法为进一步的头寸操作设置上下文属性。没有必要修改交易算法,因为CPosition的所有交易函数都是基于CTrade类的。CTrade能够修改、开仓和平仓传统的和双边的持仓头寸。  

 

在先前的CStrategy版本中使用挂单

对于挂单的使用是许多交易算法的重要组成部分。在CStrategy交易引擎的第一个版本发布之后,我收到许多关于使用挂单的问题。在这一及后续板块中我们将讨论使用挂单进行交易的相关问题。

CStrategy交易引擎最初诞生的时候并不支持挂单。但是这并不意味着基于CStrategy类开发的策略不能执行挂单操作。在第一版CSTrategy中,可以通过使用标准的MetaTrader 5函数,如OrdersTotal()和OrderSelect()来实现。

例如,一个EA在每个新柱形上设置或者修改先前放置的stop挂单,其激活价格比当前价格高(对于buy)或者低(对于sell)0.25%。这个想法是如果价格在一个柱形中突然运动(脉冲式的),然后这个订单将被激活,并且该EA将在强烈的价格运行阶段介入。如果运动不够强烈,那么订单将不会在当前柱形内被激活。在这种情况下有必要将该订单移动到新的水平上。这个策略的全面实现在下面描述此算法的版块中给出,我将其成为CImpulse。从系统的名称可见,它是基于脉冲算法的。它在挂单激活时需要一个独立的入场方法。如我们所知,CStrategy含有特殊的重写方法:BuyInit和SellInit,用于开仓。因此,挂单相关的算法应添加到这些方法中去。在没有CStrategy类的直接支持下,Buy操作的代码如下:

//+------------------------------------------------------------------+
//| Buy类型挂单的执行逻辑     
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需事件
   if(positions.open_buy > 0) return;                    // 如果已经存在至少一个未平仓的买单,无需再买入
   int buy_stop_total = 0;
   for(int i = OrdersTotal()-1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if(!OrderSelect(ticket))continue;
      ulong magic = OrderGetInteger(ORDER_MAGIC);
      if(magic != ExpertMagic())continue;
      string symbol = OrderGetString(ORDER_SYMBOL);
      if(symbol != ExpertSymbol())continue;
      ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if(order_type == ORDER_TYPE_BUY_STOP)
      {
         buy_stop_total++;
         Trade.OrderModify(ticket, Ask()*0.0025, 0, 0, 0);
      }
   }
   if(buy_stop_total == 0)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*0.0025, ExpertSymbol(), 0, 0, NULL);
  }

IsTrackEvents方法确定接收到的事件对应于当前货币对新柱形的开始。然后,EA检测买入方向的未平仓订单的数量。如果有至少一个买入头寸,EA将不继续做买入操作,逻辑完成。然后策略检测所有当前存在的挂单。它遍历挂单的索引。每一个订单通过气索引被选中,然后分析其编号和对应的货币对。如果这2个参数同EA的参数相对应,则认为该订单属于当前的EA,订单计数器则加一。将订单的入场价格设置为比当前价格高0.25% 。如果没有挂单,即buystop订单计数器的值为0,在距当前价格0.25%的地方放置一个新的挂单。

注意没有订单通过InitBuy开仓。CStrategy在开仓的方法上不施加这样的限制。因此,任何EA逻辑都可以被加入到这里。然而,为了正确的处理事件,EA逻辑必须同开仓精确的结合起来,无论通过挂单还是市价单。 

从这个例子中可以看出,对于挂单的处理方式和普通订单类似。主要的要求是一致的:通过在独立的BuyInit和SellInit方法中描述它们,有必要将Buy和Sell的逻辑区分开来。BuyInit中只处理买入类型的挂单。SellInit中只处理卖出类型的挂单。剩下来的挂单的操作E逻辑和在 MetaTrader 4中的经典方式类似:遍历订单,选择属于当前EA的订单,分析交易环境并决定是新开仓还是改变已存在的订单。 

 

用于执行挂单的CPendingOrders和COrdersEnvironment。

处理挂单的标准MetaTrader 5函数为全面且简单的监控挂单提供了一个便捷的系统。然而,CStrategy是一组面向对象的类的集合,可用于创建交易系统。所有在CStrategy中执行的操作都是面向对象的,也就是说,它们通过对象来执行交易操作。这种方法有很多好处。这里是其中的一些。

  • 减小自定义源代码的大小。许多操作,诸如价格标准化或者准备用于CopyBuffer类函数的接受数组,都在“后台”执行,方法只需给出一个现成的结果。因此,你无需写额外的处理函数来检查结果及其他中间过程,但是你直接使用MetaTrader 5的系统函数就不可避免的要写这些代码了。
  • 独立于平台的。既然所有的类和方法都是用正式的MQL语言编写的,就可以通过一种通用的方法来获取任何属性。然而,这个方法的实际实现将随着平台的不同而不同。但这对用户层面来说无所谓。这意味着在一个平台上开发的EA理论上可以编译后在另一个平台上运行。但是在实际使用中会遇到各种问题,我们不打算在这篇文章中讨论平台独立性。
  • 功能。MQL函数集提供了基本功能,通过组合你可以创建复杂的算法和有用的函数。当这些算法被包含到一个独立的类库中,如CStartegy中后就能更为方便的使用它们了。额外的新函数不会增加其操作的复杂度,因为在这种情况下仅需添加新的模块,这些模块对于EA的开发者来说可以选择用或者不用。

为了坚持用这种方法,我们打算扩展CStartegy的功能,提供一种方便的面向对象的机制来操作挂单。此机制由两个类表示:CPendingOrder 和 COrdersEnvironment。COrder提供一个简便的对象, 其中包含一个挂单的所有属性,可以通过OrderGetInteger,OrderGetDouble 和 OrderGetString来访问这些属性。COrdersEnvironment类的用途后面再介绍。

假设CPendingOrder对象代表一个真实存在于系统中的挂单。如果你删除了这个挂单,那么对于代表此订单的CPendingOrder对象来说会发生什么?如果对象在真实挂单被删除后仍旧存在,这会导致严重的错误。EA将查找CPendingOrder并将错误的“认为”挂单还在系统中存在。为了避免此类错误,我们必须确保交易环境和CStrategy对象环境的同步。换句话说,我们需要建立一种机制,确保仅允许访问在系统中确实存在的那些对象。这就是COrdersEnvironment类的作用。它的实现非常简单,它仅允许访问那些代表真实存在挂单的CPendingOrders对象。

COrdersEnvironment类的基础是由GetOrder和Total方法组成的。第一个方法返回CPendingOrders对象,对应于MetaTrader 5挂单系统中一个带有特定索引的挂单。第二个方法返回挂单的总数。现在是时候来仔细研究下这个类了。这里是类的源代码:

//+------------------------------------------------------------------+
//| 用于操作挂单的一个类
//+------------------------------------------------------------------+
class COrdersEnvironment
{
private:
   CDictionary    m_orders;         // 挂单的总数
public:
                  COrdersEnvironment(void);
   int            Total(void);
   CPendingOrder* GetOrder(int index);
};
//+------------------------------------------------------------------+
//| 我们需要知道当前货币对及EA的编号
//+------------------------------------------------------------------+
COrdersEnvironment::COrdersEnvironment(void)
{
}
//+------------------------------------------------------------------+
//| 返回一个挂单
//+------------------------------------------------------------------+
CPendingOrder* COrdersEnvironment::GetOrder(int index)
{
   ulong ticket = OrderGetTicket(index);
   if(ticket == 0)
      return NULL;
   if(!m_orders.ContainsKey(ticket))
      return m_orders.GetObjectByKey(ticket);
   if(OrderSelect(ticket))
      return NULL;
   CPendingOrder* order = new CPendingOrder(ticket);
   m_orders.AddObject(ticket, order);
   return order;
}
//+------------------------------------------------------------------+
//| 返回挂单的数量
//+------------------------------------------------------------------+
int COrdersEnvironment::Total(void)
{
   return OrdersTotal();   
}

Total方法返回当前系统中存在的挂单的总数。方法不会出错,因为它返回的是从OrdersTotal()函数中接收到的系统值。

对于GetOrder方法,需要指定系统中挂单的索引。我们总是通过Total方法来获知准确的订单总数,订单的索引也总能获知,并且和系统中真实挂单的索引相对应。进一步,GetOrder方法通过索引接受一个挂单的标识符。如果一个订单由于某种原因被移除,订单ID将等于0,那么NULL将被返回给EA,这代表索引对应的订单无法被找到。

每个对象都是动态创建的,需要通过一个特殊的删除符来显式删除。因为GetOrder使用new操作符动态创建CPendingOrders对象,这些对象也要删除之。为了使用户无需在创建对象后做删除操作,我们使用一向特殊的技术 — 该对象位于COrdersEnvironment对象内的一个特殊的字典容器中。字典元素可以通过其唯一的键来访问,在这个例子中即是订单ID。在这种情况下,如果订单还在,先前创建的代表此订单的对象很可能被创建并放到了此容器中。因此GetOrder函数返回这个先前创建的对象。如果这是首次调用带有特殊ID的订单,一个新的CPendingOrder对象被创建,之后被添加到字典中,然后返回给用户对象的一个引用。

我们能从提出的方法中能得到些什么?首先,GetOrder方法确保仅返回一个代表真实存在的挂单的对象。系统函数OrderGetTicket负责实现此功能。其次,对象仅当未被创建过时才会被创建。这能节省额外的计算机资源。最后,此方法将用户从删除结果对象中解放出来。因为所有对象都存储在字典中,在COrdersEnvironment析构的时候它们将自动被删除。 

 

EA代码中关于挂单的部分

现在是时候重写再先前板块“在原先的CStrategy版本中使用挂单进行交易”中介绍的带有挂单的执行逻辑了。这里是含有CPendingOrder和COrdersEnvironment 类的代码:

//+------------------------------------------------------------------+
//| 当快速MA在慢速MA之下卖出     
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // 仅处理所需事件
   if(positions.open_buy > 0) return;                    // 如果已经存在至少一个未平仓的买单,无需再买入
   int buy_stop_total = 0;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
     {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
       {
         buy_stop_total++;
         Order.Modify(Ask() + Ask()*0.0025);
       }
       //删除订单;没有必要删除订单对象!
     }
   if(buy_stop_total == 0)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*0.0025, ExpertSymbol(), 0, 0, NULL);
  }

PendingsOrders对象是一个COrdersEnvironment类。使用系统函数搜索挂单的方式也一样。然后根据挂单索引i变量,尝试获取订单对象。如果订单由于某些原因订单没有被获取到,或者订单属于另一个EA,则继续查找新的订单。在这种情况下使用CPendingorder对象的IsMain方法,如果订单的u哦比对和编号和EA的货币对和编号一致则返回true,这就意味着此订单是属于这个EA的。

如果订单类型为 ORDER_TYPE_BUY_STOP,意思是订单未被激活,并且它的位置应该被修正为:当前价格 + 0.25%。挂单的修改通过使用Modify方法和其重载的版本来实现。你可以设置价格以及其他想要改变的参数。

注意当对一个挂单的操作完成后,没有必要删除订单对象。订单的引用应该原封不动的留在那里。PendingOrders容器管理CPendingOrder类型的对象,在用户函数中无需删除返回的对象。 

如果没有挂单且buy_stop_total计数器等于零,那么会放置一个新的挂单。使用Trade模块来放置一个新的订单,这在本文前面的部分已经介绍过了。 

在面向对象的方法中,一个挂单的属性需通过CPendingOrders对象的适当方法来访问。这种访问方式在保留可靠性的同时精简了代码,因为PendingOrders确保通过它仅有当前存在的对象才会被接收,例如在系统中当前真实存在的挂单。

 

使用挂单的CImpulse智能交易系统的交易逻辑 


我们已经分析了如何使用挂单,现在我们可以使用交易引擎的所有能力,来创建一个成熟的EA了。我们的策略基于运行方向上的剧烈市场运动,称为CImpulse。在每个新柱形的开始,EA会检测以百分比形式表示的和柱形开盘价之间的距离。EA将在当前价格相等的距离上分别建立BuyStop和SellStop订单。距离被设置为百分比形式。如果其中的一个订单在一个柱形内被激活,这意味着价格运动了很长一段距离,预示着市场价格运动剧烈。订单将被执行并将转化为一个持仓头寸。

持仓头寸将通过简单的移动平均来管理。如果价格回到移动平均线上来,持仓将被平仓。下面的截图显示了一个典型的BuyStop挂单激活的情景:


图. 1. CImpulse策略的多头头寸。
 

在上面的截图中,一个柱形的开始用一黑色的三角形标记。在这个柱形的开始,距离柱形开盘价0.1%的地方放置两张挂单。其中之一 — BuyStop订单 — 激活了。一个新的多头头寸开仓了。一旦某一个柱形收盘在移动平均线之下,显示为红色的线,EA就将此订单平仓。在图. 1中,平仓显示为一个蓝色的小三角。

如果一个订单没有在一个柱形内被激活,那么它将被移动到基于当前价格的新水平上。

所介绍的策略有一个处理挂单的特殊特征。BuyStop订单的位置可以比当前移动平均线低。在这种情况下,头寸可能在开仓后被立即平仓,因为当前价格在移动平均线之下。对于空单也一样:SellStop的激活价格可能比移动平均线要高。为了避免这种立即被平仓的情况,我们需要在BuyInit 和 SellInit方法中增加一个对移动平均水平的额外效验。因此,如果当前价格高于移动平均线,则算法仅设置BuyStop挂单。对于SellStop也一样:它们仅在价格低于移动平均时被设置。

我们还将使用MetaTrader 5的新特性 — 对冲账户的操作。这一特性意味着在一个柱形内可以同时持有多单和空单。然而,从EA最初的设计逻辑看,多单和空单是分离的,这是得我们不用改变交易策略的代码。不考虑持仓头寸的数量,当柱形的开盘价高于移动平均是所有的空单将被平仓,多单将在价格跌落到移动平均之下被平仓。这种情况下我们是否在对冲账户交易就不重要了。 

首先我们要去开设一个具有对冲选项的帐户,可以通过在“帐户开设”窗口勾选相应的标识来实现。

 

图 2. 开设一个带有对冲选项的帐户 

开设成功后我们就可以进行交易了。首先,我们要编写CImpulse类来实现讨论过的交易算法的逻辑。 

 

CImpulse策略类

此外,有一个CImpulse类的列表,描述了我们新EA的逻辑。为了更为清晰的阐述挂单操作,这个类尽可能的简单,它不包含和日志相关的函数,以及从XML文件中解析策略参数的特殊方法:

//+------------------------------------------------------------------+
//|                                                      Impulse.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>

input double StopPercent = 0.05;
//+------------------------------------------------------------------+
//| 定义挂单相关操作
//| 
//+------------------------------------------------------------------+
enum ENUM_ORDER_TASK
{
   ORDER_TASK_DELETE,   // 删除挂单
   ORDER_TASK_MODIFY    // 修改挂单
};
//+------------------------------------------------------------------+
//| CImpulse策略           
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // 挂单位置的百分比值
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CIndMovingAverage Moving;
};
//+------------------------------------------------------------------+
//| 建立一个BuyStop多头挂单
//| 
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_buy > 0) return;                    
   int buy_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Ask() + Ask()*(m_percent/100.0);
   if(target < Moving.OutValue(0))                    // 订单的激活价格必须高于移动平均价
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            buy_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(buy_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| 建立一个SellStop空头挂单  
//| 
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_sell > 0) return;                    
   int sell_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Bid() - Bid()*(m_percent/100.0);
   if(target > Moving.OutValue(0))                    // 订单的激活价格必须低于移动平均价
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_SELL_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            sell_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(sell_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| 根据移动平均来管理一个多头头寸
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| 根据移动平均来管理一个空头头寸
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| 过滤到来的事件如果传入的事件没被 
//| 策略处理,返回false;如果已处理,   
//| 返回 true                  
//+------------------------------------------------------------------+
bool CImpulse::IsTrackEvents(const MarketEvent &event)
  {
//我们仅在当前运行货币对、当前时间框架下新柱形开始时做处理
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| 响应货币对变化
//+------------------------------------------------------------------+
void CImpulse::OnSymbolChanged(string new_symbol)
  {
   Moving.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| 响应时间框架变化
//+------------------------------------------------------------------+
void CImpulse::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   Moving.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| 返回突破位置的百分比值
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| 设置突破位置的百分比值
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

配置并开始此EA策略的标准mq5文件如下:

//+------------------------------------------------------------------+
//|                                                ImpulseExpert.mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\Impulse.mqh>

CStrategyList Manager;
//+------------------------------------------------------------------+
//| EA初始化函数             
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   CImpulse* impulse = new CImpulse();
   impulse.ExpertMagic(1218);
   impulse.Timeframe(Period());
   impulse.ExpertSymbol(Symbol());
   impulse.ExpertName("Impulse");
   impulse.Moving.MaPeriod(28);                      
   impulse.SetPercent(StopPercent);
   if(!Manager.AddStrategy(impulse))
      delete impulse;
//---
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|  EA的tick函数            
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   Manager.OnTick();
  }
//+------------------------------------------------------------------+


 

分析CImpulse策略

请看附件视频,它演示了在策略测试中使用挂单的EA操作执行情况。当新的柱形到来时,设置BuyStop和SellStop。订单被放置在距离现价一定距离的地方,从而形成了一个动态通道。一旦一个挂单的价格低于移动平均线,则被完全移除。然而,当挂单的激活价格变得高于移动平均价时,再次挂单。对于SellStop订单也一样。


下面的截图清晰的显示了对冲情况的产生,例如,有一个Buy持仓头寸并且激活了空单的平仓。之后,多单继续存在并根据其逻辑平仓,即当柱形开盘价格低于移动平均线时。

 

图 3. 支持对冲的帐户的头寸管理。

在传统账户上实现EA逻辑会有产生相似的情景,虽然存在一些细微的差异:

 

图 4. 净额帐户上的头寸管理。 

在强烈的价格运动阶段会执行Buy操作,并且会将先前的空头头寸平仓。因此,所有的持仓订单都被平仓了。因此,在下一个柱形EA开始放置新的Buy挂单,其中之一被激活了转化为新的多头头寸,然后就像对冲账户上的一样被平仓。

我们可以改变EA的逻辑使之多态化,如根据账户类型执行相应的算法。显然在净额账户上一个货币对上只会有一个未平仓头寸。为了避免当新开仓时平仓反向单,我们应该为所有净头寸添加止损。止损位置应该等于反向单的激活位置。因此,激活一个Stop挂单的同时意味着触发了反向头寸的止损(如果那时存在反向头寸的话)。这一逻辑仅在净额帐户生效。它应当被添加到BuySupport和SellSupport方法中。这里是Support方法修改后的源代码:

//+------------------------------------------------------------------+
//| 根据移动平均来管理一个多头头寸
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = Bid() - Bid()*(m_percent/100.0);
      if(target < Moving.OutValue(0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| 根据移动平均来管理一个空头头寸
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = Ask() + Ask()*(m_percent/100.0);
      if(target > Moving.OutValue(0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}

更新功能后的策略测试结果和之前略有不同。在净额帐户上,EA将像传统的只能交易系统一样只操作一个头寸:


图. 5. 在经典净额账户上由一个多态EA管理的头寸。

本文的附件包含最新版的CImpulse策略,不同的帐户类型用不同的逻辑实现。   

注意交易逻辑的所有版本都是正确的。然而,交易策略的具体操作方式取决于策略本身。CStrategy仅提供一个通用的接口,根据头寸的类型类操作它们。 

 

总结 

我们回顾了CStrategy交易引擎的新特性。新的功能包括对新账户类型的支持、对挂单对象的操作以及处理当前价格的一系列扩展函数。

CStrategy类的新方法使你能够快速简便的访问当前价格,如Ask,Bid,和 Last。重写的Digis方法现在能够总是返回货币报价的正确小数位数。 

使用特殊的CPendingOrders和COrdersEnvironment类来处理挂单简化了交易逻辑。EA可以通过一个特定的CPendingOrder类型的对象来访问一个挂单。通过改变对象的属性,例如,订单的激活位置,EA根据真实订单对应的对象来改变其属性。面向对象的模型能够提供较高水平的可靠性。如果对象在系统中没有对应真实挂单,那就不可能访问到该对象。有关挂单的操作在BuyInit和SellInit方法中实现,在策略中要重写这两个方法。BuyInit设计为仅用于处理BuyStop和BuyLimit类型的挂单。SellInit设计为仅用于处理SellStop和SellLimit类型的挂单。  

在我们的CStrategy交易引擎内,支持对冲的帐户的处理方式和传统的帐户实际上没有什么两样。持仓头寸的处理取决于其类型,通过CPosition类来实现。在这些类型帐户上操作的唯一不同之处在于策略的逻辑。如果策略处理的是单一头寸,它只需实现恰当的逻辑来管理那个唯一的头寸。如果策略同时处理多个头寸,那么其必须考虑对应的BuySupport和SellSupport方法,并且当下单成功时会转化为多个持仓头寸同时存在。交易引擎本身不实现任何交易逻辑。它仅根据帐户类型提供对应的头寸类型。