Программирование индикаторов в языке MQL5

Создание первого индикатора

Рассмотрим создание технического индикатора на примере создания простой скользящей средней. Все примеры кода этого раздела будут располагаться в папке «MQL5/Indicators/ Изучение MQL5». Создание индикатора начинается так же, как и создание скрипта – в навигаторе редактора необходимо выделить папку, в которой будет создаваться файл, и выполнить команду главного меню: Фал – Создать.

Другой способ – щелкнуть на имени папки и в контекстном меню выбрать команду «Новый файл». В открывшемся окне, имеющем имя «Мастер MQL: Файл», необходимо выбрать вариант «Пользовательский индикатор» и нажать на кнопку «Далее» (рис. 2).

Первый шаг создания файла пользовательского индикатора

Рис. 2. Первый шаг создания файла пользовательского индикатора

На следующем шаге в поле «Имя» надо добавить имя создаваемого индикатора, в данном случае будет имя «001 MA». Назначение полей «Автор» и «Ссылка» вам уже должно быть известно после изучения скриптов. Также должно быть известно назначение поля «Параметры». Используя кнопку «Добавить», добавьте один внешний параметр и нажмите на кнопку «Далее» (рис. 3).

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

Существует два варианта этой функции. Один вариант имеет параметры open, high, low, close и др. – этот вариант используется для расчета индикатора по конкретным рядам исторических данных. Второй вариант имеет параметр price – это вариант позволяет создавать универсальные индикаторы. При прикреплении такого индикатора на график, в его окне свойств можно делать выбор ряда данных, по которым индикатор будет рассчитываться, это может быть как цена, так и данные другого индикатора, находящегося на графике. В данном примере используем второй вариант.

Второй шаг создания файла индикатора

Рис. 3. Второй шаг создания файла индикатора – вводи имени файла и создание внешних параметров

Ниже выбора основного обработчика событий OnCalculate() располагаются элементы управления для выбора дополнительных обработчиков событий: OnTimer() и OnChartEven(). Событие OnTimer() выполняется с установленным интервалом времени. Существует два способа задать интервал таймера: в секундах и в миллисекундах. Событие OnChartEven() происходит при каких либо действиях с графиком, это может быть присоединение другого индикатора на график, создание, удаление графического объекта, щелчок по графическому объекту и пр. Пока не будем использовать эти обработчики, то есть галки «OnTimer» и «OnChartEvent» необходимо снять. Для перехода к следующему шагу создания индикатора необходимо нажать кнопку «Далее» (рис. 4).

На следующем шаге выбирается место отображения индикатора – на графике цены или в дополнительном подокне. Если установить галку «Индикатор в отдельном окне», становятся доступны еще два элемента управления – «Минимум» и «Максимум» – эти элементы управления позволяют задавать индикатору фиксированную шакалу по вертикали, например, как у индикатора «Стохастик» от 0 до 100. Пока будем создавать индикатор для графика цены, поэтому галку «Индикатор в отдельном окне» надо снять.

Поле (или таблица) «Отрисовка» позволяет задавать индикатору количество отображаемых рядов данных и тип их отображения. Эти ряды данных называются индикаторными буферами. Самый просто способ отображения индикаторного буфера, это линия. Существует еще огромное количество других способов отображения: гистограмма, стрелки, секции, бары, свечи, цветные линии, заливка и пр. Все эти варианты будут подробно рассмотрены в дальнейшем. А пока создадим один буфер – линию, для этого надо нажать кнопку «Добавить», сразу после этого в таблице появится запись из трех полей (рис. 5).

Третий шаг создания файла индикатора

Рис. 4. Третий шаг создания файла индикатора – выбор обработчиков событий

Четвертый шаг создания индикатора

Рис. 5. Четвертый шаг создания индикатора – добавление индикаторных буферов

Каждое из этих полей можно изменить, если щелкнуть по нему мышкой. Если щелкнуть по второму полю, откроется список типов буфера (всего 17 вариантов), для данного примера нужен вариант «Line». После нажатия кнопки «Готово» окно Мастера MQL закроется, а в рабочем поле редактора отроется файл индикатора. Рассмотрим содержимое этого файла. Начало кода:

#property copyright "Copyright 2017, MetaQuotes Software Corp." #property link https://www.mql5.com
#property version "1.00" #property indicator_chart_window #property indicator_buffers 1
#property indicator_plots 1

Свойства copyright, link, version вам уже должны быть известны. Свойство indicator_chart_window означает, что индикатор будет отображаться на графике цены. Для индикаторов в подокне вместо этого свойства используется свойство indicator_separate_window. Свойства indicator_buffers определяет общее количество индикаторных буферов, а свойство property indicator_plots определяет количество только отображаемых буферов. Не все индикаторные буферы бывает нужно отображать на графике, некоторые из них используются для промежуточных расчетов.

Следующая группа свойств определяет отображение одного буфера:

#property indicator_label1 "Label1" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1

Индикатор имеет один буфер, но все же это первый буфер по порядку, поэтому все свойства имеют в окончании цифру «1». Если бы в индикаторе был еще один буфер, то добавилась бы еще одна такая же группа, но со свойствами indicator_label2, indicator_type2 и т.д. Свойство indicator_label определяет имя буфера, отображаемое в окне данных терминала (рис. 6).

Имя буфера индикатора в окне данных

Рис. 6. Имя буфера индикатора в окне данных (нижняя строка)

Это то имя, которое было указано на четвертом шаге создания файла в столбце «Название» (рис. 5). Свойство indicator_type определяет способ отображения буфера, в данном случае – линия. Свойство indicator_color – цвет линии. Свойство indicator_style – стиль рисования (STYLE_SOLID — сплошная линия). Свойство indicator_width – толщина линии в пикселях. В следующей части кода располагается внешний параметр:

input int Input1;

Переименуем его и инициализируем:

input int period=14;

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

Следующая строка:

double Label1Buffer[];

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

Следующая часть кода – функция OnInit(). Это функция выполняется один раз на запуске индикатора. В ней обычно выполняются разные подготовительные действия, в частности преобразование массива в индикаторный буфер. Делается это при помощи функции SetIndexBuffer(). Отличие индикаторного буфера от обычного массива в том, что он, кроме того, что отображается на графике, автоматически меняет свой размер в соответствии с количеством баров на графике, то есть при появлении нового бара размер массива увеличивается на один элемент. Функция SetIndexBuffer() имеет три параметра: индекс буфера (просто порядковый номер буфера с отсчетом от нуля), имя массива, тип буфера (INDICATOR_DATA – отображаемый буфер, INDICATOR_CALCULATIONS – буфер для вспомогательных расчетов).

Функция OnCalculate() – эта функция вызывается при каждом изменении цены, в ней выполняются все расчеты индикатора. Рассмотрим параметры функции. Параметр rates_total содержит количество баров на графике. Параметр prev_calculated – количество обсчитанных баров (чуть позже об этом параметре будет подобней). Параметр begin – индекс бара, с которого располагаются данные в массиве price. А массив price – это те данные, на основе которых будет рассчитываться индикатор.

Приступаем к самому главному этапу создания индикатора. В первую очередь выполняется расчет диапазона обсчитываемых баров. Отсчет баров в индикаторе выполняется слева направо от нуля. Так что, конец диапазона очевиден – это переменная rates_total. А для начала диапазона объявим переменную с именем start:

int start;

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

if(prev_calculated==0){ start=begin+period-1;
}

К расчету диапазона обсчитываемых баров необходимо подходить очень внимательно и аккуратно, если при обращении к массиву (индикаторному буферу) указать несуществующий индекс, произойдет ошибка («Array out of range» – выход за пределы массива) и индикатор перестанет работать. Более того, если такая ошибка случается часто, то, для возобновления правильной работы индикатора, может потребоваться не только устранить ошибку в коде, но и перезапустить терминал.

Обратите внимание на строку в конце функции:

return(rates_total);

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

else{
start=prev_calculated-1;
}

Используем быстрый алгоритм расчета скользящей средней. Простая средняя эта сумма значений, деленная на их количество. Достаточно посчитать всю сумму один раз, а потом для каждого обсчитываемого бара вычитать из суммы одно старое значение и добавлять одно новое. Но поскольку у нас есть только один буфер, буем считать не сумму, а сразу среднее. Поэтому вычитать и добавлять будем значения, деленные на период средней. Следующий код пишем в составном операторе if для случая, когда значение переменной prev_calculated=0. Обнуляем элемент буфера:

Label1Buffer[start]=0;

Суммируем значения:

for(int i=start-period+1;i<=start;i++){ Label1Buffer[start]+=price[i];
}

Делим сумму на величину периода:

Label1Buffer[start]/=period;

Увеличиваем переменную start на 1, чтобы следующий расчет происходил для следующего бара:

start++;

Теперь проводим расчет в соответствии с определенным ранее диапазоном обсчитываемых баров. Главный индикаторный цикл:

for(int i=start;i<rates_total;i++){

}

Следующий код располагается внутри этого цикла. Label1Buffer[i]=Label1Buffer[i-1]+(price[i]-price[i-period])/period; Далее приведен весь код функции OnCalculate():

int OnCalculate(const int rates_total,
const int prev_calculated, const int begin,
const double &price[])
{
int start; if(prev_calculated==0){
start=begin+period-1;

Label1Buffer[start]=0;
for(int i=start-period+1;i<=start;i++){ Label1Buffer[start]+=price[i];
}
Label1Buffer[start]/=period; start++;
}
else{
start=prev_calculated-1;
}
for(int i=start;i<rates_total;i++){
Label1Buffer[i]=Label1Buffer[i-1]+(price[i]-price[i-period])/period;
}
return(rates_total);
}

Прикрепляем индикатор на графике. Обратите внимание на вкладки окна свойств индикатора, среди них есть вкладка «Параметры», в этой вкладке выбирается тип цены или ряд данных, к которой применяется индикатор (рис. 7).

Выбор типа цены или ряда данных

Рис. 7. Выбор типа цены или ряда данных, к которым применяется индикатор

Пока выберем вариант «Close». Запустим еще один индикатор и применим его к предыдущему индикатору. Для этого индикатор надо перетащить мышкой из навигатора на график, а во вкладке «Параметры» выбрать «Данные предыдущего индикатора».

Скользящая средняя от скользящей средней

Рис. 8. Скользящая средняя от скользящей средней

Во вкладке «Цвета» выберите другой цвет и/или толщину. Должна получиться скользящая средняя от скользящей средней (рис. 8).

Как было показано выше, в окне данных линия индикатора имеет имя «Label1», изменим его на более подходящее. Для этого надо изменить значение свойства indicator_label1, пусть будет имя «МА»:

#property indicator_label1 "MA"

Цветная линия

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

Константа EMPTY_VALUE равна максимальному значению переменной double – константе DBL_MAX. В случае необходимости, значение, при котором линия не отображается, можно изменить, для этого используется функция PlotIndexSetDouble(). Здесь, так же, как и во многих других случаях языка MQL5 существует функции: PlotIndexSetDouble(), PlotIndexSetInteger() и PlotIndexSetString(), позволяющие устанавливать индикаторным буферам различные свойства. А также есть одна функция для получения свойств индикаторного буфера – функция PlotIndexGetInteger(). Отдельно на них не будем останавливаться. Будем изучать их по мере возникновения соответствующей необходимости.

Недостатком создания разноцветных линий из нескольких буферов, является наличие разрывов линий на барах, на которых меняется цвет. Конечно, при желании разрывы можно устранить, но это не так просто, как может показаться сначала. К тому же в языке MQL5 существует специальные средства создания разноцветных линий. Используя эти средства, создадим индикатор на основе стандартной скользящей средней. Линия индикатора будет менять цвет в зависимости от ее направления. При движении вверх линия будет синей, при движении вниз – красной.

Создаем новый индикатор. В Мастере MQL укажем имя индикатора – «002 MA Colored», создадим одну внешнюю переменную (только чтобы обозначить место их расположения), выберем обработчик событий – первый вариант с параметрами open, high, low, close. На завершающем шаге работы мастера выберем тип буфера «Color Line». Рассмотрим получившийся файл. Обратите внимание на свойство indicator_type1, теперь оно имеет значение DRAW_COLOR_LINE – цветная линия. Чтобы потом не возвращаться, сразу изменим свойство indicator_label1 на «MA Colored». Также заметьте, не смотря на то, что в мастере создавался один буфер, в файле объявлено два массива:

double Label1Buffer[];
double Label1Colors[];

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

SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA); SetIndexBuffer(1,Label1Colors,INDICATOR_COLOR_INDEX);

Первый буфер (с индексом 0) представляет собой буфер отображаемых данных, а второму устанавливается тип INDICATOR_COLOR_INDEX – цветовые индексы. Также обратите внимание, что свойство indicator_buffers равно 2, а indicator_plots равно 1, это значит, что – всего используется два буфера, но отображается только один. Перекрашивание линии выполняется за счет присвоения элементам второго буфера различных значений. Набор цветов задается свойством indicator_color, сейчас свойству indicator_color1, присвоен один цвет, добавим через запятую еще один:

#property indicator_color1 clrBlue,clrRed

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

Поскольку будет использоваться стандартная скользящая средняя – функция iMA(), в справочном руководстве по языку MQL5, в разделе «Технические индикаторы» посмотрим, с какими параметрами она вызывается, и создадим соответствующие внешние параметры:

input int period=14; input ENUM_MA_METHOD method=MODE_SMA;
input ENUM_APPLIED_PRICE price=PRICE_CLOSE;

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

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

int h;

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

h=iMA(Symbol(),Period(),period,0,method,price); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iMA"); return(INIT_FAILED);
}

Весь код функции OnInit():

int OnInit()
{
h=iMA(Symbol(),Period(),period,0,method,price); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iMA"); return(INIT_FAILED);
}

SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA); SetIndexBuffer(1,Label1Colors,INDICATOR_COLOR_INDEX);

return(INIT_SUCCEEDED);
}

Продолжаем в функции OnCalculate(). Сначала разберемся с параметрами. Назначение параметров

rates_total и prev_calculated вам уже известно по предыдущему индикатору. Далее:

  • time – время бара,
  • open – цена открытия бара,
  • high – максимальная цена бара,
  • low – минимальная цена бара,
  • close – цена закрытия бара,
  • tick_volume – тиковый объем,
  • volume – реальный объем,
  • spread – спред.

Массивы volume и spread могут быть пустыми, поскольку реальный объем доступен не для всех символов, а история спреда не очень полена для баров, ведь спред может меняться на каждом тике, а не в моменты открытия баров.

Рассчитываем диапазон обсчета баров:

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

При первом расчете индикатора переменной start присваивается значение 1, а не 0, потому что для определения наклона линии будет необходимо значение на предыдущем баре.

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

for(int i=start;i<rates_total && !IsStopped();i++){

}

Но, прежде чем писать код внутри этого цикла, необходимо получить данные индикатора. Для этого используется функция CopyBuffer(). Важно правильно рассчитать диапазон копируемых баров. Элементы индикаторных буферов отсчитываются слева направо, а отсчет данных при вызове функции CopyBuffer() – справа налево. Значит, начинаем копирование данных с элемента 0, а количество копируемых элементов определяется переменными rates_total и start. Обязательно проверяем результат работы функции CopyBuffer() и в случае ошибки возвращаем 0, чтобы на следующем тике индикатор рассчитался полностью заново:

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

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

if(Label1Buffer[i]>Label1Buffer[i-1]){ Label1Colors[i]=0;
}
else if(Label1Buffer[i]<Label1Buffer[i-1]){ Label1Colors[i]=1;
}
else{
Label1Colors[i]=Label1Colors[i-1];
}

Весь код функции OnCalculate:

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[])
{

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

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

for(int i=start;i<rates_total && !IsStopped();i++){ if(Label1Buffer[i]>Label1Buffer[i-1]){
Label1Colors[i]=0;
}
else if(Label1Buffer[i]<Label1Buffer[i-1]){ Label1Colors[i]=1;
}
else{
Label1Colors[i]=Label1Colors[i-1];
}
}

return(rates_total);

}

У линии толщиной 1 пиксел кому-то может быть сложно заметить момент смены цвета, потому немного увеличим толщину линии – свойству indicator_width1 установим значение 2:

#property indicator_width1 2

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

input int shift=0;

В функции OnInit(), в ее конце, перед вызовом оператора return, выполним смещение отображаемого буфера:

PlotIndexSetInteger(0,PLOT_SHIFT,shift);

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

Смена цвета на баре

Рис. 9. Смена цвета на баре 1 происходит по итогу формирования бара 2,
o котором известно только на открытии бара 3

Стрелки

Индикатор может рисовать стрелки и любые другие значки из шрифта «Wingdings». Напишем индикатор, рисующий стрелки при пересечении индикатором RSI уровней. При пересечении уровня 30 снизу вверх будет ставиться стрелка вверх, а уровня 70 сверху вниз – стрелка вниз.

Параметры создания индикатора: имя индикатора «002 RSI Signal», первый вариант функции OnCalculate(), отображение на графике цены, два буфера типа «Arrow». Название первого буфера «buy», второго – «sell». Цвет первого буфера clrBlue, второго – clrRed.

Добавляем внешние параметры, необходимые для индикатора RSI, и две переменных для уровней:

input int period=14;
input ENUM_APPLIED_PRICE price=PRICE_CLOSE; input double levelBuy=30;
input double levelSell=70;

Добавляем переменную для хэндла индикатора:

int h;

В функции OnInit() загружаем индикатор:

h=iRSI(Symbol(),Period(),period,price); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iRSI"); return(INIT_FAILED);
}

Создадим вспомогательный буфер для данных индикатора RSI. Ниже массива sellBuffer объявляем массив double с именем rsi:

double rsi[];

Свойство indicator_buffers увеличиваем на 1:

#property indicator_buffers 3

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

SetIndexBuffer(2,rsi,INDICATOR_CALCULATIONS);

В функции ОnCalculate() определяем диапазон обсчитываемых баров и копируем данные RSI:

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

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

При первом расчете индикатора переменной start присваивается значение 1, потому что при определении пересечения индикаторной линии и уровня необходимы данные с предыдущего бара.

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

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

for(int i=start;i<rates_total;i++){ buyBuffer[i]=EMPTY_VALUE; sellBuffer[i]=EMPTY_VALUE; if(rsi[i]>levelBuy && rsi[i-1]<=levelBuy){
buyBuffer[i]=low[i];

}
if(rsi[i]<levelSell && rsi[i-1]>=levelSell){ sellBuffer[i]=high[i];
}
}

Если теперь индикатор прикрепить на график, то увидим не стрелки, а небольшие кружки, стоящие на верхнем или нижнем краю тени бара. Изменим их на стрелки и немного отодвинем от баров. В функции OnInit() есть две строки с вызовом функций PlotIndexSetInteger() с идентификатором PLOT_ARROW – это установка типа значков. Для одного буфера укажем код значка 233, для второго – 234:

PlotIndexSetInteger(0,PLOT_ARROW,233); PlotIndexSetInteger(1,PLOT_ARROW,234);

Чтобы отодвинуть стрелки вызовем эти же функции с идентификатором PLOT_ARROW_SHIFT. Для буфера на покупку сдвинем стрелки вниз на 10 пикселей, для этого укажем значение 10, а для буфера на продажу – вверх, для этого укажем значение -10:

PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,10); PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,-10);

Теперь пользоваться индикатором буден несколько удобней (рис. 10).

Индикатор «002 RSI Signal»

Рис. 10. Индикатор «002 RSI Signal»

Цветные стрелки

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

Создаем файл индикатора. Имя индикатора «004 RSI Color Signal», первый вариант функции OnCalculate(), отображение на графике цены, два буфера типа «Color Arrow». Название первого буфера «buy», второго – «sell». Теперь цвета буферов изменим из Мастера MQL. При смене типа буферов на «Color Arrow» одиночные цветовые образцы изменятся на ряд образцов (рис. 11).

Щелкните на первом образце буфера «buy» и выберите цвет clrBlue, затем щелкните на втором образце и выберите цвет clrDogerBlue. Так же для буфера «sell» выберите цвета clrRed и clrTomato.

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

Свойство indicator_buffers увеличиваем на 1:

#property indicator_buffers 5

Цветовые образцы цветных буферов

Рис. 11. Цветовые образцы цветных буферов

В функции OnInit(), после последнего вызова функции SetIndexBuffer() вызываем функцию

SetIndexBuffer() для массива rsi:

SetIndexBuffer(4,rsi,INDICATOR_CALCULATIONS);

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

input double levelMiddle=50;

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

for(int i=start;i<rates_total;i++){ buyBuffer[i]=EMPTY_VALUE; sellBuffer[i]=EMPTY_VALUE; if(rsi[i]>levelBuy && rsi[i-1]<=levelBuy){
buyBuffer[i]=low[i]; buyColors[i]=0;
}
if(rsi[i]<levelSell && rsi[i-1]>=levelSell){ sellBuffer[i]=high[i];
sellColors[i]=0;
}
if(rsi[i]>levelMiddle && rsi[i-1]<=levelMiddle){ buyBuffer[i]=low[i];
buyColors[i]=1;
}
if(rsi[i]<levelMiddle && rsi[i-1]>=levelMiddle){ sellBuffer[i]=high[i];
sellColors[i]=1;
}
}

Теперь так же, как и с предыдущим индикатором, необходимо изменить стрелки и отодвинуть их. Обратите внимание как в функции OnInit() выполняется вызов функций PlotIndexSetInteger() с идентификаторами PLOT_ARROW – указаны индексы 0 и 1, хотя у буфера для стрелок на продажу индекс 3. Дело в том, что в некоторых случая все буферы индексируются вместе, а в некоторых случаях индексируются только отображаемые буферы. Собственно поэтому, при создании данного индикатора, буферы создавались через Мастер MQL, а остальной код копировался из другого индикатора, а не наоборот. При самостоятельном добавлении буферов непосредственно в код очень часто совершаются ошибки с их индексацией, поэтому их создание лучше поручить Мастеру MQL. Меняем стрелки и отодвигаем их. Код тоже можно скопировать из предыдущего индикатора:

PlotIndexSetInteger(0,PLOT_ARROW,233); PlotIndexSetInteger(1,PLOT_ARROW,234); PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,10); PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,-10);

На этом создание индикатора можно считать завершенным.

Гистограмма

Гистограмма представляет собой вертикальные столбики, идущие от уровня 0 до определенного значения, поэтому этот тип буферов используется в индикаторах, отображаемых в подокне. Наиболее известный индикатор с гистограммой, это MACD (Moving Averages Convergence/Divergence – схождение/расхождение скользящих средних). Однако у стандартного индикатора MACD невозможно менять все параметры скользящих средних. Напишем свой индикатор.

Параметры создания индикатора: имя индикатора «005 MACD», первый тип функции OnCalculate(), индикатор для отдельного окна, минимум и максимум задавать не надо, один буфер с именем «MACD», тип буфера – «Histogram», цвет – clrGray.

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

input int fastPeriod=12; input ENUM_MA_METHOD fastMethod=MODE_EMA;
input ENUM_APPLIED_PRICE fastPrice=PRICE_CLOSE; input int slowPeriod=26;
input ENUM_MA_METHOD slowMethod=MODE_EMA; input ENUM_APPLIED_PRICE slowPrice=PRICE_CLOSE;

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

int hf,hs;

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

hf=iMA(Symbol(),Period(),fastPeriod,0,fastMethod,fastPrice); if(hf==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iMA fast"); return(INIT_FAILED);
}

hs=iMA(Symbol(),Period(),slowPeriod,0,slowMethod,slowPrice); if(hs==INVALID_HANDLE){

Print("Ошибка загрузки индикатора iMA slow"); return(INIT_FAILED);
}

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

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

Для значений скользящих средних нужно два вспомогательных буфера. Объявляем два массива (чуть ниже массива MACDBuffer):

double f[],s[];

Значение свойства indicator_buffers надо увеличить на 2:

#property indicator_buffers 3

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

SetIndexBuffer(1,f,INDICATOR_CALCULATIONS); SetIndexBuffer(2,s,INDICATOR_CALCULATIONS);

В функции OnCalculate() заполняем буферы данным скользящих средних:

if(CopyBuffer(hf,0,0,rates_total-start,f)==-1 || CopyBuffer(hs,0,0,rates_total-start,s)==-1
){
return(0);
}
Индикатор «005 MACD»

Рис. 12. Индикатор «005 MACD»

Теперь основной цикл. В нем выполняется вычитание значения быстрой средней из значения медленной:

for(int i=start;i<rates_total;i++){ MACDBuffer[i]=f[i]-s[i];
}

На этом этапе индикатор можно прикреплять к графику, он будет рисовать гистограмму (рис. 12).

Стандартный индикатор MACD дополнительно отображает сигнальную линию. Сигнальная линия представляет собой скользящую среднюю, рассчитанную по данным гистограммы. Добавим сигнальную линию в создаваемый индикатор.

К сожалению, в языке MQL5 не существует встроенных средств для расчета средней по индикаторному буферу, но есть включаемый файл с функциями для выполнения различных типов усреднения (файл «Include/MovingAverages.mqh»). Подключаем его к файлу индикатора (в верхней части файла после свойств и перед внешними параметрами):

#include <MovingAverages.mqh>

В индикатор необходимо добавить еще один индикаторный буфер. Увеличиваем свойство

indicator_buffers на 1 и свойство indicator_plots на 1:

#property indicator_buffers 4
#property indicator_plots 2

Добавляем набор свойств для этого буфера:

#property indicator_label2 "Signal" #property indicator_type2 DRAW_LINE #property indicator_color2 clrRed #property indicator_style2 STYLE_SOLID #property indicator_width2 1

Добавляем массив с именем signal:

double signal[];

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

SetIndexBuffer(0,MACDBuffer,INDICATOR_DATA); SetIndexBuffer(1,Signal,INDICATOR_DATA); SetIndexBuffer(2,f,INDICATOR_CALCULATIONS); SetIndexBuffer(3,s,INDICATOR_CALCULATIONS);

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

for(int i=start;i<rates_total;i++){ MACDBuffer[i]=f[i]-s[i]; signal[i]=MACDBuffer[i];
}

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

Во внешние параметры добавим переменные для периода и типа сигнальной линии:

input int signalPeriod=9;
input ENUM_MA_METHOD signalMethod=MODE_EMA;

В функцию OnCalculate() после основного цикла добавляем соответствующий switch:

switch(signalMethod){ case MODE_SMA:

break;
case MODE_EMA:

break;
case MODE_SMMA:

break;
case MODE_LWMA:

break;
}

Для каждого варианта case выполняем вызов соответствующей функции из файла

«MovingAverages.mqh». Простое сглаживание (MODE_SMA) выполняется функцией SimpleMAOnBuffer(). Первые два параметра данной функции, это те же самые первые два параметра функции OnCalculate(). Третий параметр – начало данных, достаточно указать 0. Четвертый параметр – период сглаживания. Последние два параметра: буфер с данными и буфер для результата:

SimpleMAOnBuffer(rates_total,prev_calculated,0,signalPeriod,MACDBuffer,signal); Экспоненциальное сглаживание (MODE_EMA). Набор параметров такой же: ExponentialMAOnBuffer(rates_total,prev_calculated,0,signalPeriod,MACDBuffer,signal); Сглаженное усреднение (MODE_SMMA). Набор параметров такой же: SmoothedMAOnBuffer(rates_total,prev_calculated,0,signalPeriod,MACDBuffer,signal);Линейно-взвешенное сглаживание (MODE_LWMA). Набор параметров такой же, но добавляется еще один параметр – переменная для суммы весов. Рассчитывать значение весов не нужно, достаточно объявить переменную и передавать ее в функцию. Переменную нужно объявлять на глобальном уровне:

int ws;

Вызов функции:

LinearWeightedMAOnBuffer(rates_total,prev_calculated,0,signalPeriod,MACDBuffer,signal, ws);

Дополнительная переменная передается в функцию по ссылке, она нужна для сохранения результатов подготовительных вычислений, результат которых зависит от периода усреднения. Таким образом, если функция LinearWeightedMAOnBuffer() вызывается несколько раз с разным значением периода (4-й параметр), для каждого значения периода должна использоваться своя дополнительная переменная.

Цветная гистограмма

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

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

Делаем копию индикатора «005 MACD» с именем «006 MACD Colored». Буфер для значений гистограммы уже есть в индикаторе, нужно добавить еще один буфер для цветовых индексов. Значит, свойство indicator_buffers увеличиваем на 1:

#property indicator_buffers 5

Свойство indicator_type1 меняем на DRAW_COLOR_HISTOGRAM:

#property indicator_type1 DRAW_COLOR_HISTOGRAM

Отметим разными цветами четыре состояние гистограммы: выше нуля и выше сигнальной линии, выше нуля и ниже сигнальной линии, ниже нуля и ниже сигнальной линии, ниже нуля и выше сигнальной линии. Соответственно будут использоваться следующие цвета: clrRed, clrTomato, clrBlue, clrDodgerBlue. Меняем свойство indicator_color1:

#property indicator_color1 clrRed,clrTomato,clrBlue,clrDodgerBlue На глобальном уровне объявляем массив для буфера с цветовыми индексами: double MACDColor[];Переходим в функцию OnInit(). Функцию SetIndexBuffer() для нового буфера необходимо вызывать сразу после ее вызова для буфера MACDBuffer, а для всех последующих вызовов необходимо изменить значение индексов:

SetIndexBuffer(0,MACDBuffer,INDICATOR_DATA);

SetIndexBuffer(1,MACDColor,INDICATOR_COLOR_INDEX);

SetIndexBuffer(2,signal,INDICATOR_DATA); SetIndexBuffer(3,f,INDICATOR_CALCULATIONS); SetIndexBuffer(4,s,INDICATOR_CALCULATIONS);

Выполним проверку. В конец основного цикла добавим строку для окрашивания гистограммы в один цвет:

for(int i=start;i<rates_total;i++){ MACDBuffer[i]=f[i]-s[i]; MACDColor[i]=0;
}

После этого индикатор должен рисовать гистограмму красного цвета. Удаляем проверочную строку с присвоением индекса цвета и идем дальше.

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

for(int i=start;i<rates_total;i++){ MACDColor[i]=MACDColor[i-1]; if(MACDBuffer[i]>0){
if(MACDBuffer[i]>signal[i]){ MACDColor[i]=0;
}
else if(MACDBuffer[i]<signal[i]){ MACDColor[i]=1;
}
}
else if(MACDBuffer[i]<0){ if(MACDBuffer[i]<signal[i]){
MACDColor[i]=2;
}
else if(MACDBuffer[i]>signal[i]){ MACDColor[i]=3;
}
}
}

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

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

Гистограмма 2

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

Создаем индикатор. Имя файла «007 Long Shadow», один внешний параметр типа double c именем maxBodyPercent со значением по умолчанию 10, первый тип функции OnCalculate(), отображение на графике цены, один буфер типа «Histogram2».

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

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

start=prev_calculated-1;
}

for(int i=start;i<rates_total;i++){

}

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

Label1Buffer1[i]=EMPTY_VALUE; Label1Buffer2[i]=EMPTY_VALUE;

Сравнивает размер тела и тени, если размер тела меньше или равен заданному порогу, то рисуем гистограмму:

for(int i=start;i<rates_total;i++){ Label1Buffer1[i]=EMPTY_VALUE; Label1Buffer2[i]=EMPTY_VALUE;
if(MathAbs(open[i]-close[i])<=(high[i]-low[i])*maxBodyPercent/100){ Label1Buffer1[i]=high[i];
Label1Buffer2[i]=low[i];
}
}

На этом индикатор практически готов.

Цветная гистограмма 2

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

Делаем копию индикатора «007 Long Shadow» с именем «008 Long Shadow Colored». Свойство

indicator_buffers увеличиваем на 1:

#property indicator_buffers 3

Свойство indicator_type1 меняем на DRAW_COLOR_HISTOGRAM2:

#property indicator_type1 DRAW_COLOR_HISTOGRAM2

Добавляем еще один цвет к свойству indicator_color1: #property indicator_color1 clrAqua,clrDeepPink Объявляем глобальный массив для буфера цветовых индексов: double Label1Color[];В функции OnInit() вызываем для него функцию SetIndexBuffer():

SetIndexBuffer(2,Label1Color,INDICATOR_COLOR_INDEX);

Остается в основном цикле установить индекс цвета. Ниже весь код основного цикла:

for(int i=start;i<rates_total;i++){ Label1Buffer1[i]=EMPTY_VALUE; Label1Buffer2[i]=EMPTY_VALUE;
if(MathAbs(open[i]-close[i])<=(high[i]-low[i])*maxBodyPercent/100){ Label1Buffer1[i]=high[i];
Label1Buffer2[i]=low[i]; for(int j=i;j>=0;j--){
if(close[j]>open[j]){ Label1Color[i]=0; break;
}
else if(close[j]<open[j]){ Label1Color[i]=1; break;
}
}
}
}

На этом работа над индикатором завершается.

Секции

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

Создаем файл индикатора. Имя файла «009 ZigZag Section», внешний параметр типа int с именем «period» и значением 14, первый вариант функции OnCalculate(), отображение на графике цены, один буфер с именем «ZigZag» и типом «Section».

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

#property indicator_buffers 4

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

double trend[],lastHighBar[],lastLowBar[];

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

SetIndexBuffer(1,trend,INDICATOR_CALCULATIONS); SetIndexBuffer(2,lastHighBar,INDICATOR_CALCULATIONS); SetIndexBuffer(3,lastLowBar,INDICATOR_CALCULATIONS);

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

int start; if(prev_calculated==0){
start=0; ArrayInitialize(ZigZagBuffer,EMPTY_VALUE);

ArrayInitialize(trend,EMPTY_VALUE); ArrayInitialize(lastHighBar,EMPTY_VALUE); ArrayInitialize(lastLowBar,EMPTY_VALUE);
}
else{
start=prev_calculated-1;
}

Дело в том, что расчет зигзага основывается на использовании его ранее определенных состояний, которые хранятся во вспомогательных буферах. Терминал, как известно, иногда выполняет сброс переменой prev_calculated и от этого выполняется полный перерасчет индикатора. Чтобы выполнить перерасчет правильно, нужно удалить результаты всех старых расчетов.

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

trend[i]=trend[i-1]; lastHighBar[i]=lastHighBar[i-1]; lastLowBar[i]=lastLowBar[i-1]; ZigZagBuffer[i]=EMPTY_VALUE;

Далее, в зависимости от значения из буфера trend, выполняем различные действия. При направлении последнего отрезка зигзага вверх в этом буфере будет значение 1, при направлении вниз будет -1, но пока в нем находится значение EMPTY_VALUE. То есть этот буфер хранит три состояния, соответственно создаем switch на три варианта:

switch((int)trend[i]){ case 1:

break; case -1:

break; default:

break;
}

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

start=period-1;

Определяем начальное направление зигзага. Используя функции ArayMaximum() и ArrayMinimum() находим бары последнего максимума и минимума. Если максимум находится позже минимума, значит направление вверх и наоборот:

lastHighBar[i]=ArrayMaximum(high,i-period+1,period); lastLowBar[i]=ArrayMinimum(low,i-period+1,period);

if(lastHighBar[i]>lastLowBar[i]){ trend[i]=1;
}
else if(lastLowBar[i]>lastHighBar[i]){

trend[i]=-1;
}

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

if(high[i]>high[(int)lastHighBar[i]]){ ZigZagBuffer[(int)lastHighBar[i]]=EMPTY_VALUE; ZigZagBuffer[i]=high[i];
lastHighBar[i]=i;
}

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

else{
if(ArrayMinimum(low,i-period+1,period)==i){ trend[i]=-1;
ZigZagBuffer[i]=low[i]; lastLowBar[i]=i;
}
}

Аналогично для направления вниз:

if(low[i]<low[(int)lastLowBar[i]]){ ZigZagBuffer[(int)lastLowBar[i]]=EMPTY_VALUE; ZigZagBuffer[i]=low[i];
lastLowBar[i]=i;
}
else{
if(ArrayMaximum(high,i-period+1,period)==i){ trend[i]=1;
ZigZagBuffer[i]=high[i]; lastHighBar[i]=i;
}
}
Индикатор зигзаг из буфера-секции

Рис. 13. Индикатор зигзаг из буфера-секции

На этом создание индикатора завершено. Как в итоге он должен выглядеть, показано на рис. 13.

Цветные секции

Модифицируем предыдущий индикатор, заменим простые секции цветными. Отрезки вверх будут открашены в синий цвет, отрезки вниз – в красный.

Делаем копию файла «009 ZigZag Section» с именем «010 ZigZag Section Colored». Увеличиваем на 1 значение свойства indicator_buffers:

#property indicator_buffers 5

Свойство indicator_type1 меняем на DRAW_COLOR_SECTION:

#property indicator_type1 DRAW_COLOR_SECTION

Добавляем еще один цвет в свойство indicator_color1:

#property indicator_color1 clrBlue,clrRed

Добавляем глобальный массив для буфера цветовых индексов:

double ZigZagColor[];

В функции OnInit() вызываем для него функцию SetIndexBuffer() и меняем индексы для остальных буферов:

SetIndexBuffer(0,ZigZagBuffer,INDICATOR_DATA);

SetIndexBuffer(1,ZigZagColor,INDICATOR_COLOR_INDEX);

SetIndexBuffer(2,trend,INDICATOR_CALCULATIONS); SetIndexBuffer(3,lastHighBar,INDICATOR_CALCULATIONS); SetIndexBuffer(4,lastLowBar,INDICATOR_CALCULATIONS);

В начале функции OnStart() при определении диапазона обсчитываемых баров добавляем очиcтку нового буфера (там же, где выполняется очистка других буферов):

ArrayInitialize(ZigZagColor,EMPTY_VALUE);

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

if(ZigZagBuffer[i]!=EMPTY_VALUE){

if(trend[i]==1){ ZigZagColor[i]=0;
}
else{
ZigZagColor[i]=1;
}

}

Вот и все.

Зигзаг

Если внимательно изучить линии зигзага, отображаемую индикатором «009 ZigZag Section», можно заметить неоднозначную ситуацию – максимальная цена бара является максимальной ценой за установленный период, при этом его минимальная цена является минимальной ценой за этот период. Такая ситуация показана на рис. 14, бар помеченный цифрой 1 является одновременно и максимальным и минимальным баром в диапазоне из четырнадцати баров (конец диапазона отмечен цифрой 14), а через максимум и минимум бара проведены пунктирные линии.

Однако линия зигзаг сначала (когда бар 1 является последним) будет доходить до его минимума, а потом (при появлении следующих баров) будет продолжена до нового минимума. А линии зигзага следовало бы вертикально подняться до максимума бара 1, а потом опуститься до минимума следующего бара. Кто-то считает это важным моментом, кто-то несущественным, но главное то, то терминал MetaTRader5 позволят рисовать зигзаги с вертикальными отрезками, идущими ровно вдоль бара. Создадим такой зигзаг.

Создаем файл индикатора. Имя файла «011 ZigZag», внешний параметр типа int с именем «period» и значением 14, первый вариант функции OnCalculate(), отображение на графике цены, один буфер с именем «ZigZag» и типом «ZigZag».

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

double ZigZagBuffer1[];
double ZigZagBuffer2[];

Однако свойство indicator_plots имеет значение 1. Получается, что два буфера считаются за один, подобная ситуация была с гистограммой типа 2. Здесь первый буфер используется для рисования верхних точек, а нижний – для нижних точек. Будет ли линия, проходящая вдоль бара вертикально, проходить сверху вниз или снизу вверх зависит от того, где находилась предшествующая точка.

Бар 1 является одновременно и максимальным и минимальным баром

Рис. 14. Бар 1 является одновременно и максимальным и минимальным баром

Увеличим значение свойства indicator_buffers на 3:

#property indicator_buffers 4

Объявим три массива (как уже делали с зигзагом из секций):

double trend[],lastHighBar[],lastLowBar[];

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

SetIndexBuffer(2,trend,INDICATOR_CALCULATIONS); SetIndexBuffer(3,lastHighBar,INDICATOR_CALCULATIONS); SetIndexBuffer(4,lastLowBar,INDICATOR_CALCULATIONS);

Только теперь индексы буферов увеличены на 1.

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

int start; if(prev_calculated==0){
start=period-1; ArrayInitialize(ZigZagBuffer1,EMPTY_VALUE); ArrayInitialize(ZigZagBuffer2,EMPTY_VALUE); ArrayInitialize(trend,EMPTY_VALUE); ArrayInitialize(lastHighBar,EMPTY_VALUE); ArrayInitialize(lastLowBar,EMPTY_VALUE);
}
else{
start=prev_calculated-1;
}

Основной цикл:

for(int i=start;i<rates_total;i++){

}

В начале основного цикла перемещаем данные по буферам trend, lastHighBar, lastLowBar и очищаем отображаемые буферы (теперь их два):

trend[i]=trend[i-1]; lastHighBar[i]=lastHighBar[i-1]; lastLowBar[i]=lastLowBar[i-1]; ZigZagBuffer1[i]=EMPTY_VALUE; ZigZagBuffer2[i]=EMPTY_VALUE;

Вообще, код очень похож на код зигзага «009 ZigZag Section», отличие в том, что проверка условий на необходимость продления отрезка и проверка условий на необходимость смены направления не должны быть взаимоисключающими. Второе отличие в том, что значения вершин зигзага устанавливаются через буфер ZigZagBuffer1, а нижние точки через буфер ZigZagBuffer2. Сначала выполняем поиск начальных точек зигзага и проверку на продление отрезков:

switch((int)trend[i]){ case 1:
if(high[i]>high[(int)lastHighBar[i]]){ ZigZagBuffer1[(int)lastHighBar[i]]=EMPTY_VALUE; ZigZagBuffer1[i]=high[i];
lastHighBar[i]=i;
}
break; case -1:
if(low[i]<low[(int)lastLowBar[i]]){ ZigZagBuffer2[(int)lastLowBar[i]]=EMPTY_VALUE; ZigZagBuffer2[i]=low[i];
lastLowBar[i]=i;
}
break; default:

lastHighBar[i]=ArrayMaximum(high,i-period+1,period); lastLowBar[i]=ArrayMinimum(low,i-period+1,period); if(lastHighBar[i]>lastLowBar[i]){
trend[i]=1;
}
else if(lastLowBar[i]>lastHighBar[i]){ trend[i]=-1;
}
break;
}

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

switch((int)trend[i]){ case 1:
if(ArrayMinimum(low,i-period+1,period)==i){ trend[i]=-1;
ZigZagBuffer2[i]=low[i]; lastLowBar[i]=i;
}
break; case -1:
if(ArrayMaximum(high,i-period+1,period)==i){ trend[i]=1;
ZigZagBuffer1[i]=high[i]; lastHighBar[i]=i;
}
break;
}

Теперь индикатор может рисовать вертикальные отрезки (рис. 15).

Индикатор с использованием буфера типа ZigZag

Рис. 15. Индикатор с использованием буфера типа ZigZag

Цветной зигзаг

Модифицируем предыдущий индикатор, заменим простой зигзаг цветным. Делаем копию файла «011 ZigZag» с именем «012 ZigZag Colored». Увеличиваем на 1 значение свойства indicator_buffers:

#property indicator_buffers 6

Свойство indicator_type1 меняем на DRAW_COLOR_ZIGZAG:

#property indicator_type1 DRAW_COLOR_ZIGZAG

Добавляем еще два цвета в свойство indicator_color1:

#property indicator_color1 clrBlue,clrRed,clrGreen

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

Добавляем глобальный массив для буфера цветовых индексов:

double ZigZagColor[];

В функции OnInit() вызываем для него функцию SetIndexBuffer() и меняем индексы для остальных буферов:

  • SetIndexBuffer(0,ZigZagBuffer1,INDICATOR_DATA);
  • SetIndexBuffer(1,ZigZagBuffer2,INDICATOR_DATA);
  • SetIndexBuffer(2,ZigZagColor,INDICATOR_COLOR_INDEX);
  •  SetIndexBuffer(3,trend,INDICATOR_CALCULATIONS);
  • SetIndexBuffer(4,lastHighBar,INDICATOR_CALCULATIONS);
  • SetIndexBuffer(5,lastLowBar,INDICATOR_CALCULATIONS);

В начале функции OnStart() при определении диапазона обсчитываемых баров добавляем очиcтку нового буфера (там же, где выполняется очистка других буферов):

ArrayInitialize(ZigZagColor,EMPTY_VALUE);

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

if(ZigZagBuffer2[i]!=EMPTY_VALUE && ZigZagBuffer1[i]!=EMPTY_VALUE){ ZigZagColor[i]=2;
}
else if(ZigZagBuffer2[i]!=EMPTY_VALUE){ ZigZagColor[i]=1;
}
else if(ZigZagBuffer1[i]!=EMPTY_VALUE){ ZigZagColor[i]=0;
}
зигзаг

Рис. 16. У цветного зигзага при изменении цвета вертикального отрезка, вместе с ним меняется цвет предыдущего отрезка (эти отрезки дополнительно отмечены пунктиром)

На этом работа над индикатором завершена. Особенности раскрашивания цветочного зигзага показаны на рис. 16.

Заливка

Рисование индикатора заливкой позволят заполнять разными цветами пространство между двумя уровнями. Всего для заливки может использоваться только два цвета. Если первый уровень выше второго, пространство заливается одним цветом, если наоборот, то другим. Создадим индикатор на основе полос Боллинджера. Пространство между границами канала будем окрашивать в зависимости от того, за какой уровень выходила цена. Если цены выходила за пределы верхнего уровня, будет использоваться голубая заливка до тех пор, пока цена не выйдет за пределы нижней границы, после этого пространство между линиями будет заливаться розовым цветом.

Создаем файл индикатора. Имя файла «013 Filling», первый вариант функции OnCalculate(), отображение на графике цены, один буфер типа «Filling» с именем «filling» и двумя цветами (clrLightSkyBlue и clrPink), три буфера типа «Line» с именами «upper», «lower», «middle» с цветом clrDimGray.

После открытия файла добавим в него внешние параметры:

input int period = 20;
input double width = 2;
input ENUM_APPLIED_PRICE price = PRICE_CLOSE;

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

#property indicator_buffers 6

Объявить массив:

double state[];

В функции OnInit() Применить к нему функцию SetIndexBuffer():

SetIndexBuffer(5,state,INDICATOR_CALCULATIONS);

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

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

if(CopyBuffer(h,UPPER_BAND,0,rates_total-start,upperBuffer)==-1 || CopyBuffer(h,LOWER_BAND,0,rates_total-start,lowerBuffer)==-1 || CopyBuffer(h,BASE_LINE,0,rates_total-start,middleBuffer)==-1
){

return(0);
}

for(int i=start;i<rates_total;i++){

}

Далее пишем код внутри основного цикла. В первую очередь перемещаем последнее известное состояние по буферу state:

state[i]=state[i-1];

Проверяем, не изменилось ли состояние. Если цена находится выше верхней границы Боллинджера, присваиваем буферу state состояние 1, если ниже нижней, то присваиваем -1:

if(close[i]>upperBuffer[i]){ state[i]=1;
}
if(close[i]<lowerBuffer[i]){ state[i]=-1;
}

Теперь, в зависимости от состояния, заполняем значениями буферы заливки:

if(state[i]==1){ fillingBuffer1[i]=upperBuffer[i]; fillingBuffer2[i]=lowerBuffer[i];
}
else if(state[i]==-1){ fillingBuffer1[i]=lowerBuffer[i]; fillingBuffer2[i]=upperBuffer[i];
}

На этом создание индикатора закончено.

Индикатор с таймером

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

Создаем файл индикатора. Имя файла «014 Timer», обработчик событий – любой, включить обработчик событий OnTimer(), индикатор на графике цены, один буферы «Line». Буфер использоваться не будет, но дело в том, что если буфер не добавить, компиляция будет происходить с предупреждением, к тому же индикатор все равно выделяет память под один буфер, так что сэкономить оперативную память не получится.

Существует два типа таймеров – секундный и миллисекундный. Оба этих таймера вызывают одну и ту же функцию OnTimer(). Может использоваться только один таймер. Чтобы начать пользоваться таймером его надо запустить. Запуск секундного таймера выполняется функцией EventSetTimer(), в функцию передается один параметр – интервал времени в секундах. Запуск миллисекундного таймера выполняется функцией EventSetMillisecondTimer(). В функцию передается один параметр – интервал времени в миллисекундах. Остановка таймера выполняется

функцией EventKillTimer(). Разработчики терминала рекомендуют останавливать таймер при завершении работы программы (эксперта или индикатора). При завершении работы программы происходит событие деинициализации и вызывается функция OnDeinit(). Мастер MQL не добавляет эту функцию в индикаторы, поэтому придется добавить ее самостоятельно. Функция возвращает тип void и имеет один параметр reason типа const double:

void OnDeinit(const int reason){
}

По значению переменной reason в функции OnDeinit() можно узнать причину деинициализации. Это может быть завершение работы программы (индикатора или эксперта), завершение работы терминала, смена символа, таймфрейма, изменение параметров и пр. Более подробно этот вопрос можно изучить по справочному руководству к языку MQL5 – установите курсор на имя функции и нажмите клавишу F1.

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

EventSetMillisecondTimer(100);

В функции OnDeinit() останавливаем его:

EventKillTimer();

В функции OnTimer() получаем время бара:

datetime tm[1]; if(CopyTime(Symbol(),Period(),0,1,tm)==-1){
return;
}

Подготавливаем имя графического объекта:

string name=MQLInfoString(MQL_PROGRAM_NAME)+"_timer";

Обратите внимание, имя начинается с имени файла индикатора. В функции OnDeinit() выполняем удаление всех объектов по префиксу и перерисовку графика:

ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw();

Продолжаем в функции OnTimer(). Проверяем существование графического объекта, если его нет, то создаем и устанавливаем ему все необходимые свойства:

if(ObjectFind(0,name)==-1){ ObjectCreate(0,name,OBJ_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER); ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_LOWER); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,5); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,5);
ObjectSetInteger(0,name,OBJPROP_COLOR,ChartGetInteger(0,CHART_COLOR_FOREGROUND));
}

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

datetime t=tm[0]+PeriodSeconds()-TimeCurrent();

Оставив только часы и минуты (получив остаток от деления на 86400), преобразуем время в строку:

string txt=TimeToString(t%86400,TIME_SECONDS);

Проверим, не входят ли в интервал целые сутки, если входят, добавляем их количество перед временем:

t/=86400;
if(t>0){
txt=(string)(long)t+" "+txt;
}

Выводим результат через графический объект и перерисовываем график:

ObjectSetString(0,name,OBJPROP_TEXT,txt); ChartRedraw();

Полученный индикатор отображает результаты свой работы в правом нижнем углу графика (рис. 17).

Индикатор «014 Timer»

Рис. 17. Индикатор «014 Timer» в правом нижнем углу

Бары и свечи

Индикатор может рисовать точно такие же бары и свечи, как на обычном графике в терминале. Для этого используются буферы типа «Bars» (бары) и «Candles» (свечи). Напишем универсальный индикатор с возможностью выбора типа отображения (бары или свечи). Отображать индикатор будет данные другого символа в подокне.

Создаем файл индикатора. Имя файла «015 Symbol», один внешний параметр типа string с именем «symbol», первый тип функции OnCalculate(), индикатор для отдельного окна, один буфер типа «Candles» с именем «bar».

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

double barBuffer1[];
double barBuffer2[];
double barBuffer3[];
double barBuffer4[];

Эти буферы соответствуют ценам open, high, low, close.

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

#property indicator_color1 clrGray,clrBlue,clrRed

Первый цвет – это цвет контура, второй цвет – свечи вверх, третий – свечи вниз. Инициализируем внешний параметр пустой строкой:

input string symbol="";

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

string sym;

В функции OnInit() присваиваем ей значение переменной symbol и обрезаем пробелы:

sym=symbol;

StringTrimLeft(sym);
StringTrimRight(sym);

Если переменная пустая, присваиваем ей значение символа графика:

if(sym==""){ sym=Symbol();
}

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

if(!SymbolSelect(sym,true)){ Alert("Неизвестный символ ",sym);
}

Если символ уже есть в окне обзора рынка, функция вернет true.

if(!SymbolSelect(sym,true)){ Alert("Неизвестный символ ",sym); return(INIT_FAILED);
}

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

int bars=Bars(sym,Period()); if(bars==0){
Comment("Формирование таймфрейма ",sym); return(0);
}

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

int start; if(prev_calculated==0){
Comment(""); datetime tm[1];
if(CopyTime(sym,Period(),bars-1,1,tm)==-1){ return(0);
}
start=0; for(;start<rates_total;start++){
if(time[start]>=tm[0]){ break;
}
}
}
else{
start=prev_calculated-1;
}

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

void OnDeinit(const int r){ Comment("");
}

Продолжаем в функции OnCalculate(). В основном цикле выполняем копирование данных бара по времени и присваиваем значения индикаторным буферам:

MqlRates r[1];

for(int i=start;i<rates_total;i++){ if(CopyRates(sym,Period(),time[i],1,r)==-1){
return(0);
}
barBuffer1[i]=r[0].open; barBuffer2[i]=r[0].high; barBuffer3[i]=r[0].low; barBuffer4[i]=r[0].close;
}

На этом индикатор можно считать частично готовым – он может отображать данные в виде свечей. Добавим индикатору возможность рисовать бары. Для этого добавим внешний параметр типа bool с именем drawbar, значение по умолчанию – false:

input bool drawBars=false;

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

if(drawBars){ PlotIndexSetInteger(0,PLOT_DRAW_TYPE,DRAW_BARS);
}

else{
PlotIndexSetInteger(0,PLOT_DRAW_TYPE,DRAW_CANDLES);
}

Теперь можно выбирать тип рисования: свечи или бары. При рисовании баров они так же окрашиваются в разные цвета, бары вверх – синие, бары вниз – красные, а если цена открытия равна цене закрытия – серые.

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

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

EventSetTimer(1);

А в той части кода, где выполняет определения диапазона обсчета, когда значение переменной

prev_calculated равно нулю, остановим его:

EventKillTimer();

Кроме того, и в функции OnDeinit() выполним остановку таймера.

Добавим функцию OnTimer() и из нее вызовем функцию ChartSetSymbolPeriod(). Установим текущему графику тот же самый символ и таймфрейм, который уже установлен у графика:

void OnTimer(){ ChartSetSymbolPeriod(0,Symbol(),Period());
}

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

На этом рассмотрение типов индикаторных буферов закончено.

Индикатор с другого таймфрейма

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

  1. Убедиться в готовности данных другого таймфрейма.
  2. Убедиться в том, расчет индикатора на другом таймфрейме закончен.
  3. Если другой таймфрейм является более страшим, необходимо обсчитывать не один последний бар, как обычно делается в индикаторах, а все бары, входящие в бар старшего таймфрейма. Если другой таймфрейм является младшим, необходимо вычислять время, что бы получить данные с последнего бара, входящего в обсчитываемый бар.

Создадим индикатор RSI, рассчитываемый по данным другого таймфрейма. Имя файла «016 RSI TF», индикатор в отдельном окне с фиксированным минимумом (0) и максимумом (100), один буфер типа «Line» с именем «RSI».

В файл добавляем внешние параметры:

input ENUM_TIMEFRAMES timeFrame = PERIOD_CURRENT;
input int period = 14;
input int price = PRICE_CLOSE;
input double level = 30;

Добавляем переменную для хэндла, вспомогательную переменную для таймфрейма:

int h;
ENUM_TIMEFRAMES tf;

А также переменную для коррекции времени последнего бара другого таймфрема:

int addTime;

В функции OnInit() присваиваем переменной tf значение переменной timeframe, потом проверяем его, если оно равно PERIOD_CURRENT, то используем значение, возвращаемое функцией Period():

tf=timeFrame; if(tf==PERIOD_CURRENT){
tf=Period();
}

Выполняем загрузку индикатора и проверку хэндла:

h=iRSI(Symbol(),tf,period,price); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iRSI"); return(INIT_FAILED);
}

Вычисляем значение для переменной addTime. Если другой таймфрейм является младшим, то присваиваем переменой длительность бара рабочего таймфрейма, но уменьшенную на длительность бара другого таймфрейма:

addTime=0; if(PeriodSeconds(tf)<PeriodSeconds()){
addTime=PeriodSeconds()-PeriodSeconds(tf);
}

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

int bars=Bars(Symbol(),tf); if(bars==0){
Comment("Формирование таймфрейма ",timeFrameName(tf),"..."); EventSetTimer(1);
return(0);
}

Функция timeFrameName() возвращает стоку с именем таймфрема – «H1», «H4» и т.п. Код функции:

string timeFrameName(ENUM_TIMEFRAMES a){ string str=EnumToString(a);

int p=StringFind(str,"_"); return(StringSubstr(str,p+1));
}

Функция EnumToString() возвращает строковое представление тафмфрема, например «PERIOD_H1», из него отрезается конечная часть – «H1».

Добавляем функцию OnTimer():

void OnTimer(){ ChartSetSymbolPeriod(0,Symbol(),Period());
}

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

if(BarsCalculated(h)==-1){ Comment("Расчет индикатора..."); EventSetTimer(1);
return(0);
}

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

int start; if(prev_calculated==0){
EventKillTimer(); Comment(""); datetime tm[1];
bars=MathMin(bars,TerminalInfoInteger(TERMINAL_MAXBARS)); if(CopyTime(Symbol(),tf,bars-1,1,tm)==-1){
Comment("Ошибка определения начального времени..."); return(0);
}
start=0; for(;start<rates_total;start++){
if(time[start]>=tm[0]){ break;
}
}
}

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

else{
if(PeriodSeconds(tf)>PeriodSeconds()){ start=prev_calculated-PeriodSeconds(tf)/PeriodSeconds();
}
else{
start=prev_calculated-1;

}
}

В основном цикле копируем данные другого индикатора по времени и присваиваем их индикаторному буферу:

for(int i=start;i<rates_total;i++){ if(CopyBuffer(h,0,time[i]+addTime,1,ind)==-1){
Comment("Ошибка копирования даных..."); return(0);
}
RSIBuffer[i]=ind[0];
}

На этом создание индикатор завершено.

Индикатор с уведомлением

С одной из функций уведомления – с функций Alert() вы уже знакомы, кроме того, из программы, написанной на MQL5, можно проигрывать звуковые файлы, отправлять сообщения по электронной почте и отправлять пуш-уведомления на мобильный терминал.

Для проигрывания звуковых файлов используется функция PlaySound(). В функцию передается один параметр – имя звукового файла, расширение файла можно указывать, можно не указывать. Функция может проигрывать только wav-файлы без сжатия. Располагаться файлы должны в папке «Sound», расположенной в папке терминала, также файлы могут располагаться в подпапках папки «Sound».

Для отправки сообщения по электронной почте используется функция SendMail(). В функцию передается два параметра: текст темы сообщения и текст сообщения. Для работы функции необходима предварительная настройка терминала (Главное меню – Сервис – Настройки – Почта). Настройка выполняется так же, как настройка любой почтовой программы: указывается адрес сервера (после сервера через двоеточие иногда указывается номер порта), логин, пароль. В поля «От» и «Кому» указывается адрес электронной почты. Чтобы узнать адрес сервера и номер порта, необходимо поискать инструкцию по настройке почтового клиента на сайте почтового сервера.

Для отправки пуш-уведомления используется функция SendNotification(). В функцию передается один параметр – тест сообщения. Для работы функции необходима предварительная настройка терминала (Главное меню – Сервис – Настройки – Уведомления).

Выполним доработку индикатора «003 RSI Signal», индикатор будет выполнять уведомление при появлении новой стрелки. Сделаем копию индикатора с именем «017 RSI Signal Alert» и в ней будем выполнять следующую доработку. Потребуется несколько внешних параметров: alert – для включения уведомления окном сообщения (функцией Alert()), sound – звуком (функций PlaySound()), sfile – имя звукового файла, email – для отправки сообщения по электронной почте и push – для пуш-уведомления. Также добавим параметр nbar для выбора индекса бара, на котором надо следить за появлением стрелки – на формирующемся (0) баре или на первом сформированном (1). Добавляем внешние параметры:

input bool alert = false;
input bool sound = false;
input string sfile = "";
input bool email = false;

input bool push = false;
input bool nbar = 1;

Имя звукового файла по умолчанию не указано, в этом случае будет воспроизводиться стандартный звук из файла «alert.wav».

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

void Notification ( int rates_total,
const datetime & time[],
double & aBuy[],double & aSell[]){
//…
}

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

static datetime lt=0; string mes="";

Далее проверяем, включено ли вообще какое-нибудь уведомление (код располагается в функции

Notification()):

if(alert || sound || email || push){
//…
}

Если включено выполняется следующий код. Проверяем значение переменой lt, если оно равно нулю, присваиваем переменной время последнего бара:

if(lt==0){
lt=time[rates_total-1];
}

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

if(time[rates_total-1]!=lt){
//…
}

Это сделано для ограничения количества уведомлений на один бар. То есть на одном баре уведомление выполняется только один раз. Далее проверяем, есть ли стрелка:

if(aBuy[rates_total-1-nbar]!=EMPTY_VALUE || aSell[rates_total-1-nbar]!=EMPTY_VALUE
){
//…
}

Если стрелка есть, то переменной lt присваиваем время последнего бара, чтобы на этом баре уведомление больше не выполнялось:

lt=time[rates_total-1];

Формируем текст сообщения:

mes=MQLInfoString(MQL_PROGRAM_NAME)+"("+Symbol()+","+timeFrameName(Period())+"): "; mes=mes+(aBuy[rates_total-1-nbar]!=EMPTY_VALUE?"BUY":"SELL");

Сообщение включает в себя имя файла индикатора индикатора, символ, таймфрейм и направление сигнала «BUY» или «SELL». Для вывода имени таймфрейма используется созданная ранее функция timeFrameName().

Наконец, вызывается та или иная функция, выполняющая уведомление:

if(alert){
Alert(mes);
}
if(sound){
PlaySound(sfile);
Print(mes);
}
if(email){
SendMail(mes,mes);
}
if(push){
SendNotification(mes);
}

Обратите внимание, вызов функции Sound() выполняется после вызова функции Alert(). Дело в том, что функция Alert() воспроизводит свой звук, и он будет заменен звуком функции PlaySound(). Если бы сначала вызывалась функции PlaySound(), то ее звук был бы прерван функцией Alert(), его не удалось бы услышать. Конечно, это касается случая, когда включено оба уведомления. Также заметьте, после вызова функции PlaySound() выполняется вывод текстового сообщения функцией Print(). Это сделано на случай, когда выключено уведомление функцией Alert(). Услышав звук, можно заглянуть во вкладку «Эксперты» и узнать, какой индикатор его воспроизвел.

Индикатор с управлением

При изучении исторических данных, с целью поиска на них закономерностей, иногда бывает нужно посмотреть, как на этих данных ведут себя индикаторы с различными параметрами. В этом случае, каждый раз открывать окно свойств, менять параметр и закрывать окно свойств – неудобно и долго. Доработаем индикатор «002 MA Colored», сделаем так, чтобы с клавиатуры можно было менять период скользящей средней. Хорошо бы использовать клавиш «+» и «-», но они заняты, ими меняется масштаб графика, поэтому используем расположенные ниже клавиши со скобками «{» и «}».

Сохраним копию файла «002 MA Colored» с именем «018 MA Colored Controlled». Переменной period, поскольку она является внешней, невозможно присваивать значения, поэтому для периода добавим глобальную переменную с именем «_period»:

int _period;

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

_period=period; Comment("period=",_period);

Добавим в файл функцию OnChartEvent(). Чтобы не набирать код функции вручную, можно создать новый индикатор с выбором обработчика событий OnChartEvent(), скопировать из него функцию, а файл удалить.

void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam)
{
//…
}

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

if(id==CHARTEVENT_KEYDOWN){
Alert(lparam);
}

После этого индикатор надо прикрепить на график. При нажатии на клавиши «{» и «}» открывается окно с сообщениями «219» и «221», соответственно. При коде 221 увеличиваем значение переменной _period, при коде 219 – уменьшаем, а также выгружаем старый индикатор, загружаем новый и посылаем тик вызовом функции ChartSetSymbolPeriod(). Кроме того, выводим комментарий с величиной периода:

if(id==CHARTEVENT_KEYDOWN){
if(lparam==219 || lparam==221){ if(lparam==221){
_period++;
}
else if(lparam==219){ if(_period==1){
return;
}
_period--;
}
Comment("period=",_period); IndicatorRelease(h);
h=iMA(Symbol(),Period(),_period,0,method,price); ChartSetSymbolPeriod(0,Symbol(),Period());
}
}

Остается добавить функцию OnDeinit() для очистки комментария при завершении работы индикатора:

void OnDeinit(const int reason){ Comment("");
}

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

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

Проверка на перерисовку

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

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

В функции OnCalculate(), надо объявить переменную типа int, например, с именем pc, присвоить ей значение переменой prev_calculated, а далее, везде, где используется переменная prev_calculated, заменить ее на pc. Затем необходимо обеспечит возможность обнуления переменой pc. Для этого объявляем глобальную переменную типа bool, например, с именем resolve. В функции OnChartEvent() при любом событии присваиваем ей значение true. В функции OnCalculate(), если переменная resolve равна true, обнуляем переменную pc и присваиваем переменной resolve значение false. В результате происходит полный пересчет индикатора. Заодно выводим в комментарий какое-нибудь сообщение, чтобы быть уверенным, что пересчет произошел. Ниже приведена примерная схема индикатора с такой доработкой:

bool resolve=false;

void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam)
{
resolve=true;
}

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[])
{
int pc;

if(resolve){ pc=0; resolve=false;
Comment(GetTickCount());
}
else{
pc=prev_calculated;
}

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

for(int i=start;i<rates_total;i++){
//...
}
return(rates_total);
}

Это код расположен в файле с именем «019 Resolve Test».

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

Фракталы

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

Создаем файл индикатора. Имя файла «020 FractalLevels», первый вариант функции OnCalculate(), индикатор на графике цены, четыре индикаторных буфера: синяя линия с именем «upper», красная линия с именем «lower», синяя стрелка вверх и красная стрелка вниз.

Добавляем в файл глобальную переменную для хэндла индикатора:

int h;

В функции OnInit() загружаем индикатор:

h=iFractals(Symbol(),Period()); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iFractals");

return(INIT_FAILED);
}

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

PlotIndexSetInteger(2,PLOT_ARROW,233); PlotIndexSetInteger(3,PLOT_ARROW,234); PlotIndexSetInteger(2,PLOT_ARROW_SHIFT,10); PlotIndexSetInteger(3,PLOT_ARROW_SHIFT,-10);

Еще надо добавить два вспомогательных буфера для данных индикатора фракталов. Для этого увеличиваем значение свойства indicator_buffers на 2:

#property indicator_buffers 6

Объявляем два глобальных массива:

double upperFractal[];
double lowerFractal[];

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

SetIndexBuffer(4,upperFractal,INDICATOR_CALCULATIONS); SetIndexBuffer(5,lowerFractal,INDICATOR_CALCULATIONS);

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

int start; int cnt;

Переменная start – для начала основного индикаторного цикла, cnt – для количества копируемых баров индикатора фракталов. Поскольку проверять существование фрактала надо будет на баре со сдвигом 2, начинать индикаторный цикл надо с таким же отступом – с бара 2. Так же и копирование данных индикатора фракталов надо начинать с бара с индексом 2, поэтому количество копируемых баров будет на два меньше, чем общее количества баров:

if(prev_calculated==0){ start=2; cnt=rates_total-2;
}

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

start=prev_calculated-1; cnt=rates_total-start;

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

if(CopyBuffer(h,UPPER_LINE,2,cnt,upperFractal)==-1 || CopyBuffer(h,LOWER_LINE,2,cnt,lowerFractal)==-1
){
return(0);
}

Наконец, основной индикаторный цикл:

for(int i=start;i<rates_total;i++){
//…
}

Внутри цикла продолжаем рисовать уровни:

upperBuffer[i]=upperBuffer[i-1]; lowerBuffer[i]=lowerBuffer[i-1];

Удаляем стрелки, которые могли остаться от предыдущего обсчета этого же бара:

buyBuffer[i]=EMPTY_VALUE; sellBuffer[i]=EMPTY_VALUE;

Проверяем наличие верхнего фрактала. Если он есть, меняем значение верхнего уровня:

if(upperFractal[i-2]!=EMPTY_VALUE){ upperBuffer[i]=upperFractal[i-2];
}

Так же с нижним уровнем:

if(lowerFractal[i-2]!=EMPTY_VALUE){ lowerBuffer[i]=lowerFractal[i-2];
}

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

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

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

Для скрытия уровней достаточно изменить цвет соответствующих буферов на clrNONE:

#property indicator_color1 clrNONE

#property indicator_color1 clrNONE

Индикатор прорыва фрактальных уровней

Рис. 18. Индикатор прорыва фрактальных уровней

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

Увеличиваем indicator_buffers на 1:

#property indicator_buffers 7

Добавляем массив для буфера:

double last[];

В функции OnInit() применяем к нему функцию SetIndexBuffer():

SetIndexBuffer(6,last,INDICATOR_CALCULATIONS);

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

last[i]=last[i-1];

При постановке стрелки буферу last присваиваем значение 1 (для стрелки вверх) или -1 (для стрелки вниз), а перед постановкой стрелки проверяем, чтобы значение не было равно 1 (для стрелки вверх) или -1 (для стрелки вниз). Ниже показан весь код основного индикаторного цикла:

for(int i=start;i<rates_total;i++){ last[i]=last[i-1];

upperBuffer[i]=upperBuffer[i-1]; lowerBuffer[i]=lowerBuffer[i-1];

buyBuffer[i]=EMPTY_VALUE; sellBuffer[i]=EMPTY_VALUE;

if(upperFractal[i-2]!=EMPTY_VALUE){ upperBuffer[i]=upperFractal[i-2];
}

if(lowerFractal[i-2]!=EMPTY_VALUE){ lowerBuffer[i]=lowerFractal[i-2];
}

if(upperBuffer[i]!=EMPTY_VALUE){ if(close[i]>upperBuffer[i] && last[i]!=1){
buyBuffer[i]=low[i]; upperBuffer[i]=EMPTY_VALUE; last[i]=1;
}
}

if(lowerBuffer[i]!=EMPTY_VALUE){ if(close[i]<lowerBuffer[i] && last[i]!=-1){
sellBuffer[i]=high[i]; lowerBuffer[i]=EMPTY_VALUE; last[i]=-1;
}
}

Теперь гораздо проще выполнить визуальную оценку эффективности индикатора (рис. 19).

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

Итоговый вид индикатора прорыва фрактальных уровней

Рис. 19. Итоговый вид индикатора прорыва фрактальных уровней

По-настоящему оценить эффективность индикатора можно только в тестере стратегий, протестировав торгового эксперта работающего на его основе.