通用智能交易系统:交易策略的模式(第一章)

Vasiliy Sokolov | 4 四月, 2016

目录

 

简介

在实现自动交易算法时可能会出现各种子任务,包括分析市场环境来确定入市信号,以及平仓已有头寸。另一个可能的任务是控制EA的执行并恰当处理交易报错。最后,通过EA访问市场行情和交易头寸信息是一项相对简单的任务。所有这些任务都直接在EA的源码中进行实现。

另一方面,我们应该在EA中将交易过程的技术部分同交易思想的实现分离开来。使用面向对象的方法,我们可以分离这两个本质上不同的交易任务,并且将交易处理的实现放在一个特殊的公共类中给所有策略调用,有时也被称为交易引擎

这是描述这个引擎的系列文章的第一篇,它可以被称为通用智能交易系统。此名称是一组类的统称,通过一个开仓和平仓条件通用枚举值,使得交易算法的开发简便化。你无须向EA中添加所需数据和交易逻辑,例如,持仓查询 — 所有这些处理过程都由交易引擎完成。

多提出方案的涉及面非常广,因此将其分为四个部分论述。一下是这些部分的详细信息。

第一部分。策略的交易模式。将在本文中介绍。第一部分基于交易模式描述初始持仓管理的概念。使用交易模式EA的交易逻辑可以非常容易地被定义。以这种模式编写的EA容易调试。这些EA的逻辑变得通用和相似,这有利于组织管理此类交易策略。在本文中阐述的思想是通用的,并且无需额外的面向对象的编程。这就意味着不管你是否使用我提供的类库集,这些资料对你也将是有帮助的。

第二部分。事件模型和交易策略原型。这部分描述一个基于集中事件处理的原始事件模型。也就是所有事件被及总在EA交易逻辑的一个地方“集中”处理。同时,事件是多货币对的。例如,一个EA运行在EURUSD图表上,也可以接收来自GBPUSD的新报价事件。这个事件处理模型在开发多货币对EA时就非常有用了。在这部分,我们还将介绍CStrategy交易引擎的基本类,以及在MetaTrader 5中表示头寸的CPositionMT5类。

第三部分。自定义策略和辅助交易类。资料涵盖了如何开发自定义EA的过程。从本文中,您将了解到如何通过一个简单的开平仓条件枚举来创建一个智能交易系统。这部分还将介绍各种辅助算法,可以大大简化访问交易信息的过程。

第四部分。用一组策略进行交易以及组构建策略组合。这部分包含将多个逻辑集成到一个单一可执行的ex5模块中的,特殊算法的介绍。还介绍了可以使用一个XML文件生成一组策略的机制。

 

用于开新仓和管理已有仓位的方法

要了解本文提出的方法,我们首先尝试介绍一种基于两个均线的经典交易系统,一条均线为短周期,另一条为长周期。因此,长周期均线要比短周期均线对于价格变化的反应来得慢。交易策略很简单,如果快速均线上穿慢速均线,EA下单买入。反之如果快速均线下穿慢速均线,EA下单卖出。下图是我们策略的图示:

图 1. 基于两个均线的交易策略

红线代表快速简单移动平均线,周期为50。蓝线代表慢速移动平均线,周期为120。当它们交叉时(交叉点用蓝色虚线标记),EA的交易方向改变。从非算法角度来看,任何交易者都足以从上述描述中知道如何使用此策略来交易。然而,这个描述还不足以创建基于此策略的智能交易系统。

让我们考虑当快速MA上穿慢速MA时EA要执行的交易动作:

  1. 如果当MA穿越时EA有一个未平仓的单,那么这个订单需要被平仓。
  2. 检查当前是否存在单。如果没有,则新开一个多单。如果已经有了一个多单,则不执行任何操作。

当快速MA下穿慢速MA时,需要执行相反的操作:

  1. 如果当MA穿越时EA有一个未平仓单,则需要将其平仓。
  2. 检查是否存在单。如果没有空单,则新开一个空单。如果已经存在,则不执行任何操作。

我们有四个交易动作来描述这个策略的交易过程。两个交易动作用来描述多头仓位的开仓和保持规则。另外两个用来描述空头仓位的开仓和保持规则。这么简单的一个交易处理过程用四个动作序列来描述,看起来太复杂了。事实上,在我们的策略中,多头仓位的建立和空头仓位的退出正好是一样的,因此为何不将它们组合到一个交易动作中或者至少一个执行逻辑里呢?不,不要这样做。要证明这点,让我们改变策略的初始条件。

现在我们的策略将使用不同的移动平均线组合来做买入和卖出交易。例如,当快周期为50的速移动平均线上穿周期为120的慢速移动平均线时,开一个多仓。当快周期为20的速移动平均线下穿周期为70的慢速移动平均线时,开一个空仓。现在,买入信号将和卖出信号不同 — 它们将发生在不同的时刻和不同的市场环境下。

提出的规则是没有考虑到的。策略经常使用入场和出场的“镜像”条件:开多单意味着平空单,反之亦然。然而,其他情况也有可能发生。如果我们要创建一个通用的EA模型,我们必须将其考虑在内,因此我们要有四个规则。

更进一步,我们将从不同的角度考虑执行过程。下表显示了交易擦做的类型(买入或卖出),以及交易类型(开仓或平仓)。表中的单元格包含一个特定的指令集:

买入 卖出
开仓 1. 如果没有未平仓多单,周期为50的快速MA在周期为120的慢速MA之上,则开多单 2. 如果没有未平仓空单,周期为50的快速MA在周期为120的慢速MA之下,则开多空单
平仓 3. 如果周期为50的快速MA在周期为120的慢速MA之下,多单需平仓 4. 如果周期为70的快速MA在周期为120的慢速MA之上,空单需平仓

 

表 1. 交易动作集合

从编程的观点来看,这些“规则集”或者表格块将是通用函数或者将来通用策略的类中的部分方法。我们像下面这样命名这四种方法:

我们能从提出的方法中能得到些什么?首先,我们已经将EA需要执行的交易操作进行了分类。所有的操作被分为独立的模块,例如,通用类方法。这意味着我们将无需在代码中考虑在何处处理不同的交易逻辑。编程任务简化为描述四个方法。

其次,如果我们想要改变EA的逻辑,我们仅需向方法中添加额外的条件。第三,所提出的交易逻辑的组织方法对于任何以这种风格开发的EA,都将支持简单且自然的交易模式

 

一个策略的交易模式

通常,EA的交易操作需要被限制。最简单的例子是防止EA下多单或空单的操作。MetaTrader 4 提供了这些模式的一个典型开关。它在EA加载的时候出现,位于EA属性窗口的一个标签页上。

图 2. MetaTrader 4中的交易模式

然而,有更多的可能模式。此外,我们可能需要更灵活的工具来配置这些模式。例如,一些EA在交易中的某些时刻需要暂停。假设在太平洋时间段的外汇市场,EA需要忽略新的开仓信号。这是限制EA在低流动性时间段进行交易的典型方法。实现这一功能的最好方法是什么,把它做成可选项?这可以通过4块交易逻辑的组织来实现。

可以通过暂时禁用SellInit方法来实现一段时间内禁止卖出,SellInit方法包含了开空头仓位的规则。因为所有执行卖出操作的交易动作都将在此函数中实现。对于买入操作也一样:不调用BuyInit方法将无法开多仓。因此,这些函数的特定组合将对应恰当的EA交易模式。在表2中描述这些方法:

交易模式 描述 被调用的方法 被忽略的方法
买和卖 买和卖操作被允许没有交易限制。 BuyInit
SellInit
BuySupport
SellSupport
仅买 仅允许买不执行卖出操作之前开的空头头寸以普通模式进行管理直到平仓。 BuyInit
BuySupport
SellSupport
SellInit
仅卖 仅允许卖出操作不执行买入操作。之前开的多头头寸以普通模式进行管理直到平仓。 SellInit
BuySupport
SellSupport
BuyInit
现在新开仓 不允许新的买入和卖出操作。之前开的空头头寸以普通模式进行管理,直到通过出场信号平仓。 BuySupport
SellSupport
BuyInit
SellInit
暂停 先前的开仓没有被管理起来。暂停初始化新的买入和卖出交易。这种模式通常用于当市场关闭时以及交易操作无法执行时。 BuyInit
SellInit
BuySupport
SellSupport
停止 所有先前的开仓都已平仓。买入和卖出操作未被初始化。 所有持仓头寸通过一个特定的方法平仓 BuyInit
SellInit
BuySupport
SellSupport

 

表 2. EA交易模式

所有交易模式使用一个特殊的结构体ENUM_TRADE_STATE,通过MQL语言实现给出。这里是对它的描述:

//+------------------------------------------------------------------+
//| 确定EA的交易状态。                                                 |
//+------------------------------------------------------------------+
enum ENUM_TRADE_STATE
{
   TRADE_BUY_AND_SELL,              // 允许买入和卖出操作
   TRADE_BUY_ONLY,                  // 仅允许买入操作不允许卖出。
   TRADE_SELL_ONLY,                 // 仅允许卖出操作不允许买入
   TRADE_STOP,                      // 不允许执行交易立即平仓所有头寸不接受新的交易信号。
   TRADE_WAIT,                      // 对开仓头寸的控制丢失,忽略新的信号。在新闻发布时有用。
   TRADE_NO_NEW_ENTRY               // 忽略入场信号。然而,根据交易逻辑保留持仓。
};

这些模式允许任何EA在建议的方法下,灵活地连接和断开的交易模块,从而可以非常方便的从一个交易模式却换到另一个交易模式。

 

CTradeState交易模式切换

使用交易模式开发的EA将总是能够知道某一时点执行何种交易操作。然而,每个EA应独立确定这一时点。当交易MICEX的FORTS板块,尤其需要交易模式。FORTS交易有几个特点,其中主要特点之一是每天清算两次,从14:00到14:03(中间清算)以及从18:45到19:00(主要清算)。在清算阶段建议不要允许EA进行交易操作。

当然,如果EA仅在新的报价或者新的柱形形成时执行交易,它将不会再市场关闭时运行,以为没有新的报价到来。但是很多EA在特定的时间间隔上运行(使用计时器)。对于这类EA,控制交易操作就显得十分必要。另外,有时交易可以再周末或者节假日进行,并且甚至有些外汇经纪商允许在周末进行交易。然而,由于这些日子的流动性很低,以及它们的统计意义也很弱,这些日子应该被忽略。

不管怎样,对于任何职业算法交易者而言,控制交易模式都是必要的过程。可以通过CTradeState模块来实现之。这个模块用MQL类来实现,它的任务是根据当前时段返回与之对应的交易模式。例如,如果当前时间是清算时间段,这个模块将返回TRADE_WAIT状态。如果是要平仓所有头寸的时间,那么模块返回TRADE_STOP。让我们详细描述它的操作和配置方法。下面是类的头部:

//+------------------------------------------------------------------+
//|                                                  TimeControl.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "Strategy.mqh"
#define ALL_DAYS_OF_WEEK 7
//+------------------------------------------------------------------+
//| 交易状态模块 TradeState                                            |
//+------------------------------------------------------------------+
class CTradeState
{
private:
   ENUM_TRADE_STATE  m_state[60*24*7];  // 交易状态掩码
public:
                     CTradeState(void);
                     CTradeState(ENUM_TRADE_STATE default_state);
   ENUM_TRADE_STATE  GetTradeState(void);
   ENUM_TRADE_STATE  GetTradeState(datetime time_current);
   void              SetTradeState(datetime time_begin, datetime time_end, int day_of_week, ENUM_TRADE_STATE state);
};

这个类的主要功能是返回策略的当前状态,通过调用GetTradeState方法来获得。在模块能够返回状态之前,此状态需通过SetTradeState方法进行添加。

该模块的操作算法类似于MetaTrader 5测试器中的“时间表”标签:

图 3. MetaTrader 5 测试器中的时间表标签

此窗口使你能够设置一周中哪些天可以用来执行来自MQL5云网络中的任务。CTradeState类的工作方式类似,但是允许你为每个范围设置五个ENUM_TRADE_STATE值中的一个。

为了进一步理解如何使用CTradeState,让我们配置交易状态模块。针对FORTS市场的日常操作,本文的笔者使用下面的配置表:

时间 模式 描述
10:00-10:01 TRADE_WAIT 市场开市时间。开盘时间的特征是高流动性和价格跳跃。在这些时刻交易往往蕴含了巨大的风险,因此在开盘后的几分钟内最好不要交易,为此EA需要设置为等待模式。
14:00 - 14:03 TRADE_WAIT 中间清算时间在这个时间段,市场关闭,因此EA也需要被设置为 TRADE_WAIT 模式。
18:45 - 18:49 TRADE_WAIT 主清算时间。在这个时间段市场也关闭且不允许交易。TRADE_WAIT模式被激活。
23:50 - 9:59 TRADE_WAIT 市场关闭,交易被禁止。EA被设置为TRADE_WAIT。
周五,从15:00开始 TRADE_NO_NEW_ENTRY 周五 — 一周的最后一个交易日。为了不将头寸跨周放置,它们需要在最后一个交易日被平仓。因此,没有必要在最后一个交易日新开头寸,而在几小时后又平仓。正因为此,NO_NEW_ENTRY 模式被使用。每周五,从15:00开始,忽略新的入场信号。未平仓头寸只能被平仓。
周五,23:40-23:50 TRADE_STOP 市场关闭前的时间段。这是所有头寸必须被平仓的时间。23:40 EA切换到 TRADE_STOP模式,平仓并切换到等待模式。
周六,周日 TRADE_WAIT 周末不进行交易。由于假期的转移,某些周六可能是工作日。市场在这些天仍旧运行。但这种情况非常少见,并且在这类“工作”日交易必须被避免因为低的流动性和统计上的不确定性。必须避免在这些日子进行交易,无论是否是工作日。

 

表 3. 基于时间的交易模式

从表中可见,进行必要的配置并不是一件困难的事情,相反CTradeState类使你能够创建此类的模式组合。下面是一个简单的监本,设置表格中的模式,然后请求符合特定时间的模式:

//+------------------------------------------------------------------+
//|                                               TestTradeState.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Strategy\TradeState.mqh>

CTradeState TradeState(TRADE_BUY_AND_SELL);  // 设置默认模式为Buy和Sell 
//+------------------------------------------------------------------+
//| 脚本程序start函数                                                  |
//+------------------------------------------------------------------+
void OnStart()
{
   TradeState.SetTradeState(D'15:00', D'23:39', FRIDAY, TRADE_NO_NEW_ENTRY);
   TradeState.SetTradeState(D'10:00', D'10:01', ALL_DAYS_OF_WEEK, TRADE_WAIT);
   TradeState.SetTradeState(D'14:00', D'14:03', ALL_DAYS_OF_WEEK, TRADE_WAIT);
   TradeState.SetTradeState(D'18:45', D'18:59', ALL_DAYS_OF_WEEK, TRADE_WAIT);
   TradeState.SetTradeState(D'23:50', D'23:59', ALL_DAYS_OF_WEEK, TRADE_WAIT);
   TradeState.SetTradeState(D'0:00',  D'9:59',  ALL_DAYS_OF_WEEK, TRADE_WAIT);
   TradeState.SetTradeState(D'23:40', D'23:49', FRIDAY, TRADE_STOP);
   TradeState.SetTradeState(D'00:00', D'23:59', SATURDAY, TRADE_WAIT);
   TradeState.SetTradeState(D'00:00', D'23:59', SUNDAY, TRADE_WAIT);
   
   printf("10:00 - " + EnumToString(TradeState.GetTradeState(D'10:00')));
   printf("14:01 - " + EnumToString(TradeState.GetTradeState(D'14:01')));
   printf("18:50 - " + EnumToString(TradeState.GetTradeState(D'18:50')));
   printf("23:50 - " + EnumToString(TradeState.GetTradeState(D'23:51')));
   printf("Friday, > 15:00 - " + EnumToString(TradeState.GetTradeState(D'2015.11.27 15:00')));
   printf("Saturday - " + EnumToString(TradeState.GetTradeState(D'2015.11.28')));
   printf("Sunday - " + EnumToString(TradeState.GetTradeState(D'2015.11.29')));
   printf("Default State - " + EnumToString(TradeState.GetTradeState(D'11:40')));
}
//+------------------------------------------------------------------+

脚本的输出将是这样的:

默认状态- TRADE_BUY_AND_SELL
周日 - TRADE_WAIT
周六 - TRADE_WAIT
周五, > 15:00 - TRADE_NO_NEW_ENTRY
23:50 - TRADE_STOP
18:50 - TRADE_WAIT
14:01 - TRADE_WAIT
10:00 - TRADE_WAIT

请注意交易模式设置的格式。它们不使用日期,仅使用小时和分钟(D'15:00' or D'18:40')。它将完整的日期传入方法中,例如:

TradeState.SetTradeState(D'2015.11.27 15:00', D'2015.11.27 23:39', FRIDAY, TRADE_NO_NEW_ENTRY);

但日期部分仍将被忽略。

其次要注意的是SetTradeState的调用顺序。顺序很重要!CTradeState 模块把交易状态的掩码作为 ENUM_TRADE_STATE 数组存储,其中元素的编号等于一周中的分钟数(10080个元素)。使用传入的日期,SetTradeState方法计算此数组元素的范围并用合适的状态进行填充。这意味着先前的状态被新的状态替代。因此,最近更新的是最后的状态。这个函数的代码如下:

//+------------------------------------------------------------------+
//| 设置交易状态 TradeState                                            |
//| 输入:                                                            |
//| time_begin  - 时间,从此时开始交易状态有效                           |
//|                                                                  |
//| time_end    - 时间,交易状态失效时间                                |
//| day_of_week - 星期几,交易状态的设置应用于哪一天                      |
//|               根据修饰符确定                                       |
//|               ENUM_DAY_OF_WEEK 或者 ALL_DAYS_OF_WEEK              |
//| state       - 交易状态。                                           |
//| 注意:time_begin 和 time_end 对应的日期被忽略。                      |
//+------------------------------------------------------------------+
void CTradeState::SetTradeState(datetime time_begin,datetime time_end, int day_of_week, ENUM_TRADE_STATE state)
{
   if(time_begin > time_end)
   {
      string sb = TimeToString(time_begin, TIME_MINUTES);
      string se = TimeToString(time_end, TIME_MINUTES);
      printf("Time " + sb + " must be more time " + se);
      return;
   }
   MqlDateTime btime, etime;
   TimeToStruct(time_begin, btime);
   TimeToStruct(time_end,  etime);
   for(int day = 0; day < ALL_DAYS_OF_WEEK; day++)
   {
      if(day != day_of_week && day_of_week != ALL_DAYS_OF_WEEK)
         continue;
      int i_day = day*60*24;
      int i_begin = i_day + (btime.hour*60) + btime.min;
      int i_end = i_day + (etime.hour*60) + etime.min;
      for(int i = i_begin; i <= i_end; i++)
         m_state[i] = state;
   }
}

GetTradeState 简单些。它根据请求时间计算数组元素的索引,然后返回元素的值。

//+------------------------------------------------------------------+
//| 根据传入的时间,返回先前设置的交易状态                                |
//|                                                                  |
//+------------------------------------------------------------------+
ENUM_TRADE_STATE CTradeState::GetTradeState(datetime time_current)
{
   MqlDateTime dt;
   TimeToStruct(time_current, dt);
   int i_day = dt.day_of_week*60*24;
   int index = i_day + (dt.hour*60) + dt.min;
   return m_state[index];
}

CTradeState 类的完整代码在 TradeState.mqh 文件中,并且被包含在所述交易引擎的源代码里面。下一篇文章将介绍这个类是如何在交易引擎中运作的。

 

总结

我们已经描述了四个主要的交易规则,你可以方便快速的确定几乎所有EA的逻辑。每一个规则都是一个独立的函数或者类的方法。方法调用的各种组合确定了策略的特定模式。因此,一个灵活的EA管理系统使用最少的资源来实现。

在这一系列文章的下一篇,我们将讨论一种集中事件处理模型 — 它使得交易逻辑的基本方法能够辨别发生了什么事件。我们还将讨论辅助交易算法,它们极大的简化了交易信息的获取过程。

你可以下载并安装“通用智能交易系统”的完整代码到你的计算机上。源码在文本的附件中。本系列的下一篇文章将给出更多类的描述。