English 中文 Español Deutsch 日本語 Português
Кроссплатформенный торговый советник: Временные фильтры

Кроссплатформенный торговый советник: Временные фильтры

MetaTrader 5Примеры | 24 августа 2017, 15:08
5 482 1
Enrico Lambino
Enrico Lambino

Оглавление

  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;
  };

Базовый класс имеет три члена примитивных типов данных.

  • m_active используется для включения или отключения объекта класса.
  • m_reverse используется для обратного изменения вывода объекта класса (возвращает true, если оригинальный вывод false, и возвращает false, если оригинальный вывод true).
  • 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);
   //--- initialization                    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);
   //--- processing
   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. Предполагается, что мы будем использовать побитовые процедуры, поэтому нам также нужно объявить пользовательское перечисление, члены которого представляют каждый из семи дней недели.

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);
   //--- processing   
   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 нужно установить желаемое время старта (кроме случаев, когда предпочтительное время начала — это полночь 1 января 1970 года по UTC/GMT. Ниже показаны методы установки и получения для члена класса m_time_start, с использованием перегрузки метода TimeStart:

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;
  }

Такой метод временной фильтрации используется в разных вариантах: например, установка максимального периода истечения для определенных функций советника или установка истечения рыночного ордера или позиции (аналогично торговле бинарными опционами). Этот временной фильтр примерно похож на использование события Timer (совместимого с 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-часового периода хранится в двух структурах, а член объекта используется для хранения подфильтров.

Через конструктор класса объекта мы получим начальное и конечное время в часовом, минутном и секундном выражении. Затем эти значения сохраняются в членах класса m_filter_start и m_filter_end с помощью метода Set класса. Параметр 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;
   /*
     other tasks here
   */
  }

Сравнение не включает время окончания. К примеру, если мы хотим, чтобы эксперт торговал только между 08:00 и 17:00, то он может открыть торговлю прямо с 8.00, с самым началом восьмичасовой свечи, но торговать он может только до 17.00. Значит, последняя сделка может быть открыта только до 16.59 включительно.

Поскольку в структурах не содержатся временные промежутки больше часа, пропущенные данные должны будут быть получены из текущего (или указанного) времени. Тем не менее, некоторые корректировки нам понадобятся, если начальное и конечное время находятся внутри 24-часового периода, но относятся к разным суткам. В примере выше 08:00 — это 08.00 АМ, а 17.00 - это 5:00 PM. В этом случае оба времени находятся в пределах одного дня. Но предположим, что мы переключили начальное время на 5:00 PM, а конечное — на 8:00 AM. Если время старта позже, чем время окончания торговли, это значит, что временной интервал распространяется на следующие сутки. Таким образом, время окончания относится не к тому же дню, что и время старта. Тогда мы имеем одну из двух ситуаций:
  1. начальное время — текущий день (или день, относящийся к указанному времени), конечное — следующий день после него.
  2. начальное время относится к вчерашнему дню (или конкретному указанному), а время окончания — текущий день (или конкретный указанный).

Необходимые настройки будут зависеть от текущего (или указанного) времени. Предположим, что это — 5:01 PM (17:01). В этом случае время старта будет находиться внутри того же дня, что и указанное. Но конечное время будет относиться уже к следующему дню. А вот если указанное время будет 01:00 или 1:00 AM, то оно будет находиться внутри того же дня, что и конечное, но время начала относится ко вчерашнему дню. Значит, ранее рассчитанные структуры MqlDateTime должны быть настроены следующим образом:

  1. если время старта относится к тому же дню, что и указанное, добавляем один день к конечному времени.
  2. если время окончания относится к тому же дню, что указанное, отнимем один день из начального времени.

Это применяется только если время начала и время окончания относятся к разным суткам — фильтр определяет это по признаку "час начала больше, чем час окончания". Корректировки реализуются внутри метода 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;}
   //-- initialization
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual CObject *GetContainer(void);
   virtual void      SetContainer(CObject*);
   virtual bool      Validate(void) const;
   //--- activation and deactivation
   bool              Active(void) const;
   void              Active(const bool);
   int               Selected(void);
   //--- проверка
   virtual bool      Evaluate(datetime) const;
   //--- recovery
   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 AM да 5:00 PM, а на торговлю с перерывом на обеденное время. То есть, трейдинг будет идти с 8:00 AM до 12:00 PM и с 1:00 PM до 5:00 PM. Теперь временная шкала больше не непрерывна: она разделена на две части. Может возникнуть соблазн изменить исходный код: использовать не один экземпляр 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, и наоборот. Ситуация еще сложнее, если задействованы более двух временных фильтров. Единственный способ, когда все будет работать корректно при этой настройке — это когда один фильтр активен, а остальные — нет.

Решение — сделать так, чтобы контейнер временных фильтров всегда содержал только один указатель на CTimeFilter. Если нужно более одного экземпляра CTimeFilter, остальные должны добавляться в качестве подфильтра в другой экземпляр CTimeFilter. Указатели на подфильтры хранятся внутри члена класса CTimeFilter's — 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). Поэтому если время находится в одном из указанных временных промежутков, будет окончательно возвращено true.

Возможно оперировать и более чем двумя подфильтрами — в этом случае работа будет идти по тому же алгоритму. Однако подфильтры подфильтров, скорее всего, не понадобятся, поскольку комбинации фильтра внутридневной торговли могут быть представлены только двумя уровнями.

Пример

В качестве примера, мы модифицируем советник из предыдущей статьи. Включим в него все временные фильтры, которые мы обсудили в этой статье. Начнем с включения заголовочного файла для CTimesBase, поскольку этого вполне достаточно, чтобы добавить в советник все классы временных фильтров.

#include "MQLx\Base\OrderManager\OrderManagerBase.mqh"
#include "MQLx\Base\Signal\SignalsBase.mqh"
#include "MQLx\Base\Time\TimesBase.mqh" //added include line
#include <Indicators\Custom.mqh>

Затем объявим глобальный указатель на контейнер временных фильтров:

CTimes *time_filters;

В OnInit создадим экземпляр CTimes и сохраним его в этом указателе:

time_filters = new CTimes();

Для этого советника мы применим временную фильтрацию только при входе в новые сделки, но не при выходе. Чтобы этого добиться, перед тем, как советник откроет сделку, производится дополнительная проверка: позволено ли советнику вообще входить в сделки в это время.

if(signals.CheckOpenLong())
  {
   close_last();
   if (time_filters.Evaluate(TimeCurrent()))
   {
      //Print("Entering buy trade..");            
      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("Entering sell trade..");
      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;

Значение по умолчанию 0 для этих параметров означает, что при старте они обращаются ко времени UNIX. Чтобы предотвратить случайную ошибку при использовании значений по умолчанию, введем дополнительное условие: временной фильтр создается только когда время окончания больше 0, и больше, чем начальное время. Это закодировано в функции 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));
  }

Фильтрация по внутридневному времени чуть сложнее. Продемонстрируем возможности временной фильтрации в советнике, основанной на трех разных сценариях:

  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;
// 1st set
input int intraday1_hour_start=8;
input int intraday1_minute_start=0;
input int intraday1_hour_end=17;
input int intraday1_minute_end=0;
// 2nd set
input int intraday2_hour1_start=8;
input int intraday2_minute1_start=0;
input int intraday2_hour1_end=12;
input int intraday2_minute1_end=0;
// 3rd set
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 с использованием 2-го и 3-го наборов параметров:
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("one or more time filters failed validation");
   return INIT_FAILED;
  }

Теперь перейдем к результатам тестирования советника. Тесты были выполнены с использованием Тестера стратегий на данных за весь январь 2017 года.

Результат тестов советника с фильтром по диапазону времени находится в приложении к статье (tester_time_range.html). В этом тесте временной диапазон начинается со стартом 2017 года, а заканчивается в первую пятницу месяца, 6 января 2017 года. То есть, можем сказать, что фильтр сработал, если советник больше не мог открывать сделки после финальной даты временного диапазона. Скриншот последней сделки:

time days last trade

Последняя сделка теста была открыта 6 января в 15.00 и находится внутри временного интервала, определенного для эксперта. Обратите внимание, что сделка оставалась открытой до следующей недели, что по-прежнему допустимо, поскольку временные фильтры работают только для входа в рынок, но не для выхода из него. Пунктирная вертикальная линия — последняя свеча недели.

Для фильтрации по дням нужно включить соответствующий параметр (настроить на true). Также остается включенной фильтрация по временному диапазону, но параметр Friday (регламентирующий работу в пятницу) отключен. Предыдущий тест показал, что последняя сделка открыта 6 января, в пятницу. Таким образом, если мы видим, что эта сделка больше не может быть открыта в Тестере, мы можем подтвердить, что этот конкретный фильтр тоже работает.

Результат теста также показан в приложении к статье (tester_time_days.html). Скриншот последней сделки:

time days last trade


Как видим, последняя сделка была совершена 5 января, а не 6, как на предыдущей иллюстрации. В этой новой конфигурации предпоследняя сделка теперь становится последней. Ее точка выхода совпадает со второй вертикальной линией, которая также является входной точкой последней сделки для предыдущего теста, поскольку у советника всегда имеется одна открытая позиция.

Для временного фильтра, как показано ранее, мы использовали альтернативный конструктор CTimer, принимающий только один аргумент. Затем он сохраняется в члене класса m_total, представляющем число секунд, в течение которого фильтр возвращает true до времени истечения. Поскольку это время выражается в секундах, мы должны умножить входные параметры на 60, и тогда значения тоже будут сохраняться в секундном выражении. 10080 — количество минут, установленное в эксперте по умолчанию. Оно эквивалентно одной неделе. Таким образом, если объединить первый временной фильтр с этим, результат тестирования с использованием первого фильтра должен быть идентичным результатам этого фильтра. Результат тестирования действительно совпадает. Он приводится в конце статьи (tester_timer.html).

Для окончания временной фильтрации CTimeFilter есть три разных варианта. Нам нужно протестировать каждый из них.

Поскольку советник всегда сохраняет постоянную позицию, он вынужден закрывать предыдущие сделки, прежде чем открыть новую, только чтобы закрыть ее и открыть новую, и так далее в бесконечной цепочке. Исключением будет ситуация, когда один или более временных фильтров возвращают false. Таким образом, если торговля в течение внутридневного периода была прервана — значит, это временные фильтры предостерегли советник от входа в сделку. Полные результаты теста без временных фильтров приведены в конце статьи (tester_full.html).

Для сценария №1 по внутридневной фильтрации, описанной в данной статье, мы устанавливаем начальное время на 08:00, а конечное на 17:00. В полном тесте самая первая сделка была открыта прямо в начале тестирования, которое пришлось на время 00:00. Это выходит за установленные первым сценарием временные рамки. В соответствии с этими настройками, мы ждем, что советник не примет эту сделку, а вместо нее откроет следующую, уже в границах временного фильтра. Она и будет считаться первой сделкой с примененным фильтром. Результат теста с применением этой настройки тоже приложен в конце статьи (tester_time_hour1.html), а скриншот его первой сделки показан ниже:


time hour trades 3

Как и ожидалось, советник не открыл торговлю в самом начале теста. Вместо этого он дождался, когда сделка будет попадать в установленный внутридневной диапазон. Пунктирная вертикальная линия представляет начало теста, где располагалась бы первая сделка полного теста, если бы фильтр не применялся.

Для второго сценария мы просто меняем местами начальное и конечное время фильтра, и в итоге получаем начало в 17.00, а конец — в 8.00. В полном тесте без применения фильтров мы можем обнаружить первую сделку, которая не попадает в дневной диапазон, 3 января в 10.00. 10:00 больше, чем 08:00, и меньше, чем 17:00. Значит, сделка на этой свече не входит в наш внутридневной диапазон. Результат теста с этой настройкой также приводится в конце статьи (tester_time_hour2.html), скриншот сделки показан ниже:


time hour2 trades

Как мы видим на скриншоте, советник закрыл предыдущий ордер, но не открыл новый. Пунктирная вертикальная линия представляет начало новой торговой сессии. Советник открывает первую сделку дневной сессии через три свечи после ее начала.

Для третьего сценария мы изменяем советник так, чтобы он использовал настройки первого сценария, но включил в них перерыв на обед. Таким образом, на свече в 12:00 не происходит никаких сделок, и советник возобновляет торговлю с началом свечи в 13.00. В полном тесте без использования фильтров мы видим экземпляр, который открывает сделку в 12.00 9 января. Поскольку сделка выпадает на 12.00, при применении заданных настроек советник не войдет в нее. Результат теста с этими настройками приложен в конце статьи (tester_time_hour3.html), картинка с вышеуказанной сделкой показана ниже:

time hour3 trades

Как можно видеть из скриншота, на 12-часовой свече советник закрывает текущую сделку buy, но не входит в следующую сделку. Советник входит в часовую паузу, как и предполагалось. Он открывает следующую позицию на покупку только в 16.00, через три часа после обеда, и за час до конца торговой сессии. Это самое раннее время, когда советник смог войти в рынок на основании разворотного сигнала.

Заключение

В статье мы обсудили имплементацию различных методов фильтрации времени в кроссплатформенном торговом советнике. Статья охватывает различные временные фильтры, а также способы их комбинирования с использованием контейнеров фильтров, позволяющих включать и отключать их в советнике в зависимости от определенных настроек времени.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/3395

Прикрепленные файлы |
tester_results.zip (464.13 KB)
tf_ha_ma.zip (1101.98 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Konstantin Seredkin
Konstantin Seredkin | 21 мар. 2018 в 08:04

Отличное решение, мне кажется одно из лучших, может есть люди которые брали себе данный класс за основу, структурировав под себя в более удобном виде для использования, а то тут он встроен в робота и через кучу ссылок и т.д. как то сложно разобраться что к чему там привязано.

Поделитесь если у кого есть чисто класс, без робота.

Оценка риска в последовательности сделок с одним активом Оценка риска в последовательности сделок с одним активом
В статье описано использование методов теории вероятностей и математической статистики при анализе торговых систем.
Автоматический поиск дивергенций и конвергенций Автоматический поиск дивергенций и конвергенций
В статье рассматриваются всевозможные виды дивергенции: простая, скрытая, расширенная, тройная, четвертная дивергенции, конвергенция, дивергенции классов A, B и C. Создается универсальный индикатор для их поиска и отображения на графике.
Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio" Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio"
Рассмотрен еще один пользовательский критерий оптимизации торговых стратегий, основанный на анализе графика баланса. Для этого использовалось вычисление линейной регрессии с помощью функции из библиотеки ALGLIB.
Рассматриваем на практике адаптивный метод следования за рынком Рассматриваем на практике адаптивный метод следования за рынком
Основное отличие торговой системы, предложенной в статье — использование математических инструментов для анализа биржевых котировок. В системе применяются цифровая фильтрация и спектральная оценка дискретных временных рядов. Описаны теоретические аспекты стратегии и построен советник для ее тестирования.