Как написать индикатор в MQL5

MetaQuotes | 12 января, 2010


Введение

Что представляет собой индикатор? Это набор вычисленных значений, которые мы хотим отобразить на экране монитора удобным для нас образом. Наборы значений представляются в программах в виде массивов. Таким образом, создание индикатора - это написание алгоритма, который обрабатывает одни массивы (массивы цен) и записывает результаты обработки в другие массивы (значения индикаторов).

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


Какие бывают индикаторы?

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


Индикатор True Strength Index

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

TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

где:

Из этой формулы мы можем выделить три параметра, от которых зависит расчет индикатора. Это периоды r и s, а также тип цен, на которых производятся вычисления. В данном случае указана CLOSE - цена закрытия.


Мастер создания экспертов MQL5

Сделаем индикатор TSI в виде линии синего цвета, для этого запустим "MQL5 Wizard". На первом шаге указываем тип создаваемой программы - пользовательский индикатор. На втором шаге зададим имя создаваемой программы, параметры r и s, а также их значения.

MQL5 Wizard: задание имени и параметров индикатора

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

MQL5 Wizard: задание типа индикатора

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

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| 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[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

MQL5 Wizard создал заголовок индикатора, в котором прописал свойства индикатора, а именно:

Черновая работа проделана, теперь мы готовы уточнять и улучшать наш код.


Функция OnCalculate()

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

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

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

Но нам в данном случае требуется для расчетов только один массив данных, поэтому мы изменим OnCalculate() на первую форму вызова.

int OnCalculate (const int rates_total,      // размер массива price[]
                 const int prev_calculated,  // количество доступных баров на предыдущем вызове
                 const int begin,            // с какого индекса в массиве price[] начинаются достоверные данные
                 const double& price[])      // массив, по которому и будет считаться индикатор
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }  

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

Указание типа данных для расчета пользовательского индикатора

Если мы выберем на вкладке "Parameters" значение Close (оно предлагается по умолчанию), то массив price[], передаваемый в функцию OnCalculate(), будет содержать цены закрытия. Если выберем, например, цену Typical Price, то массив price[] будет содержать цены (High+Low+Close)/3 за каждый период.

Параметр rates_total указывает размер массива price[], он будет полезен для организации вычислений в цикле. Индексация элементов в массиве price[] начинается с нуля и направлена из прошлого в будущее. То есть, элемент price[0] содержит самое старое значение, а элемент price[rates_total-1] содержит самый свежий на текущий момент элемент массива.


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

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

  1. для значений mtm - массив MTMBuffer[];
  2. для значений |mtm| - массив AbsMTMBuffer[];
  3. для EMA(mtm,r) - массив EMA_MTMBuffer[];
  4. для EMA(EMA(mtm,r),s) - массив EMA2_MTMBuffer[];
  5. для EMA(|mtm|,r) - массив EMA_AbsMTMBuffer[];
  6. для EMA(EMA(|mtm|,r),s) - массив EMA2_AbsMTMBuffer[].

Итого, требуется добавить еще 6 массивов типа double на глобальном уровне и связать в функции OnInit() эти массивы с буферами индикатора. Не забудем указать новое число индикаторных буферов, свойство indicator_buffers должно быть равно 7 (было 1, добавилось 6).

#property indicator_buffers 7

Теперь код индикатора выглядит так:

#property indicator_separate_window
#property indicator_buffers 7
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
double         MTMBuffer[];
double         AbsMTMBuffer[];
double         EMA_MTMBuffer[];
double         EMA2_MTMBuffer[];
double         EMA_AbsMTMBuffer[];
double         EMA2_AbsMTMBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // размер массива price[];
                 const int prev_calculated,// количество доступных баров ;
                                           // на предыдущем вызове;
                 const int begin,          // с какого индекса в массиве 
                                           // price[] начинаются достоверные данные;
                 const double& price[])    // массив, по которому и будет считаться индикатор;
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }


Промежуточные вычисления

Легче всего организовать вычисления значений для буферов MTMBuffer[] и AbsMTMBuffer[]. Последовательно в цикле пробегаем от значения price[1] до price[rates_total-1] и записываем в один массив разность, а в другой - абсолютное значение разности.

//--- рассчитать значения mtm и |mtm|
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

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

В языке MQL5 нет встроенных функций для расчета скользящих средних по значениям массива, но есть готовая библиотека функций MovingAverages.mqh, полный путь к которой выглядит как каталог_терминала/MQL5/Include/MovingAverages.mqh, где каталог_терминала - каталог, куда установлен терминал MetaTrader 5. Библиотека представляет собой включаемый файл и содержит функции для вычисления скользящих средних на массиве любым из четырех классических способов:

Чтобы воспользоваться этими функциями в любой программе MQL5, достаточно в заголовке исходного кода указать:

#include <MovingAverages.mqh>

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


Функция сглаживания на массиве

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

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

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

Все указанные функции, кроме массивов buffer[], price[] и периода усреднения period, получают еще 3 параметра, назначение которых аналогично параметрам функции OnCalculate() - rates_total, prev_calculated и begin. Функции этой группы правильно обрабатывают переданные массивы price[] и buffer[], учитывая направление индексации (признак AS_SERIES).

Параметр begin указывает, с какого индекса в массиве-источнике начинаются значимые данные, то есть данные, которые нужно обрабатывать. Для массива MTMBuffer[] реальные данные начинаются с индекса 1, так как MTMBuffer[1]=price[1]-price[0]. Значение MTMBuffer[0] просто не определено, поэтому begin=1.

//--- рассчитаем первую скользящую среднюю на массивах
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // с какого индекса есть значения в массиве для сглаживания 
                         r,  // период экспроненциальной средней
                         MTMBuffer,       // буфер для взятия средней
                         EMA_MTMBuffer);  // в этот буфер помещаем значения средней
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

При усреднении необходимо учитывать значение периода, так как в выходном массиве вычисленные значения заполняются с некоторым отставанием, тем большим, чем больше период усреднения. Например, если period=10, то значения в результирующем массиве начинаются с begin+period-1=begin+10-1. При последующем обращении к массиву buffer[] необходимо помнить об этом, и начинать обработку с индекса begin+period-1.

Таким образом, мы легко можем получить вторую экспоненциальную среднюю от массивов MTMBuffer[] и AbsMTMBuffer:

//--- рассчитаем вторую скользящую среднюю на массивах
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Значение begin теперь равно r, так как begin=1+r-1 (r - период первоначального экспоненциального усреднения, начинали обработку с индекса 1). В выходных массивах EMA2_MTMBuffer[] и EMA2_AbsMTMBuffer[] вычисленные значения начинаются с индекса r+s-1, так как начинали мы обрабатывать входные массивы с индекса r, а период средней для второго экспоненциального усреднения равен s.

Все предварительные вычисления сделаны, теперь мы можем вычислить значения индикаторного буфера TSIBuffer[], который и будет отображаться на графике.

//--- теперь вычислим значения индикатора
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

Компилируем код клавишей F5 и запускаем в терминале MetaTrader 5. Ура, работает!

Первая версия индикатора True Strength Index

Но остались еще некоторые вопросы.


Оптимизация вычислений

На самом деле, написать работающий индикатор - это еще не все. Если внимательно присмотреться к текущей реализации функции OnCalculate(), то обнаружим, что она написана не совсем оптимально.

int OnCalculate (const int rates_total,    // размер массива price[];
                 const int prev_calculated,// количество доступных баров ;
                 // на предыдущем вызове;
                 const int begin,// с какого индекса в массиве 
                 // price[] начинаются достоверные данные;
                 const double &price[]) // массив, по которому и будет считаться индикатор;
  {
//--- рассчитать значения mtm и |mtm|
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- рассчитаем первую скользящую среднюю на массивах
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // с какого индекса есть значения в массиве для сглаживания 
                         r,  // период экспроненциальной средней
                         MTMBuffer,       // буфер для взятия средней
                         EMA_MTMBuffer);  // в этот буфер помещаем значения средней
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- рассчитаем вторую скользящую среднюю на массивах
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
//--- теперь вычислим значения индикатора
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

На каждом запуске функции мы рассчитываем значения в массивах MTMBuffer[] и AbsMTMBuffer[], а ведь в случае, если размер массива price[] будет составлять сотни тысяч и даже миллионы, ненужные повторные вычисления могут съесть любые ресурсы процессора, каким бы мощным и современным он ни был.

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

//--- если это первый вызов 
   if(prev_calculated==0)
     {
      //--- для нулевых индексов установим нулевые значения
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- рассчитать значения mtm и |mtm|
   int start;
   if(prev_calculated==0) start=1;  // начнем заполнять MTMBuffer[] и AbsMTMBuffer[]  с 1-го индекса 
   else start=prev_calculated-1;    // установим start равным последнему индексу в массивах 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Блоки вычислений массивов EMA_MTMBuffer[], EMA_AbsMTMBuffer[], EMA2_MTMBuffer[] и EMA2_AbsMTMBuffer[] в оптимизации вычислений не нуждаются, так как функция ExponentialMAOnBuffer() сама написана уже оптимальным образом. Требуется оптимизировать только вычисление значений для массива TSIBuffer[], но это не сложно сделать тем же способом, что был использован для MTMBuffer[].

//--- теперь вычислим значения индикатора
   if(prev_calculated==0) start=r+s-1; // установим начальный индекс для входных массивов
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);

Последнее замечание для процедуры оптимизации: функция OnCalculate() возвращает значение rates_total. Напомним, что это означает количество элементов во входном массиве price[], по которому производится расчет индикатора.

Значение, возвращаемое функцией OnCalculate(), сохраняется в памяти терминала, и при следующем вызове OnCalculate() передается в нее как значение входного параметра prev_calculated.

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


Проверка входных данных

Остался последний штрих, который необходимо сделать, чтобы функция OnCalculate() работала безупречно. Добавим проверку массива price[], по которому вычисляются значения индикатора. Если размер массива (значение rates_total) слишком мал, то никакие вычисления делать не требуется, необходимо дождаться следующего вызова OnCalculate(), на котором данных будет достаточно.

//--- если размер массива price[] слишком мал
  if(rates_total<r+s) return(0); // ничего не считаем и ничего не рисуем на графике
//--- если это первый вызов 
   if(prev_calculated==0)
     {
      //--- для нулевых индексов установим нулевые значения
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

Так как для вычислений индикатора True Strength Index дважды последовательно используется экспоненциальное сглаживание, то размер массива price[] должен быть как минимум не меньше суммы периодов r и s, в противном случае выполнение прекращается, и функция OnCalculate() возвращает 0. Нулевое возвращаемое значение означает, что индикатор не будет отображаться на графике, так как его значения не рассчитаны.


Настройка отображения

С точки зрения правильности вычислений, индикатор полностью готов к использованию. Но если мы его вызовем из другой mql5-программы, то по умолчанию он будет строиться по ценам Close. Можно указать другой тип цен по умолчанию, который будет использоваться, для этого нужно указать значение из перечисления ENUM_APPLIED_PRICE в свойство индикатора indicator_applied_price.

Например, чтобы установить в качестве цены по умолчанию типичную цену ( (high+low+close)/3), запишем так:

#property indicator_applied_price PRICE_TYPICAL


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

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

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- с какого бара начнет отрисовываться индикатор
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- установим метку для отображения в DataWindow
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- установим имя для показа в отдельном подокне и во всплывающей подсказке
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- укажем точность отображения значений индикатора
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

Если запустить обе версии индикатора и прокрутить график в начало истории, то можно будет увидеть все отличия.

Вторая версия индикатора True Strength Index выглядит лучше


Заключение

На примере создания индикатора True Strength Index можно выделить основные моменты в написании любого индикатора на MQL5: