Получение данных таймсерии из индикатора: CopyBuffer

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

MQL5 API предоставляет для чтения буферов функцию CopyBuffer, имеющую 3 формы.

int CopyBuffer(int handle, int buffer, int offset, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, datetime stop, double &array[])

В параметре handle указывается дескриптор, полученный из вызова iCustom или других функций (см. далее разделы про IndicatorCreate и встроенные индикаторы). Параметр buffer задает индекс индикаторного буфера, из которого следует запросить данные. Нумерация ведется, начиная с 0.

Полученные элементы запрошенной таймсерии попадают в массив array, заданный по ссылке.

Три варианта функции различаются способом задания диапазона временных меток (start/stop) или номеров (offset) и количества (count) баров, для которых получаются данные. Принципы работы с этими параметрами полностью соответствуют тому, что мы изучали в Обзоре Copy-функций для получения массивов котировок. В частности, отсчет элементов копируемых данных в offset и count ведется от настоящего к прошлому, то есть стартовая позиция, равная 0, означает текущий бар. А в приемном массиве array элементы физически располагаются в порядке от прошлого к настоящему (однако эту адресацию можно поменять на логическом уровне на обратную с помощью ArraySetAsSeries).

В принципе CopyBuffer является аналогом функций для чтения встроенных таймсерий типа CopyOpen, CopyClose и других. Основное отличие заключается в том, что таймсерии с котировками формирует сам терминал, а таймсерии в индикаторных буферах рассчитывают пользовательские или встроенные индикаторы. Кроме того, конкретную пару символа и таймфрейма, которые определяют и идентифицируют таймсерию, в случае индикаторов мы задаем заранее — в функции создания дескриптора вроде iCustom, а в CopyBuffer эта информация передается опосредованно — через дескриптор handle.

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

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

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

Функция возвращает количество скопированных элементов или -1 в случае ошибки, включая и временное отсутствие готовых данных.

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

Если запрашиваемые таймсерии еще не построены или их необходимо загрузить с сервера, то функция ведет себя по-разному в зависимости от типа MQL-программы, из которой она вызывается.
 
При запросе еще неготовых данных из индикатора, функция сразу же вернет -1, но при этом сам процесс загрузки и построения таймсерий будет инициирован.
 
При запросе данных из эксперта или скрипта, будет инициирована загрузка с сервера и/или начнется построение нужной таймсерии, если данные можно построить из локальной истории, но они еще не готовы. Функция вернет то количество данных, которые будут готовы к моменту истечения таймаута (45 секунд), отведенного на синхронное выполнение функции (вызывающий код при этом ожидает завершения работы функции).

Обратите внимание, что функция CopyBuffer может читать данные из буферов вне зависимости от режима их работы — INDICATOR_DATA, INDICATOR_COLOR_INDEX, INDICATOR_CALCULATIONS — два последних скрыты от пользователя.

Также важно отметить, что в вызываемом индикаторе может быть установлен сдвиг таймсерии с помощью свойства PLOT_SHIFT и он влияет на смещение считываемых данных с помощью CopyBuffer. Например, если линии индикатор сдвинуты в будущее на N баров то в параметрах CopyBuffer (первой формы) нужно задавать offset равным (- N), то есть с минусом, так как текущий бар таймсерии имеет индекс 0, а индексы будущих баров со сдвигом уменьшаются на единицу на каждом баре. В частности, такая ситуация возникает с индикатором Gator, потому что его нулевая диаграмма сдвинута вперед на значение параметр TeethShift, а первая диаграмма — на значение параметра LipsShift. На максимальное из них и нужно делать поправку. Пример будет представлен в разделе Чтение данных из диаграмм, имеющих сдвиг.

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

Работать с CopyBuffer из кода экспертов нам придется в главе про эксперты, а пока ограничимся индикаторами.

Продолжим развивать пример со вспомогательным индикатором IndWPR. На этот раз в версии UseWPR3.mq5 предусмотрим индикаторный буфер и заполним его данными из IndWPR с помощью CopyBuffer. Для этого применим директивы с количеством буферов и настройками отрисовки.

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "WPR"

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

input int WPRPeriod = 14;
   
double WPRBuffer[];
   
int handle;

Обработчик OnInit практически не изменится: добавился только вызов SetIndexBuffer.

int OnInit()
{
   SetIndexBuffer(0WPRBuffer);
   handle = iCustom(_Symbol_Period"IndWPR"WPRPeriod);
   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(handle00rates_total - prev_calculated + 1WPRBuffer);
   // в случае отсутствия ошибок наши данные готовы для всех баров rates_total
   return n > -1 ? rates_total : 0;
}

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

Индикаторы IndWPR, IndTripleEMA, IndFractals

Индикаторы IndWPR, IndTripleEMA, IndFractals

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

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

Новый индикатор доступен в файле UseWPRFractals.mq5.

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

Сигналы будем отображать на основном графике (на ценах Close, по умолчанию), поэтому используем директиву indicator_chart_window. Это не мешает нам вызывать индикаторы типа WPR, которые предназначены для отдельного окна, поскольку все подчиненные индикаторы способны рассчитаться без визуализации. При необходимости мы можем вывести их на график, но поговорим об этом в главе про графики (см. ChartIndicatorAdd).

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3
// настройки отрисовки буферов
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label1  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrBlue
#property indicator_width2  1
#property indicator_label2  "Buy"
#property indicator_type3   DRAW_NONE
#property indicator_color3  clrGreen
#property indicator_width3  1
#property indicator_label3  "Filter"

Во входных переменных предусмотрим возможность указать период WPR, период усреднения (сглаживания) и порядок фракталов — это все параметры подчиненных индикаторов. Кроме того заведем переменную Offset с номером бара, на котором будут анализироваться сигналы. Значение 0 (по умолчанию) означает текущий бар и анализ в потиковом режиме (внимание: сигналы на последнем баре могу перерисовываться — некоторые трейдеры этого не любят). Если сделать Offset равным 1, будем анализировать уже сформированные бары, и такие сигналы не меняются.

input int PeriodWPR = 11;
input int PeriodEMA = 5;
input int FractalOrder = 1;
input int Offset = 0;
input double Threshold = 0.2;

Переменная Threshold определяет размер зон перекупленности и перепроданности как часть от ±1.0 (в каждую из сторон). Например, если следовать классическим настройкам WPR с уровнями -20 и -80 на шкале от 0 до -100, то Threshold должен быть равен 0.4.

Для индикаторных буферов заведены следующие массивы.

double UpBuffer[];   // верхний сигнал значит перекупленность, то есть продажа
double DownBuffer[]; // нижний сигнал значит перепроданность, то есть покупку
double Filter[];     // направление фильтра по фракталам +1 (вверх/покупка), -1 (вниз/продажа)

Дескрипторы индикаторов сохраним в глобальных переменных.

int handleWPRhandleEMA3handleFractals;

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

int OnInit()
{
   // привязка буферов
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   SetIndexBuffer(2FilterINDICATOR_DATA); // вариант: INDICATOR_CALCULATIONS
   ArraySetAsSeries(UpBuffertrue);
   ArraySetAsSeries(DownBuffertrue);
   ArraySetAsSeries(Filtertrue);
   
   // сигналы стрелочки
   PlotIndexSetInteger(0PLOT_ARROW234);
   PlotIndexSetInteger(1PLOT_ARROW233);
   
   // подчиненные индикаторы
   handleWPR = iCustom(_Symbol_Period"IndWPR"PeriodWPR);
   handleEMA3 = iCustom(_Symbol_Period"IndTripleEMA"PeriodEMA0handleWPR);
   handleFractals = iCustom(_Symbol_Period"IndFractals"FractalOrder);
   if(handleWPR == INVALID_HANDLE
   || handleEMA3 == INVALID_HANDLE
   || handleFractals == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
}

В вызовах iCustom следует обратить внимание, как создается handleEMA3. Поскольку эта средняя должна считаться по WPR, мы передаем дескриптор handleWPR (полученный в предыдущем вызове iCustom) последним параметром, после фактических параметров индикатора IndTripleEMA. При этом мы обязаны указать полный список входных параметров IndTripleEMA (напомним, что там параметрами являются int InpPeriodEMA и BEGIN_POLICY InpHandleBegin — второй параметр мы использовали для изучения пропуска начальных баров и сейчас он нам в принципе не важен, но мы обязаны его передать, в данном случае просто как 0). Если бы мы опустили второй параметр в вызове, как несущественный в текущем контексте применения, то переданный следом дескриптор handleWPR был бы принят в вызванном индикаторе за InpHandleBegin. В результате IndTripleEMA применился бы к обычной цене Close.  

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

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(handleEMA3) != rates_total
   || BarsCalculated(handleFractals) != rates_total)
   {
      return prev_calculated;
   }
   
   ArraySetAsSeries(datatrue);
   
   if(prev_calculated == 0// первый запуск
   {
      ArrayInitialize(UpBufferEMPTY_VALUE);
      ArrayInitialize(DownBufferEMPTY_VALUE);
      ArrayInitialize(Filter0);
      
      // ищем сигналы по всей истории
      for(int i = rates_total - FractalOrder - 1i >= 0; --i)
      {
         MarkSignals(iOffsetdata);
      }
   }
   else // онлайн
   {
      for(int i = 0i < rates_total - prev_calculated; ++i)
      {
         UpBuffer[i] = EMPTY_VALUE;
         DownBuffer[i] = EMPTY_VALUE;
         Filter[i] = 0;
      }
      
      // ищем сигналы на новом баре или каждом тике (если Offset == 0)
      if(rates_total != prev_calculated
      || Offset == 0)
      {
         MarkSignals(0Offsetdata);
      }
   }
   
   return rates_total;
}

Интересующая нас в первую очередь работа с функцией CopyBuffer скрыта в MarkSignals. Значения сглаженного WPR будем читать в массив wpr[2], а фракталы — в  массивы peaks[1] и hollows[1].

int MarkSignals(const int barconst int offsetconst double &data[])
{
   double wpr[2];
   double peaks[1], hollows[1];
   ...

Заполняем локальные массивы с помощью трех вызовов CopyBuffer. Обратите внимание, что нам не нужны напрямую показания IndWPR, потому что он участвует в расчетах IndTripleEMA. Мы читаем в массив wpr данные через дескриптор handleEMA3. Также важно, что во фрактальном индикаторе существует 2 буфера, и потому функция CopyBuffer вызывается два раза с разными индексами 0 и 1 для массивов peaks и hollows, соответственно. Фрактальные массивы читаются с отступом FractalOrder, потому что фрактал может сформироваться только на баре, у которого не только слева, но и справа имеется предопределенное количество баров.

   if(CopyBuffer(handleEMA30bar + offset2wpr) == 2
   && CopyBuffer(handleFractals0bar + offset + FractalOrder1peaks) == 1
   && CopyBuffer(handleFractals1bar + offset + FractalOrder1hollows) == 1)
   {
      ...

Далее берем с предыдущего бара буфера Filter прежнее направление фильтра (в начале истории там 0, но при появлении фрактала вверх или вниз мы пишем туда +1 или -1, это видно в исходном коде чуть ниже) и изменяем его соответствующим образом при обнаружении любого нового фрактала.

      int filterdirection = (int)Filter[bar + 1];
      
      // последний фрактал задает разворотное движение
      if(peaks[0] != EMPTY_VALUE)
      {
         filterdirection = -1// sell
      }
      if(hollows[0] != EMPTY_VALUE)
      {
         filterdirection = +1// buy
      }
   
      Filter[bar] = filterdirection// запоминаем текущее направление

Наконец, анализируем переход сглаженного WPR из верхней или нижней зоны в среднюю, с учетом ширины зон, заданной во Threshold.

      // переводим 2 величины WPR в диапазон [-1,+1]
      const double old = (wpr[0] + 50) / 50;     // +1.0 -1.0
      const double last = (wpr[1] + 50) / 50;    // +1.0 -1.0
      
      // отскок от верха вниз
      if(filterdirection == -1
      && old >= 1.0 - Threshold && last <= 1.0 - Threshold)
      {
         UpBuffer[bar] = data[bar];
         return -1// продажа
      }
      
      // отскок от низа вверх
      if(filterdirection == +1
      && old <= -1.0 + Threshold && last >= -1.0 + Threshold)
      {
         DownBuffer[bar] = data[bar];
         return +1// покупка
      }
   }
   return 0// сигнала нет
}

Ниже представлен скриншот получившегося индикатора на графике.

Сигнальный индикатор UseWPRFractals на основе WPR, EMA3 и фракталов

Сигнальный индикатор UseWPRFractals на основе WPR, EMA3 и фракталов