Подключение пользовательских индикаторов как ресурсов

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

Директива #resource с описанием вложенного индикатора имеет следующий формат:

#resource "путь_имя_индикатора.ex5"

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

Мы уже использовали данную возможность в большом примера эксперта, в финальной версии UnityMartingale.mq5.

#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5"

Далее этот ресурс передавался в функцию iCustom вместо имени индикатора: "::Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5".

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

Как мы знаем, для использования ресурса из MQL-программы его необходимо указывать в виде: "путь_имя_файла.ex5::имя_ресурса". Например, если индикатор EmbeddedIndicator.ex5 включается в качестве ресурса в другой индикатор MainIndicator.mq5 (а точнее, в его двоичный образ MainIndicator.ex5), то имя, указываемое при вызове самого себя через iCustom, уже не может быть кратким, без пути, а путь должен включать расположение "родительского" индикатора внутри папки MQL5. В противном случае система не сможет найти вложенный индикатор.

Действительно, в обычных обстоятельствах индикатор может вызвать самого себя, например, с помощью оператора iCustom(_Symbol, _Period, myself,...), где myself — это строка, равная либо MQLInfoString(MQL_PROGRAM_NAME), либо названию, которое было предварительно назначено в коде свойству INDICATOR_SHORTNAME. Но когда индикатор находится внутри другой MQL-программы как ресурс, имя уже не ссылается на соответствующий файл — ведь файл, который послужил прообразом для ресурса, остался на том компьютере, где производилась компиляция, а на компьютере пользователя есть только файл MainIndicator.ex5. Здесь потребуется некоторый анализ программного окружения при запуске программы.

Рассмотрим это на практике.

Создадим для начала индикатор NonEmbeddedIndicator.mq5. Важно отметить, что он расположен в папке MQL5/Indicators/MQL5Book/p7/SubFolder/, то есть в подпапке SubFolder относительно папки p7, выделенной для всех индикаторов данной Части книги. Это сделано намеренно, чтобы эмулировать ситуацию, когда откомпилированный файл отсутствует на компьютере пользователя. Как это работает (а точнее — демонстрирует проблему), мы сейчас увидим.

Индикатор имеет единственный входной параметр Reference, назначение которого — подсчет количества копий самого себя: при первом создании в параметре будет 0, и индикатор создаст свою копию со значением параметра 1. Вторая копия, "увидев" значение 1, уже не станет создавать еще одну копию (иначе мы быстро исчерпали бы ресурсы без пограничного условия остановки размножения).

input int Reference = 0;

Для дескриптора индикатора-копии зарезервирована переменная handle.

int handle = 0;

В обработчике OnInit мы для наглядности прежде всего выводим имя и путь MQL-программы.

int OnInit()
{
   const string name = MQLInfoString(MQL_PROGRAM_NAME);
   const string path = MQLInfoString(MQL_PROGRAM_PATH);
   Print(Reference);
   Print("Name: " + name);
   Print("Full path: " + path);
   ...

Далее идет код, подходящий для самозапуска обособленного индикатора (существующего в виде привычного файла NonEmbeddedIndicator.ex5).

   if(Reference == 0)
   {
      handle = iCustom(_Symbol_Periodname1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   Print("Success");
   return INIT_SUCCEEDED;
}

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

0

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\NonEmbeddedIndicator.ex5

Success

1

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\NonEmbeddedIndicator.ex5

Success

Таким образом, копия запустилась успешно просто по имени "NonEmbeddedIndicator".

Оставим пока данный индикатор и создадим второй — FaultyIndicator.mq5, в который подключим первый индикатор как ресурс (обратите внимание на указание подпапки SubFolder в относительном пути ресурса — это нужно, поскольку индикатор FaultyIndicator.mq5 находится в папке на уровень выше: MQL5/Indicators/MQL5Book/p7/).

// FaultyIndicator.mq5
#resource "SubFolder\\NonEmbeddedIndicator.ex5"
   
int handle;
   
int OnInit()
{
   handle = iCustom(_Symbol_Period"::SubFolder\\NonEmbeddedIndicator.ex5");
   if(handle == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

Если попытаться запустить откомпилированный FaultyIndicator.ex5, возникнет ошибка:

0

Name: NonEmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\FaultyIndicator.ex5 »

»  ::SubFolder\NonEmbeddedIndicator.ex5

cannot load custom indicator 'NonEmbeddedIndicator' [4802]

Во время запуска копии вложенного индикатора он ищется в папке основного индикатора, в котором описан ресурс. Но там файла NonEmbeddedIndicator.ex5 нет, поскольку требуемый ресурс находится внутри FaultyIndicator.ex5.

Чтобы решить проблему модифицируем NonEmbeddedIndicator.mq5. Прежде всего, дадим ему другое, более правильное имя EmbeddedIndicator.mq5. В исходном коде нам потребуется добавить вспомогательную функцию GetMQL5Path, которая из общего пути запускаемой MQL-программы умеет вычленить относительную часть внутри папки MQL5 (в этой части будет находиться и название ресурса, если индикатор запускается из ресурса).

// EmbeddedIndicator.mq5
string GetMQL5Path()
{
   static const string MQL5 = "\\MQL5\\";
   static const int length = StringLen(MQL5) - 1;
   static const string path = MQLInfoString(MQL_PROGRAM_PATH);
   const int start = StringFind(pathMQL5);
   if(start != -1)
   {
      return StringSubstr(pathstart + length);
   }
   return path;
}

С учетом новой функции изменим вызов iCustom в обработчике OnInit.

int OnInit()
{
   ...
   const string location = GetMQL5Path();
   Print("Location in MQL5:" + location);
   if(Reference == 0)
   {
      handle = iCustom(_Symbol_Periodlocation1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   return INIT_SUCCEEDED;
}

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

0

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Success

1

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\SubFolder\EmbeddedIndicator.ex5

Success

Здесь добавился отладочный вывод относительного пути, который получила функция GetMQL5Path. Именно эта строка теперь используется в iCustom, и она работает в данном режиме — копия создалась.

Теперь встроим этот индикатор как ресурс в другой индикатор в папке MQL5Book/p7 с именем MainIndicator.mq5. Примечательно, что MainIndicator.mq5 полностью идентичен FaultyIndicator.mq5 за исключением лишь подключаемого ресурса.

// MainIndicator.mq5
#resource "SubFolder\\EmbeddedIndicator.ex5"
...
int OnInit()
{
   handle = iCustom(_Symbol_Period"::SubFolder\\EmbeddedIndicator.ex5");
   ...
}

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

0

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\MainIndicator.ex5 »

»  ::SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\MainIndicator.ex5::SubFolder\EmbeddedIndicator.ex5

Success

1

Name: EmbeddedIndicator

Full path: C:\Program Files\MT5East\MQL5\Indicators\MQL5Book\p7\MainIndicator.ex5 »

»  ::SubFolder\EmbeddedIndicator.ex5

Location in MQL5:\Indicators\MQL5Book\p7\MainIndicator.ex5::SubFolder\EmbeddedIndicator.ex5

Success

Как мы видим, на этот раз вложенный индикатор успешно создал копию самого себя, так как использовал квалифицированное название с относительным путем и именем ресурса "\\Indicators\\MQL5Book\\p7\\MainIndicator.ex5::SubFolder\\EmbeddedIndicator.ex5".

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

Мы обещали показать, что создание индикатором своей копии не является чем-то экстраординарным. В качестве прикладной демонстрации данного приема возьмем индикатор DeltaPrice.mq5, который рассчитывает разницу приращений цен заданного порядка. Порядок 0 означает отсутствие дифференцирования (только для проверки исходного временного ряда), 1 — однократное дифференцирование, 2 — двукратное и так далее.

Порядок задается во входном параметре Differencing.

input int Differencing = 1;

Разностный ряд будет отображаться в единственном буфере в подокне.

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
   
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
#property indicator_style1 STYLE_SOLID
   
double Buffer[];

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

#include <MQL5Book/AppliedTo.mqh// APPLIED_TO_STR macro
 
int handle = 0;
   
int OnInit()
{
   const string label = "DeltaPrice (" + (string)Differencing + "/"
      + APPLIED_TO_STR() + ")";
   IndicatorSetString(INDICATOR_SHORTNAMElabel);
   PlotIndexSetString(0PLOT_LABELlabel);
   
   SetIndexBuffer(0Buffer);
   if(Differencing > 1)
   {
      handle = iCustom(_Symbol_PeriodGetMQL5Path(), Differencing - 1);
      if(handle == INVALID_HANDLE)
      {
         return INIT_FAILED;
      }
   }
   return INIT_SUCCEEDED;
}

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

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   for(int i = fmax(prev_calculated - 11); i < rates_total; ++i)
   {
      if(Differencing > 1)
      {
         static double value[2];
         CopyBuffer(handle0rates_total - i - 12value);
         Buffer[i] = value[1] - value[0];
      }
      else if(Differencing == 1)
      {
         Buffer[i] = price[i] - price[i - 1];
      }
      else
      {
         Buffer[i] = price[i];
      }
   }
   return rates_total;
}

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

Вот как на графике выглядят несколько копий индикатора с разными порядками дифференцирования.

Разница цен Close различных порядков

Разница цен Close различных порядков