Гибкое создание индикаторов с помощью IndicatorCreate

После знакомства с новым способом создания индикаторов обратимся к задаче, более приближенной к реальности. IndicatorCreate обычно применяют в тех случаях, когда вызываемый индикатор заранее не известен. Такая необходимость, например, возникает при написании универсальных экспертов, способных торговать по произвольным сигналам, настраиваемым пользователем. И даже названия индикаторов могут задаваться пользователем.

Мы пока не готовы к разработке экспертов, а потому изучим эту технологию на примере индикатора-обертки UseDemoAll.mq5, способного отобразить данные любого другого индикатора.

С точки зрения пользователя это должно выглядеть так. После наложения UseDemoAll на график, в диалоге свойств появляется список для выбора одного из встроенных индикаторов или пользовательского, причем в последнем случае дополнительно нужно будет указать его имя в поле ввода. В другом строковом параметре можно ввести список параметров, через запятую. Типы параметров будут определяться автоматически, исходя из их написания. Например, число с десятичной точкой (10.0) будет трактоваться как double, число без точки (15) — как целое, а нечто заключенное в кавычки ("текст") — как строка.

Это далеко не все, но основные настройки UseDemoAll. С остальными мы будем разбираться по мере возникновения конструктивных проблем.

За основу решения возьмем перечисление ENUM_INDICATOR: в нем уже есть элементы для всех типов индикаторов, включая пользовательские (IND_CUSTOM). Правда в чистом виде оно не подходит по нескольким причинам. Во-первых, из него невозможно получить метаданные о конкретном индикаторе, такие как количество и типы аргументов, количество буферов и в какое окно индикатор выводится (в главное или в собственное). А эта информация важна для правильного создания и визуализации индикатора. Во-вторых, если мы опишем входную переменную типа ENUM_INDICATOR, чтобы пользователь мог выбирать интересующий его индикатор, в диалоге свойств это будет представлено выпадающим списком, где варианты содержат лишь имя элемента. А было бы желательно обеспечить в этом списке подсказки для пользователя (как минимум, про параметры). Поэтому опишем свое собственное перечисление IndicatorType. Напомним, что MQL5 позволяет для каждого элемента указать справа комментарий, который показывается в интерфейсе.

В каждом элементе перечисления IndicatorType будем кодировать особым образом не только соответствующий идентификатор (ID) из ENUM_INDICATOR, но и количество параметров (P), количество буферов (B) и номер рабочего окна (W). Для этих целей разработаны следующие макросы.

#define MAKE_IND(P,B,W,ID) (int)((W << 24) | ((B & 0xFF) << 16) | ((P & 0xFF) << 8) | (ID & 0xFF))
#define IND_PARAMS(X)   ((X >> 8) & 0xFF)
#define IND_BUFFERS(X)  ((X >> 16) & 0xFF)
#define IND_WINDOW(X)   ((uchar)(X >> 24))
#define IND_ID(X)       ((ENUM_INDICATOR)(X & 0xFF))

Макрос MAKE_IND принимает в качестве параметров все вышеозвученные характеристики и упаковывает их в разные байты одного 4-х байтового целого числа, формируя таким образом уникальный код для элемента нового перечисления. Остальные 4 макроса позволяют выполнять обратную операцию — по коду вычислить все характеристики индикатора.

Само перечисление IndicatorType не станем приводить здесь полностью, а покажем лишь фрагмент. С полным исходным кодом можно ознакомиться в файле AutoIndicator.mqh.

enum IndicatorType
{
   iCustom_ = MAKE_IND(000IND_CUSTOM), // {iCustom}(...)[?]
   
   iAC_ = MAKE_IND(011IND_AC), // iAC( )[1]*
   iAD_volume = MAKE_IND(111IND_AD), // iAD(volume)[1]*
   iADX_period = MAKE_IND(131IND_ADX), // iADX(period)[3]*
   iADXWilder_period = MAKE_IND(131IND_ADXW), // iADXWilder(period)[3]*
   ...
   iMomentum_period_price = MAKE_IND(211IND_MOMENTUM), // iMomentum(period,price)[1]*
   iMFI_period_volume = MAKE_IND(211IND_MFI), // iMFI(period,volume)[1]*
   iMA_period_shift_method_price = MAKE_IND(410IND_MA), // iMA(period,shift,method,price)[1]
   iMACD_fast_slow_signal_price = MAKE_IND(421IND_MACD), // iMACD(fast,slow,signal,price)[2]*
   ...
   iTEMA_period_shift_price = MAKE_IND(310IND_TEMA), // iTEMA(period,shift,price)[1]
   iVolumes_volume = MAKE_IND(111IND_VOLUMES), // iVolumes(volume)[1]*
   iWPR_period = MAKE_IND(111IND_WPR// iWPR(period)[1]*
};

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

Список параметров особенно важен, так как пользователь должен будет ввести соответствующие значения, разделенные запятыми, в зарезервированную для этого входную переменную. Мы могли бы "подсказать" и типы параметров, но для простоты решено оставить только имена со смыслом, из которого можно заключить и тип. Например, period, fast, slow — это целые с периодом (количеством баров), method — метод усреднения ENUM_MA_METHOD, price — тип цены ENUM_APPLIED_PRICE, volume — тип объема ENUM_APPLIED_VOLUME.

Для удобства пользователя (чтобы не вспоминать значения элементов перечислений) в программе будут поддержаны названия всех перечислений. В частности, идентификатор sma обозначит MODE_SMA, ema — MODE_EMA и так далее. Цена close превратится в PRICE_CLOSE, open — в PRICE_OPEN, и прочие типы цен — аналогично, по последнему слову (после подчеркивания) в идентификаторе элемента перечисления. Например, для списка параметров индикатора iMA (iMA_period_shift_method_price) можно написать такую строку: 11,0,sma,close. Идентификаторы не надо брать в кавычки. Однако ничто не мешает, при необходимости, передать строку с таким же текстом, например, список — 1.5,"close" — содержит вещественное число 1.5 и строку "close".

Тип индикатора, а также строки со списком параметров и, опционально, именем (если индикатор пользовательский) — это основные данные для конструктора класса AutoIndicator.

class AutoIndicator
{
protected:
   IndicatorType type;      // выбранный тип индикатора
   string symbol;           // рабочий символ (необязательный)
   ENUM_TIMEFRAMES tf;      // рабочий таймфрейм (необязательный)
   MqlParamBuilder builder// "строитель" массива параметров
   int handle;              // дескриптор индикатора
   string name;             // имя пользовательского индикатора
   ...
public:
   AutoIndicator(const IndicatorType tconst string customconst string parameters,
      const string s = NULLconst ENUM_TIMEFRAMES p = 0):
      type(t), name(custom), symbol(s), tf(p), handle(INVALID_HANDLE)
   {
      PrintFormat("Initializing %s(%s) %s, %s",
         (type == iCustom_ ? name : EnumToString(type)), parameters,
         (symbol == NULL ? _Symbol : symbol), EnumToString(tf == 0 ? _Period : tf));
      // расщепляем строку на массив параметров (формируется внутри builder)
      parseParameters(parameters);
      // создаем и запоминаем дескриптор
      handle = create();
   }
   
   int getHandle() const
   {
      return handle;
   }
};

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

Процесс анализа строки с параметрами поручается методу parseParameters. В нем реализована описанная выше схема с распознаванием типов значений и их передача в объект MqlParamBuilder, с которым мы познакомились в предыдущем примере.

   int parseParameters(const string &list)
   {
      string sparams[];
      const int n = StringSplit(list, ',', sparams);
      
      for(int i = 0i < ni++)
      {
         // нормализация строки (убираем пробелы приводим к нижнему регистру)
         StringTrimLeft(sparams[i]);
         StringTrimRight(sparams[i]);
         StringToLower(sparams[i]);
   
         if(StringGetCharacter(sparams[i], 0) == '"'
         && StringGetCharacter(sparams[i], StringLen(sparams[i]) - 1) == '"')
         {
            // все, что внутри кавычек, берется как string
            builder << StringSubstr(sparams[i], 1StringLen(sparams[i]) - 2);
         }
         else
         {
            string part[];
            int p = StringSplit(sparams[i], '.', part);
            if(p == 2// double/float
            {
               builder << StringToDouble(sparams[i]);
            }
            else if(p == 3// datetime
            {
               builder << StringToTime(sparams[i]);
            }
            else if(sparams[i] == "true")
            {
               builder << true;
            }
            else if(sparams[i] == "false")
            {
               builder << false;
            }
            else // int
            {
               int x = lookUpLiterals(sparams[i]);
               if(x == -1)
               {
                  x = (int)StringToInteger(sparams[i]);
               }
               builder << x;
            }
         }
      }
      
      return n;
   }

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

   int lookUpLiterals(const string &s)
   {
      if(s == "sma"return MODE_SMA;
      else if(s == "ema"return MODE_EMA;
      else if(s == "smma"return MODE_SMMA;
      else if(s == "lwma"return MODE_LWMA;
      
      else if(s == "close"return PRICE_CLOSE;
      else if(s == "open"return PRICE_OPEN;
      else if(s == "high"return PRICE_HIGH;
      else if(s == "low"return PRICE_LOW;
      else if(s == "median"return PRICE_MEDIAN;
      else if(s == "typical"return PRICE_TYPICAL;
      else if(s == "weighted"return PRICE_WEIGHTED;
   
      else if(s == "lowhigh"return STO_LOWHIGH;
      else if(s == "closeclose"return STO_CLOSECLOSE;
   
      else if(s == "tick"return VOLUME_TICK;
      else if(s == "real"return VOLUME_REAL;
      
      return -1;
   }

После того как параметры распознаны и сохранены во внутреннем массиве объекта MqlParamBuilder, вызывается метод create. Его задача, скопировать параметры в локальный массив, дополнить его именем пользовательского индикатора (в случае такового), и вызвать функцию IndicatorCreate.

   int create()
   {
      MqlParam p[];
      // заполняем массив 'p' параметрами, собранными объектом 'builder'
      builder >> p;
      
      if(type == iCustom_)
      {
         // вставляем название пользовательского индикатора в самое начало
         ArraySetAsSeries(ptrue);
         const int n = ArraySize(p);
         ArrayResize(pn + 1);
         p[n].type = TYPE_STRING;
         p[n].string_value = name;
         ArraySetAsSeries(pfalse);
      }
      
      return IndicatorCreate(symboltfIND_ID(type), ArraySize(p), p);
   }

Метод возвращает полученный дескриптор.

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

Дополнительно класс AutoIndicator умеет формировать сокращенное имя встроенного индикатора из названия элемента перечисления.

   ...
   string getName() const
   {
      if(type != iCustom_)
      {
         const string s = EnumToString(type);
         const int p = StringFind(s"_");
         if(p > 0return StringSubstr(s0p);
         return s;
      }
      return name;
   }
};

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

Прежде всего, определимся с количеством индикаторных буферов. Поскольку максимальное количество буферов среди встроенных индикаторов равно пяти (у Ichimoku), примем его как ограничитель. Регистрацию этого количества массивов в качестве буферов поручим уже известному нам классу BufferArray (см. раздел Мультивалютные и мультитаймфреймовые индикаторы, пример IndUnityPercent).

#define BUF_NUM 5
   
#property indicator_chart_window
#property indicator_buffers BUF_NUM
#property indicator_plots   BUF_NUM
   
#include <MQL5Book/IndBufArray.mqh>
 
BufferArray buffers(5);

Важно напомнить, что индикатор может быть спроектирован либо для отображения в главном окне, либо в собственном. Совместить два режима MQL5 не позволяет. Однако нам заранее не известно, какой индикатор выберет пользователь, и потому требуется изобрести некий "обходной маневр". Пока разместим свой индикатор в главном окне, а с проблемой отдельного окна разберемся позже.

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

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

input IndicatorType IndicatorSelector = iMA_period_shift_method_price// Built-in Indicator Selector
input string IndicatorCustom = ""// Custom Indicator Name
input string IndicatorParameters = "11,0,sma,close"// Indicator Parameters (comma,separated,list)
input ENUM_DRAW_TYPE DrawType = DRAW_LINE// Drawing Type
input int DrawLineWidth = 1// Drawing Line Width

Для хранения дескриптора индикатора опишем глобальную переменную.

int Handle;

В обработчике OnInit задействуем представленный ранее класс AutoIndicator для парсинга входных данных, подготовки массива MqlParam и получения на его основе дескритора.

#include <MQL5Book/AutoIndicator.mqh>
   
int OnInit()
{
   AutoIndicator indicator(IndicatorSelectorIndicatorCustomIndicatorParameters);
   Handle = indicator.getHandle();
   if(Handle == INVALID_HANDLE)
   {
      Alert(StringFormat("Can't create indicator: %s",
         _LastError ? E2S(_LastError) : "The name or number of parameters is incorrect"));
      return INIT_FAILED;
   }
   ...

Для настройки диаграмм опишем набор цветов и получим краткое имя индикатора из объекта AutoIndicator. Также вычислим количество используемых буферов n встроенного индикатора с помощью макроса IND_BUFFERS, а для любого пользовательского индикатора (который неизвестен заранее), за неимением лучшего решения, включим все буфера. Далее в процессе копирования данных лишние вызовы функции CopyBuffer просто вернут ошибку, и такие массивы можно будет заполнить пустыми значениями.

   ...
   static color defColors[BUF_NUM] = {clrBlueclrGreenclrRedclrCyanclrMagenta};
   const string s = indicator.getName();
   const int n = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   ...

В цикле настроим свойства диаграмм, с учетом ограничителя n: буфера сверх него скрываются.

   for(int i = 0i < BUF_NUM; ++i)
   {
      PlotIndexSetString(iPLOT_LABELs + "[" + (string)i + "]");
      PlotIndexSetInteger(iPLOT_DRAW_TYPEi < n ? DrawType : DRAW_NONE);
      PlotIndexSetInteger(iPLOT_LINE_WIDTHDrawLineWidth);
      PlotIndexSetInteger(iPLOT_LINE_COLORdefColors[i]);
      PlotIndexSetInteger(iPLOT_SHOW_DATAi < n);
   }
   
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "("IndicatorParameters")");
   
   return INIT_SUCCEEDED;
}

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

В обработчике OnCalculate, по готовности данных у дескриптора, читаем их в свои массивы.

int OnCalculate(ON_CALCULATE_STD_SHORT_PARAM_LIST)
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int m = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   for(int k = 0k < m; ++k)
   {
      // заполняем свои буферы данными из индикатора с дескриптором 'Handle'
      const int n = buffers[k].copy(Handle,
         k0rates_total - prev_calculated + 1);
         
      // в случае проблем чистим буфер
      if(n < 0)
      {
         buffers[k].empty(EMPTY_VALUEprev_calculatedrates_total - prev_calculated);
      }
   }
   
   return rates_total;
}

Вышеописанная реализация является упрощенной и соответствует исходному файлу UseDemoAllSimple.mq5. Её расширением мы займемся далее, а пока проверим поведение текущей версии. На следующем изображении показаны 2 копии индикатора: синяя линия — с настройками по умолчанию (iMA_period_shift_method_price, параметры "11,0,sma,close"), а красная — iRSI_period_price с параметрами "11,close".

Два экземпляра индикатора UseDemoAllSimple с показаниями iMA и iRSI

Два экземпляра индикатора UseDemoAllSimple с показаниями iMA и iRSI

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

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