Работа с таймсериями в библиотеке DoEasy (Часть 41): Пример мультисимвольного мультипериодного индикатора

9 апреля 2020, 12:52
Artyom Trishkin
0
1 365

Содержание


Концепция

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

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

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

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

Доработка классов и данных библиотеки

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

Структура, через которую передаём из индикаторов в библиотеку данные текущего бара из обработчика OnCalculate()
у нас находится в файле \MQL5\Include\DoEasy\Defines.mqh.

//+------------------------------------------------------------------+
//| Структуры                                                        |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                                 // размер входных таймсерий
   int         prev_calculated;                             // количество обработанных баров на предыдущем вызове
   int         begin;                                       // откуда начинаются значимые данные
   double      price;                                       // текущее значение массива для расчета
   MqlRates    rates;                                       // Структура цен
  } rates_data;
//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+

Но эта структура не относится к предопределённым переменным и статическим величинам, она больше подходит под определение "Данные". Поэтому удалим её из Defines.mqh и определим её в файле \MQL5\Include\DoEasy\Datas.mqh:

//+------------------------------------------------------------------+
//|                                                        Datas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "InpDatas.mqh"
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define INPUT_SEPARATOR                (",")    // Разделитель в строке входных параметров
#define TOTAL_LANG                     (2)      // Количество используемых языков
//+------------------------------------------------------------------+
//| Структуры                                                        |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                     // размер входных таймсерий
   int         prev_calculated;                 // количество обработанных баров на предыдущем вызове
   int         begin;                           // откуда начинаются значимые данные
   double      price;                           // текущее значение массива для расчета
   MqlRates    rates;                           // Структура цен
  } rates_data;
//+------------------------------------------------------------------+
//| Массивы                                                          |
//+------------------------------------------------------------------+
string            ArrayUsedSymbols[];           // Массив имён используемых символов
ENUM_TIMEFRAMES   ArrayUsedTimeframes[];        // Массив используемых таймфреймов
//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+ 
//| Наборы данных                                                    |
//+------------------------------------------------------------------+

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

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

Вот теперь создадим новый файл \MQL5\Include\DoEasy\InpDatas.mqh для хранения перечислений для входных переменных программы:

//+------------------------------------------------------------------+
//|                                                     InpDatas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
//#define COMPILE_EN // Закомментировать строку для компиляции на русском языке 
//+------------------------------------------------------------------+
//| Перечисления входных параметров                                  |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Английский язык входных параметров                               |
//+------------------------------------------------------------------+
#ifdef COMPILE_EN
//+------------------------------------------------------------------+
//| Режимы работы с символами                                        |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work only with the current Symbol
   SYMBOLS_MODE_DEFINES,                              // Work with a given list of Symbols
   SYMBOLS_MODE_MARKET_WATCH,                         // Working with Symbols from the "Market Watch" window
   SYMBOLS_MODE_ALL                                   // Work with a complete list of Symbols
  };
//+------------------------------------------------------------------+
//| Режимы работы с таймфреймами                                     |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Work only with the current timeframe
   TIMEFRAMES_MODE_LIST,                              // Work with a given list of timeframes
   TIMEFRAMES_MODE_ALL                                // Work with a complete list of timeframes
  };
//+------------------------------------------------------------------+
//| Русский язык входных параметров                                  |
//+------------------------------------------------------------------+
#else  
//+------------------------------------------------------------------+
//| Режимы работы с символами                                        |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Работа только с текущим символом
   SYMBOLS_MODE_DEFINES,                              // Работа с заданным списком символов
   SYMBOLS_MODE_MARKET_WATCH,                         // Работа с символами из окна "Обзор рынка"
   SYMBOLS_MODE_ALL                                   // Работа с полным списком символов
  };
//+------------------------------------------------------------------+
//| Режимы работы с таймфреймами                                     |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Работа только с текущим таймфреймом
   TIMEFRAMES_MODE_LIST,                              // Работа с заданным списком таймфреймов
   TIMEFRAMES_MODE_ALL                                // Работа с полным списком таймфреймов
  };
#endif 
//+------------------------------------------------------------------+

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

В этот файл будем добавлять новые перечисления по мере их необходимости.

Внесём в файл \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh класса CTimeSeries исправление ошибки в методе добавления объекта всех таймсерий символа в список, приводящей иногда к ошибке обращения по несуществующему указателю:

//+------------------------------------------------------------------+
//| Добавляет в список указанную список-таймсерию                    |
//+------------------------------------------------------------------+
bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeriesDE *series=new CSeriesDE(this.m_symbol,timeframe,required);
   if(series==NULL)
      return res;
   this.m_list_series.Sort();
   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   if(!res)
      delete series;
   series.SetAvailable(true);
   return res;
  }
//+------------------------------------------------------------------+

После получения ошибки добавления объекта в список, мы удаляем созданный объект series, а затем пытаемся получить к нему доступ для установки флага его использования. В такой ситуации мы получим ошибку, ведь указатель на объект уже удалён.

Для исправления просто перенесём установку флага в коде до проверки результата добавления объекта в список:

   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   series.SetAvailable(true);
   if(!res)
      delete series;
   return res;
  }
//+------------------------------------------------------------------+

В методах обновления указанного списка-таймсерии и всех списков-таймсерий не всегда удавалось поместить событие "Новый бар" в список событий с правильным временем события (временем открытия нового бара). В некоторых ситуациях время было нулевым.

Для исправления создадим новую переменную для хранения времени, и если тип программы "индикатор" и работа на текущем символе и периоде графика, то время записываем в переменную из структуры цен, полученных из OnCalculate(), иначе — время получаем из значения, возвращаемого методом LastBarDate() объекта-таймсерии. Полученное время используем при добавлении события в список всех событий объекта всех таймсерий символа:

//+------------------------------------------------------------------+
//| Обновляет указанный список-таймсерию                             |
//+------------------------------------------------------------------+
void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Сбрасываем флаг события таймсерии и очищаем список всех событий таймсерии
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Получаем из списка таймсерию по её таймфрейму
   CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Обновляем список-таймсерию
   series_obj.Refresh(data_calculate);
   datetime time=
     (
      this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
      data_calculate.rates.time : 
      series_obj.LastBarDate()
     );
//--- Если у объекта-таймсерии есть событие "Новый бар"
   if(series_obj.IsNewBar(time))
     {
      //--- отправляем событие "Новый бар" на график управляющей программы
      series_obj.SendEvent();
      //--- устанавливаем значения первой даты в истории на сервере и в терминале
      this.SetTerminalServerDate();
      //--- добавляем в список событий таймсерий новое событие "Новый бар"
      //--- при успешном добавлении - устанавливаем флаг события у таймсерии
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Обновляет все списки-таймсерии                                   |
//+------------------------------------------------------------------+
void CTimeSeriesDE::RefreshAll(SDataCalculate &data_calculate)
  {
//--- Сбрасываем флаги необходимости установки первой даты в истории на сервере и в терминале
//--- и флаг события таймсерии и очищаем список всех событий таймсерии
   bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- В цикле по списку всех используемых таймсерий
   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++) 
     {
      //--- получаем очередной объект-таймсерию по индексу цикла
      CSeriesDE *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0)
         continue;
      //--- обновляем список-таймсерию
      series_obj.Refresh(data_calculate);
      datetime time=
        (
         this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
         data_calculate.rates.time : 
         series_obj.LastBarDate()
        );
      //--- Если у объекта-таймсерии есть событие "Новый бар"
      if(series_obj.IsNewBar(time))
        {
         //--- отправляем событие "Новый бар" на график управляющей программы,
         series_obj.SendEvent();
         //--- устанавливаем флаг необходимости установки первой даты в истории на сервере и в терминале
         upd=true;
         //--- добавляем в список событий таймсерий новое событие "Новый бар"
         //--- при успешном добавлении - устанавливаем флаг события у таймсерии
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- Если установлен флаг необходимости установки первой даты в истории на сервере и в терминале -
//--- устанавливаем значения первой даты в истории на сервере и в терминале
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

Для обновления всех таймсерий нам нужно разделить место, откуда вызывается обновление таймсерии для текущего символа и остальных. Любые другие таймсерии мы обновляем в таймере, тогда как таймсерии текущего символа мы обновляем в OnCalculate(). Сделано так для того, чтобы не крутить в таймере таймсерии текущего символа в поиске нового тика, когда в OnCalculate() и так вызывается обновление таймсерий текущего символа по приходу нового тика.

Для работы в таймере объявим ещё один метод в файле класса коллекции таймсерий \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh:

//--- Обновляет (1) указанную таймсерию указанного символа, (2) все таймсерии указанного символа,
//--- (3) все таймсерии всех символов, (4) все таймсерии кроме текущей
   void                    Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void                    Refresh(const string symbol,SDataCalculate &data_calculate);
   void                    Refresh(SDataCalculate &data_calculate);
   void                    RefreshAllExceptCurrent(SDataCalculate &data_calculate);

//--- Получает события из объекта таймсерий и добавляет их в список
   bool                    SetEvents(CTimeSeriesDE *timeseries);

Метод будет вызывать методы обновления всех таймсерий кроме текущего символа (реализация метода):

//+------------------------------------------------------------------+
//| Обновляет все таймсерии кроме текущей                            |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::RefreshAllExceptCurrent(SDataCalculate &data_calculate)
  {
//--- Сбрасываем флаг события в коллекции таймсерий и очищаем список событий
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- В цикле по всем объектам таймсерий символов в коллекции
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной объект таймсерий символа
      CTimeSeriesDE *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- если символ таймсерии равен символу текущего графика или
      //--- если нет нового тика на символе таймсерии - идём к следующему объекту в списке
      if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick())
         continue;
      //--- Обновляем все таймсерии символа
      timeseries.RefreshAll(data_calculate);
      //--- Если у объекта таймсерий символа поднят флаг события -
      //--- получаем события от таймсерий символа, записываем их в список событий коллекции
      //--- и устанавливаем флаг события в коллекции
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }
//+------------------------------------------------------------------+

В файл сервисных функций библиотеки \MQL5\Include\DoEasy\Services\DELib.mqh добавим функцию, возвращающую количество баров второго указанного периода внутри одного бара первого указанного периода графика:

//+------------------------------------------------------------------+
//| Возвращает количество баров одного периода в одном баре другого  |
//+------------------------------------------------------------------+
int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT)
  {
   return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period);
  }
//+------------------------------------------------------------------+

Так как функция PeriodSeconds() возвращает количество секунд в периоде, то для определения количества баров одного (меньшего) периода в одном баре другого (большего) периода, достаточно разделить количество секунд большего периода на количество секунд меньшего периода. Что мы тут и делаем.

В своих программа мы можем задать список используемых символов. Устанавливается этот список для библиотеке в методе SetUsedSymbols() класса-коллекции символов в файле \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh. Если в своей программе задать список используемых символов, в котором не будет текущего символа, то библиотека создаст в коллекции таймсерий таймсерии всех указанных в настройках символов, но текущего символа там не будет. А к нему идёт постоянное обращение для расчётов позиционирования данных на экране. Соответственно — нам нужно исправить ситуации неуказания текущего символа.

Для этого в методе SetUsedSymbols() класса коллекции символов добавим к списку текущий символ. При условии, если имени текущего символа нет в списке рабочих символов, указанных пользователем в настройках программы, символ будет добавлен в список. Если же он там уже есть, то новый символ с таким названием добавлен не будет:
//+------------------------------------------------------------------+
//| Устанавливает список используемых символов                       |
//+------------------------------------------------------------------+
bool CSymbolsCollection::SetUsedSymbols(const string &symbol_used_array[])
  {
   ::ArrayResize(this.m_array_symbols,0,1000);
   ::ArrayCopy(this.m_array_symbols,symbol_used_array);
   this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols);
   this.m_list_all_symbols.Clear();
   this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW);
   //--- Используется только текущий символ
   if(this.m_mode_list==SYMBOLS_MODE_CURRENT)
     {
      string name=::Symbol();
      ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
      return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
     }
   else
     {
      bool res=true;
      //--- Используется предопределённый список символов
      if(this.m_mode_list==SYMBOLS_MODE_DEFINES)
        {
         int total=::ArraySize(this.m_array_symbols);
         for(int i=0;i<total;i++)
           {
            string name=this.m_array_symbols[i];
            ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
            bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
            res &=add;
            if(!add) 
               continue;
           }
         //--- Создаём новый текущий символ (если он уже есть в списке, то повторно создан не будет)
         res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL));
         return res;
        }
      //--- Используется полный список символов сервера
      else if(this.m_mode_list==SYMBOLS_MODE_ALL)
        {
         return this.CreateSymbolsList(false);
        }
      //--- Используется список символов из окна "Обзор рынка"
      else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH)
        {
         this.MarketWatchEventsControl(false);
         return true;
        }
     }
   return false;
  }
//+------------------------------------------------------------------+

В файле \MQL5\Include\DoEasy\Engine.mqh основного объекта библиотеки CEngine объявим три приватных метода:

//--- Устанавливает список используемых символов в коллекции символов и создаёт коллекцию таймсерий символов
   bool                 SetUsedSymbols(const string &array_symbols[]);
private:
//--- Записывает все используемые символы и таймфреймы в массивы ArrayUsedSymbols и ArrayUsedTimeframes
   void                 WriteSymbolsPeriodsToArrays(void);
//--- Проверяет наличие (1) символа в массиве ArrayUsedSymbols, (2) таймфрейма в массиве ArrayUsedTimeframes
   bool                 IsExistSymbol(const string symbol);
   bool                 IsExistTimeframe(const ENUM_TIMEFRAMES timeframe);
public:
//--- Создаёт файл ресурса

Методы нужны для записи списка символов и таймфреймов в ранее объявленные массивы в файле Datas.mqh, а также для возврата флага существования символа в массиве имён используемых символов и флага существования таймфрейма в массиве используемых таймфреймов.

Реализация методов, возвращающих флаги наличия символа и таймфрейма в соответствующих массивах:

//+------------------------------------------------------------------+
//| Проверяет наличие символа в массиве                              |
//+------------------------------------------------------------------+
bool CEngine::IsExistSymbol(const string symbol)
  {
   int total=::ArraySize(ArrayUsedSymbols);
   for(int i=0;i<total;i++)
      if(ArrayUsedSymbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+
//| Проверяет наличие таймфрейма в массиве                           |
//+------------------------------------------------------------------+
bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   int total=::ArraySize(ArrayUsedTimeframes);
   for(int i=0;i<total;i++)
      if(ArrayUsedTimeframes[i]==timeframe)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

Просто в цикле по соответствующему массиву получаем очередной элемент массива и сравниваем его с переданным в метод значением. Если значение очередного элемента массива совпадает с переданным в метод — возвращаем true. По окончании всего цикла возвращаем false — не было найдено совпадений значений элементов в массиве и переданного в метод.

Реализация метода записи в массивы используемых символов и таймфреймов:

//+------------------------------------------------------------------+
//| Записывает все используемые символы и таймфреймы                 |
//| в массивы ArrayUsedSymbols и ArrayUsedTimeframes                 |
//+------------------------------------------------------------------+
void CEngine::WriteSymbolsPeriodsToArrays(void)
  {
//--- Получаем список всех созданных таймсерий (создаются по количеству используемых символов)
   CArrayObj *list_timeseries=this.GetListTimeSeries();
   if(list_timeseries==NULL)
      return;
//--- Получаем общее количество созданных таймсерий
   int total_timeseries=list_timeseries.Total();
   if(total_timeseries==0)
      return;
//--- Устанавливаем размер массива используемых символов равным количеству созданных таймсерий, а
//--- размер массива используемых таймфреймов устанавливаем равным максимально-возможному количеству таймфреймов в терминале
   if(::ArrayResize(ArrayUsedSymbols,total_timeseries,1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,21,21)!=21)
      return;
//--- Обнуляем оба массива
   ::ZeroMemory(ArrayUsedSymbols);
   ::ZeroMemory(ArrayUsedTimeframes);
//--- Сбрасываем количество добавленных символов и таймфреймов в ноль и
//--- в цикле по общему количеству таймсерий
   int num_symbols=0,num_periods=0;
   for(int i=0;i<total_timeseries;i++)
     {
      //--- получаем очередной объект всех таймсерий одного символа
      CTimeSeriesDE *timeseries=list_timeseries.At(i);
      if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol()))
         continue;
      //--- увеличиваем количество используемых символов и (переменная num_symbols) и
      //--- записываем наименование символа таймсерии в массив используемых символов по индексу num_symbols-1
      num_symbols++;
      ArrayUsedSymbols[num_symbols-1]=timeseries.Symbol();
      //--- Из объекта всех таймсерий символа получаем список всех его таймсерий
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- В цикле по общему количеству таймсерий символа
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- получаем очередной объект-таймсерию
         CSeriesDE *series=list_series.At(j);
         if(series==NULL || this.IsExistTimeframe(series.Timeframe()))
            continue;
         //--- увеличиваем количество используемых таймфреймов и (переменная num_periods) и
         //--- записываем значение таймфрейма таймсерии в массив используемых таймфреймов по индексу num_periods-1
         num_periods++;
         ArrayUsedTimeframes[num_periods-1]=series.Timeframe();
        }
     }
//--- По окончании цикла изменяем размеры обоих массивов под точное число добавленных символов и таймфреймов
   ::ArrayResize(ArrayUsedSymbols,num_symbols,1000);
   ::ArrayResize(ArrayUsedTimeframes,num_periods,21);
  }
//+------------------------------------------------------------------+

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

В блоке методов обновления таймсерий добавим защищённый метод для обновления всех таймсерий кроме таймсерий текущего символа:

//--- Обновляет (1) указанную таймсерию указанного символа, (2) все таймсерии указанного символа,
//--- (3) все таймсерии всех символов, (4) все таймсерии кроме текущей
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(const string symbol,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,data_calculate);                                    }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }
protected:
   void                 SeriesRefreshAllExceptCurrent(SDataCalculate &data_calculate)
                          { this.m_time_series.GetObject().RefreshAllExceptCurrent(data_calculate);               }
public  
//--- Возвращает (1) объект таймсерий указанного символа, (2) объект-таймсерию указанного символа/периода

Необходимость такого метода мы рассматривали ранее. Здесь данный метод просто вызывает одноимённый метод класса-коллекции таймсерий, рассмотренный нами выше.

В таймере класса в блоке обработки коллекции таймсерий будем вызывать именно этот метод для обновления всех таймсерий кроме текущей:

//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//
// вырезанный, не нужный для примера код
//
   //--- Таймер коллекции таймсерий
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- Если пауза завершилась - работаем со списком таймсерий (обновляем все кроме текущей)
         if(cnt6.IsTimeDone())
            this.SeriesRefreshAllExceptCurrent(data_calculate);
        }
     }
//--- Если тестер - работаем с событиями коллекций по тику
   else
     {
//
// вырезанный, не нужный для примера код
//
     }
  }

В обработчике события Calculate — в методе OnCalculate() главного объекта библиотеки CEngine — будем возвращать ноль в случае, если не все таймсерии ещё созданы, и rates_total при полном создании всех используемых таймсерий:

//+------------------------------------------------------------------+
//| Обработчик события Calculate                                     |
//+------------------------------------------------------------------+
int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0)
  {
//--- Если это не индикатор - уходим
   if(this.m_program!=PROGRAM_INDICATOR)
      return 0;
//--- Пересоздание пустых таймсерий
//--- Если хоть одна таймсерия не синхронизирована - возвращаем ноль
   if(!this.SeriesSync(data_calculate,required))
      return 0;
//--- Обновляем таймсерии текущего символа (не в тестере) и
//--- возвращаем либо 0 (в случае, если есть пустые таймсерии), либо rates_total
   if(!this.IsTester())
      this.SeriesRefresh(NULL,data_calculate);
   return(this.SeriesGetSeriesEmpty()==NULL ? data_calculate.rates_total : 0);
  }
//+------------------------------------------------------------------+

Ранее мы сразу возвращали rates_total, переданный в метод посредством структуры цен текущего бара. Но для правильной обработки синхронизации таймсерий нам нужно контролировать возвращаемую из метода величину. Ноль возвращается для запуска перерасчёта всей истории, а rates_total — только для расчёта ещё не посчитанных данных (обычно это 0 — расчёт текущего бара или 1 — расчёт прошлого и текущего баров в момент открытия нового бара).

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

//+------------------------------------------------------------------+
//| Создаёт все используемые таймсерии всех используемых символов    |
//+------------------------------------------------------------------+
bool CEngine::SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0)
  {
//--- Устанавливаем флаг успешности создания всех таймсерий всех символов
   bool res=true;
//--- Получаем список всех используемых символов
   CArrayObj* list_symbols=this.GetListAllUsedSymbols();
   if(list_symbols==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY));
      return false;
     }
   //--- В цикле по общему количеству символов
   for(int i=0;i<list_symbols.Total();i++)
     {
      //--- получаем очередной объект-символ
      CSymbol *symbol=list_symbols.At(i);
      if(symbol==NULL)
        {
         ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
         continue;
        }
      //--- В цикле по общему количеству используемых таймфреймов
      int total_periods=::ArraySize(array_periods);
      for(int j=0;j<total_periods;j++)
        {
         //--- создаём объект-таймсерию очередного символа.
         //--- Результат создания таймсерии добавляем к переменной res
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]);
         res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required);
        }
     }
//--- Записываем в массивы ArrayUsedSymbols и ArrayUsedTimeframes все используемые символы и таймфреймы
   this.WriteSymbolsPeriodsToArrays();
//--- Возвращаем результат создания всех таймсерий для всех символов
   return res;
  }
//+------------------------------------------------------------------+

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

На этом доработка классов библиотеки завершена.

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

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

Создание тестового индикатора

Смысл создания прошлого тестового индикатора и того, что сегодня будем делать, кроется не только в том, чтобы протестировать и показать работу в индикаторах с таймсериями библиотеки, но и в обкатке структуры буфера индикатора — на основе полученных знаний использования этой структуры мы будем делать набор классов-буферов индикаторов. Сегодня дополним структуру буфера, сделав её буфером рисования в стиле "Японские свечи".

Для создания тестового индикатора возьмём индикатор из прошлой статьи и сохраним его в новой папке \MQL5\Indicators\TestDoEasy\Part41\ под новым именем TestDoEasyPart41.mq5.

Для начала укажем отрисовку индикатора в отдельном окне, опишем все необходимые буферы индикатора и добавим ещё одну макроподстановку, указывающую на максимальное количество используемых символов (а соответственно — и количество рисуемых буферов индикатора):

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart41.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- properties
#property indicator_separate_window
#property indicator_buffers 21      // 5 массивов (Open[] High[] Low[] Close[] Color[]) * 4 рисуемых буфера + 1 расчётный буфер BufferTime[]
#property indicator_plots   4       // 1 буфер "Японские свечи", состоящий из 5 массивов (Open[] High[] Low[] Close[] Color[]) * 4 символа
//--- plot Pair1
#property indicator_label1  "Pair 1"
#property indicator_type1   DRAW_COLOR_CANDLES
#property indicator_color1  clrLimeGreen,clrRed,clrDarkGray
//--- plot Pair2
#property indicator_label2  "Pair 2"
#property indicator_type2   DRAW_COLOR_CANDLES
#property indicator_color2  clrDeepSkyBlue,clrFireBrick,clrDarkGray
//--- plot Pair3
#property indicator_label3  "Pair 3"
#property indicator_type3   DRAW_COLOR_CANDLES
#property indicator_color3  clrMediumPurple,clrDarkSalmon,clrGainsboro
//--- plot Pair4
#property indicator_label4  "Pair 4"
#property indicator_type4   DRAW_COLOR_CANDLES
#property indicator_color4  clrMediumAquamarine,clrMediumVioletRed,clrGainsboro

//--- classes

//--- enums

//--- defines
#define PERIODS_TOTAL   (21)              // Общее количество доступных периодов графика
#define SYMBOLS_TOTAL   (4)               // Мксимальное количество рисуемых буферов символов
//--- structures

Итак, почему у нас получилось количество буферов индикатора равным 21.
Ответ прост: стиль рисования DRAW_COLOR_CANDLES подразумевает наличие пяти связанных с ним массивов:

  1. массив цен Open
  2. массив цен High
  3. массив цен Low
  4. массив цен Close
  5. массив света (Color)

Мы будем использовать в индикаторе максимальное количество символов равное 4. Соответственно — четыре рисуемых буфера по пять связанных с ними массивами — это уже 20 индикаторных буферов, и ещё один буфер нужен для хранения в нём времени баров, которое будем передавать в функции. Итого: 21 индикаторный буфер, и четыре буфера из них — рисуемые.

Напишем структуру буфера "Японские свечи":

//--- structures
struct SDataBuffer                        // Структура буфера "Японские свечи"
  {
private:
   ENUM_TIMEFRAMES   m_buff_timeframe;    // Таймфрейм буфера
   string            m_buff_symbol;       // Символ буфера
   int               m_buff_open_index;   // Индекс индикаторного буфера, связанного с массивом Open[]
   int               m_buff_high_index;   // Индекс индикаторного буфера, связанного с массивом High[]
   int               m_buff_low_index;    // Индекс индикаторного буфера, связанного с массивом Low[]
   int               m_buff_close_index;  // Индекс индикаторного буфера, связанного с массивом Close[]
   int               m_buff_color_index;  // Индекс буфера цвета, связанного с массивом Color[]
   int               m_buff_next_index;   // Индекс следующего свободного буфера индикатора
   bool              m_used;              // Флаг использования буфера в индикаторе
   bool              m_show_data;         // Флаг отображения буфера на графике до включения/выключения его отображения
public:
   double            Open[];              // Массив, назначаемый индикаторным буфером Open как INDICATOR_DATA
   double            High[];              // Массив, назначаемый индикаторным буфером High как INDICATOR_DATA
   double            Low[];               // Массив, назначаемый индикаторным буфером Low как INDICATOR_DATA
   double            Close[];             // Массив, назначаемый индикаторным буфером Close как INDICATOR_DATA
   double            Color[];             // Массив, назначаемый индикаторным буфером Color как INDICATOR_COLOR_INDEX
//--- Устанавливает индексы рисуемым OHLC и Color буферам
   void              SetIndexes(const int index_first)
                       {
                        this.m_buff_open_index=index_first;
                        this.m_buff_high_index=index_first+1;
                        this.m_buff_low_index=index_first+2;
                        this.m_buff_close_index=index_first+3;
                        this.m_buff_color_index=index_first+4;
                        this.m_buff_next_index=index_first+5;
                       }
//--- Методы установки и возврата значений приватных членов структуры
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe)   { this.m_buff_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); }
   void              SetSymbol(const string symbol)                  { this.m_buff_symbol=symbol;        }
   void              SetUsed(const bool flag)                        { this.m_used=flag;                 }
   void              SetShowDataFlag(const bool flag)                { this.m_show_data=flag;            }
   int               IndexOpenBuffer(void)                     const { return this.m_buff_open_index;    }
   int               IndexHighBuffer(void)                     const { return this.m_buff_high_index;    }
   int               IndexLowBuffer(void)                      const { return this.m_buff_low_index;     }
   int               IndexCloseBuffer(void)                    const { return this.m_buff_close_index;   }
   int               IndexColorBuffer(void)                    const { return this.m_buff_color_index;   }
   int               IndexNextBuffer(void)                     const { return this.m_buff_next_index;    }
   ENUM_TIMEFRAMES   Timeframe(void)                           const { return this.m_buff_timeframe;     }
   string            Symbol(void)                              const { return this.m_buff_symbol;        }
   bool              IsUsed(void)                              const { return this.m_used;               }
   bool              GetShowDataFlag(void)                     const { return this.m_show_data;          }
   void              Print(void);
  };
//--- Вывод данных структуры в журнал
void SDataBuffer::Print(void)
  {
   string array[8];
   array[0]="Buffer "+this.Symbol()+" "+TimeframeDescription(this.Timeframe())+":";
   array[1]=" Open buffer index: "+(string)this.IndexOpenBuffer();
   array[2]=" High buffer index: "+(string)this.IndexHighBuffer();
   array[3]=" Low buffer index: "+(string)this.IndexLowBuffer();
   array[4]=" Close buffer index: "+(string)this.IndexCloseBuffer();
   array[5]=" Color buffer index: "+(string)this.IndexColorBuffer();
   array[6]=" Next buffer index: "+(string)this.IndexNextBuffer();
   array[7]=" Used: "+(string)(bool)this.IsUsed();
   for(int i=0;i<ArraySize(array);i++)
      ::Print(array[i]);
  }
//--- input variables

Структура имеет переменные для хранения значений индексов буферов, связываемых с соответствующими им массивами OHLC и Color — по индексу мы всегда сможем обратиться к нужному буферу. Очередной свободный индекс для привязки нового буфера индикатора к массивам структуры всегда можно получить из переменной m_buff_next_index при помощи метода IndexNextBuffer(), которвй возвращает индекс, идущий следом за буфером цвета в текущей структуре.

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

Для примера, так будут выведены в журнал данные четырёх рисуемых буферов, заданных в настройках индикатора (буфер AUDUSD выводится на график):

2020.04.08 21:55:21.528 Buffer EURUSD H1:
2020.04.08 21:55:21.528  Open buffer index: 0
2020.04.08 21:55:21.528  High buffer index: 1
2020.04.08 21:55:21.528  Low buffer index: 2
2020.04.08 21:55:21.528  Close buffer index: 3
2020.04.08 21:55:21.528  Color buffer index: 4
2020.04.08 21:55:21.528  Next buffer index: 5
2020.04.08 21:55:21.528  Used: false
2020.04.08 21:55:21.530 Buffer AUDUSD H1:
2020.04.08 21:55:21.530  Open buffer index: 5
2020.04.08 21:55:21.530  High buffer index: 6
2020.04.08 21:55:21.530  Low buffer index: 7
2020.04.08 21:55:21.530  Close buffer index: 8
2020.04.08 21:55:21.530  Color buffer index: 9
2020.04.08 21:55:21.530  Next buffer index: 10
2020.04.08 21:55:21.530  Used: true
2020.04.08 21:55:21.532 Buffer EURAUD H1:
2020.04.08 21:55:21.532  Open buffer index: 10
2020.04.08 21:55:21.532  High buffer index: 11
2020.04.08 21:55:21.532  Low buffer index: 12
2020.04.08 21:55:21.532  Close buffer index: 13
2020.04.08 21:55:21.532  Color buffer index: 14
2020.04.08 21:55:21.532  Next buffer index: 15
2020.04.08 21:55:21.532  Used: false
2020.04.08 21:55:21.533 Buffer EURGBP H1:
2020.04.08 21:55:21.533  Open buffer index: 15
2020.04.08 21:55:21.533  High buffer index: 16
2020.04.08 21:55:21.533  Low buffer index: 17
2020.04.08 21:55:21.533  Close buffer index: 18
2020.04.08 21:55:21.533  Color buffer index: 19
2020.04.08 21:55:21.533  Next buffer index: 20
2020.04.08 21:55:21.533  Used: false

Видим, что индекс буфера Open каждого последующего буфера "Японские свечи" совпадает с индексом "Next buffer index" предыдущего буфера "Японские свечи". Из распечатки видно, что следующий свободный буфер имеет индекс 20 — на этот индекс можно назначать следующий, например, расчётный буфер индикатора, что, кстати, и будет сделано для расчётного буфера, хранящего время баров текущего графика.

Добавим блок входных параметров индикатора:

//--- input variables
/*sinput*/ ENUM_SYMBOLS_MODE  InpModeUsedSymbols=  SYMBOLS_MODE_DEFINES;            // Mode of used symbols list
sinput   string               InpUsedSymbols    =  "EURUSD,AUDUSD,EURAUD,EURGBP,EURCAD,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";  // List of used symbols (comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs    =  TIMEFRAMES_MODE_LIST;            // Mode of used timeframes list
sinput   string               InpUsedTFs        =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator)
sinput   uint                 InpButtShiftX     =  0;    // Buttons X shift 
sinput   uint                 InpButtShiftY     =  10;   // Buttons Y shift 
sinput   bool                 InpUseSounds      =  true; // Use sounds
//--- indicator buffers

Так как в перечислении выбора режима использования символов ENUM_SYMBOLS_MODE, расположенного в файле Defines.mqh, есть два не нужных нам здесь режима "Работа с символами из окна "Обзор рынка" и "Работа с полным списком символов":

//+------------------------------------------------------------------+
//| Режимы работы с символами                                        |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Работа только с текущим символом
   SYMBOLS_MODE_DEFINES,                              // Работа с заданным списком символов
   SYMBOLS_MODE_MARKET_WATCH,                         // Работа с символами из окна "Обзор рынка"
   SYMBOLS_MODE_ALL                                   // Работа с полным списком символов
  };
//+------------------------------------------------------------------+

... то во избежание выбора этих двух режимов в настройках, мы сделаем переменную InpModeUsedSymbols не внешней, закомментировав её модификатор sinput. Таким образом режим работы с символами в индикаторе всегда будет "Работа с заданным списком символов", и будут использоваться первые четыре символа из списка, указанного входной переменной InpUsedSymbols.

Впишем определение буферов индикатора и блок глобальных переменных:

//--- indicator buffers
SDataBuffer    Buffers[];                       // Массив структур данных буферов индикатора, привязанных к таймсериям
double         BufferTime[];                    // Расчётный буфер для хранения и передачи данных из массива time[]
//--- global variables
CEngine        engine;                          // Главный объект библиотеки CEngine
string         prefix;                          // Префикс имён графических объектов
int            min_bars;                        // Минимальное количество баров для расчёта индикатора
int            used_symbols_mode;               // Режим работы с символами
string         array_used_symbols[];            // Массив для передачи в библиотеку используемых символов
string         array_used_periods[];            // Массив для передачи в библиотеку используемых таймфреймов
//+------------------------------------------------------------------+

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

Один расчётный буфер — буфер времени — нам нужен будет для передачи в функции индикатора времени баров из предопределённого массива time[] обработчика OnCalculate() индикатора.

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

Например, если мы находимся на периоде графика М15, а данные для отображения берём с графика Н1, то для правильного отображения всех баров нам нужно иметь в наличии минимум 4 бара — ведь в одном часовом баре вмещаются 4 пятнадцатиминутных бара.

Расчётом требуемого количества баров текущего графика, в зависимости от используемого для расчётов периода, будет заниматься ранее написанная нами в файле сервисных функций библиотеки DELib.mqh функция NumberBarsInTimeframe(), рассмотренная нами выше.

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

Рассмотрим написанные вспомогательные функции.

Функция для установки состояний рисуемых буферов индикатора:

//+------------------------------------------------------------------+
//| Устанавливает состояние рисуемым буферам                         |
//+------------------------------------------------------------------+
void SetPlotBufferState(const int buffer_index,const bool state)
  {
//--- В зависимости от переданного состояния устанавливаем выводить данные в окне данных (state==true) или не выводить (state==false)
   PlotIndexSetInteger(buffer_index,PLOT_SHOW_DATA,state);
//--- Создаём описание буфера из символа и таймфрейма и устанавливаем описание буферу по его индексу buffer_index
   string params=Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe());
   string label=params+" Open;"+params+" High;"+params+" Low;"+params+" Close";
   PlotIndexSetString(buffer_index,PLOT_LABEL,(state ? label : NULL));
//--- Если буфер активен (рисуется) - устанавливаем короткое имя индикатору с отображаемым символом и таймфреймом
   if(state)
      IndicatorSetString(INDICATOR_SHORTNAME,engine.Name()+" "+Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe()));
  }  
//+------------------------------------------------------------------+

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

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

Например, если буфер индикатора требует 2 массива, то индексы связанных с этими буферами массивов будут иметь значения 0 и 1. Эти значения устанавливаются при помощи функции SetIndexBuffer(). При использовании одного рисуемого буфера, использующего два массива данных, особых проблем понимания доступа к рисуемому буферу не наблюдается — просто указываем буфер с индексом 0 для доступа к его свойствам.

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

Рассмотрим пример трёх рисуемых буферов с двумя массивами каждый, и номера индексов рисуемых буферов и их массивов:
  • Рисуемый буфер №1 — индекс рисуемого буфера 0
    • Массив №1 — индекс буфера 0
    • Массив №2 — индекс буфера 1
  • Рисуемый буфер №2 — индекс рисуемого буфера 1
    • Массив №1 — индекс буфера 2
    • Массив №2 — индекс буфера 3
  • Рисуемый буфер №3 — индекс рисуемого буфера 2
    • Массив №1 — индекс буфера 4
    • Массив №2 — индекс буфера 5

Казалось бы — массивов здесь шесть для трёх рисуемых буферов, и чтобы получить доступ ко второму рисуемому массиву, нажно обратиться по индексу 2 (ведь 0 и 1 заняты массивами первого буфера). Но нет. Чтобы обратиться ко второму рисуемому буферу, нам нужно обратиться к индексам именно рисуемых буферов, а не всех массивов, назначенных в качестве буферов для каждого рисуемого буфера, а значит — по индексу 1.

Таким образом: чтобы связать массив с буфером функцией SetIndexBuffer() нужно указывать порядковый номер всех массивов, предназначенных для использования в качестве буферов индикатора, но чтобы  получить данные от рисуемого буфера функцией PlotIndexGetInteger() или установить данные рисуемому буферу функциями PlotIndexSetDouble(), PlotIndexSetInteger(), PlotIndexSetString(), нужно указывать индекс нужного рисуемого буфера, а не номер массива. В данном примере для первого рисуемого буфера индекс будет 0, для второго 1, а для третьего 2. Это нужно знать и учитывать.

Функция, возвращающая флаг использования символа, заданного в настройках:

//+------------------------------------------------------------------+
//| Возвращает флаг использования символа, заданного в настройках    |
//+------------------------------------------------------------------+
bool IsUsedSymbolByInput(const string symbol)
  {
   int total=ArraySize(array_used_symbols);
   for(int i=0;i<total;i++)
      if(array_used_symbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

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

Функция, возвращающая индекс рисуемого буфера по символу:

//+------------------------------------------------------------------+
//| Возвращает индекс рисуемого буфера структуры по символу          |
//+------------------------------------------------------------------+
int IndexBuffer(const string symbol)
  {
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].Symbol()==symbol)
         return i;
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

В функцию передаётся наименование символа, индекс буфера которого нужно вернуть. В цикле по всем буферам ищем буфер с таким символом и возвращаем индекс цикла при совпадении. Если буфера с таким символом нет — возвращаем -1.

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

//+------------------------------------------------------------------+
//| Возвращает первый свободный индекс рисуемого буфера              |
//+------------------------------------------------------------------+
int FirstFreePlotBufferIndex(void)
  {
   int num=WRONG_VALUE,total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].IndexNextBuffer()>num)
         num=Buffers[i].IndexNextBuffer();
   return num;
  }
//+------------------------------------------------------------------+

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

Функции, написанные для выполнения вспомогательный действий по установке и поиску состояний кнопок и буферов:

//+------------------------------------------------------------------+
//| Возвращает состояние кнопки                                      |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки                                   |
//+------------------------------------------------------------------+
void SetButtonState(const string button_name,const bool state)
  {
//--- Устанавливаем состояние кнопки и её цвет в зависимости от состояния
   ObjectSetInteger(0,button_name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'220,255,240');
   else
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'240,240,240');
//--- Если не в тестере -
//--- устанавливаем состояние в глобальную переменную терминала
   if(!engine.IsTester())
      GlobalVariableSet((string)ChartID()+"_"+button_name,state);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки символа                           |
//+------------------------------------------------------------------+
void SetButtonSymbolState(const string button_symbol_name,const bool state)
  {
//--- Устанавливаем состояние кнопки символа
   SetButtonState(button_symbol_name,state);
//--- Контроль ошибочного имени при не установленном состоянии кнопки таймфрейма
//--- Записываем в глоб.переменную состояние кнопки только если в её имени нет построки "PERIOD_CURRENT"
   if(StringFind(button_symbol_name,"PERIOD_CURRENT")==WRONG_VALUE)
      GlobalVariableSet((string)ChartID()+"_"+button_symbol_name,state);
//--- Устанавливаем видимость для всех кнопок периодов, соответствующих кнопке символа
   SetButtonPeriodVisible(button_symbol_name,state);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки периода                           |
//+------------------------------------------------------------------+
void SetButtonPeriodState(const string button_period_name,const bool state)
  {
//--- Устанавливаем состояние кнопки и записываем его в глобальную переменную терминала
   SetButtonState(button_period_name,state);
   GlobalVariableSet((string)ChartID()+"_"+button_period_name,state);
  }
//+------------------------------------------------------------------+
//| Устанавливает "видимость" кнопок периодов для кнопки символа     |
//+------------------------------------------------------------------+
void SetButtonPeriodVisible(const string button_symbol_name,const bool state_symbol)
  {
//--- В цикле по количеству используемых таймфреймов
   int total=ArraySize(array_used_periods);
   for(int j=0;j<total;j++)
     {
      //--- создаём имя очередной кнопки периода
      string butt_name_period=button_symbol_name+"_"+EnumToString(ArrayUsedTimeframes[j]);
      //--- Устанавливаем кнопке периода её состояние и видимость в зависимости от состояния кнопки символа
      ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
     }   
  }
//+------------------------------------------------------------------+
//| Сбрасывает состояния остальных кнопок символа                    |
//+------------------------------------------------------------------+
void ResetButtonSymbolState(const string button_symbol_name)
  {
//--- В цикле по всем объектам графика
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- получаем имя очередного объекта
      string name=ObjectName(0,i,0);
      //--- Если это нажатая кнопка, или объект не принадлежит индикатору, или это кнопка периода - идём к следующему
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")>0)
         continue;
      //--- Сбрасываем состояние кнопки символа по имени объекта
      SetButtonSymbolState(name,false);
     }
  }
//+------------------------------------------------------------------+
//| Сбрасывает состояния остальных кнопок периода символа            |
//+------------------------------------------------------------------+
void ResetButtonPeriodState(const string button_period_name,const string symbol)
  {
//--- В цикле по всем объектам графика
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- получаем имя очередного объекта
      string name=ObjectName(0,i,0);
      //--- Если это нажатая кнопка, или объект не принадлежит индикатору, или это не кнопка периода, или кнопка не принадлежит символу - идём к следующему
      if(name==button_period_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- Сбрасываем состояние кнопки периода по имени объекта
      SetButtonPeriodState(name,false);
     }
  }
//+------------------------------------------------------------------+
//| Возвращает имя нажатой кнопки периода, соответствующего символу  |
//+------------------------------------------------------------------+
string GetNamePressedTimeframe(const string button_symbol_name,const string symbol)
  {
//--- В цикле по всем объектам графика
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- получаем имя очередного объекта
      string name=ObjectName(0,i,0);
      //--- Если это нажатая кнопка, или объект не принадлежит индикатору, или это не кнопка периода, или кнопка не принадлежит символу - идём к следующему
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- Если кнопка в нажатом состоянии - возвращаем имя граф.объекта нажатой кнопки
      if(ButtonState(name))
         return name;
     }
//--- Возвращаем NULL если нет нажатых кнопок периода символа
   return NULL;
  }
//+------------------------------------------------------------------+
//| Устанавливает состояния буферов, true - только для указанного    |
//+------------------------------------------------------------------+
void SetAllBuffersState(const string symbol)
  {
   int total=ArraySize(Buffers);
//--- Получаем индекс указанного буфера
   int index=IndexBuffer(symbol);
//--- В цикле по количеству рисуемых буферов
   for(int i=0;i<total;i++)
     {
      //--- если индекс цикла равен индексу указанного буфера -
      //--- устанавливаем флаг его использования в true, иначе - в false
      //--- принудительно устанавливаем флаг того, что нажатие кнопки для буфера уже обработано
      Buffers[i].SetUsed(i!=index ? false : true);
      Buffers[i].SetShowDataFlag(false);
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
//--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
      //--- Получаем индекс рисуемого буфера по таймфрейму, его символ и индекс
   int index=StringFind(button,"_PERIOD_");
   string symbol=StringSubstr(button,5,index-5);
   int buffer_index=IndexBuffer(symbol);
//--- Создаём имя кнопки для глобальной переменной терминала
   string name_gv=(string)ChartID()+"_"+prefix+button;
//--- Получаем состояние кнопки (нажата/отжата), и если не в тестере, то
//--- записываем состояние в глобальную переменную кнопки (1 или 0)
   bool state=ButtonState(button_name);
   if(!engine.IsTester())
      GlobalVariableSet(name_gv,state);
//--- Устанавливаем цвет кнопки в зависимости от её состояния, 
//--- в структуру буфера записываем его состояние в зависимости от состояния кнопки (используется/не используется)
//--- инициализируем буфер, соответствующий таймфрейму кнопки по индексу буфера, полученному ранее
   if(StringFind(button_name,"_PERIOD_")==WRONG_VALUE)
     {
      SetButtonSymbolState(button_name,state);
      ResetButtonSymbolState(button_name);
     }
   else
     {
      SetButtonPeriodState(button_name,state);
      ResetButtonPeriodState(button_name,symbol);
     }
//--- Получаем таймфрейм из нажатой кнопки таймфрейма символа
   string pressed_period=GetNamePressedTimeframe(button_name,symbol);
   ENUM_TIMEFRAMES timeframe=
     (
      StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
      TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
      TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
     );
//--- Устанавливаем состояния всех буферов, true - для буфера символа нажатой кнопки, все остальные - false
   SetAllBuffersState(symbol);
//--- Устанавливаем буферу отображаемый таймфрейм
   Buffers[buffer_index].SetTimeframe(timeframe);
//--- Если нажатие кнопки ещё не обработано
   if(Buffers[buffer_index].GetShowDataFlag()!=state)
     {
      //--- Инициализируем все буферы индикатора
      InitBuffersAll();
      //--- Если буфер активен - заполняем его историческими данными
      if(state)
         BufferFill(buffer_index);
      //--- Устанавливаем буферу флаг того, что нажатие кнопки уже обработано
      Buffers[buffer_index].SetShowDataFlag(state);
     }

//--- Здесь можно вписать дополнительную обработку нажатий кнопок:
//--- Если кнопка в нажатом состоянии
   if(state)
     {
      //--- Если нажата кнопка M1
      if(button=="BUTT_M1")
        {
         
        }
      //--- Если нажата кнопка M2
      else if(button=="BUTT_M2")
        {
         
        }
      //---
      // Остальные кнопки ...
      //---
     }
   //--- Не нажата
   else 
     {
      //--- кнопка M1
      if(button=="BUTT_M1")
        {
         
        }
      //--- кнопка M2
      if(button=="BUTT_M2")
        {
         
        }
      //---
      // Остальные кнопки ...
      //---
     }
//--- перерисуем чарт
   ChartRedraw();
  }
//+------------------------------------------------------------------+

Функция для создания панели кнопок:

//+------------------------------------------------------------------+
//| Создаёт панель кнопок                                            |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int total_symbols=ArraySize(array_used_symbols);
   int total_periods=ArraySize(ArrayUsedTimeframes);
   uint ws=48,hs=18,w=26,h=16,shift_h=2,x=InpButtShiftX+1, y=InpButtShiftY+h+1;
   //--- В цикле по количеству используемых символов
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- создаём имя очередной кнопки символа
      string butt_name_symbol=prefix+"BUTT_"+array_used_symbols[i];
      //--- создаём очередную кнопку символа со смещением, рассчитанным как
      //--- ((высота кнопки + 2) * индекс цикла)
      uint ys=y+(hs+shift_h)*i;
      if(ButtonCreate(butt_name_symbol,x,ys,ws,hs,array_used_symbols[i],clrGray))
        {
         bool state_symbol=(engine.IsTester() && i==0 ? true : false);
         //--- Если не в тестере
         if(!engine.IsTester())
           {
            //--- задаём имя глобальной переменной терминала для хранения состояния кнопки символа
            string name_gv_symbol=(string)ChartID()+"_"+butt_name_symbol;
            //--- если нет глобальной переменной с именем символа - создаём её с состоянием false,
            if(!GlobalVariableCheck(name_gv_symbol))
               GlobalVariableSet(name_gv_symbol,false);
            //--- получаем из глобальной переменной терминала состояние кнопки символа
            state_symbol=GlobalVariableGet(name_gv_symbol);
           }
         //--- Устанавливаем кнопке символа её состояние
         SetButtonState(butt_name_symbol,state_symbol);
         
         //--- В цикле по количеству используемых таймфреймов
         for(int j=0;j<total_periods;j++)
           {
            //--- создаём имя очередной кнопки периода
            string butt_name_period=butt_name_symbol+"_"+EnumToString(ArrayUsedTimeframes[j]);
            uint yp=ys-(hs-h)/2;
            //--- создаём очередную кнопку периода со смещением, рассчитанным как
            //--- (ширина кнопки символа + (ширина кнопки периода + 1) * индекс цикла)
            if(ButtonCreate(butt_name_period,x+ws+2+(w+1)*j,yp,w,h,TimeframeDescription(ArrayUsedTimeframes[j]),clrGray))
               ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS));
            else
              {
               Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_period,"\"");
               return false;
              }
            bool state_period=(engine.IsTester() && ArrayUsedTimeframes[j]==Period() ? true : false);
            //--- Если не в тестере
            if(!engine.IsTester())
              {
               //--- задаём имя глобальной переменной терминала для хранения состояния кнопки периода
               string name_gv_period=(string)ChartID()+"_"+butt_name_period;
               //--- если нет глобальной переменной с именем периода - создаём её с состоянием false,
               if(!GlobalVariableCheck(name_gv_period))
                  GlobalVariableSet(name_gv_period,false);
               //--- получаем из глобальной переменной терминала состояние кнопки периода
               state_period=GlobalVariableGet(name_gv_period);
              }
            //--- Устанавливаем кнопке периода её состояние и видимость в зависимости от состояния кнопки символа
            SetButtonState(butt_name_period,state_period);
            ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
           }   
        }
      else
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_symbol,"\"");
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

Функции для инициализации рисуемых буферов индикатора:

//+------------------------------------------------------------------+
//| Инициализация таймсерии и соответствующих ей буферов по индексу  |
//+------------------------------------------------------------------+
bool InitBuffer(const int buffer_index)
  {
//--- Если передан неверный индекс - уходим
   if(buffer_index==WRONG_VALUE)
      return false;
//--- Инициализируем рисуемые буферы OHLC "пустым" значением, а буфер Color нулём
   ArrayInitialize(Buffers[buffer_index].Open,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].High,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Low,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Close,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Color,0);
//--- Устанавливаем буферу по индексу флаг его отображения в окне данных
   SetPlotBufferState(buffer_index,Buffers[buffer_index].IsUsed());
   return true;
  }
//+------------------------------------------------------------------+
//| Инициализация всех таймсерий и соответствующих им буферов        |
//+------------------------------------------------------------------+
void InitBuffersAll(void)
  {
//--- В цикле по общему количеству периодов графика инициализируем очередной буфер
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      InitBuffer(i);
  }
//+------------------------------------------------------------------+

Функция для расчёта одного бара всех активных буферов индикатора:

//+------------------------------------------------------------------+
//| Расчёт одного бара всех активных буфеов                          |
//+------------------------------------------------------------------+
void CalculateSeries(const int index,const datetime time)
  {
//--- В цикле по общему количеству буферов получаем очередной буфер
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
     {
      //--- если буфер не используется (отжата кнопка символа) - идём к следующему
      if(!Buffers[i].IsUsed())
        {
         SetBufferData(i,index,NULL);
         continue;
        }
      //--- получаем объект-таймсерию по таймфрейму буфера
      CSeriesDE *series=engine.SeriesGetSeries(Buffers[i].Symbol(),(ENUM_TIMEFRAMES)Buffers[i].Timeframe());   // Здесь нужно брать таймфрейм от нажатой кнопки рядом с нажатой кнопкой символа
      //--- если таймсерия не получена
      //--- или переданный в функцию индекс бара (index) за пределами общего количества баров в таймсерии - идём к следующему буферу
      if(series==NULL || index>series.GetList().Total()-1)
         continue;
      //--- получаем объект-бар из таймсерии, соответствующий переданному в функцию времени бара (time) на текущем графике
      CBar *bar=engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,Buffers[i].Symbol(),Buffers[i].Timeframe());
      if(bar==NULL)
         continue;
      //--- получаем указанное свойство из полученного бара и
      //--- вызываем функцию записи этого значения в буфер по индексу i
      SetBufferData(i,index,bar);
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Запись данных одного бара в указанный буфер                      |
//+------------------------------------------------------------------+
void SetBufferData(const int buffer_index,const int index,const CBar *bar)
  {
//--- Получаем индекс бара по его времени, попадающий в пределы этого времени на текущем графике
   int n=(bar!=NULL ? iBarShift(NULL,PERIOD_CURRENT,bar.Time()) : index);
//--- Если переданный индекс на текущем графике (index) меньше рассчитанного времени начала бара на другом таймфрейме
   if(index<n)
      //--- в цикле от бара n на текущем графике до нуля
      while(n>WRONG_VALUE && !IsStopped())
        {
         //--- заполняем буфер по индексу n переданным значениями, переданного в функцию бара (если ноль, то EMPTY_VALUE)
         //--- и уменьшаем значение n
         Buffers[buffer_index].Open[n]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE);
         Buffers[buffer_index].High[n]=(bar.High()>0 ? bar.High() : EMPTY_VALUE);
         Buffers[buffer_index].Low[n]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE);
         Buffers[buffer_index].Close[n]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE);
         Buffers[buffer_index].Color[n]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2);
         n--;
        }
//--- Если переданный индекс на текущем графике (index) не меньше рассчитанного времени начала бара на другом таймфрейме
//--- Устанавливаем буферу по переданному в функцию индексу index значением value (если ноль, то EMPTY_VALUE)
   else
     {
      //--- Если в функцию передан объект-бар - заполняем буферы индикатора его данными
      if(bar!=NULL)
        {
         Buffers[buffer_index].Open[index]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE);
         Buffers[buffer_index].High[index]=(bar.High()>0 ? bar.High() : EMPTY_VALUE);
         Buffers[buffer_index].Low[index]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE);
         Buffers[buffer_index].Close[index]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE);
         Buffers[buffer_index].Color[index]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2);
        }
      //--- Если в функцию передан вместо объекта-бара NULL - заполняем буферы индикатора пустым значением
      else
        {
         Buffers[buffer_index].Open[index]=Buffers[buffer_index].High[index]=Buffers[buffer_index].Low[index]=Buffers[buffer_index].Close[index]=EMPTY_VALUE;
         Buffers[buffer_index].Color[index]=2;
        }
     }
  }
//+------------------------------------------------------------------+


Теперь рассмотрим обработчик OnInit() индикатора, в котором создаются кнопки и подготавливаются все буферы индикатора:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Инициализация библиотеки DoEasy
   OnInitDoEasy();

//--- Установка глобальных переменных индикатора
   prefix=engine.Name()+"_";
   //--- Получаем индекс максимального используемого таймфрейма в массиве,
   //--- рассчитываем количество баров текущего периода, умещающихся в максимальном используемом периоде
   //--- Используем полученное значение если оно больше 2, иначе используем 2
   int index=ArrayMaximum(ArrayUsedTimeframes);
   int num_bars=NumberBarsInTimeframe(ArrayUsedTimeframes[index]);
   min_bars=(index>WRONG_VALUE ? (num_bars>2 ? num_bars : 2) : 2);
//--- Проверка и удаление неудалённых графических объектов индикатора
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

//--- Создание панели кнопок
   if(!CreateButtons(InpButtShiftX,InpButtShiftY))
      return INIT_FAILED;

//--- Проверка воспроизведения стандартного звука по макроподстановкам
   engine.PlaySoundByDescription(SND_OK);
//--- Ждём 600 милисекунд
   engine.Pause(600);
   engine.PlaySoundByDescription(SND_NEWS);

//--- indicator buffers mapping
   //--- В цикле по общему количеству всех символов
   int total_symbols=ArraySize(array_used_symbols);
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- получаем очередной символ
      //--- если индекс цикла меньше размера массива используемых символов, то имя символа берём из массива
      //--- иначе - это пустой (не используемый) буфер, и имя символа для буфера будет "EMPTY "+индекс цикла
      string symbol=(i<total_symbols ? array_used_symbols[i] : "EMPTY "+string(i+1));
      //--- Увеличиваем размер массива структур буферов и устанавливаем символ буфера и
      ArrayResize(Buffers,ArraySize(Buffers)+1,SYMBOLS_TOTAL);
      Buffers[i].SetSymbol(symbol);
      //--- устанавливаем значения всех индексов для связывания буферов индикатора с массивами структуры и
      //--- указываем индекс следующего буфера
      int index_first=(i==0 ? i : Buffers[i-1].IndexNextBuffer());
      Buffers[i].SetIndexes(index_first);
      
      //--- Настройка рисуемого буфера в соответствии с состоянием кнопки
      //--- Задаём состояние кнопки символа. В тестере первая кнопка будет активной
      bool state_symbol=(engine.IsTester() && i==0 ? true : false);
      //--- Задаём имя кнопки символа, соответствующей буферу с индексом цикла и его таймфрейму
      string name_butt_symbol=prefix+"BUTT_"+Buffers[i].Symbol();
      string name_butt_period=name_butt_symbol+"_PERIOD_"+TimeframeDescription(Buffers[i].Timeframe());
      //--- Если не в тестере, и есть на графике кнопка с заданным именем
      if(!engine.IsTester() && ObjectFind(ChartID(),name_butt_symbol)==0)
        {
         //--- задаём имя глобальной переменной терминала для хранения состояния кнопки
         string name_gv_symbol=(string)ChartID()+"_"+name_butt_symbol;
         string name_gv_period=(string)ChartID()+"_"+name_butt_period;
         //--- получаем из глобальной переменной терминала состояние кнопки символа
         state_symbol=GlobalVariableGet(name_gv_symbol);
        }
      
      //--- Получаем таймфрейм от нажатых кнопок таймфреймов символа
      string pressed_period=GetNamePressedTimeframe(name_butt_symbol,symbol);
      //--- Преобразуем имя кнопки в её строковый идентификатор
      string button=StringSubstr(name_butt_symbol,StringLen(prefix));
      ENUM_TIMEFRAMES timeframe=
        (
         StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
         TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
         TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
        );
      
      //--- Задаём значения всем полям структуры
      Buffers[i].SetTimeframe(timeframe);
      Buffers[i].SetUsed(state_symbol);
      Buffers[i].SetShowDataFlag(state_symbol);
      
      //--- Связываем рисуемые индикаторные буферы по индексу буфера, равному индексу цикла с массивами цен бара (OHLC) структуры
      SetIndexBuffer(Buffers[i].IndexOpenBuffer(),Buffers[i].Open,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexHighBuffer(),Buffers[i].High,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexLowBuffer(),Buffers[i].Low,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexCloseBuffer(),Buffers[i].Close,INDICATOR_DATA);
      //--- Связываем буфер цвета по индексу буфера, равному индексу цикла с соответствующими массивами структуры
      SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX);
      //--- задаём "пустое значение" для всех буферов структуры, 
      PlotIndexSetDouble(Buffers[i].IndexOpenBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexHighBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexLowBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexCloseBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexColorBuffer(),PLOT_EMPTY_VALUE,0);
      //--- устанавливаем тип рисования
      PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES);
      //--- В зависимости от состояния кнопки устанавливаем имя графической серии
      //--- и указываем отображать или нет данные буфера в окне данных
      SetPlotBufferState(i,state_symbol);
      //--- устанавливаем направление индексации всех буферов структуры как в таймсерии
      ArraySetAsSeries(Buffers[i].Open,true);
      ArraySetAsSeries(Buffers[i].High,true);
      ArraySetAsSeries(Buffers[i].Low,true);
      ArraySetAsSeries(Buffers[i].Close,true);
      ArraySetAsSeries(Buffers[i].Color,true);
      
      //--- Распечатаем в журнал данные очередного буфера
      //Buffers[i].Print();
     }
   //--- Связываем расчётный индикаторный буфер по индексу буфера FirstFreePlotBufferIndex() с массивом BufferTime[] индикатора
   //--- и устанавливаем направление индексации расчётного буфера BufferTime[] как в таймсерии
   int buffer_temp_index=FirstFreePlotBufferIndex();
   SetIndexBuffer(buffer_temp_index,BufferTime,INDICATOR_CALCULATIONS);
   ArraySetAsSeries(BufferTime,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Рассмотрим обработчик OnCalculate() индикатора:

//+------------------------------------------------------------------+
//| 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[])
  {
//+------------------------------------------------------------------+
//| OnCalculate Блок кода для работы с библиотекой:                  |
//+------------------------------------------------------------------+
   
//--- Передача в структуру цен текущих данных массивов из OnCalculate()
   CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread);

//--- Проверка на минимальное количество баров для расчёта
   if(rates_total<min_bars || Point()==0) return 0;
   
//--- Обработка события Calculate в библиотеке
//--- Если метод OnCalculate() библиотеки вернул ноль - значит не все таймсерии готовы - уходим до следкющего тика
   if(engine.0)
      return 0;
   
//--- Если работа в тестере
   if(MQLInfoInteger(MQL_TESTER)) 
     {
      engine.OnTimer(rates_data);   // Работа в таймере
      PressButtonsControl();        // Контроль нажатия кнопок
      EventsHandling();             // Работа с событиями
     }
//+------------------------------------------------------------------+
//| OnCalculate Блок кода для работы с индикатором:                  |
//+------------------------------------------------------------------+
//--- Установка массивов OnCalculate как таймсерий
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Проверка и расчёт количества просчитываемых баров
//--- Если limit = 0, значит новых баров нет - рассчитываем текущий
//--- Если limit = 1, значит появился новый бар - рассчитываем первый и текущий
//--- Если limit > 1, значит есть изменения в истории - полный перерасчёт всех данных
   int limit=rates_total-prev_calculated;
   
//--- Перерасчёт всей истории
   if(limit>1)
     {
      limit=rates_total-1;
      InitBuffersAll();
     }
//--- Подготовка данных

//--- Расчёт индикатора
   for(int i=limit; i>WRONG_VALUE && !IsStopped(); i--)
     {
      BufferTime[i]=(double)time[i];
      CalculateSeries(i,time[i]);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

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

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

Скомпилируем индикатор и запустим его на графике EURUSD M15:


Что видим: отобразились четыре кнопки с первыми четырьмя символами. Пока не была нажата ни одна из кнопок, для них не отображаются кнопки выбора периода для отображения. Стоит нажать на кнопку символа — для неё раскрывается список из кнопок выбора периода. Выбираем период и видим как на графике отображаются свечи выбранного символа и выбранного периода. Теперь состояние выбранных кнопок записано в глобальные переменные терминала, и после перезапуска индикатора или нажатия на кнопку другого символа и последующего возврата к предыдущему, для него уже будут отображены кнопки периодов с выбранной кнопкой ранее использованного периода.

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

Ну и на протяжении двух последних статей мы познакомились со способами лёгкого создания мультисимвольных мультипериодных индикаторов при помощи таймсерий библиотеки.

Что дальше

Со следующей статьи начнём разработку классов индикаторных буферов.

Ниже прикреплены все файлы текущей версии библиотеки и файлы тестового советника. Их можно скачать и протестировать всё самостоятельно.
При возникновении вопросов, замечаний и пожеланий, вы можете озвучить их в комментариях к статье.
Хочу обратить внимание на то, что в данной статье мы сделали тестовый индикатор на MQL5 для MetaTrader 5.
Приложенные файлы предназначены только для MetaTrader 5 и в MetaTrader 4 библиотека в её текущей версии не тестировалась.
Для четвёртой версии используемый сегодня тип рисования буферов (DRAW_COLOR_CANDLES) не поддерживается, но при создании классов буферов индикаторов некоторые вещи из MQL5 мы попробуем реализовать и для MetaTrader 4.

К содержанию

Статьи этой серии:

Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа
Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа
Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам
Работа с таймсериями в библиотеке DoEasy (Часть 38): Коллекция таймсерий - реалтайм обновление и доступ к данным из программы
Работа с таймсериями в библиотеке DoEasy (Часть 39): Индикаторы на основе библиотеки - подготовка данных и события таймсерий
Работа с таймсериями в библиотеке DoEasy (Часть 40): Индикаторы на основе библиотеки - реалтайм обновление данных

Прикрепленные файлы |
MQL5.zip (3731.94 KB)
Язык MQL как средство разметки графического интерфейса MQL-программ (Часть 3). Дизайнер форм Язык MQL как средство разметки графического интерфейса MQL-программ (Часть 3). Дизайнер форм

В этой статье мы завершаем описание концепции построения оконного интерфейса MQL-программ с помощью конструкций языка MQL. Специальный графический редактор позволит интерактивно настраивать раскладку, состоящую из основных классов элементов GUI, и затем экспортировать её в MQL-описание для использования в вашем MQL-проекте. Представлено внутреннее устройство редактора и руководство пользователя. Исходные коды прилагаются.

Создаем кроссплатформенный советник-сеточник: тестируем мультивалютный советник Создаем кроссплатформенный советник-сеточник: тестируем мультивалютный советник

За месяц рынки упали более чем на 30%. Это ли не лучшее время для тестирования советников на основе сеток и мартингейл? Данная статья является продолжением серии статей "Создаем кроссплатформенный советник-сеточник", выход которого не планировался. Но раз сам рынок предоставляет возможность устроить советнику-сеточнику стресс-тестирование, почему бы этим не воспользоваться. Так давайте займемся этим.

Непрерывная скользящая оптимизация (Часть 7): Стыковка логической части автооптимизатора с графикой и управление графикой из программы Непрерывная скользящая оптимизация (Часть 7): Стыковка логической части автооптимизатора с графикой и управление графикой из программы

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

Работа с таймсериями в библиотеке DoEasy (Часть 42): Класс объекта абстрактного индикаторного буфера Работа с таймсериями в библиотеке DoEasy (Часть 42): Класс объекта абстрактного индикаторного буфера

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