Мультивалютные и мультитаймфреймовые индикаторы

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

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

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

Для начала упростим задачу и ограничимся одним произвольным символом, который может отличаться от текущего. Довольно часто трейдеру желательно видеть одновременно несколько графиков различных символов (например, ведущий и ведомый в коррелированной паре). Разработаем индикатор IndSubChartSimple.mq5 для отображения котировки выбранного пользователем символа в подокне.

IndSubChartSimple

Чтобы повторить внешний вид основного графика предусмотрим во входных параметрах не только указание символа, но и режима рисования: DRAW_CANDLES, DRAW_BARS, DRAW_LINE. Первые два требуют 4 буфера и выводят полную четверку цен Open, High, Low, Close (японскими свечами или барами), а последний — довольствуется единственным буфером для показа линии по цене Close. Но чтобы поддержать все режимы выбираем максимальное требуемое количество буферов.

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1
#property indicator_type1   DRAW_CANDLES
#property indicator_color1  clrBlue,clrGreen,clrRed // border,bullish,bearish

Массивы для буферов описаны по названиям типов цен.

double open[];
double high[];
double low[];
double close[];

По умолчанию включено отображение японских свечей. В этом режиме MQL5 позволяет указать не один цвет, а несколько. В директиве #property indicator_colorN они задаются через запятую. Если цветов два, то первый определяет цвет контуров свечей, а второй — заполнение. Если цветов три, как в нашем случае, то первый, как и в предыдущем случае, определяет цвет контуров свечей, а второй и третий — тело бычьей и тело медвежьей свечи соответственно.

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

Элементы ENUM_CHART_MODE

Элементы ENUM_DRAW_TYPE

CHART_CANDLES

DRAW_CANDLES

CHART_BARS

DRAW_BARS

CHART_LINE

DRAW_LINE

Они соответствуют выбранным нами режимам отрисовки, что в общем-то и не удивительно, потому что для этого индикатора намеренно выбирались способы рисования, повторяющие стандартные. ENUM_CHART_MODE удобно здесь использовать, поскольку оно содержит только 3 нужных нам элемента, в отличие от ENUM_DRAW_TYPE, в котором много других способов отрисовки.

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

input string SubSymbol = ""// Symbol
input ENUM_CHART_MODE Mode = CHART_CANDLES;

Для перевода ENUM_CHART_MODE в ENUM_DRAW_TYPE реализована простая функция.

ENUM_DRAW_TYPE Mode2Style(const ENUM_CHART_MODE m)
{
   switch(m)
   {
      case CHART_CANDLESreturn DRAW_CANDLES;
      case CHART_BARSreturn DRAW_BARS;
      case CHART_LINEreturn DRAW_LINE;
   }
   return DRAW_NONE;
}

Пустая строка во входном параметре SubSymbol означает текущий символ графика. Однако поскольку MQL5 не позволяет редактировать входные переменные, нам придется добавить глобальную переменную для хранения реального рабочего символа и присваивать её в обработчике OnInit.

string symbol;
...
int OnInit()
{
   symbol = SubSymbol;
   if(symbol == ""symbol = _Symbol;
   else
   {
      // убеждаемся, что символ существует и выбран в Обзоре рынка
      if(!SymbolSelect(symboltrue))
      {
         return INIT_PARAMETERS_INCORRECT;
      }
   }
   ...
}

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

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

  • InitBuffer — настройка одного буфера
  • InitBuffers — настройка всего набора буферов
  • InitPlot — настройка одной диаграммы

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

void InitBuffer(const int indexdouble &buffer[],
   const ENUM_INDEXBUFFER_TYPE style = INDICATOR_DATA,
   const bool asSeries = false)
{
   SetIndexBuffer(indexbufferstyle);
   ArraySetAsSeries(bufferasSeries);
}
   
string InitBuffers(const ENUM_CHART_MODE m)
{
   string title;
   if(m == CHART_LINE)
   {
      InitBuffer(0closeINDICATOR_DATAtrue);
      // скрываем все буфера, неиспользуемые для линейного графика
      InitBuffer(1highINDICATOR_CALCULATIONStrue);
      InitBuffer(2lowINDICATOR_CALCULATIONStrue);
      InitBuffer(3openINDICATOR_CALCULATIONStrue);
      title = symbol + " Close";
   }
   else
   {
      InitBuffer(0openINDICATOR_DATAtrue);
      InitBuffer(1highINDICATOR_DATAtrue);
      InitBuffer(2lowINDICATOR_DATAtrue);
      InitBuffer(3closeINDICATOR_DATAtrue);
      title = "# Open;# High;# Low;# Close";
      StringReplace(title"#"symbol);
   }
   return title;
}

Обратите внимание, что при включении режима линейной диаграммы используется только массив close и ему назначается индекс 0. Остальные три массива полностью скрываются от пользователя за счет свойства INDICATOR_CALCULATIONS. В режимах свечей и баров задействованы все xtnsht массива, и их нумерация соответствует стандарту OHLC, как того требуют типы отрисовки DRAW_CANDLES и DRAW_BARS. Всем массивам назначается свойство "серийности", то есть индексации справа налево.

Функция InitBuffers возвращает заголовок для буферов в Окне данных.

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

void InitPlot(const int indexconst string nameconst int style,
   const int width = -1const int colorx = -1,
   const double empty = EMPTY_VALUE)
{
  PlotIndexSetInteger(indexPLOT_DRAW_TYPEstyle);
  PlotIndexSetString(indexPLOT_LABELname);
  PlotIndexSetDouble(indexPLOT_EMPTY_VALUEempty);
  if(width != -1PlotIndexSetInteger(indexPLOT_LINE_WIDTHwidth);
  if(colorx != -1PlotIndexSetInteger(indexPLOT_LINE_COLORcolorx);
}

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

int OnInit()
{
   ...
   InitPlot(0InitBuffers(Mode), Mode2Style(Mode));
   IndicatorSetString(INDICATOR_SHORTNAME"SubChart (" + symbol + ")");
   IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(symbolSYMBOL_DIGITS));
     
   return INIT_SUCCEEDED;
}

Хотя в данной версии индикатора настройка выполняется единожды, это происходит динамически с учетом входного параметра Mode, в отличие от статической настройки, которую предоставляют директивы #property. И в дальнейшем, в полной версии индикатора мы сможем вызывать InitPlot по много раз, меняя внешнее представление индикатора "на ходу".

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

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   if(prev_calculated == 0// требует уточнения (см. далее)
   {
      ArrayInitialize(openEMPTY_VALUE);
      ArrayInitialize(highEMPTY_VALUE);
      ArrayInitialize(lowEMPTY_VALUE);
      ArrayInitialize(closeEMPTY_VALUE);
   }
   
   if(_Symbol != symbol)
   {
      // в процессе разработки
      ...
   }
   else
   {
      ArraySetAsSeries(optrue);
      ArraySetAsSeries(hitrue);
      ArraySetAsSeries(lotrue);
      ArraySetAsSeries(cltrue);
      for(int i = 0i < MathMax(rates_total - prev_calculated1); ++i)
      {
         open[i] = op[i];
         high[i] = hi[i];
         low[i] = lo[i];
         close[i] = cl[i];
      }
   }
   
   return rates_total;
}

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

Поэтому создадим переменные, контролирующие количество баров на стороннем символе (lastAvailable), редактируемый "клон" константного аргумента prev_calculated, а также признак готовности котировок.

   static bool initialized;  // флаг готовности котировок символа
   static int lastAvailable// количество баров для символа (и текущего таймфрейма)
   int _prev_calculated = prev_calculated// редактируемая копия prev_calculated

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

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   ...
   if(iBars(symbol_Period) - lastAvailable > 1)
   {
      // докачка истории или первый старт
      _prev_calculated = 0;
      initialized = false;
      lastAvailable = 0;
   }
   
   // далее везде используем копию _prev_calculated
   if(_prev_calculated == 0)
   {
      ArrayInitialize(openEMPTY_VALUE);
      ArrayInitialize(highEMPTY_VALUE);
      ArrayInitialize(lowEMPTY_VALUE);
      ArrayInitialize(closeEMPTY_VALUE);
   }
   
   if(_Symbol != symbol)
   {
      // запрос котировок и "ожидание" их готовности
      ...
      // основной расчет (заполнение буферов)
      ...
   }
   else
   {
      ... // как есть
   } 
   lastAvailable = iBars(symbol_Period);
   return rates_total;
}

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

Проверкой готовности котировок займется следующий фрагмент кода.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const datetime &time[],
   const double &op[], const double &hi[], const double &lo[], const double &cl[],
   const long &[], const long &[], const int &[]) // unused
{
   ...
   if(_Symbol != symbol)
   {
      if(!initialized)
      {
         Print("Host "_Symbol" "rates_total" bars up to ", (string)time[0]);
         Print("Updating "symbol" "lastAvailable" -> "iBars(symbol_Period), " / ",
            (iBars(symbol_Period) > 0 ?
               (string)iTime(symbol_PeriodiBars(symbol_Period) - 1) : "n/a"),
            "... Please wait");
         if(QuoteRefresh(symbol_Periodtime[0]))
         {
            Print("Done");
            initialized = true;
         }
         else
         {
            // асинхронный запрос на обновление графика
            ChartSetSymbolPeriod(0_Symbol_Period);
            return 0// нечего показывать пока
         }
      }
      ...

Основную работу в нем выполняет особая функция QuoteRefresh. Она принимает в качестве аргументов интересующий нас символ, таймфрейм и время самого первого (старого) бара на текущем графике — более ранние даты нас не интересуют, но не факт, что на "чужом" символе имеется история на всю эту глубину. Именно поэтому удобно скрыть все сложности проверок в отдельной функции.

Функция вернет true, когда данные будут скачаны и синхронизированы в доступном объеме. Её внутреннее устройство рассмотрим через минуту.

Когда синхронизация выполнена, используем функцию iBarShift для нахождения синхронных баров и копирования их значений OHLC (функции iOpen, iHigh, iLow, iClose).

      ArraySetAsSeries(timetrue); // обход из настоящего в прошлое
      for(int i = 0i < MathMax(rates_total - _prev_calculated1); ++i)
      {
         int x = iBarShift(symbol_Periodtime[i], true);
         if(x != -1)
         {
            open[i] = iOpen(symbol_Periodx);
            high[i] = iHigh(symbol_Periodx);
            low[i] = iLow(symbol_Periodx);
            close[i] = iClose(symbol_Periodx);
         }
         else
         {
            open[i] = high[i] = low[i] = close[i] = EMPTY_VALUE;
         }
      }

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

Поскольку в функцию iBarShift последним параметром передается true, функция будет искать точное соответствие времени баров, и если в другом символе какой-либо бар отсутствует, мы получим -1 и отобразим на графике пустоту (EMPTY_VALUE).

После успешного полного расчета новые бары будут обсчитываться в экономном режиме, т.е. с учетом _prev_calculated и rates_total.

А теперь обратимся к функции QuoteRefresh. Как универсальная и полезная, она вынесена в заголовочный файл QuoteRefresh.mqh.

В самом начале делается проверка на то, не запрашивается ли таймсерия текущего символа и текущего таймфрейма из MQL-программы типа индикатор. Такие запросы запрещены, поскольку "родная" таймсерия, на которой запущен индикатор, уже находится в процессе построения терминалом или готова: подгонять его еще раз — чревато зацикливанием или блокировкой. Поэтому мы просто возвращаем признак синхронизации (SERIES_SYNCHRONIZED), и если она пока не готова, индикатору следует проверить данные позднее (на следующих тиках, по таймеру или как-то еще).

bool QuoteRefresh(const string assetconst ENUM_TIMEFRAMES period,
   const datetime start)
{
   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE) == PROGRAM_INDICATOR
      && _Symbol == asset && _Period == period)
   {
      return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
   }
   ...

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

   if(Bars(assetperiod) >= TerminalInfoInteger(TERMINAL_MAXBARS))
   {
      return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
   }
   ...

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

  • по заданному таймфрейму (SERIES_FIRSTDATE);
  • без привязки к таймфрейму (SERIES_TERMINAL_FIRSTDATE) в локальной базе терминала;
  • без привязки к таймфрейму (SERIES_SERVER_FIRSTDATE) на сервере.

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

   datetime times[1];
   datetime first = 0server = 0;
   if(PRTF(SeriesInfoInteger(assetperiodSERIES_FIRSTDATEfirst)))
   {
      if(first > 0 && first <= start)
      {
         // прикладные данные существуют, они уже готовы или подготавливаются
         return (bool)SeriesInfoInteger(assetperiodSERIES_SYNCHRONIZED);
      }
      else
      if(PRTF(SeriesInfoInteger(assetperiodSERIES_TERMINAL_FIRSTDATEfirst)))
      {
         if(first > 0 && first <= start)
         {
            // технические данные существуют в базе терминала,
            // инициируем построение таймсерии или сразу получим искомое
            return PRTF(CopyTime(assetperiodfirst1times)) == 1;
         }
         else
         {
            if(PRTF(SeriesInfoInteger(assetperiodSERIES_SERVER_FIRSTDATEserver)))
            {
               // технические данные существуют на сервере, запросим их
               if(first > 0 && first < server)
                  PrintFormat(
                    "Warning: %s first date %s on server is less than on terminal ",
                     assetTimeToString(server), TimeToString(first));
               // нельзя просить больше, чем имеет сервер - поэтому fmax
              return PRTF(CopyTime(assetperiodfmax(startserver), 1times)) == 1;
            }
         }
      }
   }
   
   return false;
}

Индикатор готов. Откомпилируем и запустим его, например, на графике EURUSD,H1, указав в качестве дополнительного символа USDRUB. В журнале появятся примерно такие записи:

Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=false / HISTORY_NOT_FOUND(4401)
Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=true / ok
Done

После индикации завершения процесса ("Done"), в подокне будут показаны свечи "чужого" графика.

Индикатор IndSubChartSimple — DRAW_CANDLES с котировками стороннего символа

Индикатор IndSubChartSimple — DRAW_CANDLES с котировками стороннего символа

Важно отметить, что из-за сокращенной торговой сессии значащие бары для USDRUB занимают лишь дневную часть каждого суточного интервала.

IndUnityPercent

Вторым индикатором, который мы создадим в рамках данного раздела, является настоящий мультивалютный (строго говоря, мультисимвольный) индикатор IndUnityPercent.mq5. Его идея заключается в том, чтобы отобразить относительную силу всех независимых валют (активов), входящих в состав заданных финансовых инструментов. Например, если мы торгуем корзиной из двух тикеров EURUSD и XAUUSD, то фактически в стоимости учитываются доллар, евро и золото — каждый из этих активов обладает относительной стоимостью по сравнению с другими.

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

EUR / USD = EURUSD
XAU / USD = XAUUSD

где переменные EUR, USD, XAU — некие самостоятельные "стоимости" активов, а EURUSD и XAUUSD — константы (известные котировки).

Для нахождения переменных дополним систему еще один уравнением, ограничив сумму квадратов переменных единицей (отсюда и первое слово в названии индикатора — Unity):

EUR * EUR + USD * USD + XAU * XAU = 1

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

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

x0 = sqrt(1 / (1 + sum(C(xix0)2))), i = 1..n
xi = C(xix0) * x0i = 1..n

где n — количество переменных, C(xi,x0) — котировка i-ой пары, включающей соответствующие переменные. Обратите внимание, что количество переменных на 1 больше, чем инструментов.

Поскольку котировки, участвующие в расчете, обычно сильно отличаются (например, как в случае EURUSD и XAUUSD), а кроме того выражаются только друг через друга (то есть без привязки к какой-либо стабильной базе), имеет смысл перейти от абсолютных значений к процентным изменениям. Таким образом, при написании алгоритмов по вышеприведенным формулам будем вместо котировки C(xi,x0) брать отношение C(xi,x0)[0] / C(xi,x0)[1], где индексы в квадратных скобках означают текущий [0] и предыдущий [1] бар. Кроме того, для ускорения расчета можно избавиться от возведения в квадрат и взятия квадратного корня.

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

#define BUF_NUM 15
#property indicator_separate_window
#property indicator_buffers BUF_NUM
#property indicator_plots BUF_NUM

При реализации данного индикатора решим попутно одну неприятную проблему. Поскольку предполагается множество однотипных буферов, стандартный подход предполагает их экстенсивное кодирование "размножением" (пресловутый нерекомендуемый стиль программирования "copy & paste").

double buffer1[];
...
double buffer15[];
   
void OnInit()
{
   SetIndexBuffer(0buffer1);
   ...
   SetIndexBuffer(14buffer15);
}

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

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

// код "движка" с поддержкой массива унифицированных индикаторных буферов
class Buffer
{
   static int count// глобальный счетчик буферов
   double array[];   // массив для этого буфера
   int cursor;       // указатель присваиваемого элемента
public:
   // конструктор настраивает и подключает массив
   Buffer()
   {
      SetIndexBuffer(count++, array);
      ArraySetAsSeries(array, ...);
   }
   // перегрузка для установки номера интересующего элемента
   Buffer *operator[](int index)
   {
      cursor = index;
      return &this;
   }
   // перегрузка для записи значения в выбранный элемент
   double operator=(double x)
   {
      buffer[cursor] = x;
      return x;
   }
   ...
};
   
static int Buffer::count;

Благодаря перегрузкам операторов мы можем придерживаться привычного синтаксиса для присваивания значений элементам объекта-буфера: buffer[i] = value.

В коде индикатора вместо множества строк с описаниями отдельных массивов достаточно будет определить один "массив массивов".

// код индикатора
// конструируем 15 объектов-буферов с авто-регистрацией и настройкой
Buffer buffers[15];
...

Полная версия классов, реализующих данный механизм, приводится в файле IndBufArray.mqh. Следует отметить, что в нем обеспечена поддержка только буферов, но не диаграмм. В идеале набор классов должен быть расширен новыми, позволяющими создавать готовые объекты-диаграммы, которые занимали бы в массиве буферов необходимое их количество согласно типу конкретной диаграммы. Изучить и дополнить файл предлагается самостоятельно. В частности, в коде имеется класс-менеджер массива индикаторных буферов BufferArray для создания "массивов массивов" с одинаковыми значениями свойств, таких как тип ENUM_INDEXBUFFER_TYPE, направление индексации, "пустое" значение. Мы его используем в новом индикаторе следующим образом:

BufferArray buffers(BUF_NUMtrue);

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

После этого определения мы можем в любом месте кода применять удобную нотацию для установки значения j-го бара i-го буфера (она использует двойную перегрузку оператора[] — не только в объекте-буфере, но и в массиве буферов):

buffers[i][j] = value;

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

input string Instruments = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
input int BarLimit = 500;

При старте программы следует произвести разбор списка символов и сформировать из них отдельный массив Symbols размера SymbolCount.

string Symbols[];
int Direction[]; // прямой(+1)/обратный(-1) курс к общей валюте
int SymbolCount;

У всех символов должна быть одна общая валюта (обычно "USD") для выявления взаимных соотношений. В зависимости от того, является эта общая валюта в конкретном символе базовой (на первом месте в паре, если речь о Forex) или валютой котирования (на втором месте в паре Forex), в расчетах должны участвовать её прямые или обратные котировки (1.0 / курс). Это направление будем хранить в массиве Direction.

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

string InitSymbols()
{
   SymbolCount = fmin(StringSplit(Instruments, ',', Symbols), BUF_NUM - 1);
   ArrayResize(SymbolsSymbolCount);
   ArrayResize(DirectionSymbolCount);
   ArrayInitialize(Direction0);
   
   string common = NULL// общая валюта
   
   for(int i = 0i < SymbolCounti++)
   {
      // гарантируем наличие символа в Обзоре рынка
      if(!SymbolSelect(Symbols[i], true)) 
      {
         Print("Can't select "Symbols[i]);
         return NULL;
      }
      
      // получаем валюты, составляющие символ
      string firstsecond;
      first = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE);
      second = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT);
    
      // подсчитываем количество включений каждой валюты
      if(first != second)
      {
         workCurrencies.inc(first);
         workCurrencies.inc(second);
      }
      else
      {
         workCurrencies.inc(Symbols[i]);
      }
   }
   ...

В цикле ведется учет встречаемости каждой валюты в составе всех инструментов с помощью вспомогательного шаблонного класса MapArray. Такой объект описан в индикаторе на глобальном уровне и требует подключения заголовочного файла MapArray.mqh.

#include <MQL5Book/MapArray.mqh>
...
// массив пар [название;количество]
// для подсчета статистики использования валют
MapArray<string,intworkCurrencies;
...
string InitSymbols()
{
   ...
}

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

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

   ...   
   // находим общую валюту на основе статистики использования валют
   for(int i = 0i < workCurrencies.getSize(); i++)
   {
      if(workCurrencies[i] > 1// счетчик больше 1
      {
         if(common == NULL)
         {
            common = workCurrencies.getKey(i); // получим имя i-ой валюты
         }
         else
         {
            Print("Collision: multiple common symbols");
            return NULL;
         }
      }
   }
   
   if(common == NULLcommon = workCurrencies.getKey(0);
   
   // зная общую валюту, определяем "направление" каждого символа   
   for(int i = 0i < SymbolCounti++)
   {
      if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT) == common)
         Direction[i] = +1;
      else if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE) == common)
         Direction[i] = -1;
      else
      {
         Print("Ambiguous symbol direction "Symbols[i], ", defaults used");
         Direction[i] = +1;
      }
   }
   
   return common;
}

Имея готовую функцию InitSymbols, мы можем написать OnInit (приводится с упрощениями).

int OnInit()
{
   const string common = InitSymbols();
   if(common == NULLreturn INIT_PARAMETERS_INCORRECT;
   
   string base = SymbolInfoString(_SymbolSYMBOL_CURRENCY_BASE);
   string profit = SymbolInfoString(_SymbolSYMBOL_CURRENCY_PROFIT);
   
   // настройка линий по количеству валют (количество символов + 1)
   for(int i = 0i <= SymbolCounti++)
   {
      string name = workCurrencies.getKey(i);
      PlotIndexSetString(iPLOT_LABELname);
      PlotIndexSetInteger(iPLOT_DRAW_TYPEDRAW_LINE);
      PlotIndexSetInteger(iPLOT_SHOW_DATAtrue);
      PlotIndexSetInteger(iPLOT_LINE_WIDTH1 + (name == base || name == profit));
   }
  
   // скрываем лишние буфера в Окне данных
   for(int i = SymbolCount + 1i < BUF_NUMi++)
   {
      PlotIndexSetInteger(iPLOT_SHOW_DATAfalse);
   }
  
   // единственный уровень на 1.0
   IndicatorSetInteger(INDICATOR_LEVELS1);
   IndicatorSetDouble(INDICATOR_LEVELVALUE01.0);
  
   // Название с параметрами
   IndicatorSetString(INDICATOR_SHORTNAME,
      "Unity [" + (string)workCurrencies.getSize() + "]");
  
   // точность
   IndicatorSetInteger(INDICATOR_DIGITS5);
   
   return INIT_SUCCEEDED;
}

Теперь познакомимся с обработчиком главного события OnCalculate.

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

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const doubleprice[])
{
   if(prev_calculated == 0)
   {
      buffers.empty(); // делегируем тотальную очистку классу BufferArray
   }
  
   // основной цикл в направлении "как в таймсерии" от настоящего в прошлое
   const int limit = MathMin(rates_total - prev_calculated + 1BarLimit);
   for(int i = 0i < limiti++)
   {
      if(!calculate(i))
      {
         EventSetTimer(1); // даем еще 1 секунду на закачку и подготовку данных
         return 0// попробуем пересчитаться на следующем вызове
      }
   }
   
   return rates_total;
}

Расчет значений для всех буферов на i-ом баре осуществляет функция Calculate (см. далее). В случае недостатка данных она вернет false, и мы запустим таймер, чтобы дать время на построение таймсерий по всем требуемым инструментам. В обработчике таймера привычным образом отправим терминалу запрос на обновление графика.

void OnTimer()
{
   EventKillTimer();
   ChartSetSymbolPeriod(0_Symbol_Period);
}

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

bool Calculate(const int bar)
{
   const datetime time0 = iTime(_Symbol_Periodbar);
   const datetime time1 = iTime(_Symbol_Periodbar + 1);
   ...

Две даты потребовалось, чтобы вызвать далее функцию CopyClose в том её варианте, где указывается интервал дат. В данном индикаторе мы не можем использовать вариант с количеством баров, потому что на любом символе могут быть произвольные пропуски в барах, отличные от пропусков на других символах. Например, если на одном символе существуют бары t (текущий) и t-1 (предыдущий), то для него возможно корректно рассчитать изменение Close[t]/Close[t-1]. Однако на другом символе бар t может отсутствовать, и запрос двух баров вернет "ближайшие" слева (в прошлом) бары, причем это прошлое может отстоять от "настоящего" достаточно далеко (например, соответствовать торговой сессии за предыдущий день, если символ не торгуется круглосуточно).

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

При этом возможны ситуации, когда такой запрос вернет больше 2 баров, и в этом случае всегда берутся два последних (правых) бара, как наиболее актуальные. Например, при размещении на графике USDRUB,H1 индикатор будет "видеть", что после бара в 17:00 каждого рабочего дня идет бар 10:00 следующего рабочего дня. Однако для основных валютных пар Forex, таких как EURUSD, между ними будет 16 вечерних, ночных и утренних баров H1.

bool Calculate(const int bar)
{
   ...
   double w[]; // приёмный массив котировок (побаровый)
   double v[]; // изменения по символам
   ArrayResize(vSymbolCount);
   
   // находим изменения котировок для каждого символа
   for(int j = 0j < SymbolCountj++)
   {
      // пробуем получить для j-го символа как минимум 2 бара,
      // соответствующих двум барам символа текущего графика
      int x = CopyClose(Symbols[j], _Periodtime0time1w);
      if(x < 2)
      {
         // если баров нет, пробуем получить предыдущий бар из прошлого
         if(CopyClose(Symbols[j], _Periodtime01w) != 1)
         {
            return false;
         }
         // затем дублируем его как индикацию отсутствия изменений
         // (в принципе, можно было 2 раза записать любую константу)
         x = 2;
         ArrayResize(w2);
         w[1] = w[0];
      }
   
      // находим обратный курс, когда необходимо
      if(Direction[j] == -1)
      {
         w[x - 1] = 1.0 / w[x - 1];
         w[x - 2] = 1.0 / w[x - 2];
      }
   
      // вычисляем изменений в виде соотношения двух величин
      v[j] = w[x - 1] / w[x - 2]; // последняя / предыдущая
   }
   ...

Когда изменения получены, алгоритм работает по приводившимся ранее формулам и записывает значения в индикаторные буфера.

   double sum = 1.0;
   for(int j = 0j < SymbolCountj++)
   {
      sum += v[j];
   }
   
   const double base_0 = (1.0 / sum);
   buffers[0][bar] = base_0 * (SymbolCount + 1);
   for(int j = 1j <= SymbolCountj++)
   {
      buffers[j][bar] = base_0 * v[j - 1] * (SymbolCount + 1);
   }
   
   return true;
}

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

Мультисимвольный индикатор IndUnityPercent с основными парами Forex

Мультисимвольный индикатор IndUnityPercent с основными валютами Forex

Расстояние между линиями двух валют в окне индикатора равно изменению соответствующей котировки в процентах (между двумя последовательными ценами Close). Отсюда второе слово в названии индикатора — Percent.

В следующей главе про программное использование индикаторов мы представим продвинутую версию IndUnityPercentPro.mq5 с заменой Copy-функций на вызов встроенных индикаторов iMA, что позволит без лишних усилий реализовать сглаживание и расчет по произвольному типу цен.