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

Nikolay Kositsin | 1 октября, 2010

Введение

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

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


Индикатор Aroon как пример двукратного количественного увеличения размера кода

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

BULLS =  (1 - (bar - SHIFT(MAX(HIGH(), AroonPeriod)))/AroonPeriod) * 100
BEARS = (1 - (bar - SHIFT(MIN (LOW (), AroonPeriod)))/AroonPeriod) * 100

где:

Уже из формул индикатора можно сделать вывод, что для построения индикатора необходимо иметь всего два индикаторных буфера, а сама схема строения индикатора по своей сути будет мало чем отличаться от схемы индикатора SMA_1.mq5, предложенной мной ранее в предыдущей статье.

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

//+------------------------------------------------------------------+
//|                                                        Aroon.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//---- авторство индикатора
#property copyright "2010, MetaQuotes Software Corp."
//---- ссылка на сайт автора
#property link      "http://www.mql5.com"
//---- номер версии индикатора
#property version   "1.00"

Теперь в двенадцатой строке кода следует изменить отрисовку индикатора с основного окна графика на дополнительное окно:

//---- отрисовка индикатора в отдельном окне
#property indicator_separate_window

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

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

//---- для расчёта и отрисовки индикатора использовано два буфера
#property indicator_buffers 2
//---- использовано два графических построения
#property indicator_plots   2

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

//+----------------------------------------------+
//|  Параметры отрисовки бычьего индикатора      |
//+----------------------------------------------+
//---- отрисовка индикатора 1 в виде линии
#property indicator_type1   DRAW_LINE
//---- в качестве цвета бычей линии индикатора использован зелёный цвет
#property indicator_color1  Lime
//---- линия индикатора 1 - непрерывная кривая
#property indicator_style1  STYLE_SOLID
//---- толщина линии индикатора 1 равна 1
#property indicator_width1  1
//---- отображение подписи BullsAroon индикатора
#property indicator_label1  "BullsAroon"
//+----------------------------------------------+
//|  Параметры отрисовки медвежьего индикатора   |
//+----------------------------------------------+
//---- отрисовка индикатора 2 в виде линии
#property indicator_type2   DRAW_LINE
//---- в качестве цвета медвежьей линии индикатора использован красный цвет
#property indicator_color2  Red
//---- линия индикатора 2 - непрерывная кривая
#property indicator_style2  STYLE_SOLID
//---- толщина линии индикатора 2 равна 1
#property indicator_width2  1
//---- отображение подписи BearsAroon индикатора
#property indicator_label2  "BearsAroon"

В этом индикаторе используются три горизонтальных уровня со значениями 30, 50 и 70.

Для отображения этих уровней в код индикатора следует дописать ещё пять строк кода:

//+----------------------------------------------+
//| Параметры отображения горизонтальных уровней |
//+----------------------------------------------+
#property indicator_level1 70.0
#property indicator_level2 50.0
#property indicator_level3 30.0
#property indicator_levelcolor Gray
#property indicator_levelstyle STYLE_DASHDOTDOT

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

//+----------------------------------------------+
//| Входные параметры индикатора                 |
//+----------------------------------------------+
input int AroonPeriod = 9; // период индикатора 
input int AroonShift = 0// сдвиг индикатора по горизонтали в барах 

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

//--- объявление динамических массивов, которые будут в 
//--- дальнейшем использованы в качестве индикаторных буферов
double BullsAroonBuffer[];
double BearsAroonBuffer[]; 

Абсолютно аналогично поступаем с блоком функции OnInit().

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

//--- превращение динамического массива BullsAroonBuffer в индикаторный буфер 
SetIndexBuffer(0, BullsAroonBuffer, INDICATOR_DATA);
//--- осуществление сдвига индикатора 1 по горизонтали на AroonShift
PlotIndexSetInteger(0, PLOT_SHIFT, AroonShift);
//--- осуществление сдвига начала отсчёта отрисовки индикатора 1 на AroonPeriod
PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, AroonPeriod);
//--- создание метки для отображения в DataWindow
PlotIndexSetString(0, PLOT_LABEL, "BearsAroon"); 

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

Затем во вставленном коде изменяем номер индикаторного буфера с нулевого на первый, меняем название индикаторного массива и название строковой подписи для этой линии индикатора:

//--- превращение динамического массива BearsAroonBuffer в индикаторный буфер 
SetIndexBuffer(1, BearsAroonBuffer, INDICATOR_DATA); 
//--- осуществление сдвига индикатора 2 по горизонтали на AroonShift 
PlotIndexSetInteger(1, PLOT_SHIFT, AroonShift); 
//--- осуществление сдвига начала отсчёта отрисовки индикатора 2 на AroonPeriod 
PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, AroonPeriod); 
//--- создание метки для отображения в DataWindow 
PlotIndexSetString(1, PLOT_LABEL, "BullsAroon");  

Сборка строки тоже претерпела небольшие изменения:

//--- инициализации переменной для короткого имени индикатора
string shortname;
StringConcatenate(shortname, "Aroon(", AroonPeriod, ", ", AroonShift, ")"); 

Теперь о точности отрисовки индикатора. Сам диапазон изменения индикатора составляет от нуля и до ста единиц, причём на графике всё время присутствует весь диапазон.

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

//--- определение точности отображения значений индикатора
IndicatorSetInteger(INDICATOR_DIGITS, 0);

В индикаторе SMA_1.mq5 мы использовали первую форму вызова функции OnCalculate().

Для построения кода индикатора Aroon эта форма вызова не подходит по причине отсутствия в ней ценовых массивов high[] и low[]. Эти массивы есть в наличии во второй форме вызова этой функции. И, стало быть, необходимо сделать замену заголовка функции:

int OnCalculate( 
                const int rates_total,    // количество истории в барах на текущем тике
                const int prev_calculated,// количество истории в барах на предыдущем тике
                const datetime& time[],
                const double& open[],    
                const double& high[],     // ценовой массив максимумов цены для расчёта индикатора
                const double& low[],      // ценовой массив минимумов  цены для расчёта индикатора
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[]
              )

После этой замены использование параметра begin потеряло всякий смысл, поэтому его следует удалить из кода!

Сам код для расчёта границ изменения переменной оператора цикла, проверки данных на достаточность для расчёта практически не изменился.

//--- проверка количества баров на достаточность для расчёта
if (rates_total < AroonPeriod - 1) return(0);
   
//--- объявления локальных переменных 
int first, bar;
double BULLS, BEARS; 

//--- расчёт стартового номера first для цикла пересчёта баров
if (prev_calculated == 0)          // проверка на первый старт расчёта индикатора 
    first = AroonPeriod - 1;       // стартовый номер для расчёта всех баров 
else first = prev_calculated - 1// стартовый номер для расчёта новых баров

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

Одним из вариантов выхода из такой ситуации является самостоятельное написание таких пользовательских функций. К счастью, в индикаторе ZigZag.mq5 из набора пользовательских индикаторов папки "MetaTrader5\MQL5\Indicators\Examples" такие функции уже есть в наличии.

Самый простой выход - это выделить код этих функций в индикаторе ZigZag.mq5 мышью, скопировать их в буфер обмена Windows и вставить в наш код, например, после описания функции OnInit() на глобальном уровне:

//+------------------------------------------------------------------+
//|  searching index of the highest bar                              |
//+------------------------------------------------------------------+
int iHighest(const double &array[], // массив для поиска индекса максимального элемента
             int count,            // число элементов массива (в направлении от текущего бара в сторону убывания индекса), 
                                   // среди которых должен быть произведен поиск.
             int startPos          // индекс (смещение относительно текущего бара) начального бара, 
             )                     // с которого начинается поиск наибольшего значения 
  {
//---+
   int index = startPos;
   
   //---- проверка стартового индекса на корректность
   if (startPos < 0)
     {
      Print("Неверное значение в функции iHighest, startPos = ", startPos);
      return (0);
     } 
   //---- проверка значения startPos на корректность
   if (startPos - count < 0) count = startPos;
    
   double max = array[startPos];
   
   //---- поиск индекса
   for(int i = startPos; i > startPos - count; i--)
     {
      if(array[i] > max)
        {
         index = i;
         max = array[i];
        }
     }
//---+ возврат индекса наибольшего бара
   return(index);
  }
//+------------------------------------------------------------------+
//|  searching index of the lowest bar                               |
//+------------------------------------------------------------------+
int iLowest(
            const double &array[], // массив для поиска индекса минимального элемента
            int count,            // число элементов массива (в направлении от текущего бара в сторону убывания индекса), 
                                  // среди которых должен быть произведен поиск.
            int startPos          // индекс (смещение относительно текущего бара) начального бара, 
            )                    // с которого начинается поиск наименьшего значения   
{
//---+
   int index = startPos;
   
   //--- проверка стартового индекса на корректность
   if (startPos < 0)
     {
      Print("Неверное значение в функции iLowest, startPos = ",startPos);
      return(0);
     }
     
   //--- проверка значения startPos на корректность
   if (startPos - count < 0) count = startPos;
    
   double min = array[startPos];
   
   //--- поиск индекса
   for(int i = startPos; i > startPos - count; i--)
     {
      if (array[i] < min)
        {
         index = i;
         min = array[i];
        }
     }
//---+ возврат индекса наименьшего бара
   return(index);
  }

После этого код функции OnCalculate() с вызовами этих функций будет иметь следующий вид:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate( const int rates_total,    // количество истории в барах на текущем тике
               const int prev_calculated,// количество истории в барах на предыдущем тике
               const datetime& time[],
               const double& open[],    
               const double& high[],     // ценовой массив максимумов цены для расчёта индикатора
               const double& low[],      // ценовой массив минимумов цены  для расчёта индикатора
               const double& close[],
               const long& tick_volume[],
               const long& volume[],
               const int& spread[]
             )
  {
//---+   
   //--- проверка количества баров на достаточность для расчёта
   if (rates_total < AroonPeriod - 1)
    return(0);
   
   //--- объявления локальных переменных 
   int first, bar;
   double BULLS, BEARS;
   
   //--- расчёт стартового номера first для цикла пересчёта баров
   if (prev_calculated == 0// проверка на первый старт расчёта индикатора
     first = AroonPeriod - 1; // стартовый номер для расчёта всех баров

   else first = prev_calculated - 1; // стартовый номер для расчёта новых баров

   //--- основной цикл расчёта индикатора
   for(bar = first; bar < rates_total; bar++)
    {
     //--- вычисление индикаторных значений
     BULLS = 100 - (bar - iHighest(high, AroonPeriod, bar) + 0.5) * 100 / AroonPeriod;
     BEARS = 100 - (bar - iLowest (low,  AroonPeriod, bar) + 0.5) * 100 / AroonPeriod;

     //--- инициализация ячеек индикаторных буферов полученными значениями 
     BullsAroonBuffer[bar] = BULLS;
     BearsAroonBuffer[bar] = BEARS;
    }
//---+     
   return(rates_total);
  }
//+------------------------------------------------------------------+

Для полной симметрии графика относительно уровня 50, я слегка подкорректировал  в коде смещение индикаторов по вертикали по сравнению с оригиналом с помощью значения 0.5.

Вот результат работы этого индикатора на графике:

                                                                              

Задачу поиска индексной позиции элемента с максимальным или минимальным значением на расстоянии, не большем чем AroonPeriod от текущего бара, можно решить и несколько иначе.  В MQL5 есть встроенные функции ArrayMaximum() и ArrayMinimum() для поиска экстремумов, но делают они это в направлении роста индекса.

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

Но в такой ситуации следует изменить направление пересчета баров в операторе цикла и сделать другой расчёт для инициализации переменной first.

Итоговый вид функции OnCalculate() для этого случая получается следующий:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(
                const int rates_total,    // количество истории в барах на текущем тике
                const int prev_calculated,// количество истории в барах на предыдущем тике
                const datetime& time[],
                const double& open[],    
                const double& high[],     // ценовой массив максимумов цены для расчёта индикатора
                const double& low[],      // ценовой массив минимумов  цены для расчёта индикатора
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[]
              )
  {
//---+   
   //--- проверка количества баров на достаточность для расчёта
   if (rates_total < AroonPeriod - 1)
    return(0);
    
   //--- индексация элементов в массивах как в таймсериях
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   ArraySetAsSeries(BullsAroonBuffer, true);
   ArraySetAsSeries(BearsAroonBuffer, true);
   
   //--- объявления локальных переменных 
   int limit, bar;
   double BULLS, BEARS;
   
   //--- расчёт стартового номера first для цикла пересчёта баров
   if (prev_calculated == 0)   // проверка на первый старт расчёта индикатора
       limit = rates_total - AroonPeriod - 1// стартовый номер для расчёта всех баров
   else limit = rates_total - prev_calculated; // стартовый номер для расчёта новых баров
   
   //--- основной цикл расчёта индикатора
   for(bar = limit; bar >= 0; bar--)
    {
     //--- вычисление индикаторных значений
     BULLS = 100 + (bar - ArrayMaximum(high, bar, AroonPeriod) - 0.5) * 100 / AroonPeriod;
     BEARS = 100 + (bar - ArrayMinimum(low,  bar, AroonPeriod) - 0.5) * 100 / AroonPeriod;

     //--- инициализация ячеек индикаторных буферов полученными значениями 
     BullsAroonBuffer[bar] = BULLS;
     BearsAroonBuffer[bar] = BEARS;
    }
//----+     
   return(rates_total);
  }
//+------------------------------------------------------------------+

Я поменял название переменной first на более подходящее в данной ситуации limit.

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

Заключение

Итак, дело сделано! Индикатор построен, и даже в двух вариантах.

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