English 中文 Español Deutsch 日本語 Português
Универсальный Зигзаг

Универсальный Зигзаг

MetaTrader 5Примеры | 20 октября 2016, 11:16
14 897 25
Dmitry Fedoseev
Dmitry Fedoseev

Содержание

Введение

Зигзаг (рис. 1) — один из самых популярных индикаторов среди пользователей MetaTrader 5. К настоящему моменту разработано множество вариантов Зигзага. Однако некоторые из них работают слишком медленно, что делает их неподходящими для создания экспертов. Другие постоянно выдают ошибки, что усложняет их использование даже для визуального наблюдения. С теми же индикаторами, которые работают и быстро, и без ошибок, все равно возникают трудности при использовании для разработки эксперта или другого индикатора. Дело в том, что из Зигзага не так-то просто извлечь показания и интерпретировать их.


Рис. 1. Индикатор ZigZag

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

При создании индикатора используем объектно-ориентированное программирование. Мы создадим несколько базовых классов для различных стадий создания Зигзага, для каждого из которых разработаем по несколько дочерних классов. Разделение на базовые и дочерние классы будет выполнено так, чтобы максимально упростить создание различных новых вариантов Зигзагов.

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

Особенности индикатора Зигзаг

Индикатор Зигзаг (рис. 1) представляет собой ломаную линию, соединяющую локальные максимумы и минимумы цены. У новичков сразу может возникнуть мысль: вот было здорово покупать на впадинах, а продавать на вершинах! Конечно, смотрится эта мысль очень заманчиво,  но увы, столь привлекательно Зигзаг выглядит только на истории. В реальности дело обстоит несколько иначе. О том, что сформировалась новая впадина или вершина, становится известно только спустя некоторое количество баров после нее. На рис. 2 показана ситуация, когда последний отрезок индикатора прекратил свое формирование (изменение), цена развернулась и движется в противоположном направлении (вверх).

 
Рис. 2. Зигзаг направлен вниз, а цена выполнила разворот вверх

Однако спустя несколько баров цена падает (рис. 3), и последний отрезок Зигзага продолжает протягиваться вниз.

  
Рис. 3. Цена продолжила движение вниз и последний отрезок зигзага продолжил свое формирование

На этот раз индикатор достиг своего минимума, но удостовериться в этом мы сможем только спустя несколько баров (рис. 4).

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


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


 Рис. 5. Красными и синими точками помечены бары на которых стало известно о развороте зигзага   

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

Варианты построения Зигзага

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

  1. Необходимо получить исходные данные. 
  2. Нужно сформулировать условия смены направления линии.  
  3. Необходимо следить за появлением новых максимумов и минимумов.

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

Условия смены направления — самый важный пункт, определяющий различные типы Зигзагов. Эти условия могут быть очень разными. К примеру, подобным условием может стать образование на формирующемся баре n-барного максимума/минимума. Другими словами, если значение исходного ряда на формирующемся баре является максимальным или минимальным за последние n баров, то это определяет направление Зигзага. По такому принципу работает классический индикатор ZigZag. Другой способ — по величине отката от зафиксированного максимального или минимального значения. Величина отката может измеряться в пунктах (если исходный ряд — это цена) или в условных единицах (если это какой-то индикатор). Этими двумя способами дело не ограничивается, можно определять направление по любому индикатору — хоть по стохастику, хоть по ADX. Если стохастик выше 50, значит, Зигзаг направлен вверх, если ниже 50 — вниз. Теперь попробуем определить направление по ADX: линия Зигзага направлена вверх, если линия PDI выше линии MDI, а если наоборот — то вниз. 

Таким образом, комбинируя различные варианты по пункту 1 и по пункту 2, получаем достаточно большое количество различных вариантов Зигзага, ведь нам ничто не мешает по пункту 1 использовать данные, например, от RSI, а направление определять по стохастику и т.п. Пункт 3 нужен только для того, чтобы индикатор имел, собственно, вид зигзага, хотя и варианты отрисовки могут быть самыми разными. 

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

Чтобы было понятней, сначала создадим отдельный индикатор, работающий по ценам high/low бара и меняющий свое направление по условию n-барного максимума/минимума.

Простой Зигзаг по high/low

В редакторе MetaEditor создайте новый индикатор (Главное меню — Файл — Создать или клавиши Ctrl+N). В Мастере создания нового индикатора введите имя "iHighLowZigZag", создайте один внешний параметр "period" (тип int, значение 12), выберите обработчик событий OnCalculate(...,open,high,low,close), создайте один буфер с именем "ZigZag" (тип Section, цвет Red) и еще три буфера с именами "Direction", "LastHighBar" и "LastLowBar" (тип line, цвет none).

Буфер "ZigZag" будет использоваться для отображения зигзага, остальные буферы — вспомогательные. В функции OnInit() для всех вспомогательных буферов при вызове функции SetIndexBuffer() замените тип INDICATOR_DATA на INDICATOR_CALCULATIONS. В верхней части файла измените значение свойства indicator_plots: установите значение 1. После этого индикатор будет рисовать только один буфер "ZigZag" и на графике не будет никаких лишних линий, но при этом дополнительные буферы будут доступны для обращения к ним через функцию iCustom()

Сначала в функции OnCalculate() вычислим индекс бара, с которого должен начинаться расчет (переменная start), так, чтобы расчет по всем барам выполнялся только при запуске индикатора, а в дальнейшем обсчитывался бы только каждый новый бар. Кроме этого инициализируем начальные элементы буферов:

  int start; // переменная для индекса бара, с которого будет выполняться расчет
  if(prev_calculated==0)
    { // на запуске
     // инициализация начальных элементов буферов
     DirectionBuffer[0]=0;
     LastHighBarBuffer[0]=0;
     LastLowBarBuffer[0]=0;
     start=1; // расчет со следующих элементов после инициализированных
    }
  else
    { // в процессе работы
     start=prev_calculated-1;
    }
}

Теперь основной индикаторный цикл:

for(int i=start;i<rates_total;i++)
     {

Как было написано выше, для достижения универсальности необходимо разделить код на расчет направления зигзага и на его рисование. Будем придерживаться этого принципа и сейчас. Сначала напишем код для определения направления. Для определения направления используем функции ArrayMaximum() и ArrayMinimum(). Если максимум или минимум выявлен на обсчитываемом баре, то элементу буфера Direction присвоим значение 1 или -1. Чтобы на каждом баре располагать информацией о текущем направлении зигзага, прежде чем выполнять определение направления, возьмем значение из предыдущего элемента буфера Direction и присвоим его текущему элементу:

// из предыдущего элемента буфера получаем
// значение определенного ранее направления
   DirectionBuffer[i]=DirectionBuffer[i-1];

// вычисление начального бара для функций
// ArrayMaximum() и ArrayMinimum()
   int ps=i-period+1;
// определение баров максимума и минимума на
// диапазоне period баров
   int hb=ArrayMaximum(high,ps,period);
   int lb=ArrayMinimum(low,ps,period);

// если выявлен максимум или минимум
   if(hb==i && lb!=i)
     { // выявлен максимум
      DirectionBuffer[i]=1;
     }
   else if(lb==i && hb!=i)
     { // выявлен минимум
      DirectionBuffer[i]=-1;
     }

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

Вообще, в терминале MetaTrader5 возможно создать Зигзаг, рисующий вертикальные отрезки, и на одном баре можно отобразить две смены направления индикатора. Однако в этой статье мы не будем рассматривать Зигзаги такого типа. 

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

LastHighBarBuffer[i]=LastHighBarBuffer[i-1];
LastLowBarBuffer[i]=LastLowBarBuffer[i-1];  

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

Обязательно очистим буфер для Зигзага:

ZigZagBuffer[i]=EMPTY_VALUE;  

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

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

switch((int)DirectionBuffer[i])
  {
   case 1:
      switch((int)DirectionBuffer[i-1])
        {
         case 1:
            // продолжение движения вверх
            ...
            break;
         case -1:
            // начало нового движения вверх
            ...
            break;
        }
      break;
   case -1:
      switch((int)DirectionBuffer[i-1])
        {
         case -1:
            // продолжение движения вниз
            ...
            break;
         case 1:
            // начало нового движения вниз    
            ...
            break;
        }
      break;

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

ZigZagBuffer[i]=high[i];
LastHighBarBuffer[i]=i;

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

// продолжение движения вверх
   if(high[i]>high[(int)LastHighBarBuffer[i]])
     { // новый максимум
      // старую точку зигзага удаляем
      ZigZagBuffer[(int)LastHighBarBuffer[i]]=EMPTY_VALUE;
      // ставим новую точку
      ZigZagBuffer[i]=high[i];
      // индекс бара с новой вершиной
      LastHighBarBuffer[i]=i;
     }

Вот и все. Не забудьте закрыть цикл закрывающей фигурной скобкой. Осталось протестировать индикатор в тестере в визуальном режиме. Полностью готовый индикатор "iHighLowZigZag" можно найти в приложении.

Простой зигзаг по close

Теперь переделаем только что созданный индикатор на работу по цене close. Писать все заново нет необходимости: сохраним индикатор  "iHighLowZigZag" с именем "iCloseZigZag" и заменим обращения к массивам high и low на обращение к массиву close. Казалось бы, на этом можно и закончить, но тестирование покажет неправильную работу индикатора (рис. 6).

 
Рис. 6. Неправильная работа Зигзага по close, переделанного из индикатора по high/low

Разберем, почему это происходит. Если цена high формирующегося бара образовала максимум на каком-то интервале баров, то, как бы еще ни менялась цена закрытия бара, этот максимум так и останется максимумом. Если же максимум образовала цена закрытия, то по мере дальнейшего формирования бара она может измениться, и максимум перестанет существовать. При определении нового максимума/минимума того же направления выполняется удаление старой точки — тут-то нас и подстерегает проблема. Новый максимум отменился, новая точка стерта, но и старая тоже стерта. Значит, нужно восстановить положение старой точки. Информация о расположении последних экстремумов находится в буферах: LastHighBarBuffer и LastLowBarBuffer. Из них мы и восстановим две последних точки. В основной цикл индикатора перед оператором switch добавим две строки кода:

ZigZagBuffer[(int)LastHighBarBuffer[i]]=close[(int)LastHighBarBuffer[i]];
ZigZagBuffer[(int)LastLowBarBuffer[i]]=close[(int)LastLowBarBuffer[i]];  

После этой доработки индикатор заработает правильно. Полученный индикатор "iCloseZigZag" можно найти в приложении к статье. 

Начало создания универсального Зигзага

Универсальность Зигзага будет достигаться раздельным решением трех задач:

  1. Заполнение буферов исходными данными. Будут использоваться два буфера. Они нужны, чтобы их можно было заполнить ценами high и low. Для получения Зигзага по close или по какому-то другому индикатору оба буфера можно заполнить одинаковыми значениями. 
  2. Заполнение буфера Direction на основе анализа исходных данных.
  3. Рисование Зигзага.
Каждая задача будет решаться своим отдельным базовым классом и дополнительными дочерними классами, что обеспечит возможность выбора различных вариантов и их комбинаций через окно свойств индикатора.

Класс исходных данных

Создайте включаемый файл "CSorceData.mqh", в него добавьте класс CSorceData. Он и станет родительским классом. Он будет содержать один виртуальный метод Calculate, подобный функции OnCalculate() индикатора, но с некоторыми изменениями. В метод передаются два дополнительных массива: BufferHigh[] и BufferLow[]. Эти буферы заполняются данными, по которым в дальнейшем и будет рассчитываться Зигзаг. Поскольку исходными данными может быть не только цена, но и значения любого другого индикатора, значит, необходимо проконтролировать загрузку индикатора. Для этого добавим виртуальный метод CheckHandle() (тип bool):

class CSorceData
  {
private:
public:
   virtual int Calculate(const int rates_total,
                         const int prev_calculated,
                         const datetime &time[],
                         const double &open[],
                         const double &high[],
                         const double &low[],
                         const double &close[],
                         const long &tick_volume[],
                         const long &volume[],
                         const int &spread[],
                         double &BufferHigh[],
                         double &BufferLow[])
     {
      return(0);
     }
   virtual bool CheckHandle()
     {
      return(true);
     }

  };

Теперь создадим несколько дочерних классов. Один — для цены high/low:

class CHighLow:public CSorceData
  {
private:
public:
   int Calculate(const int rates_total,
                 const int prev_calculated,
                 const datetime &time[],
                 const double &open[],
                 const double &high[],
                 const double &low[],
                 const double &close[],
                 const long &tick_volume[],
                 const long &volume[],
                 const int &spread[],
                 double &BufferHigh[],
                 double &BufferLow[])
     {
      int start=0;
      if(prev_calculated!=0)
        {
         start=prev_calculated-1;
        }
      for(int i=start;i<rates_total;i++)
        {
         BufferHigh[i]=high[i];
         BufferLow[i]=low[i];
        }
      return(rates_total);
     }
  };

 Второй — для цены close. Он будет отличаться только кодом в цикле:

for(int i=start;i<rates_total;i++)
  {
   BufferHigh[i]=close[i];
   BufferLow[i]=close[i];
  }

Имя этого класса "CClose:public CSorceData". Метод CheckHandle() пока не используется.

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

Создадим класс для RSI, дадим ему имя "CRSI:public CSorceData". В секцию private добавим переменную для хэндла индикатора:

   private:
      int m_handle;

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

void CRSI(int period,ENUM_APPLIED_PRICE price)
  {
   m_handle=iRSI(Symbol(),Period(),period,price);
  }

Теперь метод CheckHandle():

bool CheckHandle()
  {
   return(m_handle!=INVALID_HANDLE);
  }

В методе Calculate цикл не будем использовать, просто выполним копирование буферов:

int to_copy;
   if(prev_calculated==0)
     {
      to_copy=rates_total;
     }
   else
     {
      to_copy=rates_total-prev_calculated;
      to_copy++;
     }

   if(CopyBuffer(m_handle,0,0,to_copy,BufferHigh)<=0)
     {
      return(0);
     }

   if(CopyBuffer(m_handle,0,0,to_copy,BufferLow)<=0)
     {
      return(0);
     }
   return(rates_total);

Обратите внимание: в случае неудачного копирования (вызова функции CopyBuffer()) метод возвращает 0, а в случае удачного — rates_total. Это сделано для того, чтобы потом была возможность выполнить перерасчет индикатора в случае неудачного копирования. 

Аналогично создадим класс для скользящей средней с именем "CMA:public CSorceData". Отличия будут только в конструкторе:

void CMA(int period,int shift,ENUM_MA_METHOD method,ENUM_APPLIED_PRICE price)
   {
    m_handle=iMA(Symbol(),Period(),period,shift,method,price);
   }

Методы Calculate()  в данном случае получились совершено одинаковыми, но для других индикаторов возможны некоторые отличия, в частности, с номерами буферов. Полностью готовый файл "CSorceData.mqh" можно найти в приложении к статье. Хотя оговоримся, что полностью готовым его можно считать только условно, поскольку он подразумевает дальнейшее расширение путем добавления новых дочерних методов для других индикаторов.

Класс направления

Класс будет располагаться в файле "CZZDirection.mqh", имя базового класса — "CZZDirection". У класса будет виртуальный метод Calculate(), в который передаются параметры позволяющие определить бары для расчета (переменные rates_total, prev_calculated), буферы с исходными данными и буфер для направления. Раньше уже упоминалось, что направление для Зигзага можно определять по индикатору, поэтому обеспечим возможность использовать индикаторы. Добавим виртуальный метод CheckHandle():

class CZZDirection
  {
private:
public:
   virtual int Calculate(const int rates_total,
                         const int prev_calculated,
                         double &BufferHigh[],
                         double &BufferLow[],
                         double &BufferDirection[])
     {
      return(0);
     }
   virtual bool CheckHandle()
     {
      return(true);
     }
  };

Теперь напишем дочерний класс для определения направления, как в индикаторе "iHighLowZigZag". Для определения направления по этому методу потребуется параметр "period", поэтому в секцию private добавим переменную m_period и конструктор с параметром периода:

class CNBars:public CZZDirection
  {
private:
   int               m_period;
public:
   void CNBars(int period)
     {
      m_period=period;
     }
   int Calculate(const int rates_total,
                 const int prev_calculated,
                 double &BufferHigh[],
                 double &BufferLow[],
                 double &BufferDirection[]
                 )
     {
      int start;

      if(prev_calculated==0)
        {
         BufferDirection[0]=0;
         start=1;
        }
      else
        {
         start=prev_calculated-1;
        }

      for(int i=start;i<rates_total;i++)
        {

         BufferDirection[i]=BufferDirection[i-1];

         int ps=i-m_period+1;
         int hb=ArrayMaximum(BufferHigh,ps,m_period);
         int lb=ArrayMinimum(BufferLow,ps,m_period);

         if(hb==i && lb!=i)
           { // выявлен максимум
            BufferDirection[i]=1;
           }
         else if(lb==i && hb!=i)
           { // выявлен минимум
            BufferDirection[i]=-1;
           }

        }
      return(rates_total);
     }

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

class CCCIDir:public CZZDirection
   {
private:
    int               m_handle;
public:
    void CCCIDir(int period,ENUM_APPLIED_PRICE price)
      {
       m_handle=iCCI(Symbol(),Period(),period,price);
      }
    bool CheckHandle()
      {
       return(m_handle!=INVALID_HANDLE);
      }
    int Calculate(const int rates_total,
                  const int prev_calculated,
                  double &BufferHigh[],
                  double &BufferLow[],
                  double &BufferDirection[]
                  )
      {
       int start;
       if(prev_calculated==0)
         {
          BufferDirection[0]=0;
          start=1;
         }
       else
         {
          start=prev_calculated-1;
         }

       for(int i=start;i<rates_total;i++)
         {

          BufferDirection[i]=BufferDirection[i-1];

          double buf[1];
          if(CopyBuffer(m_handle,0,rates_total-i-1,1,buf)<=0)return(0);

          if(buf[0]>0)
            {
             BufferDirection[i]=1;
            }
          else if(buf[0]<0)
            {
             BufferDirection[i]=-1;
            }
         }
       return(rates_total);
      }
   };

В конструктор класса передаются параметры CCI и выполняется его загрузка. Используется метод CheckHandle(), который надо будет вызывать после создания объекта. В основном цикле выполняется проверка CCI и заполнение буфера BufferDirection.

Файл "CZZDirection.mqh" можно найти в приложении к статье. 

Класс рисования

Возможны различные варианты рисования зигзага. Можно отрисовывать его одной линией, можно раскрашивать, ставить точки на вершинах и т.п. В этой статье ограничимся одним способом рисования, но также создадим базовый и дочерний классы на случай дальнейшей доработки. Класс будет располагаться в файле "CZZDraw.mqh", имя класса — "CZZDraw". У класса будет один виртуальный метод Calculate() с такими же параметрами, как у класса направления. Кроме того, в него будут передаваться три массива для зигзага: BufferLastHighBar (для индекса последнего максимума), BufferLastLowBar (для индекса последнего минимума), BufferZigZag (собственно, сам Зигзаг). 

class CZZDraw
  {
private:
public:
   virtual int Calculate(const int rates_total,
                         const int prev_calculated,
                         double &BufferHigh[],
                         double &BufferLow[],
                         double &BufferDirection[],
                         double &BufferLastHighBar[],
                         double &BufferLastLowBar[],
                         double &BufferZigZag[]
                         )
     {
      return(0);
     }
  };
Дочерний класс: 
class CSimpleDraw:public CZZDraw
   {
private:
public:
    virtual int Calculate(const int rates_total,
                          const int prev_calculated,
                          double &BufferHigh[],
                          double &BufferLow[],
                          double &BufferDirection[],
                          double &BufferLastHighBar[],
                          double &BufferLastLowBar[],
                          double &BufferZigZag[]
                          )
      {
       int start;
       if(prev_calculated==0)
         {
          BufferLastHighBar[0]=0;
          BufferLastLowBar[0]=0;
          start=1;
         }
       else
         {
          start=prev_calculated-1;
         }

       for(int i=start;i<rates_total;i++)
         {
          BufferLastHighBar[i]=BufferLastHighBar[i-1];
          BufferLastLowBar[i]=BufferLastLowBar[i-1];

          BufferZigZag[i]=EMPTY_VALUE;

          BufferZigZag[(int)BufferLastHighBar[i]]=BufferHigh[(int)BufferLastHighBar[i]];
          BufferZigZag[(int)BufferLastLowBar[i]]=BufferLow[(int)BufferLastLowBar[i]];

          switch((int)BufferDirection[i])
            {
             case 1:
                switch((int)BufferDirection[i-1])
                  {
                   case 1:
                      if(BufferHigh[i]>BufferHigh[(int)BufferLastHighBar[i]])
                        {
                         BufferZigZag[(int)BufferLastHighBar[i]]=EMPTY_VALUE;
                         BufferZigZag[i]=BufferHigh[i];
                         BufferLastHighBar[i]=i;
                        }
                      break;
                   case -1:
                      BufferZigZag[i]=BufferHigh[i];
                      BufferLastHighBar[i]=i;
                      break;
                  }
                break;
             case -1:
                switch((int)BufferDirection[i-1])
                  {
                   case -1:
                      if(BufferLow[i]<BufferLow[(int)BufferLastLowBar[i]])
                        {
                         BufferZigZag[(int)BufferLastLowBar[i]]=EMPTY_VALUE;
                         BufferZigZag[i]=BufferLow[i];
                         BufferLastLowBar[i]=i;
                        }
                      break;
                   case 1:
                      BufferZigZag[i]=BufferLow[i];
                      BufferLastLowBar[i]=i;
                      break;
                  }
                break;
            }
         }
       return(rates_total);
      }
   };

Подробно рассматривать этот класс мы не будем, все это уже было описано в разделах "Простой зигзаг по high/low" и "Простой зигзаг по close". Файл "CZZDraw.mqh" можно найти в приложении к статье.

Соединяем три класса вместе

Наконец, осталось написать индикатор с использованием трех созданных выше классов. Класс исходных данных обеспечивает возможность использовать данные цены и данные индикатора RSI, обычно работающего в подокне. Ценовые данные можно отобразить в подокне, а индикатор RSI на графике цены — нельзя. Значит, будем создавать индикатор для подокна. 

В редакторе MetaEditor создайте новый индикатор (Главное меню — Файл — Создать или клавиши Ctrl+N). В Мастере создания нового индикатора введите имя "iUniZigZagSW", создайте один внешний параметр "period" (тип int, значение 12), выберите обработчик событий OnCalculate(...,open,high,low,close), создайте следующие буферы: 

НазваниеСтильЦвет
HighLineGreen
LowLineGreen
ZigZagSectionRed
DirectionLinenone
LastHighBarLine none 
LastLowBarLinenone
После создания нового индикатора подключаем к нему три файла с классами: 

#include <CSorceData.mqh>
#include <CZZDirection.mqh>
#include <CZZDraw.mqh>

У индикатора должны быть параметры для выбора типа исходных данных и типа определения направления. Создадим для этого два перечисления:

enum ESorce
  {
   Src_HighLow=0,
   Src_Close=1,
   Src_RSI=2,
   Src_MA=3
  };
enum EDirection
  {
   Dir_NBars=0,
   Dir_CCI=1
  };

Создадим два внешних параметра этих типов: 

input ESorce      SrcSelect=Src_HighLow;
input EDirection  DirSelect=Dir_NBars;

Для исходных данных RSI и МА нужны соответствующие параметры, как и для индикатора CCI. Добавим их:

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;

Еще нам нужен параметр для определения направления по n-bars:

input int                  ZZPperiod   =  14;

Теперь кое-что поинтереснее — три указателя в соответствии с типами базовых классов (ниже внешних параметров):

CSorceData * src;
CZZDirection * dir;
CZZDraw * zz;

В функции OnInit, в соответствии с выбором переменными SrcSelect и DirSelect, загружаем соответствующие дочерние классы. Сначала SrcSelect:

switch(SrcSelect)
  {
   case Src_HighLow:
      src=new CHighLow();
      break;
   case Src_Close:
      src=new CClose();
      break;
   case Src_RSI:
      src=new CRSI(RSIPeriod,RSIPrice);
      break;
   case Src_MA:
      src=new CMA(MAPeriod,MAShift,MAMethod,MAPrice);
      break;
  }

После загрузки проверяем хэндл:

if(!src.CheckHandle())
  {
   Alert("Ошибка загрузки индикатора");
   return(INIT_FAILED);
  }

Следом DirSelect:

switch(DirSelect)
  {
   case Dir_NBars:
      dir=new CNBars(ZZPeriod);
      break;
   case Dir_CCI:
      dir=new CCCIDir(CCIPeriod,CCIPrice);
      break;
  }

Проверка хэндла:

if(!dir.CheckHandle())
  {
   Alert("Ошибка загрузки индикатора 2");
   return(INIT_FAILED);
  }

Третий класс:

zz = new CSimpleDraw();

В функции OnDeinit() удаляем объекты:

void OnDeinit(const int reason)
  {
   if(CheckPointer(src)==POINTER_DYNAMIC)
     {
      delete(src);
     }
   if(CheckPointer(dir)==POINTER_DYNAMIC)
     {
      delete(dir);
     }
   if(CheckPointer(zz)==POINTER_DYNAMIC)
     {
      delete(zz);
     }
  }

И наконец, финальные штрихи — переходим в функцию OnCalculate(). Методы Calculate() классов CSorceData и CZZDirection могут вернуть 0, поэтому проверяем результат. В случае ошибки (получено значение 0), также возвращаем 0, чтобы на следующем тике произошел полный перерасчет:

int rv;

rv=src.Calculate(rates_total,
                 prev_calculated,
                 time,
                 open,
                 high,
                 low,
                 close,
                 tick_volume,
                 volume,
                 spread,
                 HighBuffer,
                 LowBuffer);

if(rv==0)return(0);

rv=dir.Calculate(rates_total,
                 prev_calculated,
                 HighBuffer,
                 LowBuffer,
                 DirectionBuffer);

if(rv==0)return(0);

zz.Calculate(rates_total,
             prev_calculated,
             HighBuffer,
             LowBuffer,
             DirectionBuffer,
             LastHighBarBuffer,
             LastLowBarBuffer,
             ZigZagBuffer);

return(rates_total);

Индикатор "iUniZigZagSW" можно найти в приложении к статье.

Вариант для графика цены

В полученном индикаторе доступны все созданные ранее варианты, как с источником данных, соответствующим графику цены, так и для подокна, поэтому он был создан для подокна. Хотелось бы видеть зигзаг и на графике цены. В этом случае придется пожертвовать источником данных RSI. Делаем копию индикатора с именем "iUniZigZag", меняем свойство indicator_separate_window на indicator_chart_window, из перечисления ESorce удаляем вариант Src_RSI, удаляем вариант с RSI из функции OnInit() и получаем вариант для графика цены. Готовый индикатор "iUniZigZag" можно найти в приложении к статье.   

Вариант для price

Для терминала MetaTrader возможно создание индикаторов, работающих не на строго заданных исходных данных, а на любом другом индикаторе, находящемся на графике. У такого индикатора при добавлении его на график или в подокно параметром "применить к" надо выбрать вариант "данные предыдущего индикатора" или "данные первого индикатора". Переделаем индикатор "iUniZigZagSW" так, чтобы его можно было "накидывать" на другой индикатор. Сохраняем индикатор с именем "iUniZigZagPriceSW" и удаляем все, что связано с классом CSorceData, меняем тип функции OnCalculate, и в начале функции пишем цикл для заполнения буферов HighBuffer и LowBuffer значениями массива price:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[]
                )
  {
   int start;
   if(prev_calculated==0)
     {
      start=0;
     }
   else
     {
      start=prev_calculated-1;
     }

   for(int i=start;i<rates_total;i++)
     {
      HighBuffer[i]=price[i];
      LowBuffer[i]=price[i];
     }
   int rv;
   rv=dir.Calculate(rates_total,
                    prev_calculated,
                    HighBuffer,
                    LowBuffer,
                    DirectionBuffer);

   if(rv==0)return(0);
   zz.Calculate(rates_total,
                prev_calculated,
                HighBuffer,
                LowBuffer,
                DirectionBuffer,
                LastHighBarBuffer,
                LastLowBarBuffer,
                ZigZagBuffer);
   return(rates_total);
  }

Аналогично можно создать вариант, работающий от price на графике цены. Для этого у индикатора "iUniZigZagPriceSW" достаточно изменить свойство indicator_separate_window на indicator_chart_window. Индикатор "iUniZigZagPriceSW" можно найти в приложении, также там есть индикатор iUniZigZagPrice — вариант от price для графика цены. 

Вызов из эксперта

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

Для получения данных от индикатора можно использовать следующий код. Поэкспериментируем с индикатором "iUniZigZagSW". В функции OnInit() загружаем индикатор:

handle=iCustom(Symbol(),Period(),"iUniZigZagSW",SrcSelect,
               DirSelect,
               RSIPeriod,
               RSIPrice,
               MAPeriod,
               MAShift,
               MAMethod,
               MAPrice,
               CCIPeriod,
               CCIPrice,
               ZZPeriod);
В функции OnTick() получаем направление и выводим его в комментарий графика:
   string cs="";
// направление
   double dir[1];
   if(CopyBuffer(handle,3,0,1,dir)<=0)
     {
      Print("Ошибка получения данных от Зигзага");
      return;
     }
   if(dir[0]==1)
     {
      cs=cs+"Направление вверх";
     }
   if(dir[0]==-1)
     {
      cs=cs+"Направление вниз";
     }
   Comment(cs,"\n",GetTickCount());

Теперь получим значения нескольких последних вершин/впадин. Если линия индикатора направлена вверх, то получим индекс бара с последней вершиной из буфера LastHighBarBuffer. Затем с его использованием вычислим индекс бара при отсчете справа налево. С использованием этого индекса получим значение буфера ZigZagBuffer. Можно пойти дальше: для того же бара, на котором получили значение Зигзага, получить значение из буфера LastLowBarBuffer. Это будет индекс бара с предшествующей впадиной. И так далее, чередуя обращение к буферам LastHighBarBuffer и LastLowBarBuffer, мы можем собрать данные о всех вершинах/впадинах линии индикатора. Ниже приведен пример кода для получения двух последних точек зигзага, когда он направлен вверх: 

if(dir[0]==1)
  {
   // индекс бара последней вершины при отсчете от нуля слева
   if(CopyBuffer(handle,4,0,1,lhb)<=0)
     {
      Print("Ошибка получения данных от Зигзага 2");
      return;
     }
   // индекс бара при отсчете справа от нуля
   ind=bars-(int)lhb[0]-1;

   // значение Зигзага на баре ind
   if(CopyBuffer(handle,2,ind,1,zz)<=0)
     {
      Print("Ошибка получения данных от Зигзага 3");
      return;
     }
   //===
   // индекс впадины, предшествующей этой вершине
   if(CopyBuffer(handle,5,ind,1,llb)<=0)
     {
      Print("Ошибка получения данных от Зигзага 4");
      return;
     }
   // индекс бара при отсчете справа от нуля
   ind=bars-(int)llb[0]-1;

   // значение Зигзага на баре ind
   if(CopyBuffer(handle,2,ind,1,zz1)<=0)
     {
      Print("Ошибка получения данных от Зигзага 5");
      return;
     }

   cs=cs+"\n"+(string)zz1[0]+" "+(string)zz[0];
  }
else if(dir[0]==-1)
  {

  }

Полный пример можно найти в приложении в эксперте с именем "eUniZigZagSW". Эксперт выводит в комментарий графика сообщение о направлении Зигзага и второй строкой — два числа со значениями двух последних точек зигзага (рис. 7). Третья строка это просто число, возвращаемое функцией GetTickCount(), чтобы было видно, что эксперт работает .

 
Рис. 7. В левом вернем углу сообщения, выводимые экспертом

Конечно, данные о двух последних точках индикатора можно получить от буферов LastHighBarBuffer и LastLowBarBuffer, взяв их значения на нулевом или первом баре, но смысл данного примера — в последовательном извлечении данных от любого количества точек Зигзага.

Вызов из другого индикатора

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

Сам по себе Зигзаг на истории — это уже не то, что было в процессе формирования этой истории, однако у нас есть буферы LastHighBarBuffer и LastLowBarBuffer, в которых сохраняются данные о промежуточных состояниях Зигзага. Для наглядности напишем индикатор, рисующий стрелки при смене направления линии индикатора (изменение значения буфера DirectionBuffer) и отмечающий точки на барах, на которых были зафиксированы новые максимумы/минимумы Зигзага (изменение значений буферов LastHighBarBuffer и LastLowBarBuffer). Подробно рассматривать код этого индикатора не будем, в приложении к статье его можно увидеть под названием "iUniZigZagSWEvents". Вид индикатора показан на рис. 8.

 
Рис. 8. Индикатор iUniZigZagSWEvents

Заключение

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

Файлы приложения

  • iHighLowZigZag.mq5 — простой Зигзаг по high/low.
  • iCloseZigZag.mq5 — простой Зигзаг по close.
  • CSorceData.mqh — класс для выбора исходных данных.
  • CZZDirection.mqh — класс определения направления Зигзага.
  • CZZDraw.mqh — класс рисования Зигзага.
  • iUniZigZagSW.mq5 — универсальный Зигзаг для подокна.
  • iUniZigZag.mq5 — универсальный Зигзаг для графика цены.
  • iUniZigZagPriceSW.mq5 — универсальный Зигзаг от price для подокна.
  • iUniZigZagPrice.mq5 — универсальный Зигзаг от price для графика цены. 
  • eUniZigZagSW — пример вызова индикатора "iUniZigZagSW" из эксперта через функцию iCustom().
  • iUniZigZagSWEvents — пример создания другого индикатора с вызовом индикатора "iUniZigZagSW" через функцию iCustom(). 
Прикрепленные файлы |
mql5.zip (18.42 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (25)
Dmitry Fedoseev
Dmitry Fedoseev | 21 окт. 2016 в 17:25
Karputov Vladimir:
Я, наверное сейчас, всё что не по теме вынесу в отдельную ветку: от сюда и ещё со второго обсуждения статьи...
Выносите (если хотите), только я не буду там участвовать. Здесь отвечаю только поскольку это комменты к моей статье. 
Vladimir Karputov
Vladimir Karputov | 21 окт. 2016 в 17:27
Dmitry Fedoseev:
Выносите (если хотите), только я не буду там участвовать. Здесь отвечаю только поскольку это комменты к моей статье. 
Я наоборот думал, что Вы против, так как совсем не по статье обсуждение. Видно ошибся.
Kofa
Kofa | 20 нояб. 2017 в 23:42
MetaQuotes Software Corp.:

Опубликована статья Универсальный Зигзаг:

Автор: Dmitry Fedoseev


Здравствуйте, Дмитрий! Нет ли у Вас индикатора, аналогичного iHighLowZigZag, только для мт 4 ?  В индикаторе, основанном на зигзаге, для поиска экстремумов, их времени и цены использую эти функции https://www.forexdengi.com/showthread.php?t=148&page=4. Очень удобно в любой части кода получить значения. Но это очень сильно загружает систему. В тестере вообще невозможно прогнать.

Sealdo Сергей
Sealdo Сергей | 23 мая 2020 в 12:28
iHighLowZigZag. Ура! Он правильно без ошибок рисуется в динамике (тестере). В MT5(2306) стандартные ZigZag-и рисуются в тестере с периодическими ошибками. Спасибо!!! :)
Nikolay Kuznetsov
Nikolay Kuznetsov | 18 нояб. 2020 в 12:20
MT5 2690 первые два варианта зигзага без ООП:
  • iHighLowZigZag.mq5 — простой Зигзаг по high/low.
  • iCloseZigZag.mq5 —
  • работают в тестере на истории и при установке на реальных данных,  но вылетают при отладке на реальных данных, без сообщений об ошибках. Отчего так?

    Торговая стратегия '80-20' Торговая стратегия '80-20'
    В статье описывается создание инструментов (индикатора и советника) для исследования торговой стратегии '80-20'. Правила ТС взяты из книги Линды Рашке и Лоуренса Коннорса "Биржевые секреты. Высокоэффективные стратегии краткосрочной торговли". На языке MQL5 формализованы правила этой стратегии, а созданные на ее основе индикатор и советник протестированы на современной истории рынка.
    Графические интерфейсы X: Элемент "Стандартный график" (build 4) Графические интерфейсы X: Элемент "Стандартный график" (build 4)
    На этот раз мы рассмотрим такой элемент графического интерфейса, как Стандартный график. С его помощью можно будет создавать массивы объектов-графиков с возможностью синхронизированной горизонтальной прокрутки. Кроме этого, продолжим оптимизировать код библиотеки для уменьшения потребления ресурсов процессора.
    LifeHack для трейдера: Сравнительный отчет нескольких тестирований LifeHack для трейдера: Сравнительный отчет нескольких тестирований
    В статье рассматривается одновременный запуск тестирования советника сразу на четырёх разных символах. Итоговое сравнение четырёх отчётов тестирования приводится в одной таблице, как при выборе товаров в интернет-магазинах. Дополнительным бонусом идут автоматически создаваемые графики распределений для каждого символа.
    Основы программирования на MQL5: Глобальные переменные терминала MetaTrader 5 Основы программирования на MQL5: Глобальные переменные терминала MetaTrader 5
    Глобальные переменные терминала — незаменимое средство при разработке сложных и надежных экспертов. Освоив работу с глобальными переменными терминала, вы уже не сможете представить себе создание экспертов на MQL5 без их использования.