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

13 марта 2020, 10:28
Artyom Trishkin
0
986

Содержание


Концепция

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

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

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

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

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


Доработка классов таймсерий

В первую очередь, как это уже повелось, добавим в файл Datas.mqh индекс нового сообщения библиотеки:

//--- CTimeSeries
   MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL,             // Сначала нужно установить символ при помощи SetSymbol()
   MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE,                   // Таймсерия не используется. Нужно установить флаг использования при помощи SetAvailable()
   MSG_LIB_TEXT_TS_TEXT_UNKNOWN_TIMEFRAME,            // Неизвестный таймфрейм

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

   {"Сначала нужно установить символ при помощи SetSymbol()","First you need to set the Symbol using SetSymbol()"},
   {"Таймсерия не используется. Нужно установить флаг использования при помощи SetAvailable()","Timeseries are not used. Need to set the use flag using SetAvailable()"},
   {"Неизвестный таймфрейм","Unknown timeframe"},

Класс базового объекта всех объектов библиотеки CBaseObj имеет в своём составе две переменные:

//+------------------------------------------------------------------+
//| Класс базового объекта для всех объектов библиотеки              |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   ENUM_LOG_LEVEL    m_log_level;                              // Уровень логирования
   ENUM_PROGRAM_TYPE m_program;                                // Тип программы
   bool              m_first_start;                            // Флаг первого запуска
   bool              m_use_sound;                              // Флаг проигрывания установленного объекту звука
   bool              m_available;                              // Флаг использования объекта-наследника в программе
   int               m_global_error;                           // Код глобальной ошибки
   long              m_chart_id_main;                          // Идентификатор графика управляющей программы
   long              m_chart_id;                               // Идентификатор графика
   string            m_name;                                   // Наименование объекта
   string            m_folder_name;                            // Имя папки хранения объектов-наследников CBaseObj
   string            m_sound_name;                             // Имя звукового файла объекта
   int               m_type;                                   // Тип объекта (соответствует идентификаторам коллекций)

public:

Переменная m_chart_id_main хранит идентификатор графика управляющей программы — это график символа, на котором запущена программа. На этот график библиотека должна отправлять все события, которые регистрируются в коллекциях и объектах библиотеки.
А переменная m_chart_id служит для хранения идентификатора графика, к которому каким-либо образом относится объект, являющийся наследником класса CBaseObj. Пока это свойство нигде не используется, но в дальнейшем будет нужно.

Но так как переменную m_chart_id_main мы добавили чуть позже переменной m_chart_id, то все сообщения отправляются на идентификатор графика, записанный в переменной m_chart_id. Это несоответствие было исправлено — теперь все идентификаторы текущего графика записываются в переменную m_chart_id_main как и должно быть, и во все классы, в которых есть отправка сообщений из библиотеки на график управляющей программы, были внесены правки — все вхождения текста "m_chart_id" заменеы на "m_chart_id_main".
Такие изменения были внесены во все классы событий из папки \MQL5\Include\DoEasy\Objects\Events\ и в файлы классов коллекций AccountsCollection.mqh, EventsCollection.mqh и SymbolsCollection.mqh.
Для экономии места в статье эти изменения здесь описывать не будем — их можно будет посмотреть в прилагаемых к статье файлах.

Для возможности вывода данных указанного бара из коллекции таймсерий добавим текстовое описание параметров бара класса CBar
в файле \MQL5\Include\DoEasy\Objects\Series\Bar.mqh.

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

//+------------------------------------------------------------------+
//| Описания свойств объекта-бара                                    |
//+------------------------------------------------------------------+
//--- Возвращает описание (1) целочисленного, (2) вещественного и (3) строкового свойства бара
   string            GetPropertyDescription(ENUM_BAR_PROP_INTEGER property);
   string            GetPropertyDescription(ENUM_BAR_PROP_DOUBLE property);
   string            GetPropertyDescription(ENUM_BAR_PROP_STRING property);

//--- Возвращает описание типа бара
   string            BodyTypeDescription(void)  const;
//--- Выводит в журнал описание свойств бара (full_prop=true - все свойства, false - только поддерживаемые)
   void              Print(const bool full_prop=false);
//--- Выводит в журнал краткое описание бара
   virtual void      PrintShort(void);
//--- Возвращает (1) краткое наименование, (2) описание параметров объекта-бара
   virtual string    Header(void);
   string            ParameterDescription(void);
//---
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает описание параметров объекта-бара                      |
//+------------------------------------------------------------------+
string CBar::ParameterDescription(void)
  {
   int dg=(this.m_digits>0 ? this.m_digits : 1);
   return
     (
      ::TimeToString(this.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+", "+
      "O: "+::DoubleToString(this.Open(),dg)+", "+
      "H: "+::DoubleToString(this.High(),dg)+", "+
      "L: "+::DoubleToString(this.Low(),dg)+", "+
      "C: "+::DoubleToString(this.Close(),dg)+", "+
      "V: "+(string)this.VolumeTick()+", "+
      (this.VolumeReal()>0 ? "R: "+(string)this.VolumeReal()+", " : "")+
      this.BodyTypeDescription()
     );
  }
//+------------------------------------------------------------------+
//| Выводит в журнал краткое описание бара                           |
//+------------------------------------------------------------------+
void CBar::PrintShort(void)
  {
   ::Print(this.Header(),": ",this.ParameterDescription());
  }
//+------------------------------------------------------------------+

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

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

Класс "Новый тик" и обновление данных

Создадим в каталоге библиотеки \MQL5\Include\DoEasy\Objects\ новую папку Ticks\, а в ней новый файл NewTickObj.mqh класса CNewTickObj, унаследованного от класса базового объекта всех объектов библиотеки CBaseObj, файл которого подключен к файлу класса, и сразу заполним его необходимыми данными:

//+------------------------------------------------------------------+
//|                                                   NewTickObj.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\..\Objects\BaseObj.mqh"
//+------------------------------------------------------------------+
//| Класс "Новый тик"                                                |
//+------------------------------------------------------------------+
class CNewTickObj : public CBaseObj
  {
private:
   MqlTick           m_tick;                          // Структура текущих цен
   MqlTick           m_tick_prev;                     // Структура текущих цен на прошлой проверке
   string            m_symbol;                        // Символ объекта
   bool              m_new_tick;                      // Флаг нового тика
public:
//--- Устанавливает симвод
   void              SetSymbol(const string symbol)   { this.m_symbol=symbol;             }
//--- Возвращает флаг появления нового тика
   bool              IsNewTick(void);
//--- Обновляет ценовые данные в структуре тика и устанавливает флаг события "Новый тик" если таковое есть
   void              Refresh(void)                    { this.m_new_tick=this.IsNewTick(); }
//--- Конструкторы
                     CNewTickObj(void){;}
                     CNewTickObj(const string symbol);
  };
//+------------------------------------------------------------------+

В переменной m_tick будем хранить ценовые данные последнего поступившего тика.

В переменной m_tick_prev будем хранить ценовые данные прошлого тика.

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

Флаг нового тика в переменной m_new_tick будет использоваться в дальнейшем.
На данный момент — для текущих потребностей библиотеки — факт события "Новый тик" на символе будем определять методом IsNewTick():

//+------------------------------------------------------------------+
//| Возвращает флаг появления нового тика                            |
//+------------------------------------------------------------------+
bool CNewTickObj::IsNewTick(void)
  {
//--- Если не удалось получить текущие цены в структуру тика - возвращаем false
   if(!::SymbolInfoTick(this.m_symbol,this.m_tick))
      return false;
//--- Если это первый запуск - копируем данные полученного тика в данные прошлого тика
//--- сбрасываем флаг первого запуска и возвращаем false
   if(this.m_first_start)
     {
      this.m_tick_prev=this.m_tick;
      this.m_first_start=false;
      return false;
     }
//--- Если время нового тика не равно времени тика на прошлой проверке -
//--- копируем данные полученного тика в данные прошлого тика и возвращаем true
   if(this.m_tick.time_msc!=this.m_tick_prev.time_msc)
     {
      this.m_tick_prev=this.m_tick;
      return true;
     }
//--- В любых иных случаях возвращаем false
   return false;
  }
//+------------------------------------------------------------------+

У класса определены два конструктора:

  • конструктор по умолчанию, без параметров — служит для определения объекта "Новый тик" в составе иного класса. В таком случае, обязательно нужно методом класса SetSymbol() установить символ объекту класса CNewTickObj, для которого и будут определяться события "Новый тик".
  • параметрический конструктор — служит для создания объекта класса посредством оператора new. В данном случае при создании объекта можно сразу же указать символ, для которого создаётся этот объект.
//+------------------------------------------------------------------+
//| Параметрический конструктор CNewTickObj                          |
//+------------------------------------------------------------------+
CNewTickObj::CNewTickObj(const string symbol) : m_symbol(symbol)
  {
//--- Обнуляем структуры нового и прошлого тиков
   ::ZeroMemory(this.m_tick);
   ::ZeroMemory(this.m_tick_prev);
//--- Если удалось получить текущие цены в структуру тика -
//--- копируем данные полученного тика в данные прошлого тика и сбрасываем флаг первого запуска
  if(::SymbolInfoTick(this.m_symbol,this.m_tick))
     { 
      this.m_tick_prev=this.m_tick;
      this.m_first_start=false;
     }
  }
//+------------------------------------------------------------------+

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

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

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

Добавим в конец листинга файла Defines.mqh новое перечисление со списком возможных событий объекта-таймсерии:

//+------------------------------------------------------------------+
//| Список возможных событий таймсерий                               |
//+------------------------------------------------------------------+
enum ENUM_SERIES_EVENT
  {
   SERIES_EVENTS_NO_EVENT = SYMBOL_EVENTS_NEXT_CODE,        // Нет события
   SERIES_EVENTS_NEW_BAR,                                   // Событие "Новый бар"
  };
#define SERIES_EVENTS_NEXT_CODE  (SERIES_EVENTS_NEW_BAR+1)  // Код следующего события после события "Новый бар"
//+------------------------------------------------------------------+

Здесь пока есть только два состояния событий таймсерии: "Нет события" и событие "Новый бар". Константы данного перечисления нам нужны будут для выполнения поиска по заданным свойствам объекта-бара в списке-коллекции баров (в таймсерии CSeries).

Так как обновляться объекты таймсерий будут в таймере библиотеки, то добавим в листинг файла Defines.mqh параметры таймера обновления коллекции объектов-таймсерий и идентификатор списка коллекции таймсерий:

//--- Параметры таймера торгового класса
#define COLLECTION_REQ_PAUSE           (300)                      // Пауза таймера торгового класса в милисекундах
#define COLLECTION_REQ_COUNTER_STEP    (16)                       // Шаг приращения счётчика таймера торгового класса
#define COLLECTION_REQ_COUNTER_ID      (5)                        // Идентификатор счётчика таймера торгового класса
//--- Параметры таймера коллекции таймсерий
#define COLLECTION_TS_PAUSE            (32)                       // Пауза таймера коллекции таймсерий в милисекундах
#define COLLECTION_TS_COUNTER_STEP     (16)                       // Шаг приращения счётчика таймера таймсерий
#define COLLECTION_TS_COUNTER_ID       (6)                        // Идентификатор счётчика таймера таймсерий
//--- Идентификаторы списков коллекций
#define COLLECTION_HISTORY_ID          (0x777A)                   // Идентификатор списка исторической коллекции
#define COLLECTION_MARKET_ID           (0x777B)                   // Идентификатор списка рыночной коллекции
#define COLLECTION_EVENTS_ID           (0x777C)                   // Идентификатор списка коллекции событий
#define COLLECTION_ACCOUNT_ID          (0x777D)                   // Идентификатор списка коллекции аккаунтов
#define COLLECTION_SYMBOLS_ID          (0x777E)                   // Идентификатор списка коллекции символов
#define COLLECTION_SERIES_ID           (0x777F)                   // Идентификатор списка коллекции таймсерий
//--- Параметры данных для файловых операций

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

И сразу же присвоим объекту-бар идентификатор коллекции таймсерий, так как объект-таймсерия является списком, в котором содержатся указатели на объекты-бары, принадлежащие этому списку.
Ещё раз откроем файл \MQL5\Include\DoEasy\Objects\Series\Bar.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 || !::TimeToStruct(rates_array[0].time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print(DFUN,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;
     }
//--- Устанавливаем свойства бара
   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,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};
      this.SetProperties(err);
      return;
     }
//--- Устанавливаем свойства бара
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+


Теперь доработаем класс объекта-таймсерии CSeries, расположенного по адресу \MQL5\Include\DoEasy\Objects\Series\Series.mqh.

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

//--- (1) Создаёт, (2) обновляет список-таймсерию
   int               Create(const uint required=0);
   void              Refresh(const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);
                             
//--- Создаёт и отправляет событие "Новый бар" на график управляющей программы
   void              SendEvent(void);

//--- Возвращает наименование таймсерии
   string            Header(void);
//--- Выводит в журнал (1) описание таймсерии, (2) краткое описание таймсерии
   void              Print(void);
   void              PrintShort(void);


//--- Конструкторы
                     CSeries(void);
                     CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
  };
//+------------------------------------------------------------------+

В конце листинга класса напишем реализацию объявленного метода:

//+------------------------------------------------------------------+
//| Создаёт и отправляет событие "Новый бар"                         |
//| на график управляющей программы                                  |
//+------------------------------------------------------------------+
void CSeries::SendEvent(void)
  {
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(0),this.Timeframe(),this.Symbol());
  }
//+------------------------------------------------------------------+

Здесь: создаём и отправляем на график управляющей программы событие, состоящее из:
идентификатора графика-получателя события,
идентификатора события (Новый бар),
в качестве long-параметра события отправляем время открытия нового бара,
в качестве double-параметра события отправляем таймфрейм графика
, на котором произошло событие, и
в качестве string-параметра отправляем имя символа, на таймсерии которого произошло событие.

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

//+------------------------------------------------------------------+
//|Синхронизирует данные по символу и таймфрейму с данными на сервере|
//+------------------------------------------------------------------+
bool CSeries::SyncData(const uint required,const uint rates_total)
  {
//--- Если таймсерия не используется - сообщаем об этом и выходим
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      return false;
     }
//--- Если удалось получить доступное количество баров в таймсерии

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

Точно такую же проверку впишем и в метод создания таймсерии:

//+------------------------------------------------------------------+
//| Создаёт список-таймсерию                                         |
//+------------------------------------------------------------------+
int CSeries::Create(const uint required=0)
  {
//--- Если таймсерия не используется - сообщаем об этом и возвращаем ноль
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      return 0;
     }
//--- Если ещё не установлена требуемая глубина истории для списка

Также в классе был переделан метод, возвращающий объект-бар по индексу таймсерии. Ранее метод выглядел так:

//+------------------------------------------------------------------+
//| Возвращает объект-бар по индексу в таймсерии                     |
//+------------------------------------------------------------------+
CBar *CSeries::GetBarBySeriesIndex(const uint index)
  {
   CArrayObj *list=this.GetList(BAR_PROP_INDEX,index);
   return(list==NULL || list.Total()==0 ? NULL : list.At(0));
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар по индексу в таймсерии                     |
//+------------------------------------------------------------------+
CBar *CSeries::GetBarBySeriesIndex(const uint index)
  {
   CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index); 
   if(tmp==NULL)
      return NULL;
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
   int idx=this.m_list_series.Search(tmp);
   delete tmp;
   CBar *bar=this.m_list_series.At(idx);
   return(bar!=NULL ? bar : NULL);
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//|                                                   TimeSeries.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Series.mqh"
#include "..\Ticks\NewTickObj.mqh"
//+------------------------------------------------------------------+
//| Класс "Таймсерии символа"                                        |
//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObj
  {
private:
   string            m_symbol;                                             // Символ таймсерий
   CNewTickObj       m_new_tick;                                           // Объект "Новый тик"
   CArrayObj         m_list_series;                                        // Список таймсерий по таймфреймам
   datetime          m_server_firstdate;                                   // Самая первая дата в истории по символу на сервере
   datetime          m_terminal_firstdate;                                 // Самая первая дата в истории по символу в клиентском терминале
//--- Возвращает (1) индекс таймфрейма в списке, (2) таймфрейм по индексу списка
   char              IndexTimeframe(const ENUM_TIMEFRAMES timeframe) const { return IndexEnumTimeframe(timeframe)-1;                            }
   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:
//--- Возвращает (1) себя, (2) полный список таймсерий, (3) указанный объект-таймсерию, (4) объект-таймсерию по индексу
   CTimeSeries      *GetObject(void)                                       { return &this;                                                      }
   CArrayObj        *GetListSeries(void)                                   { return &this.m_list_series;                                        }
   CSeries          *GetSeries(const ENUM_TIMEFRAMES timeframe)            { return this.m_list_series.At(this.IndexTimeframe(timeframe));      }
   CSeries          *GetSeriesByIndex(const uchar index)                   { return this.m_list_series.At(index);                               }
//--- Устанавливает/возвращает символ таймсерии
   void              SetSymbol(const string symbol)                        { this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);  }
   string            Symbol(void)                                    const { return this.m_symbol;                                              }
//--- Устанавливает глубину истории (1) указанной таймсерии, (2) всех используемых таймсерий символа
   bool              SetRequiredUsedData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool              SetRequiredAllUsedData(const uint required=0,const int rates_total=0);
//--- Возвращает флаг синхронизации данных с данными на сервере (1) указанной таймсерии, (2) всех таймсерий
   bool              SyncData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool              SyncAllData(const uint required=0,const int rates_total=0);
//--- Возвращает самую первую дату в истории по символу (1) на сервере, (2) в клиентском терминале, (3) флаг нового тика
   datetime          ServerFirstDate(void)                           const { return this.m_server_firstdate;                                    }
   datetime          TerminalFirstDate(void)                         const { return this.m_terminal_firstdate;                                  }
   bool              IsNewTick(void)                                       { return this.m_new_tick.IsNewTick();                                }
//--- Создаёт (1) указанный список-таймсерию, (2) все списки-таймсерии
   bool              Create(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool              CreateAll(const uint required=0);
//--- Обновляет (1) указанный список-таймсерию, (2) все списки-таймсерии
   void              Refresh(const ENUM_TIMEFRAMES timeframe,
                             const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);
   void              RefreshAll(const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);

//--- Сравнивает объекты 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);
  };
//+------------------------------------------------------------------+

Метод IsNewTick() возвращает результат запроса данных о новом тике из объекта "Новый тик" m_new_tick.

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает флаг синхронизации данных                             |
//| указанной таймсерии с данными на сервере                         |
//+------------------------------------------------------------------+
bool CTimeSeries::SyncData(const ENUM_TIMEFRAMES timeframe,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;
     }
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe));
      return false;
     }
   if(!series_obj.IsAvailable())
      return false;
   return series_obj.SyncData(required,rates_total);
  }
//+------------------------------------------------------------------+
//| Возвращает флаг синхронизации данных                             |
//| всех таймсерй с данными на сервере                               |
//+------------------------------------------------------------------+
bool CTimeSeries::SyncAllData(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 || !series_obj.IsAvailable())
         continue;
      res &=series_obj.SyncData(required,rates_total);
     }
   return res;
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Создаёт указанный список-таймсерию                               |
//+------------------------------------------------------------------+
bool CTimeSeries::Create(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe));
      return false;
     }
   if(series_obj.RequiredUsedData()==0)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_TEXT_FIRS_SET_AMOUNT_DATA));
      return false;
     }
   series_obj.SetAvailable(true);
   return(series_obj.Create(required)>0);
  }
//+------------------------------------------------------------------+
//| Создаёт все списки-таймсерии                                     |
//+------------------------------------------------------------------+
bool CTimeSeries::CreateAll(const uint required=0)
  {
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || series_obj.RequiredUsedData()==0)
         continue;
      series_obj.SetAvailable(true);
      res &=(series_obj.Create(required)>0);
     }
   return res;
  }
//+------------------------------------------------------------------+

В методах обновления таймсерии в случае, если у неё обнаружено событие "Новый бар", добавим отправку сообщения об этом событии на график управляющей программы при помощи метода SendEvent() объекта таймсерии CSeries, рассмотренного нами выше:

//+------------------------------------------------------------------+
//| Обновляет указанный список-таймсерию                             |
//+------------------------------------------------------------------+
void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,
                          const datetime time=0,
                          const double open=0,
                          const double high=0,
                          const double low=0,
                          const double close=0,
                          const long tick_volume=0,
                          const long volume=0,
                          const int spread=0)
  {
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0)
      return;
   series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread);
   if(series_obj.IsNewBar(time))
     {
      series_obj.SendEvent();
      this.SetTerminalServerDate();
     }
  }
//+------------------------------------------------------------------+
//| Обновляет все списки-таймсерии                                   |
//+------------------------------------------------------------------+
void CTimeSeries::RefreshAll(const datetime time=0,
                          const double open=0,
                          const double high=0,
                          const double low=0,
                          const double close=0,
                          const long tick_volume=0,
                          const long volume=0,
                          const int spread=0)
  {
   bool upd=false;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || series_obj.DataTotal()==0)
         continue;
      series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread);
      if(series_obj.IsNewBar(time))
        {
         series_obj.SendEvent();
         upd &=true;
        }
     }
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+


Доработаем класс-коллекцию таймсерий CTimeSeriesCollection в файле \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh.

Сделаем тип списка-коллекции таймсерий типом класса CListObj.
Для этого подключим файл класса CListObj и изменим тип списка коллекции с CArrayObj на CListObj:

//+------------------------------------------------------------------+
//|                                         TimeSeriesCollection.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Objects\Series\TimeSeries.mqh"
#include "..\Objects\Symbols\Symbol.mqh"
//+------------------------------------------------------------------+
//| Коллекция таймсерий символов                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CObject
  {
private:
   CListObj                m_list;                    // Список используемых таймсерий символов
//--- Возвращает индекс таймсерии по имени символа
   int                     IndexTimeSeries(const string symbol);
public:

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

//--- Возвращает флаг синхронизации данных с данными на сервере (1) указанной таймсерии указанного символа,
//--- (2) указанной таймсерии всех символов, (3) всех таймсерий указанного символа, (4) всех таймсерий всех символов
   bool                    SyncData(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool                    SyncData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool                    SyncData(const string symbol,const uint required=0,const int rates_total=0);
   bool                    SyncData(const uint required=0,const int rates_total=0);

//--- Возвращает бар указанной таймсерии указанного символа указанной позиции 
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true);
//--- Возвращает флаг открытия нового бара указанной таймсерии указанного символа
   bool                    IsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0);

//--- Создаёт (1) указанную таймсерию указанного символа, (2) указанную таймсерии всех символов,
//--- (3) все таймсерии указанного символа, (4) все таймсерии всех символов
   void                    RefreshOther(void);

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

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CTimeSeriesCollection::CTimeSeriesCollection()
  {
   this.m_list.Clear();
   this.m_list.Sort();
   this.m_list.Type(COLLECTION_SERIES_ID);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает бар указанной таймсерии                               |
//| указанного символа указанной позиции                             |
//| from_series=true - по индексу таймсерии, false - списка          |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
  {
//--- Получаем индекс объекта-таймсерии в списке-коллекции таймсерий по наименованию символа
   int idx=this.IndexTimeSeries(symbol);
   if(idx==WRONG_VALUE)
      return NULL;
//--- Получаем указатель на объект-таймсерий из списка-коллекции объектов-таймсерий по полученному индексу
   CTimeSeries *timeseries=this.m_list.At(idx);
   if(timeseries==NULL)
      return NULL;
//--- Получаем указанную таймсерию из объекта-таймсерий символа по указанному таймфрейму
   CSeries *series=timeseries.GetSeries(timeframe);
   if(series==NULL)
      return NULL;
//--- В зависимости от флага from_series возвращаем указатель на бар
//--- либо по индексу таймсерии графика, либо по индексу бара в списке-таймсерии
   return(from_series ? series.GetBarBySeriesIndex(index) : series.GetBarByListIndex(index));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг открытия нового бара                             |
//| указанной таймсерии указанного символа                           |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::IsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0)
  {
//--- Получаем индекс объекта-таймсерии в списке-коллекции таймсерий по наименованию символа
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return false;
//--- Получаем указатель на объект-таймсерий из списка-коллекции объектов-таймсерий по полученному индексу
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return false;
//--- Получаем указанную таймсерию из объекта-таймсерий символа по указанному таймфрейму
   CSeries *series=timeseries.GetSeries(timeframe);
   if(series==NULL)
      return false;
//--- Возвращаем результат проверки нового бара указанной таймсерии
   return series.IsNewBar(time);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обновляет все таймсерии  кроме текущего символа                  |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::RefreshOther(void)
  {
   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.Symbol()==::Symbol() || !timeseries.IsNewTick())
         continue;
      timeseries.RefreshAll();
     }
  }
//+------------------------------------------------------------------+

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

В этот метод, а также в представленные ниже методы обновления таймсерий, добавлена проверка на флаг нового тика, и если нет нового тика, то таймсерия пропускается и её данные не обновляются:

//+------------------------------------------------------------------+
//| Обновляет указанную таймсерию указанного символа                 |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return;
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return;
   if(!timeseries.IsNewTick())
      return;
   timeseries.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread);
  }
//+------------------------------------------------------------------+
//| Обновляет указанную таймсерию всех символов                      |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const ENUM_TIMEFRAMES timeframe,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   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.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread);
     }
  }
//+------------------------------------------------------------------+
//| Обновляет все таймсерии указанного символа                       |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return;
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return;
   if(!timeseries.IsNewTick())
      return;
   timeseries.RefreshAll(time,open,high,low,close,tick_volume,volume,spread);
  }
//+------------------------------------------------------------------+
//| Обновляет все таймсерии всех символов                            |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   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(time,open,high,low,close,tick_volume,volume,spread);
     }
  }
//+------------------------------------------------------------------+


Завершающим этапом нужно внести необходимые доработки в файл класса главного объекта библиотеки 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_series;                       // Коллекция таймсерий
   CResourceCollection  m_resource;                      // Список ресурсов
   CTradingControl      m_trading;                       // Объект управления торговлей
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   int                  m_global_error;                  // Код глобальной ошибки
   bool                 m_first_start;                   // Флаг первого запуска
   bool                 m_is_hedge;                      // Флаг хедж-счёта
   bool                 m_is_tester;                     // Флаг работы в тестере
   bool                 m_is_market_trade_event;         // Флаг торгового события на счёте
   bool                 m_is_history_trade_event;        // Флаг торгового события в истории счёта
   bool                 m_is_account_event;              // Флаг события изменения аккаунта
   bool                 m_is_symbol_event;               // Флаг события изменения свойств символа
   ENUM_TRADE_EVENT     m_last_trade_event;              // Последнее торговое событие на счёте
   int                  m_last_account_event;            // Последнее событие в свойствах счёта
   int                  m_last_symbol_event;             // Последнее событие в свойствах символа
   ENUM_PROGRAM_TYPE    m_program;                       // Тип программы

В публичной секции класса объявим метод для обработки событий экспертов NewTick:

//--- (1) Таймер, (2) обработчик события NewTick
   void                 OnTimer(void);
   void                 OnTick(void);

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

//--- Создаёт (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);                           }

//--- Возвращает бар указанной таймсерии указанного символа указанной позиции
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
                          { return this.m_series.GetBar(symbol,timeframe,index,from_series);                   }
//--- Возвращает флаг открытия нового бара указанной таймсерии указанного символа
   bool                 SeriesIsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0)
                          { return this.m_series.IsNewBar(symbol,timeframe,time);                  }

//--- Обновляет (1) указанную таймсерию указанного символа, (2) указанную таймсерии всех символов,
//--- (3) все таймсерии указанного символа, (4) все таймсерии всех символов

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

//--- Возвращает (1) Open, (2) High, (3) Low, (4) Close, (5) Time, (6) TickVolume,
//--- (7) RealVolume, (8) Spread указанного бара указанного символа указанного таймфрейма
   double               SeriesOpen(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   datetime             SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   long                 SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   long                 SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   int                  SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);

//--- Устанавливает для торговых классов:
//--- (1) корректное значение политики исполнения, (2) значение политики исполнения,
//--- (3) корректный тип истечения ордера, (4) тип истечения ордера,
//--- (5) магик, (6) Комментарий, (7) размер проскальзывания, (8) объём, (9) срок истечения ордера,
//--- (10) флаг асинхронной отправки торгового запроса, (11) уровень логирования, (12) количество торговых попыток

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

//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),
                     m_last_trade_event(TRADE_EVENT_NO_EVENT),
                     m_last_account_event(WRONG_VALUE),
                     m_last_symbol_event(WRONG_VALUE),
                     m_global_error(ERR_SUCCESS)
  {
   this.m_is_hedge=#ifdef __MQL4__ true #else bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) #endif;
   this.m_is_tester=::MQLInfoInteger(MQL_TESTER);
   this.m_program=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE);
   
   this.m_list_counters.Sort();
   this.m_list_counters.Clear();
   this.CreateCounter(COLLECTION_ORD_COUNTER_ID,COLLECTION_ORD_COUNTER_STEP,COLLECTION_ORD_PAUSE);
   this.CreateCounter(COLLECTION_ACC_COUNTER_ID,COLLECTION_ACC_COUNTER_STEP,COLLECTION_ACC_PAUSE);
   this.CreateCounter(COLLECTION_SYM_COUNTER_ID1,COLLECTION_SYM_COUNTER_STEP1,COLLECTION_SYM_PAUSE1);
   this.CreateCounter(COLLECTION_SYM_COUNTER_ID2,COLLECTION_SYM_COUNTER_STEP2,COLLECTION_SYM_PAUSE2);
   this.CreateCounter(COLLECTION_REQ_COUNTER_ID,COLLECTION_REQ_COUNTER_STEP,COLLECTION_REQ_PAUSE);
   this.CreateCounter(COLLECTION_TS_COUNTER_ID,COLLECTION_TS_COUNTER_STEP,COLLECTION_TS_PAUSE);
   
   ::ResetLastError();
   #ifdef __MQL5__
      if(!::EventSetMillisecondTimer(TIMER_FREQUENCY))
        {
         ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_TIMER),(string)::GetLastError());
         this.m_global_error=::GetLastError();
        }
   //---__MQL4__
   #else 
      if(!this.IsTester() && !::EventSetMillisecondTimer(TIMER_FREQUENCY))
        {
         ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_TIMER),(string)::GetLastError());
         this.m_global_error=::GetLastError();
        }
   #endif 
   //---
  }
//+------------------------------------------------------------------+

В обработчик OnTimer() библиотеки добавим работу с таймером коллекции таймсерий (лишний код вырезан):

//+------------------------------------------------------------------+
//| CEngine таймер                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(void)
  {
//--- Таймер коллекций исторических ордеров и сделок и рыночных ордеров и позиций
//...

//--- Таймер коллекции аккаунтов
//...
     
//--- Таймер1 коллекции символов (обновление котировочных данных символов в коллекции)
//...

//--- Таймер2 коллекции символов (обновление всех данных всех символов в коллекции и отслеживание событий символов и списка символов в окне обзора рынка)
//...

//--- Таймер торгового класса
//...
     
//--- Таймер коллекции таймсерий
   index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         //--- Если это не тестер
         if(!this.IsTester())
           {
            //--- Если пауза завершилась - работаем со списком таймсерий (кроме таймсерий текущего символа)
            if(counter.IsTimeDone())
               this.m_series.RefreshOther();
           }
         //--- Если тестер - работаем со списком таймсерий по тику (кроме таймсерий текущего символа)
         else
            this.m_series.RefreshOther();
        }
     }
  }
//+------------------------------------------------------------------+

Работа со счётчиками таймеров коллекций, как и с самим таймером, рассматривалась при создании главного объекта библиотеки CEngine, а всё остальное тут описано в комментариях к коду.
Одно замечание: в таймере обрабатываются только те таймсерии, символ которых не совпадает с символом графика, на котором запущена программа.
Так как здесь — в таймере — мы обновляем таймсерии при регистрации событий "Новый тик" для "неродных" символов, то эти события мы и отлавливаем в таймере.
А для обновления таймсерий текущего символа у нас создан метод OnTick(), который будем запускать из обработчика OnTick() эксперта:

//+------------------------------------------------------------------+
//| Обработчик события NewTick                                       |
//+------------------------------------------------------------------+
void CEngine::OnTick(void)
  {
//--- Если это не эксперт - уходим
   if(this.m_program!=PROGRAM_EXPERT)
      return;
//--- Обновление таймсерий текущего символа
   this.SeriesRefresh(NULL,PERIOD_CURRENT);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает Open указанного бара                                  |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
double CEngine::SeriesOpen(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Open() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает High указанного бара                                  |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
double CEngine::SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.High() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает Low указанного бара                                   |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
double CEngine::SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Low() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает Close указанного бара                                 |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
double CEngine::SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Close() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает Time указанного бара                                  |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
datetime CEngine::SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает TickVolume указанного бара                            |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
long CEngine::SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.VolumeTick() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает RealVolume указанного бара                            |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
long CEngine::SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.VolumeReal() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает Spread указанного бара                                |
//| указанного символа указанного таймфрейма                         |
//+------------------------------------------------------------------+
int CEngine::SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Spread() : INT_MIN);
  }
//+------------------------------------------------------------------+

Здесь просто: получаем объект бар по символу и таймфрейму таймсерии из указанного индекса таймсерии графика (0 — текущий бар), и возвращаем соответствующее свойство бара.

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

Тестирование

Проверим работу таким образом:
создадим три таймсерии для текущих таймфреймов трёх символов, получим объект нулевого бара (класс CBar) из объекта-коллекции таймсерий (CTimeSeriesCollection) и выведем в комментарии на графике данные этого бара при помощи его методов, возвращающих краткое наименование объекта-бара + описание параметров объекта-бара. Второй строкой комментария мы выведем данные нулевого бара в похожем формате, но созданные при помощи методов главного объекта библиотеки CEngine, возвращающих данные указанного бара указанного символа указанного таймфрейма.

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

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

Изменим обработчик OnTick() советника таким образом:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Если работа в тестере
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer();       // Работа в таймере
      PressButtonsControl();  // Контроль нажатия кнопок
      EventsHandling();       // Работа с событиями
     }
//--- Обработка события NewTick в библиотеке
   engine.OnTick();

//--- Если установлен флаг трейлинга
   if(trailing_on)
     {
      TrailingPositions();    // Трейлинг позиций
      TrailingOrders();       // Трейлинг отложенных ордеров
     }
   
//--- Получим нулевой бар текущей таймсерии
   CBar *bar=engine.SeriesGetBar(NULL,PERIOD_CURRENT,0);
   if(bar==NULL)
      return;
//--- Создаём строку параметров текущего бара такую же,
//--- какая выводится описанием от объекта-бара:
//--- bar.Header()+": "+bar.ParameterDescription()
   string parameters=
     (TextByLanguage("Бар \"","Bar \"")+Symbol()+"\" "+TimeframeDescription((ENUM_TIMEFRAMES)Period())+"[0]: "+TimeToString(bar.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+
      ", O: "+DoubleToString(engine.SeriesOpen(NULL,PERIOD_CURRENT,0),Digits())+
      ", H: "+DoubleToString(engine.SeriesHigh(NULL,PERIOD_CURRENT,0),Digits())+
      ", L: "+DoubleToString(engine.SeriesLow(NULL,PERIOD_CURRENT,0),Digits())+
      ", C: "+DoubleToString(engine.SeriesClose(NULL,PERIOD_CURRENT,0),Digits())+
      ", V: "+(string)engine.SeriesTickVolume(NULL,PERIOD_CURRENT,0)+
      ", Real: "+(string)engine.SeriesRealVolume(NULL,PERIOD_CURRENT,0)+
      ", Spread: "+(string)engine.SeriesSpread(NULL,PERIOD_CURRENT,0)
     );
//--- Выводим в комментарий на графике первой строкой данные, полученные от объекта-бара,
//--- и второй строкой - при помощи методов получения ценовых данных таймсерии
   Comment(bar.Header(),": ",bar.ParameterDescription(),"\n",parameters);
  }
//+------------------------------------------------------------------+

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

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

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

//--- Вывод списка используемых таймфреймов сделаем только для MQL5 - в MQL4 нет функции ArrayPrint()
#ifdef __MQL5__
   if(InpModeUsedTFs!=TIMEFRAMES_MODE_CURRENT)
      ArrayPrint(array_used_periods);
#endif 
//--- Создание таймсерий всех используемых символов
   CArrayObj *list_timeseries=engine.GetListTimeSeries();
   if(list_timeseries!=NULL)
     {
      int total=list_timeseries.Total();
      for(int i=0;i<total;i++)
        {
         CTimeSeries *timeseries=list_timeseries.At(i);
         int total_periods=ArraySize(array_used_periods);
         for(int j=0;j<total_periods;j++)
           {
            ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_used_periods[j]);
            timeseries.SyncData(timeframe);
            timeseries.Create(timeframe);
           }
        }
     }
//--- Проверка созданных таймсерий - выводим в журнал описания всех созданных таймсерий
//--- (true - только созданные, false - созданные и объявленные)
   engine.GetTimeSeriesCollection().PrintShort(true); // Краткие описания
   //engine.GetTimeSeriesCollection().Print(true);      // Полные описания

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

В функции обработки событий библиотеки OnDoEasyEvent() добавим блок кода для обработки событий таймсерий (лишний код вырезан):

//+------------------------------------------------------------------+
//| Обработка событий библиотеки 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;
   
//--- Обработка событий символов
//...  
     
//--- Обработка событий аккаунта
//...
     
//--- Обработка событий окна обзор рынка
//...
     
//--- Обработка событий таймсерий
   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));
        }
     }
     
//--- Обработка торговых событий
//...

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

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

Скомпилируем советник и зададим в его параметрах:

  • в Mode of used symbols list использование заданного списка символов,
  • в списке List of used symbols (comma - separator) оставим только три символа, один из которых EURUSD и
  • в Mode of used timeframes list выберем работу только с текущим таймфреймом, например:


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

Новый бар на EURUSD M5: 2020.03.11 12:55
Новый бар на EURAUD M5: 2020.03.11 12:55
Новый бар на AUDUSD M5: 2020.03.11 12:55
Новый бар на EURUSD M5: 2020.03.11 13:00
Новый бар на AUDUSD M5: 2020.03.11 13:00
Новый бар на EURAUD M5: 2020.03.11 13:00

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


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

Что дальше

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

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

К содержанию

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

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


Прикрепленные файлы |
MQL5.zip (3723.82 KB)
MQL4.zip (3723.82 KB)
Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам

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

Мультивалютный мониторинг торговых сигналов (Часть 3): Внедряем алгоритмы поиска Мультивалютный мониторинг торговых сигналов (Часть 3): Внедряем алгоритмы поиска

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

Мультивалютный мониторинг торговых сигналов (Часть 4): Улучшаем функциональность  и систему поиска сигналов Мультивалютный мониторинг торговых сигналов (Часть 4): Улучшаем функциональность и систему поиска сигналов

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

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

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