Разворотные паттерны: Тестируем паттерн "Двойная вершина/дно"

Dmitriy Gizlyk | 30 октября, 2018

Содержание

Введение

Анализ, проведенный в статье "Сколько длится тренд?", демонстрирует, что около 60% ценовое движение находится в тренде. И именно открытие позиции в начале тренда дает возможность получить максимальный результат. Поиски точек разворотов трендов породили большое количество разворотных паттернов. Одним из наиболее известных и часто применяемых паттернов является двойная вершина/дно. 

1. Теоретические аспекты формирования паттерна

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

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

Паттерн Двойная вершина

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


2. Стратегия торговли по паттерну

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

2.1. Сценарий 1

Первая точка входа основана на пробитии линии шеи. Стоп-лосс выставляется за линией вершин / впадин. При этом существуют различные подходы для определения "пробития лини шеи". Здесь может использоваться как закрытие бара под линией шеи, так и преодоление линии шеи на фиксированное расстояние. Оба подхода имеют свои плюсы и минусы. При резком движении закрытие свечи может произойти на достаточном расстоянии от линии шеи, что сделает неэффективным использование паттерна.

Первая точка входа

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

2.2. Сценарий 2

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

Вторая точка входа 


2.3. Сценарий 3

Третья точка входа основана на трендовой теории. И определяется по пробитию трендовой линии, построенной от точки начала движения до экстремума на линии шеи. Стоп-лосс, как и в первом случае, выставляется за линией вершин/впадин. Ранний вход дает меньший уровень стоп-лосса по сравнению с первой точкой входа. А также дает больше сигналов по сравнению со вторым сценарием. В тоже время, такая точка входа дает больше ложных сигналов, так как возможно формирование канала между линиями экстремумом и шеи или паттерна вымпел, которые являются фигурами продолжения тренда.

Третья точка входа. 


Во всех трех стратегиях предлагается выход на уровне равном расстоянию от экстремумом до линии шеи.

Тейк-профит

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

Более того, так как формирование паттерна основано на теории ценовых уровней, то и торговля по паттерну, как минимум, не должна ей противоречить. Поэтому, исходя из предполагаемой цели, линия шеи должна быть не ниже фибо уровня 50 начального движения. Кроме того, с целью отфильтровать ложные сигналы мы можем добавить минимальный уровень первой коррекции (формирующей линию шеи), как показатель силы ценового уровня.


3. Создаем советник

3.1. Поиск экстремумов

Создание торгового советника начнем с создания блока поиска паттерна. Для поиска экстремумом на графике воспользуемся индикатором зиг-заг из коробочной поставки MetaTrader 5. Перенесем расчетную часть индикатора в класс по технологии, описанной в статье [1]. Как известно, данный индикатор содержит два индикаторных буфера, содержащих значение цены в точках экстремумом. Между экстремумами индикаторные буферы содержат пустые значения. Чтобы не создавать два индикаторных буфера, содержащих множество пустых значений, они были заменены массивом структур, содержащих информацию об экстремуме. Структура для хранения информации об экстремуме имеет нижеследующий вид.

   struct s_Extremum
     {
      datetime          TimeStartBar;
      double            Price;
      
      s_Extremum(void)  :  TimeStartBar(0),
                           Price(0)
         {
         }
      void Clear(void)
        {
         TimeStartBar=0;
         Price=0;
        }
     };

Я думаю, каждый, кто хотя бы раз пользовался индикатором зиг-заг, знает на сколько компромиссов придется пойти при поиске оптимальных параметров. Малые значения параметров ведут к разбиению одного большого движения на мелкие части. И наоборот, слишком большие значения параметров приведут к пропуску коротких движений. Алгоритм поиска графических паттернов очень требовательный к качеству нахождения экстремумов. В попытках "совместить несовместимое" было принято решение использовать индикатор с малыми значениями параметров, при этом создать дополнительную надстройку, которая будет объединять однонаправленные движения с короткими коррекциями в одно движение.

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

class CTrends : public CObject
  {
private:
   CZigZag          *C_ZigZag;         // Ссылка на объект индикатора ZigZag
   s_Extremum        Trends[];         // Массив экстремумов
   int               i_total;          // Общее количество сохраненных эктсремумов
   double            d_MinCorrection;  // Минимальный размер движения для продолжения тренда

public:
                     CTrends();
                    ~CTrends();
//--- Метод инициализации класса
   virtual bool      Create(CZigZag *pointer, double min_correction);
//--- Получение информации об экстремуме
   virtual bool      IsHigh(s_Extremum &pointer) const;
   virtual bool      Extremum(s_Extremum &pointer, const int position=0);
   virtual int       ExtremumByTime(datetime time);
//--- Получение общей информации
   virtual int       Total(void)          {  Calculate(); return i_total;   }
   virtual string    Symbol(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return "Not Initilized"; return C_ZigZag.Symbol();  }
   virtual ENUM_TIMEFRAMES Timeframe(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return PERIOD_CURRENT; return C_ZigZag.Timeframe();  }
   
protected:
   virtual bool      Calculate(void);
   virtual bool      AddTrendPoint(s_Extremum &pointer);
  };

Для получения информации об экстремумах в классе предусмотрены методы:

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

Основная логика класса реализована в методе Calculate. Давайте рассмотрим его подробнее.

В начале метода проверим актуальность ссылки на объект индикаторного класса и наличие экстремумов, найденных индикатором.

bool CTrends::Calculate(void)
  {
   if(CheckPointer(C_ZigZag)==POINTER_INVALID)
      return false;
//---
   if(C_ZigZag.Total()==0)
      return true;

Затем, определим количество необработанных экстремумов. В случае, если все экстремумы обработаны, выходим из метода с результатом true.

   int start=(i_total<=0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-1].TimeStartBar));
   switch(start)
     {
      case 0:
        return true;
        break;
      case -1:
        start=(i_total<=1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-2].TimeStartBar));
        if(start<0 || ArrayResize(Trends,i_total-1)<=0)
          {
           ArrayFree(Trends);
           i_total=0;
           start=C_ZigZag.Total();
          }
        else
           i_total=ArraySize(Trends);
        if(start==0)
           return true;
        break;
     }

После этого запрашиваем необходимое количество экстремумов из индикаторного класса.

   s_Extremum  base[];
   if(!C_ZigZag.Extremums(base,0,start))
      return false;
   int total=ArraySize(base);
   if(total<=0)
      return true;

Если же до сих пор не было ни одного экстремума в базе, то добавляем в базу самый старый экстремум, вызвав метод AddTrendPoint.

   if(i_total==0)
      if(!AddTrendPoint(base[total-1]))
         return false;

Далее организовываем цикл с перебором всех загруженных экстремумом. Экстремумы до последнего ранее сохраненного пропускаются.

   for(int i=total-1;i>=0;i--)
     {
      int trends_pos=i_total-1;
      if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar)
         continue;

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

      if(IsHigh(Trends[trends_pos]))
        {
         if(IsHigh(base[i]))
           {
            if(Trends[trends_pos].Price<base[i].Price)
              {
               Trends[trends_pos].Price=base[i].Price;
               Trends[trends_pos].TimeStartBar=base[i].TimeStartBar;
              }
            continue;
           }

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

         else
           {
            if(trends_pos>1 && Trends[trends_pos-1].Price>base[i].Price  && Trends[trends_pos-2].Price>Trends[trends_pos].Price)
              {
               double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-1].Price);
               double correction=fabs(Trends[trends_pos].Price-base[i].Price);
               if(fabs(1-correction/trend)>d_MinCorrection)
                 {
                  Trends[trends_pos-1].Price=base[i].Price;
                  Trends[trends_pos-1].TimeStartBar=base[i].TimeStartBar;
                  i_total--;
                  ArrayResize(Trends,i_total);
                  continue;
                 }
              }
            AddTrendPoint(base[i]);
           }
        }

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

3.2. Поиск паттернов

После определения ценовых экстремумом построим блок поиска точек открытия позиции. Разделим эту работу на 2 подэтапа:

  1. Поиск паттерна для потенциального открытия позиции.
  2. Непосредственно точки открытия позиции.

Этот функционал будет возложен на класс CPttern, заголовок которого представлен ниже.

class CPattern : public CObject
  {
private:
   s_Extremum     s_StartTrend;        //Точка начала тенденци
   s_Extremum     s_StartCorrection;   //Точка начала корректиции
   s_Extremum     s_EndCorrection;     //Точка завершения коррекции
   s_Extremum     s_EndTrend;          //Точка завершения тенденции
   double         d_MinCorrection;     //Минимальная коррекция
   double         d_MaxCorrection;     //Максимальная коррекция
//---
   bool           b_found;             //Флаг "Паттерн найден"
//---
   CTrends       *C_Trends;
public:
                     CPattern();
                    ~CPattern();
//--- Инициализация класса
   virtual bool      Create(CTrends *trends, double min_correction, double max_correction);
//--- Методы поиска паттерна и точки входа
   virtual bool      Search(datetime start_time);
   virtual bool      CheckSignal(int &signal, double &sl, double &tp1, double &tp2);
//--- method of comparing the objects
   virtual int       Compare(const CPattern *node,const int mode=0) const;
//--- Методы получения информеции об экстремумах паттеррна
   s_Extremum        StartTrend(void)        const {  return s_StartTrend;       }
   s_Extremum        StartCorrection(void)   const {  return s_StartCorrection;  }
   s_Extremum        EndCorrection(void)     const {  return s_EndCorrection;    }
   s_Extremum        EndTrend(void)          const {  return s_EndTrend;         }
   virtual datetime  EndTrendTime(void)            {  return s_EndTrend.TimeStartBar;  }
  };

Паттерн будем определять по четырем соседним экстремумам, информацию о которых сохраним в четырех структурах s_StartTrend, s_StartCorrection, s_EndCorrection и s_EndTrend. Для идентификации паттерна нам также потребуются уровни минимума и максимума коррекции, которые будут храниться в переменных d_MinCorrection и d_MaxCorrection. Экстремумы будем получать из экземпляра ранее созданного класса CTrends.

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

bool CPattern::Create(CTrends *trends,double min_correction,double max_correction)
  {
   if(CheckPointer(trends)==POINTER_INVALID)
      return false;
//---
   C_Trends=trends;
   b_found=false;
   s_StartTrend.Clear();
   s_StartCorrection.Clear();
   s_EndCorrection.Clear();
   s_EndTrend.Clear();
   d_MinCorrection=min_correction;
   d_MaxCorrection=max_correction;
//---
   return true;
  }

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

Вначале метода проверим актуальность указателя на объект класса CTrends и наличие сохраненных экстремумов. В случае негативного результата выходим из метода с результатом false.

bool CPattern::Search(datetime start_time)
  {
   if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<4)
      return false;

Затем определяем экстремум, соответствующий дате, полученной в входных параметрах. Если экстремум не найден, выходим из метода с результатом false.

   int start=C_Trends.ExtremumByTime(start_time);
   if(start<0)
      return false;

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

   b_found=false;
   for(int i=start;i>=0;i--)
     {
      if((i+3)>=C_Trends.Total())
         continue;
      if(!C_Trends.Extremum(s_StartTrend,i+3) || !C_Trends.Extremum(s_StartCorrection,i+2) ||
         !C_Trends.Extremum(s_EndCorrection,i+1) || !C_Trends.Extremum(s_EndTrend,i))
         continue;

На следующем этапе проверим соответствие экстремумов искомому паттерну. Если экстремумы не удовлетворяют искомому паттерну, переходим к следующим экстремумам. При нахождении паттерна устанавливаем флаг в положение true и выходим из метода с тем же результатом.

      double trend=s_StartCorrection.Price-s_StartTrend.Price;
      double correction=s_StartCorrection.Price-s_EndCorrection.Price;
      double re_trial=s_EndTrend.Price-s_EndCorrection.Price;
      double koef=correction/trend;
      if(koef<d_MinCorrection || koef>d_MaxCorrection || (1-fmin(correction,re_trial)/fmax(correction,re_trial))>=d_MaxCorrection)
         continue;
      b_found= true; 
//---
      break;
     }
//---
   return b_found;
  }

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

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

В начале метода проверим флаг на наличие ранее найденного паттерна, и если паттерн не найден, выходим из метода с результатом false.

bool CPattern::CheckSignal(int &signal, double &sl, double &tp1, double &tp2)
  {
   if(!b_found)
      return false;

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

   string symbol=C_Trends.Symbol();
   if(symbol=="Not Initilized")
      return false;
   datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe());
   int shift=iBarShift(symbol,e_ConfirmationTF,start_time);
   if(shift<0)
      return false;
   MqlRates rates[];
   int total=CopyRates(symbol,e_ConfirmationTF,0,shift+1,rates);
   if(total<=0)
      return false;

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

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

Если обнаруживается одно из событий, отменяющих паттерн, выходим из метода с результатом false.

   signal=0;
   sl=tp1=tp2=-1;
   bool up_trend=C_Trends.IsHigh(s_EndTrend);
   double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price));
   double exit_level=2*s_EndCorrection.Price - extremum;
   bool break_neck=false;
   for(int i=0;i<total;i++)
     {
      if(up_trend)
        {
         if(rates[i].low<=exit_level || rates[i].high>extremum)
            return false;
         if(!break_neck)
           {
            if(rates[i].close>s_EndCorrection.Price)
               continue;
            break_neck=true;
            continue;
           }
         if(rates[i].high>s_EndCorrection.Price)
           {
            if(sl==-1)
               sl=rates[i].high;
            else
               sl=fmax(sl,rates[i].high);
           }
         if(rates[i].close<s_EndCorrection.Price || sl==-1)
            continue;
         if((total-i)>2)
            return false;

После обнаружения сигнала на открытие позиции укажем тип сигнала ("-1" - Продажа, "1" - Покупка) и торговые уровни. Стоп-лосс установим на уровне максимальной глубины коррекции к линии шеи после ее пробития. Для тейк-профита установим 2 уровня:

1. На уровне, равном 90% расстояния от линии экстремумов до шеи в сторону открытия позиции.

2. На уровне, равно 90% от предыдущего трендового движения.

При этом добавим ограничение, что уровень первого тейк-профита не может превышать уровень второго тейк-профита.

         signal=-1;
         double top=fmax(s_StartCorrection.Price,s_EndTrend.Price);
         tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9;
         tp2=top-(top-s_StartTrend.Price)*0.9;
         tp1=fmax(tp1,tp2);
         break;
        }

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

3.3. Собираем советник

После проведения подготовительной работы соберем все блоки в единый советник. Объявим внешние переменные, которые разделим на три блока:

sinput   string            s1             =  "---- ZigZag Settings ----";     //---
input    int               i_Depth        =  12;                              // Depth
input    int               i_Deviation    =  100;                             // Deviation
input    int               i_Backstep     =  3;                               // Backstep
input    int               i_MaxHistory   =  1000;                            // Max history, bars
input    ENUM_TIMEFRAMES   e_TimeFrame    =  PERIOD_M30;                      // Work Timeframe
sinput   string            s2             =  "---- Pattern Settings ----";    //---
input    double            d_MinCorrection=  0.118;                           // Minimal Correction
input    double            d_MaxCorrection=  0.5;                             // Maximal Correction
input    ENUM_TIMEFRAMES   e_ConfirmationTF= PERIOD_M5;                       // Timeframe for confirmation
sinput   string            s3             =  "---- Trade Settings ----";      //---
input    double            d_Lot          =  0.1;                             // Trade Lot
input    ulong             l_Slippage     =  10;                              // Slippage
input    uint              i_SL           =  350;                             // Stop Loss Backstep, points

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

CArrayObj         *ar_Objects;
CTrade            *Trade;
CPattern          *Pattern;
datetime           start_search;

Для реализации возможности выставления одновременно двух тейк-профитов к позиции воспользуемся технологией, предложенной в статье [2]

В функции OnInit() проведем инициализацию всех необходимых объектов. При этом, так как мы глобально не объявляли экземпляры классов CZigZag и CTrends, мы просто их инициализируем и добавим указатели на эти объекты в наш массив. В случае ошибки инициализации на любом из этапов выходим из функции с результатом INIT_FAILED.

int OnInit()
  {
//--- Инициализация массива объектов
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      return INIT_FAILED;
//--- Инициализация индикаторного класса Зиг-Заг
   CZigZag *zig_zag=new CZigZag();
   if(CheckPointer(zig_zag)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(zig_zag))
     {
      delete zig_zag;
      return INIT_FAILED;
     }
   zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame);
   zig_zag.MaxHistory(i_MaxHistory);
//--- Инициализация класса поиска трендовых движений
   CTrends *trends=new CTrends();
   if(CheckPointer(trends)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(trends))
     {
      delete trends;
      return INIT_FAILED;
     }
   if(!trends.Create(zig_zag,d_MinCorrection))
      return INIT_FAILED;
//--- Инициализация класса торговых операций
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetAsyncMode(false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
//--- Инициализация вспомогательных переменных
   start_search=0;
   CLimitTakeProfit::OnlyOneSymbol(true);
//---
   return(INIT_SUCCEEDED);
  }

В функции OnDeinit() произведем очистку экземпляров используемых объектов.

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(ar_Objects)!=POINTER_INVALID)
     {
      for(int i=ar_Objects.Total()-1;i>=0;i--)
         delete ar_Objects.At(i);
      delete ar_Objects;
     }
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
   if(CheckPointer(Pattern)!=POINTER_INVALID)
      delete Pattern;
  }

Как всегда, основной функционал реализован в функции OnTick. Функционал данной функции можно условно разделить на два блока:

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

2. Поиск новых паттернов. Запускается при каждом открытии новой свечи на рабочем (указанном для индикатора) таймфрейме.

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

void OnTick()
  {
//---
   static datetime Last_CfTF=0;
   datetime series=(datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE);
   if(Last_CfTF>=series)
      return;
   Last_CfTF=series;

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

   int total=ar_Objects.Total();
   for(int i=2;i<total;i++)
     {
      if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID)
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
//---
      if(!CheckPattern(ar_Objects.At(i)))
        {
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
        }
     }

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

   static datetime Last_WT=0;
   series=(datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE);
   if(Last_WT>=series)
      return;

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

   start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame)));
   if(CheckPointer(Pattern)==POINTER_INVALID)
     {
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         return;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         return;
        }
     }
   Last_WT=series;

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

   while(!IsStopped() && Pattern.Search(start_search))
     {
      start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
      bool found=false;
      for(int i=2;i<ar_Objects.Total();i++)
         if(Pattern.Compare(ar_Objects.At(i),0)==0)
           {
            found=true;
            break;
           }
      if(found)
         continue;

Если же найден новый паттерн, то проверим сигнал на открытие позиции, вызвав функцию CheckPattern(). После чего, при необходимости, сохраняем паттерн в наш массив и инициализируем новый экземпляр класса для следующего поиска. Цикл продолжается пока при очередном поиске метод Search() не вернет значение false.

      if(!CheckPattern(Pattern))
         continue;
      if(!ar_Objects.Add(Pattern))
         continue;
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         break;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         break;
        }
     }
//---
   return;
  }

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

В начале функции вызовем метод поиска сигнала на открытия позиции класса CPattern. В случае ошибки проверки выходим из функции с результатом false.

bool CheckPattern(CPattern *pattern)
  {
   int signal=0;
   double sl=-1, tp1=-1, tp2=-1;
   if(!pattern.CheckSignal(signal,sl,tp1,tp2))
      return false;

При успешном поиске сигнала на открытие позиции установим торговые уровни и отправим приказ на открытие позиции в соответствии с полученным сигналом.

   double price=0;
   double to_close=100;
//---
   switch(signal)
     {
      case 1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        CLimitTakeProfit::Clear();
        if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((tp2-price)/_Point),to_close))
              return false;
        if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,0,NULL))
           return false;
        break;
      case -1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        CLimitTakeProfit::Clear();
        if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((price-tp2)/_Point),to_close))
              return false;
        if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,0,NULL))
           return false;
        break;
     }
//---
   return true;
  }

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

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

4. Тестирование стратегии

После создания нашего советника пришло время проверить его работу на исторических данных. Тестирование будем проводить за период в 9 месяцев 2018 года для пары EURUSD. Поиск паттернов будем проводить на тайм фрейме М30, а точки открытия позиции будем искать на тайм-фрейме М5.

Тестирование советникаТестирование советника

Результаты тестирование показали возможность советника генерировать прибыль. За тестируемый период советник совершил 90 трейдов, 70 из которых было прибыльными. Советник показал профит фактор 2.02 и фактор восстановления 4.77, что свидетельствует о возможности использования советника на реальных счетах. Полные результаты тестирования приведены ниже.

Результаты тестированияРезультаты тестирования

Заключение

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

Ссылки

  1. Как перенести расчетную часть любого индикатора в код эксперта
  2. Реализация Take Profit в виде лимитных ордеров без изменения оригинального кода советника

Программы, используемые в статье:

#
Имя
Тип
Описание
1ZigZag.mqhБиблиотека классаКасс индикатора Zig Zag
2Trends.mqh Библиотека классаКласс поиска тенденций
3Pattern.mqhБиблиотека классаКласс работы с паттернами
4LimitTakeProfit.mqhБиблиотека классаКласс подмены тейк-профита ордеров на лимитные ордера
5Header.mqhБиблиотекаФайл заголовков советника
6DoubleTop.mq5СоветникСоветник по стратегии "Двойная вершина/дно"