Как перенести расчетную часть любого индикатора в код эксперта

Dmitriy Gizlyk | 19 апреля, 2018

Содержание

Введение

Когда программист создает советник, который получает сигналы от индикаторов, он всякий раз сталкивается с вопросом: использовать обращение к индикатору или перенести код индикатора в советник? Причины этому могут быть различные: желание сохранить в тайне используемые индикаторы и стратегию в целом, необходимость распространения советника единым файлом, желание снизить количество выполняемых операций в случаях использования не всех сигналов/буферов индикатора и т. д. Конечно, я не первый и, думаю, не последний, кто задается этим вопросом. Nikolay Kositsin уже рассматривал подобную тему для MetaTrader 4. Давайте посмотрим, как это можно сделать на платформе MetaTrader 5.

1. Принципы переноса кода

Прежде чем приступить к работе, разберемся с отличиями работы индикаторов и советников. Рассмотрим пустой шаблон индикатора.

//+------------------------------------------------------------------+
//|                                                        Blanc.mq5 |
//|                                             Copyright 2018, DNG® |
//|                                 http://www.mql5.com/ru/users/dng |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, DNG®"
#property link      "http://www.mql5.com/ru/users/dng"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Buffer
#property indicator_label1  "Buffer"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double         BufferBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferBuffer,INDICATOR_DATA);
   
//---
   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[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

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

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

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

2. Создаем класс расчета индикатора

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

Подытожим. Вот что нам предстоит сделать для переноса расчетной части индикатора в советник.

  1. Организовать работу индикаторных буферов. Для этого мы создадим класс CArrayBuffer, а в нем — методы для хранения данных и удобного доступа к ним. Позже мы создадим массив таких классов по количеству буферов в индикаторе.
  2. Расчетную часть индикатора из функции OnCalculate перенесем в функцию Calculate нашего класса.
  3. Доступ к таймсериям индикатор получает из параметров функции OnCalculate, чего нет у функций советника. Поэтому организуем загрузку необходимых таймсерий в функции LoadHistory.
  4. Чтобы унифицировать доступ к пересчитанным данным индикатора, создадим в классе CIndicator функцию CopyBuffer с необходимыми параметрами. 

Всю предстоящую работу можно обобщить в нижеследующую схему.


Далее, говоря об индикаторе, я буду иметь в виду копию индикатора, созданную в коде советника.

2.1. Создаем индикаторный буфер

Для создания индикаторных буферов воспользуемся классом CArrayDouble. На его базе создадим новый класс CArrayBuffer.

class CArrayBuffer   :  public CArrayDouble
  {
public:
                     CArrayBuffer(void);
                    ~CArrayBuffer(void);
//---
   int               CopyBuffer(const int start, const int count, double &double_array[]);
   int               Initilize(void);
   virtual bool      Shift(const int shift);
  };

Создадим метод CopyBuffer, чтобы получение данных по форме напоминало стандартное обращение к индикатору. Также добавим два служебных метода: Initilize —для очистки данных буфера и Shift — для смещения данных внутри буфера при появлении новой свечи. Непосредственно с кодом функций можно ознакомиться во вложении.

2.2. Родительский класс для будущих индикаторов

Следующим шагом создадим "скелет" индикатора в базовом классе CIndicator.

class CIndicator
  {
private:
//---
   datetime             m_last_load;
public:
                        CIndicator(void);
                       ~CIndicator(void);
   virtual bool         Create(const string symbol=NULL, const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
//--- Set indicator's main settings
   virtual bool         SetBufferSize(const int bars);
//--- Get indicator's data
   virtual int          CopyBuffer(const uint buffer_num,const uint start, const uint count, double &double_array[]);
   virtual double       GetData(const uint buffer_num,const uint shift);

protected:
   double               m_source_data[];
   CArrayBuffer         ar_IndBuffers[];
   int                  m_buffers;
   int                  m_history_len;
   int                  m_data_len;
//---
   string               m_Symbol;
   ENUM_TIMEFRAMES      m_Timeframe;
   ENUM_APPLIED_PRICE   m_Price;      
//--- Set indicator's main settings
   virtual bool         SetHistoryLen(const int bars=-1);
//---
   virtual bool         LoadHistory(void);
   virtual bool         Calculate()                         {  return true;   }
  };

В этом классе 6 публичных методов:

Основную часть членов класса объявим в зоне protected. Здесь объявим:

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

CIndicator::CIndicator()   :  m_buffers(0),
                              m_Symbol(_Symbol),
                              m_Timeframe(PERIOD_CURRENT),
                              m_Price(PRICE_CLOSE),
                              m_last_load(0)
  {
   m_data_len=m_history_len  =  Bars(m_Symbol,m_Timeframe)-1;
   ArrayFree(ar_IndBuffers);
   ArrayFree(m_source_data);
  }

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

bool CIndicator::Create(const string symbol=NULL,const ENUM_TIMEFRAMES timeframe=0,const ENUM_APPLIED_PRICE price=1)
  {
   m_Symbol=(symbol==NULL ? _Symbol : symbol);
   if(!SymbolInfoInteger(m_Symbol,SYMBOL_SELECT))
      if(!SymbolSelect(m_Symbol,true))
         return false;
//---
   m_Timeframe=timeframe;
   m_Price=price;
//---
   return true;
  }

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

bool CIndicator::SetBufferSize(const int bars)
  {
   if(bars>0)
      m_data_len  =  bars;
   else
      m_data_len  =  Bars(m_Symbol,m_Timeframe);
//---
   if(m_data_len<=0)
     {
      for(int i=0;i<m_buffers;i++)
         ar_IndBuffers[i].Shutdown();
      return false;
     }
//---
   if(m_history_len<m_data_len)
      if(!SetHistoryLen(m_data_len))
         return false;
//---
   for(int i=0;i<m_buffers;i++)
     {
      ar_IndBuffers[i].Shutdown();
      if(!ar_IndBuffers[i].Resize(m_data_len))
         return false;
     }
//---
   return true;
  }

Для получения исторических данных таймсерий используется функция LoadHistory. У нее нет параметров, а исходные значения она получает из данных, сохраненных в предыдущих функциях.

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

bool CIndicator::LoadHistory(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
   if(m_last_load>=cur_date && ArraySize(m_source_data)>=m_history_len)
      return true;
//---
   MqlRates rates[];
   int total=0,i;
   switch(m_Price)
     {
      case PRICE_CLOSE:
        total=CopyClose(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
        break;
      case PRICE_OPEN:
        total=CopyOpen(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_HIGH:
        total=CopyHigh(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_LOW:
        total=CopyLow(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_MEDIAN:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low)/2;
        break;
      case PRICE_TYPICAL:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low+rates[i].close)/3;
        break;
      case PRICE_WEIGHTED:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low+2*rates[i].close)/4;
        break;
     }
//---
   if(total<=0)
      return false;
//---
   m_last_load=cur_date;
   return (total>0);
  }

Если в процессе выполнения функции данные не были загружены, функция вернет false.

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

int CIndicator::CopyBuffer(const uint buffer_num,const uint start,const uint count,double &double_array[])
  {
   if(!Calculate())
      return -1;
//---
   if((int)buffer_num>=m_buffers)
     {
      ArrayFree(double_array);
      return -1;
     }
//---
   return ar_IndBuffers[buffer_num].CopyBuffer(start,count,double_array);
  }

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

Аналогично построена функция адресного доступа к данным.

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

2.3. Индикаторный класс скользящей средней

Для демонстрации технологии я выбрал индикатор скользящей средней (МА). Мой выбор не случаен. Этот индикатор технического анализа не только используется трейдерами в его классическом виде, но и широко используется для построения других индикаторов. Это и MACD, и Аллигатор, и многие другие. К тому же, в "коробочной" поставке есть пример МА, с которого мы сможем получать данные через функцию iCustom, чтобы сравнивать скорость доступа к индикатору со скоростью расчета данных в советнике.

Рассчитывать МА мы будем в классе CMA. Наш класс получит 4 публичных метода: конструктор, деструктор, метод инициализации (Create) и метод установки глубины исторических данных индикатора (который мы перепишем). Методы доступа к данным индикатора наш класс унаследует от родительского.

class CMA : public CIndicator
  {
private:
   int               m_Period;
   int               m_Shift;
   ENUM_MA_METHOD    m_Method;
   datetime          m_last_calculate;
   
public:
                     CMA();
                    ~CMA();
   bool              Create(const string symbol, const ENUM_TIMEFRAMES timeframe, const int ma_period, const int ma_shift, const ENUM_MA_METHOD ma_method, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   virtual bool      SetBufferSize(const int bars);
   
protected:
   virtual bool      Calculate();
   virtual double    CalculateSMA(const int shift);
   virtual double    CalculateEMA(const int shift);
   virtual double    CalculateLWMA(const int shift);
   virtual double    CalculateSMMA(const int shift);
  };

Как можно заметить на представленном выше заголовке класса, на этом этапе появляются элементы для непосредственного расчета индикатора. Это частные переменные для хранения периода, сдвига и метода расчета индикатора. В блоке protected мы перепишем виртуальную функцию расчета индикатора Calculate. В зависимости от указанного метода расчета индикатора, она будет вызывать подфункции CalculateSMA, CalculateEMA, CalculateLWMA или CalculateSMMA.

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

CMA::CMA()  :  m_Period(25),
               m_Shift(0),
               m_Method(MODE_SMA)
  {
   m_buffers=1;
   ArrayResize(ar_IndBuffers,1);
  }

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

bool CMA::Create(const string symbol,const ENUM_TIMEFRAMES timeframe,const int ma_period,const int ma_shift,const ENUM_MA_METHOD ma_method,const ENUM_APPLIED_PRICE price=1)
  {
   if(!CIndicator::Create(symbol,timeframe,price))
      return false;
//---
   if(ma_period<=0)
      return false;
//---
   m_Period=ma_period;
   m_Shift=ma_shift;
   m_Method=ma_method;
//---
   if(!SetBufferSize(ma_period))
      return false;
   if(!SetHistoryLen(2*ma_period+(m_Shift>0 ? m_Shift : 0)))
      return false;
//---
   return true;
  }

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

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

Теперь организуем цикл для пересчета новых элементов индикаторного буфера. Обратите внимание: если пересчитанный элемент выходит за размеры текущего индикаторного буфера (это возможно при первом запуске расчета или расчете после обрыва связи, когда количество новых свечей превышает размер буфера), то данные в буфер добавляются методом Add. Если же пересчитанный элемент попадает в размер существующего буфера, то значение элемента обновляется методом Update. Непосредственный расчет значений индикатора производится в подфункциях, соответствующих методу усреднения. Логика расчета взята из индикатора Custom Moving Average.mq5 из стандартной поставки MetaTrader 5.

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

bool CMA::Calculate(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
   if(m_last_calculate==cur_date && ArraySize(m_source_data)==m_history_len)
      return true;
//---
   if(!LoadHistory())
      return false;
//---
   int shift=Bars(m_Symbol,m_Timeframe,m_last_calculate,cur_date)-1;
   if(shift>m_data_len)
     {
      ar_IndBuffers[0].Initilize();
      shift=m_data_len;
     }
   else
      ar_IndBuffers[0].Shift(shift);
//---
   for(int i=(m_data_len-shift);i<m_data_len;i++)
     {
      int data_total=ar_IndBuffers[0].Total();
      switch(m_Method)
        {
         case MODE_SMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateSMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateSMA(i+m_Shift));
           break;
         case MODE_EMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateEMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateEMA(i+m_Shift));
           break;
         case MODE_SMMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateSMMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateSMMA(i+m_Shift));
           break;
         case MODE_LWMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateLWMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateLWMA(i+m_Shift));
           break;
        }
     }
//---
   m_last_calculate=cur_date;
   m_data_len=ar_IndBuffers[0].Total();
//---
   return true;
  }

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

3. Пример добавления индикаторного класса в советник

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

Создадим новый файл советника Test_Class.mq5. Его входные параметры будут аналогичны параметрам используемого индикатора.

input int                  MA_Period   =  25;
input int                  MA_Shift    =  0;
input ENUM_MA_METHOD       MA_Method   =  MODE_SMA;
input ENUM_APPLIED_PRICE   MA_Price    =  PRICE_CLOSE;

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

CMA   *MA;
double c_data[];

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

int OnInit()
  {
//---
   MA=new CMA;
   if(CheckPointer(MA)==POINTER_INVALID)
      return INIT_FAILED;
//---
   if(!MA.Create(_Symbol,PERIOD_CURRENT,MA_Period,MA_Shift,MA_Method,MA_Price))
      return INIT_FAILED;
   MA.SetBufferSize(3);
//---
   return(INIT_SUCCEEDED);
  }

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

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(MA)!=POINTER_INVALID)
      delete MA;
  }

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

void OnTick()
  {
//---
   static datetime last_bar=0;
   datetime cur_date=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);
   if(last_bar==cur_date)
      return;
   last_bar=cur_date;
//---
   if(!MA.CopyBuffer(MAIN_LINE,0,3,c_data))
      return;

//---
//     Здесь добавляется Ваш код обработки сигналов и торговых операций
//---
   return;
  }

С полным кодом всех программ и классов можно ознакомиться во вложении.

4. "Стоимость" использования перенесенного индикатора

И еще один важный вопрос: как перенос кода индикатора отразится на работе советника? Чтобы на него ответить, проведем несколько экспериментов.

4.1. Эксперимент 1

Я уже говорил, что выбрал индикатор МА не случайно. Теперь мы можем проверить скорость получения одних и тех же данных тремя способами:

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

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

Функция Среднее время выполнения, мкр. сек. Доля от общего времени
OnTick
99.14%
Проверка открытия нового бара 0.528 67,23%
Внутренний расчет
21.524 2.36%
      в т.ч. CopyClose 1.729  0.19%
 iMA  2.231  0.24%
 iCustom  0.748  0.08%
 OnInit  241439  0.86%
 Получение хэндла iCustom  235676  0.84%

Первое, что "бросается в глаза" — долгое время получения хэндла индикатора через функцию iCustom. Оно в десятки раз превышает время на инициализацию индикаторного класса и получение хэндла индикатора через функцию iMA. В то же время, получение данных от индикатора, инициализированного функцией iCustom, происходит в 3 раза быстрее, чем получение данных от индикатора iMA, и в 30 раз быстрее расчета значения индикатора в классе.

Среднее время выполнения операций

Рассмотрим подробнее время выполнения различных функций нашего индикаторного класса. Обратите внимание на то, что само время получения исторических данных функцией CopyClose сопоставимо со временем получения данных индикаторов. Неужели индикатор практически не тратит времени на расчет? В действительности всё немного не так. В архитектуре MetaTrader 5 организован асинхронный доступ к значениям индикаторов. Иными словами, при получении хэндла индикатора он прикрепляется к графику. Далее этот индикатор производит свои расчеты вне потока советника. Они взаимодействуют лишь на этапе передачи данных, аналогично получению данных тайм-серий. Поэтому и время на выполнение этих операций сопоставимо.

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

4.2. Эксперимент 2

Cоздадим 4 отдельных советника.

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

После этого запустим их оптимизацию на 11 проходах в тестере стратегий и сравним среднее время одного прохода.

Эксперимент 2Эксперимент 2

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

Результаты эксперимента 2

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

4.3. Эксперимент 3

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

Эксперимент 3Эксперимент 3

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

Результаты эксперимента 3

Заключение

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

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

Программы, используемые в статье:

#
 Имя
Тип 
Описание 
1 Indicarot.mqh  Библиотека класса  Базовый класс для переноса индикаторов.
2 MA.mqh  Библиотека класса  Класс для расчета индикатора MA внутри советника
3 Test.mq5  Эксперт  Эксперт для проведения эксперимента 1
4 Test_Class.mq5  Эксперт  Эксперт с расчетом индикатора внутри советника (эксперименты 2 и 3)
5 Test_iMA.mq5  Эксперт  Эксперт с получением данных индикатора через iMA
6 Test_iCustom.mq5  Эксперт  Эксперт с получением данных индикатора через iCustom