Расширенный способ создания индикаторов: IndicatorCreate

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

Например, при оптимизации эксперта в тестере имеет смысл подбирать не только период скользящего среднего, но и алгоритм его расчета. Разумеется, если строить алгоритм на единственном индикаторе iMA, можно предусмотреть задание в настройках его методов ENUM_MA_METHOD. Но кто-то, вероятно, хотел бы расширить выбор за счет переключения между двойной экспоненциальной, тройной экспоненциальной и фрактальной скользящей средней. На первый взгляд, для этого можно было бы использовать switch с вызовом, соответственно, iDEMA, iTEMA и iFrAMA. Однако, как быть с включением в этот список пользовательских индикаторов?

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

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

int IndicatorCreate(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR indicator, int count = 0, const MqlParam &parameters[] = NULL)

Функция создает экземпляр индикатора для указанной пары символа и таймфрейма. Тип индикатора задается с помощью параметра indicator. Его тип — перечисление ENUM_INDICATOR (см. далее) содержит идентификаторы для всех встроенных индикаторов, а также вариант для iCustom. Количество параметров индикатора и их описания передаются, соответственно, в аргументе count и массиве структур MqlParam (см. ниже).

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

Нарушение этого правила может привести к ошибке на стадии выполнения программы (см. пример далее) и невозможности создать дескриптор. А в худшем случае переданные параметры будут интерпретированы неправильно, и индикатор поведет себя не так, как ожидалось, однако из-за отсутствия ошибок заметить это нелегко. Исключением является передача пустого массива или не передача его вовсе (поскольку аргументы count и parameters являются опциональными): в этом случае индикатор будет создан с настройками по умолчанию. Также для пользовательских индикаторов можно опускать произвольное количество параметров с конца списка.

Структура MqlParam специально разработана для передачи входных параметров при создании индикатора с помощью IndicatorCreate или для получения информации о параметрах стороннего индикатора (выполняющегося на графике) с помощью IndicatorParameters.

struct MqlParam 

   ENUM_DATATYPE type;          // тип входного параметра 
   long          integer_value// поле для хранения целочисленного значения 
   double        double_value;  // поле для хранения значения double или float 
   string        string_value;  // поле для хранения значения строкового типа 
};

Фактическое значение параметра должно быть задано в одном из полей integer_value, double_value, string_value, в соответствии со значением первого поля type. В свою очередь для описания поля type используется перечисление ENUM_DATATYPE, содержащее идентификаторы для всех встроенных типов MQL5.

Идентификатор

Тип данных

TYPE_BOOL

bool

TYPE_CHAR

char

TYPE_UCHAR

uchar

TYPE_SHORT

short

TYPE_USHORT

ushort

TYPE_COLOR

color

TYPE_INT

int

TYPE_UINT

uint

TYPE_DATETIME

datetime

TYPE_LONG

long

TYPE_ULONG

ulong

TYPE_FLOAT

float

TYPE_DOUBLE

double

TYPE_STRING

string

Если какой-либо параметр индикатора имеет тип перечисления, для его описания в поле type необходимо применять значение TYPE_INT.

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

Идентификатор

Индикатор

IND_AC

Accelerator Oscillator

IND_AD

Accumulation/Distribution

IND_ADX

Average Directional Index

IND_ADXW

ADX by Welles Wilder

IND_ALLIGATOR

Alligator

IND_AMA

Adaptive Moving Average

IND_AO

Awesome Oscillator

IND_ATR

Average True Range

IND_BANDS

Bollinger Bands®

IND_BEARS

Bears Power

IND_BULLS

Bulls Power

IND_BWMFI

Market Facilitation Index

IND_CCI

Commodity Channel Index

IND_CHAIKIN

Chaikin Oscillator

IND_CUSTOM

Custom indicator

IND_DEMA

Double Exponential Moving Average

IND_DEMARKER

DeMarker

IND_ENVELOPES

Envelopes

IND_FORCE

Force Index

IND_FRACTALS

Fractals

IND_FRAMA

Fractal Adaptive Moving Average

IND_GATOR

Gator Oscillator

IND_ICHIMOKU

Ichimoku Kinko Hyo

IND_MA

Moving Average

IND_MACD

MACD

IND_MFI

Money Flow Index

IND_MOMENTUM

Momentum

IND_OBV

On Balance Volume

IND_OSMA

OsMA

IND_RSI

Relative Strength Index

IND_RVI

Relative Vigor Index

IND_SAR

Parabolic SAR

IND_STDDEV

Standard Deviation

IND_STOCHASTIC

Stochastic Oscillator

IND_TEMA

Triple Exponential Moving Average

IND_TRIX

Triple Exponential Moving Averages Oscillator

IND_VIDYA

Variable Index Dynamic Average

IND_VOLUMES

Volumes

IND_WPR

Williams Percent Range

Важно отметить, что если в качестве типа индикатора передается значение IND_CUSTOM, то первый элемент массива параметров должен иметь поле type со значением TYPE_STRING, а поле string_value должно содержать имя (путь) пользовательского индикатора.

В случае успеха функция IndicatorCreate возвращает дескриптор созданного индикатора, а в случае неудачи — INVALID_HANDLE. Код ошибки будет находиться в _LastError.

Напомним, что для тестирования MQL-программ, создающих пользовательские индикаторы, имена которых не известны на стадии компиляции (что имеет место и при использовании IndicatorCreate), необходимо явным образом обеспечивать их привязку с помощью директивы:

#property tester_indicator "indicator_name.ex5"

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

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

Первый пример — UseEnvelopesParams1.mq5 — создает встроенную копию индикатора Envelopes. Для этого описываем два буфера и две диаграммы, массивы под них и входные параметры, повторяющие параметры iEnvelopes.

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
   
// drawing settings
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "Upper"
#property indicator_style1  STYLE_DOT
   
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_width2  1
#property indicator_label2  "Lower"
#property indicator_style2  STYLE_DOT
   
input int WorkPeriod = 14;
input int Shift = 0;
input ENUM_MA_METHOD Method = MODE_EMA;
input ENUM_APPLIED_PRICE Price = PRICE_TYPICAL;
input double Deviation = 0.1// Deviation, %
   
double UpBuffer[];
double DownBuffer[];
   
int Handle// дескриптор подчиненного индикатора

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

int OnInit()
{
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   
   Handle = iEnvelopes(WorkPeriodShiftMethodPriceDeviation);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

Привязка буферов останется прежней, но для создания дескриптора мы сейчас пойдем другим путем. Опишем массив MqlParam, заполним его и вызовем функцию IndicatorCreate.

int OnInit()
{
   ...
   MqlParam params[5] = {};
   params[0].type = TYPE_INT;
   params[0].integer_value = WorkPeriod;
   params[1].type = TYPE_INT;
   params[1].integer_value = Shift;
   params[2].type = TYPE_INT;
   params[2].integer_value = Method;
   params[3].type = TYPE_INT;
   params[3].integer_value = Price;
   params[4].type = TYPE_DOUBLE;
   params[4].double_value = Deviation;
   Handle = IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int n = CopyBuffer(Handle, 0, 0, rates_total - prev_calculated + 1, UpBuffer);
   const int m = CopyBuffer(Handle, 1, 0, rates_total - prev_calculated + 1, DownBuffer);
      
   return n > -1 && m > -1 ? rates_total : 0;
}

Проверим, как созданный индикатор UseEnvelopesParams1 выглядит на графике.

Индикатор UseEnvelopesParams1

Индикатор UseEnvelopesParams1

Выше был представлен стандартный, но не очень изящный способ заполнения свойств. Поскольку вызов IndicatorCreate может потребоваться во многих проектах, имеет смысл упростить процедуру для вызывающего кода. Для этой цели разработаем класс MqlParamBuilder (см. файл MqlParamBuilder.mqh). Его задачей будет принимать с помощью некоторых методов значения параметров, определять их тип и добавлять в массив соответствующие элементы — корректно заполненные структуры.

К сожалению, MQL5 не поддерживает в полной мере концепцию, которая есть во многих других языках программирования и называется "информацией о типах времени исполнения" (Run-Time Type Information, RTTI). С помощью неё программы могут запрашивать у среды исполнения описательные мета-данные о своих составных частях — переменных, структурах, классах, функциях и т.д. К немногочисленным встроенным возможностям MQL5, которые можно отнести к разряду RTTI, являются операторы typename и offsetof. Поскольку typename возвращает название типа в виде строки, построим свой автодетектор типов на строках (см. файл RTTI.mqh).

template<typename T>
ENUM_DATATYPE rtti(T v = (T)NULL)
{
   static string types[] =
   {
      "null",     //               (0)
      "bool",     // 0 TYPE_BOOL=1 (1)
      "char",     // 1 TYPE_CHAR=2 (2)
      "uchar",    // 2 TYPE_UCHAR=3 (3)
      "short",    // 3 TYPE_SHORT=4 (4)
      "ushort",   // 4 TYPE_USHORT=5 (5)
      "color",    // 5 TYPE_COLOR=6 (6)
      "int",      // 6 TYPE_INT=7 (7)
      "uint",     // 7 TYPE_UINT=8 (8)
      "datetime"// 8 TYPE_DATETIME=9 (9)
      "long",     // 9 TYPE_LONG=10 (A)
      "ulong",    // 10 TYPE_ULONG=11 (B)
      "float",    // 11 TYPE_FLOAT=12 (C)
      "double",   // 12 TYPE_DOUBLE=13 (D)
      "string",   // 13 TYPE_STRING=14 (E)
   };
   const string t = typename(T);
   for(int i = 0i < ArraySize(types); ++i)
   {
      if(types[i] == t)
      {
         return (ENUM_DATATYPE)i;
      }
   }
   return (ENUM_DATATYPE)0;
}

Шаблонная функция rtti получает с помощью typename строку с именем типа-параметра шаблона и сравнивает её с элементами массива, содержащего все встроенные типы из перечисления ENUM_DATATYPE. Порядок перечисления имен в массиве соответствует значению элемента перечисления, поэтому при обнаружении совпадающей строки достаточно привести индекс к типу (ENUM_DATATYPE) и вернуть вызывающему коду. Например, вызов rtti(1.0) или rtti<double>() даст значение TYPE_DOUBLE.

Имея этот инструмент, мы можем вернуться к разработке MqlParamBuilder. Опишем в классе свой массив структур MqlParam и переменную n, которая будет содержать индекс последнего, заполняемого элемента.

class MqlParamBuilder
{
protected:
   MqlParam array[];
   int n;
   ...

Публичный метод для добавления очередного значения в список параметров сделаем шаблонным. Более того, реализуем его в виде перегрузки оператора '<<', который возвращает указатель на сам объект "строителя". Это позволит записывать в массив несколько значений одной строкой, например, так: builder << WorkPeriod << PriceType << SmoothingMode.

Именно в этом методе мы увеличиваем размер массива, получаем рабочий индекс n для заполнения и сразу же обнуляем эту n-ую структуру.

...
public:
   template<typename T>
   MqlParamBuilder *operator<<(T v)
   {
      // расширяем массив
      n = ArraySize(array);
      ArrayResize(arrayn + 1);
      ZeroMemory(array[n]);
      ...
      return &this;
   }

Там, где стоит многоточие, последует основная рабочая часть, то есть заполнение полей структуры. Можно было бы предположить, что тип параметра мы напрямую определим с помощью самодельного rtti. Но следует обратить внимание на один нюанс. Если мы напишем инструкцию array[n].type = rtti(v), она неправильно сработает для перечислений. Каждое перечисление является самостоятельным типом со своим названием, несмотря на то, что хранится по образцу целых чисел. Для перечислений функция rtti вернет 0, в связи с чем, нужно явным образом заменить его на TYPE_INT.

      ...
      // определяем тип значения
      array[n].type = rtti(v);
      if(array[n].type == 0array[n].type = TYPE_INT// подразумеваем enum
      ...

Осталось положить само значение v в одно из трех полей структуры: integer_value типа long (обратите внимание, long — это длинное целое, отсюда и название поля), double_value типа double или string_value типа string. При этом количество встроенных типов гораздо больше, поэтому подразумевается, что все целочисленные типы (включая int, short, char, color, datetime, перечисления) должны попадать в поле integer_value, значения float — в поле double_value, и лишь для поля string_value существует однозначное толкование: это всегда string.

Для выполнения данной задачи реализуем несколько перегруженных методов assign: три с конкретными типами float, double, string, и один шаблонный для всего остального.

class MqlParamBuilder
{
protected:
   ...
   void assign(const float v)
   {
      array[n].double_value = v;
   }
   
   void assign(const double v)
   {
      array[n].double_value = v;
   }
   
   void assign(const string v)
   {
      array[n].string_value = v;
   }
   
   // здесь обрабатываем int, enum, color, datetime и пр. совместимое с long
   template<typename T>
   void assign(const T v)
   {
      array[n].integer_value = v;
   }
   ...

На этом процесс заполнения структур заканчивается, и остается вопрос передачи сформированного массива в вызывающий код. Это действие поручено публичному методу с перегрузкой оператора '>>', который имеет единственный аргумент: ссылку на приемный массив MqlParam.

   // экспортируем внутренний массив наружу
   void operator>>(MqlParam &params[])
   {
      ArraySwap(arrayparams);
   }

Теперь все готово, чтобы заняться исходным кодом модифицированного индикатора UseEnvelopesParams2.mq5. Изменения по сравнению с первой версией коснуться только заполнения массива MqlParam в обработчике OnInit. В нем мы описываем объект "строителя", отсылаем в него через '<<' все параметры и возвращаем через '>>' готовый массив. Все в одной строке.

int OnInit()
{
   ...
   MqlParam params[];
   MqlParamBuilder builder;
   builder << WorkPeriod << Shift << Method << Price << Deviation >> params;
   ArrayPrint(params);
   /*
       [type] [integer_value] [double_value] [string_value]
   [0]      7              14        0.00000 null            <- "INT" period
   [1]      7               0        0.00000 null            <- "INT" shift
   [2]      7               1        0.00000 null            <- "INT" EMA
   [3]      7               6        0.00000 null            <- "INT" TYPICAL
   [4]     13               0        0.10000 null            <- "DOUBLE" deviation
   */

Для контроля выводим массив в журнал (выше показан результат для значений по умолчанию).

Если массив заполнен не полностью, вызов IndicatorCreate завершится ошибкой. Например, если передать только 3 параметра из 5 требуемых Envelopes, получим ошибку 4002 и недействительный дескриптор.

   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES3params));
   // Пример ошибки:
   // indicator Envelopes cannot load [4002]   
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,3,params)=
      -1 / WRONG_INTERNAL_PARAMETER(4002)

Однако более длинный массив, чем в спецификации индикатора, не считается ошибкой: лишние значения просто не принимаются во внимание.

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

Но мы, разумеется, оставим правильный вариант вызова IndicatorCreate и получим рабочий индикатор, также как и в первой версии.

   ...
   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params));
   // успех:
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,ArraySize(params),params)=10 / ok
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

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