Работа с таймсериями в библиотеке DoEasy (Часть 39): Индикаторы на основе библиотеки - подготовка данных и события таймсерий

Artyom Trishkin | 24 марта, 2020

Содержание


Концепция

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

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

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

При запросе данных любой таймсерии любого символа Copy-функциями, в индикаторе и советнике будет разное поведение при отдаче исторических данных терминалом:

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

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

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

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

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

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

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

Доработка классов для работы с индикаторами, создание событий таймсерий

Для начала в файл Datas.mqh добавим новые сообщения библиотеки — индексы сообщений:

   MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,        // Не удалось подготовить массив используемых символов. Ошибка 
   MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY,              // Не удалось получить массив используемых символов.
   MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING,            // Ошибка. Строка предопределённых периодов пустая, будет использоваться

...

//--- CBar
   MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA,              // Не удалось получить данные бара
   MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE,           // Не удалось записать время в структуру времени
   MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA,           // Не удалось получить данные таймсерии

...

   MSG_LIB_TEXT_TS_TEXT_SYMBOL_TERMINAL_FIRSTDATE,    // Самая первая дата в истории по символу в клиентском терминале
   MSG_LIB_TEXT_TS_TEXT_CREATED_OK,                   // создана успешно
   MSG_LIB_TEXT_TS_TEXT_NOT_CREATED,                  // не создана
   MSG_LIB_TEXT_TS_TEXT_IS_SYNC,                      // синхронизирована
   MSG_LIB_TEXT_TS_TEXT_ATTEMPT,                      // Попытка:
   MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC,                // Ожидание синхронизации данных ...

  };
//+------------------------------------------------------------------+

и тексты сообщений, соответствующие вновь добавленным индексам:

   {"Не удалось подготовить массив используемых символов. Ошибка ","Failed to create an array of used symbols. Error "},
   {"Не удалось получить массив используемых символов","Failed to get array of used symbols"},
   {"Ошибка. Строка предопределённых периодов пустая, будет использоваться ","Error. String of predefined periods is empty, the Period will be used: "},

...

   {"Не удалось получить данные бара","Failed to get bar data"},
   {"Не удалось записать время в структуру времени","Failed to write time to datetime structure"},
   {"Не удалось получить данные таймсерии","Failed to get timeseries data"},

...

   {"Самая первая дата в истории по символу в клиентском терминале","The very first date in the history of the symbol in the client terminal"},
   {"создана успешно","created successfully"},
   {"не создана","not created"},
   {"синхронизирована","synchronized"},
   {"Попытка: ","Attempt: "},
   {"Ожидание синхронизации данных ...","Waiting for data synchronization ..."},
   
  };
//+---------------------------------------------------------------------+

В конструкторе класса базового объекта всех объектов библиотеки CBaseObj в файле \MQL5\Include\DoEasy\Objects\BaseObj.mqh изменена инициализация переменной m_available — сразу при создании все объекты-наследники базового класса CBaseObj будут иметь свойство своей доступности для работы с ними в программе в состоянии "используется" (true). Ранее значение устанавливалось при инициализации в состояние "не используется" false:

//--- Конструктор
                     CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                                  m_global_error(ERR_SUCCESS),
                                  m_log_level(LOG_LEVEL_ERROR_MSG),
                                  m_chart_id_main(::ChartID()),
                                  m_chart_id(::ChartID()),
                                  m_folder_name(DIRECTORY),
                                  m_sound_name(""),
                                  m_name(__FUNCTION__),
                                  m_type(0),
                                  m_use_sound(false),
                                  m_available(true),
                                  m_first_start(true) {}
  };
//+------------------------------------------------------------------+

И было изменено название метода, устанавливающего флаг того, что в объекте зарегистрировано событие, в классе расширенного базового объекта всех объектов библиотеки CBaseObjExt в файле \MQL5\Include\DoEasy\Objects\BaseObj.mqh:

//--- Устанавливает/Возвращает флаг произошедшего события в данных объекта
   void              SetEventFlag(const bool flag)                   { this.m_is_event=flag;                   }

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

Соответственно, в файлы классов, в которых использовался этот метод, также были внесены изменения — заменён вызов метода SetEvent() на вызов SetEventFlag(), что тут описывать и показывать не имеет смысла — всё есть в прилагаемых к статье файлах.

Так как в индикаторах торговые функции запрещены, то внесём изменения в классы торговых объектов.
В классе кроссплатформенного торгового объекта в файле \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh в начале всех торговых методов впишем проверку на тип программы, и если это индикатор или сервис, то уходим из метода с возвратом true:

//+------------------------------------------------------------------+
//| Открывает позицию                                                |
//+------------------------------------------------------------------+
bool CTradeObj::OpenPosition(const ENUM_POSITION_TYPE type,
                             const double volume,
                             const double sl=0,
                             const double tp=0,
                             const ulong magic=ULONG_MAX,
                             const string comment=NULL,
                             const ulong deviation=ULONG_MAX,
                             const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Закрывает позицию                                                |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePosition(const ulong ticket,
                              const string comment=NULL,
                              const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Частично закрывает позицию                                       |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionPartially(const ulong ticket,
                                       const double volume,
                                       const string comment=NULL,
                                       const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Закрывает позицию встречной                                      |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionBy(const ulong ticket,const ulong ticket_by)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Модифицирует позицию                                             |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyPosition(const ulong ticket,const double sl=WRONG_VALUE,const double tp=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Устанавливает ордер                                              |
//+------------------------------------------------------------------+
bool CTradeObj::SetOrder(const ENUM_ORDER_TYPE type,
                         const double volume,
                         const double price,
                         const double sl=0,
                         const double tp=0,
                         const double price_stoplimit=0,
                         const ulong magic=ULONG_MAX,
                         const string comment=NULL,
                         const datetime expiration=0,
                         const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                         const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Удаляет ордер                                                    |
//+------------------------------------------------------------------+
bool CTradeObj::DeleteOrder(const ulong ticket)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Модифицирует ордер                                               |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyOrder(const ulong ticket,
                            const double price=WRONG_VALUE,
                            const double sl=WRONG_VALUE,
                            const double tp=WRONG_VALUE,
                            const double price_stoplimit=WRONG_VALUE,
                            const datetime expiration=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

Точно такие же изменения были сделаны во всех одноимённых торговых методах основного торгового класса библиотеки
в файле \MQL5\Include\DoEasy\Trading.mqh.

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

Теперь рассмотрим изменения, коснувшиеся непосредственно классов объектов таймсерий.

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

//+------------------------------------------------------------------+
//| Конструктор 1                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   this.m_type=COLLECTION_SERIES_ID;
   MqlRates rates_array[1];
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- Если не удалось получить запрашиваемые данные по индексу и записать данные бара в MqlRates-массив
//--- выводим сообщение об ошибке, создаём и заполняем структуру нулями и записываем её в массив rates_array
   if(::CopyRates(symbol,timeframe,index,1,rates_array)<1)
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
      MqlRates err={0};
      rates_array[0]=err;
     }
   ::ResetLastError();
//--- Если произошла ошибка установки времени в структуру времени - выводим сообщение об ошибке
   if(!::TimeToStruct(rates_array[0].time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
     }
//--- Устанавливаем свойства бара
   this.SetProperties(rates_array[0]);
  }
//+------------------------------------------------------------------+
//| Конструктор 2                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- Если произошла ошибка установки времени в структуру времени, выводим сообщение об ошибке,
//--- создаём и заполняем структуру нулями, устанавливаем свойства бара из этой структуры и выходим
   if(!::TimeToStruct(rates.time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(2) ",symbol," ",TimeframeDescription(timeframe)," ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
      MqlRates err={0};
      this.SetProperties(err);
      return;
     }
//--- Устанавливаем свойства бара
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+

Эти действия дают нам больше данных при ошибке создания объекта-бара.

Так как для запроса данных о количестве баров и их значениях на текущем символе-периоде нам нужно использовать массивы таймсерий, предоставляемые обработчиком OnCalculate(), то эти массивы и значения нам нужно как-то передать в классы библиотеки.
Для этого создадим структуру в файле \MQL5\Include\DoEasy\Defines.mqh, в которой будут храниться переменные, через которые и будем передавать все необходимые данные, рассчитываемые для текущей таймсерии, в таймсерии библиотеки:

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

Как видим, структура содержит все необходимые поля для передачи данных в библиотеку при любой реализации обработчика OnCalculate() индикатора.

Для первой формы обработчика

int OnCalculate(
   const int        rates_total,       // размер массива price[]
   const int        prev_calculated,   // количество обработанных баров на предыдущем вызове
   const int        begin,             // номер индекса в массиве price[], с которого начинаются значимые данные
   const double&    price[]            // массив значений для расчета
   );

используются переменные структуры rates_total, prev_calculated, begin и price.

Для второй формы обработчика

int OnCalculate(
   const int        rates_total,       // размер входных таймсерий
   const int        prev_calculated,   // количество обработанных баров на предыдущем вызове
   const datetime&  time{},            // массив Time
   const double&    open[],            // массив Open
   const double&    high[],            // массив High
   const double&    low[],             // массив Low
   const double&    close[],           // массив Close
   const long&      tick_volume[],     // массив Tick Volume
   const long&      volume[],          // массив Real Volume
   const int&       spread[]           // массив Spread
   );

используются переменные структуры rates_total, prev_calculated и структура MqlRates rates для хранения значений массивов.

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

В классе CSeries в файле \MQL5\Include\DoEasy\Objects\Series\Series.mqh в методы установки символа и таймфрейма добавим флаг установки дат сервера:

//--- Устанавливает (1) символ, (2) таймфрейм, (3) символ и таймфрейм, (4) количество используемых данных таймсерии
   void              SetSymbol(const string symbol,const bool set_server_date=false);
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false);

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

//+------------------------------------------------------------------+
//| Устанавливает символ                                             |
//+------------------------------------------------------------------+
void CSeries::SetSymbol(const string symbol,const bool set_server_date=false)
  {
   if(this.m_symbol==symbol)
      return;
   this.m_symbol=(symbol==NULL || symbol==""   ? ::Symbol() : symbol);
   this.m_new_bar_obj.SetSymbol(this.m_symbol);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+
//| Устанавливает таймфрейм                                          |
//+------------------------------------------------------------------+
void CSeries::SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false)
  {
   if(this.m_timeframe==timeframe)
      return;
   this.m_timeframe=(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe);
   this.m_new_bar_obj.SetPeriod(this.m_timeframe);
   this.m_period_description=TimeframeDescription(this.m_timeframe);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Устанавливает символ и таймфрейм                                 |
//+------------------------------------------------------------------+
void CSeries::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_symbol==symbol && this.m_timeframe==timeframe)
      return;
   this.SetSymbol(symbol);
   this.SetTimeframe(timeframe,true);
  }
//+------------------------------------------------------------------+

Здесь: сначала вызывается метод установки символа (флаг снят), затем вызывается метод установки таймфрейма с установленным флагом для вызова метода установки дат сервера из метода установки таймфрейма.

В метод обновления данных таймсерии теперь передаётся новая структура данных обработчика OnCalculate() вместо полного списка его массивов:

//--- (1) Создаёт, (2) обновляет список-таймсерию
   int               Create(const uint required=0);
   void              Refresh(SDataCalculate &data_calculate);
                            
//--- Создаёт и отправляет событие "Новый бар" на график управляющей программы
   void              SendEvent(void);

Соответственно, в реализации метода Refresh() теперь идёт обращение не к массивам, а к данным этой структуры:

//+------------------------------------------------------------------+
//| Обновляет список и данные тайм-серии                             |
//+------------------------------------------------------------------+
void CSeries::Refresh(SDataCalculate &data_calculate)
  {
//--- Если таймсерия не используется - выходим
   if(!this.m_available)
      return;
   MqlRates rates[1];
//--- Устанавливаем флаг сортировки списка баров по индексу
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
//--- Если есть новый бар на символе и периоде
   if(this.IsNewBarManual(data_calculate.rates.time))
     {
      //--- создаём новый объект-бар и добавляем его в конец списка
      CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,0);
      if(new_bar==NULL)
         return;
      if(!this.m_list_series.InsertSort(new_bar))
        {
         delete new_bar;
         return;
        }
      //--- Записываем самую первую дату по символу-периоду на данный момент и новое время открытия последнего бара по символу-периоду 
      this.SetServerDate();
      //--- если размер таймсерии стал больше запрашиваемого количества баров - удаляем самый ранний бар
      if(this.m_list_series.Total()>(int)this.m_required)
         this.m_list_series.Delete(0);
      //--- сохраняем новое время бара как прошлое для последующей проверки на новый бар
      this.SaveNewBarTime(data_calculate.rates.time);
     }
//--- Получаем объект-бар из списка по индексу таймсерии терминала (нулевой бар)
   CBar *bar=this.GetBarBySeriesIndex(0);
//--- если работа в индикаторе, и таймсерия принадлежит текущему символу и таймфрейму,
//--- копируем в структуру цен бара переданные в метод извне параметры цен
   int copied=1;
   if(this.m_program==PROGRAM_INDICATOR && this.m_symbol==::Symbol() && this.m_timeframe==(ENUM_TIMEFRAMES)::Period())
     {
      rates[0].time=data_calculate.rates.time;
      rates[0].open=data_calculate.rates.open;
      rates[0].high=data_calculate.rates.high;
      rates[0].low=data_calculate.rates.low;
      rates[0].close=data_calculate.rates.close;
      rates[0].tick_volume=data_calculate.rates.tick_volume;
      rates[0].real_volume=data_calculate.rates.real_volume;
      rates[0].spread=data_calculate.rates.spread;
     }
//--- иначе - получаем данные в структуру цен бара из окружения
   else
      copied=::CopyRates(this.m_symbol,this.m_timeframe,0,1,rates);
//--- Если цены получены - устанавливаем объекту-бару новые свойства из структуры цен
   if(copied==1)
      bar.SetProperties(rates[0]);
  }
//+------------------------------------------------------------------+

Для возможности поиска в списке объектов-таймсерий по их таймфрейму теперь реализован виртуальный метод сравнения двух объектов-таймсерий:

//--- Метод сравнения для поиска по таймфрейму одинаковых объектов-таймсерий
   virtual int       Compare(const CObject *node,const int mode=0) const 
                       {   
                        const CSeries *compared_obj=node;
                        return(this.Timeframe()>compared_obj.Timeframe() ? 1 : this.Timeframe()<compared_obj.Timeframe() ? -1 : 0);
                       } 
//--- Конструкторы
                     CSeries(void);
                     CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
  };
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает количество требуемых данных                        |
//+------------------------------------------------------------------+
bool CSeries::SetRequiredUsedData(const uint required,const uint rates_total)
  {
   this.m_required=(required<1 ? SERIES_DEFAULT_BARS_COUNT : required);
//--- Запустим закачку исторических данных
   if(this.m_program!=PROGRAM_INDICATOR || (this.m_program==PROGRAM_INDICATOR && (this.m_symbol!=::Symbol() || this.m_timeframe!=::Period())))
     {
      datetime array[1];
      ::CopyTime(this.m_symbol,this.m_timeframe,0,1,array);
     }
//--- Установим количество доступных баров таймсерии


Когда мы создавали объект, хранящий списки всех таймсерий одного символа (класс CTimeSeries), мы сделали так, что этот объект всегда имеет список, в котором записан полный набор всех возможных в терминале таймфреймов. И списки-таймсерии сразу же добавляются в этот список, но при этом не создаются. Создаются они по мере необходимости. И обращение к указателям на нужный список-таймсерию мы сделали по неизменному индексу, соответствующему положению индекса таймфрейма списка в перечислении ENUM_TIMEFRAMES со смещением на 1 (описано в статье).

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

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

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

Откроем файл \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh и внесём в него необходимые доработки.

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

//+------------------------------------------------------------------+
//| Класс "Таймсерии символа"                                        |
//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {

Метод, возвращающий индекс таймсерии в списке по наименованию таймфрейма теперь просто объявлен в приватной секции класса:

//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {
private:
   string            m_symbol;                                             // Символ таймсерий
   CNewTickObj       m_new_tick;                                           // Объект "Новый тик"
   CArrayObj         m_list_series;                                        // Список таймсерий по таймфреймам
   datetime          m_server_firstdate;                                   // Самая первая дата в истории по символу на сервере
   datetime          m_terminal_firstdate;                                 // Самая первая дата в истории по символу в клиентском терминале
//--- Возвращает (1) индекс таймфрейма в списке, (2) таймфрейм по индексу списка
   int               IndexTimeframe(const ENUM_TIMEFRAMES timeframe);
   ENUM_TIMEFRAMES   TimeframeByIndex(const uchar index)             const { return TimeframeByEnumIndex(uchar(index+1));                       }
//--- Устанавливает самую первую дату в истории по символу на сервере и в клиентском терминале
   void              SetTerminalServerDate(void)
                       {
                        this.m_server_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE);
                        this.m_terminal_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE);
                       }
public:

Реализация этого метода теперь выполнена за пределами тела класса:

//+------------------------------------------------------------------+
//| Возвращает индекс таймфрейма в списке                            |
//+------------------------------------------------------------------+
int CTimeSeries::IndexTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   const CSeries *obj=new CSeries(this.m_symbol,timeframe);
   if(obj==NULL)
      return WRONG_VALUE;
   this.m_list_series.Sort();
   int index=this.m_list_series.Search(obj);
   delete obj;
   return index;
  }
//+------------------------------------------------------------------+

В метод передаётся таймфрейм, указатель на таймсерию которого необходимо вернуть.
Далее создаём временный объект-таймсерию с искомым таймфреймом.
Устанавливаем списку объектов-таймсерий флаг сортированного списка
и
получаем индекс объекта-таймсерии в списке, у которого таймфрейм равен таймфрейму временного объекта
.
Если такой объект существует в списке, то будет получен его индекс, иначе — WRONG_VALUE (-1).
Удаляем временный объект и возвращаем полученный индекс.

Вместо методов Create() и CreateAll() объявим методы для добавления указанной таймсерии в список и метод создания указанного объекта-таймсерии,
а методы обновления списков-таймсерий теперь получают структуру значений параметров и массивов OnCalculate() вместо полного списка массивов:

//--- (1) Добавляет в список указанную список-таймсерию, создаёт (2) указанный список-таймсерию
   bool              AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool              CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
//--- Обновляет (1) указанный список-таймсерию, (2) все списки-таймсерии
   void              Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void              RefreshAll(SDataCalculate &data_calculate);

//--- Сравнивает объекты CTimeSeries между собой (по символу)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Выводит в журнал (1) описание, (2) краткое описание таймсерий символа
   void              Print(const bool created=true);
   void              PrintShort(const bool created=true);
   
//--- Конструкторы
                     CTimeSeries(void){;}
                     CTimeSeries(const string symbol);
  };
//+------------------------------------------------------------------+

Из конструктора класса удалим цикл создания списков таймсерий:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CTimeSeries::CTimeSeries(const string symbol) : m_symbol(symbol)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort();
   for(int i=0;i<21;i++)
     {
      ENUM_TIMEFRAMES timeframe=this.TimeframeByIndex((uchar)i);
      CSeries *series_obj=new CSeries(this.m_symbol,timeframe);
      this.m_list_series.Add(series_obj);
     }
   this.SetTerminalServerDate();
   this.m_new_tick.SetSymbol(this.m_symbol);
   this.m_new_tick.Refresh();
  }
//+------------------------------------------------------------------+

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

В методах установки глубины истории всех используемых таймсерий SetRequiredAllUsedData() и возврата флага синхронизации всех используемых таймсерий SyncAllData() цикл по полному количеству всех возможных таймфреймов

//+------------------------------------------------------------------+
//| Устанавливает глубину истории всех используемых таймсерий символа|
//+------------------------------------------------------------------+
bool CTimeSeries::SetRequiredAllUsedData(const uint required=0,const int rates_total=0)
  {
   if(this.m_symbol==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL));
      return false;
     }
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL)
         continue;
      res &=series_obj.SetRequiredUsedData(required,rates_total);
     }
   return res;
  }
//+------------------------------------------------------------------+

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

   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)

Это понятно — у нас теперь список состоит только из реально созданных объектов-таймсерий, и циклы проводим по реальному их количеству.

Реализация метода добавления в список указанного объекта-таймсерии:

//+------------------------------------------------------------------+
//| Добавляет в список указанную список-таймсерию                    |
//+------------------------------------------------------------------+
bool CTimeSeries::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeries *series=new CSeries(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;
  }
//+------------------------------------------------------------------+

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

Создаём объект-таймсерию с таймфреймом, значение которого передано в метод.
Устанавливаем списку таймсерий флаг сортированного списка и ищем в списке объект-таймсерию, равный только что созданному.
Если такого объекта в списке ещё нет (поиск вернул -1), то добавляем в список созданный объект-таймсерию.
Иначе
— удаляем созданный объект — такой объект-таймсерия уже есть в списке.
Раз таймсерию создаём, значит она нужна — выставляем флаг её использования в программе и
возвращаем результат добавления таймсерии в список
.
При успешном добавлении будет возвращено значение true, при неудачном — false.

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

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

В рассматриваемом классе объекта всех таймсерий одного символа CTimeSeries, самое место для определения событий всех его списков-таймсерий — это метод обновления указанной таймсерии Refresh(), и метод обновления всех таймсерий символа RefreshAll().

Рассмотрим реализацию методов обновления списков-таймсерий:

//+------------------------------------------------------------------+
//| Обновляет указанный список-таймсерию                             |
//+------------------------------------------------------------------+
void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Сбрасываем флаг события таймсерии и очищаем список всех событий таймсерии
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Получаем из списка таймсерию по её таймфрейму
   CSeries *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);
//--- Если у объекта-таймсерии есть событие "Новый бар"
   if(series_obj.IsNewBar(data_calculate.rates.time))
     {
      //--- отправляем событие "Новый бар" на график управляющей программы
      series_obj.SendEvent();
      //--- устанавливаем значения первой даты в истории на сервере и в терминале
      this.SetTerminalServerDate();
      //--- добавляем в список событий таймсерий новое событие "Новый бар"
      //--- при успешном добавлении - устанавливаем флаг события у таймсерии
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Обновляет все списки-таймсерии                                   |
//+------------------------------------------------------------------+
void CTimeSeries::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++)
     {
      //--- получаем очередной объект-таймсерию по индексу цикла
      CSeries *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);
      //--- Если у объекта-таймсерии есть событие "Новый бар"
      if(series_obj.IsNewBar(data_calculate.rates.time))
        {
         //--- отправляем событие "Новый бар" на график управляющей программы,
         series_obj.SendEvent();
         //--- устанавливаем флаг необходимости установки первой даты в истории на сервере и в терминале
         upd=true;
         //--- добавляем в список событий таймсерий новое событие "Новый бар"
         //--- при успешном добавлении - устанавливаем флаг события у таймсерии
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- Если установлен флаг необходимости установки первой даты в истории на сервере и в терминале -
//--- устанавливаем значения первой даты в истории на сервере и в терминале
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

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

С классом объекта всех таймсерий одного символа CTimeSeries завершили.

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

Откроем файл \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh и унаследуем его от расширенного базового класса всех объектов библиотеки:

//+------------------------------------------------------------------+
//| Коллекция таймсерий символов                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CBaseObjExt
  {

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

public:
//--- Возвращает (1) себя, (2) список таймсерий
   CTimeSeriesCollection  *GetObject(void)            { return &this;         }
   CArrayObj              *GetList(void)              { return &this.m_list;  }
//--- Возвращает (1) объект таймсерий указанного символа, (2) объект-таймсерию указанного символа/периода
   CTimeSeries            *GetTimeseries(const string symbol);
   CSeries                *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

За пределами тела класса сразу напишем их реализацию.
Метод, возвращающий объект таймсерий указанного символа:

//+------------------------------------------------------------------+
//| Возвращает объект таймсерий указанного символа                   |
//+------------------------------------------------------------------+
CTimeSeries *CTimeSeriesCollection::GetTimeseries(const string symbol)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return NULL;
   CTimeSeries *timeseries=this.m_list.At(index);
   return timeseries;
  }
//+------------------------------------------------------------------+

Здесь: получаем индекс объекта таймсерий по наименованию символа методом IndexTimeSeries(), рассмотренный нами в части 37 описания создания библиотеки. По полученному индексу получаем объект таймсерий из списка. При неудачном получении индекса или объекта из списка будет возвращено значение NULL, иначе — указатель на запрашиваемый объект в списке.

Метод, возвращающий объект-таймсерию указанного символа/периода:

//+------------------------------------------------------------------+
//| Возвращает объект-таймсерию указанного символа/периода           |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return NULL;
   CSeries *series=timeseries.GetSeries(timeframe);
   return series;
  }
//+-----------------------------------------------------------------------+

Здесь: получаем объект таймсерий при помощи вышерассмотренного метода GetTimeseries() по переданному в метод символу.
Из полученного объекта таймсерий получаем список-таймсерию по указанному таймфрейму и возвращаем указатель на полученный объект-таймсерию.

Метод GetSeries() объекта таймсерий использует для возврата требуемой таймсерии метод IndexTimeframe(), рассмотренный нами выше, а метод GetSeries() объекта таймсерий CTimeSeries выглядит так:

CSeries *GetSeries(const ENUM_TIMEFRAMES timeframe) { return this.m_list_series.At(this.IndexTimeframe(timeframe)); }

Из публичной секции класса удалим три метода для создания таймсерий, оставив только один — для создания указанной таймсерии указанного символа:

//--- Создаёт (1) указанную таймсерию указанного символа, (2) указанную таймсерии всех символов,
//--- (3) все таймсерии указанного символа, (4) все таймсерии всех символов
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const string symbol,const uint required=0);
   bool                    CreateSeries(const uint required=0);
//--- Обновляет (1) указанную таймсерию указанного символа, (2) указанную таймсерии всех символов,
//--- (3) все таймсерии указанного символа, (4) все таймсерии всех символов, (5) все таймсерии кроме текущего символа

Три удалённые метода пока кажутся тут лишними, и вместо них объявим три новых метода — для пересоздания указанной таймсерии, для возврата пустой таймсерии и для возврата не полностью заполненной таймсерии:

//--- (1) Создаёт, (2) пересоздаёт указанную таймсерию указанного символа
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
   bool                    ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
//--- Возвращает (1) пустую, (2) не полностью заполненную данными таймсерию
   CSeries                *GetSeriesEmpty(void);
   CSeries                *GetSeriesIncompleted(void);

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

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

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

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

//--- Выводит в журнал (1) полное, (2) краткое описание коллекции
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
//--- Конструктор
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//|Возвращает пустую (созданную, но не заполненную данными) таймсерию|
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesEmpty(void)
  {
//--- В цикле по списку объектов таймсерий
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- получаем очередной объект всех таймсерий символа по индексу цикла
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- получаем список объектов-таймсерий из объекта всех таймсерий символа
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- в цикле по списку таймсерий символа
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- получаем очередную таймсерию
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- если в таймсерии нет ни одного объекта-бара -

         //--- возвращаем указатель на эту таймсерию
         if(series.DataTotal()==0)
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+
//| Возвращает не полностью заполненную данными таймсерию            |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesIncompleted(void)
  {
//--- В цикле по списку объектов таймсерий
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- получаем очередной объект всех таймсерий символа по индексу цикла
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- получаем список объектов-таймсерий из объекта всех таймсерий символа
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- в цикле по списку таймсерий символа
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- получаем очередную таймсерию
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- если в таймсерии есть объекты-бары,
         //--- но их количество не равно запрошенному и доступному для символа -
         //--- возвращаем указатель на эту таймсерию
         if(series.DataTotal()>0 && series.AvailableUsedData()!=series.DataTotal())
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

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

Методы возвращают первую встречную таймсерию, удовлетворяющую условия поиска. И так сделано специально — чтобы на каждом очередном тике (входе в OnCalculate) получать последовательно все возможные пустые или не полностью заполненные таймсерии, что соответствует рекомендациям MetaQuotes по правильной обработке нехватки данных в индикаторах — выходить из обработчика и проверять наличие данных на следующем тике.

Реализация метода создания указанной таймсерии указанного символа:

//+------------------------------------------------------------------+
//| Создаёт указанную таймсерию указанного символа                   |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.AddSeries(timeframe,required))
      return false;
   if(!timeseries.SyncData(timeframe,required,rates_total))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

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

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

Реализация метода пересоздания указанной таймсерии указанного символа:

//+------------------------------------------------------------------+
//| Пересоздаёт указанную таймсерию указанного символа               |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.SyncData(timeframe,rates_total,required))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Получает события из объекта таймсерий и добавляет их в список    |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::SetEvents(CTimeSeries *timeseries)
  {
//--- Устанавливаем флаг успешности добавления события в список и
//--- получаем список событий объекта таймсерий символа
   bool res=true;
   CArrayObj *list=timeseries.GetListEvents();
   if(list==NULL)
      return false;
//--- В цикле по полученному списку событий
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередное событие по индексу цикла и
      CEventBaseObj *event=timeseries.GetEvent(i);
      if(event==NULL)
         continue;
      //--- добавляем к значению флага результат добавления полученного события
      //--- из списка таймсерий символа в список коллекции таймсерий
      res &=this.EventAdd(event.ID(),event.LParam(),event.DParam(),event.SParam());
     }
//--- Возвращаем результат добавления событий в список
   return res;
  }
//+------------------------------------------------------------------+

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

Реализация метода обновления указанной таймсерии указанного символа и добавления его событий к списку событий коллекции таймсерий:

//+------------------------------------------------------------------+
//| Обновляет указанную таймсерию указанного символа                 |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Сбрасываем флаг события в коллекции таймсерий и очищаем список событий
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Получаем объект всех таймсерий символа по наименованию символа
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- Если нет нового тика на символе объекта таймсерий - уходим
   if(!timeseries.IsNewTick())
      return;
//--- Обновляем требуемую таймсерию объекта всех таймсерий символа
   timeseries.Refresh(timeframe,data_calculate);
//--- Если у таймсерии поднят флаг события -
//--- получаем события от таймсерий символа, записываем их в список событий коллекции
//--- и устанавливаем флаг события в коллекции
   if(timeseries.IsEvent())
      this.m_is_event=this.SetEvents(timeseries);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обновляет все таймсерии всех символов                            |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(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++)
     {
      //--- получаем очередной объект таймсерий символа
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- если нет нового тика на символе таймсерии - идём к следующему объекту в списке
      if(!timeseries.IsNewTick())
         continue;
      //--- Обновляем все таймсерии символа
      timeseries.RefreshAll(data_calculate);
      //--- Если у объекта таймсерий символа поднят флаг события -
      //--- получаем события от таймсерий символа, записываем их в список событий коллекции
      //--- и устанавливаем флаг события в коллекции
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }
//+------------------------------------------------------------------+

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

Доработка всех классов таймсерий на данном этапе завершена.

Теперь доработаем основной объект библиотеки CEngine (\MQL5\Include\DoEasy\Engine.mqh) для работы с коллекцией таймсерий из программ.

В приватной секции класса объявим объект-паузу:

class CEngine
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CMarketCollection    m_market;                        // Коллекция рыночных ордеров и сделок
   CEventsCollection    m_events;                        // Коллекция событий
   CAccountsCollection  m_accounts;                      // Коллекция аккаунтов
   CSymbolsCollection   m_symbols;                       // Коллекция символов
   CTimeSeriesCollection m_time_series;                  // Коллекция таймсерий
   CResourceCollection  m_resource;                      // Список ресурсов
   CTradingControl      m_trading;                       // Объект управления торговлей
   CPause               m_pause;                         // Объект "Пауза"

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

//--- Возвращает флаг (1) счёта-хедж, (2) работы в тестере, (3) события аккаунта, (4) события символа, (5) торгового события
   bool                 IsHedge(void)                             const { return this.m_is_hedge;                             }
   bool                 IsTester(void)                            const { return this.m_is_tester;                            }
   bool                 IsAccountsEvent(void)                     const { return this.m_accounts.IsEvent();                   }
   bool                 IsSymbolsEvent(void)                      const { return this.m_symbols.IsEvent();                    }
   bool                 IsTradeEvent(void)                        const { return this.m_events.IsEvent();                     }
   bool                 IsSeriesEvent(void)                       const { return this.m_time_series.IsEvent();                }

Метод возвращает результат работы метода IsEvent() объекта-коллекции таймсерий.

Так как теперь в методы обновления таймсерий нам необходимо отправлять данные массивов из обработчика OnCalculate() индикатора для обработки данных текущей таймсерии, то добавим в методы обработки событий Timer и Tick передачу структуры данных массивов OnCalculate(), а заодно и объявим метод обработки события Calculate:

//--- (1) Таймер, обработчик события (2) NewTick, (3) Calculate
   void                 OnTimer(SDataCalculate &data_calculate);
   void                 OnTick(SDataCalculate &data_calculate,const uint required=0);
   int                  OnCalculate(SDataCalculate &data_calculate,const uint required=0);

Там же, в публичной секции класса впишем метод, возвращающий список событий таймсерий:

//--- Возвращает (1) коллекцию таймсерий, (2) список таймсерий из коллекции таймсерий, (3) список событий таймсерий
   CTimeSeriesCollection *GetTimeSeriesCollection(void)                       { return &this.m_time_series;                                     }
   CArrayObj           *GetListTimeSeries(void)                               { return this.m_time_series.GetList();                            }
   CArrayObj           *GetListSeriesEvents(void)                             { return this.m_time_series.GetListEvents();                      }

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

В публичной секции класса у нас есть четыре метода для создания различных таймсерий. Удалим три пока не нужных метода:

//--- Создаёт (1) указанную таймсерию указанного символа, (2) указанную таймсерии всех символов,
//--- (3) все таймсерии указанного символа, (4) все таймсерии всех символов
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,timeframe,required);          }
   bool                 SeriesCreate(const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(timeframe,required);                 }
   bool                 SeriesCreate(const string symbol,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,required);                    }
   bool                 SeriesCreate(const uint required=0)
                          { return this.m_series.CreateSeries(required);                           }

и вместо них впишем объявление метода для создания всех таймсерий всех используемых символов коллекции, напишем метод для пересоздания указанной таймсерии и объявим метод для запроса синхронизации таймсерий с сервером:

//--- Создаёт (1) указанную таймсерию указанного символа, (2) все используемые таймсерии всех используемых символов
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.CreateSeries(symbol,timeframe,rates_total,required);        }
   bool                 SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0);
//--- Пересоздаёт указанную таймсерию указанного символа
   bool                 SeriesReCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.ReCreateSeries(symbol,timeframe,rates_total,required);      }
//--- Синхронизирует данные таймсерий с сервером
   void                 SeriesSync(SDataCalculate &data_calculate,const uint required=0);

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

//--- Обновляет (1) указанную таймсерию указанного символа, (2) все таймсерии всех символов
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }

Теперь в методы вместо значений массивов OnCalculate() передаётся структура с данными переменных и массивов OnCalculate().

И добавим ещё четыре метода — для возврата указателя на объект таймсерий указанного символа, на указанный объект-таймсерию, а так же методы, возвращающие указатели на пустую и не полностью заполненную таймсерии:

//--- Возвращает (1) объект таймсерий указанного символа, (2) объект-таймсерию указанного символа/периода
   CTimeSeries         *SeriesGetTimeseries(const string symbol)
                          { return this.m_time_series.GetTimeseries(symbol);                                      }
   CSeries             *SeriesGetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
                          { return this.m_time_series.GetSeries(symbol,timeframe);                                }
//--- Возвращает (1) пустую, (2) не полностью заполненную данными таймсерию
   CSeries             *SeriesGetSeriesEmpty(void)       { return this.m_time_series.GetSeriesEmpty();            }
   CSeries             *SeriesGetSeriesIncompleted(void) { return this.m_time_series.GetSeriesIncompleted();      }

Методы возвращают результат возврата одноимённых методов коллекции таймсерий, рассмотренные нами выше.

Метод TradingOnInit(), передающий в торговый класс указатели на все необходимые коллекции был переименован в CollectionOnInit() так как такое название ему больше подходит — в нём будем производить необходимые инициализации всех классов-коллекций.

В конце кода тела класса впишем блок с методами для работы с объектом-паузой:

//--- Устанавливает новое (1) время начала отсчёта паузы, (2) паузу в милисекундах
   void                 PauseSetTimeBegin(const ulong time)             { this.m_pause.SetTimeBegin(time);                    }
   void                 PauseSetWaitingMSC(const ulong pause)           { this.m_pause.SetWaitingMSC(pause);                  }
//--- Возвращает (1) прошедшее время от начала отсчёта паузы в милисекундах, (2) флаг завершения ожидания
//--- (3) время начала отсчёта паузы, (4) установленный размер паузы в милисекундах
   ulong                PausePassed(void)                         const { return this.m_pause.Passed();                       }
   bool                 PauseIsCompleted(void)                    const { return this.m_pause.IsCompleted();                  }
   ulong                PauseTimeBegin(void)                      const { return this.m_pause.TimeBegin();                    }
   ulong                PauseTimeWait(void)                       const { return this.m_pause.TimeWait();                     }
//--- Возвращает описание (1) прошедшего времени от начала отсчёта в милисекундах,
//--- (2) времени начала отсчёта паузы, (3) установленного размера паузы в милисекундах
   string               PausePassedDescription(void)              const { return this.m_pause.PassedDescription();            }
   string               PauseTimeBeginDescription(void)           const { return this.m_pause.TimeBeginDescription();         }
   string               PauseWaitingMSCDescription(void)          const { return this.m_pause.WaitingMSCDescription();        }
   string               PauseWaitingSECDescription(void)          const { return this.m_pause.WaitingSECDescription();        }
//--- Запускает новый отсчёт паузы
   void                 Pause(const ulong pause_msc,const datetime time_start=0)
                          {
                           this.PauseSetWaitingMSC(pause_msc);
                           this.PauseSetTimeBegin(time_start*1000);
                           while(!this.PauseIsCompleted() && !IsStopped()){}
                          }

//--- Конструктор/Деструктор
                        CEngine();
                       ~CEngine();

Класс "Пауза" был нами рассмотрен в статье 30, и предназначен для организации пауз вместо функции Sleep(), которая в индикаторах не работает.

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

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

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

//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//--- Если это не тестер - работаем с событиями коллекций по таймеру
   if(!this.IsTester())
     {
   //--- Таймер коллекций исторических ордеров и сделок и рыночных ордеров и позиций
      int index=this.CounterIndex(COLLECTION_ORD_COUNTER_ID);
      CTimerCounter* cnt1=this.m_list_counters.At(index);
      if(cnt1!=NULL)
        {
         //--- Если пауза завершилась - работаем с событиями коллекций ордеров, сделок и позиций
         if(cnt1.IsTimeDone())
            this.TradeEventsControl();
        }
   //--- Таймер коллекции аккаунтов
      index=this.CounterIndex(COLLECTION_ACC_COUNTER_ID);
      CTimerCounter* cnt2=this.m_list_counters.At(index);
      if(cnt2!=NULL)
        {
         //--- Если пауза завершилась - работаем с событиями коллекции аккаунтов
         if(cnt2.IsTimeDone())
            this.AccountEventsControl();
        }
   //--- Таймер1 коллекции символов (обновление котировочных данных символов в коллекции)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID1);
      CTimerCounter* cnt3=this.m_list_counters.At(index);
      if(cnt3!=NULL)
        {
         //--- Если пауза завершилась - обновляем котировочные данные всех символов в коллекции
         if(cnt3.IsTimeDone())
            this.m_symbols.RefreshRates();
        }
   //--- Таймер2 коллекции символов (обновление всех данных всех символов в коллекции и отслеживание событий символов и списка символов в окне обзора рынка)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID2);
      CTimerCounter* cnt4=this.m_list_counters.At(index);
      if(cnt4!=NULL)
        {
         //--- Если пауза завершилась
         if(cnt4.IsTimeDone())
           {
            //--- обновляем данные и работаем с событиями всех символов в коллекции
            this.SymbolEventsControl();
            //--- Если работаем со списком из обзора рынка - проверяем события окна обзора рынка
            if(this.m_symbols.ModeSymbolsList()==SYMBOLS_MODE_MARKET_WATCH)
               this.MarketWatchEventsControl();
           }
        }
   //--- Таймер торгового класса
      index=this.CounterIndex(COLLECTION_REQ_COUNTER_ID);
      CTimerCounter* cnt5=this.m_list_counters.At(index);
      if(cnt5!=NULL)
        {
         //--- Если пауза завершилась - работаем со списком отложенных запросов
         if(cnt5.IsTimeDone())
            this.m_trading.OnTimer();
        }
   //--- Таймер коллекции таймсерий
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- Если пауза завершилась - работаем со списком таймсерий
         if(cnt6.IsTimeDone())
            this.SeriesRefresh(data_calculate);
        }
     }
//--- Если тестер - работаем с событиями коллекций по тику
   else
     {
      //--- работаем с событиями коллекций ордеров, сделок и позиций по тику
      this.TradeEventsControl();
      //--- работаем с событиями коллекции аккаунтов по тику
      this.AccountEventsControl();
      //--- обновляем котировочные данные всех символов в коллекции по тику
      this.m_symbols.RefreshRates();
      //--- работаем с событиями всех символов в коллекции по тику
      this.SymbolEventsControl();
      //--- работаем со списком отложенных запросов по тику
      this.m_trading.OnTimer();
      //--- работаем со списком таймсерий по тику
      this.SeriesRefresh(data_calculate);
     }
  }
//+------------------------------------------------------------------+

Обработчик теперь стал компактнее и с более понятной логикой, и избавлен от лишних повторяющихся проверок.

Метод, синхронизирующий данные пустой таймсерии с сервером, и пересоздающий пустую таймсерию:

//+------------------------------------------------------------------+
//| Синхронизирует данные таймсерий с сервером                       |
//+------------------------------------------------------------------+
void CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0)
  {
//--- Если данные таймсерий не посчитаны - пробуем пересоздать таймсерии
//--- Получаем указатель на пустую таймсерию
   CSeries *series=this.SeriesGetSeriesEmpty();
   if(series!=NULL)
     {
      //--- Выводим комментарий на график с данными пустой таймсерии и пробуем синхронизировать таймсерию с данными на сервере
      ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC));
      ::ChartRedraw(::ChartID());
      //--- если данные синхронизированы
      if(series.SyncData(0,data_calculate.rates_total))
        {
         //--- если таймсерию удалось пересоздать
         if(this.m_time_series.ReCreateSeries(series.Symbol(),series.Timeframe(),data_calculate.rates_total))
           {
            //--- выводим комментарий на график и запись в журнал с данными пересозданной таймсерии
            ::Comment(series.Header(),": OK");
            ::ChartRedraw(::ChartID());
            Print(series.Header()," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK),":");
            series.PrintShort();
           }
        }
     }
//--- Стираем все комментарии
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
     }
  }
//+------------------------------------------------------------------+

Итак. Данный метод является краеугольным камнем для правильной подгрузки исторических данных любых используемых таймсерий — любых символов и любых периодов графиков.

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

И так на каждом тике — получаем очередную пустую таймсерию, синхронизируем и пересоздаём — до тех пор пока не останется незаполненных таймсерий.

Реализация обработчиков событий NewTick и Calculate:

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

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

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

//+------------------------------------------------------------------+
//| Создаёт все используемые таймсерии всех используемых символов    |
//+------------------------------------------------------------------+
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);
        }
     }
//--- Возвращаем результат создания всех таймсерий для всех символов
   return res;
  }
//+------------------------------------------------------------------+

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

Это все необходимые на сегодня доработки для работы с таймсериями.


Тестирование работы таймсерий и их событий в индикаторах

Для тестирования работы класса-коллекции таймсерий в индикаторах создадим в каталоге индикаторов терминала новую папку
\MQL5\Indicators\TestDoEasy\, а в ней новую подпапку Part39\, в которой создадим новый индикатор с именем TestDoEasyPart39.mq5.

Количество и тип рисуемых буферов индикатора пока для нас не имеет значения — мы в нём рисовать ничего не собираемся. Но на будущее я задал два рисуемых буфера с типом отрисовки DRAW_LINE.

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

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart39.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>
//--- enums
//--- defines
//--- structures
//--- properties
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Label2
#property indicator_label2  "Label2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGreen
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- indicator buffers
double         Buffer1[];
double         Buffer2[];
//--- input variables
sinput   ENUM_SYMBOLS_MODE InpModeUsedSymbols   =  SYMBOLS_MODE_CURRENT;            // Mode of used symbols list
sinput   string            InpUsedSymbols       =  "EURUSD,AUDUSD,EURAUD,EURCAD,EURGBP,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   bool              InpUseSounds         =  true; // Use sounds
//--- global variables
CEngine        engine;                          // Главный объект библиотеки CEngine
string         prefix;                          // Префикс имён графических объектов
bool           testing;                         // Флаг работы в тестере
int            used_symbols_mode;               // Режим работы с символами
string         array_used_symbols[];            // Массив используемых символов
string         array_used_periods[];            // Массив используемых таймфреймов
//+------------------------------------------------------------------+

В обработчике OnInit() индикатора пропишем установку глобальных переменных индикатора и вызов функции инициализации библиотеки:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,Buffer1,INDICATOR_DATA);
   SetIndexBuffer(1,Buffer2,INDICATOR_DATA);

//--- Установка глобальных переменных индикатора
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   testing=engine.IsTester();
   ZeroMemory(rates_data);
   
//--- Инициализация библиотеки DoEasy
   OnInitDoEasy();

//--- Проверка и удаление неудалённых графических объектов индикатора
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

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

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Обработчик OnDeinit() индикатора возьмём из тестового советника из прошлой статьи:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Удаление графических объектов индикатора по префиксу имени объектов
   ObjectsDeleteAll(0,prefix);
   Comment("");
  }
//+------------------------------------------------------------------+

Обработчики OnTimer() и OnChartEvent() также возьмём из советника:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Запускаем таймер библиотеки только не в тестере
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer(rates_data);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если работа в тестере - выход
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Обработка событий мыши
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Обработка нажатий кнопок в панели
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Обработка событий библиотеки DoEasy
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     } 
  }
//+------------------------------------------------------------------+

Для заполнения стуктуры данных массивов и переменных из первой и второй формы OnCalculate() индикатора создадим две функции:

//+------------------------------------------------------------------+
//| Копирование данных из OnCalculate() первой формы в структуру     |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              const int rates_total,
              const int prev_calculated,
              const int begin,
              const double &price[])
  {
//--- Получим флаг индексации массива как в таймсерии, и если нет, то
//--- установим направление индексации массиву как в таймсерии
   bool as_series_price=ArrayGetAsSeries(price);
   if(!as_series_price)
      ArraySetAsSeries(price,true);
//--- Скопируем нулевой бар массива в структуру данных OnCalculate() SDataCalculate
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.begin=begin;
   data_calculate.price=price[0];
//--- Вернём массивум его изначальное направление индексации
   if(!as_series_price)
      ArraySetAsSeries(price,false);
  }
//+------------------------------------------------------------------+
//| Копирование данных из OnCalculate() второй формы в структуру     |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              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[])
  {
//--- Получим флаги индексации массивов как в таймсерии, и если нет, то
//--- установим направление индексации массивам как в таймсерии
   bool as_series_time=ArrayGetAsSeries(time);
   if(!as_series_time)
      ArraySetAsSeries(time,true);
   bool as_series_open=ArrayGetAsSeries(open);
   if(!as_series_open)
      ArraySetAsSeries(open,true);
   bool as_series_high=ArrayGetAsSeries(high);
   if(!as_series_high)
      ArraySetAsSeries(high,true);
   bool as_series_low=ArrayGetAsSeries(low);
   if(!as_series_low)
      ArraySetAsSeries(low,true);
   bool as_series_close=ArrayGetAsSeries(close);
   if(!as_series_close)
      ArraySetAsSeries(close,true);
   bool as_series_tick_volume=ArrayGetAsSeries(tick_volume);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,true);
   bool as_series_volume=ArrayGetAsSeries(volume);
   if(!as_series_volume)
      ArraySetAsSeries(volume,true);
   bool as_series_spread=ArrayGetAsSeries(spread);
   if(!as_series_spread)
      ArraySetAsSeries(spread,true);
//--- Скопируем нулевой бар массивов в структуру данных OnCalculate() SDataCalculate
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.rates.time=time[0];
   data_calculate.rates.open=open[0];
   data_calculate.rates.high=high[0];
   data_calculate.rates.low=low[0];
   data_calculate.rates.close=close[0];
   data_calculate.rates.tick_volume=tick_volume[0];
   data_calculate.rates.real_volume=(#ifdef __MQL5__ volume[0] #else 0 #endif);
   data_calculate.rates.spread=(#ifdef __MQL5__ spread[0] #else 0 #endif);
//--- Вернём массивам их изначальное направление индексации
   if(!as_series_time)
      ArraySetAsSeries(time,false);
   if(!as_series_open)
      ArraySetAsSeries(open,false);
   if(!as_series_high)
      ArraySetAsSeries(high,false);
   if(!as_series_low)
      ArraySetAsSeries(low,false);
   if(!as_series_close)
      ArraySetAsSeries(close,false);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,false);
   if(!as_series_volume)
      ArraySetAsSeries(volume,false);
   if(!as_series_spread)
      ArraySetAsSeries(spread,false);
  }
//+------------------------------------------------------------------+

Функцию обработки событий библиотеки DoEasy тоже перенесём из тестового советника:

//+------------------------------------------------------------------+
//| Обработка событий библиотеки DoEasy                              |
//+------------------------------------------------------------------+
void OnDoEasyEvent(const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
  {
   int idx=id-CHARTEVENT_CUSTOM;
//--- Извлекаем из lparam (1) милисекунды времени события, (2) причину, (3) источник события и (4) устанавливаем точное время события
   ushort msc=engine.EventMSC(lparam);
   ushort reason=engine.EventReason(lparam);
   ushort source=engine.EventSource(lparam);
   long time=TimeCurrent()*1000+msc;
   
//--- Обработка событий символов
   if(source==COLLECTION_SYMBOLS_ID)
     {
      CSymbol *symbol=engine.GetSymbolObjByName(sparam);
      if(symbol==NULL)
         return;
      //--- Количество знаков после запятой в значении события - если long-событие, то 0, иначе - Digits() символа
      int digits=(idx<SYMBOL_PROP_INTEGER_TOTAL ? 0 : symbol.Digits());
      //--- Текстовое описание события
      string id_descr=(idx<SYMBOL_PROP_INTEGER_TOTAL ? symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_INTEGER)idx) : symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_DOUBLE)idx));
      //--- Текстовое значение величины изменения свойства
      string value=DoubleToString(dparam,digits);
      
      //--- Проверка причин события и просто вывод в журнал его описания
      if(reason==BASE_EVENT_REASON_INC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     }   
     
//--- Обработка событий аккаунта
   else if(source==COLLECTION_ACCOUNT_ID)
     {
      CAccount *account=engine.GetAccountCurrent();
      if(account==NULL)
         return;
      //--- Количество знаков после запятой в значении события - если long-событие, то 0, иначе - Digits() символа
      int digits=int(idx<ACCOUNT_PROP_INTEGER_TOTAL ? 0 : account.CurrencyDigits());
      //--- Текстовое описание события
      string id_descr=(idx<ACCOUNT_PROP_INTEGER_TOTAL ? account.GetPropertyDescription((ENUM_ACCOUNT_PROP_INTEGER)idx) : account.GetPropertyDescription((ENUM_ACCOUNT_PROP_DOUBLE)idx));
      //--- Текстовое значение величины изменения свойства
      string value=DoubleToString(dparam,digits);
      
      //--- Проверка причин события и обработка увеличения средств на заданную величину,
      
      //--- Если это увеличение значения свойства
      if(reason==BASE_EVENT_REASON_INC)
        {
         //--- Распечатаем событие в журнал
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
         //--- если это увеличение средств
         if(idx==ACCOUNT_PROP_EQUITY)
           {
            //--- Получаем список всех открытых позиций по текущему символу
            CArrayObj* list_positions=engine.GetListMarketPosition();
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_SYMBOL,Symbol(),EQUAL);
            //--- Выбираем позиции с прибылью болше нуля
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_PROFIT_FULL,0,MORE);
            if(list_positions!=NULL)
              {
               //--- Сортируем список по прибыли с учётом комиссии и свопа
               list_positions.Sort(SORT_BY_ORDER_PROFIT_FULL);
               //--- Получаем индекс позиции с наибольшей прибылью
               int index=CSelect::FindOrderMax(list_positions,ORDER_PROP_PROFIT_FULL);
               if(index>WRONG_VALUE)
                 {
                  COrder* position=list_positions.At(index);
                  if(position!=NULL)
                    {
                     //--- Получаем тикет позиции с наибольшей прибылью и закрываем позицию по тикету
                     engine.ClosePosition(position.Ticket());
                    }
                 }
              }
           }
        }
      //--- Остальные события просто выводим в журнал
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     } 
     
//--- Обработка событий окна обзор рынка
   else if(idx>MARKET_WATCH_EVENT_NO_EVENT && idx<SYMBOL_EVENTS_NEXT_CODE)
     {
      //--- Событие окна "Обзор рынка"
      string descr=engine.GetMWEventDescription((ENUM_MW_EVENT)idx);
      string name=(idx==MARKET_WATCH_EVENT_SYMBOL_SORT ? "" : ": "+sparam);
      Print(TimeMSCtoString(lparam)," ",descr,name);
     }
     
//--- Обработка событий таймсерий
   else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE)
     {
      //--- Событие "Новый бар"
      if(idx==SERIES_EVENTS_NEW_BAR)
        {
         Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam));
        }
     }
     
//--- Обработка торговых событий
   else if(idx>TRADE_EVENT_NO_EVENT && idx<TRADE_EVENTS_NEXT_CODE)
     {
      //--- Получим список всех торговых событий
      CArrayObj *list=engine.GetListAllOrdersEvents();
      if(list==NULL)
         return;
      //--- получим смещение индекса события относительно конца списка
      //--- в тестере смещение передаётся параметром lparam в обработчик событий
      //--- не в тестере - каждое событие отправляется по одному и обрабатывается в OnChartEvent()
      int shift=(testing ? (int)lparam : 0);
      CEvent *event=list.At(list.Total()-1-shift);
      if(event==NULL)
      return;
      //--- Начисление кредита
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CREDIT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Дополнительные сборы
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CHARGE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Корректирующая запись
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CORRECTION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Перечисление бонусов
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BONUS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Дополнительные комиссии
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Комиссия, начисляемая в конце торгового дня
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Комиссия, начисляемая в конце месяца
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Агентская комиссия, начисляемая в конце торгового дня
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Агентская комиссия, начисляемая в конце месяца
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Начисления процентов на свободные средства
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_INTEREST)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Отмененная сделка покупки
      if(event.TypeEvent()==TRADE_EVENT_BUY_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Отмененная сделка продажи
      if(event.TypeEvent()==TRADE_EVENT_SELL_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Начисление дивиденда
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Начисление франкированного дивиденда
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT_FRANKED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Начисление налога
      if(event.TypeEvent()==TRADE_EVENT_TAX)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Пополнение средств на балансе
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_REFILL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Снятие средств с баланса
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      
      //--- Отложенный ордер установлен
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_PLASED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Отложенный ордер удалён
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_REMOVED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Отложенный ордер активирован ценой
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Отложенный ордер активирован ценой частично
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция открыта
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция открыта частично
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта встречной
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта по StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта по TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Разворот позиции новой сделкой (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Разворот позиции активацией отложенного ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Разворот позиции частичным исполнением маркет-ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Разворот позиции частичной активацией отложенного ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Добавлен объём к позиции новой сделкой (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Добавлен объём к позиции частичным исполнением маркет-ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Добавлен объём к позиции активацией отложенного ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Добавлен объём к позиции частичной активацией отложенного ордера (неттинг)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта частично
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта частично встречной
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта частично по StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Позиция закрыта частично по TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Срабатывание StopLimit ордера
      if(event.TypeEvent()==TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение цены установки ордера
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение цены установки ордера и StopLoss
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение цены установки ордера и TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение цены установки ордера, StopLoss и TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение StopLoss и TakeProfit ордера
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение StopLoss ордера
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение TakeProfit ордера
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение StopLoss и TakeProfit позиции
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение StopLoss позиции
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Изменение TakeProfit позиции
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Работа с событиями в тестере                                     |
//+------------------------------------------------------------------+
void EventsHandling(void)
  {
//--- Если есть торговое событие
   if(engine.IsTradeEvent())
     {
      //--- Количество торговых событий, произошедших одновременно
      int total=engine.GetTradeEventsTotal();
      for(int i=0;i<total;i++)
        {
         //--- Получим по индексу очередное событие из списка событий, произошедших одновременно
         CEventBaseObj *event=engine.GetTradeEventByIndex(i);
         if(event==NULL)
            continue;
         long   lparam=i;
         double dparam=event.DParam();
         string sparam=event.SParam();
         OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
        }
     }
//--- Если есть событие аккаунта
   if(engine.IsAccountsEvent())
     {
      //--- Получим список всех событий аккаунта, произошедших одновременно
      CArrayObj* list=engine.GetListAccountEvents();
      if(list!=NULL)
        {
         //--- В цикле получаем очередное событие
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- берём событие из списка
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Отправляем событие в обработчик событий
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- Если есть событие коллекции символов
   if(engine.IsSymbolsEvent())
     {
      //--- Получим список всех событий символов, произошедших одновременно
      CArrayObj* list=engine.GetListSymbolsEvents();
      if(list!=NULL)
        {
         //--- В цикле получаем очередное событие
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- берём событие из списка
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Отправляем событие в обработчик событий
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- Если есть событие коллекции таймсерий
   if(engine.IsSeriesEvent())
     {
      //--- Получим список всех событий таймсерий, произошедших одновременно
      CArrayObj* list=engine.GetListSeriesEvents();
      if(list!=NULL)
        {
         //--- В цикле получаем очередное событие
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- берём событие из списка
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Отправляем событие в обработчик событий
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает состояние кнопки                                      |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки                                   |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
//--- Кнопка 1
   if(name=="BUTT_1")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
//--- Кнопка 2
   if(name=="BUTT_2")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'255,220,90');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
  }
//+------------------------------------------------------------------+
//| Контроль состояния кнопок                                        |
//+------------------------------------------------------------------+
void PressButtonsControl(void)
  {
   int total=ObjectsTotal(0,0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Если кнопка в нажатом состоянии
   if(ButtonState(button_name))
     {
      //--- Если нажата кнопка 1
      if(button=="BUTT_1")
        {

        }
      //--- Если нажата кнопка 2
      else if(button=="BUTT_2")
        {

        }
      //--- Подождём 1/10 секунды
      engine.Pause(100);
      //--- "Отожмём" кнопку (если это не кнопка трейлинга и не кнопки активации работы отложенными запросами)
      ButtonState(button_name,false);
      //--- перерисуем чарт
      ChartRedraw();
     }
   //--- Не нажата
   else 
     {
      //--- кнопка 1
      if(button=="BUTT_1")
        {
         ButtonState(button_name,false);
        }
      //--- кнопка 2
      if(button=="BUTT_2")
        {
         ButtonState(button_name,false);
        }
      //--- перерисуем чарт
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

Как видим, большинство функций советника без проблем и переделок можно использовать в индикаторах. Это наталкивает на мысль, что все действительно необходимые функции для работы с библиотекой из советников и индикаторов нужно будет перенести в подключаемый файл библиотеки, и оттуда уже использовать. Но это потом, не сейчас. Сейчас нам нужно создать обработчик 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);

//--- Обработка события Calculate в библиотеке
   engine.OnCalculate(rates_data);

//--- Если работа в тестере
   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(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Установка массивов буферов как таймсерий
   ArraySetAsSeries(Buffer1,true);
   ArraySetAsSeries(Buffer2,true);

//--- Проверка на минимальное количество баров для расчёта
   if(rates_total<2 || Point()==0) return 0;

//--- Проверка и расчёт количества просчитываемых баров
   int limit=rates_total-prev_calculated;
   if(limit>1)
     {
      limit=rates_total-1;
      ArrayInitialize(Buffer1,EMPTY_VALUE);
      ArrayInitialize(Buffer2,EMPTY_VALUE);
     }
//--- Подготовка данных
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // код для подготовки расчётных буферов индикатора
     }

//--- Расчёт индикатора
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      Buffer1[i]=high[i];
      Buffer2[i]=low[i];
     }

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

Как видим, всё, что связано с работой библиотеки, всё уложилось в маленький блок кода в обработчике OnCalculate(), и по сути, тут разница с советником лишь в том, что мы предварительно заполняем структуру цен текущих данных массивов из OnCalculate() функцией CopyData(), а всё остальное абсолютно идентично работе в советнике — библиотека работает в таймере если индикатор запущен на графике символа, и в OnCalculate() по тикам — если индикатор запущен в тестере.
Буферы индикатора в расчётной части OnCalculate() просто заполним данными массивов high[] и low[].

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

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


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

Счёт 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10425.23 USD, 1:100, Hedge, Демонстрационный счёт MetaTrader 5
--- Инициализация библиотеки "DoEasy" ---
Работа только с текущим символом: "USDCAD"
Работа с заданным списком таймфреймов:
"M1"  "M5"  "M15" "M30" "H1"  "H4"  "D1"  "W1"  "MN1"
Таймсерия символа USDCAD: 
- Таймсерия "USDCAD" M1: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" M5: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" M15: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" M30: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" H1: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" H4: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" D1: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" W1: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
- Таймсерия "USDCAD" MN1: Запрошено: 1000, Фактически: 0, Создано: 0, На сервере: 0
Время инициализации библиотеки: 00:00:01.406
Таймсерия "USDCAD" M1 создана успешно:
- Таймсерия "USDCAD" M1: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 5001
Таймсерия "USDCAD" M5 создана успешно:
- Таймсерия "USDCAD" M5: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 5741
Таймсерия "USDCAD" M15 создана успешно:
- Таймсерия "USDCAD" M15: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 5247
Таймсерия "USDCAD" M30 создана успешно:
- Таймсерия "USDCAD" M30: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 5123
Таймсерия "USDCAD" H1 создана успешно:
- Таймсерия "USDCAD" H1: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 6257
Таймсерия "USDCAD" H4 создана успешно:
- Таймсерия "USDCAD" H4: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 6232
Таймсерия "USDCAD" D1 создана успешно:
- Таймсерия "USDCAD" D1: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 5003
Таймсерия "USDCAD" W1 создана успешно:
- Таймсерия "USDCAD" W1: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 1403
Таймсерия "USDCAD" MN1 создана успешно:
- Таймсерия "USDCAD" MN1: Запрошено: 1000, Фактически: 323, Создано: 323, На сервере: 323
Новый бар на USDCAD M1: 2020.03.19 12:18
Новый бар на USDCAD M1: 2020.03.19 12:19
Новый бар на USDCAD M1: 2020.03.19 12:20
Новый бар на USDCAD M5: 2020.03.19 12:20

Здесь видим, что при инициализации библиотеки были созданы все запрашиваемые таймсерии, но данными они заполнены не были в виду их отсутствия. При первом обращении к запрашиваемым данным была инициирована подкачка данных терминалом. И по приходу каждого последующего тика мы получали очередной пустой объект-таймсерию, синхронизировали его данные с сервером и заполняли объект-таймсерию данными баров в запрошенном количестве. На MN1 фактически доступно всего 323 бара — они и были все добавлены в список-таймсерию.

Теперь запустим индикатор в визуальном режиме тестера с теми же настройками:


Тестер подгружает необходимую историю по всем используемым таймфреймам, библиотека сообщает о создании всех таймсерий кроме текущей, затем на первом входе в OnCalculate() успешно пересоздаётся таймсерия для текущего символа и периода, и далее после снятия с паузы тестера, видим как в тестере отрабатывают события "Новый бар" используемых таймсерий.

Всё работает так, как и предполагалось.

Что дальше

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

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

К содержанию

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

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