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

2 апреля 2020, 09:26
Artyom Trishkin
0
1 016

Содержание


Концепция

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

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

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

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

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

В файле \MQL5\Include\DoEasy\Defines.mqh из перечисления целочисленных свойств объекта-бара удалим свойство индекса бара:

//+------------------------------------------------------------------+
//| Целочисленные свойства бара                                      |
//+------------------------------------------------------------------+
enum ENUM_BAR_PROP_INTEGER
  {
   BAR_PROP_INDEX = 0,                                      // Индекс бара в таймсерии
   BAR_PROP_TYPE,                                           // Тип бара (из перечисления ENUM_BAR_BODY_TYPE)

На его место переместим свойство "Время бара" и уменьшим на 1 количество целочисленных свойств объекта-бара14 до 13):

//+------------------------------------------------------------------+
//| Целочисленные свойства бара                                      |
//+------------------------------------------------------------------+
enum ENUM_BAR_PROP_INTEGER
  {
   BAR_PROP_TIME = 0,                                       // Время начала периода бара
   BAR_PROP_TYPE,                                           // Тип бара (из перечисления ENUM_BAR_BODY_TYPE)
   BAR_PROP_PERIOD,                                         // Период бара (таймфрейм)
   BAR_PROP_SPREAD,                                         // Спред бара
   BAR_PROP_VOLUME_TICK,                                    // Тиковый объём бара
   BAR_PROP_VOLUME_REAL,                                    // Биржевой объём бара

   BAR_PROP_TIME_DAY_OF_YEAR,                               // Порядковый номер дня бара в году
   BAR_PROP_TIME_YEAR,                                      // Год, к которому относится бар
   BAR_PROP_TIME_MONTH,                                     // Месяц, к которому относится бар
   BAR_PROP_TIME_DAY_OF_WEEK,                               // День недели бара
   BAR_PROP_TIME_DAY,                                       // День месяца бара (число)
   BAR_PROP_TIME_HOUR,                                      // Час бара
   BAR_PROP_TIME_MINUTE,                                    // Минута бара
  }; 
#define BAR_PROP_INTEGER_TOTAL (13)                         // Общее количество целочисленных свойств бара
#define BAR_PROP_INTEGER_SKIP  (0)                          // Количество неиспользуемых в сортировке свойств бара
//+------------------------------------------------------------------+

Соответственно, в перечислении возможных критериев сортировки баров нам необходимо точно так же удалить сортировку по индексу, и на её место поставить сортировку по времени бара:

//+------------------------------------------------------------------+
//| Возможные критерии сортировки баров                              |
//+------------------------------------------------------------------+
#define FIRST_BAR_DBL_PROP          (BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP)
#define FIRST_BAR_STR_PROP          (BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP+BAR_PROP_DOUBLE_TOTAL-BAR_PROP_DOUBLE_SKIP)
enum ENUM_SORT_BAR_MODE
  {
//--- Сортировка по целочисленным свойствам
   SORT_BY_BAR_TIME = 0,                                    // Сортировать по времени начала периода бара
   SORT_BY_BAR_TYPE,                                        // Сортировать по типу бара (из перечисления ENUM_BAR_BODY_TYPE)
   SORT_BY_BAR_PERIOD,                                      // Сортировать по периоду бара (таймфрейму)
   SORT_BY_BAR_SPREAD,                                      // Сортировать по спреду бара
   SORT_BY_BAR_VOLUME_TICK,                                 // Сортировать по тиковому объёму бара
   SORT_BY_BAR_VOLUME_REAL,                                 // Сортировать по биржевому объёму бара
   SORT_BY_BAR_TIME_DAY_OF_YEAR,                            // Сортировать по порядковому номеру дня бара в году
   SORT_BY_BAR_TIME_YEAR,                                   // Сортировать по году, к которому относится бар
   SORT_BY_BAR_TIME_MONTH,                                  // Сортировать по месяцу, к которому относится бар
   SORT_BY_BAR_TIME_DAY_OF_WEEK,                            // Сортировать по дню недели бара
   SORT_BY_BAR_TIME_DAY,                                    // Сортировать по дню бара
   SORT_BY_BAR_TIME_HOUR,                                   // Сортировать по часу бара
   SORT_BY_BAR_TIME_MINUTE,                                 // Сортировать по минуте бара
//--- Сортировка по вещественным свойствам
   SORT_BY_BAR_OPEN = FIRST_BAR_DBL_PROP,                   // Сортировать по цене открытия бара
   SORT_BY_BAR_HIGH,                                        // Сортировать по наивысшей цене за период бара
   SORT_BY_BAR_LOW,                                         // Сортировать по наименьшей цене за период бара
   SORT_BY_BAR_CLOSE,                                       // Сортировать по цене закрытия бара
   SORT_BY_BAR_CANDLE_SIZE,                                 // Сортировать по размеру свечи
   SORT_BY_BAR_CANDLE_SIZE_BODY,                            // Сортировать по размеру тела свечи
   SORT_BY_BAR_CANDLE_BODY_TOP,                             // Сортировать по верху тела свечи
   SORT_BY_BAR_CANDLE_BODY_BOTTOM,                          // Сортировать по низу тела свечи
   SORT_BY_BAR_CANDLE_SIZE_SHADOW_UP,                       // Сортировать по размеру верхней тени свечи
   SORT_BY_BAR_CANDLE_SIZE_SHADOW_DOWN,                     // Сортировать по размеру нижней тени свечи
//--- Сортировка по строковым свойствам
   SORT_BY_BAR_SYMBOL = FIRST_BAR_STR_PROP,                 // Сортировать по символу бара
  };
//+------------------------------------------------------------------+

Перестроим класс CBar в файле \MQL5\Include\DoEasy\Objects\Series\Bar.mqh на работу с временем бара.

Ранее метод SetSymbolPeriod() устанавливал объекту-бар указанный символ, период графика и индекс бара. Теперь вместо индекса будем устанавливать время бара:

//--- Устанавливает (1) символ, таймфрейм и время бара, (2) параметры объекта-бар
   void              SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   void              SetProperties(const MqlRates &rates);

Исправим и реализацию метода:

//+------------------------------------------------------------------+
//| Устанавливает символ, таймфрейм и индекс бара                    |
//+------------------------------------------------------------------+
void CBar::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time)
  {
   this.SetProperty(BAR_PROP_TIME,time);
   this.SetProperty(BAR_PROP_SYMBOL,symbol);
   this.SetProperty(BAR_PROP_PERIOD,timeframe);
   this.m_digits=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
   this.m_period_description=TimeframeDescription(timeframe);
  }
//+------------------------------------------------------------------+

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

//--- Конструкторы
                     CBar(){;}
                     CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time,const string source);
                     CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const MqlRates &rates);

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

//+------------------------------------------------------------------+
//| Конструктор 1                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time,const string source)
  {
   this.m_type=COLLECTION_SERIES_ID;
   MqlRates rates_array[1];
   this.SetSymbolPeriod(symbol,timeframe,time);
   ::ResetLastError();
//--- Если не удалось получить запрашиваемые данные по времени и записать данные бара в MqlRates-массив
//--- выводим сообщение об ошибке, создаём и заполняем структуру нулями и записываем её в массив rates_array
   if(::CopyRates(symbol,timeframe,time,1,rates_array)<1)
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(1)-> ",source,symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(time),": ",
         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};
      err.time=time;
      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)," ",::TimeToString(time),": ",
         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]);
  }
//+------------------------------------------------------------------+

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

Второй параметрический конструктор теперь тоже оперирует временем бара вместо его индекса:

//+------------------------------------------------------------------+
//| Конструктор 2                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,rates.time);
   ::ResetLastError();
//--- Если произошла ошибка установки времени в структуру времени, выводим сообщение об ошибке,
//--- создаём и заполняем структуру нулями, устанавливаем свойства бара из этой структуры и выходим
   if(!::TimeToStruct(rates.time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print
        (
         DFUN,"(2) ",symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(rates.time),": ",
         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};
      err.time=rates.time;
      this.SetProperties(err);
      return;
     }
//--- Устанавливаем свойства бара
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+

В публичной секции класса, в блоке методов для упрощённого доступа к свойствам объекта-бара переименуем метод Period() в Timeframe() и удалим метод Index(), возвращающий это (уже убранное) свойство бара:

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта-бара              |
//+------------------------------------------------------------------+
//--- Возвращает (1) тип, (2) период, (3) спред, (4) тиковый, (5) биржевой объём,
//--- (6) время начала периода бара, (7) год, (8) месяц, к которому относится бар
//--- (9) номер недели от начала года, (10) номер недели от начала месяца
//--- (11) день, (12) час, (13) минута
   ENUM_BAR_BODY_TYPE TypeBody(void)                                    const { return (ENUM_BAR_BODY_TYPE)this.GetProperty(BAR_PROP_TYPE);  }
   ENUM_TIMEFRAMES   Timeframe(void)                                    const { return (ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD);   }
   int               Spread(void)                                       const { return (int)this.GetProperty(BAR_PROP_SPREAD);               }
   long              VolumeTick(void)                                   const { return this.GetProperty(BAR_PROP_VOLUME_TICK);               }
   long              VolumeReal(void)                                   const { return this.GetProperty(BAR_PROP_VOLUME_REAL);               }
   datetime          Time(void)                                         const { return (datetime)this.GetProperty(BAR_PROP_TIME);            }
   long              Year(void)                                         const { return this.GetProperty(BAR_PROP_TIME_YEAR);                 }
   long              Month(void)                                        const { return this.GetProperty(BAR_PROP_TIME_MONTH);                }
   long              DayOfWeek(void)                                    const { return this.GetProperty(BAR_PROP_TIME_DAY_OF_WEEK);          }
   long              DayOfYear(void)                                    const { return this.GetProperty(BAR_PROP_TIME_DAY_OF_YEAR);          }
   long              Day(void)                                          const { return this.GetProperty(BAR_PROP_TIME_DAY);                  }
   long              Hour(void)                                         const { return this.GetProperty(BAR_PROP_TIME_HOUR);                 }
   long              Minute(void)                                       const { return this.GetProperty(BAR_PROP_TIME_MINUTE);               }
   long              Index(void)                                        const { return this.GetProperty(BAR_PROP_INDEX);                     }

Теперь метод Index() будет возвращать не существующее свойство объекта-бара, а рассчитанное значение по времени бара:

//--- Возвращает символ бара
   string            Symbol(void)                                       const { return this.GetProperty(BAR_PROP_SYMBOL);                    }
//--- Возвращает индекс бара на указанном таймфрейме, в который попадает время бара
   int               Index(const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT)  const
                       { return ::iBarShift(this.Symbol(),(timeframe>PERIOD_CURRENT ? timeframe : this.Timeframe()),this.Time());            }  
//+------------------------------------------------------------------+

Метод возвращает индекс бара текущей таймсерии для таймфрейма, указанного во входном параметре метода, рассчитанный функцией iBarShift().

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

//+------------------------------------------------------------------+
//| Возвращает краткое наименование объекта-бара                     |
//+------------------------------------------------------------------+
string CBar::Header(void)
  {
   return
     (
      CMessage::Text(MSG_LIB_TEXT_BAR)+" \""+this.GetProperty(BAR_PROP_SYMBOL)+"\" "+
      TimeframeDescription((ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD))+"["+(string)this.Index()+"]"
     );
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает описание целочисленного свойства бара                 |
//+------------------------------------------------------------------+
string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER property)
  {
   return
     (
      property==BAR_PROP_INDEX               ?  CMessage::Text(MSG_LIB_TEXT_BAR_INDEX)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_TYPE                ?  CMessage::Text(MSG_ORD_TYPE)+

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

//+------------------------------------------------------------------+
//| Возвращает описание целочисленного свойства бара                 |
//+------------------------------------------------------------------+
string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER property)
  {
   return
     (
      property==BAR_PROP_TIME                ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+::TimeToString(this.GetProperty(property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
         )  :
      property==BAR_PROP_TYPE                ?  CMessage::Text(MSG_ORD_TYPE)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.BodyTypeDescription()
         )  :
      property==BAR_PROP_PERIOD              ?  CMessage::Text(MSG_LIB_TEXT_BAR_PERIOD)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.m_period_description
         )  :
      property==BAR_PROP_SPREAD              ?  CMessage::Text(MSG_LIB_TEXT_BAR_SPREAD)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_VOLUME_TICK         ?  CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_TICK)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_VOLUME_REAL         ?  CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_REAL)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_TIME_YEAR           ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_YEAR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.Year()
         )  :
      property==BAR_PROP_TIME_MONTH          ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MONTH)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+MonthDescription((int)this.Month())
         )  :
      property==BAR_PROP_TIME_DAY_OF_YEAR    ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_YEAR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.DayOfYear(),3,'0')
         )  :
      property==BAR_PROP_TIME_DAY_OF_WEEK    ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_WEEK)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+DayOfWeekDescription((ENUM_DAY_OF_WEEK)this.DayOfWeek())
         )  :
      property==BAR_PROP_TIME_DAY            ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Day(),2,'0')
         )  :
      property==BAR_PROP_TIME_HOUR           ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_HOUR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Hour(),2,'0')
         )  :
      property==BAR_PROP_TIME_MINUTE         ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MINUTE)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Minute(),2,'0')
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

На этом изменения класса объекта-бар завершены.

Если внимательно поглядеть на списки классов стандартной библиотеки, то увидим по адресу MQL5\Include\Indicators\ два файла: Series.mqh и TimeSeries.mqh.
У нас в библиотеке также есть в наличии одноимённые файлы классов. Это неправильно. Переименуем наши два класса — припишем к их названию и названию их файлов аббревиатуру DE (от DoEasy) и исправим везде их название, где встречается обращение к этим файлам и классам. Эти изменения коснулись трёх файлов: Series.mqh (теперь переименован в SeriesDE.mqh и класс CSeriesDE), TimeSeries.mqh (теперь переименован в TimeSeriesDE.mqh и класс CTimeSeriesDE) и TimeSeriesCollection.mqh (использует оба переименованных класса). Рассмотрим все эти файлы и их классы по порядку.

Файл Series.mqh теперь сохранён под новый именем \MQL5\Include\DoEasy\Objects\Series\SeriesDE.mqh, и имя класса стало соответствующим:

//+------------------------------------------------------------------+
//| Класс "Таймсерия"                                                |
//+------------------------------------------------------------------+
class CSeriesDE : public CBaseObj
  {
private:

Соответственно, и метод, возвращающий объект этого класса, теперь имеет новый тип класса:

public:
//--- Возвращает (1) себя, (2) список-таймсерию
   CSeriesDE        *GetObject(void)                                    { return &this;         }

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

//--- Возвращает объект-бар по (1) реальному индексу в списке, (2) по индексу как в таймсерии, (3) бар по времени, (4) реальный размер списка
   CBar             *GetBarByListIndex(const uint index);
   CBar             *GetBar(const uint index);
   CBar             *GetBar(const datetime time);
   int               DataTotal(void)                                       const { return this.m_list_series.Total();               }

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

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар по времени в таймсерии                     |
//+------------------------------------------------------------------+
CBar *CSeriesDE::GetBar(const datetime time)
  {
   CBar *obj=new CBar(this.m_symbol,this.m_timeframe,time,DFUN_ERR_LINE); 
   if(obj==NULL)
      return NULL;
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   int index=this.m_list_series.Search(obj);
   delete obj;
   CBar *bar=this.m_list_series.At(index);
   return bar;
  }
//+------------------------------------------------------------------+

В метод передаётся время, по которому нужно найти и вернуть соответствующий объект-бар.
Создаём временный объект-бар для текущей таймсерии со свойством времени, равным переданному в метод.
Устанавливаем флаг сортировки списка объектов-баров по времени
и ищем в списке объект-бар со свойством времени, равным переданному в метод.
В результате поиска нам будет возвращён индекс бара в списке, если таковой будет найден, либо -1 в случае его отсутствия.
Удаляем временный объект-бар и получаем нужный бар из списка по полученному индексу. Если индекс будет меньше ноля, то метод At() класса CArrayObj вернёт NULL.
Возвращаем из метода либо объект-бар, если объект был найден по времени, либо NULL
.

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар по индексу как в таймсерии                 |
//+------------------------------------------------------------------+
CBar *CSeriesDE::GetBar(const uint index)
  {
   datetime time=::iTime(this.m_symbol,this.m_timeframe,index);
   if(time==0)
      return NULL;
   return this.GetBar(time);
  }
//+------------------------------------------------------------------+

В метод передаётся индекс искомого бара.
Функцией iTime() получаем время бара по индексу
и возвращаем результат работы метода GetBar(), рассмотренного нами выше, возвращающего объект-бар по полученному времени.

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

//--- Возвращает (1) Open, (2) High, (3) Low, (4) Close, (5) время, (6) тиковый объём, (7) реальный объём, (8) спред бара по индексу
   double            Open(const uint index,const bool from_series=true);
   double            High(const uint index,const bool from_series=true);
   double            Low(const uint index,const bool from_series=true);
   double            Close(const uint index,const bool from_series=true);
   datetime          Time(const uint index,const bool from_series=true);
   long              TickVolume(const uint index,const bool from_series=true);
   long              RealVolume(const uint index,const bool from_series=true);
   int               Spread(const uint index,const bool from_series=true);

//--- Возвращает (1) Open, (2) High, (3) Low, (4) Close, (5) время, (6) тиковый объём, (7) реальный объём, (8) спред бара по индексу
   double            Open(const datetime time);
   double            High(const datetime time);
   double            Low(const datetime time);
   double            Close(const datetime time);
   datetime          Time(const datetime time);
   long              TickVolume(const datetime time);
   long              RealVolume(const datetime time);
   int               Spread(const datetime time);

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

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

//--- (1) Создаёт, (2) обновляет список-таймсерию
   int               Create(const uint required=0);
   void              Refresh(SDataCalculate &data_calculate);
//--- Копирует в массив указанное double-свойство таймсерии
//--- Независимо от направления индексации массива, копирование производится как в массив-таймсерию
   bool              CopyToBufferAsSeries(const ENUM_BAR_PROP_DOUBLE property,double &array[],const double empty=EMPTY_VALUE);

//--- Создаёт и отправляет событие "Новый бар" на график управляющей программы
   void              SendEvent(void);

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

Рассмотрим его реализацию:

//+------------------------------------------------------------------+
//| Копирует в массив указанное double-свойство таймсерии            |
//+------------------------------------------------------------------+
bool CSeriesDE::CopyToBufferAsSeries(const ENUM_BAR_PROP_DOUBLE property,double &array[],const double empty=EMPTY_VALUE)
  {
//--- Получаем количество баров в списке-таймсерии
   int total=this.m_list_series.Total();
   if(total==0)
      return false;
//--- Если в метод передан динамический массив и его размер не равен размеру списка-таймсерии -
//--- устанавливаем новый размер переданного массива равным размеру списка-таймсерии
   if(::ArrayIsDynamic(array) && ::ArraySize(array)!=total)
      if(::ArrayResize(array,total,this.m_required)==WRONG_VALUE)
         return false;
//--- В цикле от самого последнего элемента списка-таймсерии (от текущего бара)
   int n=0;
   for(int i=total-1;i>WRONG_VALUE && !::IsStopped();i--)
     {
      //--- получаем очередной объект-бар по индексу цикла,
      CBar *bar=this.m_list_series.At(i);
      //--- рассчитываем индекс, по которому в переданный массив будет сохранено свойство бара
      n=total-1-i;
      //--- записываем в массив по рассчитанному индексу значение свойства полученного бара
      //--- если бар не получен, или свойство равно нулю - вписываем в массив значение, переданное в метод как "пустое"
      array[n]=(bar==NULL ? empty : (bar.GetProperty(property)>0 && bar.GetProperty(property)<EMPTY_VALUE ? bar.GetProperty(property) : empty));
     }
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Конструктор 1 (таймсерия текущего символа и периода)             |
//+------------------------------------------------------------------+
CSeriesDE::CSeriesDE(void) : m_bars(0),m_amount(0),m_required(0),m_sync(false)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   this.SetSymbolPeriod(NULL,(ENUM_TIMEFRAMES)::Period());
   this.m_period_description=TimeframeDescription(this.m_timeframe);
  }
//+------------------------------------------------------------------+
//| Конструктор 2 (таймсерия указанных символа и периода)            |
//+------------------------------------------------------------------+
CSeriesDE::CSeriesDE(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0) : m_bars(0), m_amount(0),m_required(0),m_sync(false)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   this.SetSymbolPeriod(symbol,timeframe);
   this.m_sync=this.SetRequiredUsedData(required,0);
   this.m_period_description=TimeframeDescription(this.m_timeframe);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Создаёт список-таймсерию                                         |
//+------------------------------------------------------------------+
int CSeriesDE::Create(const uint required=0)
  {
//--- Если ещё не установлена требуемая глубина истории для списка
//--- выводим об этом сообщение и возвращаем ноль,
   if(this.m_amount==0)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_BAR_TEXT_FIRS_SET_AMOUNT_DATA));
      return 0;
     }
//--- иначе, если переданное значение required больше нуля, не равно уже установленному, 
//--- и при этом переданное значение required меньше доступного количества баров,
//--- устанавливаем новое значение требуемой глубины истории для списка
   else if(required>0 && this.m_amount!=required && required<this.m_bars)
     {
      //--- Если не удалось установить новое значение - возвращаем ноль
      if(!this.SetRequiredUsedData(required,0))
         return 0;
     }
//--- Для массива rates[], в который будем получать исторические данные,
//--- установим признак направленности как в таймсерии,
//--- очистим список объектов-баров и установим ему флаг сортировки по индексу бара
   MqlRates rates[];
   ::ArraySetAsSeries(rates,true);
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   ::ResetLastError();
//--- Получим в массив rates[] исторические данные структуры MqlRates, начиная от текущего бара в количестве m_amount,
//--- и если получить данные не удалось - выводим об этом сообщение и возвращаем ноль
   int copied=::CopyRates(this.m_symbol,this.m_timeframe,0,(uint)this.m_amount,rates),err=ERR_SUCCESS;
   if(copied<1)
     {
      err=::GetLastError();
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA)," ",this.m_symbol," ",TimeframeDescription(this.m_timeframe),". ",
                   CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(err),CMessage::Retcode(err));
      return 0;
     }
//--- Исторические данные получены в массив rates[]
//--- В цикле по массиву rates[]
   for(int i=0; i<copied; i++)
     {
      //--- создаём новый обект-бар из данных текущей структуры MqlRates из массива rates[] по индексу цикла
      ::ResetLastError();
      CBar* bar=new CBar(this.m_symbol,this.m_timeframe,rates[i]);
      if(bar==NULL)
        {
         ::Print
           (
            DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_BAR_OBJ)," ",this.Header()," ",::TimeToString(rates[i].time),". ",
            CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError())
           );
         continue;
        }
      //--- Если не удалось добавить новый объект-бар в список
      //--- выводим об этом сообщение в журнал с описанием ошибки
      if(!this.m_list_series.Add(bar))
        {
         err=::GetLastError();
         ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_ADD_TO_LIST)," ",bar.Header()," ",::TimeToString(rates[i].time),". ",
                      CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(err),CMessage::Retcode(err));
        }
     }
//--- Возвращаем размер созданного списка объектов-баров
   return this.m_list_series.Total();
  }
//+------------------------------------------------------------------+

Метод обновления списка и данных таймсерии тоже немного доработан:

//+------------------------------------------------------------------+
//| Обновляет список и данные тайм-серии                             |
//+------------------------------------------------------------------+
void CSeriesDE::Refresh(SDataCalculate &data_calculate)
  {
//--- Если таймсерия не используется - выходим
   if(!this.m_available)
      return;
   MqlRates rates[1];
//--- Устанавливаем флаг сортировки списка баров по времени
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
//--- Если есть новый бар на символе и периоде
   if(this.IsNewBarManual(data_calculate.rates.time))
     {
      //--- создаём новый объект-бар и добавляем его в конец списка
      CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,this.m_new_bar_obj.TimeNewBar(),DFUN_ERR_LINE);
      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);
     }
     
//--- Получаем индекс бара с максимальным временем (нулевой бар) и объект-бар из списка по полученному индексу
   int index=CSelect::FindBarMax(this.GetList(),BAR_PROP_TIME);
   CBar *bar=this.m_list_series.At(index);
   if(bar==NULL)
      return;
//--- если работа в индикаторе, и таймсерия принадлежит текущему символу и таймфрейму,
//--- копируем в структуру цен бара переданные в метод извне параметры цен
   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]);
  }
//+------------------------------------------------------------------+

Здесь тоже сортировка списка теперь установлена по времени, а при создании нового объекта-бара, передаём в конструктор класса время бара из объекта "Новый бар" — ведь мы добавляем новый бар в список только в момент определения факта открытия нового бара, и в объекте "Новый бар" как раз уже содержится время открытия этого бара  — его и передаём в конструктор. И в дополнение передаём в конструктор описание метода, в котором происходит создание нового объекта-бара. При ошибке создания нового объекта-бара, из его конструктора будет выведено сообщение в журнал, в котором будет прописан метод CSeriesDE::Refresh и строка кода, из которой был вызван конструктор класса CBar.
Для того, чтобы получить из списка-таймсерии однозначно самый последний (текущий) бар, мы его найдём по максимальному времени всех объектов-баров, находящихся в списке-таймсерии. Для этого сначала найдём индекс объекта-бара с максимальным временем при помощи метода FindBarMax() класса CSelect, и по полученному индексу возьмём из списка самый последний бар — он и будет текущим. Если же по какой-либо причине мы не получим индекс текущего бара, то значение индекса будет -1, а при получении элемента списка методом At() при отрицательном индексе, нам будет возвращего значение NULL, и если оно таковым является, то просто выходим из метода обновления.

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

//+------------------------------------------------------------------+
//| Возвращает Open бара по времени                                  |
//+------------------------------------------------------------------+
double CSeriesDE::Open(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Open() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает High бара по времени                                  |
//+------------------------------------------------------------------+
double CSeriesDE::High(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.High() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает Low бара по времени                                   |
//+------------------------------------------------------------------+
double CSeriesDE::Low(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Low() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает Close бара по времени                                 |
//+------------------------------------------------------------------+
double CSeriesDE::Close(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Close() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает время бара по времени                                 |
//+------------------------------------------------------------------+
datetime CSeriesDE::Time(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает тиковый объём бара по времени                         |
//+------------------------------------------------------------------+
long CSeriesDE::TickVolume(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.VolumeTick() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает реальный объём бара по времени                        |
//+------------------------------------------------------------------+
long CSeriesDE::RealVolume(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.VolumeReal() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает спред бара по времени                                 |
//+------------------------------------------------------------------+
int CSeriesDE::Spread(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Spread() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+

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

Метод создания и отправки события "Новый бар" на график управляющей программы тоже был доработан с учётом необходимости получения текущего объекта-бара по времени:

//+------------------------------------------------------------------+
//| Создаёт и отправляет событие "Новый бар"                         |
//| на график управляющей программы                                  |
//+------------------------------------------------------------------+
void CSeriesDE::SendEvent(void)
  {
   int index=CSelect::FindBarMax(this.GetList(),BAR_PROP_TIME);
   CBar *bar=this.m_list_series.At(index);
   if(bar==NULL)
      return;
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,bar.Time(),this.Timeframe(),this.Symbol());
  }
//+------------------------------------------------------------------+

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

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

Как уже ранее упоминалось, этот класс CTimeSerirs может конфликтовать с одноимённым классом стандартной библиотеки. Поэтому он у нас уже переименован в CTimeSerirsDE. Соответственно, внутри листинга класса заменены все вхождения строки "CTimeSerirs" на строку "CTimeSerirsDE", а также все вхождения строки "CSerirs" на строку "CSerirsDE", и здесь эти замены мы рассматривать не будем. Лишь в качестве примера:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "SeriesDE.mqh"
#include "..\Ticks\NewTickObj.mqh"
//+------------------------------------------------------------------+
//| Класс "Таймсерии символа"                                        |
//+------------------------------------------------------------------+
class CTimeSeriesDE : public CBaseObjExt
  {
private:

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

//--- Копирует в массив указанное double-свойство указанной таймсерии
//--- Независимо от направления индексации массива, копирование производится как в массив-таймсерию
   bool              CopyToBufferAsSeries(const ENUM_TIMEFRAMES timeframe,
                                          const ENUM_BAR_PROP_DOUBLE property,
                                          double &array[],
                                          const double empty=EMPTY_VALUE);

//--- Сравнивает объекты CTimeSeriesDE между собой (по символу)
   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);
   
//--- Конструкторы
                     CTimeSeriesDE(void){;}
                     CTimeSeriesDE(const string symbol);
  };
//+------------------------------------------------------------------+

Этот метод мы рассматривали выше при доработке класса CSeriesDE. Рассмотрим реализацию метода:

//+------------------------------------------------------------------+
//| Копирует в массив указанное double-свойство указанной таймсерии  |
//+------------------------------------------------------------------+
bool CTimeSeriesDE::CopyToBufferAsSeries(const ENUM_TIMEFRAMES timeframe,
                                         const ENUM_BAR_PROP_DOUBLE property,
                                         double &array[],
                                         const double empty=EMPTY_VALUE)
  {
   CSeriesDE *series=this.GetSeries(timeframe);
   if(series==NULL)
      return false;
   return series.CopyToBufferAsSeries(property,array,empty);
  }
//+------------------------------------------------------------------+

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

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

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

При создании временного объекта для поиска, проверяем введённое значение таймфрейма, и если введено значение CURRENT_PERIOD, то для поиска используем текущий таймфрейм.

В методе обновления указанного списка-таймсерии при добавлении нового события в список событий будем использовать время открытия нового бара из структуры data_calculate как значение параметра lparam:

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

С классом CTimeSeriesDE завершили. Перейдём к классу объекта-коллекции объектов всех таймсерий всех символов CTimeSeriesCollection.

У нас на текущий момент переименованы два класса: CSeriesDE и CTimeSerirsDE. Соответственно, внутри листинга класса CTimeSeriesCollection заменим все вхождения строки "CTimeSerirs" на строку "CTimeSerirsDE", и все вхождения строки "CSerirs" на строку "CSerirsDE".
Здесь эти замены мы рассматривать не будем. Лишь в качестве примера:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Objects\Series\TimeSeriesDE.mqh"
#include "..\Objects\Symbols\Symbol.mqh"
//+------------------------------------------------------------------+
//| Коллекция таймсерий символов                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CBaseObjExt
  {
private:
   CListObj                m_list;                    // Список используемых таймсерий символов
//--- Возвращает индекс таймсерии по имени символа
   int                     IndexTimeSeries(const string symbol);
public:
//--- Возвращает (1) себя, (2) список таймсерий
   CTimeSeriesCollection  *GetObject(void)            { return &this;         }
   CArrayObj              *GetList(void)              { return &this.m_list;  }
//--- Возвращает (1) объект таймсерий указанного символа, (2) объект-таймсерию указанного символа/периода
   CTimeSeriesDE          *GetTimeseries(const string symbol);
   CSeriesDE              *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

//--- Создаёт список-коллекцию таймсерий символов

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

//--- Возвращает объект-бар указанной таймсерии указанного символа указанной позиции (1) по индексу, (2) по времени
//--- объект-бар первой таймсерии, соответствующий времени открытия бара на второй таймсерии (3) по индексу, (4) по времени
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true);
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime bar_time);
   CBar                   *GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                             const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);
   CBar                   *GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime first_bar_time,
                                                             const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);

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

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

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

//--- Выводит в журнал (1) полное, (2) краткое описание коллекции
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
//--- Копирует в массив указанное double-свойство указанной таймсерии указанного символа
//--- Независимо от направления индексации массива, копирование производится как в массив-таймсерию
   bool                    CopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                                const ENUM_BAR_PROP_DOUBLE property,
                                                double &array[],
                                                const double empty=EMPTY_VALUE);
//--- Конструктор
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар указанной таймсерии                        |
//| указанного символа указанной позиции по времени                  |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime bar_time)
  {
   CSeriesDE *series=this.GetSeries(symbol,timeframe);
   if(series==NULL)
      return NULL;
   return series.GetBar(bar_time);
  }
//+------------------------------------------------------------------+

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

Получаем объект-таймсерию с указанными символом и таймфреймом и возвращаем объект-бар, взятый из полученной таймсерии по времени бара.
Если бар получить не удалось — возвращается NULL.

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар первой таймсерии по индексу,               |
//| соответствующий времени открытия бара на второй таймсерии        |
//+------------------------------------------------------------------+F
CBar *CTimeSeriesCollection::GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                               const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
  {
   CBar *bar_first=this.GetBar(symbol_first,timeframe_first,index);
   if(bar_first==NULL)
      return NULL;
   CBar *bar_second=this.GetBar(symbol_second,timeframe_second,bar_first.Time());
   return bar_second;
  }
//+------------------------------------------------------------------+

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

Получаем первый объект-бар из таймсерии первого символа-периода по указанному индексу,
получаем и возвращаем второй объект-бар второго символа-периодапо времени первого полученного бара
.

Метод позволяет получить позицию бара, указанную по индексу, на указанном первом символе-периоде графика, которая совпадает по времени открытия с позицией бара на втором указанном символе-периоде графика.
Что это даёт? Можно, как пример, быстро отметить на графике М15 все бары Н1.
Достаточно в метод передать текущий символ, период графика М15, позицию бара по его индексу на графике (допустим индекс цикла расчёта индикатора), текущий символ и период Н1. И метод вернёт объект-бар с графика текущего символа и периода Н1, время открытия которого включает в себя время открытия первого указанного бара.

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

//+------------------------------------------------------------------+
//| Возвращает объект-бар первой таймсерии по времени,               |
//| соответствующий времени открытия бара на второй таймсерии        |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime first_bar_time,
                                                               const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
  {
   CBar *bar_first=this.GetBar(symbol_first,timeframe_first,first_bar_time);
   if(bar_first==NULL)
      return NULL;
   CBar *bar_second=this.GetBar(symbol_second,timeframe_second,bar_first.Time());
   return bar_second;
  }
//+------------------------------------------------------------------+

Метод идентичен только что рассмотренному методу получения объекта-бара по индексу. Здесь вместо индекса бара в таймсерии указывается время его открытия в указанной первой таймсерии.

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

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

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

Зачем это нужно? Дело в том, что мы обновляем все таймсерии, которые не принадлежат текущему символу-периоду, в таймере библиотеки. А обновление таймсерий, принадлежащих символу, на котором запущена программа, нужно производить из обработчика события Start, NewTick или Calculate программы. Поэтому, чтобы не проверять в таймере событие нового тика для текущего символа (таймсерия текущего символа и так обновляется по тику), мы сравниваем символ таймсерии на совпадение его с текущим символом и проверяем событие таймсерии "новый тик" только если таймсерия принадлежит не текущему символу.

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

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

Здесь каждая строка логики метода прописана в комментариях к коду, и поэтому повторяться не будем — надеюсь тут всё понятно.

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

//+------------------------------------------------------------------+
//| Копирует в массив указанное double-свойство                      |
//| указанной таймсерии указанного символа                           |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::CopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                                 const ENUM_BAR_PROP_DOUBLE property,
                                                 double &array[],
                                                 const double empty=EMPTY_VALUE)
  {
   CSeriesDE *series=this.GetSeries(symbol,timeframe);
   if(series==NULL)
      return false;
   return series.CopyToBufferAsSeries(property,array,empty);
  }
//+------------------------------------------------------------------+

Работу метода мы рассматривали выше при доработке класса CSeriesDE.
Здесь же мы всего лишь получаем требуемый объект-таймсерию по указанным символу и периоду и возвращаем результат вызова одноимённого метода полученной таймсерии.

С классом-коллекцией таймсерий закончили.

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

Откроем файл по адресу \MQL5\Include\DoEasy\Engine.mqh и заменим в нём все вхождения строки "CSerirs" на строку "CSerirsDE" и все вхождения строки "CTimeSerirs" на строку "CTimeSerirsDE".

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

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
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;                         // Объект "Пауза"
   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;                       // Тип программы
   string               m_name;                          // Имя программы

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

//+------------------------------------------------------------------+
//| 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_name=::MQLInfoString(MQL_PROGRAM_NAME);
   
...

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

//--- Возвращает объект-бар указанной таймсерии указанного символа указанной позиции (1) по индексу, (2) по времени
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
                          { return this.m_time_series.GetBar(symbol,timeframe,index,from_series);                 }
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time)
                          { return this.m_time_series.GetBar(symbol,timeframe,time);                              }
//--- Возвращает объект-бар первой таймсерии, соответствующий времени открытия бара на второй таймсерии (1) по индексу, (2) по времени
   CBar                *SeriesGetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                                const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
                          { return this.m_time_series.GetBarSeriesFirstFromSeriesSecond(symbol_first,timeframe_first,index,symbol_second,timeframe_second); }
   
   CBar                *SeriesGetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime time,
                                                                const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
                          { return this.m_time_series.GetBarSeriesFirstFromSeriesSecond(symbol_first,timeframe_first,time,symbol_second,timeframe_second); }

//--- Возвращает флаг открытия нового бара указанной таймсерии указанного символа
   bool                 SeriesIsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0)
                          { return this.m_time_series.IsNewBar(symbol,timeframe,time);                            }

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

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

//--- Возвращает (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) 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 datetime time);
   double               SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   double               SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   double               SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   datetime             SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   long                 SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   long                 SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   int                  SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   
//--- Копирует в массив указанное double-свойство указанной таймсерии указанного символа
//--- Независимо от направления индексации массива, копирование производится как в массив-таймсерию
   bool                 SeriesCopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const ENUM_BAR_PROP_DOUBLE property,
                                                   double &array[],const double empty=EMPTY_VALUE)
                          { return this.m_time_series.CopyToBufferAsSeries(symbol,timeframe,property,array,empty);}

...

//--- Возвращает имя программы
   string               Name(void)                                const { return this.m_name;                                 }

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

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

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

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

В обработчике события NewTick текущего символа впишем обновление всех таймсерий текущего символа:

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Синхронизирует данные таймсерий с сервером                       |
//+------------------------------------------------------------------+
bool CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0)
  {
//--- Если данные таймсерий не посчитаны - пробуем пересоздать таймсерии
//--- Получаем указатель на пустую таймсерию
   CSeriesDE *series=this.SeriesGetSeriesEmpty();
//--- Если есть пустая таймсерия
   if(series!=NULL)
     {
      //--- Выводим комментарий на график с данными пустой таймсерии и пробуем синхронизировать таймсерию с данными на сервере
      ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC));
      ::ChartRedraw(::ChartID());
      //--- если данные синхронизированы
      if(series.SyncData(required,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();
            return true;
           }
        }
      //--- Данные ещё не синхронизированы или таймсерию пересоздать не удалось
      return false;
     }
//--- Нет пустых таймсерий - всё синхронизировано, стираем все комментарии
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+

С классом CEngine на текущий момент завершили.

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

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

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

Как сделаем... У нас может быть использована 21 таймсерия — по количеству стандартных доступных периодов графика. В настройках будет стандартный для библиотеки выбор используемых таймфреймов, а на график выведем кнопки, соответствующие выбранным в настройках используемым таймфреймам. Чтобы не городить слишком много в настройках индикатора, и соответственно, кода для обслуживания буферов индикатора, просто привяжем буферы индикатора к каждому из имеющихся в терминале периодов графика при помощи массива структур.
Включать/отключать видимость линии буфера на графике и его данных в окне данных индикатора будем включением/отключением соответствующей кнопки. Для каждого таймфрейма у нас будет назначено два буфера — один рисуемый, второй — расчётный. В расчётном буфере можно будет хранить промежуточные данные соответствующей ему таймсерии. Но в данном исполнении расчётный буфер использоваться не будет. А чтобы не прописывать все 42 буфера (21 рисуемый и 21 расчётный), мы создадим структуру, в которой будут храниться параметры для каждого из таймфреймов:

  • Массив, назначаемый рисуемым индикаторным буфером
  • Массив, назначаемый расчётным индикаторным буфером
  • Идентификатор буфера (таймфрейм таймсерии, данные которой будет выводить буфер)
  • Индекс индикаторного буфера, связанного с массивом рисуемого буфера
  • Индекс индикаторного буфера, связанного с массивом расчётного буфера
  • Флаг использования буфера в индикаторе (нажата/не нажата кнопка)
  • Флаг отображения буфера в индикаторе до включения/выключения отображения буфера кнопкой на графике

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

Пропишем все параметры каждого буфера индикатора (можно было задавать программно, но так быстрее):

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart40.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- properties
#property indicator_chart_window
#property indicator_buffers 43
#property indicator_plots   21
//--- plot M1
#property indicator_label1  " M1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGray
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot M2
#property indicator_label2  " M2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGray
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- plot M3
#property indicator_label3  " M3"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrGray
#property indicator_style3  STYLE_SOLID
#property indicator_width3  1
//--- plot M4
#property indicator_label4  " M4"
#property indicator_type4  DRAW_LINE
#property indicator_color4  clrGray
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1
//--- plot M5
#property indicator_label5  " M5"
#property indicator_type5   DRAW_LINE
#property indicator_color5  clrGray
#property indicator_style5  STYLE_SOLID
#property indicator_width5  1
//--- plot M6
#property indicator_label6  " M6"
#property indicator_type6   DRAW_LINE
#property indicator_color6  clrGray
#property indicator_style6  STYLE_SOLID
#property indicator_width6  1
//--- plot M10
#property indicator_label7  " M10"
#property indicator_type7   DRAW_LINE
#property indicator_color7  clrGray
#property indicator_style7  STYLE_SOLID
#property indicator_width7  1
//--- plot M12
#property indicator_label8  " M12"
#property indicator_type8   DRAW_LINE
#property indicator_color8  clrGray
#property indicator_style8  STYLE_SOLID
#property indicator_width8  1
//--- plot M15
#property indicator_label9  " M15"
#property indicator_type9   DRAW_LINE
#property indicator_color9  clrGray
#property indicator_style9  STYLE_SOLID
#property indicator_width9  1
//--- plot M20
#property indicator_label10 " M20"
#property indicator_type10  DRAW_LINE
#property indicator_color10 clrGray
#property indicator_style10 STYLE_SOLID
#property indicator_width10 1
//--- plot M30
#property indicator_label11 " M30"
#property indicator_type11  DRAW_LINE
#property indicator_color11 clrGray
#property indicator_style11 STYLE_SOLID
#property indicator_width11 1
//--- plot H1
#property indicator_label12 " H1"
#property indicator_type12  DRAW_LINE
#property indicator_color12 clrGray
#property indicator_style12 STYLE_SOLID
#property indicator_width12 1
//--- plot H2
#property indicator_label13 " H2"
#property indicator_type13  DRAW_LINE
#property indicator_color13 clrGray
#property indicator_style13 STYLE_SOLID
#property indicator_width13 1
//--- plot H3
#property indicator_label14 " H3"
#property indicator_type14  DRAW_LINE
#property indicator_color14 clrGray
#property indicator_style14 STYLE_SOLID
#property indicator_width14 1
//--- plot H4
#property indicator_label15 " H4"
#property indicator_type15  DRAW_LINE
#property indicator_color15 clrGray
#property indicator_style15 STYLE_SOLID
#property indicator_width15 1
//--- plot H6
#property indicator_label16 " H6"
#property indicator_type16  DRAW_LINE
#property indicator_color16 clrGray
#property indicator_style16 STYLE_SOLID
#property indicator_width16 1
//--- plot H8
#property indicator_label17 " H8"
#property indicator_type17  DRAW_LINE
#property indicator_color17 clrGray
#property indicator_style17 STYLE_SOLID
#property indicator_width17 1
//--- plot H12
#property indicator_label18 " H12"
#property indicator_type18  DRAW_LINE
#property indicator_color18 clrGray
#property indicator_style18 STYLE_SOLID
#property indicator_width18 1
//--- plot D1
#property indicator_label19 " D1"
#property indicator_type19  DRAW_LINE
#property indicator_color19 clrGray
#property indicator_style19 STYLE_SOLID
#property indicator_width19 1
//--- plot W1
#property indicator_label20 " W1"
#property indicator_type20  DRAW_LINE
#property indicator_color20 clrGray
#property indicator_style20 STYLE_SOLID
#property indicator_width20 1
//--- plot MN1
#property indicator_label21 " MN1"
#property indicator_type21  DRAW_LINE
#property indicator_color21 clrGray
#property indicator_style21 STYLE_SOLID
#property indicator_width21 1

//--- classes

Как видим, у нас общее количество буферов задано равным 43, тогда как рисуемых буферов 21. Так как к каждому из рисуемых буферов мы договорились добавить по одному расчётному, то 21+21=42. Откуда один лишний? А он нам нужен будет для хранения данных о времени из массива time[] OnCalculate(). Так как в некоторых функциях нам нужно будет время бара по индексу, а массив time[] существует только в области видимости обработчика OnCalculate(), то самым простым решением иметь данные времени для каждого бара текущего таймфрейма — это сохранить массив time[] в одном из расчётных буферов индикатора. Вот для этого мы и задали на один буфер больше.

В индикаторе у нас будет возможность отображать четыре цены бара: Open, High, Low и Close. У объекта-бара вещественных свойств больше:

  • Цена открытия бара (Open)
  • Наивысшая цена за период бара (High)
  • Наименьшая цена за период бара (Low)
  • Цена закрытия бара (Close)
  • Размер свечи
  • Размер тела свечи
  • Верх тела свечи
  • Низ тела свечи
  • Размер верхней тени свечи
  • Размер нижней тени свечи

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

//--- classes

//--- enums
enum ENUM_BAR_PRICE
  {
   BAR_PRICE_OPEN    =  BAR_PROP_OPEN,    // Bar Open
   BAR_PRICE_HIGH    =  BAR_PROP_HIGH,    // Bar High
   BAR_PRICE_LOW     =  BAR_PROP_LOW,     // Bar Low
   BAR_PRICE_CLOSE   =  BAR_PROP_CLOSE,   // Bar Close
  };
//--- defines
#define PERIODS_TOTAL   (21)              // Общее количество доступных периодов графика
//--- structures

Теперь создадим структуру данных одного рисуемого и одного расчётного буферов, назначаемых одной таймсерии (периоду графика):

//--- structures
struct SDataBuffer
  {
private:
   int               m_buff_id;           // Идентификатор буфера (таймфрейм)
   int               m_buff_data_index;   // Индекс индикаторного буфера, связанного с массивом Data[]
   int               m_buff_tmp_index;    // Индекс индикаторного буфера, связанного с массивом Temp[]
   bool              m_used;              // Флаг использования буфера в индикаторе
   bool              m_show_data;         // Флаг отображения буфера на графике до включения/выключения его отображения
public:
   double            Data[];              // Массив, назначаемый индикаторным буфером как INDICATOR_DATA
   double            Temp[];              // Массив, назначаемый индикаторным буфером как INDICATOR_CALCULATIONS
//--- Устанавливает индексы рисуемому и расчётному буферам, привязанным к таймфрейму
   void              SetIndex(const int index)
                       {
                        this.m_buff_data_index=index;
                        this.m_buff_tmp_index=index+PERIODS_TOTAL;
                       }
//--- Методы установки и возврата значений приватных членов структуры
   void              SetID(const int id)              { this.m_buff_id=id;             }
   void              SetUsed(const bool flag)         { this.m_used=flag;              }
   void              SetShowData(const bool flag)     { this.m_show_data=flag;         }
   int               IndexDataBuffer(void)      const { return this.m_buff_data_index; }
   int               IndexTempBuffer(void)      const { return this.m_buff_tmp_index;  }
   int               ID(void)                   const { return this.m_buff_id;         }
   bool              IsUsed(void)               const { return this.m_used;            }
   bool              GetShowDataFlag(void)      const { return this.m_show_data;       }
   void              Print(void);
  };
//--- Вывод данных структуры в журнал
void SDataBuffer::Print(void)
  {
   ::Print
     (
      "Buffer[",this.IndexDataBuffer(),"], ID: ",(string)this.ID(),
      " (",TimeframeDescription((ENUM_TIMEFRAMES)this.ID()),
      "), temp buffer index: ",(string)this.IndexTempBuffer(),
      ", used: ",this.IsUsed()
     );
  }
//--- input variables

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

Пропишем входные параметры индикатора:

//--- 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   ENUM_BAR_PRICE       InpBarPrice       =  BAR_PRICE_OPEN;                  // Applied bar price
sinput   bool                 InpShowBarTimes   =  false;                           // Show bar time comments
sinput   uint                 InpControlBar     =  1;                               // Control bar
sinput   uint                 InpButtShiftX     =  0;    // Buttons X shift 
sinput   uint                 InpButtShiftY     =  10;   // Buttons Y shift 
sinput   bool                 InpUseSounds      =  true; // Use sounds
//--- indicator buffers

Здесь всё стандартно — как и во всех тестовых советниках и индикаторах, которые мы делаем для кажой статьи. Так как тестировать сегодня будем работу только с текущим символом, то закомментируем модификаторы sinput в настройках символа, указывающие на то, что переменная является входным параметром индикатора (sinput-модификатор указывает на запрет оптимизации параметров этой переменной). Таким образом, эти параметры невозможно будет выбирать в настройках ввиду их отсутствия там, и переменной InpModeUsedSymbols будет присвоено значение SYMBOLS_MODE_CURRENT — работа только с текущим символом.
Переменная InpShowBarTimes позволяет включить/отключить отображение комментариев на графике — отображение соответствия бара на текущем периоде графика бару с таким временем на графиках тестируемых таймсерий. А переменная InpControlBar служит для указания номера бара, значение которого можно будет контролировать в комментариях на графике.

И наконец, пропишем буферы индикатора и глобальные переменные:

//--- indicator buffers
SDataBuffer    Buffers[PERIODS_TOTAL];          // Массив структур данных буферов индикатора, привязанных к таймсериям
double         BufferTime[];                    // Расчётный буфер для хранения и передачи данных из массива time[]
//--- 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()
  {
//--- Установка глобальных переменных индикатора
   prefix=engine.Name()+"_";
   testing=engine.IsTester();
   ZeroMemory(rates_data);
   
//--- Инициализация библиотеки DoEasy
   OnInitDoEasy();

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

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

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

//--- indicator buffers mapping

   //--- В цикле по общему количеству доступных таймфреймов
   for(int i=0;i<PERIODS_TOTAL;i++)
     {
      //--- получаем очередной таймфрейм
      ENUM_TIMEFRAMES timeframe=TimeframeByEnumIndex(uchar(i+1));
      //--- Связываем рисуемый индикаторный буфер по индексу буфера, равному индексу цикла с массивом Data[] структуры
      SetIndexBuffer(i,Buffers[i].Data);
      //--- задаём "пустое значение" для буфера Data[], 
      //--- устанавливаем имя графической серии, отображаемое в окне данных для буфера Data[]
      //--- устанавливаем направление индексации рисуемого буфера Data[] как в таймсерии
      PlotIndexSetDouble(i,PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetString(i,PLOT_LABEL,"Buffer "+TimeframeDescription(timeframe));
      ArraySetAsSeries(Buffers[i].Data,true);
      //--- Настройка рисуемого буфера в соответствии с состоянием кнопки
      bool state=false;
      //--- Задаём имя кнопки, соответствующей буферу с индексом цикла и его таймфрейму
      string name=prefix+"BUTT_"+TimeframeDescription(timeframe);
      //--- Если не в тестере, и есть на графике кнопка с заданным именем
      if(!engine.IsTester() && ObjectFind(ChartID(),name)==0)
        {
         //--- задаём имя глобальной переменной терминала для хранения состояния кнопки
         string name_gv=(string)ChartID()+"_"+name;
         //--- если нет глобальной переменной с таким именем - создаём её с состоянием false,
         if(!GlobalVariableCheck(name_gv))
            GlobalVariableSet(name_gv,false);
         //--- получаем из глобальной переменной терминала состояние кнопки
         state=GlobalVariableGet(name_gv);
        }
      //--- Задаём значения всем полям структуры
      Buffers[i].SetID(timeframe);
      Buffers[i].SetIndex(i);
      Buffers[i].SetUsed(state);
      Buffers[i].SetShowData(state);
      //--- Устанавливаем состояние кнопки
      ButtonState(name,state);
      //--- В зависимости от состояния кнопки указываем отображать или нет данные буфера в окне данных
      PlotIndexSetInteger(i,PLOT_SHOW_DATA,state);
      //--- Связываем расчётный индикаторный буфер по индексу буфера из IndexTempBuffer() с массивом Temp[] структуры
      SetIndexBuffer(Buffers[i].IndexTempBuffer(),Buffers[i].Temp,INDICATOR_CALCULATIONS);
      //--- устанавливаем направление индексации расчётного буфера Temp[] как в таймсерии
      ArraySetAsSeries(Buffers[i].Temp,true);
     }
   //--- Связываем расчётный индикаторный буфер по индексу буфера PERIODS_TOTAL*2 с массивом BufferTime[] индикатора
   SetIndexBuffer(PERIODS_TOTAL*2,BufferTime,INDICATOR_CALCULATIONS);
   //--- устанавливаем направление индексации расчётного буфера BufferTime[] как в таймсерии
   ArraySetAsSeries(BufferTime,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

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

Функции для работы с кнопками:

//+------------------------------------------------------------------+
//| Создаёт панель кнопок                                            |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int total=ArraySize(array_used_periods);
   uint w=30,h=20,x=InpButtShiftX+1, y=InpButtShiftY+h+1;
   //--- В цикле по количеству используемых таймфреймов
   for(int i=0;i<total;i++)
     {
      //--- создаём имя очередной кнопки
      string butt_name=prefix+"BUTT_"+array_used_periods[i];
      //--- создаём очередную кнопку со смещением на ((ширина кнопки + 1) * индекс цикла)
      if(!ButtonCreate(butt_name,x+(w+1)*i,y,w,h,array_used_periods[i],clrGray))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),array_used_periods[i]);
         return false;
        }
     }   
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+
//| Создаёт кнопку                                                   |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Устанавливает значение глобальной переменной терминала           |
//+------------------------------------------------------------------+
bool SetGlobalVariable(const string gv_name,const double value)
  {
//--- Если длина имени переменной больше 63 символов - возвращаем false
   if(StringLen(gv_name)>63)
      return false;
   return(GlobalVariableSet(gv_name,value)>0);
  }
//+------------------------------------------------------------------+
//| Возвращает состояние кнопки                                      |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Возвращает состояние кнопки по наименованию таймфрейма           |
//+------------------------------------------------------------------+
bool ButtonState(const ENUM_TIMEFRAMES timeframe)
  {
   string name=prefix+"BUTT_"+TimeframeDescription(timeframe);
   return ButtonState(name);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки                                   |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
   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));
//--- Создаём имя кнопки для глобальной переменной терминала
   string name_gv=(string)ChartID()+"_"+prefix+button;
//--- Получаем состояние кнопки (нажата/отжата), и если не в тестере, то
//--- записываем состояние в глобальную переменную кнопки (1 или 0)
   bool state=ButtonState(button_name);
   if(!engine.IsTester())
      SetGlobalVariable(name_gv,state);
//--- Получаем таймфрейм из строкового идентификатора кнопки и
//--- индекс рисуемого буфера по таймфрейму
   ENUM_TIMEFRAMES timeframe=TimeframeByDescription(StringSubstr(button,5));
   int buffer_index=IndexBuffer(timeframe);
//--- Устанавливаем цвет кнопки в зависимости от её состояния, 
//--- в структуру буфера записываем его состояние в зависимости от состояния кнопки (используется/не используется)
//--- инициализируем буфер, соответствующий таймфрейму кнопки по индексу буфера, полученному ранее
   ButtonState(button_name,state);
   Buffers[buffer_index].SetUsed(state);
   if(Buffers[buffer_index].GetShowDataFlag()!=state)
     {
      InitBuffer(buffer_index);
      BufferFill(buffer_index);
      Buffers[buffer_index].SetShowData(state);
     }

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

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

Рассмотрим обработчик 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(time,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Установка массивов буферов как таймсерий

//--- Проверка на минимальное количество баров для расчёта
   if(rates_total<2 || Point()==0) return 0;
   
//--- Вывод контрольной информации по времени открытия баров
   if(InpShowBarTimes)
     {
      string txt="";
      int total=ArraySize(array_used_periods);
      //--- В цикле по количеству используемых таймфреймов
      for(int i=0;i<total;i++)
        {
         //--- получаем очередной таймфрейм, индекс буфера и объект-таймсерию по таймфрейму
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_used_periods[i]);
         int buffer_index=IndexBuffer(timeframe);
         CSeriesDE *series=engine.SeriesGetSeries(NULL,timeframe);
         //--- Если таймсерию получить не удалось, или буфер не используется (отжата кнопка) - идём к следующему
         if(series==NULL || !Buffers[buffer_index].IsUsed())
            continue;
         //--- Получаем контрольный бар из списка-таймсерии
         CBar *bar=series.GetBar(InpControlBar);
         if(bar==NULL)
            continue;
         //--- Собираем данные для текста комментария
         string t1=TimeframeDescription((ENUM_TIMEFRAMES)Period());
         string t2=TimeframeDescription(bar.Timeframe());
         string t3=(string)InpControlBar;
         string t4=TimeToString(bar.Time());
         string t5=(string)bar.Index((ENUM_TIMEFRAMES)Period());
         //--- Составляем текст комментария в зависимости от языка терминала
         string tn=TextByLanguage
           (
            "Бар на "+t1+", соответствующий бару "+t2+"["+t3+"] со временеи открытия "+t4+", расположен на баре "+t5,
            "The bar on "+t1+", corresponding to the "+t2+"["+t3+"] bar since the opening time of "+t4+", is located on bar "+t5
           );
         txt+=tn+"\n";
        }
      //--- Выводим комментарий на график
      Comment(txt);
     }

//--- Проверка и расчёт количества просчитываемых баров
   int limit=rates_total-prev_calculated;

//--- Пересчёт всей истории
   if(limit>1)
     {
      limit=rates_total-1;
      InitBuffersAll();
     }
//--- Подготовка данных

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

Если в настройках параметр "Show bar time comments" (переменная InpShowBarTimes) установлен в true, то этот блок кода выведет на график информацию по указанному в переменной InpControlBar ("ControlBar") бару на текущем графике о его соответствии бару на таймфреймах всех используемых таймсерий.

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

Индикатор рассчитывается от значения limit (в нормальных условиях её значение 1 (новый бар) или ноль — рассчитывается текущий бар) до нуля.
В главном цикле расчёта индикатора заполняем расчётный буфер времени из массива time[] (буфер времени нам нужен в других функциях индикатора, где необходимо получить время по индексу, но там массив time[] не доступен), и вызываем функцию расчёта одного бара для всех используемых буферов индикатора.

Функции инициализации буферов индикатора:

//+------------------------------------------------------------------+
//| Инициализация таймсерии и соответствующих ей буферов по индексу  |
//+------------------------------------------------------------------+
bool InitBuffer(const int buffer_index)
  {
//--- Если передан неверный индекс - уходим
   if(buffer_index==WRONG_VALUE)
      return false;
//--- Инициализируем переменные стилем рисования как "Не отрисовывается" и запрещаем отображение в окне данных
   int draw_type=DRAW_NONE;
   bool show_data=false;
//--- Если буфер используется (кнопка нажата)
//--- Устанавливаем переменным стиль рисования как "Линия" и разрешаем отображение в окне данных
   if(Buffers[buffer_index].IsUsed())
     {
      draw_type=DRAW_LINE;
      show_data=true;
     }
//--- Устанавливаем буферу по его индексу заданные стиль рисования и отображение в окне данных
   PlotIndexSetInteger(Buffers[buffer_index].IndexDataBuffer(),PLOT_DRAW_TYPE,draw_type);
   PlotIndexSetInteger(Buffers[buffer_index].IndexDataBuffer(),PLOT_SHOW_DATA,show_data);
//--- Инициализируем расчётный буфер нулём, а рисуемый "пустым" значением 
   ArrayInitialize(Buffers[buffer_index].Temp,0);
   ArrayInitialize(Buffers[buffer_index].Data,EMPTY_VALUE);
   return true;
  }
//+------------------------------------------------------------------+
//|Инициализация таймсерии и соответствующих ей буферов по таймфрейму|
//+------------------------------------------------------------------+
bool InitBuffer(const ENUM_TIMEFRAMES timeframe)
  {
   return InitBuffer(IndexBuffer(timeframe));
  }
//+------------------------------------------------------------------+
//| Инициализация всех таймсерий и соответствующих им буферов        |
//+------------------------------------------------------------------+
void InitBuffersAll(void)
  {
//--- В цикле по общему количеству периодов графика инициализируем очередной буфер
   for(int i=0;i<PERIODS_TOTAL;i++)
      if(!InitBuffer(i))
         continue;
  }
//+------------------------------------------------------------------+

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Заполняет весь буфер историческими данными                       |
//+------------------------------------------------------------------+
void BufferFill(const int buffer_index)
  {
//--- Если передан неверный индекс - уходим
   if(buffer_index==WRONG_VALUE)
      return;
//--- Если буфер не используется (кнопка отжата) - уходим
   if(!Buffers[buffer_index].IsUsed())
      return;
//--- Получаем объект-таймсерию по таймфрейму буфера
   CSeriesDE *series=engine.SeriesGetSeries(NULL,(ENUM_TIMEFRAMES)Buffers[buffer_index].ID());
   if(series==NULL)
      return;
//--- Если буфер принадлежит текущему графику - копируем в буфер данные баров из таймсерии
   if(Buffers[buffer_index].ID()==Period())
      series.CopyToBufferAsSeries((ENUM_BAR_PROP_DOUBLE)InpBarPrice,Buffers[buffer_index].Data,EMPTY_VALUE);
//--- Иначе - в цикле по количеству баров текущего графика рассчитываем каждый очередной бар таймсерии и записываем его в буфер
   else 
      for(int i=rates_data.rates_total-1;i>WRONG_VALUE && !IsStopped();i--)
         CalculateSeries((ENUM_BAR_PROP_DOUBLE)InpBarPrice,i,(datetime)BufferTime[i]);
  }
//+------------------------------------------------------------------+

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

Хочу отметить, что этот тестовый индикатор разрабатывался на MQL5. На MQL4 он тоже работает без каких-либо правок, но не совсем корректно — текущий период графика при нажатии соответствующей кнопки не отображается, но начинает отображаться при активации ещё одного таймфрейма. Если задать в настройках нестандартные для MetaTrader 4 периоды графиков, то индикатор будет всегда ожидать их синхронизацию.
Такжене корректно отображаются данные в окне данных терминала — выводятся абсолютно все буферы индикатора — даже расчётные, что естественно, ведь не все MQL5-функции работают в MQL4, и нужно заменять их MQL4-аналогами.
Мало того, индикатор и в MetaTrader 5 не всегда корректно обрабатывает изменения в исторических данных, что естественно — это всего-лишь тестовая версия для проверки работы в мультипериодном режиме, и все выявленные недочёты будем потихоньку исправлять в последующих статьях. И когда всё будет доведено до правильной работы в MetaTrader 5 — только тогда будем корректировать работу библиотеки в индикаторах на MetaTrader 4.

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


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

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



Что дальше

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

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

К содержанию

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

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



Прикрепленные файлы |
MQL5.zip (3726.57 KB)
MQL4.zip (3726.57 KB)
Непрерывная скользящая оптимизация (Часть 6): Логическая часть автооптимизатора и его структура Непрерывная скользящая оптимизация (Часть 6): Логическая часть автооптимизатора и его структура

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

Язык MQL как средство разметки графического интерфейса MQL-программ. Часть 1 Язык MQL как средство разметки графического интерфейса MQL-программ. Часть 1

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

Язык MQL как средство разметки графического интерфейса MQL-программ. Часть 2 Язык MQL как средство разметки графического интерфейса MQL-программ. Часть 2

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

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

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