English 中文 Español Deutsch 日本語 Português
Волны Вульфа

Волны Вульфа

MetaTrader 5Примеры | 12 апреля 2017, 10:23
11 727 15
Dmitry Fedoseev
Dmitry Fedoseev

Содержание

Введение

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

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

Правила определения волн Вульфа

Рассмотрим волны Вульфа на примере покупки (рис. 1). Цена образует две последовательно снижающихся впадины (синяя линия, точки 1 и 3) и два последовательно снижающихся пика (точки 2 и 4). После разворота и образования вершины в точке 4 цена продолжает падать, при образовании ценой линии 1—3 выполняется покупка (точка 5). 


Рис. 1. Волны Вульфа для покупки. Синяя линия — цена, красная линия — построение для определения цели. Вход выполняется в точке 5, цель — точка 7

Точка 6, получаемая пересечением линий 1—3 и 2— 4 определяет время достижения цели. Значение цели (точка 7) определяется как пересечение линий 1— 4 и вертикальной линии, проведенной через точку 6. Алгоритма расчета уровня стоп-лосса в методике нет, есть лишь общий совет использовать его на свое усмотрение. На этом изложение правил определения волн в книге Вульфа заканчивается. 

При разработке индикатора для этой статьи было добавлено еще несколько правил.

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

    v3<v1-d1

    где:

    • v3 — уровень точки 3;
    • v1 — уровень точки 1;
    • d1 — расстояние по вертикали между точками 1 и 2 (отрезок 1''-2''), умноженное на коэффициент K1 (коэффициент K1 — параметр в окне свойств, значение по умолчанию 0.1).

  2. Линия 1—4, определяющая цель, должна быть направлена вверх, то есть, точка 4 должна быть заметно выше точки 1. Для этого проверяется условие:

    v4>v1+d1;

    где v4 — уровень точки 4. 

  3. Точка 4 должна быть заметно ниже точки 2, это достигается проверкой условия:

    v4<v2-d2;

    где v2 — уровень точки 2, d2 — расстояние по вертикали между точками 2 и 3 (отрезок 2''-3'')? умноженное на коэффициент K2 (коэффициент K2 — параметр в окне свойств, значение по умолчанию 0.1).

  4. Линии 2-4 и 1-3, определяющие время достижения цели, должны пересечься справа, для этого высота 2-2' должна быть заметно больше высоты 4-4'. Выполнение этого условия осуществляется проверкой:

    h2-h4>K3*h2;

    где h2 — величина отрезка 2-2', h4 — величина отрезка 4-4', K3 — коэффициент (коэффициент K3 является параметром в окне свойств, значение по умолчанию 0.1).

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

Выбор Зигзага для доработки

Прежде чем приступить к работе, скачаем приложение к статье "Универсальный зигзаг", в котором есть много различных вариантов индикатора Зигзаг. Определимся, какой из них мы возьмем за основу. Сразу исключим варианты iUniZigZagPrice и iUniZigZagPriceSW, которые предназначены для расчета Зигзага от какого-либо другого индикатора, находящегося на графике, и поэтому представляют интерес только для визуального наблюдения и анализа. Больший интерес представляют остальные индикаторы, каждый из которых можно использовать для создания экспертов. Исключим также варианты iCloseZigZag и iHighLowZigZag — это только начальные примеры создания Зигзага. Остаются два варианта: iUniZigZag и iUniZigZagSW. Из них более предпочтителен iUniZigZagSW, работающий в подокне, в силу более широких возможностей. Но в приложении есть еще индикатор iUniZigZagSWEvents представляющий собой пример использования функции iCustom() для обращения к индикатору iUniZigZagSW. На этом варианте мы и остановимся, поскольку он позволит не только использовать все возможности индикатора iUniZigZagSW, но и полностью отделить код выявления волн Вульфа от кода Зигзага.

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

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

  • Src_HighLow — по ценам high и low;
  • Src_Close — по ценам close;
  • Src_RSI — по индикатору RSI;
  • Src_MA — по индикатору MA.

На основе создаваемого в статье индикатора будет построен торговый эксперт. Поэтому, если строить Зигзаг от цены, то для установки тейк-профита можно использовать прогнозируемую цель. С отображением цели на графике проблем тоже не возникнет. Если же строить Зигзаг по RSI (SrcSelect=Src_RSI), то и прогнозируемая цель будет не для цены, а для индикатора RSI.  Значит, при достижении целевого значения индикатором RSI надо будет выполнять рыночное закрытие, а самое главное —  будет невозможно отобразить на графике целевую цену и вспомогательные построения.

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

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

Сбор данных о вершинах Зигзага

Приступаем к созданию индикатора. Открываем в редакторе файл iUniZigZagSWEvents и сохраняем его с именем iWolfeWaves. В нем и будем работать.

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

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

struct SPeackTrough{
   double   Val; // значение
   int      Dir; // направление
   int      Bar; // индекс бара
};

Создадим массив этих структур:

SPeackTrough PeackTrough[];

Если бы использовался только Зигзаг по high и low (SrcSelect = Src_HighLow), при смене направления было бы достаточно увеличить массив, установить значения и обновлять последний элемент по мере продления последнего отрезка индикатора. С Зигзагом по цене close (SrcSelect = Src_Close) или по данным любого другого индикатора дело обстоит сложнее. По мере формирования бара, на котором произошла смена направления, Зигзаг может вернуться в исходное состояние (предшествующее открытию формирующегося бара). Значит, при каждом новом обсчете одного и того же бара массив для вершин нужно возвращать в исходное состояние, которое было у него на предыдущем баре. Если часто менять размер массива, это может замедлить индикатор. Поэтому введем дополнительную переменную, в которой будет храниться используемый размер массива, а сам массив будет изменяться блоками по мере необходимости только в сторону увеличения размера. Перед повторным обсчетом одного и того же бара будем возвращать исходное значение этой переменной.

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

int PreCount; // размер массива PeackTrough на предыдущем баре
int CurCount; // размер массива PeackTrough на обсчитываемом баре

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

datetime LastTime;

Переменные PreCount, LastCount, LastTime — глобальные переменные индикатора. Но они могут быть объявлены и как статические в функции OnCalculate() индикатора. 

Перейдем к работе в функции OnCalculate(). По значению переменной prev_calculated определяется, выполняется первый расчет индикатора или же обсчет только новых баров. Значение 0 означает полный расчет, при этом надо будет инициализировать переменные PreCount, CurCount и LastTime. Следующий код, определяющий диапазон баров для расчета и инициализирующий вспомогательные переменные, располагается в самом верху функции OnCalculte():

int start; // переменная для индекса первого обсчитываемого бара

if(prev_calculated==0){ // полный обсчет всех баров
   start=1;
   CurCount=0;   
   PreCount=0;
   LastTime=0;
}
else{ // обсчет нового бара
   start=prev_calculated-1;
}

Теперь займемся стандартным индикаторным циклом, в его начале обеспечим перемещение значений в переменных PreCount, CurCount:

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

   if(time[i]>LastTime){ // первый обсчет нового (очередного) бара
      LastTime=time[i];
      PreCount=CurCount;
      PreDir=CurDir;
   }
   else{ // повторный обсчет бара
      CurCount=PreCount;
      CurDir=PreDir;
   }

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

Далее в главном индикаторном цикле должен располагаться следующий код, оставшийся от индикатора iUniZigZagSWEvents:

UpArrowBuffer[i]=EMPTY_VALUE;
DnArrowBuffer[i]=EMPTY_VALUE;

UpDotBuffer[i]=EMPTY_VALUE;
DnDotBuffer[i]=EMPTY_VALUE;      

// направление

double dir[2];
if(CopyBuffer(handle,3,rates_total-i-1,2,dir)<=0){
   return(0);
}
if(dir[0]==1 && dir[1]==-1){
   DnArrowBuffer[i]=high[i];
   c++;

}
else if(dir[0]==-1 && dir[1]==1){
   UpArrowBuffer[i]=low[i];
   c++;
}

// новый максимум

double lhb[2];
if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){
   return(0);
}
if(lhb[0]!=lhb[1]){
   UpDotBuffer[i]=high[i];
}

// новый минимум

double llb[2];
if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){
   return(0);
}
if(llb[0]!=llb[1]){
   DnDotBuffer[i]=low[i];
}  

Часть кода, рисующего стрелки нам не нужна, удалим ее.

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

Для слежения за направлением зигзага и определения моментов их смены потребуется еще пара переменных, подобных переменным CurCount и PreCount: это переменные PreDir и CurDir:

int PreDir; // направление зигзага на предыдущем баре
int CurDir; // направление зигзага на обсчитываемом баре

Они также могут быть как глобальными так и статическими в функции OnCalculate(). В начале обсчета индикатора их тоже надо инициализировать и перемещать их значения при каждом обсчете бара — всё как и с переменными PreCount и CurCount. Ниже приведен окончательный код функции OnCalculate() на данном этапе создания индикатора:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {

   int start; // переменная для начального бара обсчета
   
   if(prev_calculated==0){ // полный обсчет 
      start=1; 
      CurCount=0;
      PreCount=0;
      CurDir=0;
      PreDir=0;      
      LastTime=0;
   }
   else{ // обсчет только новых баров
      start=prev_calculated-1;
   }

   // основной индикаторный цикл
   for(int i=start;i<rates_total;i++){
   
      if(time[i]>LastTime){ // новый бар
         LastTime=time[i];
         PreCount=CurCount;
         PreDir=CurDir;
      }
      else{ // повторный обсчет бара
         CurCount=PreCount;
         CurDir=PreDir;
      }

      // очистка буферов, рисующих стрелки и точки
      
      UpArrowBuffer[i]=EMPTY_VALUE;
      DnArrowBuffer[i]=EMPTY_VALUE;
      
      UpDotBuffer[i]=EMPTY_VALUE;
      DnDotBuffer[i]=EMPTY_VALUE;    
      
      // вспомогательные переменные
      
      double hval[1];
      double lval[1];
      
      double zz[1];
      
      // новый максимум
      
      double lhb[2];
      // получаем два элемента буфера с индексами баров новых максимумов
      if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){ 
         return(0);
      }
      if(lhb[0]!=lhb[1]){ // есть новый максимум
         // получаем значение максимума цены (или данных, по которым рассчитывается Зигзаг)
         if(CopyBuffer(handle,0,rates_total-i-1,1,hval)<=0){
            return(0);
         }      
         if(CurDir==1){ // последнее известное направление вверх 
            // обновляем данные о последней точке зигзага
            RefreshLast(i,hval[0]);
         }
         else{ // Зигзаг изменил направление
               // добавляем новое значение
            AddNew(i,hval[0],1);
         }
         // здесь будет проверка условий на выявление волн Вульфа вниз  
      }
      
      // новый минимум
      
      double llb[2];
      // получаем два элемента буфера с индексами баров новых минимумов
      if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){
         return(0);
      }
      if(llb[0]!=llb[1]){ // есть новый минимум
         // получаем значение минимума цены (или данных, по которым рассчитывается Зигзаг)
         if(CopyBuffer(handle,1,rates_total-i-1,1,lval)<=0){
            return(0);
         }         
         if(CurDir==-1){ // последнее известное направление вниз
            // обновляем данные о последней точке Зигзага
            RefreshLast(i,lval[0]);
         }
         else{ // Зигзаг изменил направление
            // добавляем новое значение
            AddNew(i,lval[0],-1);
         }
         // здесь будет проверка условий на выявление волн Вульфа вверх 
      }      
   }   
   return(rates_total);
}

В этом коде встречаются функции AddNew() и RefreshLast(). В обе функции передаются индекс бара, на котором произошло изменение Зигзага, и значение нового экстремума, а в функцию AddNew() — еще и направление зигзага.

Функция добавления новой точки AddNew():

void AddNew(int i,double v,int d){
   if(CurCount>=ArraySize(PeackTrough)){ // в массиве нет свободных элементов
      ArrayResize(PeackTrough,ArraySize(PeackTrough)+1024); // увеличение размера массива
   }
   PeackTrough[CurCount].Dir=d; // установка направления
   PeackTrough[CurCount].Val=v; // установка значения
   PeackTrough[CurCount].Bar=i; // установка бара
   CurCount++; // увеличения переменной с количеством занятых элементов массива   
   CurDir=d; // запоминаем последнее направление Зигзага
} 

Функция обновления последней точки RefreshLast():

void RefreshLast(int i,double v){
   PeackTrough[CurCount-1].Bar=i; // установка нового бара
   PeackTrough[CurCount-1].Val=v; // установка нового значения
} 

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

Немного геометрии

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

Задача 1. Прямая линия задана парой точек x-y, где х — это индекс бара, у — значение (цена или значение индикатора). Дана координата x третьей точки, необходимо найти значение линии в этой точке (рис. 2).


Рис. 2. Задано: X1, Y1, X2, Y2, X3. Необходимо найти Y3.

Решение задачи 1. Определяем величину прироста линии по оси Y на единицу прироста по оси X:

D=(Y2-Y1)/(X2-X1)

Где D — величина прироста, Y1 — значение цены или индикатора в точке 1, Y2 — значение цены или индикатора в точке 2, X1 — индекс бара в точке 1, X2 — индекс бара в точке 2.  

Определяем Y3:

Y3=Y1+(X3-X1)*D

Где X3 — индекс бара в точке 3, Y3 — искомое значение линии в точке 3.

Получаем такую функцию:

double y3(double x1,double y1,double x2,double y2,double x3){
   return(y1+(x3-x1)*(y2-y1)/(x2-x1));
}

В функцию передаются следующие параметры:

  • x1 — индекс бара точки 1;
  • y1 — значение в точке 1;
  • x2 — индекс бара в точке 2;
  • y2 — значение в точке 2.

Задача 2. Двумя парами точек х-у заданы две линии. Необходимо найти координату x точки их пересечения (рис. 3). Здесь может возникнуть вопрос: почему именно координату x? Это не принципиально, ведь после получения координаты x в любом случае будет вычисляться координата y точки 3 (с использованием уравнения одной из прямых). Поэтому сначала можно получить и координату y точки 3, а затем, по уравнению одной из прямых, вычислить ее абсциссу.  


Рис. 3. Задано две линии, необходимо найти точку их пересечения


Сначала, используя координаты двух точек получим уравнения линий в форме y=a+b*x.

Выполним предварительные вычисления. Величина наклона линии (в единицах по оси y на единицу по оси x):

D1=(Y12-Y11)/(X12-X11)

Где D1 — искомая величина наклона первой линии (величина изменения значения линии на один бар), X11 — индекс бара в точке 1 первой линии, X12 — индекс бара в точке 2 первой линии, Y11 — значение первой линии в точке 1, Y12 — значение первой линии в точке 2.     

Величина наклона линии 2:

D2=(Y22-Y21)/(X22-X21)

Где D2 — искомая величина наклона второй линии (величина изменения значения линии на один бар), X21 — индекс бара в точке 1 второй линии, X22 — индекс бара в точке 2 второй линии, Y21 — значение второй линии в точке 1, Y22 — значение второй линии в точке 2.

Теперь получим уравнения линий. Уравнение линии 1:

Y3=Y11+D1*(X3-X11)

Где Y3 — значение линии в точке пересечения (точка 3), X3 — индекс бара в точке 3.

Уравнение линии 2:

Y3=Y21+D2*(X3-X21)

В точке пересечения значения линий равны, поэтому приравняем уравнение линии 1 к уравнению линии 2:

Y11+D1*(X3-X11)=Y21+D2*(X3-X21);

Используя полученное выражение, выразим X3. В результате получим функцию TwoLinesCrossX() для определения координаты X пересечения двух линий:

double TwoLinesCrossX(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){
   double k2=(y22-y21)/(x22-x21);
   double k1=(y12-y11)/(x12-x11);
   return((y11-y21-k1*x11+k2*x21)/(k2-k1));
}

В функцию передаются следующие параметры:

  • x11 — индекс бара точки 1 первой линии;
  • y11 — значение в точке 1 у первой линии;
  • x12 — индекс бара точки 2 первой линии;
  • y12 — значение в точке 2 у первой линии;
  • x21 — индекс бара точки 1 второй линии;
  • y21 — значение в точке 1 у второй линии;
  • x22 — индекс бара точки 2 второй линии;
  • y22 — значение в точке 2 у второй линии.

После того, как определена координата x точки пересечения двух линий, координату y можно вычислить, используя координаты двух точек одной из линий и функцию y3() полученную при решении задачи 1.

Если сначала необходимо получить координату y, то уравнения линий надо преобразовать так, чтобы они выражали координату x через y. Сначала для одной линии:

X3=X11+(Y3-Y11)/D1

Для второй линии:

X3=X21+(Y3-Y21)/D2

Приравняем оба уравнения:

X11+(Y3-Y11)/D1=X21+(Y3-Y21)/D2

Из полученного уравнения необходимо выразить Y3. В итоге получим функцию TwoLinesCrossY():

double TwoLinesCrossY(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){
   double k2=(x22-x21)/(y22-y21);
   double k1=(x12-x11)/(y12-y11);
   return((x11-x21-k1*y11+k2*y21)/(k2-k1));
}

Параметры этой функции такие же, как у TwoLinesCrossX(). 

Определение волн

Теперь, имея легкий доступ ко всем вершинам зигзага и вспомогательные геометрические функции, займемся непосредственно определением волн Вульфа. Необходимо "поймать" момент, когда последний отрезок зигзага пересечет линию 1-3 (см. рис. 1), то есть точку 5. Значит, проверять условия, выявляющие волны Вульфа, будем при каждом появлении нового экстремума Зигзага (как при смене направления, так и при продлении последнего отрезка). Выше, в коде функции OnCalculate() подробно прокомментированы места, в которых должна выполняться проверка условий. Из них будет вызываться функция CheckDn() и CheckUp(). Рассмотрим подробно одну из них — функциюCheckUp():

void CheckUp(int rates_total,const double & low[],const datetime & time[],int i){

   if(CurCount<5 || CurDir!=-1){ 
      // если мало вершин или Зигзаг не направлен вниз, не выполняем проверку
      return;
   }   
   
   // подготавливаем короткие переменные с данными о вершинах 

   // переменные со значениями вершин
   double v1=PeackTrough[CurCount-5].Val;
   double v2=PeackTrough[CurCount-4].Val;
   double v3=PeackTrough[CurCount-3].Val;
   double v4=PeackTrough[CurCount-2].Val;
   double v5=PeackTrough[CurCount-1].Val;
   
   // переменные с барами вершин
   int i1=PeackTrough[CurCount-5].Bar;
   int i2=PeackTrough[CurCount-4].Bar;               
   int i3=PeackTrough[CurCount-3].Bar;
   int i4=PeackTrough[CurCount-2].Bar;
   int i5=PeackTrough[CurCount-1].Bar;
                  
   if(CurLastBuySig!=i4){ // если на этой конфигурации Зигзага еще не были выявлены волны
      double d1=K1*(v2-v1); // минимальная величина смещения вершины 3 относительно вершины 1
      if(v3<v1-d1){ // вершина 3 находится заметно ниже вершины 1
         if(v4>v1+d1){ // линия 1-4 наклонена вверх
            double d2=K2*(v2-v3); // минимальная величина смещения вершины 4 относительно вершины 2
            if(v4<v2-d2){ // вершина 4 находится заметно ниже вершины 2
               double v5l=y3(i1,v1,i3,v3,i); // значение точки 5
               if(v5<v5l){ // последний отрезок зигзага пересек линию 1-3
                  double v4x=y3(i1,v1,i3,v3,i4); // значение в точке 4'
                  double v2x=y3(i1,v1,i3,v3,i2); // значение в точке 2'
                  double h4=v4-v4x; // высота 4-4'
                  double h2=v2-v2x; // высота 2-2'
                  if(h2-h4>K3*h2){ // линии 1-3 и 2-4 сходятся справа
                     double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // бар пересечения линий 1-3 и 2-4
                     double tv=y3(i1,v1,i4,v4,tb); // значение в точке пересечения линий 1-3 и 2-4
                     UpArrowBuffer[i]=low[i]; // отображение стрелки вверх
                     UpDotBuffer[i]=tv; // отображение точки на уровне цели
                     CurLastBuySig=i4; // запоминаем, что на данной конфигурации зигзага фигура выявлена
                     if(_DrawWaves){ // рисование фигуры и построения
                        DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total);
                     }
                  }
               }
            }
         }
      }
   }
}

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

if(CurCount<5 || CurDir!=-1){ 
   // если мало вершин или Зигзаг не направлен вниз, не выполняем проверку
   return;
}   

Для получения данных о вершинах можно обращаться непосредственно к массиву PeackTrough, но это неудобно. Проще пользоваться вспомогательными переменными с короткими именами:

// переменные со значениями вершин
double v1=PeackTrough[CurCount-5].Val;
double v2=PeackTrough[CurCount-4].Val;
double v3=PeackTrough[CurCount-3].Val;
double v4=PeackTrough[CurCount-2].Val;
double v5=PeackTrough[CurCount-1].Val;
   
// переменные с барами вершин
int i1=PeackTrough[CurCount-5].Bar;
int i2=PeackTrough[CurCount-4].Bar;               
int i3=PeackTrough[CurCount-3].Bar;
int i4=PeackTrough[CurCount-2].Bar;
int i5=PeackTrough[CurCount-1].Bar;

Если волна выявлена, стрелка поставлена, то больше не нужно использовать эту же конфигурацию Зигзага. Идентификация конфигурации зигзага выполняется проверкой индекса вершины 4 (последней сформированной вершины):

if(CurLastBuySig!=i4){ // если на этой конфигурации зигзага еще не были выявлены волны

Для хранения значения идентификатора конфигурации используется пара переменных, подобных переменным CurCount и PreCount.

Теперь мы подошли непосредственно к определению волн. Рассчитывается величина минимального смещения точки 3 относительно точки 1 и точки 2 относительно точки 1:

double d1=K1*(v2-v1); // минимальная величина смещения вершины 3 относительно вершины 1

Следом проверяются смещения точек:

if(v3<v1-d1){ // вершина 3 находится заметно ниже вершины 1
   if(v4>v1+d1){ // линия 1-4 наклонена вверх

Рассчитываем минимальную величину смещения точки 4 относительно точки 2:

double d2=K2*(v2-v3); // минимальная величина смещения вершины 4 относительно вершины 2

Проверяем положение точек 2 и 4:

if(v4<v2-d2){ // вершина 4 находится заметно ниже вершины 2

Вычисляем значение точки, находящейся на линии 1-3 и соответствующей обсчитываемому бару:

double v5l=y3(i1,v1,i3,v3,i); // значение точки 5

Проверяем, произошло ли касание линии 1-3:

if(v5<v5l){ // последний отрезок зигзага пересек линию 1-3

Вычисляем значения в точках 4' и 2':

double v4x=y3(i1,v1,i3,v3,i4); // значение в точке 4'
double v2x=y3(i1,v1,i3,v3,i2); // значение в точке 2'

Вычисляем высоты 4-4' и 2-2':

double h4=v4-v4x; // высота 4-4'
double h2=v2-v2x; // высота 2-2'

Используя эти высоты, проверяем сходимость линий  1-3 и 2-4 справа:

if(h2-h4>K3*h2){ // линии 1-3 и 2-4 сходятся справа

Выполнение этого условия означает, что найдена волна. 

Определяем цель. Сначала бар достижения цели:

double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // бар пересечения линий 1-3 и 2-4

Заметьте, что для точности вычислений используется переменная double.     

Значение цели:

double tv=y3(i1,v1,i4,v4,tb); // значение в точке пересечения линий 1-3 и 2-4

 Отображаем значки и "запоминаем" идентификатор конфигурации зигзага:

UpDotBuffer[i]=tv; // отображение точки на уровне цели
CurLastBuySig=i4; // запоминаем, что на данной конфигурации зигзага фигура выявлена

Наконец, рисуем волны и построение, определяющее цель:

if(_DrawWaves){ // рисование фигуры и построения
   DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total);
}

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

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

void CheckDn(int rates_total,const double & high[],const datetime & time[],int i){

   // мало вершин или направление не вверх 
   if(CurCount<5 || CurDir!=1){ 
      return;
   }

   double v1=PeackTrough[CurCount-5].Val;
   double v2=PeackTrough[CurCount-4].Val;
   double v3=PeackTrough[CurCount-3].Val;
   double v4=PeackTrough[CurCount-2].Val;
   double v5=PeackTrough[CurCount-1].Val;
   
   int i1=PeackTrough[CurCount-5].Bar;
   int i2=PeackTrough[CurCount-4].Bar;               
   int i3=PeackTrough[CurCount-3].Bar;
   int i4=PeackTrough[CurCount-2].Bar;
   int i5=PeackTrough[CurCount-1].Bar;
   
   if(CurLastSellSig!=i4){               
      double d1=K1*(v1-v2); // вершина v1 выше вершины v2
      if(v3>v1+d1){ // вершина v3 выше вершины v1
         if(v4<v1-d1){ // вершина v4 ниже вершины v1                     
            double d2=K2*(v3-v2); // вершина v3 выше вершины v2                     
            if(v4>v2+d2){ // вершина v4 выше вершины v2  
               double v5l=y3(i1,v1,i3,v3,i);
               if(v5>v5l){ // зигзаг пробивает линию 1-3 вверх
                  double v4x=y3(i1,v1,i3,v3,i4);
                  double v2x=y3(i1,v1,i3,v3,i2);
                  double h4=v4x-v4; // точка 4' выше точки 4
                  double h2=v2x-v2; // точка 4' выше точки 4
                  if(h2-h4>K3*h2){   
                     double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4);
                     double tv=y3(i1,v1,i4,v4,tb);                              
                     DnArrowBuffer[i]=high[i];
                     DnDotBuffer[i]=tv;
                     CurLastSellSig=i4;   
                     if(_DrawWaves){
                        // рисование с другими цветами
                        DrawObjects(SellColor,
                                    SellTargetColor,
                                    v1,
                                    v2,
                                    v3,
                                    v4,
                                    v5l,
                                    i1,
                                    i2,
                                    i3,
                                    i4,
                                    i5,
                                    time,
                                    i,
                                    tb,
                                    tv,
                                    rates_total);
                     }
                  }
               }
            }
         }
      }
   }
}

Первое отличие в начальной проверке:

// мало вершин или направление не вверх 
if(CurCount<5 || CurDir!=1){ 
   return;
}

Если мало вершин или зигзаг направлен вниз, завершаем работу функции.

Для направления вниз пики и впадины меняются местами: точки 1, 3, 5, 6 находятся сверху, а точки  2, 4, 7 — снизу, поэтому некоторые переменные в формулах меняются местами. Определение минимального расстояния между вершинами 1, 3 и 1, 4:

double d1=K1*(v1-v2); // вершина v1 выше вершины v2

Проверка положения вершин 1, 3:

if(v3>v1+d1){ // вершина v3 выше вершины v1

Проверка положения вершин 1, 4:

if(v4<v1-d1){ // вершина v4 ниже вершины v1 

Вычисление минимального расстояния между вершинами 2, 3 и его проверка:

double d2=K2*(v3-v2); // вершина v3 выше вершины v2                     
if(v4>v2+d2){ // вершина v4 выше вершины v2  

Проверка, не образовалась ли точка 5 (зигзаг пробивает линию 1-3 вверх):

if(v5>v5l){ // зигзаг пробивает линию 1-3 вверх

Расчет высот 2-2' и 4-4' для проверки сходимости линий 1-3 и 2-4 справа:

double h4=v4x-v4; // точка 4' выше точки 4
double h2=v2x-v2; // точка 4' выше точки 4

Рисование волны и построений выполняется с другим цветом:

// рисование с другими цветами
DrawObjects(SellColor,
            SellTargetColor,
            v1,
            v2,
            v3,
            v4,
            v5l,
            i1,
            i2,
            i3,
            i4,
            i5,
            time,
            i,
            tb,
            tv,
            rates_total);

Рисование волн и цели

Все волны и построения отрисовываются с помощью единого алгоритма, поэтому используется одна функция DrawObjects(). Элементы, направленные вверх и вниз, отрисовываются разным цветом. Для этого в функцию передается параметр цвета BuyColor или SellColor. Также разным цветом отрисовываются волны и построений, определяющих цель, поэтому в функцию передаются еще параметры BuyTargetColor или SellTargetColor. Эти переменные — внешние переменные индикатора, с помощью которых можно установить удобные для себя цвета. Кроме цвета, потребуется еще несколько внешних параметров. Ниже приведены все дополнительные параметры для функции рисования волн и построений:

input bool   DrawWaves       =  true;             // включение рисования волн и построений
input color  BuyColor        =  clrAqua;          // цвет волн покупки
input color  SellColor       =  clrRed;           // цвет волн продажи
input int    WavesWidth      =  2;                // толщина волн
input bool   DrawTarget      =  true;             // дополнительное включение/выключение построений
input int    TargetWidth     =  1;                // толщина построений
input color  BuyTargetColor  =  clrRoyalBlue;     // цвет построений для покупки
input color  SellTargetColor =  clrPaleVioletRed; // цвет построений для продажи

После цвета в функцию предаются переменные со значениями и индексами баров вершин. Исключение составляет значение вершины 5, для которой передается не значение на конце отрезка зигзага, а рассчитанное значение на линии 1-3. Координаты всех точек зигзага указаны в барах, а для графических объектов нужно время, поэтому в функцию передается указатель на массив time. Далее передается индекс обсчитываемого бара — i, бар цели — tb, значение цели — tv и общее количество баров на графике — rates_total. 

Как уже отмечалось в начале статьи, рисование волн и построений должно выполняться, только если выбран Зигзаг по high/low (SrcSelect равно Src_HighLow) или close (SrcSelect равно Src_Close). Значит, в функции OnInit(), в зависимости от значения переменой SrcSelect, должно выполняться принудительное отключение рисования (переменной DrawWaves).  Для этого объявим дополнительную переменную, которая будет использоваться вместо переменной DrawWaves:

bool _DrawWaves;

Далее в функции OnInit() установим ей значение переменной DrawWaves или отключим — установим значение false. Дополнительно установим буферу для рисования цели невидимый цвет:

if(SrcSelect==Src_HighLow || SrcSelect==Src_Close){
   _DrawWaves=DrawWaves;
}
else{
   _DrawWaves=false;
   PlotIndexSetInteger(2,PLOT_LINE_COLOR,clrNONE);
   PlotIndexSetInteger(3,PLOT_LINE_COLOR,clrNONE);      
}   

Перейдем к рассмотрению функции DrawObjects(). Сначала приведем весь код функции, затем рассмотрим его более подробно:

void DrawObjects( color col,
                  color tcol,
                  double v1,
                  double v2,
                  double v3,
                  double v4,
                  double v5,
                  int i1,
                  int i2,
                  int i3,
                  int i4,
                  int i5,
                  const datetime & time[],
                  int i,
                  double target_bar,
                  double target_value,
                  int rates_total){

   // префикс имен графических объектов 
   string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_";

   // рисование волн                   
   fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,WavesWidth);
   fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,WavesWidth);   
   fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,WavesWidth);
   fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,WavesWidth);

   // рисование построений
   if(DrawTarget){   
    
      datetime TargetTime;
      
      // получение целого значения индекса бара цели 
      int tbc=(int)MathCeil(target_bar);
      
      if(tbc<rates_total){ // цель в пределах существующих баров графика
         TargetTime=time[tbc];
      }
      else{ // цель в будущем
         TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds();
      }
      
      // вычисление значений линий построения на баре цели
      double tv13=y3(i1,v1,i3,v3,tbc);   
      double tv24=y3(i2,v2,i4,v4,tbc);  
      double tv14=y3(i1,v1,i4,v4,tbc); 

      // построения

      fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth);   
      fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth);  
      fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth);
      
      // горизонтальная линия цели
      fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth);   
      // вертикальная линия цели 
      fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);      
   }
}

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

// префикс имен графических объектов 
string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_";

Затем выполняется рисование волн, координаты всех вершин переданы в функцию:

// рисование волн                   
fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,WavesWidth);
fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,WavesWidth);   
fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,WavesWidth);
fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,WavesWidth);

Рисуем построение, определяющее цель. Проверяем, включено ли рисование построений:

// рисование построений
if(DrawTarget){  

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

datetime TargetTime;

Переменная target_bar имеет дробное значение, увеличим его до ближайшего целого:

// получение целого значения индекса бара цели 
int tbc=(int)MathCeil(target_bar);

Далее будем использовать полученную переменную tbc. Здесь можно было бы использовать функцию MathFloor() — получить наименьшее ближайшее целое. На конечный результат это бы не повлияло , поскольку построения выполняют только информационную функцию. С использованием MathCeil() концы линий 1-3 и 2-4 обязательно пересекутся возле бара цели и построения будут выглядеть естественнее.

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

if(tbc<rates_total){ // цель в пределах существующих баров графика
   TargetTime=time[tbc];
}
else{ // цель в будущем
   TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds();
}

Рассчитаем значения всех линий (1-3, 2-4 и 1-4) на баре цели:

// вычисление значений линий построения на баре цели
double tv13=y3(i1,v1,i3,v3,tbc);   
double tv24=y3(i2,v2,i4,v4,tbc);  
double tv14=y3(i1,v1,i4,v4,tbc); 

Несмотря на то, что в функцию передается рассчитанное ранее значение цели (переменная target_value), для построений, даже для линии 2-4, оно вычисляется заново. Это связано с тем, что вместо точного значения из переменной target_bar используется значение из переменной tbc, которое будет немного больше чем target_bar. За счет таких вычислений на точной координате target_bar линии будут пересекаться точно на уровне target_value.

По рассчитанным значениям нарисуем линии:

fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth);   
fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth);  
fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth);

Линии отрисовываются c использованием вспомогательной функции fObjTrend():

void fObjTrend(   string  aObjName,
                  datetime aTime_1,
                  double   aPrice_1,
                  datetime aTime_2,
                  double   aPrice_2,
                  color    aColor      =  clrRed,  
                  color    aWidth      =  1,                
                  bool     aRay_1      =  false,
                  bool     aRay_2      =  false,
                  string   aText       =  "",
                  int      aWindow     =  0,                  
                  color    aStyle      =  0,
                  int      aChartID    =  0,
                  bool     aBack       =  false,
                  bool     aSelectable =  false,
                  bool     aSelected   =  false,
                  long     aTimeFrames =  OBJ_ALL_PERIODS
               ){
   ObjectCreate(aChartID,aObjName,OBJ_TREND,aWindow,aTime_1,aPrice_1,aTime_2,aPrice_2);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_BACK,aBack);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_COLOR,aColor);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTABLE,aSelectable);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTED,aSelected);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_TIMEFRAMES,aTimeFrames);
   ObjectSetString(aChartID,aObjName,OBJPROP_TEXT,aText);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_WIDTH,aWidth);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_STYLE,aStyle);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_LEFT,aRay_1);   
   ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_RIGHT,aRay_2);
   ObjectMove(aChartID,aObjName,0,aTime_1,aPrice_1);
   ObjectMove(aChartID,aObjName,1,aTime_2,aPrice_2);   
}

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

Таблица 1. Параметры функции fObjTrend()

Параметр Назначение
string aObjName имя объекта
datetime aTime_1 время первой точки привязки
double aPrice_1 цена первой точки привязки
datetime aTime_2 время второй точки привязки
double aPrice_2 цена второй точки привязки
color aColor цвет
color aWidth толщина
bool aRay_1 продлевать линию со стороны первой точки привязки
bool aRay_2 продлевать линию со стороны второй точки привязки
string aText текст подсказки
int aWindow подокно
color aStyle стиль линии
int aChartID идентификатор чарта
bool aBack рисовать на заднем плане
bool aSelectable объект можно выделять
bool aSelected объект выделен
long aTimeFrames на каких таймфреймах отображать линию

Осталось нарисовать две дополнительных линии: вертикальную на баре цели и горизонтальную на уровне цели:

fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth);   
fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);  

В итоге получаем изображение волн и построений:


Рис. 4. Волны Вульфа и построения для определения цели при покупке

Удаление графических объектов

При использовании Зигзага по Сlose (SrcSelect=Src_Close) или по другому индикатору, по мере формирования бара фигура может периодически появляться и исчезать. Для этого в начале основного индикаторного цикла выполняется очистка буферов со стрелками и точками: 

UpArrowBuffer[i]=EMPTY_VALUE;
DnArrowBuffer[i]=EMPTY_VALUE;
      
UpDotBuffer[i]=EMPTY_VALUE;
DnDotBuffer[i]=EMPTY_VALUE;  

Также в начале цикла необходимо обеспечить удаление графических объектов. Если включено рисование волн и построений, то в начале индикаторного цикла вызывается функция DeleteObjects():

if(_DrawWaves){
   DeleteObjects(time[i]);
}  

Код функции DeleteObjects():

void DeleteObjects(datetime time){
   string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time)+"_";
   ObjectDelete(0,prefix+"12");
   ObjectDelete(0,prefix+"23");
   ObjectDelete(0,prefix+"34");
   ObjectDelete(0,prefix+"45");
   ObjectDelete(0,prefix+"13");
   ObjectDelete(0,prefix+"24"); 
   ObjectDelete(0,prefix+"14");    
   ObjectDelete(0,prefix+"67"); 
   ObjectDelete(0,prefix+"7h");    
}

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

При снятии индикатора с графика необходимо удалить все созданные им графические объекты. Из функции DeInit(), автоматически выполняемой при завершении работы индикатора, вызывается функция ObjectsDeleteAll(). Вторым параметром в функцию передается имя индикатора, которое также является и префиксом всех его графических объектов. Так обеспечивается удаление только графических объектов, принадлежащих конкретному индикатору:

void OnDeinit(const int reason){
   ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME));
   ChartRedraw(0);
}  

Функция алерта

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

Функция алерта может следить за появлением стрелки на формирующемся баре (подойдет при использовании зигзага по high-low) или на сформированном (подойдет при использовании зигзага по close или по другим индикаторам). Значит, для выбора типа алерта создаем перечисление:

enum EAlerts{
   Alerts_off=0,  // алерт выключен
   Alerts_Bar0=1, // алерт по формирующемуся бару
   Alerts_Bar1=2  // алерт по сформированному бару
};

В окно свойств добавляем переменную:

input EAlerts              Alerts         =  Alerts_off;

Код функции алерта вынесен в отдельную функцию CheckAlerts(). В нее передается количество баров на графике и массив времени:

void CheckAlerts(int rates_total,const datetime & time[]){
   if(Alerts!=Alerts_off){ // алерты включены
      static datetime tm0=0; // переменная для времени бара последнего алерта покупки
      static datetime tm1=0; // переменная для времени бара последнего алерта покупки
      if(tm0==0){ // первое выполнение функции
         // инициализация переменных
         tm0=time[rates_total-1];
         tm1=time[rates_total-1];
      }
      string mes=""; // переменная для сообщения

      // есть стрелка вверх, и на последнем баре еще не было алерта
      if(UpArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && 
         tm0!=time[rates_total-1]
      ){
         tm0=time[rates_total-1]; // запомнить время последнего алерта
         mes=mes+" buy"; // формирование сообщения
      }

      // есть стрелка вниз, и на последнем баре еще не было алерта
      if(DnArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && 
         tm1!=time[rates_total-1]
      ){
         tm1=time[rates_total-1]; // запомнить время последнего алерта
         mes=mes+" sell"; // формирование сообщения
      } 
      if(mes!=""){ // есть сообщение
         // открытие окна с сообщением
         Alert(MQLInfoString(MQL_PROGRAM_NAME)+"("+Symbol()+","+IntegerToString(PeriodSeconds()/60)+"):"+mes);
      }        
   }   
}

Вызов функции CheckAlerts() выполняется в конце функции OnCalculate() после главного цикла. Также в конце функции OnCalculate() выполняется вызов функции обновления графика для ускорения отрисовки волн и построений:

if(_DrawWaves){
   ChartRedraw(0);
}
На этом создание индикатора полностью завершено. Он называется iWolfeWaves, его можно найти в приложении к статье.

Эксперт

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

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

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

input double               StopLoss_K     =  1;      // коэффициент стоп-лосса
input bool                 FixedSLTP      =  false;  // фиксированный стоп-лосс и тейк-профит
input int                  StopLoss       =  50;     // величина фиксированного стоп-лосса
input int                  TakeProfit     =  50;     // величина фиксированного тейк-профита

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

Если FixedSLTP=false, то действует переменная StopLoss_K. В этом случае тейк-профит устанавливается по индикатору — на уровне точки, отображающей цель, а стоплосс рассчитывается пропорционально величине тейк-профита с использованием коэффициента StopLoss_K. Этот вариант определения стоплосса и тейкпрофита подойдет только для Зигзага по цене: по high-low или по close (SrcSelect равно Src_HighLow или Src_Close).

При FixedSLTP=true используются переменные StopLoss и TakeProfit. Этот вариант обеспечит возможность использования зигзага по индикаторам, но и с зигзагами по цене тоже может использоваться.   

В функции OnInit() эксперта проверим тип счета. Если счет не допускает хеджирования, то запуск эксперта будет завершен:

if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
   Print("Its not hedging account");
   return(INIT_FAILED);
}

Если эксперт тестируется не в визуальном режиме, принудительно отключим рисование волн и построений:

bool _DrawWaves;

if(MQLInfoInteger(MQL_VISUAL_MODE)){
   _DrawWaves=DrawWaves;
}
else{
   _DrawWaves=false;
}

Переменная _DrawWaves будет использоваться вместо переменной DrawWaves при вызове индикатора iWolfeWaves функцией iCustom(). Вызываем индикатор и проверяем, удачно ли он загрузился:

h=iCustom(  Symbol(),
            Period(),
            "iWolfeWaves",
            Alerts,
            SrcSelect,
            DirSelect,
            RSIPeriod,
            RSIPrice,
            MAPeriod,
            MAShift,
            MAMethod,
            MAPrice,
            CCIPeriod,
            CCIPrice,
            ZZPeriod,
            K1,
            K2,
            K3,
            _DrawWaves,
            BuyColor,
            SellColor,
            WavesWidth,
            DrawTarget,
            TargetWidth,
            BuyTargetColor,
            SellTargetColor);
            
if(h==INVALID_HANDLE){
   Print("Cant load indicator");
   return(INIT_FAILED);
}

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

При использовании индикатора по high-low стрелка индикатора после появления не исчезает, значит, эксперт может работать на формирующемся баре. В остальных случаях стрелку индикатора эксперт должен "смотреть" на первом сформированном баре. Для этого объявим глобальную переменную эксперта Shift:

int Shift;

Установим ей нужное значение в зависимости от типа Зигзага: 

if(SrcSelect==Src_HighLow){
   Shift=0;
}
else{
   Shift=1;
}

Ниже приведен весь код функции OnInit():

int OnInit(){

   // проверка типа счета
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
      Print("Its not hedging account");
      return(INIT_FAILED);
   }

   // отключение рисования волн и построений

   bool _DrawWaves;
   
   if(MQLInfoInteger(MQL_VISUAL_MODE)){
      _DrawWaves=DrawWaves;
   }
   else{
      _DrawWaves=false;
   }

   // загрузка индикатора
   h=iCustom(  Symbol(),
               Period(),
               "iWolfeWaves",
               Alerts,
               SrcSelect,
               DirSelect,
               RSIPeriod,
               RSIPrice,
               MAPeriod,
               MAShift,
               MAMethod,
               MAPrice,
               CCIPeriod,
               CCIPrice,
               ZZPeriod,
               K1,
               K2,
               K3,
               _DrawWaves,
               BuyColor,
               SellColor,
               WavesWidth,
               DrawTarget,
               TargetWidth,
               BuyTargetColor,
               SellTargetColor);
       
   // проверка успешности загрузки индикатора         
   if(h==INVALID_HANDLE){
      Print("Cant load indicator");
      return(INIT_FAILED);
   }
   
   // определения бара на котором эксперт "смотрит" стрелки индикатора
   if(SrcSelect==Src_HighLow){
      Shift=0;
   }
   else{
      Shift=1;
   }

   return(INIT_SUCCEEDED);
}

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

datetime tm[1];     // время формирующегося бара
static datetime lt; // время последнего обработанного бара

Получим время последнего (формирующегося) бара:

if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return;

Выполним проверку времени бара:

if(Shift==0 || tm[0]!=lt){

Если Shift==0, эксперт работает на каждом тике. Иначе, только если значение переменной lt не равно времени формирующегося бара (раз на бар). 

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

double tp,sl; // переменные для расчета стоп-лосса и тейк-профита

double buf_buy[1];         // для стрелки на покупку 
double buf_sell[1];        // для стрелки на продажу

double buf_buy_target[1];  // для цели покупки
double buf_sell_target[1]; // для цели продажи 

if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return;
if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return;
if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return;
if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return;

При наличии торговых сигналов выполняется расчет стоп-лосса, тейк-профита и выполняется открытие позиции:

// есть стрелка, и на данном баре позиция buy не открывалась
if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){
   // стоплосс и тейкпрофит
   if(FixedSLTP){
      tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit;
      sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss;            
   }
   else{
      tp=NormalizeDouble(buf_buy_target[0],_Digits);
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits);         
   }
   // открытие
   if(!trade.Buy(0.1,Symbol(),0,sl,tp))return;
   // "запоминаем" время последнего открытия
   LastBuyTime=tm[0];
}

Если используются фиксированные стоп-лосс и тейк-профит (FixedStopLoss=true), то для расчета тейк-профита к цене открытия позиции (цена Ask при покупке) прибавляется значение переменной TakeProfit, умноженное на _Point. Для расчета стоп-лосса от цены открытия отнимается значение переменной стоп-лосс, умноженное на _Point. После вычислений полученные значения нормализуются функцией NormalizeDouble() до количества знаков, соответствующего количеству знаков у котировок, которое можно получить из переменной _Digits.

При нефиксированных стоп-лоссе и тейк-профите сначала определяем значение тейк-профита и пропорционально вычисляем значение стоп-лосса. Если позицию не удалось открыть, выполняется завершение работы функции OnTick(), на следующем тике будет выполнена повторная попытка открытия. Попытки будут выполняться все время, пока существует сигнал индикатора, то есть, в течение одного бара. При удачном открытии переменой LastBuyTime присваивается время текущего бара, чтобы на этом баре больше не выполнялось открытие (в случае потиковой работы, когда Shift=0). Переменная LastBuyTime — глобальная переменная эксперта.

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

// есть стрелка и на данном баре позиция sell не открывалась
if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){
   // стоп-лосс и тейк-профит
   if(FixedSLTP){
      tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit;
      sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss;            
   }
   else{
      tp=NormalizeDouble(buf_sell_target[0],_Digits);
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits);         
   }
   // открытие
   if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; 
   // "запоминаем" время последнего открытия
   LastSellTime=tm[0];        
}  

При продаже вместо переменной LastBuyTime используется переменная LastSelTime и уровни стоп-лосса, тейк-профита вычисляются от цены bid.

В конце переменной lt устанавливается время формирующегося бара, чтобы эксперт больше ничего не делал на этом баре (конечно, если он работает побарно, то есть Shift=1). Ниже приведен весь код функции OnTick():

void OnTick(){
   
   datetime tm[1]; // время формирующегося бара
   static datetime lt; // время последнего обработанного бара
   
   // копируем время
   if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return;
   
   if(Shift==0 || tm[0]!=lt){ // проверка для работы раз на бар

      double tp,sl; // переменные для расчета стоп-лосса и тейк-профита

      double buf_buy[1];         // для стрелки на покупку 
      double buf_sell[1];        // для стрелки на продажу
      
      double buf_buy_target[1];  // для цели покупки
      double buf_sell_target[1]; // для цели продажи     
      
      if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return;
      if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return;
      if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return;
      if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return;

      // есть стрелка и на данном баре позиция buy не открывалась
      if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){
         // стоплосс и тейкпрофит
         if(FixedSLTP){
            tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit;
            sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss;            
         }
         else{
            tp=NormalizeDouble(buf_buy_target[0],_Digits);
            double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
            sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits);         
         }
         // открытие
         if(!trade.Buy(0.1,Symbol(),0,sl,tp))return;
         // "запоминаем" время последнего открытия
         LastBuyTime=tm[0];
      }
      
      // есть стрелка и на данном баре позиция sell не открывалась
      if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){
         // стоплосс и тейкпрофит
         if(FixedSLTP){
            tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit;
            sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss;            
         }
         else{
            tp=NormalizeDouble(buf_sell_target[0],_Digits);
            double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
            sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits);         
         }
         // открытие
         if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; 
         // "запоминаем" время последнего открытия
         LastSellTime=tm[0];        
      }      
      
      lt=tm[0];
   }
}

В приложении эксперт находится в файле eWolfeWaves. 

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


Рис. 5. Эксперт выполняет вход на каждой стрелке индикатора

Конечно, нас в первую очередь интересует, насколько эффективен полученный индикатор для торговли. Результаты тестирования эксперта с настройками по умолчанию на всей истории EURUSD H1 приведены на рис. 6.


Рис. 6. Результаты тестирования эксперта на всей истории EURUSD H1

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

Еще несколько советов из книги Билла Вульфа

Кроме правил выявления волн, Билл Вульф в своей книге дает еще несколько советов, которые сам называет психологическими и техническими замечаниями. Одно из важнейших технических замечаний — совет следить за тиковым объемом: в точках разворота может наблюдаться его снижение, которое может свидетельствовать о развороте. Второй совет — следить за трендовыми линиями. Замеченные Биллом Вульфом волновые движения часто возникают после слома тренда — иными словами, после пробоя трендовой линии. То есть, волны, возникшие после слома тренда, могут быть более надежными. Третий совет — следить за линией 1-4, особенно за точкой 4, и выходить в случае любых непредвиденных обстоятельств: в случае обратной волны, сильного повышения объема или в случае быстрого взятия хорошей прибыли.

Заключение

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

Возможно, кто-то пожелает усовершенствовать индикатор. На данный момент внешние параметры индикатора имеют три переменных-коэффициента: K1, K2, K3. Сейчас коэффициент K1 используется для проверки положения точки 3 относительно точки 1 и точки 4 относительно точки 1. Возможно, для этих проверок будет лучше использовать раздельные коэффициенты. С другой стороны, увеличение количества параметров усложняет оптимизацию системы и повышает риск подгонки результатов вместо оптимизации. Может быть, будет лучше объединить коэффициенты K1 и K2. Это сделает настройку индикатора более простой и понятной. С другой стороны, может быть, лучше сделать только один коэффициент. Код индикатора довольно четко разделен по функциям, что облегчает любые его доработки. Каждый желающий сможет самостоятельно поэкспериментировать с его модификациями.  

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

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

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

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

  • Indicators/iWolfeWaves_Step_1.mq5
  • Indicators/iWolfeWaves.mq5
  • Experts/eWolfeWaves.mq5 

Чтобы все это заработало, потребуются дополнительные файлы из статьи "Универсальный зигзаг":

  • Indicators/iUniZigZagSW.mq5
  • Include/CSorceData.mqh
  • Include/CZZDirection.mqh>
  • Include/CZZDraw.mqh

Прикрепленные файлы |
MQL5.zip (8.67 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (15)
Dmitry Fedoseev
Dmitry Fedoseev | 17 апр. 2017 в 08:38
ilmirgalaxy:

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

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

Есть ли какой ниб метод определения т6 по-барно в будущее?


С чего это будет расчет неверный? В выходные нет баров. Хотите как бы нарисовать бары в выходные, и вдруг получить цель где-то в обед в воскресенье. Как может быть цель достигнута в обед в воскресенье, если в воскресенье никто ничего не делает, цена не меняется, даже баров нет?
Dmitry Fedoseev
Dmitry Fedoseev | 17 апр. 2017 в 08:39
Евгений Скрябин:
нужно многое доработать 

Давайте хоть про немного для начала  - парочку пунктиков требующих доработки.
Ilmir Galiev
Ilmir Galiev | 17 апр. 2017 в 09:04

извиняюсь, некорректно я написал, там я хотел выявить дату образования т6,

бар будет известен, например -10. Соответственно дата может плавать изза выходных, но дата выявляется только путем отслеживания точки6 в тот момент когда бар т6 будет равен 0.

Dmitry Fedoseev
Dmitry Fedoseev | 17 апр. 2017 в 11:42
ilmirgalaxy:

извиняюсь, некорректно я написал, там я хотел выявить дату образования т6,

бар будет известен, например -10. Соответственно дата может плавать изза выходных, но дата выявляется только путем отслеживания точки6 в тот момент когда бар т6 будет равен 0.


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

    

Aleksandr Masterskikh
Aleksandr Masterskikh | 12 мая 2017 в 11:38

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

Дело в том, что волны Вульфа, как аналитический инструмент имеет ряд серьёзных недостатков, а именно:


1.По сути мы имеем дело с двумя треугольниками - один сужающийся, другой расширяющийся.

2. Такая конструкция не может быть корректной для анализа, так как точка пересечения  может быть

на значительном расстоянии по времени от конструкции Вульфа (большем, чем указанная конструкция),

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

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

текущей динамики.


Кроме того, использование для прогноза традиционных индикаторов (пусть и модифицированных) также

вызывает сомнения, так как алгоритмы таких индикаторов не могут корректно анализировать динамику

рынка (так как график цен - это процесс, где постоянно изменяется не только амплитуда, но и частота).

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

изменяющаяся частота.

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

Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников
С введением хеджинга в MetaTrader 5 появилась отличная возможность одновременной торговли несколькими советниками на одном торговом счёте. При этом возможна ситуация, когда одна стратегия прибыльна, вторая убыточна, а в итоге график прибыли болтается около нуля. В таком случае полезно построить графики Баланса и Средств для каждой торговой стратегии по отдельности.
Рецепты MQL5 - Создаем кольцевой буфер для быстрого расчета индикаторов в скользящем окне Рецепты MQL5 - Создаем кольцевой буфер для быстрого расчета индикаторов в скользящем окне
Кольцевой буфер — самый простой и в то же время наиболее эффективный способ организации данных для расчетов в скользящем окне. В статье описано, как устроен этот алгоритм, и показано, как с его помощью сделать вычисление в скользящем окне простым и эффективным процессом.
Графические интерфейсы X: Выделение текста в многострочном поле ввода (build 13) Графические интерфейсы X: Выделение текста в многострочном поле ввода (build 13)
В этой статье будет реализована возможность выделения текста с помощью различных клавишных комбинаций и удаление выделенного текста, по образцу того, как это сделано в любом другом текстовом редакторе. Кроме этого, продолжим оптимизировать код и подготовим классы для перехода к завершающему процессу второго этапа развития библиотеки, когда все элементы управления будут нарисованными на отдельных картинках (холстах для рисования).
Графические интерфейсы X: Алгоритм переноса слов в многострочном поле ввода (build 12) Графические интерфейсы X: Алгоритм переноса слов в многострочном поле ввода (build 12)
Продолжим развивать элемент "Многострочное поле ввода". Наша задача на этот раз — сделать автоматический перенос слов на следующую строку в случае переполнения по ширине поля ввода или же обратный перенос на предыдущую строку, если появляется такая возможность.