跨平台智能交易系统: 时间过滤器

Enrico Lambino | 28 八月, 2017


内容目录

  1. 概述
  2. 目标
  3. 基类
  4. 时间过滤器类和类型
  5. 时间过滤器容器
  6. 子过滤器 (CTimeFilter)
  7. 示例
  8. 结束语

概述

时间过滤仅在定义了特定时间段时方可使用, 智能交易系统必须检查给定时间是否在所述时间段内。当条件满足或不满足时, 可以启用或禁用某些功能。若智能交易系统的给定功能设置为非全时运作 (周期性地, 或在少数例外发生后全时运作) 这一功能是非常有用的。以下是可以应用时间过滤的一些示例:

  1. 规避某些时间段 (如, 横盘走势或高波动的时间段)
  2. 为市场订单或持仓设置 "到期" 时间 (在 "到期" 时离场)
  3. 交易周结束时平仓。

这些是交易者最常使用的功能, 当然还有其它变体。

目标

  • 理解并应用最常用的时间过滤方法
  • 允许智能交易系统轻松使用多个时间过滤器
  • 与 MQL4 和 MQL5 兼容

基类

名为 CTime 的类将作为我们的智能交易系统中其它所要讨论的时间过滤器对象的基类。类 CTimeBase (其基于 CTime) 的定义显示在以下代码片段中:

class CTimeBase : public CObject
  {
protected:
   bool              m_active;
   bool              m_reverse;
   CSymbolManager   *m_symbol_man;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CTimeBase(void);
                    ~CTimeBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_TIME;}
   //--- 初始化
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual CObject *GetContainer(void);
   virtual void      SetContainer(CObject*);
   virtual bool      Validate(void);
   //--- 赋值与取值
   bool              Active(void) const;
   void              Active(const bool);
   bool              Reverse(void);
   void              Reverse(const bool);
   //--- 检查
   virtual bool      Evaluate(datetime)=0;
  };

基类有 3 个基元数据类型的成员。m_active 用于启用或禁用类对象。m_reverse 用于反转类对象的输出 (如果原本输出为 false 则返回 true, 如果原本输出为 true, 则返回 false)。m_time_start 用于引用创建的类实例, 无论是在 OnInit 中创建还是在执行智能交易系统之后创建。

时间过滤器类和类型

按特定日期范围进行时间过滤

这是最简单的时间过滤方法。要使用此方法检查时间, 只需要开始和结束日期, 如果时间设置处于这些日期之间, 则输出为 true。否则, 输出为 false。

此方法已实现为 CTimeRange。以下代码显示了 CTimeRangeBase 的定义, 而 CTimeRange 正是有基于此:

class CTimeRangeBase : public CTime
  {
protected:
   datetime          m_begin;
   datetime          m_end;
public:
                     CTimeRangeBase(void);
                     CTimeRangeBase(datetime,datetime);
                    ~CTimeRangeBase(void);
   //--- 初始化                    datetime,datetime
   virtual bool      Set(datetime,datetime);
   virtual bool      Validate(void);
   //--- 赋值与取值
   datetime          Begin(void) const;
   void              Begin(const datetime);
   datetime          End(void) const;
   void              End(const datetime);
   //--- 处理
   virtual bool      Evaluate(datetime);
  };

在类构造函数中, 应指定开始和结束时间。在调用类方法 Evaluate 时设置的实际时间将会与这两个值进行比较。如果时间未设置或为零, 则该方法会使用调用时的当前时间:

bool CTimeRangeBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   if(current==0)
      current=TimeCurrent();
   bool result=current>=m_begin && current<m_end;
   return Reverse()?!result:result;
  }

按周内所在日进行时间过滤

按周内所在日进行过滤是最简单且最常用的时间过滤方法之一。通常使用此时间过滤器针对智能交易系统的某些功能在周内的某些日子里进行限制或允许。

现在, 这个特定的类可按多种方式实现。一种方法是提供自定义的函数 TimeDayOfWeek, 该函数在 MQL4 中可用, 但不能用在 MQL5 中。另一种方法将所检查的时间转换为结构 MqlDateTime, 然后检查其 day_of_week 参数是否匹配预设值的标志。推荐选择后一种方法, 因为它可令我们将所有可用的类方法放在基类中。

此方法在我们的智能交易系统中体现为 CTimeDays。以下代码显示 CTimeDaysBase 的定义, 而 CTimeDays 正是有基于此:

class CTimeDaysBase : public CTime
  {
protected:
   long              m_day_flags;
public:
                     CTimeDaysBase(void);
                     CTimeDaysBase(const bool sun=false,const bool mon=true,const bool tue=true,const bool wed=true,
                                   const bool thu=true,const bool fri=true,const bool sat=false);
                    ~CTimeDaysBase(void);
   //--- 初始化                    
   virtual bool      Validate(void);
   virtual bool      Evaluate(datetime);
   virtual void      Set(const bool,const bool,const bool,const bool,const bool,const bool,const bool);
   //--- 赋值与取值
   bool              Sunday(void) const;
   void              Sunday(const bool);
   bool              Monday(void) const;
   void              Monday(const bool);
   bool              Tuesday(void) const;
   void              Tuesday(const bool);
   bool              Wednesday(void) const;
   void              Wednesday(const bool);
   bool              Thursday(void) const;
   void              Thursday(const bool);
   bool              Friday(void) const;
   void              Friday(const bool);
   bool              Saturday(void) const;
   void              Saturday(const bool);
  };

如定义所示, 该类只有一个类型为 long 的类成员。这一类成员是设置所在日标志时使用的, 其值为评估时应返回 true 的所在日 (周内的 7 天)。这意味着我们将使用按位操作, 所以我们还必须声明一个自定义枚举, 其成员将代表 7 天中的每一天:

enum ENUM_TIME_DAY_FLAGS
  {
   TIME_DAY_FLAG_SUN=1<<0,
   TIME_DAY_FLAG_MON=1<<1,
   TIME_DAY_FLAG_TUE=1<<2,
   TIME_DAY_FLAG_WED=1<<3,
   TIME_DAY_FLAG_THU=1<<4,
   TIME_DAY_FLAG_FRI=1<<5,
   TIME_DAY_FLAG_SAT=1<<6
  };

使用 Set 方法设置 (或清除) 周内所在日标志。为防止尚未设置标志的情况下意外调用类实例进行评估, 出于便利起见, 采用的措施是该方法在其类构造函数之一中被调用。

void CTimeDaysBase::Set(const bool sun=false,const bool mon=true,const bool tue=true,const bool wed=true,
                        const bool thu=true,const bool fri=true,const bool sat=false)
  {
   Sunday(sun);
   Monday(mon);
   Tuesday(tue);
   Wednesday(wed);
   Thursday(thu);
   Friday(fri);
   Saturday(sat);
  }

标志也可以单独设置。当只需要修改一个标志, 而无需调用 Set 函数设置所有 7 天的标志 (在某些状况下很容易出错) 时, 这是非常有用的。以下代码片段显示了一个名为 Monday 的方法, 用于从所在日当中于设置/取消第二日的标志。其它日子的赋值和取值也以相同的方式进行编码。

void CTimeDaysBase::Monday(const bool set)
  {
   if(set)
      m_day_flags|=TIME_DAY_FLAG_MON;
   else
      m_day_flags &=~TIME_DAY_FLAG_MON;
  }

利用设置标志的方法, 我们的下一个方法处理过滤器的实际评估, 即判断特定时间是否落在周内的某一天:

bool CTimeDaysBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=false;
   MqlDateTime time;
   if(current==0)
      current=TimeCurrent();
   TimeToStruct(current,time);
   switch(time.day_of_week)
     {
      case 0: result=Sunday();      break;
      case 1: result=Monday();      break;
      case 2: result=Tuesday();     break;
      case 3: result=Wednesday();   break;
      case 4: result=Thursday();    break;
      case 5: result=Friday();      break;
      case 6: result=Saturday();    break;
     }
   return Reverse()?!result:result;
  }

如早前简述, 该方法首先获取 datetime 类型的参数。如果方法调用中没有参数, 该方法将使用当前时间。之后, 它将此时间转换为 MqlDateTime 格式, 并获取其 day_of_week 成员, 然后根据类的唯一成员 (m_day_flags) 的当前值进行评估。

这种方法通常用于满足交易者的要求, 诸如 "星期五不进行交易", 甚或他们的 "经纪商" 在 "星期日" 也营业时交易者不期望进行交易。

使用计时器

时间过滤的另一种方法是使用计时器。在计时器中, 当前时间将会与过去的某个时间点进行比较。如果评估后时间仍在期满之内, 它应该返回 true, 否则返回 false。该方法由 CTimer 类体现。以下片段显示 CTimerBase 定义的代码, CTimer 正是有基于此:

class CTimerBase : public CTime
  {
protected:
   uint              m_years;
   uint              m_months;
   uint              m_days;
   uint              m_hours;
   uint              m_minutes;
   uint              m_seconds;
   int               m_total;
   int               m_elapsed;
   datetime          m_time_start;
public:
                     CTimerBase(const int);
                     CTimerBase(const uint,const uint,const uint,const uint,const uint,const uint);                     
                    ~CTimerBase(void);
   //--- 初始化
   virtual bool      Set(const uint,const uint,const uint,const uint,const uint,const uint);
   virtual bool      Validate(void);
   //--- 赋值与取值
   uint              Year(void) const;
   void              Year(const uint);
   uint              Month(void) const;
   void              Month(const uint);
   uint              Days(void) const;
   void              Days(const uint);
   uint              Hours(void) const;
   void              Hours(const uint);
   uint              Minutes(void) const;
   void              Minutes(const uint);
   uint              Seconds(void) const;
   void              Seconds(const uint);
   bool              Total(void) const;
   datetime          TimeStart(void) const;
   void              TimeStart(const datetime);
   //--- 处理   
   virtual bool      Elapsed(void) const;
   virtual bool      Evaluate(datetime);
   virtual void      RecalculateTotal(void);
  };

构造函数的参数用于构建从开始时间起的总时间或到期时间, 存储在类成员 m_total 中。出于便利起见, 我们将根据特定时间段的秒数来声明常数, 从一年到一分钟:

#define YEAR_SECONDS 31536000
#define MONTH_SECONDS 2419200
#define DAY_SECONDS 86400
#define HOUR_SECONDS 3600
#define MINUTE_SECONDS 60

类的构造函数需要计时器的到期时间, 从年数至秒数表达:

CTimerBase::CTimerBase(const uint years,const uint months,const uint days,const uint hours,const uint minutes,const uint seconds) : m_years(0),
                                                                                                                                    m_months(0),
                                                                                                                                    m_days(0),
                                                                                                                                    m_hours(0),
                                                                                                                                    m_minutes(0),
                                                                                                                                    m_seconds(0),
                                                                                                                                    m_total(0),
                                                                                                                                    m_time_start(0)
  {
   Set(years,months,days,hours,minutes,seconds);
  }

或者, 我们可以使用 m_total 的首选值作为唯一的参数来构造类的实例:

CTimerBase::CTimerBase(const int total_time) : m_years(0),
                                               m_months(0),
                                               m_days(0),
                                               m_hours(0),
                                               m_minutes(0),
                                               m_seconds(0),
                                               m_total(0),
                                               m_time_start(0)
  {
   m_total=total_time;
  }

在类实例化之后调用该类的 Evaluate 方法将用 m_total 与 UNIX 的开始时间 (此时间是 datetime 数据类型的基准点) 进行比较。因此, 在调用 Evaluate 方法之前, 必须设置所需的开始时间 (除非首选开始时间为 1970 年 1 月 1 日 (UTC/GMT 午夜)。下面显示使用重载的 TimeStart 方法, 为 m_time_start 类成员进行赋值和取值的方法:

datetime CTimerBase::TimeStart(void) const
  {
   return m_time_start;
  }

void CTimerBase::TimeStart(const datetime time_start)
  {
   m_time_start=time_start;
  }

该类的 Evaluate 方法非常简单: 它获取传递到方法的参数 (通常为当前时间) 距开始时间之间的差值。这就是所消耗的时间, 如果消耗的时间超过允许的总时间 (m_total), 则该方法返回 false。

bool CTimerBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=true;
   if(current==0)
      current= TimeCurrent();
   m_elapsed=(int)(current-m_time_start);
   if(m_elapsed>=m_total) result=false;
   return Reverse()?!result:result;
  }

这种时间过滤方法的使用方式很多, 例如为智能交易系统的某些功能设置最大期限 (到期), 并为市场订单或持仓 (类似于二元期权交易) 设置 "到期"。此时间过滤器与使用计时器事件 (与 MQL4 和 MQL5 兼容) 大致相同, 但由于只能使用此事件函数设置一个定时器事件, 所以只有在智能交易系统真正需要额外的定时器时才会使用 CTimer。

使用日内时间表进行过滤

使用日内时间表进行过滤是交易者最常用的方法之一。过滤器使用 24 小时时间表, 智能交易系统选择 (通过其参数) 允许执行操作的某些时间 (通常, 根据入场信号进行交易)。这种过滤方法由 CTimeFilter 类体现。以下代码显示了 CTimeFilterBase 的定义, 而 CTimeFilter 正是有基于此。

class CTimeFilterBase : public CTime
  {
protected:
   MqlDateTime       m_filter_start;
   MqlDateTime       m_filter_end;
   CArrayObj         m_time_filters;
public:
                     CTimeFilterBase(void);
                     CTimeFilterBase(const int,const int,const int,const int,const int,const int,const int);
                    ~CTimeFilterBase(void);
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual bool      Validate(void);
   virtual bool      Evaluate(datetime);
   virtual bool      Set(const int,const int,const int,const int,const int,const int,const int);
   virtual bool      AddFilter(CTimeFilterBase*);
  };

该类有两个类型为 MqlDateTime 的成员, 以及一个为 CArrayObj 的成员。这两个结构用于包含 24 小时内的范围, 而对象成员用于存储其子过滤器。

通过类对象构造函数, 我们得到格式为小时、分和秒的开始和结束时间。这些值最后通过类的 Set 方法最终存储在类成员 m_filter_start 和 m_filter_end 当中。gmt 参数用于计算经纪商的 GMT 偏移量。

bool CTimeFilterBase::Set(const int gmt,const int starthour,const int endhour,const int startminute=0,const int endminute=0,
                          const int startseconds=0,const int endseconds=0)
  {
   m_filter_start.hour=starthour+gmt;
   m_filter_start.min=startminute;
   m_filter_start.sec=startseconds;
   m_filter_end.hour=endhour+gmt;
   m_filter_end.min=endminute;
   m_filter_end.sec=endseconds;
   return true;
  }

然后我们继续处置该类的 Evaluate 方法。在初始化阶段, 两个 MqlDateTime 参数中的数据仅以 24 小时以内的小时、分和秒表示。它不包含其它数据, 譬如年份和月份。为了用开始和结束时间与所示时间 (或者如果方法参数是省缺值, 则为当前时间) 比较。至少有两种方法可以做到这一点:

  1. 以小时、分和秒表达所示时间, 然后将其与结构参数进行比较。
  2. 用当前时间更新缺省的结构参数, 将结构转换为 UNIX 时间 ( datetime 类型), 并将其与所示时间进行比较。

选择第二种方法在 Evaluate 方法中实现, 如下所示:

bool CTimeFilterBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=true;
   MqlDateTime time;
   if(current==0)
      current=TimeCurrent();
   TimeToStruct(current,time);
   m_filter_start.year= time.year;
   m_filter_start.mon = time.mon;
   m_filter_start.day = time.day;
   m_filter_start.day_of_week = time.day_of_week;
   m_filter_start.day_of_year = time.day_of_year;
   m_filter_end.year= time.year;
   m_filter_end.mon = time.mon;
   m_filter_end.day = time.day;
   m_filter_end.day_of_week = time.day_of_week;
   m_filter_end.day_of_year = time.day_of_year;
   /*
     其它任务在这里处理
   */
  }

比较不包括结束时间。这意味着如果我们使用这个类, 要求智能交易系统仅在 08:00 至 17:00 之间进行交易, 那么智能交易系统可在 08:00 蜡烛开始时交易, 但是只允许交易到 17:00, 这意味着它最后的交易时间最多到 16:59。

由于结构不包含大于小时的数据, 因此需要从当前时间 (或所示时间) 恢复缺失的数据。不过, 当开始和结束时间在 24 小时内, 但不属于同一天时, 需要进行一些调整。在上面的示例中, 08:00 是上午 8:00, 17:00 是下午 5:00。在这种情况下, 两个时间可以在同一天内找到。然而, 假设我们将两者互换, 开始时间为下午 5:00, 结束时间为上午 8:00。如果开始时间大于结束时间, 则表示时间范围延伸到第二天。如此, 结束时间不在开始时间的同一天内。情况就是两者之一:
  1. 开始时间是从当天 (或从指定时间开始的那天), 结束时间是第二天。
  2. 开始时间是从昨天 (所示时间的前一天) 开始, 结束时间是当天 (或所示时间的那天)。

调整是否必要将取决于当前时间或所示时间。假设我们有的所示时间 (或当前时间) 为 5:01 PM (17:01)。在这种情况下, 开始时间与所示时间在同一天内。此处, 我们判定结束时间属于第二天。另一方面, 如果所示时间为 01:00 或 1:00 AM, 则所示时间与结束时间相同, 而开始时间属于昨天。所以, 以前计算的 MqlDateTime 结构应如下进行调整:

  1. 如果开始时间在所示时间同日之下, 则结束时间加 1 天。
  2. 如果结束时间在所示时间同日之下, 则开始时间减去 1 天。

这些仅适用于当开始时间和结束时间不属于同一天时, 即开始时间大于过滤器的结束时间时。在 Evaluate 方法中的调整实现如下:

if(m_filter_start.hour>=m_filter_end.hour)
  {
   if(time.hour>=m_filter_start.hour)
     {
      m_filter_end.day++;
      m_filter_end.day_of_week++;
      m_filter_end.day_of_year++;
     }
   else if(time.hour<=m_filter_end.hour)
     {
      m_filter_start.day--;
      m_filter_start.day_of_week--;
      m_filter_start.day_of_year--;
     }
  }

返回变量最初设置为 true, 因此时间过滤器的实际检查将取决于所示时间是否处于开始时间和结束时间之间。Evaluate 方法中的计算确保开始时间始终小于或等于结束时间。如果开始时间等于结束时间 (以小时、分和秒为单位), 则该方法仍将返回 true。例如, 如果开始时间为 05:00, 结束时间为 05:00, 则过滤器会将其视为不落在同一天内的两个时间, 在这种情况下, 过滤器涵盖整个 24 小时周期。

时间过滤器容器

与本系列中讨论的其它类对象相似, 时间过滤器也将有一个容器, 对象指针存储于内。这可以通过调用此容器的 Evaluate 方法进行评估。如果所有时间过滤器的 Evaluate 方法返回 true (就时间过滤而言, 没有异议), 那么这个容器也应该返回 true。这是由 CTimes 类实现的。以下代码显示了 CTimesBase 的定义, 而 CTimes 正是有基于此:

class CTimesBase : public CArrayObj
  {
protected:
   bool              m_active;
   int               m_selected;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CTimesBase(void);
                    ~CTimesBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_TIMES;}
   //-- 初始化
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual CObject *GetContainer(void);
   virtual void      SetContainer(CObject*);
   virtual bool      Validate(void) const;
   //--- 激活和蛰伏
   bool              Active(void) const;
   void              Active(const bool);
   int               Selected(void);
   //--- 检查
   virtual bool      Evaluate(datetime) const;
   //--- 复原
   virtual bool      CreateElement(const int);
  };

子过滤器 (CTimeFilter)

时间过滤器容器的 Evaluate 方法为了返回 true, 要求所有它的主成员也应该返回 true。虽然大多数的时间过滤对象在同一智能交易系统上不需要多个实例, 但 CTimeFilter 是一个例外, 而它是所有时间过滤器当中最常用的。请考察以下代码:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,17);
times.Add(GetPointer(time1));

我们假设时间过滤器用于交易 (入场)。在这种情况下, 时间过滤器容器在其动态指针数组中只包含一个指针。在此设置下, 当调用 Evaluate 方法时, 最终结果将取决于所示时间是否处于 08:00 至 17:00 之间。

现在, 考虑这样的情况, 交易时间从上午 8:00 到下午 5:00, 配置智能交易系统跳过午餐时间。也就是说, 它的交易时间为上午 8:00 至中午 12:00, 下午 1:00 至 5:00。现在, 时间轴不再是连续的, 而是切分为两段。程序员可以修改代码, 使用 CTimeFilter 的两个实例而非仅仅一个:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,12);
CTimeFilter time2 = new CTimeFilter(gmt,13,17);
times.Add(GetPointer(time1));
times.Add(GetPointer(time2));

上述代码将无法正常运行。它将始终返回 false, 因为时间过滤器容器要求其所有基元时间过滤器实例返回 true。在上述设置中, 当一个返回 true 时, 另一个返回 false, 反之亦然。如果涉及的时间过滤器超过 2 个, 情况会进一步复杂化。使用这种设置, 唯一可以正常工作的方法是当一个过滤器处于活动状态时, 其余的过滤器蛰伏。

解决方案是始终确保时间过滤器容器最多只能存储一个指向 CTimeFilter 的指针。如果需要多个 CTimeFilter 实例, 则应将其添加为另一个 CTimeFilter 实例的子过滤器。子过滤器的指针存储在 CTimeFilter 的类成员之一 m_time_filters 当中, 并且通过其 AddFilter 方法添加指针。用于评估子过滤器的代码可以在类的 Evaluate 方法中找到, 如下所示:

if(!result)
  {
   for(int i=0;i<m_time_filters.Total();i++)
     {
      CTimeFilter *filter=m_time_filters.At(i);
      if(filter.Evaluate(current))
      {
         return true;
      }   
     }
  }

只有当主过滤器返回 false 时才执行此代码, 即最后的方法来验证初始评估是否有异常。如果至少有一个子过滤器返回 true, 则主过滤器将始终返回 true。如此, 我们修改以前的代码示例如下:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,12);
CTimeFilter time2 = new CTimeFilter(gmt,13,17);
CTimeFilter time0 = new CTimeFilter(gmt,0,0);
time0.Reverse();
time0.AddFilter(GetPointer(time1));
time0.AddFilter(GetPointer(time2));
times.Add(GetPointer(time0));
这些给出的修改, 时间过滤器对象只包含其数组中的一个指针, 它是 time0。time0, 另一方面, 有两个子过滤器, time1 和 time2, 它们原本是在时间过滤容器之下。time0 具有与起始和结束时间相同的参数, 因此总是返回 true。我们称之为 Reverse 方法, 以便 time0 始终返回 false, 强制它检查初始评估是否有异常 (通过其子过滤器)。当以图形表示时, 我们可以看到时间表如下:


主时间滤波器和子滤波器的图形表示

在时间过滤器容器中可以找到指向 time0 的指针。如上图所示, 首先要进行评估。由于 time0 总是返回 false, 它会检查其子过滤器。首先, 它会检查时间是否在 8:00 到 12:00 之间。如果不是, 则会检查所示时间是否在 13:00 至 17:00 之间。如果其中任何一个返回 true, 那么 time0 也将返回 true (否则为 false)。所以, 如果所示时间在 8:00 至 12:00 之间, 或 13:00 和 17:00 之间, 最终时间表将返回 true。有两个以上的子过滤器是可能的, 它仍然遵循与上述相同的规则。然而, 很可能不需要子过滤器的子过滤器, 因为日内时间过滤器组合只能在两个级别内表示。

示例

举例来说, 我们将改编来自 以前的文章 的智能交易系统示例。在这款智能交易系统中, 我们将包含文章中讨论的所有时间过滤器。我们首先包含 CTimesBase 的头文件, 因为这足以让我们的智能交易系统包含所有的时间过滤器类。

#include "MQLx\Base\OrderManager\OrderManagerBase.mqh"
#include "MQLx\Base\Signal\SignalsBase.mqh"
#include "MQLx\Base\Time\TimesBase.mqh" //加入包含行
#include <Indicators\Custom.mqh>

然后我们声明一个指向时间过滤器容器的全局指针:

CTimes *time_filters;

OnInit 下面, 我们创建一个 CTimes 的实例, 然后将其存储在这个指针上:

time_filters = new CTimes();

对于这款智能交易系统, 我们仅对新交易的入场进行时间过滤, 而不是离场。为了实现此目的, 在智能交易系统入场交易之前, 要进行额外的检查, 看看系统此时是否确实可以入场交易:

if(signals.CheckOpenLong())
  {
   close_last();
   if (time_filters.Evaluate(TimeCurrent()))
   {
      //Print("入场买入..");            
      money_manager.Selected(0);
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
   }
  }
else if(signals.CheckOpenShort())
  {
   close_last();
   if (time_filters.Evaluate(TimeCurrent()))
   {
      //Print("入场卖出..");
      money_manager.Selected(1);
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
   }
  }

如上面的代码所示, 在交易时间检查和新交易实际入场之前先要调用最后一笔持仓离场, 因此时间过滤器只适用于入场而不适用于现有交易的离场。

对于使用日期范围的时间过滤, 我们将为开始和结束日期提供输入参数。这些参数是 datetime 类型。我们还提供一个参数, 允许用户打开或关闭此功能:

input bool time_range_enabled = true;
input datetime time_range_start = 0;
input datetime time_range_end = 0;

两个参数的省缺值为零表示这些参数引用的是 UNIX 的开始时间。为了防止在使用省缺值时发生意外错误, 我们将提供额外的测量, 以便仅在结束时间大于零, 且结束时间大于开始时间的条件下才可创建时间过滤器。这将在 EA 的 OnInit 函数里进行编码:

if (time_range_enabled && time_range_end>0 && time_range_end>time_range_start)
 {
     CTimeRange *timerange = new CTimeRange(time_range_start,time_range_end);
     time_filters.Add(GetPointer(timerange));
 }    

在上面的代码中还找到了在时间过滤器容器上实际添加对象的指针。

对于交易日时间过滤器, 我们提供 7 个不同的参数, 每个参数代表周内的特定所在日。我们还提供一个参数来打开或关闭此功能:

input bool time_days_enabled = true;
input bool sunday_enabled = false;
input bool monday_enabled = true;
input bool tuesday_enabled = true;
input bool wednesday_enabled = true;
input bool thursday_enabled = true;
input bool friday_enabled = false;
input bool saturday_enabled = false;

在 OnInit 函数下, 如果启用该功能, 我们还要创建一个新的时间过滤器实例, 并将其添加到容器中:

if (time_days_enabled)
 {
    CTimeDays *timedays = new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled);
    time_filters.Add(GetPointer(timedays));
 }

对于计时器, 除了将功能开关的参数外, 我们只声明了一个参数, 它是到期前过滤器的总时间:

input bool timer_enabled= true;
input int timer_minutes = 10080;

与早前讨论的早期过滤器类似, 我们创建一个新的 CTimer 实例, 并将其指针添加到容器 (如果启用该功能):

if(timer_enabled)
  {
   CTimer *timer=new CTimer(timer_minutes*60);
   timer.TimeStart(TimeCurrent());
   time_filters.Add(GetPointer(timer));
  }

日内时间过滤有点复杂, 因而我们希望基于以下场景演示 EA 在时间过滤中的能力:

  1. 开始和结束时间在同一天内的时间过滤
  2. 开始和结束时间不在同一天内的时间过滤
  3. CTimeFilter 的多个实例

场景 #1 & #2 可以使用相同的参数集进行演示。如果开始时间小于结束时间 (场景 #1), 我们只需交换两者的值, 即获得场景 #2。然而, 对于 #3, 我们需要两个或更多的实例, 最好使用不同的值, 因此我们至少需要两组 24 小时内的开始和结束时间。要实现这一点, 首先, 我们声明一个自定义枚举, 含有三种可能的设置: 禁用, 方案 #1/#2 和方案 #3:

enum ENUM_INTRADAY_SET 
  {
   INTRADAY_SET_NONE=0,
   INTRADAY_SET_1,
   INTRADAY_SET_2
  };

然后, 我们声明参数如下:

input ENUM_INTRADAY_SET time_intraday_set=INTRADAY_SET_1;
input int time_intraday_gmt=0;
// 第一个集合
input int intraday1_hour_start=8;
input int intraday1_minute_start=0;
input int intraday1_hour_end=17;
input int intraday1_minute_end=0;
// 第二个集合
input int intraday2_hour1_start=8;
input int intraday2_minute1_start=0;
input int intraday2_hour1_end=12;
input int intraday2_minute1_end=0;
// 第三个集合
input int intraday2_hour2_start=13;
input int intraday2_minute2_start=0;
input int intraday2_hour2_end=17;
input int intraday2_minute2_end=0;
若要初始化这个时间过滤器, 我们使用 switch 语句。如果 time_intraday_set 设为 INTRADAY_SET_1, 那么我们使用第一套参数初始化单一的 CTimeFilter 实例。另一方面, 如果设置为 INTRADAY_SET_2, 则使用第二套和第三套参数创建两个不同的 CTimeFilter 实例:
switch(time_intraday_set)
  {
   case INTRADAY_SET_1:
     {
      CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end);
      time_filters.Add(timefilter);
      break;
     }
   case INTRADAY_SET_2:
     {
      CTimeFilter *timefilter=new CTimeFilter(0,0,0);
      timefilter.Reverse(true);
      CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end);
      CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end);
      timefilter.AddFilter(sub1);
      timefilter.AddFilter(sub2);
      time_filters.Add(timefilter);
      break;
     }
   default: break;
  }

在时间过滤器类实例化的所有代码之后, 我们初始化时间过滤器容器, CTimes。首先, 我们为品种管理器分配一个指针 (在这个例子中不需要, 但在需要扩展时间过滤器的情况下可能需要), 然后检查它们的设置:

time_filters.Init(GetPointer(symbol_manager));
if(!time_filters.Validate())
  {
   Print("一个或多个时间过滤器验证失败");
   return INIT_FAILED;
  }

现在, 我们来看看智能交易系统的测试结果。使用策略测试器在 2017 年 1 月的整月范围内进行测试。

可在本文的底部 (tester_time_range.html) 找到启用时间范围过滤后运行智能交易系统的测试结果。在此测试当中, 时间范围从 2017 年开始, 到 2017 年 1 月 6 日的第一个星期五结束。所以, 智能交易系统在结束日期后不再有任何交易入场时, 我们可以知道过滤器已然工作。最后一笔交易的屏幕截图如下所示:

最后交易的时间日期

测试的最后一笔交易是在 1 月 6 日 15:00 入场, 这是智能交易系统设定的限制。请注意, 由于时间过滤器仅适用于入场, 交易保持持仓至下周为止, 这仍然可以接受。垂直虚线代表本周最后一根蜡烛。

为了按照所在日过滤, 要把 days 参数设置为 true。还有, 按日期范围过滤保持启用, 但禁用 Friday 参数。前次测试的最后一笔交易显示, 入场的最后一笔交易是在 1 月 6 日 (星期五)。因此, 如果我们看到这笔交易在测试中不再入场, 我们可以确认这个特定的时间过滤器可以正常工作。测试结果也显示在本文的底部 (tester_time_days.html)。最后一笔交易的屏幕截图如下所示:

最后交易的时间日期


如屏幕截图所示, 最后一笔交易是在 1 月 5 日生成的, 与上一次配置的 1 月 6 日交易相左。在这此新配置下, 倒数第二笔交易现在是测试的最终交易。它的离场点与第二条垂直线重合, 这也是上一次测试的最后一笔交易的入场点, 因为 EA 准备在任意时间里保持一笔持仓(买入或卖出)。

对于计时器过滤器, 如前所示, 我们使用只接受单个参数的 CTimer 的替代构造函数。然后将其存储在类成员 m_total 中, 该成员表示过滤器返回 true 的到期前秒数。由于它以秒表示, 我们必须将输入参数乘以 60, 以便存储的值以秒为单位。10080 是智能交易系统的省缺分钟数, 相当于一周。所以, 如果我们将第一个过滤器与该过滤器组合, 则使用第一个过滤器的测试结果应与该过滤器的结果相同。测试结果与第一次测试结果完全相同, 并在本文末尾提供 (tester_timer.html)。

对于 CTimeFilter 的最终时间过滤器, 它有三种不同的情况, 因此我们还需要对它们进行测试。由于 EA 始终保留持仓, 因此它被迫把以往的交易平仓并开新仓, 如此往复。当一个或多个时间过滤器返回 false 时除外。因此, 如果在日内交易中错失一笔交易, 则 EA 的入场交易是被时间过滤器阻止。没有时间过滤器的完整测试结果在本文末尾提供 (tester_full.html)。

对于本文中提到的日内时间过滤场景 #1, 我们将开始时间设置为 08:00, 结束时间为 17:00。在完整测试中, 第一笔交易是在测试开始时入场的, 它处于第一个小时 (00:00)。这个点是在第一种场景所设定的界限之外。鉴于这些设置, EA 预计不会进行该交易, 而是将下一笔落在时间过滤器之内的交易作为第一笔交易。此设置的测试结果在本文末尾提供 (tester_time_hour1.html), 其第一笔交易的屏幕截图如下所示:


时间 hour 交易 3

符合预期, EA 在测试开始时没有交易。相反, 它一直等到设置的日内范围。垂直虚线表示测试的开始, 可以找到完整测试 (无过滤器) 的第一笔交易。

对于第二种场景, 我们只需交换过滤器的开始和结束时间, 从而开始时间为 17:00, 结束时间为 08:00。在完整测试 (不含过滤器) 的情况下, 我们可以在 1 月 3 日 10:00 找到不属于日内范围的第一笔交易。10:00 大于 08:00 且低于 17:00, 所以我们确信这根蜡烛的交易超出了日内的范围。此设置的测试结果在本文结尾提供 (tester_time_hour2.html), 所述交易的屏幕截图如下所示:


时间 hour2 交易

从屏幕截图可以看出, EA 将以前的持仓平仓, 但没有开新单。垂直虚线表示新时段的开始。时段始后, EA 在日内时段的第三根蜡烛处开了首单。

对于第三种场景, 我们将 EA 配置为使用第一种场景的设置, 但午餐时间除外。因此, 在 12:00 的蜡烛上, 不应该进行任何交易, EA 将在 13:00 的蜡烛开始时恢复交易。在完整的测试 (没有过滤器) 中, 我们可以找到一笔在 1 月 9 日 12:00 开立交易的实例。由于交易在 12 点以下, 鉴于此情况下 EA 的设定, 我们预计该笔交易 EA 不会入场。此设置的测试结果在本文末尾提供 (tester_time_hour3.html), 所述交易的屏幕截图如下所示:


时间 hour3 交易

如屏幕截图所示, 在 12:00 的蜡烛上, EA 将现有持仓平仓, 但没有入场开立另一笔交易。EA 按预期暂停了一个小时。它仅在 16:00 时入场开立了一笔仓位 (买入), 这个时间是在下午时段开始 3 个小时之后, 且在时段结束前 1 小时, 但这是 EA 根据下一次反转信号入场交易的最早时间。

结束语

在本文中, 我们讨论了跨平台智能交易系统时间过滤的各种方法的实现。本文涵盖了各种时间过滤器的使用, 以及如何通过时间过滤器容器进行组合, 以便某些功能可以在智能交易系统中按照某些时间设置启用或禁用。