Как написать индикатор в MQL5
Введение
Что представляет собой индикатор? Это набор вычисленных значений, которые мы хотим отобразить на экране монитора удобным для нас образом. Наборы значений представляются в программах в виде массивов. Таким образом, создание индикатора - это написание алгоритма, который обрабатывает одни массивы (массивы цен) и записывает результаты обработки в другие массивы (значения индикаторов).
Несмотря на то, что существует множество уже написанных и ставших классическими индикаторов, необходимость в создании своего собственного индикатора не исчезнет никогда. Такие индикаторы, которые мы создаем по собственному алгоритму, называются пользовательскими индикаторами. В данной статье рассказывается, как написать свой собственный простой индикатор.
Какие бывают индикаторы?
Индикатор может представляться в виде цветных линий или областей, а может отображаться и в виде специальных значков, отмечающих на графике моменты, благоприятные для входа в позицию. Возможны также сочетания этих видов, что дает еще большее множество индикаторов. Мы рассмотрим создание индикатора на примере известного индикатора True Strength Index, разработанного Уильямом Блау.
Индикатор True Strength Index
Индикатор TSI (индекс истинной силы) основан на дважды сглаженном моментуме для идентификации трендов и зон перекупленности и перепроданности. Математическое обоснование вы можете прочесть в книге автора "Моментум, направленность и расхождение", здесь же мы только приведем формулу для расчета.
где:
- mtm = CLOSEcurrent – CLOSprev, массив значений, означающих разницу между ценами закрытия текущего и предыдущего баров;
- EMA(mtm,r) = экспоненциальное сглаживание значений массива mtm с длиной периода равной r;
- EMA(EMA(mtm,r),s) = экспоненциальное сглаживание значений EMA(mtm,r) с периодом, равным s;
- |mtm| = абсолютные значения mtm;
- r = 25,
- s = 13.
Из этой формулы мы можем выделить три параметра, от которых зависит расчет индикатора. Это периоды r и s, а также тип цен, на которых производятся вычисления. В данном случае указана CLOSE - цена закрытия.
Мастер создания экспертов MQL5
Сделаем индикатор TSI в виде линии синего цвета, для этого запустим "MQL5 Wizard". На первом шаге указываем тип создаваемой программы - пользовательский индикатор. На втором шаге зададим имя создаваемой программы, параметры r и s, а также их значения.
На следующем шаге укажем, что индикатор будет отображаться в отдельном окне в виде синей линии, и зададим метку "TSI" для этой линии.
Все начальные данные введены, жмем кнопку "Готово" и получаем заготовку для нашего индикатора.
//+------------------------------------------------------------------+ //| 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 создал заголовок индикатора, в котором прописал свойства индикатора, а именно:
- отображать индикатор в отдельном окне;
- количество индикаторных буферов, indicator_buffers=1;
- количество графических построений, indicator_plots= 1;
- отображаемое наименование (Label) для построения номер 1, indicator_label1="TSI";
- стиль первого построения - линия, indicator_type1=DRAW_LINE;
- цвет отображения построения номер 1, indicator_color1=Blue;
- стиль отображения линии, indicator_style1=STYLE_SOLID;
- толщина линий для построения номер 1, indicator_width1=1.
Черновая работа проделана, теперь мы готовы уточнять и улучшать наш код.
Функция OnCalculate()
Функция OnCalculate() является обработчиком события Calculate, которое возникает в тех случаях, когда требуется провести пересчет значений индикатора и заново отобразить его на графике. Это событие прихода нового тика, обновление истории по символу и т.д. Поэтому основной код для всех расчетов значений индикатора должен находиться именно в этой функции.
Конечно, вспомогательные расчеты могут быть реализованы и в других отдельных функциях, но использоваться эти функции должны именно в обработчике OnCalculate.
По умолчанию MQL5 Wizard создает вторую форму OnCalculate(), которая обеспечивает доступ ко всем видам таймсерий:
- цены Open, High, Low, Close;
- объемы (реальный и/или тиковый);
- спреды;
- время открытия периода.
Но нам в данном случае требуется для расчетов только один массив данных, поэтому мы изменим 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. Из формулы видно, что нам требуются дополнительные массивы:
- для значений mtm - массив MTMBuffer[];
- для значений |mtm| - массив AbsMTMBuffer[];
- для EMA(mtm,r) - массив EMA_MTMBuffer[];
- для EMA(EMA(mtm,r),s) - массив EMA2_MTMBuffer[];
- для EMA(|mtm|,r) - массив EMA_AbsMTMBuffer[];
- для 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 восемь функций, которые можно разбить на две группы однотипных функций по четыре в каждой. Первая группа - это функции, которые получают массив и просто возвращают значение скользящей на указанной позиции:
- SimpleMA() - для вычисления значения простой средней;
- ExponentialMA() - для вычисления значения экспоненциальной средней;
- SmoothedMA() - для вычисления значения сглаженной средней;
- LinearWeightedMA() - для вычисления значения линейно-взвешенной средней.
Эти функции предназначены для однократного получения значения средней на массиве и не оптимизированы под многократные вызовы. Если вы хотите использовать функцию из этой группы в цикле (для вычисления значений средней с последующей записью каждого вычисленного значения в массив), то вам необходимо самостоятельно позаботиться об организации оптимального алгоритма.
Вторая группа функций предназначена для заполнения массива-приемника значениями скользящей средней на основе массива исходных значений:
- SimpleMAOnBuffer() - заполняет выходной массив buffer[] значениями простой средней от массива price[];
- ExponentialMAOnBuffer() - заполняет выходной массив buffer[] значениями экспоненциальной средней от массива price[];
- SmoothedMAOnBuffer() - заполняет выходной массив buffer[] значениями сглаженной средней от массива price[];
- LinearWeightedMAOnBuffer() - заполняет выходной массив buffer[] значениями линейно-взвешенной средней от массива price[].
Все указанные функции, кроме массивов 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. Ура, работает!
Но остались еще некоторые вопросы.
Оптимизация вычислений
На самом деле, написать работающий индикатор - это еще не все. Если внимательно присмотреться к текущей реализации функции 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(), то более ничего и не требуется улучшать. Но для использования его по прямому назначению, то есть на графике, желательно сделать дополнительные настройки:
- номер бара, с которого индикатор начинает отображаться;
- название линии (Label) для значений в TSIBuffer[], которое будет отображаться в окне DataWindow;
- краткое имя индикатора, отображаемое в отдельном окне и во всплывающей подсказке, если поднести курсор мышки к линии индикатора;
- количество знаков после запятой, показываемое в значениях индикатора (на точность вычислений это не влияет).
Эти настройки можно сделать в обработчике 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 можно выделить основные моменты в написании любого индикатора на MQL5:
- Для создания собственного пользовательского индикатора используйте MQL5 Wizard, который поможет сделать предварительные рутинные операции по настройке индикатора. Выберите нужный вариант функции OnCalculate().
- При необходимости добавьте дополнительные массивы под промежуточные вычисления и свяжите их функцией SetIndexBuffer() с соответствующими индикаторными буферами. Укажите для этих буферов тип INDICATOR_CALCULATIONS.
- Проведите оптимизацию вычислений в функции OnCalculate(), так как она будет вызываться при каждом изменении ценовых данных. Используйте готовые отлаженные функции для облегчения написания и лучшей читаемости кода.
- Сделайте дополнительный визуальный тюнинг индикатора, чтобы им было удобно и приятно пользоваться не только в другой mql5-программе, но и человеку.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Можно ли к этим индикаторным буферам для промежуточных расчетов обращаться из советника? Т.е. они как обычные буферы индикатора, но просто не отображаются?
Можно ли к этим индикаторным буферам для промежуточных расчетов обращаться из советника?
А вы попробуйте
r+s-1
why
-1
, please?
Thanks so much.
Hello, someone could explain me in
why
, please?
Thanks so much.
Да, проще русский язык выучить.