Как перенести расчетную часть любого индикатора в код эксперта
Dmitriy Gizlyk | 19 апреля, 2018
Содержание
- Введение
- 1. Принципы переноса кода
- 2. Создаем класс расчета индикатора
- 3. Пример добавления индикаторного класса в советник
- 4. "Стоимость" использования перенесенного индикатора
- Заключение
Введение
Когда программист создает советник, который получает сигналы от индикаторов, он всякий раз сталкивается с вопросом: использовать обращение к индикатору или перенести код индикатора в советник? Причины этому могут быть различные: желание сохранить в тайне используемые индикаторы и стратегию в целом, необходимость распространения советника единым файлом, желание снизить количество выполняемых операций в случаях использования не всех сигналов/буферов индикатора и т. д. Конечно, я не первый и, думаю, не последний, кто задается этим вопросом. 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.
Подытожим. Вот что нам предстоит сделать для переноса расчетной части индикатора в советник.
- Организовать работу индикаторных буферов. Для этого мы создадим класс CArrayBuffer, а в нем — методы для хранения данных и удобного доступа к ним. Позже мы создадим массив таких классов по количеству буферов в индикаторе.
- Расчетную часть индикатора из функции OnCalculate перенесем в функцию Calculate нашего класса.
- Доступ к таймсериям индикатор получает из параметров функции OnCalculate, чего нет у функций советника. Поэтому организуем загрузку необходимых таймсерий в функции LoadHistory.
- Чтобы унифицировать доступ к пересчитанным данным индикатора, создадим в классе 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. Здесь объявим:
- массив исходных данных для расчета (m_source_data);
- массив
индикаторных буферов (ar_IndBuffers);
- переменные для хранения количества буферов индикатора (m_buffers), глубины требуемой истории исходных данных (m_history_len), глубины требуемой истории значений индикатора (m_data_len);
- используемые инструмент (m_Symbol) и таймфрейм (m_Timeframe);
- тип цены для расчета индикатора (m_Price);
- методы для установки: глубины исходных данных (SetHistoryLen); загрузки исторических данных таймсерий (LoadHistory); пересчета индикатора (Calculate).
Все методы создаются виртуальными, чтобы впоследствии их можно было бы подстроить под нужды конкретного индикатора. В конструкторе класса инициализируем переменные и освобождаем массивы.
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
Я уже говорил, что выбрал индикатор МА не случайно. Теперь мы можем проверить скорость получения одних и тех же данных тремя способами:
- через функцию встроенного в терминал индикатора (iMA);
- через вызов аналогичного пользовательского индикатора (iCustom);
- непосредственно расчет внутри советника.
Первое, что приходит в голову, — воспользоваться функцией профилирования редактора 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 отдельных советника.
- Эталонный, пустой советник, не выполняющий никаких функций. Он будет служить для оценки затрат времени самим терминалом для перебора истории котировок.
- Советник, получающий данные путем расчета значений в индикаторном классе.
- Советник, получающий данные от индикатора iMA.
- Советник, получающий данные от пользовательского индикатора.
После этого запустим их оптимизацию на 11 проходах в тестере стратегий и сравним среднее время одного прохода.
Результаты тестирования показали экономию времени при использовании расчетов внутри советника. Самым затратным по времени оказалось получение данных от пользовательского индикатора.
Обратите внимание: в эксперименте рассчитывалась МА по цене закрытия. Расчетная часть такого индикатора довольно проста. Возникает вопрос: а как изменится ситуация при усложнении расчетов? Выясним это, проведя еще один эксперимент.
4.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 |