Главное событие индикаторов: OnCalculate

Функция OnCalculate является основной точкой входа в MQL5 код индикатора: она вызывается при наступлении одноименного события, которое генерируется при изменений ценовых данных. Например, оно может случиться при поступлении нового тика по символу или изменении старых цен (заполнение пропуска в истории или её докачка с сервера).

Существует два варианта функции, отличающиеся тем, какие данные выступают исходным материалом для расчетов:

  • полная — предоставляет в параметрах набор стандартных ценовых таймсерий (цены OHLC, объемы, спреды);
  • сокращенная — для одной произвольной таймсерии (не обязательно стандартной);

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

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

Выбор исходной таймсерии для индикатора с сокращенной формой OnCalculate

Выбор исходной таймсерии для индикатора с сокращенной формой OnCalculate

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

Запрещено применять индикаторы к нескольким встроенным индикаторам: Fractals, Gator, Ichimoku и Parabolic SAR.

Краткая форма OnCalculate имеет следующий прототип.

int OnCalculate(const int rates_total, const int prev_calculated, const int begin,
  const double &data[])

Массив data содержит исходные данные для расчета. Это может быть одна из ценовых таймсерий либо рассчитанный буфер другого индикатора. Параметр rates_total указывает размер массива data. В принципе, вызовы ArraySize(data) или iBars(NULL, 0) должны дать то же самое значение, что и rates_total.

Параметр prev_calculated предназначен для эффективного пересчета индикатора на небольшом количестве новых баров (обычно на одном, последнем), вместо полного расчета на всех барах. Значение prev_calculated равно результату функции OnCalculate, возвращенному среде исполнения из предыдущего вызова функции. Например, если при поступлении очередного тика индикатор посчитал заложенную в него формулу для всех баров, он должен вернуть из OnCalculate значение rates_totalA (здесь индекс A означает начальный момент времени). Тогда на следующем тике, при получении события OnCalculate терминал установит prev_calculated в прежнее значение rates_totalA. Однако количество баров за это время может уже измениться и новое значение rates_total увеличится — обозначим его rates_totalB. Таким образом, пересчету будут подлежать только бары от prev_calculated (он же rates_totalA) до rates_totalB.

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

При первом вызове функции OnCalculate значение prev_calculated равно 0.

Если с момента последнего вызова функции OnCalculate ценовые данные были изменены (например, подкачана более глубокая история или в ней заполнены пропуски), то значение параметра prev_calculated будет также установлено в 0 самим терминалом. Таким образом, индикатору будет дан сигнал на полный пересчет по всей доступной истории.

Если функция OnCalculate возвращает нулевое значение, индикатор не отрисовывается, а названия и значения его буферов в Окне данных будут скрыты.

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

Направление индексации массива data может быть выбрано с помощью вызова ArraySetAsSeries (по умолчанию оно равно false, в чем можно убедиться, вызвав ArrayGetAsSeries). В то же время, если применить к массиву функцию ArrayIsSeries, она вернет значение true. Это означает, что данный массив является внутренним массивом, управляемым терминалом. Индикатор не может его как-либо менять, а только читать, тем более что в описании параметра присутствует модификатор const.

Параметр begin сообщает количество начальных значений массива data, которые следует исключить из расчета. Параметр устанавливается системой, когда наш индикатор настроен пользователем таким образом, что получает data из другого индикатора (см. изображение выше). Например, если выбранный индикатор-источник данных выполняет расчет скользящего среднего периода N, то на первых N - 1 барах исходных данных по определению не существует, поскольку там невозможно провести усреднение по N барам. Если в этом индикаторе-источнике его разработчик установил специальное свойство, оно корректно передастся нам в параметре begin. Мы скоро проверим этот аспект на практике (см. раздел Пропуск отрисовки на начальных барах).

Попробуем создать пустой индикатор с сокращенной формой OnCalculate. Он пока ничего не будет уметь, но послужит заготовкой для дальнейших экспериментов. Исходный файл IndStub.mq5 можно найти в папке MQL5/Indicators/MQL5Book/p5/. Чтобы убедиться в работе индикатора добавим в OnCalculate вывод значений prev_calculated и rates_total в журнал, а также подсчет количества вызовов функции.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   static int count = 0;
   ++count;
   // сравним количество баров на предыдущем вызове и текущем
   if(prev_calculated != rates_total)
   {
      // сигнализируем только при различии
      PrintFormat("calculated=%d rates=%d; %d ticks",
         prev_calculatedrates_totalcount);
   }
   return rates_total// вернем количество обработанных баров
}

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

Остальные параметры пока остались не у дел, но мы постепенно задействуем все возможности.

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

no indicator window property is defined, indicator_chart_window is applied
no indicator plot defined for indicator

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

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

calculated=0 rates=10002; 1 ticks
calculated=10002 rates=10003; 30 ticks
calculated=10003 rates=10004; 90 ticks
calculated=10004 rates=10005; 167 ticks
calculated=10005 rates=10006; 240 ticks

Таким образом, мы видим, что обработчик OnCalculate вызывается, как ожидалось, и в нем можно выполнять расчеты, как на каждом тике, так и по барам. Напомним, что убрать индикатор с графика можно, вызвав диалог Список индикаторов из контекстного меню графика и нажав там кнопку Удалить, предварительно выделив требуемый индикатор в списке.

Сейчас вернемся ненадолго к той особенности функции 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[])

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

Массивы open, high, low и close содержат, соответственно, цены открытия, максимальную, минимальную и закрытия для баров текущего графика — таймсерии рабочей пары символа и таймфрейма. Массив time содержит время открытия каждого бара, а tick_volume и volume — торговые объемы за бар: тиковый и биржевой.

В предыдущей главе мы изучили Временные ряды со стандартными типами цен и объемов, предоставляемые терминалом для MQL-программ через набор функций. Так вот для удобства расчета индикатора эти временные ряды передаются в обработчик OnCalculate напрямую по ссылке в виде массивов. Это избавляет от необходимости вызывать эти функции и копировать (дублировать) котировки во внутренние массивы. Разумеется, такой прием подходит только для тех индикаторов, которые рассчитываются на одном сочетании рабочего символа и таймфрейма, совпадающих с текущим графиком. Однако MQL5 позволяет создавать мультивалютные, мультитаймфреймовые индикаторы, а также индикаторы для "чужих" по отношению к графику символов и таймфреймов. Во всех таких случаях уже на обойтись без функций доступа к таймсериям. Чуть позже мы посмотрим, как это делается.

Если проверить для всех переданных массивов атрибут принадлежности терминалу с помощью ArrayIsSeries, эта функция вернет true. Все массивы доступны только на чтение. Это подчеркивает и модификатор const в описании параметров.

Выбор между полной и сокращенной формами делается исходя из потребностей расчетного алгоритма в данных. Например, для сглаживания массива с помощью алгоритма скользящей средней требуется только один входной массив, и потому индикатор может быть построен по любому типу цены, на выбор пользователя. Однако известные индикаторы ParabolicSAR или ZigZag требуют цен High и Low, а потому должны использовать полную версию OnCalculate. В следующих разделах мы увидим примеры индикаторов как для простой версии OnCalculate, так и для полной.