Автоматический поиск дивергенций и конвергенций

Dmitry Fedoseev | 21 августа, 2017

Содержание

Введение

Термин "дивергенция" происходит от латинского слова "divergere" ("обнаруживать расхождение"). Обычно под дивергенцией понимается несоответствие в показаниях индикатора и направлении движения цены. Вместе с этим термином часто используется антонимичное ему слово — "конвергенция", происходящее от латинского слова "convergo" ("сближаю"). Есть более широкие системы классификации дивергенций/конвергенций, включающих такие определения, как "скрытая дивергенция", "расширенная дивергенция", дивергенции классов A, B, C и т.д.

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

Дивергенция и конвергенция (определение понятия)

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

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


Рис. 1. Несоответствие в направлениях движения цены и индикатора. Слева цена движется вверх, 
индикатор вниз — расхождение. Справа: цена движется вниз, индикатор вверх — схождение

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


Рис. 2. Несоответствие в направлениях движения цены и индикатора. Слева: цена движется вверх, 
индикатор вниз — схождение. Справа: цена движется вниз, индикатор вверх — расхождение

Теперь посмотрим на рис. 1 и 2. с точки зрения выбора направления торговли. Допустим, цена движется вверх, индикатор — вниз, и мы решили продавать. Следуя аналогии, покупка должна выполняться, когда цена движется вниз, а индикатор — вверх (рис. 3).


Рис 3. Слева: условия для продажи, цена и индикатор расходятся. Справа: условия
для покупки (идентичны условия для продажи), цена и индикатор сходятся
 

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

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

Дивергенция — это сигнал разворота цены, представляющий собой несоответствие показаний индикатора и направления движения цены

Поскольку это сигнал разворота — значит, для продажи цена сначала должна двигаться вверх, а для покупки — вниз. А для того, чтобы появилось несоответствие движения цены с показаниями индикатора, индикатор должен двигаться, соответственно, вниз и вверх. Можно заметить, что в этом определении за эталонное направление принята продажа, а график индикатора должен быть расположен под графиком цены. По этому определению, на рис. 3 показана дивергенция. 

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

Конвергенция — это сигнал продолжения тенденции, представляющий собой несоответствие показаний индикатора и направления движения цены

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


Рис. 4. Сигналы конвергенции

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

Для закрепления анализа терминологии, проведенного в этом разделе, и в качестве удобной "шпаргалки" на рис. 5 одновременно показаны сигналы дивергенции и конвергенции.


Рис. 5. Сигналы дивергенции и конвергенции

 

Методы определения направления движения цены и индикатора

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

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

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

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

Определение пиков/впадин по барам. Используется количество баров пика/впадины. Например, если значение этого параметра 2, то значение индикатора на баре вершины должно быть выше, чем у двух баров слева и у двух баров справа от вершины. Соответственно, для впадины значение должно быть ниже, чем у соседних баров (рис. 6).


Рис. 6. Определение вершин и впадин по двум барам. Слева определение вершины. На баре, обозначенном стрелкой, стало
известно о формировании вершины, обозначенной птичкой. Справа определение впадины

Необходимое количество баров слева и справа от вершины/впадины может быть разным: например, 5 слева и 2 справа (рис. 7).


Рис. 7. Вершина по пяти барам слева и двум барам справа 

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


Рис. 8. Определение вершин и впадин по пороговому значению. В левом верхнем углу показана величина порога.
До бара 2 индикатор двигался вверх, на баре 2 было зафиксировано максимальное значение, на баре 5 значение
снизилось на пороговое значение, что означало смену направления 
индикатора. На баре 6 индикатор снова
преодолел пороговое значение и изменил направление и т.д.

Вариант по барам удобнее всех, поскольку он совершенно не зависит от характера индикатора. Напротив, величина порогового значения зависит от типа индикатора. Например, для RSI с диапазоном колебаний 0 - 100 пороговое значение может быть около 5. Для Momentum порог будет от 0.1 до 1, ведь индикатор незначительно колеблется вокруг уровня 100, к тому же величина этих колебаний зависит от таймфрейма. Это еще сильнее усложняет использование порогового значения. 

Максимальное/минимальное значение при положении выше/ниже центральной линии индикатора. Этот способ используется реже остальных. Он тоже зависит от используемого индикатора, ведь не у всех индикаторов среднее значение находится на уровне 0 (например у индикатора RSI это уровень 50). Но главный его недостаток — сильное запаздывание (рис. 9). 


Рис. 9. Определения пиков и впадин по пересечению центральной линии. О вершине, отмеченной цифрой 1, станет известно
только после  пересечения центральной линии на баре, обозначенном цифрой 2. О впадине, обозначенной цифрой 3, станет
известно после пересечения на баре, обозначенном цифрой 4


Системы классификаций дивергенции

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

Классическая дивергенция. Это явление уже описано выше и показано на рис. 5.

Скрытая дивергенция. Скрытая дивергенция отличается от классической направлением движения цены и индикаторов. То есть, скрытая дивергенция — это то же, что определено выше как конвергенция.

Расширенная дивергенция. До сих пор мы говорили о направлении движения цены и индикатора только вверх и вниз. Если добавить в рассмотрение горизонтальное движение, вариантов становится больше. Несмотря на множество вариантов, которые теоретически можно получить, сочетая три направления движения цены и три направления движения индикатора, был выделен только один вариант расширенной дивергенции:

  • Горизонтальное движение цены, индикатор падает — расширенная медвежья дивергенция (сигнал на продажу)
  • Горизонтальное движение цены, индикатор растет — расширенная бычья дивергенция (сигнал на покупку).

Классы: A, B, C. Класс А — это классическая дивергенция, а классы B и C — варианты расширенной дивергенции.

Класс B: 

    • Горизонтальное движение цены, индикатор падает — медвежья (сигнал на продажу).
    • Горизонтальное движение цены, индикатор растет — бычья (сигнал на покупку).

Класс С: 

    • Цена растет, вершины индикатора на одном уровне — медвежья (сигнал на продажу).
    • Цена падает, впадины индикатора на одном уровне — бычья (сигнал на покупку).

Как видим, классы B и С — варианты расширенной дивергенции, причем вариант B полностью повторяет вышеописанное определение.

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

Полная систематизация вариантов движения цены и индикатора

Во-первых, выделим два варианта по количеству возможных направлений движения цены и индикатора.

  1. Два направления движения: вверх и вниз. 
  2. Три направления движения: вверх, вниз и горизонтально.

В первом случае возможны только 4 варианта сочетаний. Рассмотрим их на примере сигналов продажи.

  1. Цена вверх, индикатор вверх. 
  2. Цена вверх, индикатор вниз (дивергенция).
  3. Цена вниз, индикатор вверх (конвергенция).
  4. Цена вниз, индикатор вниз.  

Разобравшись в предыдущем разделе статьи со способами определения направлений, визуализируем эти варианты (рис. 10). 


Рис. 10. Все варианты сочетаний различных направления движения цены и индикатора при двух вариантах движения 

В случае с тремя вариантами движения цены и индикатора возможны уже девять сочетаний.

  1. Цена вверх, индикатор вверх.
  2. Цена вверх, индикатор горизонтально.
  3. Цена вверх, индикатор вниз (дивергенция).
  4. Цена горизонтально, индикатор вверх.
  5. Цена горизонтально, индикатор горизонтально.
  6. Цена горизонтально, индикатор вниз.
  7. Цена вниз, индикатор вверх (конвергенция).
  8. Цена вниз, индикатор горизонтально.
  9. Цена вниз, индикатор вниз.

Графически эти варианты показаны на рис. 11.


Рис. 11. Все варианты сочетаний различных направления движения цены и индикатора при трех вариантах движения 

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

Тройная дивергенция

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

  1. Вверх, вверх.
  2. Вверх, горизонтально.
  3. Вверх, вниз.
  4. Горизонтально, вверх.
  5. Горизонтально, горизонтально.
  6. Горизонтально, вниз.
  7. Вниз, вверх.
  8. Вниз, горизонтально.
  9. Вниз, вниз.

В этом случае правильнее говорить не о направлении, а о форме движения. Графически варианты различных форм движения, определяемые по трем вершинам, показаны на рис. 12.


Рис. 12. Различные формы движения, образуемого тремя вершинами 

Соответствующие им формы движения, определяемые по впадинам, показаны на рис. 13.

 
Рис. 13. Различные формы движения, образуемого тремя впадинами 

Комбинируя 9 вариантов формы движения цены и 9 вариантов формы движения индикатора, можно получить 81 вариант тройной дивергенции.

Таким образом, определять форму движения можно по любому количеству точек. Если добавить четвертую точку, получится 81 вариант формы движения цены или индикатора, и, соответственно 6561 (81*81) вариант их сочетания. Конечно, чем больше возможных вариантов, тем реже они будут встречаться. Может быть, использование четвертой точки для определения формы движения не имеет смысла, однако индикатор в этой статье будет создан без ограничения на количество точек, по которым определяется форма движения.

Универсальный индикатор определения дивергенции

Разобравшись с теорией, приступим к созданию индикатора. 

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

Создание нового индикатора. В MetaEditor создаем новый индикатор iDivergence. При его создании выбираем функцию OnCalculate. Функция OnTimer() не потребуется. Отмечаем галочку "Индикатор в отдельном окне". Создаем три буфера: линию отображения осциллятора и два стрелочных буфера для отрисовки стрелок при возникновении дивергенции. После того, как новый файл откроется в редакторе, изменим имена буферов: 1 — buf_osc, 2 — buf_buy, 3 — buf_sell.  Имена изменить нужно там, где выполняется объявление массивов и в функции OnInit(). Еще можно подправить свойства буферов: indicator_label1, indicator_label2, indicator_label3 — значения этих свойства отображаются во всплывающей подсказке при наведении мышки на линии или значок индикатора и отображаются в окне данных. Назовем их "osc", "buy", "sell".

Применение универсального осциллятора. Вставляем в новый индикатор все внешние параметры индикатора  iUniOsc. Параметры ColorLine1, ColorLine2, ColorHisto не нужны в окне свойств, скроем их. Параметр Type имеет пользовательский тип OscUni_RSI описанный в файле UniOsc/UniOscDefines.mqh. Подключим этот файл. По умолчанию значение параметра Type выставлено на OscUni_ATR — выбор индикатора ATR. Но индикатор ATR не зависит от направления движения цены, а значит, не подходит для определения дивергенции. Поэтому установим по умолчанию OscUni_RSI — индикатор RSI:

#include <UniOsc/UniOscDefines.mqh>

input EOscUniType          Type              =  OscUni_RSI;
input int                  Period1           =  14;
input int                  Period2           =  14;
input int                  Period3           =  14;
input ENUM_MA_METHOD       MaMethod          =  MODE_EMA;
input ENUM_APPLIED_PRICE   Price             =  PRICE_CLOSE;   
input ENUM_APPLIED_VOLUME  Volume            =  VOLUME_TICK;   
input ENUM_STO_PRICE       StPrice           =  STO_LOWHIGH;   
      color                ColorLine1        =  clrLightSeaGreen;
      color                ColorLine2        =  clrRed;
      color                ColorHisto        =  clrGray;

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

int h;

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

h=iCustom(Symbol(),Period(),"iUniOsc", Type,
                                       Period1,
                                       Period2,
                                       Period3,
                                       MaMethod,
                                       Price,
                                       Volume,
                                       StPrice,
                                       ColorLine1,
                                       ColorLine2,
                                       ColorHisto);
if(h==INVALID_HANDLE){
   Alert("Can't load indicator");
   return(INIT_FAILED);
}

В функции OnCalculate() скопируем данные универсального осциллятора в буфер buf_osc:

int cnt;   

if(prev_calculated==0){
   cnt=rates_total;
}
else{ 
   cnt=rates_total-prev_calculated+1; 
}

if(CopyBuffer(h,0,0,cnt,buf_osc)<=0){
   return(0);
}  

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

Определение экстремумов осциллятора. Выше мы рассмотрели три варианта определения экстремумов. Впишем все эти варианты в создаваемый индикатор и предусмотрим возможность выбора любого из них (внешняя переменная с выпадающим списком). В папке Include создадим папку UniDiver, где будем создавать все дополнительные файлы с кодом. Создадим включаемый файл UniDiver/UniDiverDefines.mqh, в нем напишем перечисление EExtrType:

enum EExtrType{
   ExtrBars,
   ExtrThreshold,
   ExtrMiddle
};

Варианты перечисления:

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

input EExtrType            ExtremumType      =  ExtrBars; // Тип экстремума
input int                  LeftBars          =  2;        // Баров слева для варианта ExtrBars
input int                  RightBars         =  -1;       // Баров справа для варианта ExtrBars
input double               MinMaxThreshold   =  5;        // Величина порога для варианта ExtrThreshold 

Cделаем так, чтобы можно было использовать не только два параметра сразу, но и какой-то один: RightBars или LeftBars. У параметра RightBars по умолчанию стоит значение -1. Это значит, что он не используется и ему будет присвоено значение второго параметра.

Классы определения экстремумов. В процессе работы индикатора менять способ определения экстремума не потребуется, поэтому вместо оператора if или switch рациональнее использовать ООП. Создадим базовый класс и три дочерних для трех вариантов определения экстремума. Один из этих дочерних классов будет выбираться при запуске индикатора. Фактически, в этих классах будут определяться не только экстремумы, но и выполняться вся работа по поиску дивергенции. Различаются в них только способы определения экстремумов, а само определение дивергенции совершенно идентично во всех случаях. Поэтому функция определения дивергенции будет находиться в базовом классе и вызываться из дочерних классов. Но сначала надо будет обеспечить легкий доступ ко всем экстремумам индикатора (как это сделано в статье "Волны Вульфа" с вершинами зигзага).

Для хранения данных об одной вершине будет использоваться структура SExtremum, описание структуры располагается в файле UniDiverDefines:

struct SExtremum{
   int SignalBar;
   int ExtremumBar;
   datetime ExtremumTime;
   double IndicatorValue;
   double PriceValue;
};

Назначение полей структуры:

Для хранения данных о всех вершинах и впадинах будут использоваться два массива этих структур. Они будут являться членами базового класса.

Классы определения экстремумов находятся в файле UniDiver/CUniDiverExtremums.mqh, имя базового класса — CDiverBase. Рассмотрим структуру класса, но пока только с основными методами. Остальные добавятся потом, по мере необходимости.

class CDiverBase{
   protected: 
      
      SExtremum m_upper[];
      SExtremum m_lower[];

      void AddExtremum( SExtremum & a[],
                        int & cnt,
                        double iv,
                        double pv,
                        int mb,
                        int sb,
                        datetime et);
   
      void CheckDiver(  int i,
                        int ucnt,
                        int lcnt,
                        const datetime & time[],
                        const double &high[],
                        const double &low[],                     
                        double & buy[],
                        double & sell[],
                        double & osc[]
      );

   public:
      virtual void Calculate( const int rates_total,
                              const int prev_calculated,
                              const datetime &time[],
                              const double &high[],
                              const double &low[],
                              double & osc[],
                              double & buy[],     
                              double & sell[]
      );
}; 

Виртуальный метод Calculate() обеспечивает выбор варианта определения экстремумов. Методы AddExtremum() и CheckDiver() располагаются в секции protected — они будут вызываться из метода Calculate() дочерних классов. Метод AddExtremum() собирает данные о вершинах и впадинах в массивы m_upper[] и m_lower[]. Метод CheckDiver() проверяет, выполнено ли условие дивергенции, и устанавливает индикаторные стрелки. Ниже рассмотрим все эти методы подробнее, а пока познакомимся с дочерними классами для других способов определения экстремумов.

Определение экстремумов по барам. Класс для определения экстремумов по барам:

class CDiverBars:public CDiverBase{
   private:   
   
      SPseudoBuffers1 Cur;
      SPseudoBuffers1 Pre;  
           
      int m_left,m_right,m_start,m_period;   
   
   public:
         
      void CDiverBars(int Left,int Right);
   
      void Calculate( const int rates_total,
                      const int prev_calculated,
                      const datetime &time[],
                      const double &high[],
                      const double &low[],
                      double & osc[],
                      double & buy[],
                      double & sell[]
      );
}; 

В конструктор класса передаются параметры определения экстремумов (внешние переменные LeftBars, RightBars), выполняется проверка правильности их значений, коррекция в случае необходимости и вычисляются дополнительные параметры:

void CDiverBars(int Left,int Right){
   m_left=Left;
   m_right=Right;   
   if(m_left<1)m_left=m_right;   // параметр Left не задан
   if(m_right<1)m_right=m_left;  // параметр Right не задан
   if(m_left<1 && m_right<1){    // оба параметра не заданы
      m_left=2;
      m_right=2;
   }
   m_start=m_left+m_right;       // смещение точки начала интервала
   m_period=m_start+1;           // количество баров интервала
}

Сначала проверяются значения параметров. Если какое-то из них не положительно (то есть, не задано), то ему присваивается значение второго параметра. Если не задан ни один параметр, им присваиваются значения по умолчанию (числа 2). Затем вычисляется величина отступа начального бара для поиска экстремума (m_start) и общее количество баров экстремума (m_period).

Метод Calculate() идентичен стандартной функции OnCalculate(), но в нее передаются не все ценовые массивы, а только нужные: time[], high[], low[] и индикаторные буферы osc[] (данные осциллятора), buy[] и sell[] (стрелки). Как и всегда в функции OnCalculte(), вычисляется диапазон обсчитываемых баров. Затем в стандартном индикаторном цикле определяются экстремумы индикатора (функции ArrayMaximum() и ArrayMinimum()). При обнаружении экстремума вызывается метод AddExtremum() для добавления данных в массив m_upper[] или m_lower[]. В завершение вызывается метод CheckDiver(), анализирующий данные из массивов с экстремумами. Если дивергенция найдена, устанавливаются стрелки.

void Calculate( const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &high[],
                const double &low[],
                double & osc[],
                double & buy[],
                double & sell[]
){

   int start; // переменная для индекса начального бара
   
   if(prev_calculated==0){ // полный расчет индикатора
      start=m_period; // определение начального бара расчета 
      m_LastTime=0;   // сброс переменной для определения нового бара 
      Cur.Reset();    // сброс вспомогательных структур
      Pre.Reset();    // сброс вспомогательных структур
   }
   else{ // при расчете только новых баров
      start=prev_calculated-1; // вычисление индекса бара, с которого надо продолжить расчеты
   }

   for(int i=start;i<rates_total;i++){ // основной индикаторный цикл
      
      if(time[i]>m_LastTime){ // новый бар
         m_LastTime=time[i];
         Pre=Cur;
      }
      else{ // повторный обсчет того же бара
         Cur=Pre;
      }
      
      // вычисление параметров поиска максимума/минимума 
      int sb=i-m_start; // индекс бара, с которого начинается интервал
      int mb=i-m_right; // индекс бара с вершиной/впадиной  
      
      if(ArrayMaximum(osc,sb,m_period)==mb){ // есть вершина
         // добавление вершины в массив
         this.AddExtremum(m_upper,Cur.UpperCnt,osc[mb],high[mb],mb,i,time[mb]);
      }
      if(ArrayMinimum(osc,sb,m_period)==mb){ // есть впадина
         // добавление впадины в массив
         this.AddExtremum(m_lower,Cur.LowerCnt,osc[mb],low[mb],mb,i,time[mb]);
      }
      
      // проверка на дивергенцию
      this.CheckDiver(i,Cur.UpperCnt,Cur.LowerCnt,time,high,low,buy,sell,osc);
   } 
}

Рассмотрим более подробно этот код. В самом начале индикаторного цикла:

if(time[i]>m_LastTime){ // новый бар
   m_LastTime=time[i];
   Pre=Cur;
}
else{ // повторный обсчет того же бара
   Cur=Pre;
}

переменная m_LastTime объявлена в базовом классе. Если время бара time[i] больше значения этой переменной — значит, бар обсчитывается впервые. При этом переменной m_LastTime присваивается время бара, а переменной Pre — значение переменной Cur. При повторном обсчете этого же бара, наоборот, переменной Cur выполняется присвоение переменной Pre. Прием с переменными Cur и Pre подробно рассматривался в этой статье. Переменные Pre и Cur имеют тип SPseudoBuffers1, описанный в файле UniDiverDefines:

struct SPseudoBuffers1{
   int UpperCnt;
   int LowerCnt;
   void Reset(){
      UpperCnt=0;
      LowerCnt=0;  
   }   
};

Структура включает два поля:

  • UpperCount — количество используемых элементов массива m_upper[];
  • LowerCount — количество используемых элементов массива m_lower[];

Метод Reset() создан для быстрого обнуления всех полей структуры.

После манипуляций с переменными Cur и Pre вычисляются индексы баров для поиска экстремумов:

// вычисление параметров поиска максимума/минимума 
int sb=i-m_start; // индекс бара, с которого начинается интервал
int mb=i-m_right; // индекс бара, с вершиной/впадиной  

Переменной sb присваивается индекс бара, с которого выполняется поиск максимума или минимума. Переменной mb присваивается индекс бара, на котором должен находиться максимум или минимум.

Определяем пик или впадину с помощью функций ArrayMaximum() и ArrayMinimum():

if(ArrayMaximum(osc,sb,m_period)==mb){ // есть вершина
   // добавление вершины в массив
   this.AddExtremum(m_upper,Cur.UpperCnt,osc[mb],high[mb],mb,i,time[mb]);
}
if(ArrayMinimum(osc,sb,m_period)==mb){ // есть впадина
   // добавление впадины в массив
   this.AddExtremum(m_lower,Cur.LowerCnt,osc[mb],low[mb],mb,i,time[mb]);
}

Если функция ArrayMaximum() или ArrayMinimum() возвращает значение mb, значит, слева и справа от пика/впадины находится заданное количество баров. В свою очередь, это свидетельствует о том, что образовался искомый пик/впадина. При этом вызывается метод AddExtremum(), и данные добавляются в массив m_upper[] или m_lower[].

Разберем простой метод AddExtremum():

void AddExtremum( SExtremum & a[], // массив, в который добавляются данные
                  int & cnt,       // количество занятых элементов массива 
                  double iv,       // значение индикатора
                  double pv,       // значение цены 
                  int mb,          // индекс бара с экстремумом
                  int sb,          // индекс бара, на котором стало известно об экстремуме 
                  datetime et      // время бара с экстремумом
){
   if(cnt>=ArraySize(a)){ //массив заполнен
      // увеличение размера массива
      ArrayResize(a,ArraySize(a)+1024);
   }
   // добавление новых данных
   a[cnt].IndicatorValue=iv; // значение индикатора
   a[cnt].PriceValue=pv;     // значение цены
   a[cnt].ExtremumBar=mb;    // индекс бара с экстремумом
   a[cnt].SignalBar=sb;      // индекс бара, на котором стало известно об экстремуме 
   a[cnt].ExtremumTime=et;   // время бара с экстремумом
   cnt++;                    // увеличение счетчика занятых элементов
}

Через параметры в метод передается массив a[], в который нужно добавить новые данные. Это может быть массив m_upper[] или m_lower[]. Через переменную cnt передается количество занятых элементов массива a[]. Это может быть переменная Cur.UpperCnt или Cur.LowerCnt. Массив a[] и переменная cnt передаются по ссылкам, поскольку изменяются в методе. 

Переменная iv — значение индикатора на баре экстремума, pv — значение цены на баре с экстремумом, mb — индекс бара с экстремумом, sb — сигнальный бар (на котором стало известно о появлении экстремума), et — время бара с экстремумом.

В начале метода AddExtremum() проверяется размер массива. Если он полностью заполнен, его размер вырастает на 1024 элемента, затем выполняется добавление данных и увеличение переменной cnt.

Метод CheckDiver() мы рассмотрим чуть позже.

Определение экстремума по пороговому значению.

Класс определения по пороговому значению отличается от класса определения по барам, в первую очередь, типом переменных Cur и Pre: это тип SPseudoBuffers2, описанный в файле  UniDiverDefines.mqh:

struct SPseudoBuffers2{
   int UpperCnt;
   int LowerCnt;
   double MinMaxVal;
   int MinMaxBar;   
   int Trend;
   void Reset(){
      UpperCnt=0;
      LowerCnt=0;  
      MinMaxVal=0;
      MinMaxBar=0;
      Trend=1;
   }   
};

Структура SPseudoBuffers2 имеет все те же поля, что и структура SPseudoBuffers1, и еще несколько дополнительных:

  • MinMaxVal — переменная для максимального или минимального значения индикатора
  • MinMaxBar — переменная для индекса бара, на котором найдено максимальное или минимальное значение индикатора
  • Trend — переменная для направления движения индикатора. Если значение 1, значит индикатор движется вверх и отслеживается его максимальное значение. При -1 отслеживается минимальное значение.

В конструктор класса передается внешний параметр с величиной порога — переменная MinMaxThreshold. Ее значение сохраняется в переменной m_threshold, объявленной в секции private. 

Метод Calculate() этого класса отличается только способом определения экстремума:

switch(Cur.Trend){ // текущее направление движение индикатора
   case 1: // вверх
      if(osc[i]>Cur.MinMaxVal){ // новый максимум
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;
      }
      if(osc[i]<Cur.MinMaxVal-m_threshold){ // преодолено пороговое значение
         // добавление вершины в массив
         this.AddExtremum(m_upper,Cur.UpperCnt,Cur.MinMaxVal,high[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]);
         Cur.Trend=-1;          // смена отслеживаемого направления 
         Cur.MinMaxVal=osc[i];  // начальное значение минимума
         Cur.MinMaxBar=i;       // бар с начальным значением минимума        
      }
   break;
   case -1: // вниз
      if(osc[i]<Cur.MinMaxVal){ // новый минимум
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;
      }
      if(osc[i]>Cur.MinMaxVal+m_threshold){ // преодолено пороговое значение
         // добавление впадины в массив
         this.AddExtremum(m_lower,Cur.LowerCnt,Cur.MinMaxVal,low[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]);
         Cur.Trend=1;           // смена отслеживаемого направления 
         Cur.MinMaxVal=osc[i];  // начальное значение максимума
         Cur.MinMaxBar=i;       // бар с начальным значением максимума 
      }         
   break;
}

Если значение переменной Cur.Trend 1, сравнивается значение осциллятора со значением переменной Cur.MinMaxValue. Если новое значение осциллятора выше значения из переменной, то значение переменной обновляется. При этом переменной Cur.MinMaxBar присваивается индекс бара, на котором выявлен новый максимум. Тут же проверяется, не опустилось ли значение осциллятора от последнего известного максимума на величину m_threshold. Если да, значит осциллятор изменил направление. Вызывается метод AddExtremum(), данные о новом экстремуме сохраняются в массиве, значение переменной Cur.Trend меняется на противоположное, а в переменных Cur.MinMaxVal и Cur.MinMaxBar фиксируются начальные параметры нового минимума. Поскольку значение переменной Cur.Trend изменилось, с этого момента выполняется другая секция case — слежение за минимальными значениями осциллятора и за преодолением порога вверх.

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

void CDiverMiddle(EOscUniType type){
   if(type==OscUni_Momentum){
      m_level=100.0;
   }
   else if(type==OscUni_RSI || type==OscUni_Stochastic){
      m_level=50.0;
   }
   else if(type==OscUni_WPR){
      m_level=-50.0;
   }
   else{
      m_level=0.0;
   }
}

Для Моментума это 100, для RSI и Стохастика это 50, для WPR -50, для остальных осцилляторов 0.

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

switch(Cur.Trend){
   case 1:
      if(osc[i]>Cur.MinMaxVal){
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;
      }
      if(osc[i]<m_level){
         this.AddExtremum(m_upper,Cur.UpperCnt,Cur.MinMaxVal,high[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]);
         Cur.Trend=-1;
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;               
      }
   break;
   case -1:
      if(osc[i]<Cur.MinMaxVal){
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;
      }
      if(osc[i]>m_level){
         this.AddExtremum(m_lower,Cur.LowerCnt,Cur.MinMaxVal,low[Cur.MinMaxBar],Cur.MinMaxBar,i,time[Cur.MinMaxBar]);
         Cur.Trend=1;
         Cur.MinMaxVal=osc[i];
         Cur.MinMaxBar=i;
      }         
   break;
}

Единственное отличие в том, что смена направления выполняется через сравнение с серединным уровнем осциллятора: osc[i]<m_level или osc[i]>m_level.

Способ задания типа дивергенции. Добавим во внешние параметры переменную для выбора типа распознаваемой дивергенции:

input int                  Number            =  3;

По умолчанию значение переменной 3, что в соответствии с рис. 11 означает классическую дивергенцию. Всего на рис. 11 показано 9 вариантов сочетаний движения цены и индикатора. Добавим еще один вариант — "не проверяется". Получится 10 вариантов. Таким образом, используя привычное десятичное число, можно описать любое сочетание различных движений в любом количестве (в том числе тройную дивергенцию). Получается, что однозначное число соответствует простой дивергенции (по двум вершинам/впадинам), двухзначное число — тройной и т.д. Например, число 13 будет соответствовать сочетанию, показанному на рис. 14.


Рис. 14. Сочетание движений индикатора и
цены при Number=13 для продажи
 

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

Классы проверки условия располагаются в файле UniDiver/CUniDiverConditions.mqh. Базовый класс имеет имя CDiverConditionsBase, разберем его:

class CDiverConditionsBase{
   protected:
      double m_pt;
      double m_it;
   public:
   void SetParameters(double pt,double it){
      m_pt=pt;
      m_it=it;
   }
   virtual bool CheckBuy(double i1,double p1,double i2,double p2){
      return(false);
   }
   virtual bool CheckSell(double i1,double p1,double i2,double p2){
      return(false);
   }
};

У класса два виртуальных метода для сравнения двух соседних вершин, в них передаются параметры этих вершин: 

  • i1 — значение индикатора в точке 1
  • p1 — значение цены в точке 1
  • i2 — значение индикатора в точке 2
  • p2 — значение цены в точке 2

Точки отсчитываем справа налево, начиная с единицы.

Метод SetParameters() используем для установки дополнительных параметров сравнения: pt — допустимая разница значений цены, при которой считается, что точки цены находятся на одном уровне. it — аналогичный параметр для сравнения вершин индикатора. Значения этих параметров задаются через окно свойств:

input double               IndLevel          =  0;
input int                  PriceLevel        =  0;

Ниже приведен код одного из дочерних классов:

class CDiverConditions1:public CDiverConditionsBase{
   private:
   public:
   bool CheckBuy(double i1,double p1,double i2,double p2){
      return((p1>p2+m_pt) && (i1>i2+m_it));
   }
   bool CheckSell(double i1,double p1,double i2,double p2){
      return((p1<p2-m_pt) && (i1<i2-m_it));
   }
};

В методе для CheckBuy() проверяется, превышает ли цена точки 1 цену точки 2. Так же и с индикатором: значение в точке 1 должно превышать значение в точке 2. Метод CheckSell() зеркально симметричен методу CheckBuy(). Все остальные классы подобны и отличаются только логическими выражениями, за исключением CDiverConditions0. В нем методы CheckSell() и CheckBuy() сразу возвращают true. Он используется при отключенной проверке условий (возможен любой вариант).

Подготовка к проверке условий дивергенции. В секции protected класса CDiverBase объявлен массив и переменная для его размера:

CDiverConditionsBase * m_conditions[];
int m_ccnt;  

В методе SetConditions() выполняется изменение размера массива m_conditions и создание объектов проверки условия дивергенции:

void SetConditions(int num,      // номер дивергенции
                   double pt,    // параметр PriceLevel
                   double it){   // параметр IndLevel
   if(num<1)num=1; // номер дивергенции не может быть меньше 1
   ArrayResize(m_conditions,10); // максимально возможное количество условий
   m_ccnt=0; // счетчик фактического количеств условий
   while(num>0){
      int cn=num%10; // вариант дивергенции между очередной парой экстремумов
      m_conditions[m_ccnt]=CreateConditions(cn); // создание объекта
      m_conditions[m_ccnt].SetParameters(pt,it); // установка параметров проверки условия
      num=num/10; // переход с следующему условию 
      m_ccnt++; // подсчет количества условий
   }
   // коррекция размера массива в соответствии с фактическим количеством условий 
   ArrayResize(m_conditions,m_ccnt);    
}

В метод передаются параметры:

  • num — внешний параметр Number;
  • pt — внешний параметр PriceLevel;
  • it — внешний параметр IndLevel.

Сначала проверяется параметр num:

if(num<1)num=1; // номер дивергенции не может быть меньше 1

Затем массив m_conditions увеличивается до максимально возможного размера (10 — длина максимального значения переменной int). Затем в цикле while, в зависимости от значения каждого из разрядов числа num, создается объект проверки условия методом CreateConditions() и устанавливаются параметры методом SetParameters(). После цикла размер массива меняется в соответствии с реальным количеством используемых условий.

Разберем метод CreateConditions():

CDiverConditionsBase * CreateConditions(int i){
   switch(i){
      case 0:
         return(new CDiverConditions0());      
      break;
      case 1:
         return(new CDiverConditions1());      
      break;
      case 2:
         return(new CDiverConditions2());      
      break;
      case 3:
         return(new CDiverConditions3());      
      break;
      case 4:
         return(new CDiverConditions4());      
      break;
      case 5:
         return(new CDiverConditions5());      
      break;      
      case 6:
         return(new CDiverConditions6());      
      break;
      case 7:
         return(new CDiverConditions7());      
      break;
      case 8:
         return(new CDiverConditions8());      
      break;
      case 9:
         return(new CDiverConditions9());
      break;
   }
   return(new CDiverConditions0()); 
}

Метод прост: в зависимости от значения параметра i, выполняется создание соответствующего объекта и возвращается ссылка на него.

Определение дивергенции. Теперь можно рассмотреть метод CheckDivergence() класса CDiverBase. Сначала приведем весь код метода, затем рассмотрим его построчно:

void CheckDiver(  int i,                    // индекс обсчитываемого бара
                  int ucnt,                 // количество вершин в массиве m_upper
                  int lcnt,                 // количество впадин в массиве m_lower
                  const datetime & time[],  // массив с временем баров 
                  const double &high[],     // массив с ценами high баров
                  const double &low[],      // массив с ценами low баров               
                  double & buy[],           // индикаторный буфер со стрелками вверх 
                  double & sell[],          // индикаторный буфер со стрелками вниз
                  double & osc[]            // индикаторный буфер со значениями осциллятора
){

   // очистка буферов со стрелками
   buy[i]=EMPTY_VALUE;
   sell[i]=EMPTY_VALUE;
   
   // удаление графических объектов
   this.DelObjects(time[i]);
   
   // для вершин (сигналы продажи)
   if(ucnt>m_ccnt){ // есть достаточное количество вершин
      if(m_upper[ucnt-1].SignalBar==i){ // на обсчитываемом баре выявлена вершина

         bool check=true; // предполагаем, что дивергенция состоялась
         
         for(int j=0;j<m_ccnt;j++){ // по всем парам вершин
            // проверка выполнения условия на очередной паре вершин
            bool result=m_conditions[j].CheckSell( m_upper[ucnt-1-j].IndicatorValue,
                                                   m_upper[ucnt-1-j].PriceValue,
                                                   m_upper[ucnt-1-j-1].IndicatorValue,
                                                   m_upper[ucnt-1-j-1].PriceValue
                                                );
            if(!result){ // условие не выполняется 
               check=false; // дивергенция не состоялась
               break; 
            } 
                                
         }
         if(check){ // дивергенция состоялась
            // установка стрелки индикаторного буфера
            sell[i]=osc[i];
            // рисование вспомогательных линий и/или стрелки на графике цены
            this.DrawSellObjects(time[i],high[i],ucnt);
         }
      }
   }
   
   // для впадин (сигналы покупки)
   if(lcnt>m_ccnt){
      if(m_lower[lcnt-1].SignalBar==i){
         bool check=true;
         for(int j=0;j<m_ccnt;j++){
            bool result=m_conditions[j].CheckBuy(  m_lower[lcnt-1-j].IndicatorValue,
                                                   m_lower[lcnt-1-j].PriceValue,
                                                   m_lower[lcnt-2-j].IndicatorValue,
                                                   m_lower[lcnt-2-j].PriceValue
                                                );
            if(!result){
               check=false;
               break;
            }                                          
         }
         if(check){
            buy[i]=osc[i];
            this.DrawBuyObjects(time[i],low[i],lcnt);
         }
      }
   }    
}

В метод передаются параметры:

  • i — индекс текущего обсчитываемого бара;
  • ucnt — количество используемых элементов массива m_upper[];
  • lcnt — количество используемых элементов массива m_lower[];
  • time[] — массив со временем баров;
  • high[] — массив с ценами high баров;
  • low[] — массив с ценами low баров;                  
  • buy[] — индикаторный буфер для стрелок на покупку;
  • sell[] — индикаторный буфер для стрелок на продажу;
  • osc[] — индикаторный буфер со значениями осциллятора.

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

// очистка буферов со стрелками
buy[i]=EMPTY_VALUE;
sell[i]=EMPTY_VALUE;

Удаляются графические объекты, соответствующие обсчитываемому бару:

// удаление графических объектов
this.DelObjects(time[i]);

Кроме стрелок, индикатор iDivergence будет рисовать графическими объектами стрелки на графике цены и линии, соединяющие экстремумы на графике цены с вершинами/впадинами на графике осциллятора. 

Затем идут два идентичных участка кода для проверки условий на продажу и на покупку. Рассмотрим первый участок для продажи. Количество доступных вершин осциллятора должно быть на 1 больше, чем количество проверяемых условий. Поэтому выполняется проверка:  

// для вершин (сигналы продажи)
if(ucnt>m_ccnt){ // есть достаточное количество вершин

}

Далее выполняется проверка существования вершины на обсчитываемом баре. Это определяется соответствием индекса обсчитываемого бара и индекса из массива с данными вершин/впадин:

if(m_upper[ucnt-1].SignalBar==i){ // на обсчитываем баре выявлена вершина

}

Потребуется вспомогательная переменная для результатов проверки условий:

bool check=true; // предполагаем что дивергенция состоялась

В цикле for проходим по всем условиям, передавая в них данные о вершинах:

for(int j=0;j<m_ccnt;j++){ // по всем парам вершин
   // проверка выполнения условия на очередной паре вершин
   bool result=m_conditions[j].CheckSell( m_upper[ucnt-1-j].IndicatorValue,
                                          m_upper[ucnt-1-j].PriceValue,
                                          m_upper[ucnt-1-j-1].IndicatorValue,
                                          m_upper[ucnt-1-j-1].PriceValue
                                        );
   if(!result){ // условие не выполняется 
      check=false; // дивергенция не состоялась
      break; 
   } 
}

Если какое-то из условий не выполняется, выходим из цикла. Переменной check присваивается значение false. Если все условия выполнены, check сохраняет значение true, при этом устанавливается стрелка и создается графический объект:

if(check){ // дивергенция состоялась
   // установка стрелки индикаторного буфера
   sell[i]=osc[i];
   // рисование вспомогательных линий и/или стрелки на графике цены
   this.DrawSellObjects(time[i],high[i],ucnt);
}

Отображение графических объектов может включаться/выключаться в окне свойств индикатора, для этого объявлены переменные:

input bool                 ArrowsOnChart     =  true;
input bool                 DrawLines         =  true;
input color                ColBuy            =  clrAqua;
input color                ColSell           =  clrDeepPink;
  • ArrowsOnChart — включение стрелок из графических объектов на графике цены
  • DrawLines — включение рисования линий, соединяющих вершины и впадины цены и индикатора
  • ColBuy и ColSell — цвета графических объектов для сигналов покупки и продажи.

В секции protected класса CDiverBase объявлены соответствующие переменные:

bool m_arrows;  // соответствует переменной ArrowsOnChart
bool m_lines;   // соответствует переменной DrawLines
color m_cbuy;   // соответствует переменной ColBuy
color m_csell;  // соответствует переменной ColSell 

Значения этих переменных устанавливаются в методе SetDrawParmeters():

void SetDrawParmeters(bool arrows,bool lines,color cbuy,color csell){
   m_arrows=arrows;
   m_lines=lines;
   m_cbuy=cbuy;
   m_csell=csell;
}

Рассмотрим методы, работающие с графическими объектами. Удаление:

void DelObjects(datetime bartime){ // включено рисование линий
   if(m_lines){
      // формирование общего префикса
      string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_";
      for(int j=0;j<m_ccnt;j++){ // по количеству условий дивергенции
         ObjectDelete(0,pref+"bp_"+IntegerToString(j)); // линия на графике цены при сигнале покупки
         ObjectDelete(0,pref+"bi_"+IntegerToString(j)); // линия на графике индикатора при сигнале покупки 
         ObjectDelete(0,pref+"sp_"+IntegerToString(j)); // линия на графике цены при сигнала продажи 
         ObjectDelete(0,pref+"si_"+IntegerToString(j)); // линия на графике индикатора при сигнале продажи 
      }            
   }
   if(m_arrows){ // включены стрелки на графике цены
      // 
      ObjectDelete(0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_ba");
      // 
      ObjectDelete(0,MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_sa");
   }
}

Линии, соединяющие вершины/впадины, и стрелки удаляются раздельно. Имена всех графических объектов начинаются с имени индикатора, затем добавляется время бара, на котором была выявлена дивергенция. В цикле, соответствующем количеству условий m_ccnt, выполняется дальнейшее формирование имен. Для сигналов покупки на графике цены добавляется "bp", для сигналов покупки на графике индикатора — "bi", аналогично для сигналов продажи добавляется "sp" и "ip". К концам имен добавляется индекс j. К именам стрелок добавляется "_ba" — стрелка сигнала покупки или "_sa" — стрелка сигнала продажи.

Создание графических объектов выполняется в методах DrawSellObjects() и DrawBuyObjects(), рассмотрим один из них:

void DrawSellObjects(datetime bartime,double arprice,int ucnt){
   if(m_lines){ // включено рисование линий
      
      // формирование общего префикса
      string pref=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_";
      
      for(int j=0;j<m_ccnt;j++){  // по всем условиям дивергенции
                  
         // линия на графике цены
         fObjTrend(  pref+"sp_"+IntegerToString(j),
                     m_upper[ucnt-1-j].ExtremumTime,
                     m_upper[ucnt-1-j].PriceValue,
                     m_upper[ucnt-2-j].ExtremumTime,
                     m_upper[ucnt-2-j].PriceValue,
                     m_csell);
                     
         // линия на графике индикатора
         fObjTrend(  pref+"si_"+IntegerToString(j),
                     m_upper[ucnt-1-j].ExtremumTime,
                     m_upper[ucnt-1-j].IndicatorValue,
                     m_upper[ucnt-2-j].ExtremumTime,
                     m_upper[ucnt-2-j].IndicatorValue,
                     m_csell,
                     ChartWindowFind(0,MQLInfoString(MQL_PROGRAM_NAME)));  
      }
   }
      
   if(m_arrows){ // включены стрелки на графике цены
      fObjArrow(MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString((long)bartime)+"_sa",
               bartime,
               arprice,
               234,
               m_csell,
               ANCHOR_LOWER); 
   }            
} 

Имена объектов формируются точно так же, как и при удалении, затем создаются графические объекты посредством функций fObjTrend() и fObjArrow().  Они располагаются в подключаемом файле UniDiver/UniDiverGObjects.mqh. Функции достаточно просты, нет смысла их разбирать.

Завершение индикатора. Остается применить в индикаторе созданные классы. В функции OnInit(), в зависимости от выбранного типа определения экстремумов, создаем соответствующий объект:

switch(ExtremumType){
   case ExtrBars:
      diver=new CDiverBars(LeftBars,RightBars);
   break;
   case ExtrThreshold:
      diver=new CDiverThreshold(MinMaxThreshold);
   break;      
   case ExtrMiddle:
      diver=new CDiverMiddle(Type);
   break;      
}

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

input bool                 Auto5Digits       =  true;

Далее в функции OnInit() объявляем вспомогательную переменную для откорректированного параметра и корректируем его:

int pl=PriceLevel;   
if(Auto5Digits && (Digits()==5 || Digits()==3)){
   pl*=10;
}  

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

diver.SetConditions(Number,Point()*pl,IndLevel);
diver.SetDrawParmeters(ArrowsOnChart,DrawLines,ColBuy,ColSell);

Осталось несколько строк в функции OnCalculate(). Вызов основного метода Calculate():

diver.Calculate(  rates_total,
                  prev_calculated,
                  time,
                  high,
                  low,
                  buf_osc,
                  buf_buy,
                  buf_sell);

В случае рисования графическими объектами необходимо ускорить их отрисовку:

if(ArrowsOnChart || DrawLines){
   ChartRedraw();
}

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

void ~CDiverBase(){
   for(int i=0;i<ArraySize(m_conditions);i++){ // по всем условиям
      if(CheckPointer(m_conditions[i])==POINTER_DYNAMIC){
         delete(m_conditions[i]); // удаление объекта
      }      
   }  
   // удаление графических объектов
   ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME));
   ChartRedraw();          
}   

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


Рис. 15. Индикатор дивергенции на графике цены с отображением стрелок на графике цены и линий меду экстремумами

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

Заключение

Несмотря на высочайшую универсальность индикатора, можно отметить и его недостатки. Основной недостаток — зависимость параметра IndLevel от типа используемого осциллятора, а параметра PriceLevel — от таймфрейма. Чтобы исключить эту зависимость, для этих параметров установлены значения по умолчанию 0. Но при этом практически нереально выполнение условий при некоторых сочетаниях движения цены и индикатора. Если в условие проверки дивергенции входит проверка на горизонтальное движение, ее выполнение маловероятно. При этом остаются варианты дивергенции 1, 3, 7 и 9. Это может представлять собой проблему только в тестере при оптимизации эксперта, использующего индикатор.

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

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

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

Вcе файлы приложения:

  • Include/InDiver/CUniDiverConditions.mqh — файл с классами проверки условий дивергенции;
  • Include/InDiver/CUniDiverExtremums.mqh — файл с классами определения экстремумов; 
  • Include/InDiver/UniDiverDefines.mqh — описание структур и перечислений;
  • Include/InDiver/UniDiverGObjects.mqh — функции для работы с графическими объектами;
  • Indicators/iDivergence.mq5 — индикатор;
  • Indicators/iUniOsc.mq5 — универсальный осциллятор;
  • Include/UniOsc/CUniOsc.mqh — файл с классами для универсального осциллятора;
  • Include/UniOsc/UniOscDefines.mqh — описание структур и перечислений для универсального осциллятора.