Рецепты MQL5 - Создаем кольцевой буфер для быстрого расчета индикаторов в скользящем окне

Vasiliy Sokolov | 7 апреля, 2017

Оглавление


Введение

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


Проблема расчета скользящей средней

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

 

Реализуем этот расчет, написав простой скрипт на MQL5:

//+------------------------------------------------------------------+
//|                                                          SMA.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input int N = 10;       // Период средней
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   double closes[];
   if(CopyClose(Symbol(), Period(), 0, N, closes)!= N)
   {
      printf("Need more data");
      return;
   }
   double sum = 0.0;
   for(int i = 0; i < N; i++)
      sum += closes[i];
   sum /= N;
   printf("SMA: " + DoubleToString(sum, Digits()));  
  }
//+------------------------------------------------------------------+

С точки зрения самого расчета все выполнено верно: результатом работы этого скрипта будет значение скользящей средней, выведенное в окне терминала. Однако что делать, когда мы работаем в скользящем окне? На практике последнее значение котировки будет постоянно меняться и будут добавляться новые бары. Алгоритм каждый раз будет заново пересчитывать значение скользящей средней с задействованием двух очень ресурсоемких операций:

Последнее действие наиболее ресурсоемкое. Для периода 10 будет выполнено десять итераций, а для периода 500 — уже пятьсот. Получается, что сложность алгоритма напрямую зависит от периода усреднения и может быть записана как O(n), где O — функция сложности.

Однако есть куда более быстрый алгоритм расчета средней в скользящем окне. Для этого достаточно знать сумму всех значений на предыдущем расчете:

SMA = (Сумма всех значений - первое значение скользящего окна + новое значение)/Период средней

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

Если последнее значение не добавляется, а лишь обновляется, алгоритм становится еще сложнее:

Дополнительную сложность вносит то, что MQL5, как и большинство системных языков программирования, имеет встроенные средства для работы лишь с базовыми типами данных — например, с массивами. Однако массивы без должной модификации плохо подходят для этой роли, ведь в самом очевидном случае необходимо организовать очередь FIFO (First In - First Out), т.е. сделать некий список, первый добавленный элемент которого удаляется при добавлении нового элемента. Массивы позволяют и удалять, и добавлять элементы. Но эти процедуры достаточно ресурсоемкие, ведь фактически при каждой такой операции идет перераспределение массива. 

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


Теория кольцевого буфера

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

Благодаря этой возможности, кольцевой буфер может быть основан на обычном массиве. Создадим класс, в основе которого будет обычный массив:

class CRingBuffer
{
private:
   double      m_array[];
};

Предположим, что наш буфер будет состоять только из трех элементов. Тогда первый элемент добавится в ячейку массива с индексом 0, второй элемент — в ячейку с индексом 1, а третий — в ячейку с индексом 2. Что произойдет, если мы добавим четвертый элемент? Очевидно, что при его добавлении должен быть удален первый элемент. Тогда самым подходящим местом для четвертого элемента будет место первого, то есть его индекс будет снова нулевым. Как же рассчитать этот индекс? Для этого воспользуемся специальной операцией 'остаток от деления'. В MQL5 эта операция обозначается специальным символом процента %. Так как нумерация начинается с нуля, наш четвертый элемент будет третьим в очереди, и его индекс размещения будет рассчитываться по формуле:

int index = 3 % total;

Здесь 'total' — общий размер буфера. В нашем примере три делится на три без остатка. Таким образом, index будет содержать остаток, равный нулю. Последующие элементы будут располагаться по тем же правилам: номер добавляемого элемента будет делиться на количество элементов в массиве. Остаток от этого деления и будет фактическим индексом в кольцевом буфере. Приведем условный расчет индексов первых 8 элементов, добавляемых в кольцевой буфер с размерностью 3:

0 % 3 = [0]
1 % 3 = [1]
2 % 3 = [2]
3 % 3 = [0]
4 % 3 = [1]
5 % 3 = [2]
6 % 3 = [0]
7 % 3 = [1]

...


Рабочий прототип

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

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

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

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

Наибольшую сложность в этом алгоритме представляет расчет реальных индексов внутреннего буфера, в котором и будут содержаться реальные значения. Так, например, если пользователь запросит элемент с индексом 0, то фактическое значение, в котором располагается этот элемент, может быть другим. Например, при добавлении 17 элемента в кольцевой буфер с размерностью 10 нулевой элемент будет располагаться по индексу 8, а последний, девятый элемент — по индексу 7. 

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

//+------------------------------------------------------------------+
//| Кольцевой буфер Double                                           |
//+------------------------------------------------------------------+
class CRiBuffDbl
{
private:
   bool           m_full_buff;
   int            m_max_total;
   int            m_head_index;
protected:
   double         m_buffer[];                //Кольцевой буфер для прямого доступа. Внимание: индексы не соответствуют их порядковому номеру!
   ...
   int            ToRealInd(int index);
public:
                  CRiBuffDbl(void);
   void           AddValue(double value);
   void           ChangeValue(int index, double new_value);
   double         GetValue(int index);
   int            GetTotal(void);
   int            GetMaxTotal(void);
   void           SetMaxTotal(int max_total);
   void           ToArray(double& array[]);
};
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CRiBuffDbl::CRiBuffDbl(void) : m_full_buff(false),
                                 m_head_index(-1),
                                 m_max_total(0)
{
   SetMaxTotal(3);
}
//+------------------------------------------------------------------+
//| Устанавливает новый размер кольцевого буфера                     |
//+------------------------------------------------------------------+
void CRiBuffDbl::SetMaxTotal(int max_total)
{
   if(ArraySize(m_buffer) == max_total)
      return;
   m_max_total = ArrayResize(m_buffer, max_total);
}
//+------------------------------------------------------------------+
//| Возвращает фактический размер кольцевого буфера                  |
//+------------------------------------------------------------------+
int CRiBuffDbl::GetMaxTotal(void)
{
   return m_max_total;
}
//+------------------------------------------------------------------+
//| Возвращает значение по индексу                                   |
//+------------------------------------------------------------------+
double CRiBuffDbl::GetValue(int index)
{
   return m_buffer[ToRealInd(index)];
}
//+------------------------------------------------------------------+
//| Возвращает общее количество элементов                            |
//+------------------------------------------------------------------+
int CRiBuffDbl::GetTotal(void)
{
   if(m_full_buff)
      return m_max_total;
   return m_head_index+1;
}
//+------------------------------------------------------------------+
//| Добавляет новое значение в кольцевой буфер                       |
//+------------------------------------------------------------------+
void CRiBuffDbl::AddValue(double value)
{
   if(++m_head_index == m_max_total)
   {
      m_head_index = 0;
      m_full_buff = true;
   }  
   //...
   m_buffer[m_head_index] = value;
}
//+------------------------------------------------------------------+
//| Изменяет ранее добавленное значение на новое                     |
//+------------------------------------------------------------------+
void CRiBuffDbl::ChangeValue(int index, double value)
{
   int r_index = ToRealInd(index);
   double prev_value = m_buffer[r_index];
   m_buffer[r_index] = value;
}
//+------------------------------------------------------------------+
//| Преобразует виртуальный индекс в реальный                        |
//+------------------------------------------------------------------+
int CRiBuffDbl::ToRealInd(int index)
{
   if(index >= GetTotal() || index < 0)
      return m_max_total;
   if(!m_full_buff)
      return index;
   int delta = (m_max_total-1) - m_head_index;
   if(index < delta)
      return m_max_total + (index - delta);
   return index - delta;
}

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

Н‌аиболее сложная функция кольцевого буфера — внутренний метод ToRealInd. Он принимает на вход индекс буфера с точки зрения пользователя, а возвращает реальный индекс массива, по которому размещен необходимый элемент.

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

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

//+------------------------------------------------------------------+
//| Кольцевой буфер Double                                           |
//+------------------------------------------------------------------+
class CRiBuffDbl
{
private:
   ...
protected:
   virtual void   OnAddValue(double value);
   virtual void   OnRemoveValue(double value);
   virtual void   OnChangeValue(int index, double prev_value, double new_value);
   virtual void   OnChangeArray(void);
   virtual void   OnSetMaxTotal(int max_total);
};

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

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

  1. добавление нового элемента (OnAddValue);
  2. удаление старого элемента (OnRemoveValue);
  3. изменение элемента по произвольному индексу (OnChangeValue);
  4. изменение всего содержимого кольцевого буфера (OnChangeArray);
  5. изменение максимального количества элементов в кольцевом буфере (OnSetMaxTotal).

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

К‌ласс кольцевого буфера называется CRiBuffDbl. Как видно из названия, он работает со значениями типа double. Вещественные числа — наиболее распространенный тип данных для вычислительных алгоритмов. Однако, помимо вещественных чисел, может потребоваться работа с целыми числами, поэтому комплект классов, помимо класса CRiBuffDbl, содержит аналогичный класс CRiBuffInt, работающий с числами типа integer. На современных процессорах, целочисленная арифметика выполняется значительно быстрей, чем расчеты с плавающей точкой. Поэтому для специализированных целочисленных задач лучше использовать CRiBuffInt.

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


П‌ример расчета простой скользящей средней в кольцевом буфере

Мы подробно рассмотрели устройство классов, реализующих принцип кольцевого буфера, и теперь настало время решить несколько практических задач с их помощью. Начнем с самого простого: создадим всем хорошо знакомый индикатор Simple Moving Average. Это обычная скользящая средняя, а значит, для ее расчета потребуется сумму ряда разделить на период средней. Повторим формулу расчета, приведенную в начале статьи:

SMA = (Сумма всех значений - первое значение скользящего окна + новое значение)/Период средней

Для реализации нашего алгоритма потребуется переопределить два метода в классе-наследнике от CRiBuffDbl: OnAddValue и OnRemoveValue. Среднее значение будет рассчитываться в методе Sma. Приведем код получившегося класса:

//+------------------------------------------------------------------+
//|                                                   RingBuffer.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "RiBuffDbl.mqh"
//+------------------------------------------------------------------+
//| Расчет скользящей средней в кольцевом буфере                     |
//+------------------------------------------------------------------+
class CRiSMA : public CRiBuffDbl
{
private:
   double        m_sum;
protected:
   virtual void  OnAddValue(double value);
   virtual void  OnRemoveValue(double value);
   virtual void  OnChangeValue(int index, double del_value, double new_value);
public:
                 CRiSMA(void);
   
   double        SMA(void);
};

CRiSMA::CRiSMA(void) : m_sum(0.0)
{
}
//+------------------------------------------------------------------+
//| Увеличиваем общую сумму                                          |
//+------------------------------------------------------------------+
void CRiSMA::OnAddValue(double value)
{
   m_sum += value;
}
//+------------------------------------------------------------------+
//| Уменьшаем общую сумму                                            |
//+------------------------------------------------------------------+
void CRiSMA::OnRemoveValue(double value)
{
   m_sum -= value;
}
//+------------------------------------------------------------------+
//| Изменяем общую сумму                                             |
//+------------------------------------------------------------------+
void CRiSMA::OnChangeValue(int index,double del_value,double new_value)
{
   m_sum -= del_value;
   m_sum += new_value;
}
//+------------------------------------------------------------------+
//| Возвращает простую скользящую среднюю                            |
//+------------------------------------------------------------------+
double CRiSMA::SMA(void)
{
   return m_sum/GetTotal();
}

Помимо методов, реагирующих на добавление и удаление элемента (OnAddValue и OnRemoveValue соответственно), нам потребовалось переопределить еще один метод, вызывающийся при изменении произвольного элемента (OnChangeValue). Кольцевой буфер поддерживает произвольное изменение любого элемента, входящего в его состав, поэтому такое изменение необходимо отслеживать. Как правило, изменению подвергается лишь самый последний элемент, в режиме формирования последнего бара. Именно на этот случай предусмотрено событие OnChangeValue которое и требуется переопределить.

Напишем пользовательский индикатор, использующий класс кольцевого буфера для расчета скользящей средней:

//+------------------------------------------------------------------+
//|                                                        RiEma.mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#include <RingBuffer\RiSMA.mqh>

input int MaPeriod = 13;
double buff[];
CRiSMA Sma;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0, buff, INDICATOR_DATA);
   Sma.SetMaxTotal(MaPeriod);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
//---
   bool calc = false;
   for(int i = prev_calculated; i < rates_total; i++)
   {
      Sma.AddValue(price[i]);
      buff[i] = Sma.SMA();
      calc = true;
   }
   if(!calc)
   {
      Sma.ChangeValue(MaPeriod-1, price[rates_total-1]);
      buff[rates_total-1] = Sma.SMA();
   }
   return(rates_total-1);
}
//+------------------------------------------------------------------+

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

Графическое отображение самого индикатора эквивалентно одноименному стандартному индикатору MovingAverage:

 

Рис. 1. Отображение простой скользящей средней, рассчитанной в кольцевом буфере.


П‌ример расчета экспоненциальной скользящей средней в кольцевом буфере

Возьмем пример посложнее: расчет экспоненциальной скользящей средней. В отличие от простой средней, экспоненциальная средняя не реагирует на удаление наиболее старого элемента из буфера значений, поэтому для ее расчета нужно переопределить лишь два метода: OnAddValue и OnChangeValue. Как и в предыдущем примере, создадим класс CRiEMA, наследник от CRiBuffDbl и переопределим соответствующие методы:

//+------------------------------------------------------------------+
//|                                                   RingBuffer.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "RiBuffDbl.mqh"
//+------------------------------------------------------------------+
//| Расчет экспоненциальной скользящей средней в кольцевом буфере    |
//+------------------------------------------------------------------+
class CRiEMA : public CRiBuffDbl
{
private:
   double        m_prev_ema;        // Предыдущее значение EMA
   double        m_last_value;      // Последнее значение цены
   double        m_smoth_factor;    // Фактор сглаживания
   bool          m_calc_first_v;    // Флаг, указывающий на расчет первого значения
   double        CalcEma();         // Непосредственный расчет средней
protected:
   virtual void  OnAddValue(double value);
   virtual void  OnChangeValue(int index, double del_value, double new_value);
   virtual void  OnSetMaxTotal(int max_total);
public:
                 CRiEMA(void);
   double        EMA(void);
};
//+------------------------------------------------------------------+
//| Подписываемся на уведомления о добавлении и изменении значения   |
//+------------------------------------------------------------------+
CRiEMA::CRiEMA(void) : m_prev_ema(EMPTY_VALUE), m_last_value(EMPTY_VALUE),
                                                m_calc_first_v(false)
{
}
//+------------------------------------------------------------------+
//| Расчет сглаживающего фактора, согласно формуле MetaQuotes EMA    |
//+------------------------------------------------------------------+
void CRiEMA::OnSetMaxTotal(int max_total)
{
   m_smoth_factor = 2.0/(1.0+max_total);
}
//+------------------------------------------------------------------+
//| Увеличиваем общую сумму                                          |
//+------------------------------------------------------------------+
void CRiEMA::OnAddValue(double value)
{
   //Рассчитываем предыдущее значение EMA
   if(m_prev_ema != EMPTY_VALUE)
      m_prev_ema = CalcEma();
   //Запоминаем текущую цену
   m_last_value = value;
}
//+------------------------------------------------------------------+
//| Корректируем EMA                                                 |
//+------------------------------------------------------------------+
void CRiEMA::OnChangeValue(int index,double del_value,double new_value)
{
   if(index != GetMaxTotal()-1)
      return;
   m_last_value = new_value;
}
//+------------------------------------------------------------------+
//| Непосредственный расчет EMA                                      |
//+------------------------------------------------------------------+
double CRiEMA::CalcEma(void)
{
   return m_last_value*m_smoth_factor+m_prev_ema*(1.0-m_smoth_factor);
}
//+------------------------------------------------------------------+
//| Возвращает простую скользящую среднюю                            |
//+------------------------------------------------------------------+
double CRiEMA::EMA(void)
{
   if(m_calc_first_v)
      return CalcEma();
   else
   {
      m_prev_ema = m_last_value;
      m_calc_first_v = true;
   }
   return m_prev_ema;
}

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

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

‌Рис. 2. Расчет экспоненциальной скользящей средней, рассчитанной в кольцевом буфере


Расчет максимумов/минимумов в кольцевом буфере

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

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

Р‌ис. 3. Иерархия экстремумов баров

Первый бар всегда имеет нулевой экстремум, т.к. нет предыдущих значений для проверки. Бар №2 выше него, поэтому его индекс экстремума будет равен единице. Третий бар выше предыдущего, а значит — и выше первого бара. Его число экстремума равно двум. За ним идут три бара, каждый из которых ниже предыдущего. Все они ниже бара № 3, поэтому их числа экстремума равны нулю. Далее идет седьмой бар, который выше трех предыдущих, но ниже четвертого, поэтому его индекс экстремума будет равен трем. Аналогичным образом, для каждого нового бара вычисляется его индекс экстремума при добавлении.

К‌огда все предыдущие индексы рассчитаны, очень легко высчитать экстремум текущего бара. Для этого достаточно сравнить экстремум бара с другими экстремумами. К каждому последующему экстремуму можно обращаться напрямую, перепрыгивая через несколько баров подряд, ведь благодаря расставленным числам, мы знаем  его индекс. Проиллюстрируем сказанное:

Р‌ис. 4. Поиск экстремума текущего бара

П‌редставим, что мы добавляем бар, выделенный красным. Это бар с номером 9, т.к. нумерация происходит с нуля. Для определения его индекса экстремума сравним его с баром №8, выполнив шаг I: он оказался выше его, поэтому его экстремум как минимум равен единице. Сравним его с баром №7, выполнив шаг II — он снова выше этого бара. Т.к. бар №7 выше четырех предыдущих, то мы можем сразу сравнить наш последний бар с баром №3, выполнив шаг III. Бар №9 выше бара №3 и, следовательно, выше всех баров на текущий момент. Благодаря ранее расчитанным индексам, мы избежали сравнения с четырьмя промежуточными барами, которые заведомо ниже текущего. Именно так работает быстрый поиск экстремума в кольцевом буфере. Аналогичным способом работает и поиск минимума, с той лишь разницей, что используется дополнительный индекс минимумов.

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

//+------------------------------------------------------------------+
//|                                                   RingBuffer.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "RiBuffDbl.mqh"
#include "RiBuffInt.mqh"
//+------------------------------------------------------------------+
//| Расчет экспоненциальной скользящей средней в кольцевом буфере   |
//+------------------------------------------------------------------+
class CRiMaxMin : public CRiBuffDbl
{
private:
   CRiBuffInt    m_max;
   CRiBuffInt    m_min;
   bool          m_full;
   int           m_max_ind;
   int           m_min_ind;
protected:
   virtual void  OnAddValue(double value);
   virtual void  OnCalcValue(int index);
   virtual void  OnChangeValue(int index, double del_value, double new_value);
   virtual void  OnSetMaxTotal(int max_total);
public:
                 CRiMaxMin(void);
   int           MaxIndex(int max_period = 0);
   int           MinIndex(int min_period = 0);
   double        MaxValue(int max_period = 0);
   double        MinValue(int min_period = 0);
   void          GetMaxIndexes(int& array[]);
   void          GetMinIndexes(int& array[]);
};

CRiMaxMin::CRiMaxMin(void)
{
   m_full = false;
   m_max_ind = 0;
   m_min_ind = 0;
}
void CRiMaxMin::GetMaxIndexes(int& array[])
{
   m_max.ToArray(array);
}
void CRiMaxMin::GetMinIndexes(int& array[])
{
   m_min.ToArray(array);
}
//+------------------------------------------------------------------+
//| Изменяем размер внутренних буферов согласно новому размеру       |
//| основного буфера                                                 |
//+------------------------------------------------------------------+
void CRiMaxMin::OnSetMaxTotal(int max_total)
{
   m_max.SetMaxTotal(max_total);
   m_min.SetMaxTotal(max_total);
}
//+------------------------------------------------------------------+
//| Расчет индексов Max/Min                                          |
//+------------------------------------------------------------------+
void CRiMaxMin::OnAddValue(double value)
{
   m_max_ind--;
   m_min_ind--;
   int last = GetTotal()-1;
   if(m_max_ind > 0 && value >= GetValue(m_max_ind))
      m_max_ind = last;
   if(m_min_ind > 0 && value <= GetValue(m_min_ind))
      m_min_ind = last;
   OnCalcValue(last);
}
//+------------------------------------------------------------------+
//| Расчет индексов Max/Min                                          |
//+------------------------------------------------------------------+
void CRiMaxMin::OnCalcValue(int index)
{
   int max = 0, min = 0;
   int offset = m_full ? 1 : 0;
   double value = GetValue(index);
   int p_ind = index-1;
   //Поиск максимума
   while(p_ind >= 0 && value >= GetValue(p_ind))
   {
      int extr = m_max.GetValue(p_ind+offset);
      max += extr + 1;
      p_ind = GetTotal() - 1 - max - 1;
   }
   p_ind = GetTotal()-2;
   //Поиск минимума
   while(p_ind >= 0 && value <= GetValue(p_ind))
   {
      int extr = m_min.GetValue(p_ind+offset);
      min += extr + 1;
      p_ind = GetTotal() - 1 - min - 1;
   }
   m_max.AddValue(max);
   m_min.AddValue(min);
   if(!m_full && GetTotal() == GetMaxTotal())
      m_full = true;
}
//+------------------------------------------------------------------+
//| Пересчитывает индексы максимумов/минимумов вслед за изменением   |
//| значения по произвольному индексу                                |
//+------------------------------------------------------------------+
void CRiMaxMin::OnChangeValue(int index, double del_value, double new_value)
{
   if(m_max_ind >= 0 && new_value >= GetValue(m_max_ind))
      m_max_ind = index;
   if(m_min_ind >= 0 && new_value >= GetValue(m_min_ind))
      m_min_ind = index;
   for(int i = index; i < GetTotal(); i++)
      OnCalcValue(i);
}
//+------------------------------------------------------------------+
//| Возвращает индекс максимального элемента                         |
//+------------------------------------------------------------------+
int CRiMaxMin::MaxIndex(int max_period = 0)
{
   int limit = 0;
   if(max_period > 0 && max_period <= m_max.GetTotal())
   {
      m_max_ind = -1;
      limit = m_max.GetTotal() - max_period;
   }
   if(m_max_ind >=0)
      return m_max_ind;
   int c_max = m_max.GetTotal()-1;
   while(c_max > limit)
   {
      int ext = m_max.GetValue(c_max);
      if((c_max - ext) <= limit)
         return c_max;
      c_max = c_max - ext - 1;
   }
   return limit;
}
//+------------------------------------------------------------------+
//| Возвращает индекс минимального элемента                          |
//+------------------------------------------------------------------+
int CRiMaxMin::MinIndex(int min_period = 0)
{
   int limit = 0;
   if(min_period > 0 && min_period <= m_min.GetTotal())
   {
      limit = m_min.GetTotal() - min_period;
      m_min_ind = -1;
   }
   if(m_min_ind >=0)
      return m_min_ind;
   int c_min = m_min.GetTotal()-1;
   while(c_min > limit)
   {
      int ext = m_min.GetValue(c_min);
      if((c_min - ext) <= limit)
         return c_min;
      c_min = c_min - ext - 1;
   }
   return limit;
}
//+------------------------------------------------------------------+
//| Возвращает значение максимального элемента                       |
//+------------------------------------------------------------------+
double CRiMaxMin::MaxValue(int max_period = 0)
{
   return GetValue(MaxIndex(max_period));
}
//+------------------------------------------------------------------+
//| Возвращает значение минимального элемента                        |
//+------------------------------------------------------------------+
double CRiMaxMin::MinValue(int min_period = 0)
{
   return GetValue(MinIndex(min_period));
}

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

Вот так выглядит отображение максимумов и минимумов на графике:

Р‌ис. 5. Канал максимумов/минимумов в виде индикатора

Д‌обавим, что класс определения максимума/минимума имеет расширенные возможности. Он может вернуть индекс экстремума в кольцевом буфере или просто его значение. Также он способен рассчитать экстремум за период, меньший, чем период кольцевого буфера, для чего достаточно указать лимитирующий период в методах MaxIndex/MinIndex и MaxValue/MinValue.


Интегрирование кольцевого буфера с библиотекой AlgLib

Еще один интересный пример использования кольцевого буфера — специализированные математические расчеты. Как правило, алгоритмы для расчета разнообразных статистик создаются без учета использования в скользящем окне. Такой алгоритм не всегда бывает удобно использовать. Кольцевой буфер решает эту проблему. Напишем индикатор, рассчитывающий основные характеристики Гауссова распределения:

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

//+------------------------------------------------------------------+
//|                                                        RiEma.mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#include <RingBuffer\RiBuffDbl.mqh>
#include <Math\AlgLib\AlgLib.mqh>
 
//+------------------------------------------------------------------+
//| Расчет основных характеристик распределения Гаусса              |
//+------------------------------------------------------------------+
class CRiGaussProperty : public CRiBuffDbl
{
private:
   double        m_mean;      // Среднее
   double        m_variance;  // Отклонение
   double        m_skewness;  // Асимметричность
   double        m_kurtosis;  // Эксцесс
protected:
   virtual void  OnChangeArray(void);
public:
   double        Mean(void){ return m_mean;}
   double        StdDev(void){return MathSqrt(m_variance);}
   double        Skewness(void){return m_skewness;}
   double        Kurtosis(void){return m_kurtosis;}
};
//+------------------------------------------------------------------+
//| Расчет производится при любом изменении массива              |
//+------------------------------------------------------------------+
void CRiGaussProperty::OnChangeArray(void)
{
   double array[];
   ToArray(array);
   CAlglib::SampleMoments(array, m_mean, m_variance, m_skewness, m_kurtosis);
}
//+------------------------------------------------------------------+
//| Тип свойства гауссова распределения                            |
//+------------------------------------------------------------------+
enum ENUM_GAUSS_PROPERTY
{
   GAUSS_MEAN,       // Среднее
   GAUSS_STDDEV,     // Отклонение
   GAUSS_SKEWNESS,   // Асимметричность
   GAUSS_KURTOSIS    // Эксцесс
};
 
input int                  BPeriod = 13;       //Period
input ENUM_GAUSS_PROPERTY  Property;

double buff[];
CRiGaussProperty RiGauss;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0, buff, INDICATOR_DATA);
   RiGauss.SetMaxTotal(BPeriod);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
//---
   bool calc = false;
   for(int i = prev_calculated; i < rates_total; i++)
   {
      RiGauss.AddValue(price[i]);
      buff[i] = GetGaussValue(Property);
      calc = true;
   }
   if(!calc)
   {
      RiGauss.ChangeValue(BPeriod-1, price[rates_total-1]);
      buff[rates_total-1] = GetGaussValue(Property);
   }
   return(rates_total-1);
}
//+------------------------------------------------------------------+
//| Возвращает значение одного из свойств гауссова распределения   |
//+------------------------------------------------------------------+
double GetGaussValue(ENUM_GAUSS_PROPERTY property)
{
   double value = EMPTY_VALUE;
   switch(Property)
   {
      case GAUSS_MEAN:
         value = RiGauss.Mean();
         break;
      case GAUSS_STDDEV:
         value = RiGauss.StdDev();
         break;
      case GAUSS_SKEWNESS:
         value = RiGauss.Skewness();
         break;
      case GAUSS_KURTOSIS:
         value = RiGauss.Kurtosis();
         break;    
   }
   return value;
}


К‌ак видно из приведенного выше листинга, класс CRiGaussProperty получился элементарным. Однако за этой простотой скрыта большая функциональность. Теперь для работы функции CAlglib::SampleMoments не требуется подготавливать скользящий массив на каждой итерации, а достаточно просто добавлять новые значения в методе AddValue. На рисунке ниже приведен результат работы данного индикатора. В настройках выберем расчет стандартного отклонения и отобразим его в подокне графика:

Р‌ис. 6. Основные характеристики гауссова распределения в виде скользящего индикатора

 


Построение MACD на основе кольцевых примитивов

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

В‌ примере с индикатором максимумов/минимумов мы уже использовали два дополнительных кольцевых буфера в составе класса CRiMaxMin. Поступим так же и в случае с MACD. Наш класс при добавлении нового значения будет просто пересылать его в свои дополнительные буферы, а затем рассчитывать простую разницу между ними. Разница будет попадать в третий кольцевой буфер, рассчитывающий простую SMA на ее основе. Это и будет сигнальная линия MACD:

//+------------------------------------------------------------------+
//|                                                   RingBuffer.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "RiBuffDbl.mqh"
#include "RiSMA.mqh"
#include "RiEMA.mqh"
//+------------------------------------------------------------------+
//| Расчет скользящей средней в кольцевом буфере                     |
//+------------------------------------------------------------------+
class CRiMACD
{
private:
   CRiEMA        m_slow_macd;    // Быстрая экспоненциальная скользящая средняя
   CRiEMA        m_fast_macd;    // Медленная экспоненциальная скользящая средняя
   CRiSMA        m_signal_macd;  // Сигнальная линия
   double        m_delta;        // Разница между быстрой и медленной EMA
public:
   double        Macd(void);
   double        Signal(void);
   void          ChangeLast(double new_value);
   void          SetFastPeriod(int period);
   void          SetSlowPeriod(int period);
   void          SetSignalPeriod(int period);
   void          AddValue(double value);
};
//+------------------------------------------------------------------+
//| Пересчитывает MACD                                               |
//+------------------------------------------------------------------+
void CRiMACD::AddValue(double value)
{
   m_slow_macd.AddValue(value);
   m_fast_macd.AddValue(value);
   m_delta = m_slow_macd.EMA() - m_fast_macd.EMA();
   m_signal_macd.AddValue(m_delta);
}

//+------------------------------------------------------------------+
//| Изменяет MACD                                                    |
//+------------------------------------------------------------------+
void CRiMACD::ChangeLast(double new_value)
{
   m_slow_macd.ChangeValue(m_slow_macd.GetTotal()-1, new_value);
   m_fast_macd.ChangeValue(m_fast_macd.GetMaxTotal()-1, new_value);
   m_delta = m_slow_macd.EMA() - m_fast_macd.EMA();
   m_signal_macd.ChangeValue(m_slow_macd.GetTotal()-1, m_delta);
}
//+------------------------------------------------------------------+
//| Возвращает гистограмму MACD                                      |
//+------------------------------------------------------------------+
double CRiMACD::Macd(void)
{
   return m_delta;
}
//+------------------------------------------------------------------+
//| Возвращает сигнальную линию                                      |
//+------------------------------------------------------------------+
double CRiMACD::Signal(void)
{
   return m_signal_macd.SMA();
}
//+------------------------------------------------------------------+
//| Устанавливает быстрый период                                     |
//+------------------------------------------------------------------+
void CRiMACD::SetFastPeriod(int period)
{
   m_slow_macd.SetMaxTotal(period);
}
//+------------------------------------------------------------------+
//| Устанавливает медленный период                                   |
//+------------------------------------------------------------------+
void CRiMACD::SetSlowPeriod(int period)
{
   m_fast_macd.SetMaxTotal(period);
}
//+------------------------------------------------------------------+
//| Устанавливает период сигнальной линии                            |
//+------------------------------------------------------------------+
void CRiMACD::SetSignalPeriod(int period)
{
   m_signal_macd.SetMaxTotal(period);
}

Обратите внимание, что сам класс CRiMacd — независимый класс, он не наследуется от CRiBuffDbl. Действительно, класс CRiMacd не использует собственные расчетные буферы. Вместо этого в классе применяется схема "включения", когда классы кольцевых примитивов размещаются как независимые объекты в секции private.

Два основных метода Macd() и Signal() возвращают значения индикатора MACD и его сигнальной линии. Приведенный код получился простым, при этом каждый буфер имеет свой скользящий период. Класс CRiMacd не отслеживает изменения произвольного элемента. Вместо этого он отслеживает изменение только последнего элемента, предоставляя изменение индикатора на нулевом баре.

Визуально индикатор MACD, рассчитанный в кольцевом буфере, выглядит так же, как и классический индикатор:

Р‌ис. 7. Индикатор MACD, рассчитанный в кольцевом буфере

Построение индикатора Stochastic на основе кольцевых примитивов

Аналогичным образом построим индикатор Stochastic. Этот индикатор объединяет в себе поиск экстремумов и расчет скользящей средней. Таким образом, здесь мы используем алгоритмы, которые мы уже рассчитали ранее.

Stochastic использует три ценовых ряда: цены максимумов (High баров), цены минимумов (Low баров) и цены закрытия (Close баров). Его расчет прост: вначале ищется максимум для цен High и минимум для цен Low. Затем рассчитывается отношение текущей цены close к диапазону между максимумом и минимумом. И наконец, на основе этого отношения рассчитывается среднее значение за N периодов (в индикаторе N носит название "замедление K%"):

K% = SMA((close-min)/((max-min)*100.0%), N)

Далее для получившегося K% рассчитывается еще одна средняя с периодом %D — это сигнальная линия, наподобие сигнальной линии MACD:

Signal D% = SMA(K%, D%)

Получившиеся два значения — K% и его сигнальная D% — и будут отображать индикатор Stochastic.

Прежде чем написать код Стохастика для кольцевого буфера, приведем его код, исполненный в классической манере, для чего воспользуемся готовым примером Stochastic.mq5 из папки Indicators\Examples:

//+------------------------------------------------------------------+
//| Stochastic Oscillator                                            |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   int i,k,start;
//--- check for bars count
   if(rates_total<=InpKPeriod+InpDPeriod+InpSlowing)
      return(0);
//---
   start=InpKPeriod-1;
   if(start+1<prev_calculated) start=prev_calculated-2;
   else
     {
      for(i=0;i<start;i++)
        {
         ExtLowesBuffer[i]=0.0;
         ExtHighesBuffer[i]=0.0;
        }
     }
//--- calculate HighesBuffer[] and ExtHighesBuffer[]
   for(i=start;i<rates_total && !IsStopped();i++)
     {
      double dmin=1000000.0;
      double dmax=-1000000.0;
      for(k=i-InpKPeriod+1;k<=i;k++)
        {
         if(dmin>low[k])  dmin=low[k];
         if(dmax<high[k]) dmax=high[k];
        }
      ExtLowesBuffer[i]=dmin;
      ExtHighesBuffer[i]=dmax;
     }
//--- %K
   start=InpKPeriod-1+InpSlowing-1;
   if(start+1<prev_calculated) start=prev_calculated-2;
   else
     {
      for(i=0;i<start;i++) ExtMainBuffer[i]=0.0;
     }
//--- main cycle
   for(i=start;i<rates_total && !IsStopped();i++)
     {
      double sumlow=0.0;
      double sumhigh=0.0;
      for(k=(i-InpSlowing+1);k<=i;k++)
        {
         sumlow +=(close[k]-ExtLowesBuffer[k]);
         sumhigh+=(ExtHighesBuffer[k]-ExtLowesBuffer[k]);
        }
      if(sumhigh==0.0) ExtMainBuffer[i]=100.0;
      else             ExtMainBuffer[i]=sumlow/sumhigh*100;
     }
//--- signal
   start=InpDPeriod-1;
   if(start+1<prev_calculated) start=prev_calculated-2;
   else
     {
      for(i=0;i<start;i++) ExtSignalBuffer[i]=0.0;
     }
   for(i=start;i<rates_total && !IsStopped();i++)
     {
      double sum=0.0;
      for(k=0;k<InpDPeriod;k++) sum+=ExtMainBuffer[i-k];
      ExtSignalBuffer[i]=sum/InpDPeriod;
     }
//--- OnCalculate done. Return new prev_calculated.
   return(rates_total);
  }
//+------------------------------------------------------------------+

Этот код написан в едином блоке и содержит 8 циклов for, три из которых вложенные. Расчет выполняется в два захода: сначала рассчитываются максимумы и минимумы, значения которых запоминаются в двух дополнительных буферах. Сам расчет максимумов и минимумов требует двойного перебора: на каждом баре делаются дополнительные N итераций во вложенном цикле for, где N - период K%.

После расчета максимумов/минимумов делается расчет K%, для чего также  используется двойной цикл, делающий на каждом баре дополнительные F итераций, где F — период замедления K%. 

Затем рассчитывается сигнальная линия D%, и тоже с двойным перебором for, где для каждого бара требуются дополнительные T итераций (T — период усреднения D%).

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

Чтобы продемонстрировать сказанное, приведем содержимое основного расчетного метода в классе CRiStoch. Он выполняет ровно ту же работу, что и код, размещенный выше:

//+------------------------------------------------------------------+
//| Добавления новых значений и расчет Stochastic                    |
//+------------------------------------------------------------------+
void CRiStoch::AddValue(double close, double high, double low)
{
   m_max.AddValue(high);                     // Добавляем новое значение максимума
   m_min.AddValue(low);                      // Добавляем новое значение минимума
   double c = close;
   double max = m_max.MaxValue()             // Получаем максимум
   double min = m_min.MinValue();            // Получаем минимум
   double delta = max - min;
   double k = 0.0;
   if(delta != 0.0)
      k = (c-min)/delta*100.0;               // Находим K% по формуле Stochastic
   m_slowed_k.AddValue(k);                   // Сглаживаем K% (Замедление K%)
   m_slowed_d.AddValue(m_slowed_k.SMA());    // Находим %D от сглаженного K%
}

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

Остальные методы класса CRiStoch тривиальны и являются Get/Set-методами установки периодов и соответствующих значений индикатора. Приведем код CRiStoch полностью:

//+------------------------------------------------------------------+
//|                                                   RingBuffer.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include "RiBuffDbl.mqh"
#include "RiSMA.mqh"
#include "RiMaxMin.mqh"
//+------------------------------------------------------------------+
//| Класс индикатора Stochastic                                      |
//+------------------------------------------------------------------+
class CRiStoch
{
private:
   CRiMaxMin     m_max;          // Индикатор минимумов/максимумов
   CRiMaxMin     m_min;          // Индикатор минимумов/максимумов
   CRiSMA        m_slowed_k;     // Усреднение K%
   CRiSMA        m_slowed_d;     // Скользящая средняя D%
public:
   void          ChangeLast(double new_value);
   void          AddValue(double close, double high, double low);
   void          AddHighValue(double value);
   void          AddLowValue(double value);
   void          AddCloseValue(double value);
   void          SetPeriodK(int period);
   void          SetPeriodD(int period);
   void          SetSlowedPeriodK(int period);
   double        GetStochK(void);
   double        GetStochD(void);
};
//+------------------------------------------------------------------+
//| Добавления новых значений и расчет Stochastic                    |
//+------------------------------------------------------------------+
void CRiStoch::AddValue(double close, double high, double low)
{
   m_max.AddValue(high);                     // Добавляем новое значение максимума
   m_min.AddValue(low);                      // Добавляем новое значение минимума
   double c = close;
   double max = m_max.MaxValue()
   double min = m_min.MinValue();
   double delta = max - min;
   double k = 0.0;
   if(delta != 0.0)
      k = (c-min)/delta*100.0;               // Находим K% по формуле
   m_slowed_k.AddValue(k);                   // Сглаживаем K% (Замедление K%)
   m_slowed_d.AddValue(m_slowed_k.SMA());    // Находим %D от сглажинного K%
}
//+------------------------------------------------------------------+
//| Устанавливает быстрый период                                     |
//+------------------------------------------------------------------+
void CRiStoch::SetPeriodK(int period)
{
   m_max.SetMaxTotal(period);
   m_min.SetMaxTotal(period);
}
//+------------------------------------------------------------------+
//| Устанавливает медленный период                                   |
//+------------------------------------------------------------------+
void CRiStoch::SetSlowedPeriodK(int period)
{  
   m_slowed_k.SetMaxTotal(period);
}
//+------------------------------------------------------------------+
//| Устанавливает период сигнальной линии                            |
//+------------------------------------------------------------------+
void CRiStoch::SetPeriodD(int period)
{  
   m_slowed_d.SetMaxTotal(period);
}
//+------------------------------------------------------------------+
//| Получает значение %K                                             |
//+------------------------------------------------------------------+
double CRiStoch::GetStochK(void)
{
   return m_slowed_k.SMA();
}
//+------------------------------------------------------------------+
//| Получает значение %D                                             |
//+------------------------------------------------------------------+
double CRiStoch::GetStochD(void)
{
   return m_slowed_d.SMA();
}

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

Рис. 8 Стандартный и кольцевой индикаторы Stochastic.


Оптимизация использования оперативной памяти

Расчет индикаторов требует определенных ресурсов. Работа с системными индикаторами через так называемые хэндлы — не исключение. По сути, хэндл индикатора — некий особый тип указателя на внутренний расчетный блок индикатора и его буферы данных. Сам по себе хэндл не занимает ощутимого места, это всего лишь 64-разрядное число. Основной размер спрятан "за кулисами" MetaTrader, поэтому когда создается новый хэндл, выделяется определенный объем памяти, больший, чем размер хэндла.

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

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

Рис. 8 Информационная панель, использующая множество индикаторов


Мы видим, что здесь анализируются 17 различных инструментов по 9 различным показателям. Каждый такой показатель представлен своим индикатором. Нетрудно подсчитать, что потребуется 17 * 9 = 153 индикатора только для того, чтобы отобразить "всего несколько значков". Для анализа всех 21 таймфрейма по каждому символу понадобится уже 3213 индикаторов. Чтобы их все разместить, потребуется огромный объем памяти.

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

  1. вызов стандартного индикатора и копирование его значений через получившийся хэндл;
  2. расчет индикатора в кольцевом буфере.

Во втором случае индикаторы создаваться не будут, а все расчеты будут производиться внутри эксперта, с помощью кольцевых индикаторов, которых будет два: MACD и Stochastic. У каждого из них будет три варианта настройки: быстрая, стандартная и медленная. Индикаторы будут рассчитываться на четырех инструментах: EURUSD, GBPUSD, USDCHF и USDJPY по 21 таймфрейму. Несложно подсчитать общее количество рассчитываемых значений:

общее количество значений = 2 индикатора * 3 набора параметров * 4 инструмента * 21 таймфрейм = 504;

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

Приведем исходный код прототипа контейнера в виде абстрактного класса:

//+------------------------------------------------------------------+
//|                                                    RiIndLoad.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Arrays\ArrayObj.mqh>
#include "NewBarDetector.mqh"
//+------------------------------------------------------------------+
//| Тип создаваемого индикатора                                      |
//+------------------------------------------------------------------+
enum ENUM_INDICATOR_TYPE
{
   INDICATOR_SYSTEM,       // Системный индикатор 
   INDICATOR_RIBUFF        // Индикатор кольцевого буфера
};
//+------------------------------------------------------------------+
//| Контейнер индикатора                                             |
//+------------------------------------------------------------------+
class CIndBase : public CObject
{
protected:
   int         m_handle;               // Хендл индикатора
   string      m_symbol;               // Символ для расчета индикатора
   ENUM_INDICATOR_TYPE m_ind_type;     // Тип индикатора
   ENUM_TIMEFRAMES m_period;           // Период для расчета индикатора
   CBarDetector m_bar_detect;          // Определитель нового бара
   CIndBase(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type);
public:
   string          Symbol(void){return m_symbol;}
   ENUM_TIMEFRAMES Period(void){return m_period;}
   virtual double  GetLastValue(int index_buffer);
};
//+------------------------------------------------------------------+
//| Защищенный конструктор требует указания символа,таймфрейма и типа|
//| индикатора                                                       |
//+------------------------------------------------------------------+
CIndBase::CIndBase(string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR_TYPE ind_type)
{
   m_handle = INVALID_HANDLE;
   m_symbol = symbol;
   m_period = period;
   m_ind_type = ind_type;
   m_bar_detect.Symbol(symbol);
   m_bar_detect.Timeframe(period);
}
//+------------------------------------------------------------------+
//| Получает последнее значение индикатора                           |
//+------------------------------------------------------------------+
double CIndBase::GetLastValue(int index_buffer)
{
   return EMPTY_VALUE;
}

Он содержит виртуальный метод GetLastValue. Этот метод принимает номер буфера индикатора и возвращает последнее значение индикатора для этого буфера. Также класс содержит базовые свойства индикатора: его таймфрейм, символ и тип расчета (ENUM_INDICATOR_TYPE).

На его основе создадим два класса-потомка CRiInMacd и CRiStoch. Оба будут рассчитывать значения соответствующих индикаторов и возвращать их через переопределенный метод GetLastValue. Приведем исходный текст одного из этих классов CRiIndMacd:

//+------------------------------------------------------------------+
//|                                                    RiIndLoad.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <RingBuffer\RiMACD.mqh>
#include "RiIndBase.mqh"
//+------------------------------------------------------------------+
//| Контейнер индикатора                                             |
//+------------------------------------------------------------------+
class CIndMacd : public CIndBase
{
private:
   CRiMACD        m_macd;                 // Кольцевая версия индикатора
public:
                  CIndMacd(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type, int fast_period, int slow_period, int signal_period);
   virtual double GetLastValue(int index_buffer);
};
//+------------------------------------------------------------------+
//| Создаем индикатор MACD                                           |
//+------------------------------------------------------------------+
CIndMacd::CIndMacd(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type,
                          int fast_period,int slow_period,int signal_period) : CIndBase(symbol, period, ind_type)
{
   if(ind_type == INDICATOR_SYSTEM)
   {
      m_handle = iMACD(m_symbol, m_period, fast_period, slow_period, signal_period, PRICE_CLOSE);
      if(m_handle == INVALID_HANDLE)
         printf("Create iMACD handle failed. Symbol: " + symbol + " Period: " + EnumToString(period));
   }
   else if(ind_type == INDICATOR_RIBUFF)
   {
      m_macd.SetFastPeriod(fast_period);
      m_macd.SetSlowPeriod(slow_period);
      m_macd.SetSignalPeriod(signal_period);
   }
} 
//+------------------------------------------------------------------+
//| Получает последнее значение индикатора                           |
//+------------------------------------------------------------------+
double CIndMacd::GetLastValue(int index_buffer)
{
   if(m_handle != INVALID_HANDLE)
   {
      double array[];
      if(CopyBuffer(m_handle, index_buffer, 1, 1, array) > 0)
         return array[0];
      return EMPTY_VALUE;
   }
   else
   {
      if(m_bar_detect.IsNewBar())
      {
         //printf("Получен новый бар на " + m_symbol + " Период " + EnumToString(m_period));
         double close[];
         CopyClose(m_symbol, m_period, 1, 1, close);
         m_macd.AddValue(close[0]);
      }
      switch(index_buffer)
      {
         case 0: return m_macd.Macd();
         case 1: return m_macd.Signal();
      }
      return EMPTY_VALUE;
   }
}

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

Расчет значений индикаторов происходит только в момент открытия нового бара. Это облегчает тестирование. Для этого в базовый класс CRiIndBase встроен специальный модуль NewBarDetecter. Этот класс умеет определять открытие нового бара и сигнализировать об этом, возвращая true методом IsNewBar.

Теперь приведем код тестировочного эксперта. Он называется TestIndEA.mq5:

//+------------------------------------------------------------------+
//|                                                    TestIndEA.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "RiIndBase.mqh"
#include "RiIndMacd.mqh"
#include "RiIndStoch.mqh"
#include "NewBarDetector.mqh"
//+------------------------------------------------------------------+
//| Параметры MACD                                                   |
//+------------------------------------------------------------------+
struct CMacdParams
{
   int slow_period;
   int fast_period;
   int signal_period;
};
//+------------------------------------------------------------------+
//| Параметры Stoch                                                  |
//+------------------------------------------------------------------+
struct CStochParams
{
   int k_period;
   int k_slowed;
   int d_period;
};

input ENUM_INDICATOR_TYPE IndType = INDICATOR_SYSTEM;    // Тип индикатора

string         Symbols[] = {"EURUSD", "GBPUSD", "USDCHF", "USDJPY"};
CMacdParams    MacdParams[3];
CStochParams   StochParams[3];
CArrayObj      ArrayInd; 
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{  
   MacdParams[0].fast_period = 3;
   MacdParams[0].slow_period = 13;
   MacdParams[0].signal_period = 6;
   
   MacdParams[1].fast_period = 9;
   MacdParams[1].slow_period = 26;
   MacdParams[1].signal_period = 12;
   
   MacdParams[2].fast_period = 18;
   MacdParams[2].slow_period = 52;
   MacdParams[2].signal_period = 24;
   
   StochParams[0].k_period = 6;
   StochParams[0].k_slowed = 3;
   StochParams[0].d_period = 3;
   
   StochParams[1].k_period = 12;
   StochParams[1].k_slowed = 5;
   StochParams[1].d_period = 6;
   
   StochParams[2].k_period = 24;
   StochParams[2].k_slowed = 7;
   StochParams[2].d_period = 12;
   // Здесь создается 504 индикатора MACD и Stochastic
   for(int symbol = 0; symbol < ArraySize(Symbols); symbol++)
   {
      for(int period = 1; period <=21; period++)
      {
         for(int i = 0; i < 3; i++)
         {
            CIndMacd* macd = new CIndMacd(Symbols[symbol], PeriodByIndex(period), IndType,
                                          MacdParams[i].fast_period, MacdParams[i].slow_period,
                                          MacdParams[i].signal_period);
            CIndStoch* stoch = new CIndStoch(Symbols[symbol], PeriodByIndex(period), IndType,
                                          StochParams[i].k_period, StochParams[i].k_slowed,
                                          StochParams[i].d_period);
            ArrayInd.Add(macd);
            ArrayInd.Add(stoch);
         }
      }
   }
   printf("Create " + (string)ArrayInd.Total() + " indicators sucessfully");
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   for(int i = 0; i < ArrayInd.Total(); i++)
   {
      CIndBase* ind = ArrayInd.At(i);
      double value = ind.GetLastValue(0);
      double value_signal = ind.GetLastValue(1);
   }
}
//+------------------------------------------------------------------+
//| Возвращает таймфрейм по его индексу                              |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES PeriodByIndex(int index)
{
   switch(index)
   {
      case  0: return PERIOD_CURRENT;
      case  1: return PERIOD_M1;
      case  2: return PERIOD_M2;
      case  3: return PERIOD_M3;
      case  4: return PERIOD_M4;
      case  5: return PERIOD_M5;
      case  6: return PERIOD_M6;
      case  7: return PERIOD_M10;
      case  8: return PERIOD_M12;
      case  9: return PERIOD_M15;
      case 10: return PERIOD_M20;
      case 11: return PERIOD_M30;
      case 12: return PERIOD_H1;
      case 13: return PERIOD_H2;
      case 14: return PERIOD_H3;
      case 15: return PERIOD_H4;
      case 16: return PERIOD_H6;
      case 17: return PERIOD_H8;
      case 18: return PERIOD_H12;
      case 19: return PERIOD_D1;
      case 20: return PERIOD_W1;
      case 21: return PERIOD_MN1;
      default: return PERIOD_CURRENT;
   }
}
//+------------------------------------------------------------------+

Основной функционал выполнен в блоке OnInit. В нем перебираются символы, таймфреймы и наборы параметров для индикаторов. Сами наборы параметров индикаторов хранятся в вспомогательных структурах CMacdParams и CStochParams. 

Блок обработки значений размещен в функции OnTick и представляет собой тривиальный перебор индикаторов и получение их последних значений с помощью виртуального метода GetLastalue. Т.к. количество расчетных буферов у обоих индикаторов одинаковое, дополнительных проверок делать не требуется, и получать значения обоих индикаторов можно через обобщенный базовый метод GetLastValue.

Запуск эксперта показал следующее: в режиме расчета на основе вызовов стандартных индикаторов потребовалось 11,9 Гб оперативной памяти, в то время как запуск в режиме расчета индикаторов на основе кольцевых примитивов потребовал 2,9 Гб. Тестирование выполнялось на компьютере с 16 Гб оперативной памяти.

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

Экономия памяти в четыре раза — более чем достойный результат. Однако все равно нам потребовалось почти 3 Гб оперативной памяти. Можно ли как-то еще уменьшить эту цифру? Можно, если оптимизировать количество таймфреймов. Попробуем немного изменить тестировочный код, и вместо 21 таймфрейма будем использовать только один — PERIOD_M1. При этом количество индикаторов останется прежним, просто некоторые из них будут дублироваться:

...
for(int symbol = 0; symbol < ArraySize(Symbols); symbol++)
   {
      for(int period = 1; period <=21; period++)
      {
         for(int i = 0; i < 3; i++)
         {
            CIndMacd* macd = new CIndMacd(Symbols[symbol], PERIOD_M1, IndType,
                                          MacdParams[i].fast_period, MacdParams[i].slow_period,
                                          MacdParams[i].signal_period);
            CIndStoch* stoch = new CIndStoch(Symbols[symbol], PERIOD_M1, IndType,
                                          StochParams[i].k_period, StochParams[i].k_slowed,
                                          StochParams[i].d_period);
            ArrayInd.Add(macd);
            ArrayInd.Add(stoch);
         }
      }
   }
...

Теперь те же 504 индикатора в режиме внутреннего расчета занимают уже 548 Мб оперативной памяти. Если же использовать более точные формулировки, память занимают не столько индикаторы, сколько данные, загружаемые для их расчета. Также около 100 Мб от общего объема занимает сам терминал, поэтому фактически загружаемых данных еще меньше. Мы снова существенно снизили расход памяти:


Расчет на основе системных индикаторов в этом режиме требует 1,9 Гб памяти, что тоже существенно меньше, чем при использовании всего списка из 21 таймфреймов.


Оптимизация времени тестирования эксперта

MetaTrader 5 обладает возможностью обращаться во время тестирования сразу к нескольким торговым инструментам, а так же к произвольному таймфрейму каждого инструмента. Тем самым можно создавать и тестировать мультиэкспертов, когда один эксперт торгует сразу на нескольких инструментах. Доступ к торговому окружению может потребовать время, особенно если требуется получить доступ к данным каких-либо индикаторов, рассчитанных на этих инструментах. Время доступа можно сократить, если все расчеты выполнять внутри одного эксперта. Проиллюстрируем сказанное с помощью тестирования нашего предыдущего примера в тестере стратегий MetaTrader 5.  Для начала протестируем эксперт за последний месяц на EURUSD в режиме "только цены открытия" на M1. Для расчетов воспользуемся системными индикаторами. Данный тест на компьютере с процессором Intel Core i7 870 2.9 Ghz выполнился за 58 секунд:

2017.03.30 14:07:12.223 Core 1 EURUSD,M1: 114357 ticks, 28647 bars generated. Environment synchronized in 0:00:00.078. Test passed in 0:00:57.923.

Теперь выполним этот же тест, но в режиме внутренних расчетов:

2017.03.30 14:08:29.472 Core 1 EURUSD,M1: 114357 ticks, 28647 bars generated. Environment synchronized in 0:00:00.078. Test passed in 0:00:12.292.

Как видно, время расчета существенно сократилось. В этом режиме оно заняло 12 секунд.


Выводы и предложения по улучшению производительности

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


Заключение

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

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

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