English 中文 Español Deutsch 日本語 Português
Паттерн прорыва канала

Паттерн прорыва канала

MetaTrader 5Примеры | 11 января 2018, 11:13
12 544 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

Мировой рынок — это извечная борьба продавцов с покупателями. Первые хотят заработать и продать подороже, а вторые не хотят отдавать свои заработанные средства и стремятся купить подешевле. Теория экономики говорит, что истинная цена товара находится в точке равенства спроса и предложения. И с этим нельзя не согласиться. Но "масла в огонь" подливает динамичность рынка — объемы спроса и предложения находятся в постоянном движении.

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

1. Теоретические аспекты стратегии

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

Терминал MetaTrader 5 предоставляет своим пользователям возможность построить 4 вида каналов.

  1. Равноудаленный канал.
  2. Канал стандартных отклонений.
  3. Канал регрессии.
  4. Вилы Эндрюса.
Подробнее с принципами построения каналов и их различиями можно ознакомиться в документации терминала. Мы же в статье рассмотрим общие аспекты построения каналов.

Для примера рассмотрим график EURUSD M30, на котором можно заметить ряд ценовых колебаний.

График EURUSD M30

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

График EURUSD M30 с выделением ценовых каналов.

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

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

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

2. Автоматизация поиска паттернов

Для создания алгоритма поиска паттернов воспользуемся методом, предложенным Dmitry Fedoseev в статье [1]. Из представленного в его статье индикатора возьмем только определение горизонтальной формации и перенесем код из индикатора в класс CChannel.

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

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

Таким образом, наш класс примет нижеследующий вид.

class CChannel : public CObject
  {
private:
   string            s_Symbol;      // Symbol
   ENUM_TIMEFRAMES   e_Timeframe;   // Timeframe
   int               i_Handle;      // Indicator's handle
   datetime          dt_LastCalc;   // Last calculated bar
   SPeackTrough      PeackTrough[]; // Array of ZigZag's peacks
   int               CurCount;      // Count of peaks
   int               PreDir;        // Previus ZigZag's leg direction
   int               CurDir;        // Current ZigZag's leg direction
   int               RequiredCount; // Minimal peacks in channel
   double            d_Diff;        
   bool              b_FoundChannel;
   bool              b_Breaked;
   datetime          dt_Breaked;
   double            d_BreakedPrice;                     

   void              RefreshLast(datetime time,double v);
   void              AddNew(datetime time,double v,int d);
   bool              CheckForm(double base);
   double            GetRessistPrice(SPeackTrough &start_peack, datetime time);
   double            GetSupportPrice(SPeackTrough &start_peack, datetime time);
   bool              DrawChannel(MqlRates &break_bar);
   bool              DrawChannel(void);
   bool              UnDrawChannel(void);

public:
                     CChannel(int handle,datetime start_time,string symbol,ENUM_TIMEFRAMES timeframe);
                    ~CChannel();
   bool              Calculate(ENUM_ORDER_TYPE &type,double &stop_loss,datetime &deal_time,bool &breaked,datetime &breaked_time);
  };

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

CChannel::CChannel(int handle,datetime start_time,string symbol,ENUM_TIMEFRAMES timeframe) : RequiredCount(4),
                                                                                             CurCount(0),
                                                                                             CurDir(0),
                                                                                             PreDir(0),
                                                                                             d_Diff(0.1),
                                                                                             b_Breaked(false),
                                                                                             dt_Breaked(0),
                                                                                             b_FoundChannel(false)
  {
   i_Handle=handle;
   dt_LastCalc=fmax(start_time-1,0);
   s_Symbol=symbol;
   e_Timeframe=timeframe;
  }

В функции деинициализации класса вызовем функцию UnDrawChannel, которая удалит с графика все нанесенные ранее графические объекты.

Основная работа будет проводиться в функции Calculate. Ее параметрами выступают ссылки на переменные для записи информации о прорыве канала и открываемой по паттерну сделке. Использование в параметрах ссылок на переменные позволяет вернуть из функции значения целого ряда переменных.

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

bool CChannel::Calculate(ENUM_ORDER_TYPE &type,double &stop_loss,datetime &deal_time, bool &breaked,datetime &breaked_time)
  {
   MqlRates rates[];
   CurCount=ArraySize(PeackTrough);
   if(CurCount>0)
     {
      dt_LastCalc=PeackTrough[CurCount-1].Bar;
      CurDir=PeackTrough[CurCount-1].Dir;
     }
   int total=CopyRates(s_Symbol,e_Timeframe,fmax(dt_LastCalc-PeriodSeconds(e_Timeframe),0),TimeCurrent(),rates);
   if(total<=0)
      return false;

 После этого проведем инициализацию возвращаемых переменных.

   stop_loss=-1;
   breaked=b_Breaked;
   breaked_time=dt_Breaked;
   deal_time=0;

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

   for(int i=0;i<total;i++)
     {
      if(rates[i].time>dt_LastCalc)
        {
         dt_LastCalc=rates[i].time;
         PreDir=CurDir;
        }
      else
         continue;

      // new max      

      double lhb[2];
      if(CopyBuffer(i_Handle,4,total-i-1,2,lhb)<=0)
         return false;

      if(lhb[0]!=lhb[1])
        {
         if(CurDir==1)
            RefreshLast(rates[i].time,rates[i].high);
         else
            AddNew(rates[i].time,rates[i].high,1);
        }

      // new min

      double llb[2];
      if(CopyBuffer(i_Handle,5,total-i-1,2,llb)<=0)
         return false;

      if(llb[0]!=llb[1])
        {
         if(CurDir==-1)
            RefreshLast(rates[i].time,rates[i].low);
         else
            AddNew(rates[i].time,rates[i].low,-1);
        }

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

Если соответствие найдено, то переменной b_FoundChannel присваивается значение true. В противном случае, из списка вершин отбрасывается самая старая, переменным присваиваются начальные значения, и мы возвращаемся к началу цикла.

      double base=(CurCount>=2 ? MathAbs(PeackTrough[1].Val-PeackTrough[0].Val) : 0);
   
      if(CurCount>=RequiredCount && !b_FoundChannel)
        {
         if(CurDir!=PreDir)
           {
            if(CheckForm(base))
              {
               b_FoundChannel=true;
              }
            else
              {
               UnDrawChannel();
               dt_LastCalc=PeackTrough[0].Bar+PeriodSeconds(e_Timeframe);
               ArrayFree(PeackTrough);
               CurCount=0;
               CurDir=0;
               PreDir=0;
               b_Breaked=false;
               dt_Breaked=0;
               b_FoundChannel=false;
               deal_time=0;
               total=CopyRates(s_Symbol,e_Timeframe,fmax(dt_LastCalc,0),TimeCurrent(),rates);
               i=-1;
               continue;
              }
           }
        }

После того, как канал найден, переходим к поиску его пробития. При пробое переменным b_Breaked и breaked присваиваем значение true. В переменные dt_Breaked и breaked_time сохраняем время открытия пробойной свечи, а в переменной d_BreakedPrice — ее экстремальное значение. Затем вызываем функцию DrawChannel, которая нанесет на график наш канал и точку его пробоя. Обратите внимание: пробой ищется только в сторону, противоположную текущему тренду. Если тенденция усиливается и выход из канала происходит в сторону текущего тренда, то классом инициализируется создание нового экземпляра класса для поиска канала (глобальную функцию SearchNewChannel подробно рассмотрим ниже).

После определения пробоя канала приступаем к поиску паттерна на вход в позицию. Как уже говорилось выше, после пробоя канала класс ждет возврата цены к его границам, и затем дает сигнал на вход в рынок. Дополнительным сигналом на вход я сделал закрытие очередной свечи выше экстремума пробойной свечи — для Buy и ниже него — для Sell. Этот паттерн используется для входа в рынок, когда цена мощным движением пробивает канал и уходит без коррекции.

При поступлении сигнала в переменную type вносим необходимый тип ордера, рассчитываем и сохраняем в соответствующую переменную значение стоп-лосса. В переменную deal_time вносим время начала бара поступления сигнала.

      if(b_FoundChannel)
        {
         if(PeackTrough[0].Dir==1)
           {
            if(PeackTrough[0].Val>PeackTrough[2].Val)
              {
               if(!b_Breaked)
                 {
                  if((rates[i].close-GetRessistPrice(PeackTrough[0],rates[i].time))>=(d_Diff*base))
                    {
                     b_Breaked=breaked=true;
                     dt_Breaked=breaked_time=rates[i].time;
                     d_BreakedPrice=rates[i].high;
                     DrawChannel(rates[i]);
                     continue;
                    }
                  if(CurCount>4 && PeackTrough[CurCount-1].Dir==1 && (GetRessistPrice(PeackTrough[1],rates[i].time)-PeackTrough[CurCount-1].Val)>0)
                    {
                     int channels=ArraySize(ar_Channels);
                     if(ar_Channels[channels-1]==GetPointer(this))
                       {
                        SearchNewChannel(PeackTrough[CurCount-3].Bar-PeriodSeconds(e_Timeframe));
                       }
                    }
                 }
               else
                 {
                  if(rates[i].time<=dt_Breaked)
                     continue;
                  //---
                  double res_price=GetRessistPrice(PeackTrough[0],rates[i].time);
                  if(((rates[i].low-res_price)<=0 && (rates[i].close-res_price)>0 && (rates[i].close-res_price)<=(d_Diff*base)) || rates[i].close>d_BreakedPrice)
                    {
                     type=ORDER_TYPE_BUY;
                     stop_loss=res_price-base*(1+d_Diff);
                     deal_time=rates[i].time;
                     return true;
                    }
                 }
              }
            else
              {
               UnDrawChannel();
               dt_LastCalc=PeackTrough[0].Bar+PeriodSeconds(e_Timeframe);
               ArrayFree(PeackTrough);
               CurCount=0;
               CurDir=0;
               PreDir=0;
               b_Breaked=false;
               dt_Breaked=0;
               b_FoundChannel=false;
               deal_time=0;
               total=CopyRates(s_Symbol,e_Timeframe,fmax(dt_LastCalc,0),TimeCurrent(),rates);
               i=-1;
               continue;
              }
           }
         else
           {
            if(PeackTrough[0].Val<PeackTrough[2].Val)
              {
               if(!b_Breaked)
                 {
                  if((GetSupportPrice(PeackTrough[0],rates[i].time)-rates[i].close)>=(d_Diff*base))
                    {
                     b_Breaked=breaked=true;
                     dt_Breaked=breaked_time=rates[i].time;
                     d_BreakedPrice=rates[i].low;
                     DrawChannel(rates[i]);
                     continue;
                    }
                  if(CurCount>4 && PeackTrough[CurCount-1].Dir==-1 && (PeackTrough[CurCount-1].Val-GetSupportPrice(PeackTrough[1],rates[i].time))>0)
                    {
                     int channels=ArraySize(ar_Channels);
                     if(ar_Channels[channels-1]==GetPointer(this))
                       {
                        SearchNewChannel(PeackTrough[CurCount-3].Bar-PeriodSeconds(e_Timeframe));
                       }
                    }
                 }
               else
                 {
                  if(rates[i].time<=dt_Breaked)
                     continue;
                  double sup_price=GetSupportPrice(PeackTrough[0],rates[i].time);
                  if(((sup_price-rates[i].high)<=0 && (sup_price-rates[i].close)>0 && (sup_price-rates[i].close)<=(d_Diff*base)) || rates[i].close<d_BreakedPrice)
                    {
                     type=ORDER_TYPE_SELL;
                     stop_loss=sup_price+base*(1+d_Diff);
                     deal_time=rates[i].time;
                     return true;
                    }
                 }
              }
            else
              {
               UnDrawChannel();
               dt_LastCalc=PeackTrough[0].Bar+PeriodSeconds(e_Timeframe);
               ArrayFree(PeackTrough);
               CurCount=0;
               CurDir=0;
               PreDir=0;
               b_Breaked=false;
               dt_Breaked=0;
               b_FoundChannel=false;
               deal_time=0;
               total=CopyRates(s_Symbol,e_Timeframe,fmax(dt_LastCalc,0),TimeCurrent(),rates);
               i=-1;
               continue;
              }
           }
        }
     }
   return b_Breaked;
  }

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

3. Создаем советник для тестирования стратегии

Теперь, когда мы создали класс поиска каналов, нужно протестировать нашу стратегию. Для проведения теста создадим небольшой советник. Мы помним, что каналы ищутся по вершинам универсального зиг-зага из статьи [3]. Поэтому нужно загрузить и перекомпилировать этот индикатор. Я внес его в список ресурсов для удобства использования советника. Такой подход дает возможность переноса советника между терминалами без переноса индикатора. Также в советник я включил наш класс CChannel и стандартный класс для совершения торговых операций CTrade.
#resource "\\Indicators\\ZigZags\\iUniZigZagSW.ex5"
#include <\\Break_of_channel_DNG\\Channel.mqh>
#include <Trade\\Trade.mqh>

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

input ESorce               SrcSelect      =  Src_HighLow;
input EDirection           DirSelect      =  Dir_NBars;
input int                  RSIPeriod      =  14;
input ENUM_APPLIED_PRICE   RSIPrice       =  PRICE_CLOSE;
input int                  MAPeriod       =  14;
input int                  MAShift        =  0;
input ENUM_MA_METHOD       MAMethod       =  MODE_SMA;
input ENUM_APPLIED_PRICE   MAPrice        =  PRICE_CLOSE;
input int                  CCIPeriod      =  14;
input ENUM_APPLIED_PRICE   CCIPrice       =  PRICE_TYPICAL;
input int                  ZZPeriod       =  50;

Глобальных переменных будет всего четыре. В них будем записывать:

  • хэндл нашего индикатора,
  • массив указателей на наши каналы (объекты класса CChannel),
  • указатель на класс CTrade (используется для совершения торговых операций),
  • время открытия бара последнего пробоя.
int         zz_handle;
CChannel   *ar_Channels[];
CTrade     *Trade;
datetime    dt_last_break;

В функции OnInit советника вызовем индикатор и инициализируем необходимые классы. В случае ошибки функция вернет INIT_FAILED.

int OnInit()
  {
//---
   zz_handle=iCustom(Symbol(),Period(),"::Indicators\\ZigZags\\iUniZigZagSW",SrcSelect,
                                             DirSelect,
                                             RSIPeriod,
                                             RSIPrice,
                                             MAPeriod,
                                             MAShift,
                                             MAMethod,
                                             MAPrice,
                                             CCIPeriod,
                                             CCIPrice,
                                             ZZPeriod);
                                             
   if(zz_handle==INVALID_HANDLE){
      Alert("Error load indicator");
      return(INIT_FAILED);
   }  
//---
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
//---
   dt_last_break=0;
//---
   return(INIT_SUCCEEDED);
  }

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

void OnDeinit(const int reason)
  {
//---
   int total=ArraySize(ar_Channels);
   for(int i=0;i<total;i++)
     {
      if(CheckPointer(ar_Channels[i])!=POINTER_INVALID)
         delete ar_Channels[i];
     }
   ArrayFree(ar_Channels);
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
  }

Как всегда, основная работа будет проводиться в функции OnTick.

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

void OnTick()
  {
//---
   static datetime last_bar=0;
   if(last_bar>=SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE))
      return;
   last_bar=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);

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

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

   int total=ArraySize(ar_Channels);
   if(total==0)
      if(SearchNewChannel(dt_last_break))
         total++;

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

   for(int i=0;i<total;i++)
     {
      if(CheckPointer(ar_Channels[i])==POINTER_INVALID)
        {
         DeleteChannel(i);
         i--;
         total--;
         continue;
        }

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

      ENUM_ORDER_TYPE type;
      double stop_loss=-1;
      bool breaked=false;
      datetime breaked_time=0;
      datetime deal_time=0;
      if(ar_Channels[i].Calculate(type,stop_loss,deal_time,breaked,breaked_time))
        {

После успешного выполнения функции пересохраним время бара последнего прорыва канала.

         dt_last_break=fmax(dt_last_break,breaked_time);

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

         if(breaked && i==(total-1))
            if(SearchNewChannel(breaked_time))
              { 
               if(total>=5)
                  i--;
               else
                  total++;
              }

Обратите внимание, что функция SearchNewChannel построена на сохранение 5 последних каналов. Следовательно, значение переменной total растет, только если в массиве было менее 5 каналов. Иначе уменьшаем переменную i, указывающую на номер обрабатываемого канала.

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

         if(deal_time>=0 && stop_loss>=0)
           {
            int bars=Bars(_Symbol,PERIOD_CURRENT,deal_time,TimeCurrent());
            double lot=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
            switch(type)
              {
               case ORDER_TYPE_BUY:
                 if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
                    Trade.PositionClose(_Symbol);
                 if(bars<=2)
                    Trade.Buy(lot,_Symbol,0,fmax(stop_loss,0));
                 break;
               case ORDER_TYPE_SELL:
                 if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
                    Trade.PositionClose(_Symbol);
                 if(bars<=2)
                    Trade.Sell(lot,_Symbol,0,fmax(stop_loss,0));
                 break;
              }
            DeleteChannel(i);
            i--;
            total--;
           }
        }
     }
  }

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

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

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

Как вы могли заметить, в коде использованы две вспомогательные функции: SearchNewChannel и DeleteChannel.

Первая функция, SearchNewChannel, инициализирует новый экземпляр класса CChannel в нашем массиве каналов. В начале функции мы проверяем наличие хэндла индикатора. В случае некорректного хэндла выходим из функции с результатом false.

bool SearchNewChannel(datetime time)
  {
   if(zz_handle==INVALID_HANDLE)
      return false;

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

   int total=ArraySize(ar_Channels);
   if(total>4)
     {
      for(int i=0;i<total-4;i++)
        {
         if(CheckPointer(ar_Channels[i])!=POINTER_INVALID)
            delete ar_Channels[i];
        }
      for(int i=0;i<4;i++)
         ar_Channels[i]=ar_Channels[total-4+i];
      if(total>5)
        {
         if(ArrayResize(ar_Channels,5)>0)
            total=5;
         else
            return false;
        }
     }

Если в массиве менее 5 каналов, то мы просто увеличиваем его.

   else
     {
      if(ArrayResize(ar_Channels,total+1)>0)
         total++;
      else
         return false;
     }

В заключение функции инициализируем новый экземпляр класса CChannel в последней ячейке массива.

   ar_Channels[total-1]=new CChannel(zz_handle,time,_Symbol,PERIOD_CURRENT);
   return (CheckPointer(ar_Channels[total-1])!=POINTER_INVALID);
  }

Функция DeleteChannel удаляет из массива экземпляр класса CChannel по указанному номеру. В начале функции проверяем, находится ли этот номер в пределах существующего массива. При несоответствии выходим из функции с результатом false.

bool DeleteChannel(int pos)
  {
   int total=ArraySize(ar_Channels);
   if(pos<0 || pos>=total)
      return false;

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

   delete ar_Channels[pos];
   for(int i=pos;i<total-1;i++)
      ar_Channels[i]=ar_Channels[i+1];

Если до запуска функции в массиве был только один объект, то освобождаем массив. Иначе уменьшаем его на 1 элемент.

   if(total==1)
     {
      ArrayFree(ar_Channels);
      return true;
     }
   return (ArrayResize(ar_Channels,total-1)>0);
  }

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

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

4.1. Таймфрейм H1

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

Тестирование советника на таймфрейме Н1.Параметры советника для тестирования.

Первый же тест показал способность стратегии приносить прибыль. За тестируемый период советник совершил всего 26 сделок, в результате которых было открыто 10 позиций. 80% из открытых позиций были закрыты с прибылью. Это дало довольно ровный рост баланса. Профит-фактор по результатам тестирования составил 4.06. Это неплохой результат.

Результаты тестирования на таймфрейме Н1.

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

4.2. Таймфрейм M15

Второе тестирование стратегии было проведено на таймфрейме M15, остальные параметры не изменились.

Тестирование советника на таймфрейме М15.Параметры советника для тестирования.

Как и ожидалось, в результате тестирования советник увеличил количество сделок. За тестируемый период советник открыл 63 позиции. Но это увеличение не дало качественного результата. Общая прибыль от совершенных операций составила $130.60 против $133.46 на Н1, доля прибыльных позиций снизилась почти в 2 раза — до 41.27%. В результате мы получили более ломанный график баланса, и профит-фактор составил 1.44 — почти втрое меньше предыдущего тестирования.

Результаты тестирования на таймфрейме М15.

4.3. Тестирование на других инструментах

Результаты тестов выше однозначно показали, что результативность стратегии выше на таймфрейме H1. Чтобы оценить перспективы ее использования на различных инструментах, я провел еще 3 теста. Использовался таймфрейм H1, параметры и период тестирования не менялись. С полными результатами тестирования можно ознакомиться во вложении к статье, в таблице ниже сгруппированы основные показатели.

ИнструментКоличество трейдовКоличество сделокДоля прибыльных трейдов, %Профит-факторФактор восстановленияСреднее время удержания трейда, часов 
EURUSD1026804.061.78552 
GBPUSD28501.470.232072 
EURGBP51400.0-0.71976 
USDJPY617830.72-0.19875 

Как видно из приведенных данных, наихудший результат советник показал на паре EURGBP. Ни один из 5 трейдов здесь не был закрыт в прибяль. Но все же, если взглянуть на график движения цены, можно заметить упущенный потенциал получения прибыли при входах по стратегии. Как видно на приведенном ниже скриншоте, стратегия прорыва канала дает неплохие сигналы на вход, но для стабильной работы надо доработать стратегию выхода из позиции. Это подтверждается и длительностью удержания позиции. По результатам проведенных тестов, в зависимости от инструмента, среднее время удержания позиции составляет от 550 до 2100 часов. За такой промежуток времени рынок успевает не один раз сменить свои тенденции.

Пример совершения сделок советником на графике EURGBP.

Заключение

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

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

Ссылки

  1. Паттерн Флаг
  2. Графики и диаграммы в формате HTML
  3. Универсальный зигзаг

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

#
 Имя
Тип 
Описание 
1Break_of_channel_DNG.mq5 Советник Советник для тестирования стратегии
 2Channel.mqh Библиотека класса Класс для поиска ценовых каналов и сигналов на открытие позиции
 3Break_of_channel_DNG.mqproj  Файл описания проекта
 4iUniZigZagSW.ex5 Инидикатор Универсальный зиг-заг
 5Reports.zip Архив Отчеты тестирования советника.


Прикрепленные файлы |
MQL5.zip (204.37 KB)
Reports.zip (241.95 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Evgeniy Scherbina
Evgeniy Scherbina | 30 дек. 2018 в 13:44
Прочитал, очень интересно! Один вопрос, на который не получил ответа: Как определяется точка, пересечение которое означает сделку? Для продолжения линии тренда в "будущее" можно использовать простую алгебраическую формулу: y = kx+b, насколько я понимаю она не используется?
Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть III Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть III
Мы заканчиваем тестирование паттернов, которые можно увидеть при торговле корзинами пар. В статье представлены результаты тестирования паттернов, отслеживающих движение валют пары по отношению друг к другу.
Пользовательский тестер стратегий на основе быстрых математических вычислений Пользовательский тестер стратегий на основе быстрых математических вычислений
Статья описывает создание пользовательского тестера стратегий и своего собственного анализатора прогонов оптимизации. Прочитав ее, вы поймете, как работает режим математических вычислений и механизм так называемых фреймов, как можно подготовить и загрузить свои собственные данные для расчетов и использовать эффективные алгоритмы их сжатия. Также эта статья будет интересна всем, кто интересуется способами хранения пользовательской информации внутри эксперта.
Управление капиталом по Винсу. Реализация в виде модуля Мастера MQL5 Управление капиталом по Винсу. Реализация в виде модуля Мастера MQL5
Статья написана на основе книги Р.Винса "Математика управления капиталом". В ней рассматриваются эмпирические и параметрические методы нахождения оптимального размера торгового лота, на основе которых написаны торговые модули управления капиталом для мастера MLQ5.
Как снизить риски трейдера Как снизить риски трейдера
Торговля на финансовых рынках связана с целым комплексом рисков, которые должны учитываться в алгоритмах торговых систем. Снижение таких рисков — важнейшая задача для получения прибыли при трейдинге.