English 中文 Español Deutsch 日本語 Português
Рецепты MQL5 - Торговые сигналы скользящих каналов

Рецепты MQL5 - Торговые сигналы скользящих каналов

MetaTrader 5Примеры | 6 сентября 2016, 12:31
7 887 8
Denis Kirichenko
Denis Kirichenko

Введение

В своей статье «Рецепты MQL5 - Программируем скользящие каналы» я представил способ построения равноудалённых каналов, которые часто называют скользящими. При этом для решения задачи я обращался к инструменту «Равноудалённый канал» и возможностям ООП.

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

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

Тем, кто начинает своё знакомство с этой стратегией, я предлагаю изучать материал от простого к сложному. Сначала создадим базовую стратегию, а потом по возможности будем её усложнять и дополнять.


1. Индикатор равноудалённых каналов

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


Графические объекты при тестировании

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

Данное ограничение не распространяется на тестирование в визуальном режиме.


Поэтому я пошёл по другому пути и создал индикатор, отражающий как фракталы, так и актуальный канал.

Этот индикатор называется EquidistantChannels. Он состоит, по сути, из двух блоков. В первом рассчитываются буферы фракталов, а во втором — буферы канала.

Приведу код обработчика события Calculate.

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- если на предыдущем вызове не было баров
   if(prev_calculated==0)
     {
      //--- обнулить буферы
      ArrayInitialize(gUpFractalsBuffer,0.);
      ArrayInitialize(gDnFractalsBuffer,0.);
      ArrayInitialize(gUpperBuffer,0.);
      ArrayInitialize(gLowerBuffer,0.);
      ArrayInitialize(gNewChannelBuffer,0.);
     }
//--- Расчёт для фракталов [start]
   int startBar,lastBar;
//---
   if(rates_total<gMinRequiredBars)
     {
      Print("Недостаточно данных для расчёта");
      return 0;
     }
//---
   if(prev_calculated<gMinRequiredBars)
      startBar=gLeftSide;
   else
      startBar=rates_total-gMinRequiredBars;
//---
   lastBar=rates_total-gRightSide;
   for(int bar_idx=startBar; bar_idx<lastBar && !IsStopped(); bar_idx++)
     {
      //---
      if(isUpFractal(bar_idx,gMaxSide,high))
         gUpFractalsBuffer[bar_idx]=high[bar_idx];
      else
         gUpFractalsBuffer[bar_idx]=0.0;
      //---
      if(isDnFractal(bar_idx,gMaxSide,low))
         gDnFractalsBuffer[bar_idx]=low[bar_idx];
      else
         gDnFractalsBuffer[bar_idx]=0.0;
     }
//--- Расчёт для фракталов [end]

//--- Расчёт для границ канала [start]
   if(prev_calculated>0)
     {
      //--- если набор не инициализирован
      if(!gFracSet.IsInit())
         if(!gFracSet.Init(
            InpPrevFracNum,
            InpBarsBeside,
            InpBarsBetween,
            InpRelevantPoint,
            InpLineWidth,
            InpToLog
            ))
           {
            Print("Ошибка инициализации фрактального набора!");
            return 0;
           }
      //--- обсчёт
      gFracSet.Calculate(gUpFractalsBuffer,gDnFractalsBuffer,time,
                         gUpperBuffer,gLowerBuffer,
                         gNewChannelBuffer
                         );
     }
//--- Расчёт для границ канала [end]

//--- return value of prev_calculated for next call
   return rates_total;
  }

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

Теперь несколько слов о наборе фрактальных точек — объекте CFractalSet. Ввиду того, что я изменил способ отображения канала, также пришлось модифицировать класс CFractalSet. Ключевым методом стал CFractalSet::Calculate, обсчитывающий буферы канала индикатора. Код приведён в файле CFractalPoint.mqh.



Теперь у нас есть основа — поставщик сигналов от равноудалённого канала. Работа индикатора представлена на видео.


2. Базовая стратегия

Итак, предлагаю начать с чего-то несложного, что с помощью ООП можно будет усовершенствовать и доработать. Пусть имеется какая-то базовая стратегия.

Эта стратегия будет рассматривать достаточно простые правила торговли. Вход в рынок производится от границ канала. При касании ценой нижней границы открываем покупку, а верхней – продажу. На Рис.1 цена коснулась нижней границы, поэтому робот купил некоторый объём. Торговые уровни (стоп-лосс и тейк-профит) имеют фиксированный размер и были выставлены автоматически. Если позиция открыта, повторные сигналы на вход игнорируем.

Рис.1 Сигнал на вход

Рис.1 Сигнал на вход


Хотел бы ещё отметить, что Стандартная библиотека уже довольно-таки сильно разрослась. В ней уже есть много готовых классов, которыми можно воспользоваться. Попробуем для начала «присоседиться» к сигнальному классу CExpertSignal. Согласно документации, он является базовым классом для создания генераторов торговых сигналов.

На мой взгляд, данный класс был назван очень точно. Это не CTradeSignal и не CSignal, а именно класс сигналов, предназначенных для использования в коде советника — CExpertSignal.

Я не буду останавливаться на его содержании. В статье «Мастер MQL5: Как написать свой модуль торговых сигналов» подробно описаны методы сигнального класса.


2.1 Сигнальный класс CSignalEquidChannel 

Итак, производный сигнальный класс получился таким:

//+------------------------------------------------------------------+
//| Class CSignalEquidChannel                                        |
//| Purpose: Класс торговых сигналов на основе равноудалённого       |
//|          канала.                                                 |
//| Потомок класса CExpertSignal.                                    |
//+------------------------------------------------------------------+
class CSignalEquidChannel : public CExpertSignal
  {
protected:
   CiCustom          m_equi_chs;          // объект-индикатор "EquidistantChannels"   
   //--- настраиваемые параметры
   int               m_prev_frac_num;     // предыдущих фракталов
   bool              m_to_plot_fracs;     // отображать фракталы?
   int               m_bars_beside;       // баров слева/справа от фрактала
   int               m_bars_between;      // промежуточных баров
   ENUM_RELEVANT_EXTREMUM m_relevant_pnt; // актуальная точка
   int               m_line_width;        // толщина линии
   bool              m_to_log;            // вести журнал?
   double            m_pnt_in;            // внутренний допуск,пп
   double            m_pnt_out;           // внешний допуск,пп
   bool              m_on_start;          // флаг сигнала при старте
   //--- расчётные
   double            m_base_low_price;    // базовая минимальная цена
   double            m_base_high_price;   // базовая максимальная цена
   double            m_upper_zone[2];     // верхняя зона: [0]-внутренний допуск, [1]-внешний  
   double            m_lower_zone[2];     // нижняя зона
   datetime          m_last_ch_time;      // время появления последнего канала
   //--- "веса" рыночных моделей (0-100)
   int               m_pattern_0;         //  "касание нижней границы канала - buy, верхней - sell"

   //--- === Methods === --- 
public:
   //--- конструктор/деструктор
   void              CSignalEquidChannel(void);
   void             ~CSignalEquidChannel(void){};
   //--- методы установки настраиваемых параметров
   void              PrevFracNum(int _prev_frac_num)   {m_prev_frac_num=_prev_frac_num;}
   void              ToPlotFracs(bool _to_plot)        {m_to_plot_fracs=_to_plot;}
   void              BarsBeside(int _bars_beside)      {m_bars_beside=_bars_beside;}
   void              BarsBetween(int _bars_between)    {m_bars_between=_bars_between;}
   void              RelevantPoint(ENUM_RELEVANT_EXTREMUM _pnt) {m_relevant_pnt=_pnt;}
   void              LineWidth(int _line_wid)          {m_line_width=_line_wid;}
   void              ToLog(bool _to_log)               {m_to_log=_to_log;}
   void              PointsOutside(double _out_pnt)    {m_pnt_out=_out_pnt;}
   void              PointsInside(double _in_pnt)      {m_pnt_in=_in_pnt;}
   void              SignalOnStart(bool _on_start)     {m_on_start=_on_start;}
   //--- методы настраивания "весов" рыночных моделей
   void              Pattern_0(int _val) {m_pattern_0=_val;}
   //--- метод проверки настроек
   virtual bool      ValidationSettings(void);
   //--- метод создания индикатора и таймсерий
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- методы проверки, если модели рынка сформированы
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);
   virtual double    Direction(void);
   //---
protected:
   //--- метод инициализации индикатора
   bool              InitCustomIndicator(CIndicators *indicators);
   //- получение значения верхней границы канала
   double            Upper(int ind) {return(m_equi_chs.GetData(2,ind));}
   //- получение значения нижней границы канала
   double            Lower(int ind) {return(m_equi_chs.GetData(3,ind));}
   //- получение флага появления канала
   double            NewChannel(int ind) {return(m_equi_chs.GetData(4,ind));}
  };
//+------------------------------------------------------------------+

Отмечу несколько нюансов.

В данном классе основной сигнальщик — это сетап на равноудалённом канале. И в текущем варианте он является единственным. Других пока не будет совсем. В состав класса, в свою очередь, входит класс для работы с техническим индикатором пользовательского типаCiCustom

В качестве сигнальной модели используется базовая: "касание нижней границы канала — buy, верхней — sell". Ввиду того, что касание с точностью до пункта, скажем так, не самое вероятное событие, то используется некоторый буфер, границы которого можно регулировать. Параметр внешнего допуска m_pnt_out определяет размер допустимого выхода цены за пределы канала, а параметр внутреннего m_pnt_in — размер того, насколько цена не дойдёт до границы. Логика совершенно простая. Считаем, что цена коснулась границы канала, если она немного не дошла до неё или немного вышла за её пределы. На Рис.2 схематично представлен буфер, попав в который снизу, цена вместе с границей отработает модель.

Рис.2 Отработка базовой сигнальной модели

Рис.2 Отработка базовой сигнальной модели


Параметр-массив m_upper_zone[2] очерчивает границы верхнего буфера, а m_lower_zone[2] — нижнего.

В примере уровень на $1,11552 выступает в качестве верхней границы канала (красная прямая). Уровень на $1,11452 отвечает за нижний предел буфера, а $1,11702 — за верхний. Тогда размер внешнего допуска — 150 пп, а внутреннего — 100 пп. Цена отображена синей кривой.

Параметр m_on_start позволяет проигнорировать сигналы первого канала при запуске робота на график, если такой канал уже отрисован. Если флаг сброшен, то робот будет работать только на следующем канале, а на текущем не будет обрабатывать торговые сигналы.

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

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

Начну с метода Direction(), который количественно оценивает потенциальное торговое направление:

//+------------------------------------------------------------------+
//| Определение "взвешенного" направления                            |
//+------------------------------------------------------------------+
double CSignalEquidChannel::Direction(void)
  {
   double result=0.;
//--- появление нового канала
   datetime last_bar_time=this.Time(0);
   bool is_new_channel=(this.NewChannel(0)>0.);
//--- если игнорировать сигналы первого канала
   if(!m_on_start)
      //--- если первый канал обычно отражается при инициализации
      if(m_prev_frac_num==3)
        {
         static datetime last_ch_time=0;
         //--- если появился новый канал
         if(is_new_channel)
           {
            last_ch_time=last_bar_time;
            //--- если первый запуск
            if(m_last_ch_time==0)
               //--- запомнить время бара, где появился первый канал
               m_last_ch_time=last_ch_time;
           }
         //--- если времена совпадают
         if(m_last_ch_time==last_ch_time)
            return 0.;
         else
         //--- сбросить флаг
            m_on_start=true;
        }
//--- индекс актуального бара
   int actual_bar_idx=this.StartIndex();
//--- установить границы
   double upper_vals[2],lower_vals[2]; // [0]-бар предшествующий актуальному, [1]-актуальный бар
   ArrayInitialize(upper_vals,0.);
   ArrayInitialize(lower_vals,0.);
   for(int idx=ArraySize(upper_vals)-1,jdx=0;idx>=0;idx--,jdx++)
     {
      upper_vals[jdx]=this.Upper(actual_bar_idx+idx);
      lower_vals[jdx]=this.Lower(actual_bar_idx+idx);
      if((upper_vals[jdx]==0.) || (lower_vals[jdx]==0.))
         return 0.;
     }
//--- получить цены
   double curr_high_pr,curr_low_pr;
   curr_high_pr=this.High(actual_bar_idx);
   curr_low_pr=this.Low(actual_bar_idx);
//--- если цены получены
   if(curr_high_pr!=EMPTY_VALUE)
      if(curr_low_pr!=EMPTY_VALUE)
        {
         //--- запомнить цены
         m_base_low_price=curr_low_pr;
         m_base_high_price=curr_high_pr;
         //--- Определить цены для буферных зон
         //--- верхняя зона: [0]-внутренний допуск, [1]-внешний 
         this.m_upper_zone[0]=upper_vals[1]-m_pnt_in;
         this.m_upper_zone[1]=upper_vals[1]+m_pnt_out;
         //--- нижняя зона: [0]-внутренний допуск, [1]-внешний 
         this.m_lower_zone[0]=lower_vals[1]+m_pnt_in;
         this.m_lower_zone[1]=lower_vals[1]-m_pnt_out;
         //--- нормализация
         for(int jdx=0;jdx<ArraySize(m_lower_zone);jdx++)
           {
            this.m_lower_zone[jdx]=m_symbol.NormalizePrice(m_lower_zone[jdx]);
            this.m_upper_zone[jdx]=m_symbol.NormalizePrice(m_upper_zone[jdx]);
           }
         //--- проверить, не сходятся ли зоны
         if(this.m_upper_zone[0]<=this.m_lower_zone[0])
            return 0.;
         //--- результат
         result=m_weight*(this.LongCondition()-this.ShortCondition());
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

Первый блок в теле метода — это проверка того, требуется ли игнорировать первый канал на графике, если такой вообще присутствует.

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

Целевая строка выделена синим. Здесь получаем количественную оценку торгового направления, если оно есть.

Рассмотрим теперь метод LongCondition().

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- если задана минимальная цена
   if(m_base_low_price>0.)
      //--- если минимальная цена на уровне нижней границы
      if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
        {
         if(IS_PATTERN_USAGE(0))
            result=m_pattern_0;
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

Для покупки проверяем, попала ли цена в нижнюю буферную зону. Если попала, то проверяем разрешение на задействование рыночной модели. Более подробно о конструкции вида "IS_PATTERN_USAGE(k)" можно прочесть в статье  «Генератор торговых сигналов пользовательского индикатора».

Метод ShortCondition() работает по аналогии с вышеописанным. Только здесь в фокусе верхняя буферная зона.

//+------------------------------------------------------------------+
//| Проверка условия на продажу                                      |
//+------------------------------------------------------------------+
int CSignalEquidistantChannel::ShortCondition(void)
  {
   int result=0;
//--- если задана максимальная цена
   if(m_base_high_price>0.)
      //--- если максимальная цена на уровне верхней границы
      if((m_base_high_price>=m_upper_zone[0]) && (m_base_high_price<=m_upper_zone[1]))
        {
         if(IS_PATTERN_USAGE(0))
            result=m_pattern_0;       
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

В классе инициализируется собственный индикатор с помощью метода InitCustomIndicator():

//+------------------------------------------------------------------+
//| Инициализация собственных индикаторов                            |
//+------------------------------------------------------------------+
bool CSignalEquidChannel::InitCustomIndicator(CIndicators *indicators)
  {
//--- добавление объекта в коллекцию
   if(!indicators.Add(GetPointer(m_equi_chs)))
     {
      PrintFormat(__FUNCTION__+": error adding object");
      return false;
     }
//--- задание параметров индикатора
   MqlParam parameters[8];
   parameters[0].type=TYPE_STRING;
   parameters[0].string_value="EquidistantChannels.ex5";
   parameters[1].type=TYPE_INT;
   parameters[1].integer_value=m_prev_frac_num;   // 1) предыдущих фракталов
   parameters[2].type=TYPE_BOOL;
   parameters[2].integer_value=m_to_plot_fracs;   // 2) отображать фракталы?
   parameters[3].type=TYPE_INT;
   parameters[3].integer_value=m_bars_beside;     // 3) баров слева/справа от фрактала
   parameters[4].type=TYPE_INT;
   parameters[4].integer_value=m_bars_between;    // 4) промежуточных баров
   parameters[5].type=TYPE_INT;
   parameters[5].integer_value=m_relevant_pnt;    // 5) актуальная точка
   parameters[6].type=TYPE_INT;
   parameters[6].integer_value=m_line_width;    // 6) толщина линии
   parameters[7].type=TYPE_BOOL;
   parameters[7].integer_value=m_to_log;          // 7) вести журнал?

//--- инициализация объекта
   if(!m_equi_chs.Create(m_symbol.Name(),_Period,IND_CUSTOM,8,parameters))
     {
      PrintFormat(__FUNCTION__+": error initializing object");
      return false;
     }
//--- количество буферов
   if(!m_equi_chs.NumBuffers(5))
      return false;
//--- ok
   return true;
  }
//+------------------------------------------------------------------+

В массиве параметров первым нужно указать строковое название индикатора.

В классе имеется и свой виртуальный метод ValidationSettings(). Он вызывает аналогичный метод предка и проверяет, корректно ли были заданы параметры канального индикатора. Ещё есть сервисные методы, получающие значения соответствующих буферов пользовательского индикатора.

Вот пока и всё, что касается производного сигнального класса.


2.2 Класс торговой стратегии CEquidChannelExpert

Для реализации заложенной базовой идеи придётся написать класс, производный от стандартного класса CExpert. На текущем шаге его код будет максимально компактным, поскольку, по сути, нужно изменить только поведение главного обработчика — метода Processing(). Он является виртуальным, что даёт нам возможность писать любые стратегии.

//+------------------------------------------------------------------+
//| Класс CEquidChannelExpert.                                       |
//| Цель: Класс для советника, торгующего по равноудалённому каналу. |
//| Потомок класса CExpert.                                          |
//+------------------------------------------------------------------+
class CEquidChannelExpert : public CExpert
  {
   //--- === Data members === --- 
private:


   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CEquidChannelExpert(void){};
   void             ~CEquidChannelExpert(void){};

protected:
   virtual bool      Processing(void);
  };
//+------------------------------------------------------------------+

Вот сам метод:

//+------------------------------------------------------------------+
//| Главный модуль                                                   |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- расчёт направления
   m_signal.SetDirection();
//--- check if open positions
   if(!this.SelectPosition())
     {
      //--- модуль открытия позиции
      if(this.CheckOpen())
         return true;
     }
//--- если нет торговых операций
   return false;
  }

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

Код базовой стратегии реализован в файле BaseChannelsTrader.mq5.




Пример работы базовой стратегии представлен на видео.


Рис.3 Результаты базовой стратегии за за 2013-2016 гг.

Рис.3 Результаты базовой стратегии за 2013-2015 гг.

Был сделан прогон в Тестере стратегий на часовом таймфрейме по символу EURUSD. На графике баланса заметно, что базовая стратегия на некоторых участках работала по "принципу пилы": за убыточной серией следовала прибыльная. Значения параметров пользователя, которые были задействованы при тестировании, находятся в файле base_signal.set. В нём же указаны и параметры канала, значения которых будут неизменными во всех версиях стратегии.

Здесь и далее используется режим тестирования "Каждый тик на основе реальных тиков".

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

В следующем разделе будем править базовую стратегию и искать факторы результативности.


3. Факторы результативности

Несколько слов о диспозиции. На мой взгляд, удобно расположить все файлы стратегии, которые делают её уникальной в одной папке проектов. Так, реализация базовой стратегии находится в подпапке Base (Рис.4) и т.д.

Рис.4 Пример иерархии папок проектов стратегии каналов

Рис.4 Пример иерархии папок проектов стратегии каналов

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

3.1 Использование трала

Для начала предлагаю добавить в нашу стратегию возможность трала. Пусть это будет объект класса CTrailingFixedPips, который позволяет сопровождать открытые позиции на фиксированном "расстоянии" (в пунктах). При этом тралиться будет как цена стоп-лосса, так и цена тейк-профита. Чтобы профит не тралился, нужно указать нулевое значение для соответствующего параметра (InpProfitLevelPips).

Внесем в код следующие изменения.

В исходный файл советника ChannelsTrader1.mq5 добавим группу пользовательских параметров:

//---
sinput string Info_trailing="+===-- Трал --====+"; // +===-- Трал --====+
input int InpStopLevelPips=30;          // Уровень для StopLoss, пп
input int InpProfitLevelPips=50;        // Уровень для TakeProfit, пп

В блок инициализации запишем, что создаём объект типа CTrailingFixedPips, включаем его в состав стратегии и задаём параметры трала.

//--- объект трала
   CTrailingFixedPips *trailing=new CTrailingFixedPips;
   if(trailing==NULL)
     {
      //--- ошибка
      printf(__FUNCTION__+": error creating trailing");
      myChannelExpert.Deinit();
      return(INIT_FAILED);
     }
//--- добавление объекта трала
   if(!myChannelExpert.InitTrailing(trailing))
     {
      //--- ошибка
      PrintFormat(__FUNCTION__+": error initializing trailing");
      myChannelExpert.Deinit();
      return INIT_FAILED;
     }
//--- параметры трала
   trailing.StopLevel(InpStopLevelPips);
   trailing.ProfitLevel(InpProfitLevelPips);

Поскольку мы будем использовать трал, то нужно модифицировать и главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert1.mqh.

//+------------------------------------------------------------------+
//| Главный модуль                                                   |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- расчёт направления
   m_signal.SetDirection();
//--- если позиции нет
   if(!this.SelectPosition())
     {
      //--- модуль открытия позиции
      if(this.CheckOpen())
         return true;
     }
//--- если есть позиция
   else
     {
      //--- проверка возможности модификации позиции
      if(this.CheckTrailingStop())
         return true;
     }
//--- если нет торговых операций
   return false;
  }

Вот и всё. Трал добавлен. Файлы обновлённой стратегии расположены в отдельной подпапке ChannelsTrader1.

Попробуем проверить, сказывается ли на результативности наше нововведение.

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

 Переменная СтартШаг
Стоп
Уровень для StopLoss, пп
0
10
100
Уровень для TakeProfit, пп
0
10
150

Результаты оптимизации находятся в файле ReportOptimizer-signal1.xml. Лучший проход представлен на Рис.5, где Уровень для StopLoss = 0, а для TakeProfit = 150.

Рис.5 Результаты стратегии с использованием трала за 2013-2016 гг.

Рис.5 Результаты стратегии с использованием трала за 2013-2015 гг.

Легко заметить, что последний рисунок напоминает Рис.3. Таким образом можно сказать, что в данном диапазоне значений использование трала не способствовало улучшению результата.


3.2 Тип канала

Есть предположение, что тип канала влияет на торговые результаты. Общая идея такая: продавать лучше в рамках нисходящего канала, а покупать — восходящего. Если канал ровный (не наклонный), то можно торговать от обеих границ.

Перечисление ENUM_CHANNEL_TYPE определяет тип канала:

//+------------------------------------------------------------------+
//| Тип канала                                                       |
//+------------------------------------------------------------------+
enum ENUM_CHANNEL_TYPE
  {
   CHANNEL_TYPE_ASCENDING=0,  // восходящий
   CHANNEL_TYPE_DESCENDING=1, // нисходящий
   CHANNEL_TYPE_FLAT=2,       // ровный
  };
//+------------------------------------------------------------------+

В исходный файл советника ChannelsTrader2.mq5 в блок инициализации запишем, что задаём параметр допуска для поиска типа канала.

//--- параметры фильтра
   filter0.PointsInside(_Point*InpPipsInside);
   filter0.PointsOutside(_Point*InpPipsOutside);
   filter0.TypeTolerance(_Point*InpTypePips);
   filter0.PrevFracNum(InpPrevFracNum);
   ...

Этот параметр контролирует скорость изменения цены в пунктах. Допустим, что он равен 7 пп. Значит, если на каждом баре канал "растёт" на 6 пп, то он не дотягивает до того, чтобы считаться восходящим. Тогда просто будем считать его ровным (не наклонным).

В исходный файл сигнала SignalEquidChannel2.mqh в метод Direction() добавим поиск типа канала.

//--- если новый канал
   if(is_new_channel)
     {
      m_ch_type=CHANNEL_TYPE_FLAT;                // ровный (не наклонный) канал 
      //--- если указан допуск для типа
      if(m_ch_type_tol!=EMPTY_VALUE)
        {
         //--- Тип канала
         //--- скорость изменения
         double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[1]-upper_vals[0]);
         //--- если достаточная скорость
         if(MathAbs(pr_speed_pnt)>m_ch_type_tol)
           {
            if(pr_speed_pnt>0.)
               m_ch_type=CHANNEL_TYPE_ASCENDING;  // восходящий канал
            else
               m_ch_type=CHANNEL_TYPE_DESCENDING; // нисходящий канал             
           }
        }
     }

Изначально канал считается ровным — не растёт и не снижается. Если не задавалось значение параметра допуска для поиска типа канала, то до вычисления скорости изменения дело не дойдёт.

 Условие на покупку будет включать проверку того, что канал не является нисходящим.

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- если задана минимальная цена
   if(m_base_low_price>0.)
      //--- если канал не нисходящий
      if(m_ch_type!=CHANNEL_TYPE_DESCENDING)
         //--- если минимальная цена на уровне нижней границы
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
//---
   return result;
  }
//+------------------------------------------------------------------+

Аналогичная проверка производится в условии на продажу, что канал не является восходящим.

Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert2.mqh будет таким же, что и в базовой версии, т.к. трал исключаем.

Проверим результативность данного фактора. Параметр оптимизируем только один.

 Переменная СтартШаг
Стоп
Допуск для типа, пп
0
5
150

Результаты оптимизации находятся в файле ReportOptimizer-signal2.xml. Лучший проход представлен на Рис.6.

Рис.6 Результаты стратегии с использованием типа канала за 2013-2015 гг.

Рис.6 Результаты стратегии с использованием типа канала за 2013-2015 гг.


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


3.3 Ширина канала

Мне кажется, что ширина канала может влиять на тип самой стратегии. Если канал получился узким, то при пробитии его границы (верхней или нижней) можно играть в сторону пробития, а не против. Тогда получим пробойную стратегию. Если канал получился широким, то можно торговать от его границ. Это и есть отбойная стратегия. Именно такой и является текущая стратегия — торговля идёт от границ канала.

Здесь, очевидно, нужен критерий для определения того, узок канал или широк. Чтобы не впадать в крайности, предлагаю добавить еще что-то среднее, чтобы считать исследуемый канал и не узким, и не широким. В итоге понадобятся 2 критерия:

  1. достаточная ширина узкого канала;
  2. достаточная ширина широкого канала.

Если канал окажется и не первым, и не вторым, то можно воздержаться от входа в рынок.

Рис.5 Ширина канала, схема

Рис.7 Ширина канала, схема

Сразу отмечу, что есть геометрическая проблема при определении ширины канала. Ведь на графике оси измеряются в разных величинах. Так, легко вычислить длину отрезков AB и CD. Но есть проблема с вычислением отрезка CE (Рис.7).

Я избрал, возможно, спорный и не самый точный способ нормализации, но зато он простой. Формула такая:

длина отрезка CE ≃ длина отрезка CD / (1.0 + скорость канала)

Ширину канала фиксируем с помощью перечисления ENUM_CHANNEL_WIDTH_TYPE:

//+------------------------------------------------------------------+
//| Ширина канала                                                    |
//+------------------------------------------------------------------+
enum ENUM_CHANNEL_WIDTH_TYPE
  {
   CHANNEL_WIDTH_NARROW=0,   // узкий
   CHANNEL_WIDTH_MID=1,      // средний
   CHANNEL_WIDTH_BROAD=2,    // широкий
  };

В исходный файл советника ChannelsTrader3.mq5 в группу пользовательских параметров "Каналы" добавим критерии ширины канала:

//---
sinput string Info_channels="+===-- Каналы --====+"; // +===-- Каналы --====+
input int InpPipsInside=100;            // Внутренний допуск, пп
input int InpPipsOutside=150;           // Внешний допуск, пп
input int InpNarrowPips=250;            // Узкий канал, пп
input int InpBroadPips=1200;            // Широкий канал, пп
...

Если критерий узкого канала будет иметь значение, превышающее аналогичное значение широкого, то будет ошибка инициализации:

//--- параметры фильтра
   filter0.PointsInside(_Point*InpPipsInside);
   filter0.PointsOutside(_Point*InpPipsOutside);
   if(InpNarrowPips>=InpBroadPips)
     {
      PrintFormat(__FUNCTION__+": error specifying narrow and broad values");
      return INIT_FAILED;
     }
   filter0.NarrowTolerance(_Point*InpNarrowPips);
   filter0.BroadTolerance(_Point*InpBroadPips);

В коде момент определения степени ширины канала представлен в теле метода Direction().

//--- Ширина канала 
   m_ch_width=CHANNEL_WIDTH_MID;               // средний
   double ch_width_pnt=((upper_vals[1]-lower_vals[1])/(1.0+pr_speed_pnt));
//--- если указан критерий узкого
   if(m_ch_narrow_tol!=EMPTY_VALUE)
      if(ch_width_pnt<=m_ch_narrow_tol)
         m_ch_width=CHANNEL_WIDTH_NARROW;      // узкий      
//--- если указан критерий широкого
   if(m_ch_narrow_tol!=EMPTY_VALUE)
      if(ch_width_pnt>=m_ch_broad_tol)
         m_ch_width=CHANNEL_WIDTH_BROAD;       // широкий 

Изначально считаем по ширине канал средним. Потом проверяем, не является ли он узким или широким.

Также нам нужно изменить еще и методы определения торгового направления. Так, условие на покупку будет выглядеть теперь следующим образом:

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- если канал узкий - играем на пробой верхней границы
   if(m_ch_width==CHANNEL_WIDTH_NARROW)
     {
      //--- если задана максимальная цена
      if(m_base_high_price>0.)
         //--- если максимальная цена на уровне верхней границы
         if(m_base_high_price>=m_upper_zone[1])
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
     }
//--- или если канал широкий - играем на отбой от нижней границы
   else if(m_ch_width==CHANNEL_WIDTH_BROAD)
     {
      //--- если задана минимальная цена
      if(m_base_low_price>0.)
         //--- если минимальная цена на уровне нижней границы
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
     }
//---
   return result;
  }
//+------------------------------------------------------------------+

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

Метод проверки возможности продавать — ShortCondition() — создан по аналогии.

Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert3.mqh не изменится.

Для оптимизации есть 2 параметра.

 Переменная СтартШаг
Стоп
Узкий канал, пп
100
20
250
Широкий канал, пп
350
50
1250

Результаты оптимизации находятся в файле ReportOptimizer-signal3.xml. Лучший проход представлен на Рис.8.

Рис.8 Результаты стратегии с учётом ширины канала за 2013-2015 гг.

Рис.8 Результаты стратегии с учётом ширины канала за 2013-2015 гг.


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


3.4 Приграничные уровни стоп-лосса и тейк-профита

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

Я добавил ещё пару моделей для удобства. Теперь они выглядят так:

//--- "веса" рыночных моделей (0-100)
   int               m_pattern_0;         //  Модель "отбой от границы канала"
   int               m_pattern_1;         //  Модель "пробой границы канала"
   int               m_pattern_2;         //  Модель "новый канал"

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

Условие на покупку выглядит следующим образом:

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
   bool is_position=PositionSelect(m_symbol.Name());
//--- если канал узкий - играем на пробой верхней границы
   if(m_ch_width_type==CHANNEL_WIDTH_NARROW)
     {
      //--- если задана максимальная цена
      if(m_base_high_price>0.)
         //--- если максимальная цена на уровне верхней границы
         if(m_base_high_price>=m_upper_zone[1])
           {
            if(IS_PATTERN_USAGE(1))
              {
               result=m_pattern_1;
               //--- если позиции нет
               if(!is_position)
                  //--- в Журнал
                  if(m_to_log)
                    {
                     Print("\nСработала Модель \"пробой границы канала\" на покупку.");
                     PrintFormat("High-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_high_price);
                     PrintFormat("Триггер-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]);
                    }
              }
           }
     }
//--- или если канал широкий или средний - играем на отбой от нижней границы
   else
     {
      //--- если задана минимальная цена
      if(m_base_low_price>0.)
         //--- если минимальная цена на уровне нижней границы
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
              {
               result=m_pattern_0;
               //--- если позиции нет
               if(!is_position)
                  //--- в Журнал
                  if(m_to_log)
                    {
                     Print("\nСработала Модель \"отбой от границы канала\" на покупку.");
                     PrintFormat("Low-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_low_price);
                     PrintFormat("Зона вверх: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[0]);
                     PrintFormat("Зона низ: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]);
                    }
              }
           }
     }
//---
   return result;
  }
//+------------------------------------------------------------------+

Также появилась проверка условия на закрытие:

//+------------------------------------------------------------------+
//| Проверка условия закрытия покупки                                |
//+------------------------------------------------------------------+
bool CSignalEquidChannel::CheckCloseLong(double &price) const
  {
   bool to_close_long=true;
   int result=0;
   if(IS_PATTERN_USAGE(2))
      result=m_pattern_2;
   if(result>=m_threshold_close)
     {
      if(m_is_new_channel)
         //--- если закрывать покупку
         if(to_close_long)
           {
            price=NormalizeDouble(m_symbol.Bid(),m_symbol.Digits());
            //--- в Журнал
            if(m_to_log)
              {
               Print("\nСработала Модель \"новый канал\" для закрытия покупки.");
               PrintFormat("Цена закрытия: %0."+IntegerToString(m_symbol.Digits())+"f",price);
              }
           }
     }
//---
   return to_close_long;
  }
//+------------------------------------------------------------------+
Для короткой позиции условие на закрытие будет идентичным.


Теперь несколько слов о трале. Для него был написан свой класс CTrailingEquidChannel, родителем для которого стал стандартный класс CExpertTrailing.

//+------------------------------------------------------------------+
//| Class CTrailingEquidChannel.                                     |
//| Purpose: Class of trailing stops based on Equidistant Channel.   |
//|              Derives from class CExpertTrailing.                 |
//+------------------------------------------------------------------+
class CTrailingEquidChannel : public CExpertTrailing
  {
protected:
   double            m_sl_distance;       // расстояние для стопа
   double            m_tp_distance;       // расстояние для профита
   double            m_upper_val;         // верхняя граница
   double            m_lower_val;         // нижняя граница
   ENUM_CHANNEL_WIDTH_TYPE m_ch_wid_type; // тип канала по ширине
   //---
public:
   void              CTrailingEquidChannel(void);
   void             ~CTrailingEquidChannel(void){};
   //--- methods of initialization of protected data
   void              SetTradeLevels(double _sl_distance,double _tp_distance);
   //---
   virtual bool      CheckTrailingStopLong(CPositionInfo *position,double &sl,double &tp);
   virtual bool      CheckTrailingStopShort(CPositionInfo *position,double &sl,double &tp);
   //---
   bool              RefreshData(const CSignalEquidChannel *_ptr_ch_signal);
  };
//+------------------------------------------------------------------+

Красным цветом выделен метод, который получает информацию от канального сигнала.

Методы проверки возможности трала короткой и длинной позиций предка были переопределены посредством полиморфизма - базового принципа ООП .

Для того, чтобы класс трала мог получать временные и ценовые ориентиры актуального канала, потребовалось создать связку с сигнальным классом CSignalEquidChannel. Её реализовал константный указатель в составе класса CEquidChannelExpert. Такой подход позволяет получать всю необходимую информацию от сигнала, не подвергая угрозе изменения значений параметров самого сигнала.

//+------------------------------------------------------------------+
//| Класс CEquidChannelExpert.                                       |
//| Цель: Класс для советника, торгующего по равноудалённому каналу. |
//| Потомок класса CExpert.                                          |
//+------------------------------------------------------------------+
class CEquidChannelExpert : public CExpert
  {
   //--- === Data members === --- 
private:
   const CSignalEquidChannel *m_ptr_ch_signal;

   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CEquidChannelExpert(void);
   void             ~CEquidChannelExpert(void);
   //--- указатель на объект сигнала каналов
   void              EquidChannelSignal(const CSignalEquidChannel *_ptr_ch_signal){m_ptr_ch_signal=_ptr_ch_signal;};
   const CSignalEquidChannel *EquidChannelSignal(void) const {return m_ptr_ch_signal;};

protected:
   virtual bool      Processing(void);
   //--- trade close positions check
   virtual bool      CheckClose(void);
   virtual bool      CheckCloseLong(void);
   virtual bool      CheckCloseShort(void);
   //--- trailing stop check
   virtual bool      CheckTrailingStop(void);
   virtual bool      CheckTrailingStopLong(void);
   virtual bool      CheckTrailingStopShort(void);
  };
//+------------------------------------------------------------------+

В классе советника были также переопределены методы, отвечающие за закрытие и трал.

Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert4.mqh выглядит так:

//+------------------------------------------------------------------+
//| Главный модуль                                                   |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- расчёт направления
   m_signal.SetDirection();
//--- если позиции нет
   if(!this.SelectPosition())
     {
      //--- модуль открытия позиции
      if(this.CheckOpen())
         return true;
     }
//--- если есть позиция
   else
     {
      if(!this.CheckClose())
        {
         //--- проверка возможности модификации позиции
         if(this.CheckTrailingStop())
            return true;
         //---
         return false;
        }
      else
        {
         return true;
        }
     }
//--- если нет торговых операций
   return false;
  }
//+------------------------------------------------------------------+
Будем оптимизировать такие параметры:
 Переменная СтартШаг
Стоп
Стоп-лосс, поинт
25
5
75
Тэйк-профит, поинт50
5
200

Результаты оптимизации находятся в файле ReportOptimizer-signal4.xml. Лучший проход представлен на Рис.9.

Рис.9 Результаты стратегии с учётом приграничных уровней за 2013-2015 гг.

Рис.9 Результаты стратегии с учётом приграничных уровней за 2013-2015 гг.


Очевидно, что данный фактор — приграничные ценовые уровни — не способствовали улучшению результативности.


Заключение

В этой статье я постарался представить процесс разработки и реализации класса-сигнальщика на основе скользящих каналов. За каждой из версий сигнала следовала торговая стратегия с результатами тестирования.

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

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


Прикрепленные файлы |
Reports.zip (17.39 KB)
base_signal.set (1.54 KB)
channelstrader.zip (249.84 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (8)
Denis Kirichenko
Denis Kirichenko | 14 сент. 2016 в 15:27
fxsaber:

...на видео строятся каналы в виде двух отрезков. А почему Вы не делаете следующее

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

А в чём цимес?
fxsaber
fxsaber | 14 сент. 2016 в 15:46
Dennis Kirichenko:
А в чём цимес?
Видеть канал на протяжении всей истории.
Denis Kirichenko
Denis Kirichenko | 14 сент. 2016 в 15:52
fxsaber:
Видеть канал на протяжении всей истории.
Чтобы прошлые каналы не пропадали при появлении нового?
fxsaber
fxsaber | 14 сент. 2016 в 15:54
Dennis Kirichenko:
Чтобы прошлые каналы не пропадали при появлении нового?
Чтобы видеть на истории, где бы по краям канала располагались отложенники, в случае его торговли.
Denis Kirichenko
Denis Kirichenko | 14 сент. 2016 в 15:56
fxsaber:
Чтобы видеть на истории, где бы по краям канала располагались отложенники, в случае его торговли.
Ну да, можно усложнить это дело, согласен. Правда в моих примерах отложек не было :-))
Портфельная торговля в MetaTrader 4 Портфельная торговля в MetaTrader 4
В статье обсуждаются принципы портфельной торговли и особенности применения к валютному рынку. Рассматриваются несколько простых математических моделей для формирования портфеля. Приводятся примеры практической реализации портфельной торговли в MetaTrader 4: портфельный индикатор и советник для полуавтоматической торговли. Описываются элементы торговых стратегий, их достоинства и "подводные камни".
Работа с корзинами валютных пар на рынке Форекс Работа с корзинами валютных пар на рынке Форекс
В статье рассматриваются вопросы о том, как разбить валютные пары по группам - корзинам; как получить данные о состоянии таких корзин (например, перекупленности и перепроданности); какие индикаторы могут предоставить такие данные; наконец, о том, как можно применить полученную информацию в практическом трейдинге.
Сравнение MQL5 и QLUA - почему торговые операции в MQL5 до 28 раз быстрее? Сравнение MQL5 и QLUA - почему торговые операции в MQL5 до 28 раз быстрее?
Многие трейдеры зачастую не задумываются над тем, как быстро доходит их заявка до биржи, как долго она там исполняется, и когда наконец-то торговый терминал трейдера узнает о результате торговой операции. Мы обещали дать сравнение скорости торговых операций, ведь никто до нас не делал таких замеров с помощью программ на MQL5 и QLUA.
Кроссплатформенный торговый советник: повторное использование компонентов из Стандартной библиотеки MQL5 Кроссплатформенный торговый советник: повторное использование компонентов из Стандартной библиотеки MQL5
В Стандартной библиотеке MQL5 есть некоторые компоненты, которые могут оказаться полезными в версиях кроссплатформенных торговых экспертов для MQL4. В этой статье рассматривается метод создания некоторых компонентов Стандартной библиотеки MQL5, совместимых с компилятором MQL4.