Рецепты MQL5 - Торговые сигналы пивотов

Denis Kirichenko | 6 марта, 2017


Введение

Текущая статья продолжает серию, в которой описываются индикаторы и сетапы, генерирующие торговые сигналы. На этот раз рассмотрим пивоты — разворотные уровни (точки). Снова будет задействована Стандартная библиотека. Сначала поработаем с индикатором разворотных уровней, а затем на его основе создадим базовую стратегию, которую будем дорабатывать.

Желательно, чтобы читатель был знаком с базовым классом для создания генераторов торговых сигналов CExpertSignal.


1. Индикатор пивотов — уровней разворота

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

Подсчитывать уровни можно несколькими различными способами. Более подробно об этом можно узнать в статье "Стратегия торговли, основанная на Анализе Точек Вращения (Pivot Points)".

Остановимся пока на классическом подходе, где уровни определяются по таким формулам:



RES — это i-ый уровень сопротивления, а SUP — i-ый уровень поддержки. Всего будет: 1 основной разворотный уровень (PP), 6 уровней сопротивления (RES) и 6 уровней поддержки (SUP).

Итак, визуально индикатор выглядит как набор горизонтальных уровней, строящихся по разным ценам. Первый запуск индикатора на график построит уровни только для текущего дня (рис.1).


Рис.1 Индикатор пивотов: отрисовка для текущего дня

Рис. 1 Индикатор пивотов: отрисовка для текущего дня


Рассмотрим код индикатора поблочно, начнем с расчетной части.

Итак, когда начинается новый день, нужно пересчитать все разворотные уровни.

//--- если есть новый день
   if(gNewDay.isNewBar(today))
     {
      PrintFormat("Новый день: %s",TimeToString(today));
      //--- нормализация цен
      double d_high=NormalizeDouble(daily_rates[0].high,_Digits);
      double d_low=NormalizeDouble(daily_rates[0].low,_Digits);
      double d_close=NormalizeDouble(daily_rates[0].close,_Digits);
      //--- запомнить цены
      gYesterdayHigh=d_high;
      gYesterdayLow=d_low;
      gYesterdayClose=d_close;
      //--- 1) пивот: PP = (HIGH + LOW + CLOSE) / 3        
      gPivotVal=NormalizeDouble((gYesterdayHigh+gYesterdayLow+gYesterdayClose)/3.,_Digits);
      //--- 4) RES1.0 = 2*PP - LOW
      gResVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayLow,_Digits);
      //--- 5) SUP1.0 = 2*PP – HIGH
      gSupVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayHigh,_Digits);
      //--- 8) RES2.0 = PP + (HIGH -LOW)
      gResVal_2_0=NormalizeDouble(gPivotVal+(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 9) SUP2.0 = PP - (HIGH – LOW)
      gSupVal_2_0=NormalizeDouble(gPivotVal-(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 12) RES3.0 = 2*PP + (HIGH – 2*LOW)
      gResVal_3_0=NormalizeDouble(2.*gPivotVal+(gYesterdayHigh-2.*gYesterdayLow),_Digits);
      //--- 13) SUP3.0 = 2*PP - (2*HIGH – LOW)
      gSupVal_3_0=NormalizeDouble(2.*gPivotVal-(2.*gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 2) RES0.5 = (PP + RES1.0) / 2
      gResVal_0_5=NormalizeDouble((gPivotVal+gResVal_1_0)/2.,_Digits);
      //--- 3) SUP0.5 = (PP + SUP1.0) / 2
      gSupVal_0_5=NormalizeDouble((gPivotVal+gSupVal_1_0)/2.,_Digits);
      //--- 6) RES1.5 = (RES1.0 + RES2.0) / 2
      gResVal_1_5=NormalizeDouble((gResVal_1_0+gResVal_2_0)/2.,_Digits);
      //--- 7) SUP1.5 = (SUP1.0 + SUP2.0) / 2
      gSupVal_1_5=NormalizeDouble((gSupVal_1_0+gSupVal_2_0)/2.,_Digits);
      //--- 10) RES2.5 = (RES2.0 + RES3.0) / 2
      gResVal_2_5=NormalizeDouble((gResVal_2_0+gResVal_3_0)/2.,_Digits);
      //--- 11) SUP2.5 = (SUP2.0 + SUP3.0) / 2
      gSupVal_2_5=NormalizeDouble((gSupVal_2_0+gSupVal_3_0)/2.,_Digits);

      //--- бар начала текущего дня
      gDayStart=today;
      //--- найти стартовый бар активного ТФ
      //--- как тайм-серия
      for(int bar=0;bar<rates_total;bar++)
        {
         //--- время выбранного бара
         datetime curr_bar_time=time[bar];
         user_date.DateTime(curr_bar_time);
         //--- день выбранного бара
         datetime curr_bar_time_of_day=user_date.DateOfDay();
         //--- если текущий бар был днём ранее
         if(curr_bar_time_of_day<gDayStart)
           {
            //--- зафиксировать стартовый бар
            gBarStart=bar-1;
            break;
           }
        }
      //--- сбросить локальный счётчик
      prev_calc=0;
     }

Красным я выделил строки, где именно и пересчитываются уровни. Затем нужно найти номер бара для текущего таймфрейма, который будет началом для отрисовки уровней. За его значение отвечает переменная gBarStart. При поиске задействуется пользовательская структура SUserDateTime для работы с датами и временем — потомок структуры CDateTime.

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

//--- если новый бар на активном ТФ
   if(gNewMinute.isNewBar(time[0]))
     {
      //--- по какой бар считать
      int bar_limit=gBarStart;
      //--- если не первый запуск
      if(prev_calc>0)
         bar_limit=rates_total-prev_calc;

      //--- обсчёт буферов
      for(int bar=0;bar<=bar_limit;bar++)
        {
         //--- 1) пивот
         gBuffers[0].data[bar]=gPivotVal;
         //--- 2) RES0.5
         if(gToPlotBuffer[1])
            gBuffers[1].data[bar]=gResVal_0_5;
         //--- 3) SUP0.5
         if(gToPlotBuffer[2])
            gBuffers[2].data[bar]=gSupVal_0_5;
         //--- 4) RES1.0
         if(gToPlotBuffer[3])
            gBuffers[3].data[bar]=gResVal_1_0;
         //--- 5) SUP1.0
         if(gToPlotBuffer[4])
            gBuffers[4].data[bar]=gSupVal_1_0;
         //--- 6) RES1.5
         if(gToPlotBuffer[5])
            gBuffers[5].data[bar]=gResVal_1_5;
         //--- 7) SUP1.5
         if(gToPlotBuffer[6])
            gBuffers[6].data[bar]=gSupVal_1_5;
         //--- 8) RES2.0
         if(gToPlotBuffer[7])
            gBuffers[7].data[bar]=gResVal_2_0;
         //--- 9) SUP2.0
         if(gToPlotBuffer[8])
            gBuffers[8].data[bar]=gSupVal_2_0;
         //--- 10) RES2.5
         if(gToPlotBuffer[9])
            gBuffers[9].data[bar]=gResVal_2_5;
         //--- 11) SUP2.5
         if(gToPlotBuffer[10])
            gBuffers[10].data[bar]=gSupVal_2_5;
         //--- 12) RES3.0
         if(gToPlotBuffer[11])
            gBuffers[11].data[bar]=gResVal_3_0;
         //--- 13) SUP3.0
         if(gToPlotBuffer[12])
            gBuffers[12].data[bar]=gSupVal_3_0;
        }
     }

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

Полный код индикатора пивотов находится в файле Pivots.mq5.


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

На основе описанного индикатора попробуем создать несложную базовую стратегию. Пусть сигнал на открытие будет зависеть от расположения цены открытия дня относительно центрального пивота. Подтверждением для сигнала будет служить касание ценой уровня пивота.

Так, на графике EURUSD, M15 ( Рис.2 ) отображается ситуация, когда день (14 января 2015 г.) открылся ниже центрального пивота, а потом внутри дня цена коснулась его уровня снизу вверх. Таким образом, появился сигнал на продажу. Закрываться, если не сработал ни стоп-лосс, ни тейк-профит, будем при наступлении нового дня.

Рис.2 Базовая стратегия: сигнал на продажу

Рис. 2 Базовая стратегия: сигнал на продажу


Уровни стопа и профита будем привязывать к разворотным уровням пивотного индикатора. Для продажи стопом станет промежуточный уровень сопротивления Res0.5, который расположен на $1.18153. Стопом для профита выступит основной уровень поддержки Sup1.0 на $1.17301. Чуть позже вернёмся к торговому дню 14 января. А пока поговорим о коде, который ляжет в алгоритмическую основу базовой стратегии.


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

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

//+------------------------------------------------------------------+
//| Class CSignalPivots                                              |
//| Purpose: Класс торговых сигналов на основе пивотов.              |
//| Потомок класса CExpertSignal.                                    |
//+------------------------------------------------------------------+
class CSignalPivots : public CExpertSignal
  {
   //--- === Data members === ---
protected:
   CiCustom          m_pivots;            // объект-индикатор "Pivots"  
   //--- настраиваемые параметры
   bool              m_to_plot_minor;     // отрисовка второстепенных уровней
   double            m_pnt_near;          // допуск
   //--- расчётные
   double            m_pivot_val;         // значение пивота
   double            m_daily_open_pr;     // цена открытия текущего дня  
   CisNewBar         m_day_new_bar;       // новый бар дневного ТФ

   //--- рыночные модели  
   //--- 1) Модель 0 "первое касание уровня PP" (сверху - buy, снизу - sell)
   int               m_pattern_0;         // вес
   bool              m_pattern_0_done;    // признак отработанной модели

   //--- === Methods === ---
public:
   //--- конструктор/деструктор
   void              CSignalPivots(void);
   void             ~CSignalPivots(void){};
   //--- методы установки настраиваемых параметров
   void              ToPlotMinor(const bool _to_plot) {m_to_plot_minor=_to_plot;}
   void              PointsNear(const uint _near_pips);
   //--- методы настраивания "весов" рыночных моделей
   void              Pattern_0(int _val) {m_pattern_0=_val;m_pattern_0_done=false;}
   //--- метод проверки настроек
   virtual bool      ValidationSettings(void);
   //--- метод создания индикатора и таймсерий
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- методы проверки, если модели рынка сформированы
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);
   virtual double    Direction(void);
   //--- методы определения уровней входа в рынок
   virtual bool      OpenLongParams(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      OpenShortParams(double &price,double &sl,double &tp,datetime &expiration);
   //---
protected:
   //--- метод инициализации индикатора
   bool              InitCustomIndicator(CIndicators *indicators);
   //--- получение значения уровня пивота
   double            Pivot(void) {return(m_pivots.GetData(0,0));}
   //--- получение значения основного уровня сопротивления
   double            MajorResistance(uint _ind);
   //--- получение значения второстепенного уровня сопротивления
   double            MinorResistance(uint _ind);
   //--- получение значения основного уровня поддержки
   double            MajorSupport(uint _ind);
   //--- получение значения второстепенного уровня поддержки
   double            MinorSupport(uint _ind);
  };
//+------------------------------------------------------------------+


В статье "Рецепты MQL5 - Торговые сигналы скользящих каналов" я уже использовал подход, когда касание ценой какой-то линии фиксируется, если цена попадает в окрестность этой линии. Допуск для разворотного уровня будет задавать член-данные m_pnt_near.

Пожалуй, самую важную роль в классе нужно отвести той сигнальной модели, которую обслуживает класс. В базовом классе будет одна модель. Кроме веса (m_pattern_0), она будет ещё иметь признак отработки в течение торгового дня (m_pattern_0_done).

Базовый сигнальный класс CExpertSignal богат на виртуальные методы. И это богатство позволяет реализовать тонкую настройку производного класса.

В частности, для расчёта торговых уровней я переопределил 2 метода OpenLongParams() и OpenShortParams().

Посмотрим на код первого метода — определение значений для торговых уровней при покупке.

//+------------------------------------------------------------------+
//| Определение торговые уровни для покупки                          |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
        {
         //--- цена открытия - по рынку
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- sl-цена - уровень Sup0.5
         sl=this.MinorSupport(0);

         if(sl==DBL_MAX)
            return false;
         //--- если задана sl-цена
         sl=m_symbol.NormalizePrice(sl);
         //--- tp-цена - уровень Res1.0        
         tp=this.MajorResistance(0);

         if(tp==DBL_MAX)
            return false;
         //--- если задана tp-цена
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- если цены заданы
         params_set=true;
         //--- модель отработана
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
//+------------------------------------------------------------------+


Цена стоп-лосса расчитывается как значение первого второстепенного уровня поддержки с помощью метода MinorSupport(). Профит устанавливаем по цене первого основного уровня сопротивления с помощью метода MajorResistance(). Для продажи эти методы поменяются, соответственно, на MinorResistance() и MajorSupport().

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

//+------------------------------------------------------------------+
//| Detecting the levels for buying                                  |
//+------------------------------------------------------------------+
bool CExpertSignal::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL;
//---
   if(general==NULL)
     {
      //--- if a base price is not specified explicitly, take the current market price
      double base_price=(m_base_price==0.0) ? m_symbol.Ask() : m_base_price;
      price      =m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
      sl         =(m_stop_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price-m_stop_level*PriceLevelUnit());
      tp         =(m_take_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price+m_take_level*PriceLevelUnit());
      expiration+=m_expiration*PeriodSeconds(m_period);
      return(true);
     }
//---
   return(general.OpenLongParams(price,sl,tp,expiration));
  }
//+------------------------------------------------------------------+

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

//--- фильтр CSignalPivots
   CSignalPivots *filter0=new CSignalPivots;
   if(filter0==NULL)
     {
      //--- ошибка
      PrintFormat(__FUNCTION__+": error creating filter0");
      return INIT_FAILED;
     }
   signal.AddFilter(filter0);
   signal.General(0);  


Метод проверки условия на покупку представлен так:

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
         //--- если день открылся выше пивота
         if(m_daily_open_pr>m_pivot_val)
           {
            //--- минимальная цена на текущем баре
            double last_low=m_low.GetData(1);
            //--- если цена получена
            if((last_low>WRONG_VALUE) && (last_low<DBL_MAX))
               //--- если было касание сверху (с учётом допуска)
               if(last_low<=(m_pivot_val+m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- в Журнал
                  Print("\n---== Касание ценой уровня пивота сверху ==---");
                  PrintFormat("Цена: %0."+IntegerToString(m_symbol.Digits())+"f",last_low);
                  PrintFormat("Пивот: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("Допуск: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
//+------------------------------------------------------------------+

Легко заметить, что касание сверху проверяется с учётом допуска last_low<=(m_pivot_val+m_pnt_near).

Метод определения "взвешенного" направления Direction(), кроме всего прочего, проверяет отработку базовой модели.

//+------------------------------------------------------------------+
//| Определение "взвешенного" направления                            |
//+------------------------------------------------------------------+
double CSignalPivots::Direction(void)
  {
   double result=0.;
//--- получить дневные исторические данные
   MqlRates daily_rates[];
   if(CopyRates(_Symbol,PERIOD_D1,0,1,daily_rates)<0)
      return 0.;
//--- если Модель 0 отработана
   if(m_pattern_0_done)
     {
      //--- проверить появление нового дня
      if(m_day_new_bar.isNewBar(daily_rates[0].time))
        {
         //--- сбросить флаг отработки модели
         m_pattern_0_done=false;
         return 0.;
        }
     }
//--- если Модель 0 не отработана
   else
     {
      //--- цена открытия дня
      if(m_daily_open_pr!=daily_rates[0].open)
         m_daily_open_pr=daily_rates[0].open;
      //--- пивот
      double curr_pivot_val=this.Pivot();
      if(curr_pivot_val<DBL_MAX)
         if(m_pivot_val!=curr_pivot_val)
            m_pivot_val=curr_pivot_val;
     }

//--- результат
   result=m_weight*(this.LongCondition()-this.ShortCondition());
//---
   return result;
  }
//+------------------------------------------------------------------+


Что касается сигналов для закрытия, то переопределим методы родительского класса CloseLongParams() и CloseShortParams(). Пример кода для блока покупки:

//+------------------------------------------------------------------+
//| Определение торгового уровня для покупки                         |
//+------------------------------------------------------------------+
bool CSignalPivots::CloseLongParams(double &price)
  {
   price=0.;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
        {
         price=m_symbol.Bid();
         //--- в Журнал
         Print("\n---== Сигнал на закрытие покупки ==---");
         PrintFormat("Рыночная цена: %0."+IntegerToString(m_symbol.Digits())+"f",price);
         return true;
        }
//--- return the result
   return false;
  }
//+------------------------------------------------------------------+

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

signal.ThresholdClose(0);

Тогда в родительском классе проверки по направлению не будет.

//+------------------------------------------------------------------+
//| Generating a signal for closing of a long position               |
//+------------------------------------------------------------------+
bool CExpertSignal::CheckCloseLong(double &price)
  {
   bool   result   =false;
//--- the "prohibition" signal
   if(m_direction==EMPTY_VALUE)
      return(false);
//--- check of exceeding the threshold value
   if(-m_direction>=m_threshold_close)
     {
      //--- there's a signal
      result=true;
      //--- try to get the level of closing
      if(!CloseLongParams(price))
         result=false;
     }
//--- zeroize the base price
   m_base_price=0.0;
//--- return the result
   return(result);
  }
//+------------------------------------------------------------------+

Возникает вопрос: за счёт чего в таком случае будет проверяться сигнал на закрытие? Он будет проверяться, во-первых, за счёт наличия позиции (в методе Processing() ), а во-вторых, за счёт признака m_pattern_0_done (в переопределённых методах CloseLongParams() и CloseShortParams() ). В общем, как только эксперт выявляет наличие позиции при неотработанной Модели 0, то он сразу пытается закрыть позицию. Происходит это в начале торгового дня.

Мы рассмотрели основы пользовательского сигнального класса CSignalPivots, теперь поговорим о классе стратегии.


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

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

Главный метод-обработчик выглядит так:

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

Вот и всё. Теперь мы можем запускать базовую стратегию. Её код представлен в файле BasePivotsTrader.mq5.


Рис.3 Базовая стратегия: продажа

Рис. 3 Базовая стратегия: продажа


Вернёмся к примеру 14 января 2015 г. В данном случае модель была отработана просто идеально. Мы открылись вниз на пивоте, а закрылись на уровне основной поддержки Sup1.0.

Был сделан прогон в Тестере стратегий с 07.01.2013 по 07.01.2017 на пятнадцатиминутном таймфрейме по символу EURUSD со следующими значениями параметров:

Как оказалось, стратегия торгует с устойчивым результатом. Правда, результат этот отрицательный (Рис.4).

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

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


Судя по графику результативности, мы всё делали неправильно. Нужно было покупать, когда поступал сигнал на продажу, и продавать — при сигнале на покупку. Так ли это? Попробуем проверить. Для этого создадим вторую базовую стратегию.

В неё внесём изменения в части сигналов. Тогда, к примеру, условие на покупку будет таким:

//+------------------------------------------------------------------+
//| Проверка условия на продажу                                      |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
         //--- если день открылся ниже пивота
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- максимальная цена на текущем баре
            double last_high=m_high.GetData(1);
            //--- если цена получена
            if((last_high>WRONG_VALUE) && (last_high<DBL_MAX))
               //--- если было касание снизу (с учётом допуска)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- в Журнал
                  Print("\n---== Касание ценой уровня пивота снизу ==---");
                  PrintFormat("Цена: %0."+IntegerToString(m_symbol.Digits())+"f",last_high);
                  PrintFormat("Пивот: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("Допуск: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
//+------------------------------------------------------------------+

Снова прогоним стратегию в Тестере и получим такой результат:

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

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

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

Попробуем снова изменить базовую стратегию, а именно — её вторую версию. Сделаем так, чтобы для покупки уровень стоп-лосса выставлялся подальше — до основной поддержки Sup1.0, а размер профита ограничивался промежуточным уровнем сопротивления Res0.5. Для продажи стоп поставим на уровень Res1.0, а профит — на Sup0.5.

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

//+------------------------------------------------------------------+
//| Определение торговых уровней для покупки                         |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
        {
         //--- цена открытия — по рынку
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- sl-цена - уровень Sup1.0
         sl=this.MajorSupport(0);

         if(sl==DBL_MAX)
            return false;
         //--- если задана sl-цена
         sl=m_symbol.NormalizePrice(sl);
         //--- tp-цена - уровень Res0.5        
         tp=this.MinorResistance(0);

         if(tp==DBL_MAX)
            return false;
         //--- если задана tp-цена
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- если цены заданы
         params_set=true;
         //--- модель отработана
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
//+------------------------------------------------------------------+


Получим в Тестере такой результат для третьей версии:

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

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


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


3. К вопросу о робастности

Если внимательно присмотреться к Рис.6, то легко заметить, что кривая баланса росла неравномерно. Были участки, где баланс стабильно накапливал прибыль. Были участки просадки.  А также были и участки, где кривая баланса двигалась вправо, строго на восток.

Робастность (robustness) – это устойчивость торговой системы, которая свидетельствует об относительном постоянстве и эффективности системы на длительном промежутке времени.

В целом, можно сказать, что стратегии не хватает робастности. Можно ли её повысить? Попробуем.


3.1 Индикатор тренда

На мой взгляд, описанные выше торговые правила лучше работают, когда на рынке есть направленное движение — тренд. Так, стратегия показывала самый хороший результат на EURUSD в 2014 — начале 2015 гг., когда пара устойчиво снижалась.

Выходит, что нужен какой-то фильтр, позволяющий избегать бокового движения. На тему определения устойчивого тренда существует масса материалов, в том числе и здесь, в разделе "Статьи" на mql5.com. Лично мне больше всего понравилась статья "Несколько способов определения тренда на MQL5". В ней автор предлагает удобный и, что важнее, универсальный способ поиска тренда.

Я создал аналогичный индикатор MaTrendCatcher. В нём берутся два мувинга — быстрый и медленный. Если разница между значениями положительна, то считаем, что тренд бычий. Тогда в индикаторе столбики гистограммы будут равны 1. Если такая разница будет отрицательна, значит, на рынке царит медвежий тренд. Столбики будут равны минус 1 (Рис.7.).


Рис.7  Индикатор тренда MaTrendCatcher

Рис. 7  Индикатор тренда MaTrendCatcher


Кроме того, если относительно прошлого бара разница между мувингами растёт (тренд усиливается), то столбик будет зелёный, если снижается — красный.

И ещё одна возможность, добавленная в индикатор: там, где разность между МА небольшая, столбики не будут отображаться. Насколько небольшой должна быть эта разница, чтобы скрыть ее визуальное отображение — зависит от настраиваемого параметра индикатора "Отсечка, пп" (Рис.8).


Рис.8  Индикатор тренда MaTrendCatcher, отсечка небольших разниц

Рис. 8  Индикатор тренда MaTrendCatcher, отсечка небольших разниц


Итак, для целей фильтрации будем использовать индикатор MaTrendCatcher.

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

Для целей стратегии нужно было получить рассчитанную величину "взвешенного" направления. Поэтому пришлось создать пользовательский класс-потомок от базового сигнального класса.

class CExpertUserSignal : public CExpertSignal

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

По сути своей, она дополняет Модель 0. Поэтому её можно назвать субмоделью. Чуть позже в коде отметим этот момент.

Теперь метод проверки условия на покупку выглядит так:

//+------------------------------------------------------------------+
//| Проверка условия на покупку                                      |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- если Модель 0 учитывается
   if(IS_PATTERN_USAGE(0))
      //--- если Модель 0 не отработана
      if(!m_pattern_0_done)
        {
         m_is_signal=false;
         //--- если день открылся ниже пивота
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- максимальная цена на прошлом баре
            double last_high=m_high.GetData(1);
            //--- если цена получена
            if(last_high>WRONG_VALUE && last_high<DBL_MAX)
               //--- если было касание снизу (с учётом допуска)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  m_is_signal=true;
                  //--- в Журнал
                  this.Print(last_high,ORDER_TYPE_BUY);
                 }
           }
         //--- если Модель 1 учитывается
         if(IS_PATTERN_USAGE(1))
           {
            //--- если на прошлом баре был бычий тренд
            if(m_trend_val>0. && m_trend_val!=EMPTY_VALUE)
              {
               //--- если есть ускорение
               if(m_trend_color==0. && m_trend_color!=EMPTY_VALUE)
                  result+=(m_pattern_1+m_speedup_allowance);
               //--- если нет ускорения
               else
                  result+=(m_pattern_1-m_speedup_allowance);
              }
           }

        }
//---
   return result;
  }

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

Смысл расчёта такой: если вход в рынок был выполнен без учёта субмодели, то результат сигнала будет равен весу Модели 0. Если же субмодель учитывается, то возможны следующие варианты:

  1. вход по тренду с ускорением (премия за тренд и премия за ускорение);
  2. вход по тренду без ускорения (премия за тренд и штраф за ускорение);
  3. вход против тренда с ускорением (штраф за контртренд и штраф за ускорение);
  4. вход против тренда без ускорения (штраф за контртренд и премия за ускорение).

Такой подход позволит не реагировать на слабый сигнал. А если вес сигнала преодолевает пороговое значение, то он влияет на размер торгового объёма. В классе советника по пивотам есть метод CPivotsExpert::LotCoefficient():

//+------------------------------------------------------------------+
//| Коэффициент лота                                                 |
//+------------------------------------------------------------------+
double CPivotsExpert::LotCoefficient(void)
  {
   double lot_coeff=1.;
//--- общий сигнал
   CExpertUserSignal *ptr_signal=this.Signal();
   if(CheckPointer(ptr_signal)==POINTER_DYNAMIC)
     {
      double dir_val=ptr_signal.GetDirection();
      lot_coeff=NormalizeDouble(MathAbs(dir_val/100.),2);
     }
//---
   return lot_coeff;
  }
//+------------------------------------------------------------------+

Так, если сигнал собрал 120 баллов, то начальный объём скорректируется на 1,2, а если 70 — на 0,7.

Чтобы применить коэффициент, нужно ещё переопределить методы OpenLong() и OpenShort(). В частности, метод для покупки представлен так:

//+------------------------------------------------------------------+
//| Long position open or limit/stop order set                       |
//+------------------------------------------------------------------+
bool CPivotsExpert::OpenLong(double price,double sl,double tp)
  {
   if(price==EMPTY_VALUE)
      return(false);
//--- get lot for open
   double lot_coeff=this.LotCoefficient();
   double lot=LotOpenLong(price,sl);
   lot=this.NormalLot(lot_coeff*lot);
//--- check lot for open
   lot=LotCheck(lot,price,ORDER_TYPE_BUY);
   if(lot==0.0)
      return(false);
//---
   return(m_trade.Buy(lot,price,sl,tp));
  }
//+------------------------------------------------------------------+

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


3.2 Размер диапазона

Также несложно заметить, что когда разворотные уровни (пивоты) расположены близко друг от друга, то это свидетельствует о невысокой волатильности на рынке. Чтобы воздержаться от торговли в такие дни, введён такой параметр, как "Лимит по ширине, пп". Модель 0 (а вместе с ней и субмодель) будет считаться отработанной, если порог лимита не был преодолён. Проверяется этот лимит в теле метода Direction(). Приведу часть кода:

//--- если задан лимит
   if(m_wid_limit>0.)
     {
      //--- расчётный верхний лимит
      double norm_upper_limit=m_symbol.NormalizePrice(m_wid_limit+m_pivot_val);
      //--- фактический верхний лимит
      double res1_val=this.MajorResistance(0);
      if(res1_val>WRONG_VALUE && res1_val<DBL_MAX)
        {
         //--- если лимит не преодолён
         if(res1_val<norm_upper_limit)
           {
            //--- Модель 0 отработана
            m_pattern_0_done=true;
            //--- в Журнал
            Print("\n---== Не преодолён верхний лимит ==---");
            PrintFormat("Расчётный: %0."+IntegerToString(m_symbol.Digits())+"f",norm_upper_limit);
            PrintFormat("Фактический: %0."+IntegerToString(m_symbol.Digits())+"f",res1_val);
            //---
            return 0.;
           }
        }
      //--- расчётный нижний лимит
      double norm_lower_limit=m_symbol.NormalizePrice(m_pivot_val-m_wid_limit);
      //--- фактический нижний лимит
      double sup1_val=this.MajorSupport(0);
      if(sup1_val>WRONG_VALUE && sup1_val<DBL_MAX)
        {
         //--- если лимит не преодолён
         if(norm_lower_limit<sup1_val)
           {
            //--- Модель 0 отработана
            m_pattern_0_done=true;
            //--- в Журнал
            Print("\n---== Не преодолён нижний лимит ==---");
            PrintFormat("Расчётный: %0."+IntegerToString(m_symbol.Digits())+"f",norm_lower_limit);
            PrintFormat("Фактический: %0."+IntegerToString(m_symbol.Digits())+"f",sup1_val);
            //---
            return 0.;
           }
        }
     }

И если проверку по ширине диапазона сигнал не прошёл, то в журнале появится, к примеру, такая запись:

2015.08.19 00:01:00   ---== Не преодолён верхний лимит ==---
2015.08.19 00:01:00   Расчётный: 1.10745
2015.08.19 00:01:00   Фактический: 1.10719
Т.е., в данном случае, чтобы стать полноценным, сигналу не хватило 26 пп.


Запустим стратегию в Тестере в режиме оптимизации. Я использовал следующие оптимизационные параметры:

  1. "Лимит по ширине, пп";
  2. "Допуск, пп";
  3. "Быстрая МА";
  4. "Медленная МА";
  5. "Отсечка, пп".

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

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

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

По большому счёту, чего и следовало ожидать, были отсеяны некоторые сигналы. Кривая баланса стала более плавной.

Но стоит сказать и о том, что не удалось. Стратегия, как это видно на графике, начиная с 2015 г., генерирует участки, на которых кривая баланса колеблется в узком диапазоне без явного роста прибыли. Результаты оптимизации находятся в файле EURUSD_model.xml.

Посмотрим на результаты на других инструментах.

Так, на паре USDJPY самый лучший проход отображён на Рис.10.

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

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

Теперь посмотрим на спотовое золото‌. Самый лучший результат представлен на Рис.11.

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

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

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

‌Что касается британского фунта, то самый лучший из проходов представлен на Рис.12.

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

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

Фунт неплохо торговался по тренду . Но коррекция в начале 2015 г. подпортила итоговый результат.

‌В общем и целом, стратегия лучше всего  работает тогда, когда на рынке есть направленное движение.

Заключение

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

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


Расположение файлов

Расположение файлов

Удобнее всего расположить файлы со стратегиями в одной папке Pivots. Файлы индикаторов (Pivots.ex5 и MaTrendCatcher.ex5) после компиляции нужно перенести в папку индикаторов %MQL5\Indicators.