English 中文 Español Deutsch 日本語 Português
preview
Простое создание сложных индикаторов с помощью объектов

Простое создание сложных индикаторов с помощью объектов

MetaTrader 5Примеры | 12 октября 2022, 15:52
1 414 2
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

1. Введение

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

А ведь еще есть и графики: необходимо объявить тип графика, настроить все его свойства, а затем убедиться, что всё правильно стыкуется друг с другом и что вы объявили правильное количество буферов и графиков (если их меньше, вы получите ошибку выхода за пределы массива Array Out Of Range или невидимый график, который будет обнаружен только в ходе дальнейшей работы).

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

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

Вы можете установить в качестве буферов массивы, содержащиеся внутри объектов

В этой статье я покажу, что дает нам эта особенность и как ее можно использовать в объектно-ориентированном программировании.


2. Первый пример

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

class CIndicatorPlot
{
public:
   double            array[];
};

Здесь есть только публичный массив. Это необходимо, чтобы мы могли получить к нему доступ при установке его в качестве буфера или настройке/доступе к его данным (как и в случае с любым другим индикатором).

Создадим индикатор, который будет отображать 10 RSI с разными периодами и их среднее значение. Начнем со свойств, входных параметров и функции OnInit .

#property indicator_buffers 11
#property indicator_plots 11

input int firstPeriod = 6;
input int increment = 2;

CIndicatorPlot indicators[];
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{

   ArrayResize(indicators, 11);
//--- indicator buffers mapping

   for (int i=0; i<11; i++)
   {
      SetIndexBuffer(i, indicators[i].array, INDICATOR_DATA);
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
   }

   for (int i=0; i<10; i++)
      PlotIndexSetInteger(i, PLOT_LINE_COLOR, clrRed);


   PlotIndexSetInteger(10, PLOT_LINE_COLOR, clrCyan);
   PlotIndexSetInteger(10, PLOT_LINE_STYLE, STYLE_DASH);
   PlotIndexSetInteger(10, PLOT_LINE_WIDTH, 2);

   ArrayResize(handles, 10);
   for (int i=0; i<10; i++)
      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);


//---
   return(INIT_SUCCEEDED);
}

Обратите внимание, что мы использовали только два свойства: indicator_buffers и indicator_plots. Это два постоянно используемых свойства, если не считать общих (copyright, link, version, separate/chart window и др.). Другие свойства (цвет линии, тип отрисовки...) являются необязательными, но для более компактного кода мы настроим их с помощью PlotIndexSetInteger в цикле.
Для этого индикатора нам понадобятся 10 буферов для каждого RSI с разным периодом и еще один для их среднего значения. Мы поместим их все внутрь массива, а также создадим хэндлы индикатора в OnInit. 

Теперь проведем вычисления и копирование данных...

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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 limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
   {
      if (limit==0)
         CopyBuffer(handles[i], 0, 0, rates_total-limit, indicators[i].array);
      else
      {
         double newValues[];
         CopyBuffer(handles[i], 0, 0, rates_total-limit, newValues);

         for (int k=0; k<rates_total-limit; k++)
         {
            indicators[i].array[limit+k] = newValues[k];
         }
      }
   }

   for (int i=limit; i<rates_total; i++)
   {
      indicators[10].array[i] = 0.0;
      for (int j=0; j<10; j++)                            
         indicators[10].array[i] +=indicators[j].array[i];

      indicators[10].array[i]/=10.0;
   }


//--- return value of prev_calculated for next call
   return(rates_total);
}

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

Не забудьте отпустить хэндлы...

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
}
//+------------------------------------------------------------------+

Результат показан ниже:

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


3. Добавляем больше возможностей

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

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

class CIndicatorPlot
{
private:
   int               indicator_plot;

public:
   double            array[];

   void              SetBuffer(int &buffer, int &plot);
   void              SetLineWidth(int width);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineColor(color line_color);
   void              SetLabel(string label);
};

Функция SetBuffer установит индикаторный буфер и график. Когда две переменные передаются по ссылке, изменения, вносимые в них одним объектом, будут отражаться в следующих вызовах другими объектами. Индекс графика сохраняется для установки других свойств.

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

//+------------------------------------------------------------------+
void CIndicatorPlot::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);

   buffer++; //Increment for other steps (One buffer in this case)
   plot++;   //Increment one plot in any case
}

//+------------------------------------------------------------------+
void CIndicatorPlot::SetLineWidth(int width)
{
   PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width);
}

//---
//...

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

//+------------------------------------------------------------------+
//| Function to linearly interpolate 2 colors                        |
//+------------------------------------------------------------------+
color InterpolateColors(color colorA, color colorB, double factor)
{
   if (factor<=0.0) return colorA;
   if (factor>=1.0) return colorB;

   int result = 0;

   for (int i=0; i<3; i++) //R-G-B
   {
      int subcolor = int(
                        ((colorA>>(8*i))&(0xFF))*(1.0-factor) +
                        ((colorB>>(8*i))&(0xFF))*factor
                     );

      subcolor = subcolor>0xFF?0xFF:(
                    subcolor<0x00?0x00:
                    subcolor);

      result |= subcolor<<(8*i);
   }
   return (color)result;
}

Now the OnInit function looks like this:

CIndicatorPlot* indicators[];
CIndicatorPlot average;
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);
   ArrayResize(handles, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
        indicators[i] = new CIndicatorPlot();
   
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

//---
   return(INIT_SUCCEEDED);
}

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

В этом примере я также изменил структуру индикатора, используя массив указателей для RSI (чтобы доказать возможность использования динамически создаваемых объектов) и отделив среднее значение от массива. Теперь необходимо изменить ссылки на среднее значение в OnCalculate и удалить индикаторы в массиве указателей в OnDeInit.

void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
   for (int i=0; i<10; i++)
        delete indicators[i];
}

Результат выглядит так:

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

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

Во-первых, добавим в базовый класс виртуальные пустые обработчики событий:

class CIndicatorPlot
{
   //...

public:
   
   //...

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int start, const int rates_total) { }
};

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

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

class CRSIIndividual : public CIndicatorPlot
{
private:
   int               handle;
   int               rsi_period;

public:

   void              SetPeriodRSI(int period);

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIIndividual::SetPeriodRSI(int period)
{
   rsi_period = period;
}

//+------------------------------------------------------------------+
void CRSIIndividual::Init(void)
{
   handle = iRSI(NULL, PERIOD_CURRENT, rsi_period, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CRSIIndividual::Update(const int start,const int rates_total)
{
   if (start==0)
      CopyBuffer(handle, 0, 0, rates_total-start, array);
   else
   {
      double newValues[];
      CopyBuffer(handle, 0, 0, rates_total-start, newValues);

      for (int k=0; k<rates_total-start; k++)
      {
         array[start+k] = newValues[k];
      }
   }
}

//+------------------------------------------------------------------+
void CRSIIndividual::DeInit(void)
{
   IndicatorRelease(handle);
}

Для класса Average нам нужно хранить указатели для доступа к остальным объектам графика индикатора (Individual RSI). В этом случае Init() и DeInit() не нужны.

class CRSIAverage : public CIndicatorPlot
{
private:
   CRSIIndividual*   rsi_indicators[];

public:
   void              SetRSIPointers(const CRSIIndividual &rsi_objects[]);

   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIAverage::SetRSIPointers(const CRSIIndividual &rsi_objects[])
{
   int total = ArraySize(rsi_objects);
   ArrayResize(rsi_indicators, total);

   for (int i=0; i<total; i++)
      rsi_indicators[i] = (CRSIIndividual*)GetPointer(rsi_objects[i]);
}

//+------------------------------------------------------------------+
void CRSIAverage::Update(const int start,const int rates_total)
{
   for (int i=start; i<rates_total; i++)
   {
      array[i] = 0.0;
      for (int j=0; j<10; j++)
         array[i] +=rsi_indicators[j].array[i];

      array[i]/=10.0;
   }
}

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

На финальном этапе функция OnInit (и объявления объектов выше) будет выглядеть так...

CRSIIndividual indicators[];
CRSIAverage average;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      indicators[i].SetPeriodRSI(firstPeriod+i*increment);
      indicators[i].Init();                               
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

   average.SetRSIPointers(indicators);                    

//---
   return(INIT_SUCCEEDED);
}

...и мы сможем сделать другие функции обработки событий намного чище:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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 limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
      indicators[i].Update(limit, rates_total);

   average.Update(limit, rates_total);

//--- return value of prev_calculated for next call
   return(rates_total);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      indicators[i].DeInit();
}
//+------------------------------------------------------------------+

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



4. Расширяем класс

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

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

Имея это в виду, я сначала объясню, как реализованы классы и как структурировано наследование.

Во-первых, классы структурированы следующим образом:

  • CIndicatorBufferBase
    • CIndicatorCalculations
    • CIndicatorPlotBase
      • CIndicator_1Data
        • CIndicatorPlotLine
        • CIndicatorPlotHistogram
        • ...
        • CIndicator_1Data1Color
          • CIndicatorPlotColorLine
          • ...
      • CIndicator_2Data
        • CIndicatorPlotHistogram2
        • ...
        • CIndicator_2Data1Color
          • CIndicatorPlotColorHistogram2
          • ...
      • CIndicator_4Data
        • CIndicatorPlotCandles
        • ...
        • CIndicator_4Data1Color
          • CIndicatorPlotColorCandles
          • ...

Несколько ключевых моментов:

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

В обобщенном виде реализация выглядит так:

//+------------------------------------------------------------------+
//| Base class for plots and calculation buffers                     |
//+------------------------------------------------------------------+
class CIndicatorBufferBase
{
public:
   virtual void      SetBuffer(int &buffer, int &plot)=NULL;
   virtual void      SetAsSeries(bool set)=NULL;

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(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[]) { }
};

//+------------------------------------------------------------------+
//| Calculations Buffer (with no plot)                               |
//+------------------------------------------------------------------+
class CIndicatorCalculations : public CIndicatorBufferBase
{
public:
   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetBuffer(int &index, int &plot)
{
   SetIndexBuffer(index, array, INDICATOR_CALCULATIONS);

   index++;
//No plot is used
}

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetAsSeries(bool set)
{
   ArraySetAsSeries(array, set);
}

//+------------------------------------------------------------------+
//| Base indicator plot class                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotBase : public CIndicatorBufferBase
{
protected:

   int               indicator_plot;

   virtual void      SetDrawType()=NULL; //Implicit in each class

public:

   void              SetArrow(uchar arrow);
   void              SetArrowShift(int shift);
   void              SetDrawBegin(int begin);
   void              SetShowData(bool show);
   void              SetShift(int shift);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineWidth(int width);
   void              SetColorIndexes(color &color_array[]);
   void              SetLineColor(color line_color);
   void              SetLineColor(color line_color, int index);
   void              SetEmptyValue(double empty);
   void              SetLabel(string label);

   int               GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, int property_modifier=0);
};

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer                           |
//+------------------------------------------------------------------+
class CIndicator_1Data : public CIndicatorPlotBase
{
public:

   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   SetDrawType();

   buffer++;
   plot++;
}

//...

//+------------------------------------------------------------------+
//| Plot Line (1 data buffer)                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotLine : public CIndicator_1Data
{
protected:

   virtual void      SetDrawType() final;
};

//+------------------------------------------------------------------+
void CIndicatorPlotLine::SetDrawType(void)
{
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 2 Data Buffers                          |
//+------------------------------------------------------------------+
class CIndicator_2Data : public CIndicatorPlotBase
{
public:

   double            first_array[];
   double            second_array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};


//+------------------------------------------------------------------+
void CIndicator_2Data::SetBuffer(int &buffer, int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, first_array, INDICATOR_DATA);
   SetIndexBuffer(buffer+1, second_array, INDICATOR_DATA);
   SetDrawType();

   buffer+=2;
   plot++;
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer & 1 Color Buffer          |
//+------------------------------------------------------------------+
class CIndicator_1Data1Color : public CIndicator_1Data
{
public:

   double            color_buffer[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetBuffer(int &buffer, int &plot)
{
   CIndicator_1Data::SetBuffer(buffer, plot);

   SetIndexBuffer(buffer, color_buffer, INDICATOR_COLOR_INDEX);

   buffer++; //Add color buffer
}

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetAsSeries(bool set)
{
   CIndicator_1Data::SetAsSeries(set);
   ArraySetAsSeries(color_buffer, set);
}

//...

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

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

ArraySetAsSeries также был включен, так как это очень распространенная функция, которая почти всегда требует, чтобы все буферы устанавливались одинаково.


Теперь, когда у нас есть классы, мы можем создать индикатор. Добавим кое-что еще в качестве примера:

  • Сначала создадим полосы на основе индикатора ATR и отобразим их заполненными цветом.
  • Затем создадим 10 скользящих средних с разными периодами и отобразим их на графике в виде линий.
  • Наконец используем окраску свечей, чтобы менять окраску свечей в зависимости от того, сколько скользящих средних находится выше/ниже полос.

Сначала объявим входные данные и включим файлы для классов индикаторов и цветовую интерполяцию, которую мы сделали в разделе 3:

#property indicator_buffers 19
#property indicator_plots 13

#include <OOPIndicators/IndicatorClass.mqh>
#include <OOPIndicators/ColorLerp.mqh>

input int atr_period = 10; //ATR Period
input double atr_band_multiplier = 0.8; //ATR Multiplier for bands
input bool show_bands = true; //Show Bands
input bool show_data = false; //Show Extra Data

input int ma_faster_period = 14; //MA Faster Period
input int ma_step = 2; //MA Step
input ENUM_MA_METHOD ma_method = MODE_SMA; //MA Method

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

Затем мы создадим классы для каждой части индикатора.

Начнем с диапазонов ATR:

//+------------------------------------------------------------------+
//| ATR Bands class (inherit from Filling Plot)                      |
//+------------------------------------------------------------------+
class CATRBand : public CIndicatorPlotFilling
{
private:

   int               handle;

public:

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total, const double &close[]);
};

//+------------------------------------------------------------------+
void CATRBand::Init(void)
{
   handle = iATR(NULL, PERIOD_CURRENT, atr_period);
}

//+------------------------------------------------------------------+
void CATRBand::Update(const int limit,const int rates_total,const double &close[])
{
   double atr[];
   CopyBuffer(handle, 0, 0, rates_total-limit, atr);

   for (int i=limit; i<rates_total; i++)
   {
      first_array[i] = close[i]+atr[i-limit]*atr_band_multiplier;
      second_array[i] = close[i]-atr[i-limit]*atr_band_multiplier;
   }
}

//+------------------------------------------------------------------+
void CATRBand::DeInit(void)
{
   IndicatorRelease(handle);
}

Класс скользящей средней, имеющий параметры в Init() для периода и метода:

//+------------------------------------------------------------------+
//| Moving Averages class (inherit from Line Plot)                   |
//+------------------------------------------------------------------+
class CMA : public CIndicatorPlotLine
{
private:

   int               handle;

public:
   virtual void      Init(int period, ENUM_MA_METHOD mode);
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total);
};

//+------------------------------------------------------------------+
void CMA::Init(int period, ENUM_MA_METHOD mode)
{
   handle = iMA(NULL, PERIOD_CURRENT, period, 0, mode, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CMA::Update(const int limit,const int rates_total)
{
   if (limit==0) CopyBuffer(handle, 0, 0, rates_total, array);
   else
   {
      double newVals[];
      CopyBuffer(handle, 0, 0, rates_total-limit, newVals);

      for (int i=limit; i<rates_total; i++)
         array[i] = newVals[i-limit];
   }
}

//+------------------------------------------------------------------+
void CMA::DeInit(void)
{
   IndicatorRelease(handle);
}

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

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

//+------------------------------------------------------------------+
//| Color Candles class (inherit from Color Candles Plot)            |
//+------------------------------------------------------------------+
class CColorCandles : public CIndicatorPlotColorCandles
{
public:
   virtual void      Update(const int limit,
                            const int rates_total,
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[]);
};

//+------------------------------------------------------------------+
void CColorCandles::Update(const int limit,
                           const int rates_total,
                           const double &open[],
                           const double &high[],
                           const double &low[],
                           const double &close[])
{
   for (int i=limit; i<rates_total; i++)
   {
      open_array[i] = open[i];
      high_array[i] = high[i];
      low_array[i] = low[i];
      close_array[i] = close[i];

      int count_ma = TOTAL_MA;

      for (int m=0; m<TOTAL_MA; m++)
      {
         if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++;
         if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--;
      }

      color_buffer[i] = count_ma;

      //Update inside of this other object (to avoid making an extra inheritance, or an external loop)
      showIndex.array[i] = TOTAL_MA - count_ma;
   }
}

Теперь нам нужно объявить объекты и настроить визуальные эффекты буферов и графиков:

#define TOTAL_MA 10

CMA maIndicators[TOTAL_MA];
CATRBand bands;
CColorCandles candles;
CIndicatorPlotNone showIndex; //To show MAs above/below

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   int buffer=0, plot=0;

   bands.SetBuffer(buffer, plot);
   candles.SetBuffer(buffer, plot);
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].SetBuffer(buffer, plot);
   showIndex.SetBuffer(buffer, plot);

//Print("Buffers: ", buffer, "  Plots: ", plot);

//--- plot settings
   if (show_bands) bands.SetLineColor(clrDarkSlateGray);
   else bands.SetLineColor(clrNONE);
   bands.SetShowData(show_data);
   if (show_data)
      bands.SetLabel("Close + ATR;Close - ATR");


   for (int i=0; i<TOTAL_MA; i++)
   {
      maIndicators[i].SetLineColor(InterpolateColors(clrAqua, clrRoyalBlue, i/(TOTAL_MA-1.0)));
      maIndicators[i].SetLabel("MA("+IntegerToString(ma_faster_period+i*ma_step)+")");
      maIndicators[i].SetShowData(show_data);
      if (i>0 && i <TOTAL_MA-1) maIndicators[i].SetLineStyle(STYLE_DOT);
      else maIndicators[i].SetLineWidth(2);
   }

   color arrow_colors[TOTAL_MA*2+1];

   for (int i=0; i<TOTAL_MA; i++)
      arrow_colors[i] = InterpolateColors(clrGreenYellow, clrGray, i/double(TOTAL_MA));
   arrow_colors[TOTAL_MA] = clrGray;
   for (int i=TOTAL_MA+1; i<TOTAL_MA*2+1; i++)
      arrow_colors[i] = InterpolateColors(clrGray, clrOrange, (i-TOTAL_MA)/double(TOTAL_MA));

   candles.SetColorIndexes(arrow_colors);
   candles.SetLabel("Open;High;Low;Close");
   candles.SetShowData(false);

   showIndex.SetLabel("MAs above/below");
   showIndex.SetShowData(true);

//--- initialize classes
   bands.Init();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Init(ma_faster_period+i*ma_step, ma_method);

   return(INIT_SUCCEEDED);
}

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

Как было сказано ранее, вы можете легко узнать количество буферов и графиков, которые вам нужны, печатая значения переменных buffer и plot. Это позволит вам правильно установить свойства (в начале вы можете установить их на большее число, чем нужно, чтобы избежать ошибок).

Обратите также внимание, что мы включили экземпляр класса Plot None. Этот объект обновляется объектом свечей, поэтому ему не нужны специальные обработчики событий. Он отображает количество скользящих средних, которые находятся выше или ниже полос в окне данных.

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

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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 limit = MathMax(0, prev_calculated-1);

   bands.Update(limit, rates_total, close);

   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Update(limit, rates_total);

   candles.Update(limit, rates_total, open, high, low, close);

//--- return value of prev_calculated for next call
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   bands.DeInit();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].DeInit();
}

Финальный результат выглядит так:


5. Ограничения метода

Несмотря на свое удобство, использование функций вместо свойств имеет несколько недостатков: наиболее заметным из них является интерференция при изменении цветов/стиля любого графика. Иногда они остаются, а иногда перезаписываются при инициализации.

Этой проблемы можно избежать, используя входные данные для цветов (вместо их изменения на вкладке Colors) или проверяя, есть ли цвет по умолчанию, отличный от черного (0x000000). Это работает со всеми цветами, кроме черного.

if (obj.GetInteger(PLOT_LINE_COLOR)==clrBlack)
   obj.SetLineColor(clrYellow);

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

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


6. Заключение

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


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/11233

Прикрепленные файлы |
MQL5.zip (10.17 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Dmitriy Skub
Dmitriy Skub | 13 окт. 2022 в 10:53
Думал графические объекты имеются в виду - удивился и стал смотреть. Оказалось объекты классов) И в чем новизна?
Valeriy Yastremskiy
Valeriy Yastremskiy | 13 окт. 2022 в 10:55
Dmitriy Skub #:
Думал графические объекты имеются в виду - удивился и стал смотреть. Оказалось объекты классов) И в чем новизна?

Да просто хороший подход к реализации в статье, ничего нового, просто не плохое решение задачи. 

Популяционные алгоритмы оптимизации: Рой частиц (PSO) Популяционные алгоритмы оптимизации: Рой частиц (PSO)
В данной статье рассмотрим популярный алгоритм "Рой Частиц" (PSO — particle swarm optimisation). Ранее мы обсудили такие важные характеристики алгоритмов оптимизации как сходимость, скорость сходимости, устойчивость, масштабируемость, разработали стенд для тестирования, рассмотрели простейший алгоритм на ГСЧ.
Разработка торгового советника с нуля (Часть 24): Обеспечиваем надежность системы (I) Разработка торгового советника с нуля (Часть 24): Обеспечиваем надежность системы (I)
В этой статье мы сделаем систему более надежной, чтобы обеспечить более стабильное и безопасное использование. Один из способов достижения нужной надежности — постараться как можно больше повторно использовать код, чтобы он постоянно проверялся в разных ситуациях. Однако, это только один из путей, а другой — использование ООП.
DoEasy. Элементы управления (Часть 22): SplitContainer. Изменение свойств созданного объекта DoEasy. Элементы управления (Часть 22): SplitContainer. Изменение свойств созданного объекта
В статье реализуем возможность изменять свойства и внешний вид элемента управления SplitContainer после его создания.
DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей
В статье создадим класс вспомогательного объекта-разделителя панелей для элемента управления SplitContainer.