Кроссплатформенный торговый советник: Временные фильтры
Оглавление
- Введение
- Цели
- Базовый класс
- Классы и типы временного фильтра
- Контейнер временных фильтров
- Подфильтры (CTimeFilter)
- Пример
- Заключение
Введение
Временная фильтрация используется, когда советник должен проверить, находится ли определенный период времени внутри ранее заданного интервала. От удовлетворения (или неудовлетворения) этому условию зависит включение (или отключение) некоторых функций. Это очень полезно, когда заданная функция советника настроена на работу не всё время (периодическую, либо постоянную за исключением некоторых условий). Приведу примеры, в которых может быть применена временная фильтрация.
- Избегание определенных периодов времени (к примеру, периодов флэта или высокой волатильности на рынке)
- Установка времени истечения для рыночного ордера или позиции (выход из рынка по окончании определенного периода)
- Закрытие сделок в конце торговой недели
Это некоторые из самых распространенных функций, используемых трейдерами. Но есть и другие варианты.
Цели
- Понимать и применять самые распространенные методы временной фильтрации
- Дать советникам возможность простого использования сразу нескольких временных фильтров
- Обеспечить совместимость с 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-часового периода. В нем нет данных другого формата — например, годов или месяцев. Чтобы сравнить начальное и конечное время с указанным (или текущим, если аргумент установлен по умолчанию) есть как минимум два метода:
- выразить указанное время в значениях часов, минут и секунд, после чего сравнить их с параметрами структуры;
- обновить пропущенные параметры структур с использованием текущего времени, конвертировать структуры во формат 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. Если время старта позже, чем время окончания торговли, это значит, что временной интервал распространяется на следующие сутки. Таким образом, время окончания относится не к тому же дню, что и время старта. Тогда мы имеем одну из двух ситуаций:- начальное время — текущий день (или день, относящийся к указанному времени), конечное — следующий день после него.
- начальное время относится к вчерашнему дню (или конкретному указанному), а время окончания — текущий день (или конкретный указанный).
Необходимые настройки будут зависеть от текущего (или указанного) времени. Предположим, что это — 5:01 PM (17:01). В этом случае время старта будет находиться внутри того же дня, что и указанное. Но конечное время будет относиться уже к следующему дню. А вот если указанное время будет 01:00 или 1:00 AM, то оно будет находиться внутри того же дня, что и конечное, но время начала относится ко вчерашнему дню. Значит, ранее рассчитанные структуры MqlDateTime должны быть настроены следующим образом:
- если время старта относится к тому же дню, что и указанное, добавляем один день к конечному времени.
- если время окончания относится к тому же дню, что указанное, отнимем один день из начального времени.
Это применяется только если время начала и время окончания относятся к разным суткам — фильтр определяет это по признаку "час начала больше, чем час окончания". Корректировки реализуются внутри метода 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)); }
Фильтрация по внутридневному времени чуть сложнее. Продемонстрируем возможности временной фильтрации в советнике, основанной на трех разных сценариях:
- когда начало и окончание приходятся на один и тот же день;
- когда начало и окончание происходят в разные дни;
- вариант с несколькими экземплярами 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 года. То есть, можем сказать, что фильтр сработал, если советник больше не мог открывать сделки после финальной даты временного диапазона. Скриншот последней сделки:
Последняя сделка теста была открыта 6 января в 15.00 и находится внутри временного интервала, определенного для эксперта. Обратите внимание, что сделка оставалась открытой до следующей недели, что по-прежнему допустимо, поскольку временные фильтры работают только для входа в рынок, но не для выхода из него. Пунктирная вертикальная линия — последняя свеча недели.
Для фильтрации по дням нужно включить соответствующий параметр (настроить на true). Также остается включенной фильтрация по временному диапазону, но параметр Friday (регламентирующий работу в пятницу) отключен. Предыдущий тест показал, что последняя сделка открыта 6 января, в пятницу. Таким образом, если мы видим, что эта сделка больше не может быть открыта в Тестере, мы можем подтвердить, что этот конкретный фильтр тоже работает.
Результат теста также показан в приложении к статье (tester_time_days.html). Скриншот последней сделки:
Как видим, последняя сделка была совершена 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), а скриншот его первой сделки показан ниже:
Как и ожидалось, советник не открыл торговлю в самом начале теста. Вместо этого он дождался, когда сделка будет попадать в установленный внутридневной диапазон. Пунктирная вертикальная линия представляет начало теста, где располагалась бы первая сделка полного теста, если бы фильтр не применялся.
Для второго сценария мы просто меняем местами начальное и конечное время фильтра, и в итоге получаем начало в 17.00, а конец — в 8.00. В полном тесте без применения фильтров мы можем обнаружить первую сделку, которая не попадает в дневной диапазон, 3 января в 10.00. 10:00 больше, чем 08:00, и меньше, чем 17:00. Значит, сделка на этой свече не входит в наш внутридневной диапазон. Результат теста с этой настройкой также приводится в конце статьи (tester_time_hour2.html), скриншот сделки показан ниже:
Как мы видим на скриншоте, советник закрыл предыдущий ордер, но не открыл новый. Пунктирная вертикальная линия представляет начало новой торговой сессии. Советник открывает первую сделку дневной сессии через три свечи после ее начала.
Для третьего сценария мы изменяем советник так, чтобы он использовал настройки первого сценария, но включил в них перерыв на обед. Таким образом, на свече в 12:00 не происходит никаких сделок, и советник возобновляет торговлю с началом свечи в 13.00. В полном тесте без использования фильтров мы видим экземпляр, который открывает сделку в 12.00 9 января. Поскольку сделка выпадает на 12.00, при применении заданных настроек советник не войдет в нее. Результат теста с этими настройками приложен в конце статьи (tester_time_hour3.html), картинка с вышеуказанной сделкой показана ниже:
Как можно видеть из скриншота, на 12-часовой свече советник закрывает текущую сделку buy, но не входит в следующую сделку. Советник входит в часовую паузу, как и предполагалось. Он открывает следующую позицию на покупку только в 16.00, через три часа после обеда, и за час до конца торговой сессии. Это самое раннее время, когда советник смог войти в рынок на основании разворотного сигнала.
Заключение
В статье мы обсудили имплементацию различных методов фильтрации времени в кроссплатформенном торговом советнике. Статья охватывает различные временные фильтры, а также способы их комбинирования с использованием контейнеров фильтров, позволяющих включать и отключать их в советнике в зависимости от определенных настроек времени.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/3395
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Отличное решение, мне кажется одно из лучших, может есть люди которые брали себе данный класс за основу, структурировав под себя в более удобном виде для использования, а то тут он встроен в робота и через кучу ссылок и т.д. как то сложно разобраться что к чему там привязано.
Поделитесь если у кого есть чисто класс, без робота.