English 中文 Español Deutsch 日本語 Português
Строим индикатор ZigZag по осцилляторам. Пример выполнения технического задания

Строим индикатор ZigZag по осцилляторам. Пример выполнения технического задания

MetaTrader 5Примеры | 10 апреля 2018, 12:26
8 788 6
Dmitry Fedoseev
Dmitry Fedoseev

Содержание

Введение

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

Индикатор ZigZag на основе осцилляторов

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

Общий анализ задания

Основные требования к разрабатываемому индикатору выявляются при первом же прочтении.

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

Алгоритм Зигзага. Алгоритм построения Зигзага отличается от классического.

  1. Зигзаг меняет направление не при образовании локального экстремума, а при выходе значения осциллятора за уровень перекупленности или перепроданности. 
  2. Зигзаг строится на графике цены. Соответственно, новый экстремум определяется по ценовым данным.

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

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



    Рис. 1. Выход WPR в зону перекупленности произошел на баре, отмеченном стрелкой 1,
    однако новый отрезок Зигзага надо рисовать до бара, обозначенного стрелкой 2.

  2. Поскольку смена направления Зигзага определяется по осциллятору, его значение будет меняться по мере формирования бара. То есть, Зигзаг может сменить направление, но потом, по мере формирования бара, смена направления может быть отменена. Необходимо обеспечить правильную работу индикатора в таких случаях.
  3. Поскольку новый экстремум определяется по графику цены (по ценам high/low), образование нового максимума/минимума не может быть отменено по мере формирования бара. Однако на баре с новым экстремумом Зигзаг может развернуться. В этом случае новый максимум/минимум отменяется (рис. 2).


     
    Рис. 2. 1 — вершина Зигзага находится на ценовом максимуме, образуемом формирующимся баром.
    2 — Зигзаг разворачивается, а ранее зафиксированный максимум отменяется

    Конечно, об этой ситуации можно поспорить, ведь в MetaTrader 5 есть стиль рисования Color ZigZag. Он позволил бы провести отрезок Зигзага вертикально, не перенося его вершину левее — на ранее определенный максимум. Однако при такой отрисовке у нас не будет возможности раскрасить два отрезка Зигзага (вертикальный и соседний с ним наклонный) независимо друг от друга. К тому же, подобный способ отрисовки Зигзага не очень распространен, и если бы это было нужно, в задании следовало бы это указать. То есть по умолчанию выбирается наиболее распространенный вариант.

Отображение. Отображение Зигзага тоже имеет свои особенности.

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



    Рис. 3. Перекрывающиеся паттерны

    Рассмотрим участок Зигзага, изображенного на рис. 3. Отрезки с 1 по 4 составляют паттерн восходящего тренда. Значит, отрезок 1 должен быть окрашен синим цветом. Но ранее он вошел в состав нисходящего паттерна, поэтому уже окрашен красным. При появлении отрезка 6 образуется еще один восходящий паттерн (отрезки с 3 по 6). Поскольку перед каждым обсчетом бара нужно возвращать участку Зигзага изначальный цвет, то в этом случае нужно очищать только отрезки 5 и 6, так как отрезки 3 и 4 уже относятся к другому паттерну.

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

Графический интерфейс. Требования к графическому интерфейсу относительно простые. Набор элементов управления на форме постоянен, его не нужно изменять в зависимости от выбранного осциллятора. Из числовых параметров используется только два (уровни перекупленности/перепроданности), и их набор идентичен для всех осцилляторов.

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

Этап 1 — построение Зигзага

В редакторе MetaEditor создаем новый пользовательский индикатор с именем OscZigZagStep1. Чтобы обозначить место в коде для переменных, добавляем одну внешнюю переменную. В окне выбора обработчиков событий выбираем первый вариант — OnCalculate(...,open,high,low,close), другие обработчики не нужны. В окне параметров отображения создаем два буфера. Первому буферу даем имя "HighLow", тип — Color Arrow и два цвета: Golg и LimeGreen. Второму буферу даем имя "ZigZag", тип — Color Section и три цвета: Gray, CornflowerBlue и Red (рис. 4).


Рис. 4. Выбор параметров отображения в окне мастера создания индикатора

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

По нажатию на кнопку "Готово" в редакторе откроется файл индикатора. Сначала подкорректируем в нем значения свойств indicator_color1 — удалим лишние цветовые образцы. Строка со свойством indicator_color1 должна иметь следующий вид:

#property indicator_color1  clrGold,clrLimeGreen

Аналогично надо подкорректировать свойство indicator_color2 (оставить три цвета).

Находим строку с автоматически созданным внешним параметром:

input int      Input1;

Удаляем ее, а вместо нее объявляем переменные для параметров индикатора WPR:

input int         WPRperiod   =  14;
input double      WPRmax      =  -20;
input double      WPRmin      =  -80;

Чуть ниже объявляем переменную для хэндла:

int h;

В самом начале функции OnInit() выполняем загрузку индикатора:

h=iWPR(Symbol(),Period(),WPRperiod);
if(h==INVALID_HANDLE){
   Print("Can't load indicator");
   return(INIT_FAILED);
}  

В функции OnDeinit() выполняем освобождение хэндла:

void OnDeinit(const int reason){
   if(h!=INVALID_HANDLE){
      IndicatorRelease(h);
   }
}  

При использовании Мастера создания индикатора мы создали отображаемые буферы, но нам потребуются еще и вспомогательные. К примеру, конкретно сейчас нам нужен буфер для значений осциллятора. Увеличиваем свойство indicator_buffers на одну единицу (ставим значение 5 вместо 4):

#property indicator_buffers 5

Туда, где уже объявлены массивы для буферов, добавляем еще один массив — для буфера со значениями осциллятора:

double         wpr[];

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

SetIndexBuffer(4,wpr,INDICATOR_CALCULATIONS); 

Переходим в функцию OnCalculate(), пишем стандартный код вычисления диапазона обсчета баров и копирования значений осциллятора wpr в буфер:

int start;

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

if(CopyBuffer(h,0,0,rates_total-start,wpr)==-1){
   return(0);
}

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

for(int i=start;i<rates_total;i++){
   HighLowBuffer[i]=EMPTY_VALUE;
   if(wpr[i]>WPRmax){
      HighLowBuffer[i]=high[i];
      HighLowColors[i]=0;
   }
   else if(wpr[i]<WPRmin){
      HighLowBuffer[i]=low[i];
      HighLowColors[i]=1;      
   }      
}

На данном этапе индикатор можно прикрепить на график. Прикрепим также стандартный WPR и убедимся в правильности выполненной работы (рис. 5).


Рис. 5. Отображение на графике цены зон перекупленности/перепроданности

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

double         dir[]; // для направления
double         lhb[]; // индекс бара последней вершины
double         llb[]; // индекс бара последней впадины

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

#property indicator_buffers 8

В функции OnInit() к только что объявленным массивам применяем функцию SetIndexBuffer():

SetIndexBuffer(5,dir,INDICATOR_CALCULATIONS);  
SetIndexBuffer(6,lhb,INDICATOR_CALCULATIONS);   
SetIndexBuffer(7,llb,INDICATOR_CALCULATIONS);    

Этот код располагается сразу же после последнего вызова функции SetIndexBuffer(), уже имеющегося в функции OnInit().

Теперь очень важный момент. Чтобы буфер типа Color Section работал правильно, для него надо указать пустое значение 0. Иначе вместо Зигзага на графике будут отображаться вот такие странные линии:


Рис. 6. Если для буфера типа Color Section не указать пустое значение, отображение Зигзага происходит неправильно

Чтобы указать пустое значение, в самый конец функции OnInit(), перед строкой с return, добавляем строку:

PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0); 

Обратите внимание, что значение первого параметра 1. В данном случае это индекс в группе отображаемых буферов, то есть 1 соответствует буферу ZigZagBuffer[].

С функцией OnInit() всё готово.  Теперь снова переходим к функции OnCalculate() и продолжаем писать код в стандартном индикаторном цикле.

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

lhb[i]=lhb[i-1];      
llb[i]=llb[i-1];
dir[i]=dir[i-1];

В наш код, где определялся выход осциллятора в зоны перекупленности/перепроданности, добавим установку направления в буфер dir[]:

if(wpr[i]>WPRmax){
   HighLowBuffer[i]=high[i];
   HighLowColors[i]=0;
   dir[i]=1;
}
else if(wpr[i]<WPRmin){
   HighLowBuffer[i]=low[i];
   HighLowColors[i]=1;      
   dir[i]=-1;
} 

Теперь самое интересное на первом этапе — построение Зигзага. Направление Зигзага определено, в буфере dir[] располагается значение 1 при направлении вверх и -1 при направлении вниз. Нужно еще определить, на каких непосредственно барах меняется направление. Основу построения Зигзага будет составлять следующий код, разделяющийся на 4 ветви:

if(dir[i]==1){
   if(dir[i-1]==-1){ 
      // изменение направления снизу вверх
   }
   else{
      // продолжение движения вверх
   }      
}
else if(dir[i]==-1){
   if(dir[i-1]==1){ 
      // изменение направления сверху вниз
   }
   else{
      // продолжение движения вниз
   }      
}

Подробно рассмотрим изменение направления Зигзага снизу вверх и продолжение вверх. Две другие ветви будут симметричны.

Изменение направления вверх

1. На диапазоне баров от последней впадины и до обсчитываемого бара (бар впадин не входит в диапазон) ищем максимальное значение цены:

if(dir[i]==1){
   if(dir[i-1]==-1){ 
      // изменение направления снизу вверх
      // поиск максимума
      int hb=i;
      for(int j=i;j>llb[i];j--){
         if(high[j]>high[hb]){
            hb=j;
         }
      }
      //...
   }
   else{
      // продолжение движения вверх
   }      
}

2. На найденном баре ставим точку Зигзага, в буфере lhb[] указываем индекс этого бара — и через буфер ZigZagColor устанавливаем нейтральный цвет: 

ZigZagBuffer[hb]=high[hb];
lhb[i]=hb;            
ZigZagColors[hb]=0;

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

ZigZagBuffer[i]=0;

Но в данном случае формирующаяся вершина Зигзага отстоит от обсчитываемого бара на неизвестное количество баров (рис. 1). Поэтому надо сохранить индекс бара, на котором находится новая вершина, и время обсчитываемого бара:

NewDotTime=time[i];
NewDotBar=hb;

Переменные NewDotTime и NewDotBar объявлены на глобальном уровне индикатора.

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

if(NewDotTime==time[i]){
   ZigZagBuffer[NewDotBar]=0;  
}

Движение вверх

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

if(high[i]>ZigZagBuffer[(int)lhb[i]]){ 
   // убрать старую точку
   ZigZagBuffer[(int)lhb[i]]=0;
   // поставить новую
   ZigZagBuffer[i]=high[i];
   ZigZagColors[i]=0;
   lhb[i]=i;
}

В начале индикаторного цикла, перед повторным обсчетом бара нужно вернуть индикатор в исходное состояние — вернуть удаленную точку на место:

ZigZagBuffer[(int)lhb[i]]=high[(int)lhb[i]];
ZigZagBuffer[(int)llb[i]]=low[(int)llb[i]];  

Чтобы в начале работы индикатора не происходило ошибок выхода за пределы массива, начальные элементы буферов lhb[] и llb[] нужно инициализировать нулевыми значениями. Еще нужно обнулить переменные NewDotTime и NewDotBar, делается это при вычислении диапазона обсчета:

if(prev_calculated==0){
   start=1;
   lhb[0]=0;
   llb[0]=0;   
   NewDotTime=0; 
}
else{
   start=prev_calculated-1;
}

На этом первый этап создания индикатора закончен. В приложении к статье индикатор на этом этапе имеет имя OscZigZagStep1.mq5.

Этап 2 — выявление паттерна и раскрашивание

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

Данные о вершинах Зигзага будем сохранять в массив структур. Структура должна содержать поля для индекса бара, значения, направления — и еще одно поле типа bool. В нем будет сохраняться значение true, если вершина является последней в паттерне (чтобы ограничить раскрашивание Зигзага до ранее распознанного паттерна). Описываем структуру и объявляем массив:

struct SZZDot{
   int bar;
   double val;
   int dir;
   bool pat;
};

SZZDot ZZDot[];

Затем добавляем в окончание каждой из четырех частей кода построения Зигзага вызов функции AddZZDot(). Она будет добавлять новые вершины Зигзага в массив ZZDot[]:

if(dir[i]==1){ 
   if(dir[i-1]==-1){          
      //...
      AddZZDot(1,high[hb],hb,i);
   }
   else{ 
      if(high[i]>ZigZagBuffer[(int)lhb[i]]){
         //...
         AddZZDot(1,high[i],i,i);
      }
   }      
}
else if(dir[i]==-1){
   if(dir[i-1]==1){
      //...
      AddZZDot(-1,low[lb],lb,i);
   }
   else{
      if(low[i]<ZigZagBuffer[(int)llb[i]]){
         //...
         AddZZDot(-1,low[i],i,i);
      }
   }      
}

В функцию AddZdot() передаем четыре параметра: направление, значение, индекс бара с вершиной, индекс обсчитываемого бара (саму функцию рассмотрим чуть позже). Для количества найденных вершин (занятых элементов в массива AADot[]) используем индикаторный буфер cnt[]. Объявим массив cnt[]:

double         cnt[];

В функции OnInit() вызовем для него функцию SetIndexBuffer():

SetIndexBuffer(8,cnt,INDICATOR_CALCULATIONS);  

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

#property indicator_buffers 9

В начале индикаторного цикла перемещаем по буферу последнее значение:

cnt[i]=cnt[i-1];

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

Рассмотрим функцию  AddZdot():

void AddZZDot(int d,double v,int b,int i){
   
   int c=(int)cnt[i];

   if(c==0){ 
      // на запуске индикатора или при его полном перерасчете
      ArrayResize(ZZDot,1024);
      ZZDot[c].dir=d;
      ZZDot[c].val=v;
      ZZDot[c].bar=b;
      ZZDot[c].pat=false;
      cnt[i]=1;
   }
   else{
      if(ZZDot[c-1].dir==d){
         // обновление вершины того же направления
         ZZDot[c-1].val=v;
         ZZDot[c-1].bar=b;         
      }
      else{
         // добавление новой вершины
         // увеличение массива по мере необходимости блоками по 1024 элемента
         if(c>=ArraySize(ZZDot)){ 
            ArrayResize(ZZDot,c+1024);
         }
         // добавление новой вершины
         ZZDot[c].dir=d;
         ZZDot[c].val=v;
         ZZDot[c].bar=b;
         ZZDot[c].pat=false;
         cnt[i]=c+1;
      }
   }
}

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

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

if(cnt[i]>0){
   int ub=(int)cnt[i]-1;
   if(ZZDot[ub].dir==1){
      ZZDot[ub].bar=(int)lhb[i];
      ZZDot[ub].val=high[(int)lhb[i]];
   }
   else{
      ZZDot[ub].bar=(int)llb[i];
      ZZDot[ub].val=low[(int)llb[i]];         
   }
}

Теперь, если на обсчитываемом баре выявится новая вершина, ее значение будет обновлено в массиве ZZDot, а если же образуется разворот, то останется ранее известное значение вершины.

Перед первым расчетом индикатора и при выполнении его полного перерасчета необходимо инициализировать начальный элемент массива cnt[]:

if(prev_calculated==0){
   //...
   cnt[0]=0;
}
else{
   start=prev_calculated-1;
}

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

if(cnt[i]>=5)

Вычислим индекс последнего элемента в массиве вершин:

int li=(int)cnt[i]-1;

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

ZZDot[li].pat=false;

Вернем части Зигзага первоначальный цвет:

for(int j=0;j<4;j++){
   if(ZZDot[li-j].pat){
      break;
   }
   ZigZagColors[ZZDot[li-j].bar]=0;
}

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

Проверяем условия паттерна:

if(ZZDot[li].dir==1){ // вверх
   if(
      ZZDot[li].val>ZZDot[li-2].val && 
      ZZDot[li-2].val>ZZDot[li-4].val && 
      ZZDot[li-1].val>ZZDot[li-3].val
   ){
      ZZDot[li].pat=true; 
      // раскрашивание 
   }
}
else{ // вниз
   if( 
      ZZDot[li].val<ZZDot[li-2].val && 
      ZZDot[li-2].val<ZZDot[li-4].val && 
      ZZDot[li-1].val<ZZDot[li-3].val
   ){
      ZZDot[li].pat=true; 		
      // раскрашивание                 
   }            
}

Осталось написать код раскрашивания. Он подобен коду очистки. Для направления вверх:   

for(int j=0;j<4;j++){
   if(j!=0 && ZZDot[li-j].pat){
      break;
   }
   ZigZagColors[ZZDot[li-j].bar]=1;
} 

Небольшое отличие от кода очистки заключается в том, что выход из цикла не выполняется при j=0.

На этом второй этап создания индикатора закончен. Индикатор выглядит вот так: 


Рис. 7. Вид индикатора в конце этапа №2. 

В приложении к статье индикатор на этом этапе имеет имя OscZigZagStep2.mq5. 

Этап 3 — добавление осцилляторов

Описываем перечисление:

enum EIType{
   WPR,
   CCI,
   Chaikin, 
   RSI,
   Stochastic
};

Объявляем внешнюю переменную для выбора осциллятора:

input EIType               Type        =  WPR;

Добавляем параметры остальных осцилляторов:

// CCI
input int                  CCIperiod   =  14;
input ENUM_APPLIED_PRICE   CCIprice    =  PRICE_TYPICAL;
input double               CCImax      =  100;
input double               CCImin      =  -100;
// Chaikin
input int                  CHfperiod   =  3;
input int                  CHsperiod   =  10;
input ENUM_MA_METHOD       CHmethod    =  MODE_EMA;
input ENUM_APPLIED_VOLUME  CHvolume    =  VOLUME_TICK;
input double               CHmax       =  1000;
input double               CHmin       =  -1000;
// RSI
input int                  RSIperiod   =  14;
input ENUM_APPLIED_PRICE   RSIprice    =  PRICE_CLOSE;
input double               RSImax      =  70;
input double               RSImin      =  30;
// Stochastic
input int                  STperiodK   =  5;  
input int                  STperiodD   =  3;
input int                  STperiodS   =  3;
input ENUM_MA_METHOD       STmethod    =  MODE_EMA;
input ENUM_STO_PRICE       STprice     =  STO_LOWHIGH;
input double               STmax       =  80;
input double               STmin       =  20; 

Объявляем переменные для уровней:

double max,min;

В начале функции OnStart делаем выбор осциллятора:

switch(Type){
   case WPR:
      max=WPRmax;
      min=WPRmin;  
      h=iWPR(Symbol(),Period(),WPRperiod);      
   break;
   case CCI:
      max=CCImax;
      min=CCImin;  
      h=iCCI(Symbol(),Period(),CCIperiod,CCIprice);  
   break;      
   case Chaikin:
      max=CHmax;
      min=CHmin;  
      h=iChaikin(Symbol(),Period(),CHfperiod,CHsperiod,CHmethod,CHvolume);  
   break;          
   case RSI:
      max=RSImax;
      min=RSImin;  
      h=iRSI(Symbol(),Period(),RSIperiod,RSIprice);  
   break;   
   case Stochastic:
      max=STmax;
      min=STmin;  
      h=iStochastic(Symbol(),Period(),STperiodK,STperiodD,STperiodS,STmethod,STprice);  
   break; 
}

if(h==INVALID_HANDLE){
   Print("Can't load indicator");
   return(INIT_FAILED);
}

В функции OnCalculate() переменные WPRmax и WPmin меняем на переменные max и min. 

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

Этап 4 — создание графического интерфейса

Для создания графического интерфейса используется библиотека IncGUI. Этой библиотеке посвящена серия статей "Пользовательские графические элементы управления", состоящая из трех частей (часть 1, часть 2, часть 3). Последняя исправленная версия библиотеки (IncGUI_v4.mqh) находится в приложении к статье "Универсальный осциллятор с графическим интерфейсом". Будет она приложена и к данной статье. Перед началом работы над графическим интерфейсом скопируйте файл IncGUI_v4.mqh в папку MQL5/Includes папки данных терминала.

Рассмотрим процесс создания графического интерфейса по шагам.

Подключение библиотеки. Сделайте копию индикатора OscZigZagStep3 с именем OscZigZagStep3 и подключите к нему библиотеку:

#include <IncGUI_v4.mqh>

Класс формы. В файле IncGUI_v4.mqh можно найти класс CFormTemplate — это своего рода шаблон для создания форм. Копируем его, вставляем в файл индикатора сразу после подключения библиотеки, переименовываем из CFormTemplate в CForm.

Свойства формы. В методе MainProperties() устанавливаем основные свойства формы:

m_Name         =  "Form";
m_Width        =  200;
m_Height       =  150;
m_Type         =  2;
m_Caption      =  "ZigZag on Oscillator";
m_Movable      =  true;
m_Resizable    =  true;
m_CloseButton  =  true;
  • Переменная m_Name — имя формы (префикс всех графических объектов, составляющих форму).
  • Переменные m_Width и m_Height — ширина и высота формы в пикселях.
  • Переменная m_Type — тип формы. При значении 2 в нижней части формы будет находиться кнопка закрытия.
  • Переменная m_Caption — заголовок формы.
  • Переменная m_Movable — перемещаемая форма, в левом верхнем углу форму будет находиться кнопка для перемещения.
  • Переменная m_Resizable — форму можно сворачивать/разворачивать, кнопка для этого будет находиться в правом верхнем углу.
  • Переменная m_CloseButton — форму можно закрывать, кнопка для этого будет находиться в правом верхнем углу.

Элементы управления. Создание элементов управления формы. Форма будет иметь два фрейма (рамки). На одном фрейме будет группа радиокнопок, на другом — два поля ввода. В секции public класса формы размещаем код:

CFrame m_frm1; // фрейм 1
CFrame m_frm2; // фрейм 2 
CRadioGroup m_rg; // группа радиокнопок      
CInputBox m_txt_max; // текстовое поле для верхнего уровня    
CInputBox m_txt_min; // текстовое поле для нижнего уровня

Инициализация элементов управления. В методе OnInitEvent() инициализируем элементы управления.

Инициализация первого фрейма с шириной/высотой 85/97 пикселей, заголовком "Osc Type" и местом для заголовка шириной 44 пикселя:

m_frm1.Init("frame1",85,97,"Osc Type",44);

В этом фрейме будет располагаться группа радиокнопок.

Второй фрейм с такими же размерами, заголовком "Levels" и местом для заголовка шириной 32 пикселя:

m_frm2.Init("frame2",85,97,"Levels",32);

В этом фрейме будут находиться поля ввода уровней.

Инициализация группы радиокнопок:

m_rg.Init();

Добавление радиокнопок в группу:

m_rg.AddButton(" WPR",0,0);
m_rg.AddButton(" CCI",0,16);
m_rg.AddButton(" Chaikin",0,32);
m_rg.AddButton(" RSI",0,48);            
m_rg.AddButton(" Stochastik",0,64); 

Инициализация текстовых полей для ввода верхнего и нижнего уровня:

m_txt_max.Init("max",45,-1," Max");
m_txt_min.Init("min",45,-1," Min");

Оба поля имеют ширину 45 пикселей, допускают ввод текста (третий параметр -1), в одном из них надпись "Max", во втором — "Min".

Отображение элементов управления. В методе OnShowEvent() вызываем методы Show() всех элементов управления и указываем их координаты на форме:

m_frm1.Show(aLeft+10,aTop+10);
m_frm2.Show(aLeft+105,aTop+10);
m_rg.Show(aLeft+17,aTop+20);
m_txt_max.Show(aLeft+115,aTop+30);
m_txt_min.Show(aLeft+115,aTop+50);     

Скрытие элементов управления. В методе OnHideEvent() скрываем все элементы управления:

m_frm1.Hide();
m_frm2.Hide();            
m_rg.Hide();
m_txt_max.Hide();
m_txt_min.Hide(); 

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

void SetCaption(string str){
   m_Caption="ZigZag on "+str;
   ObjectSetString(0,m_Name+"_Caption",OBJPROP_TEXT,m_Caption);
}

Создание объекта формы. Создаем объект класса CForm:

CForm form;

События формы. Чтобы форма и элементы управления реагировали на действия пользователя, из функции OnChartEvent() индикатора надо вызывать метод Event(). В зависимости от типа события, метод возвращает различные значения. Закрытию формы соответствует значение 1, при этом надо снять индикатор с графика:

if(form.Event(id,lparam,dparam,sparam)==1){
   ChartIndicatorDelete(0,0,MQLInfoString(MQL_PROGRAM_NAME)); 
   ChartRedraw();
}

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

Часть кода, в которой выполняется выбор индикатора в функции OnInit(), вынесем в отдельную функцию:

bool LoadIndicator(int aType){
   switch(aType){
      case WPR:
         max=WPRmax;
         min=WPRmin;  
         h=iWPR(Symbol(),Period(),WPRperiod);      
      break;
      case CCI:
         max=CCImax;
         min=CCImin;  
         h=iCCI(Symbol(),Period(),CCIperiod,CCIprice);  
      break;      
      case Chaikin:
         max=CHmax;
         min=CHmin;  
         h=iChaikin(Symbol(),Period(),CHfperiod,CHsperiod,CHmethod,CHvolume);  
      break;          
      case RSI:
         max=RSImax;
         min=RSImin;  
         h=iRSI(Symbol(),Period(),RSIperiod,RSIprice);  
      break;   
      case Stochastic:
         max=STmax;
         min=STmin;  
         h=iStochastic(Symbol(),Period(),STperiodK,STperiodD,STperiodS,STmethod,STprice);  
      break; 
   }
   
   if(h==INVALID_HANDLE){
      Print("Can't load indicator");
      return(false);
   }   
   
   return(true);
   
}   

Она будет вызываться из функции OnInit() индикатора (в самом ее начале) и по событию радиокнопок. Сразу после выбора индикатора в функции OnInit() инициализируем форму, устанавливаем значения элементам управления и отображаем форму:

if(!LoadIndicator(Type)){
   return(INIT_FAILED);
}

form.Init(1);
form.m_rg.SetValue(Type);
form.m_txt_max.SetValue(max);   
form.m_txt_min.SetValue(min);  
form.SetCaption(EnumToString(Type));
form.Show(5,20);

В функции OnChartEvent() обрабатываем события элементов управления. Событие радиокнопок для смены индикатора:

if(form.m_rg.Event(id,lparam,dparam,sparam)==1){
   
   if(h!=INVALID_HANDLE){
      IndicatorRelease(h);
      h=INVALID_HANDLE;
   }      
   
   if(!LoadIndicator(form.m_rg.Value())){
      Alert("Can't load indicator");
   }
   
   form.m_txt_max.SetValue(max);   
   form.m_txt_min.SetValue(min);    

   EventSetMillisecondTimer(100);
}

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

Изменение уровней:

if(form.m_txt_max.Event(id,lparam,dparam,sparam)==1 ||
   form.m_txt_min.Event(id,lparam,dparam,sparam)==1
){
   max=form.m_txt_max.ValueDouble();
   min=form.m_txt_min.ValueDouble();      
   EventSetMillisecondTimer(100);
}

По событию полей ввода, переменным min и max присваиваются новые значения и запускается таймер.  

В функции OnTimer() индикатор пересчитывается. Если это происходит успешно, таймер отключается и дальше индикатор продолжает работать как обычно — по тикам. Все действия, необходимые для перерасчета индикатора, подробно рассматривались в вышеупомянутой статье "Универсальный осциллятор с графическим интерфейсом". Поэтому рассмотрим здесь только принципиальные отличия. Универсальный осциллятор рассчитывался в методе класса, не требующем ценовых данных, здесь же нам надо вызвать функцию OnCalculate(), а в нее — передать массивы с ценами. Объявляем массивы:

datetime time[];
double open[];
double high[];
double low[];
double close[];
long tick_volume[];
long volume[];
int spread[];

Получаем количество баров:

int bars=Bars(Symbol(),Period());
      
if(bars<=0){
   return;
}

Для построения Зигзага нужны не все ценовые данные, а только три массива: time, high, low. Только их мы и скопируем:

if(CopyTime(Symbol(),Period(),0,bars,time)==-1){
   return;
}

if(CopyHigh(Symbol(),Period(),0,bars,high)==-1){
   return;
}      

if(CopyLow(Symbol(),Period(),0,bars,low)==-1){
   return;
} 

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

if(ArraySize(time)<bars){
   int sz=ArraySize(time);
   ArrayResize(time,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      time[j]=time[i];
   }   
}

if(ArraySize(high)<bars){
   int sz=ArraySize(high);
   ArrayResize(high,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      high[j]=high[i];
   }
}      

if(ArraySize(low)<bars){
   int sz=ArraySize(low);
   ArrayResize(low,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      low[j]=low[i];
   }
} 

Остается вызвать функцию OnCalculate():

int rv=OnCalculate(
            bars,
            0,
            time,
            open,
            high,
            low,
            close,
            tick_volume,
            volume,
            spread
);

Если функция OnCalculte() отработала без ошибок, отключаем таймер: 

if(rv!=0){
   ChartRedraw();     
   EventKillTimer();
   form.SetCaption(EnumToString((EIType)form.m_rg.Value()));
}

Полный код функции OnTimer(), а также полностью завершенный индикатор можно увидеть в приложении в файле OscZigZagStep4.mq5.

При прикреплении индикатора на график в левом верхнем углу должна отобразиться форма с элементами управления (рис. 8).


Рис. 8. Графический интерфейс на шаге 4

Заключение

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

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

Кроме этого, в процессе создания индикатора была выявлена его очень неожиданная и неочевидная особенность, о которой невозможно подумать даже при первом прочтении задания, и уж тем более — при его составлении. Я имею в виду некоторые случаи, когда нужно удалять последний максимум/минимум Зигзага при его развороте. Как упоминалось в статье, можно было бы использовать буфер Color Zigzag, но в этом случае возникли бы сложности при его раскрашивании, потому что в наборе буферов Color ZigZag есть два буфера для данных и только один — для цвета. Если на одном баре оба буфера данных имеют значения (случай, когда вдоль бара проходит вертикальная линия), то указанный в цветовом буфере цвет присваивается сразу двум отрезкам Зигзага. Можно было бы использовать буфер не Color ZigZag, а просто ZigZag, а отрезки Зигзага перекрашивать графическими объектами, или же просто ставить стрелки или точки. В общем, любое задание требует очень внимательного прочтения и предварительного обсуждения.

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

Все файлы разложены по папкам так, как они должны быть разложены в терминале. В папке MQL5/Indicators расположены файлы, соответствующие этапам создания индикатора: OscZigZagStep1.mq5, OscZigZagStep2.mq5, ОscZigZagStep3.mq5,  OscZigZagStep4.mq5.

В папке MQL5/Includes находится файл IncGUI_v4.mqh, необходимый для создания графического интерфейса в индикаторе OscZigZagStep4.


Прикрепленные файлы |
MQL5.zip (92.85 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
Dmitry Fedoseev
Dmitry Fedoseev | 11 июн. 2019 в 08:51
Photic:

1.Код индикатора OscZigZagStep1

Да но при смене направления NewDotBar=hb и lhb[i]=hb. Здесь ZigZagBuffer сначала приравнивается нулю, а затем ему возращается тоже самое значение что и было. ZigZagBuffer[(int)lhb[i]]=ZigZagBuffer[NewDotBar]=ZigZagBuffer[hb].Не понял в чём тут смысл. Значение сначала обнуляется затем присваивается тоже самое значение.

2. Индикатор на паре EURUSD 06/06/19

Здесь вроде как не должно быть никакой вершины

1. Это связано с тем, что на формирующемся баре новый отрезок зигзага может исчезать. А после исчезновения нового отрезка, hb не тот же самый, а старый, от ранее сформированной вершины.

2. Не факт, что не должно. В подокне осциллятор по которому строится зигзаг? Но по нему направление определяется, а минимум и максимум по цене. Если посмотреть на осциллятор на том баре, который обведен, там низина. Дальше осциллятор еще ниже падает, но цена-то не падает. ... Не. Скорее всего дело не в той низине. Направление вниз должно было появиться примерно в 3:00 (или в 7:00), но на всем интервале от последнего максимума до 3:00 минимальная цена находится именно на этом обведенном баре.

...и если вам кажется, что это неправильный зигзаг, то может быть, это не тот зигзаг, который вам нужен. Прочитав статью внимательно вы сами сможете написать себе любой зигзаг.
Photic
Photic | 15 июн. 2019 в 13:21
Dmitry Fedoseev:

1. Это связано с тем, что на формирующемся баре новый отрезок зигзага может исчезать. А после исчезновения нового отрезка, hb не тот же самый, а старый, от ранее сформированной вершины.

2. Не факт, что не должно. В подокне осциллятор по которому строится зигзаг? Но по нему направление определяется, а минимум и максимум по цене. Если посмотреть на осциллятор на том баре, который обведен, там низина. Дальше осциллятор еще ниже падает, но цена-то не падает. ... Не. Скорее всего дело не в той низине. Направление вниз должно было появиться примерно в 3:00 (или в 7:00), но на всем интервале от последнего максимума до 3:00 минимальная цена находится именно на этом обведенном баре.

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

1. Первый вопрос отпал вместе с жарой

2. Из статьи и кода я понял,что макс/мин ищутся при смене направления или корректируются по мере движения. Но на обведённом участке нет смены направления, dir =1 и до и после. Зачем же и когда ищется здесь минимум. 7 июня часиков в 7:00 при смене dir=1 на dir=-1 ищется на этом участке максимум и всё. Наверно я что-то гдей-то упустил.

Photic
Photic | 18 июн. 2019 в 14:09
Наконец то пошел дождь и картина кажется прояснилась.
Dmitry Fedoseev
Dmitry Fedoseev | 18 июн. 2019 в 14:24
Photic:

1. Первый вопрос отпал вместе с жарой

2. Из статьи и кода я понял,что макс/мин ищутся при смене направления или корректируются по мере движения. Но на обведённом участке нет смены направления, dir =1 и до и после. Зачем же и когда ищется здесь минимум. 7 июня часиков в 7:00 при смене dir=1 на dir=-1 ищется на этом участке максимум и всё. Наверно я что-то гдей-то упустил.

2. Направление сменилось, и в момент смены направления минимум ищется на всем промежутке от последней вершины до места смены направления.

Albert Idrisov
Albert Idrisov | 31 мая 2020 в 14:20
а на МТ4 есть он?
Random Decision Forest в обучении с подкреплением Random Decision Forest в обучении с подкреплением
Random Forest (RF) с применением бэггинга — один из самых сильных методов машинного обучения, который немного уступает градиентному бустингу. В статье делается попытка разработки самообучающейся торговой системы, которая принимает решения на основании полученного опыта взаимодействия с рынком.
Работаем с результатами оптимизации через графический интерфейс Работаем с результатами оптимизации через графический интерфейс
Продолжаем развивать тему обработки и анализа результатов оптимизации. На этот раз задача состоит в том, чтобы выбрать 100 лучших результатов оптимизации и отобразить их в таблице графического интерфейса. Сделаем так, чтобы пользователь, выделяя ряд в таблице результатов оптимизации, получал мультисимвольный график баланса и просадки на отдельных графиках.
Применение метода Монте-Карло для оптимизации торговых стратегий Применение метода Монте-Карло для оптимизации торговых стратегий
Перед запуском робота на торговом счете мы обычно тестируем и оптимизируем его на истории котировок. И тут возникает резонный вопрос: как прошлые результаты на истории могут помочь нам в будущем? В статье показано применение метода Монте-Карло для построения собственных критериев оптимизации торговых стратегий. Кроме того, рассмотрены критерии устойчивости советника.
Как создать графическую панель любой сложности и как это работает Как создать графическую панель любой сложности и как это работает
В статье подробно рассматривается, как создать панель на базе класса CAppDialog и как добавить в нее элементы управления. Описывается структура панели и схема наследования объектов в ней. Продемонстрировано, что нужно для обработки событий и как события раздаются подчинённым элементам управления. Приведены примеры изменения параметров панели: размера, цвета фона.