Работа с ценами в библиотеке DoEasy (Часть 60): Список-серия тиковых данных символа

Artyom Trishkin | 13 января, 2021

Содержание


Концепция

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

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

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


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

В первую очередь добавим новые сообщения библиотеки в файле \MQL5\Include\DoEasy\Data.mqh. Добавим индексы новых сообщений:

//--- CTick
   MSG_TICK_TEXT_TICK,                                // Тик
   MSG_TICK_TIME_MSC,                                 // Время последнего обновления цен в миллисекундах
   MSG_TICK_TIME,                                     // Время последнего обновления цен
   MSG_TICK_VOLUME,                                   // Объем для текущей цены Last
   MSG_TICK_FLAGS,                                    // Флаги
   MSG_TICK_VOLUME_REAL,                              // Объем для текущей цены Last c повышенной точностью
   MSG_TICK_SPREAD,                                   // Спред
   MSG_LIB_TEXT_TICK_CHANGED_DATA,                    // Изменённые данные на тике:
   MSG_LIB_TEXT_TICK_FLAG_BID,                        // Изменение цены Bid
   MSG_LIB_TEXT_TICK_FLAG_ASK,                        // Изменение цены Ask
   MSG_LIB_TEXT_TICK_FLAG_LAST,                       // Изменение цены последней сделки
   MSG_LIB_TEXT_TICK_FLAG_VOLUME,                     // Изменение объема
   
//--- CTickSeries
   MSG_TICKSERIES_TEXT_TICKSERIES,                    // Тиковая серия
   MSG_TICKSERIES_ERR_GET_TICK_DATA,                  // Ошибка получения тиковых данных
   MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ,        // Не удалось создать объект тиковых данных
   MSG_TICKSERIES_FAILED_ADD_TO_LIST,                 // Не удалось добавить объект тиковых данных в список
   MSG_TICKSERIES_TEXT_IS_NOT_USE,                    // Тиковая серия не используется. Нужно установить флаг использования при помощи SetAvailable()
   MSG_TICKSERIES_REQUIRED_HISTORY_DAYS,              // Запрошенное количество дней
   
  };
//+------------------------------------------------------------------+

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

//--- CTick
   {"Тик","Tick"},
   {"Время последнего обновления цен в миллисекундах","Last price update time in milliseconds"},
   {"Время последнего обновления цен","Last price update time"},
   {"Объем для текущей цены Last","Volume for the current Last price"},
   {"Флаги","Flags"},
   {"Объем для текущей цены Last c повышенной точностью","Volume for the current \"Last\" price with increased accuracy"},
   {"Спред","Spread"},
   {"Изменённые данные на тике:","Changed data on a tick:"},
   {"Изменение цены Bid","Bid price change"},
   {"Изменение цены Ask","Ask price change"},
   {"Изменение цены последней сделки","Last price change"},
   {"Изменение объема","Volume change"},
   
//--- TickSeries
   {"Тиковая серия","Tickseries"},
   {"Ошибка получения тиковых данных","Error getting tick data"},
   {"Не удалось создать объект тиковых данных","Failed to create tick data object"},
   {"Не удалось добавить объект тиковых данных в список","Failed to add tick data object to the list"},
   {"Тиковая серия не используется. Нужно установить флаг использования при помощи SetAvailable()","Tick series are not used. Need to set the use flag using SetAvailable()"},
   {"Запрошенное количество дней: ","Number of days requested: "},
   
  };
//+---------------------------------------------------------------------+

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

//--- Параметры таймсерий
#define SERIES_DEFAULT_BARS_COUNT      (1000)                     // Требуемое количество данных таймсерий по умолчанию
#define PAUSE_FOR_SYNC_ATTEMPTS        (16)                       // Количество миллисекунд паузы между попытками синхронизации
#define ATTEMPTS_FOR_SYNC              (5)                        // Количество попыток получения факта синхронизации с сервером
//--- Параметры тиковых серий
#define TICKSERIES_DEFAULT_DAYS_COUNT  (1)                        // Требуемое количество дней для тиковых данных в сериях по умолчанию

При повторном разборе кода класса таймсерии в файле \MQL5\Include\DoEasy\Objects\Series\SeriesDE.mqh, я заметил допущенную мною ошибку — если созданный объект-бар по какой-либо причине не был добавлен в список, то он не удалялся. Это может привести к утечке памяти. Поэтому в методе создания списка-таймсерии добавим удаление объекта в случае, если он не был добавлен в список:

//+------------------------------------------------------------------+
//| Создаёт список-таймсерию                                         |
//+------------------------------------------------------------------+
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));
         delete bar;
        }
     }
//--- Возвращаем размер созданного списка объектов-баров
   return this.m_list_series.Total();
  }
//+------------------------------------------------------------------+

Для возможности поиска, сортировки и выбора объектов-тиков из создаваемого списка, нам необходимо добавить в файл
\MQL5\Include\DoEasy\Services\Select.mqh методы для работы с этим списком и с тиковыми данными.

Подключим к классу файл класса объекта тиковых данных:

//+------------------------------------------------------------------+
//|                                                       Select.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\Objects\Orders\Order.mqh"
#include "..\Objects\Events\Event.mqh"
#include "..\Objects\Accounts\Account.mqh"
#include "..\Objects\Symbols\Symbol.mqh"
#include "..\Objects\PendRequest\PendRequest.mqh"
#include "..\Objects\Series\SeriesDE.mqh"
#include "..\Objects\Indicators\Buffer.mqh"
#include "..\Objects\Indicators\IndicatorDE.mqh"
#include "..\Objects\Indicators\DataInd.mqh"
#include "..\Objects\Ticks\DataTick.mqh"
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Методы работы с данными индикаторов                              |
//+------------------------------------------------------------------+
   //--- Возвращает список данных индикаторов, где одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
   static CArrayObj *ByIndicatorDataProperty(CArrayObj *list_source,ENUM_IND_DATA_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByIndicatorDataProperty(CArrayObj *list_source,ENUM_IND_DATA_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByIndicatorDataProperty(CArrayObj *list_source,ENUM_IND_DATA_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode);
   //--- Возвращает индекс данных индикаторов в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства данных
   static int        FindIndDataMax(CArrayObj *list_source,ENUM_IND_DATA_PROP_INTEGER property);
   static int        FindIndDataMax(CArrayObj *list_source,ENUM_IND_DATA_PROP_DOUBLE property);
   static int        FindIndDataMax(CArrayObj *list_source,ENUM_IND_DATA_PROP_STRING property);
   //--- Возвращает индекс данных индикаторов в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства данных
   static int        FindIndDataMin(CArrayObj *list_source,ENUM_IND_DATA_PROP_INTEGER property);
   static int        FindIndDataMin(CArrayObj *list_source,ENUM_IND_DATA_PROP_DOUBLE property);
   static int        FindIndDataMin(CArrayObj *list_source,ENUM_IND_DATA_PROP_STRING property);
//+------------------------------------------------------------------+
//| Методы работы с тиковыми данными                                 |
//+------------------------------------------------------------------+
   //--- Возвращает список тиковых данных, где одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
   static CArrayObj *ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode);
   static CArrayObj *ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode);
   //--- Возвращает индекс тиковых данных в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства данных
   static int        FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER property);
   static int        FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE property);
   static int        FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_STRING property);
   //--- Возвращает индекс тиковых данных в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства данных
   static int        FindTickDataMin(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER property);
   static int        FindTickDataMin(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE property);
   static int        FindTickDataMin(CArrayObj *list_source,ENUM_TICK_PROP_STRING property);
//---
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Методы работы со списками тиковых данных                         |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Возвращает список тиковых данных, где одно из целочисленных      |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   int total=list_source.Total();
   for(int i=0; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      if(!obj.SupportProperty(property)) continue;
      long obj_prop=obj.GetProperty(property);
      if(CompareValues(obj_prop,value,mode)) list.Add(obj);
     }
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список тиковых данных, где одно из вещественных       |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   for(int i=0; i<list_source.Total(); i++)
     {
      CDataTick *obj=list_source.At(i);
      if(!obj.SupportProperty(property)) continue;
      double obj_prop=obj.GetProperty(property);
      if(CompareValues(obj_prop,value,mode)) list.Add(obj);
     }
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список тиковых данных, где одно из строковых          |
//| свойств удовлетворяет заданному критерию                         |
//+------------------------------------------------------------------+
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode)
  {
   if(list_source==NULL) return NULL;
   CArrayObj *list=new CArrayObj();
   if(list==NULL) return NULL;
   list.FreeMode(false);
   ListStorage.Add(list);
   for(int i=0; i<list_source.Total(); i++)
     {
      CDataTick *obj=list_source.At(i);
      if(!obj.SupportProperty(property)) continue;
      string obj_prop=obj.GetProperty(property);
      if(CompareValues(obj_prop,value,mode)) list.Add(obj);
     }
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с максимальным значением целочисленного свойства                 |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   CDataTick *max_obj=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      long obj1_prop=obj.GetProperty(property);
      max_obj=list_source.At(index);
      long obj2_prop=max_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с максимальным значением вещественного свойства                  |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   CDataTick *max_obj=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      double obj1_prop=obj.GetProperty(property);
      max_obj=list_source.At(index);
      double obj2_prop=max_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с максимальным значением строкового свойства                     |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_STRING property)
  {
   if(list_source==NULL) return WRONG_VALUE;
   int index=0;
   CDataTick *max_obj=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      string obj1_prop=obj.GetProperty(property);
      max_obj=list_source.At(index);
      string obj2_prop=max_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с минимальным значением целочисленного свойства                  |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_INTEGER property)
  {
   int index=0;
   CDataTick *min_obj=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      long obj1_prop=obj.GetProperty(property);
      min_obj=list_source.At(index);
      long obj2_prop=min_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с минимальным значением вещественного свойства                   |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_DOUBLE property)
  {
   int index=0;
   CDataTick *min_obj=NULL;
   int total=list_source.Total();
   if(total== 0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      double obj1_prop=obj.GetProperty(property);
      min_obj=list_source.At(index);
      double obj2_prop=min_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+
//| Возвращает индекс тиковых данных в списке                        |
//| с минимальным значением строкового свойства                      |
//+------------------------------------------------------------------+
int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_STRING property)
  {
   int index=0;
   CDataTick *min_obj=NULL;
   int total=list_source.Total();
   if(total==0) return WRONG_VALUE;
   for(int i=1; i<total; i++)
     {
      CDataTick *obj=list_source.At(i);
      string obj1_prop=obj.GetProperty(property);
      min_obj=list_source.At(index);
      string obj2_prop=min_obj.GetProperty(property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   return index;
  }
//+------------------------------------------------------------------+

Работу подобных методов мы неоднократно обсуждали, и подробнее с ними можно ознакомиться в статье №3.


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

Теперь напишем класс объекта-списка тиковых данных. Список, как и все остальные в библиотеке, будет представлять собой массив, созданный на основе класса динамического массива указателей на экземпляры класса CObject и его наследников.

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

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

//+------------------------------------------------------------------+
//|                                                   TickSeries.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\..\Services\Select.mqh"
#include "NewTickObj.mqh"
#include "DataTick.mqh"
//+------------------------------------------------------------------+
//| Класс "Серия тиковых данных"                                     |
//+------------------------------------------------------------------+
class CTickSeries : public CBaseObj
  {
private:
   string            m_symbol;                                          // Символ
   uint              m_amount;                                          // Количество используемых данных тиковой серии
   uint              m_required;                                        // Требуемое количество дней для данных тиковой серии
   CArrayObj         m_list_ticks;                                      // Список тиковых данных
   CNewTickObj       m_new_tick_obj;                                    // Объект "Новый тик"

public:
//--- Возвращает (1) себя, (2) список тиковых данных, (3) объект "Новый тик" тиковой серии
   CTickSeries      *GetObject(void)                                    { return &this;               }
   CArrayObj        *GetList(void)                                      { return &m_list_ticks;       }
   CNewTickObj      *GetNewTickObj(void)                                { return &this.m_new_tick_obj;}

//--- Возвращает список объектов-тиков по выбранному (1) double, (2) integer и (3) string свойству, удовлетворяющему сравниваемому условию
   CArrayObj        *GetList(ENUM_TICK_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL){ return CSelect::ByTickDataProperty(this.GetList(),property,value,mode); }
   CArrayObj        *GetList(ENUM_TICK_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByTickDataProperty(this.GetList(),property,value,mode); }
   CArrayObj        *GetList(ENUM_TICK_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL){ return CSelect::ByTickDataProperty(this.GetList(),property,value,mode); }
//--- Возвращает объект тиковых данных по (1) индексу в списке, (2) по времени, (4) размер списка
   CDataTick        *GetTickByListIndex(const uint index);
   CDataTick        *GetTick(const datetime time); 
   CDataTick        *GetTick(const ulong time_msc); 
   int               DataTotal(void)                              const { return this.m_list_ticks.Total();       }

//--- Метод сравнения для поиска по символу одинаковых объектов тиковых серий
   virtual int       Compare(const CObject *node,const int mode=0) const 
                       {   
                        const CTickSeries *compared_obj=node;
                        return(this.Symbol()==compared_obj.Symbol() ? 0 : this.Symbol()>compared_obj.Symbol() ? 1 : -1);
                       } 
//--- Возвращает наименование тиковой серии
   string            Header(void);
//--- Выводит в журнал (1) описание тиковой серии, (2) краткое описание тиковой серии
   void              Print(void);
   void              PrintShort(void);

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

//+------------------------------------------------------------------+ 
//| Методы работы с объектами и доступа к их свойствам               |
//+------------------------------------------------------------------+
//--- Устанавливает (1) символ, (2) количество используемых данных тиковой серии
   void              SetSymbol(const string symbol);                     
   void              SetRequiredUsedBars(const uint required=0);

//--- Возвращает (1) символ, (2) количество используемых, (3) запрошенных тиковых данных, (4) флаг нового тика
   string            Symbol(void)                                 const { return this.m_symbol;                   }
   ulong             AvailableUsedData(void)                      const { return this.m_amount;                   }
   ulong             RequiredUsedDays(void)                       const { return this.m_required;                 }
   bool              IsNewTick(void)                                    { return this.m_new_tick_obj.IsNewTick(); }

//--- Возвращает (1) Bid, (2) Ask, (3) Last, (4) объём с повышенной точностью,
//--- (5) спред, (6) объём, (7) флаги тиков, (8) время, (9) время в миллисекундах по индексу в списке
   double            Bid(const uint index);
   double            Ask(const uint index);
   double            Last(const uint index);
   double            VolumeReal(const uint index);
   double            Spread(const uint index);
   long              Volume(const uint index);
   uint              Flags(const uint index);
   datetime          Time(const uint index);
   long              TimeMSC(const uint index);
   
//--- Возвращает (1) Bid, (2) Ask, (3) Last, (4) объём с повышенной точностью,
//--- (5) спред, (6) объём, (7) флаги тиков по времени тика в миллисекундах
   double            Bid(const ulong time_msc);
   double            Ask(const ulong time_msc);
   double            Last(const ulong time_msc);
   double            VolumeReal(const ulong time_msc);
   double            Spread(const ulong time_msc);
   long              Volume(const ulong time_msc);
   uint              Flags(const ulong time_msc);
   
//--- Возвращает (1) Bid, (2) Ask, (3) Last, (4) объём с повышенной точностью,
//--- (5) спред, (6) объём, (7) флаги тиков по времени тика
   double            Bid(const datetime time);
   double            Ask(const datetime time);
   double            Last(const datetime time);
   double            VolumeReal(const datetime time);
   double            Spread(const datetime time);
   long              Volume(const datetime time);
   uint              Flags(const datetime time);

//--- (1) Создаёт, (2) обновляет список-таймсерию
   int               Create(const uint required=0);
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

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

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

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

Рассмотрим методы объекта.

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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CTickSeries::CTickSeries(const string symbol,const uint required=0) : m_symbol(symbol)
  {
   this.m_list_ticks.Clear();
   this.m_list_ticks.Sort(SORT_BY_TICK_TIME_MSC);
   this.SetRequiredUsedDays(required);
  }
//+------------------------------------------------------------------+

Метод установки символа списка тиков:

//+------------------------------------------------------------------+
//| Устанавливает символ                                             |
//+------------------------------------------------------------------+
void CTickSeries::SetSymbol(const string symbol)
  {
   if(this.m_symbol==symbol)
      return;
   this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);
  }
//+------------------------------------------------------------------+

Если переданный в метод символ уже задан — уходим из метода, иначе — если передано значение NULL или пустая строка, то устанавливаем текущий символ, в противном случае — устанавливаем переданный в метод символ.

Метод для установки количества дней, за которые нужны тиковые данные:

//+------------------------------------------------------------------+
//| Устанавливает количество дней требуемых тиковых данных           |
//+------------------------------------------------------------------+
void CTickSeries::SetRequiredUsedDays(const uint required=0)
  {
   this.m_required=(required<1 ? TICKSERIES_DEFAULT_DAYS_COUNT : required);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает объект-тик по его индексу в списке                    |
//+------------------------------------------------------------------+
CDataTick *CTickSeries::GetTickByListIndex(const uint index)
  {
   return this.m_list_ticks.At(index);
  }
//+------------------------------------------------------------------+

Просто возвращаем объект, находящийся в списке по переданному в метод индексу. Следует учитывать, что метод At() вернёт NULL в случае, если указан неверный индекс либо если список пуст.

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

//+------------------------------------------------------------------+
//| Возвращает последний объект-тик по его времени                   |
//+------------------------------------------------------------------+
CDataTick *CTickSeries::GetTick(const datetime time)
  {
   CArrayObj *list=GetList(TICK_PROP_TIME,time,EQUAL);
   if(list==NULL) return NULL;
   return list.At(list.Total()-1);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает последний объект-тик по его времени в миллисекундах   |
//+------------------------------------------------------------------+
CDataTick *CTickSeries::GetTick(const ulong time_msc)
  {
   CArrayObj *list=GetList(TICK_PROP_TIME_MSC,time_msc,EQUAL);
   if(list==NULL) return NULL;
   return list.At(list.Total()-1);
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Возвращает Bid тика по индексу в списке                          |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Получаем из списка объект по переданному в метод индексу и возвращаем либо Bid из полученного объекта, либо 0 (если объект получить не удалось)

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

//+------------------------------------------------------------------+
//| Возвращает Bid тика по времени в миллисекундах                   |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Получаем из списка последний объект по переданному в метод времени в миллисекундах (метод получения рассматривали выше) и возвращаем либо Bid из полученного объекта, либо 0 (если объект получить не удалось)

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

//+------------------------------------------------------------------+
//| Возвращает Bid тика по времени                                   |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Получаем из списка последний объект по переданному в метод времени (метод получения рассматривали выше) и возвращаем либо Bid из полученного объекта, либо 0 (если объект получить не удалось)

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

//+------------------------------------------------------------------+
//| Возвращает Ask тика по индексу в списке                          |
//+------------------------------------------------------------------+

double CTickSeries::Ask(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает Ask тика по времени в миллисекундах                   |
//+------------------------------------------------------------------+
double CTickSeries::Ask(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает Ask тика по времени                                   |
//+------------------------------------------------------------------+
double CTickSeries::Ask(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает Last тика по индексу в списке                         |
//+------------------------------------------------------------------+
double CTickSeries::Last(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает Last тика по времени в миллисекундах                  |
//+------------------------------------------------------------------+
double CTickSeries::Last(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает Last тика по времени                                  |
//+------------------------------------------------------------------+
double CTickSeries::Last(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает объём с повышенной точностью тика по индексу в списке |
//+------------------------------------------------------------------+
double CTickSeries::VolumeReal(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+------------------------------------------------------------------+
//|Возвращает объём с повышенной точностью тика по времени в миллисек|
//+------------------------------------------------------------------+
double CTickSeries::VolumeReal(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает объём с повышенной точностью тика по времени          |
//+------------------------------------------------------------------+
double CTickSeries::VolumeReal(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает спред тика по индексу в списке                        |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает спред тика по времени в миллисекундах                 |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает спред тика по времени                                 |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает объём тика по индексу в списке                        |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает объём тика по времени в миллисекундах                 |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает объём тика по времени                                 |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает флаги тика по индексу в списке                        |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает флаги тика по времени в миллисекундах                 |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+
//| Возвращает флаги тика по времени                                 |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+

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

Метод, возвращающий строковое наименование тиковой серии:

//+------------------------------------------------------------------+
//| Возвращает наименование тиковой серии                            |
//+------------------------------------------------------------------+
string CTickSeries::Header(void)
  {
   return CMessage::Text(MSG_TICKSERIES_TEXT_TICKSERIES)+" \""+this.m_symbol+"\"";
  }
//+------------------------------------------------------------------+

Возвращает строку в формате

Tickseries "symbol name"

Например:

Тиковая серия "EURUSD"

Метод, выводящий в журнал полное описание тиковой серии:

//+------------------------------------------------------------------+
//| Выводит в журнал описание тиковой серии                          |
//+------------------------------------------------------------------+
void CTickSeries::Print(void)
  {
   string txt=
     (
      CMessage::Text(MSG_TICKSERIES_REQUIRED_HISTORY_DAYS)+(string)this.RequiredUsedDays()+", "+
      CMessage::Text(MSG_LIB_TEXT_TS_AMOUNT_HISTORY_DATA)+(string)this.DataTotal()
     );
   ::Print(this.Header(),": ",txt);
  }
//+------------------------------------------------------------------+

В методе создаётся строка из описания количества дней, за которые хранятся тики в серии, и сколько реально хранится тиков в списке.
Затем распечатывается заголовок тиковой серии и созданная строка. Как пример:

Тиковая серия "EURUSD": Запрошенное количество дней: 1, Создано исторических данных: 256714

Метод, выводящий в журнал краткое описание тиковой серии:

//+------------------------------------------------------------------+
//| Выводит в журнал краткое описание тиковой серии                  |
//+------------------------------------------------------------------+
void CTickSeries::PrintShort(void)
  {
   ::Print(this.Header());
  }
//+------------------------------------------------------------------+

Просто распечатывает в журнал строковое наименование тиковой серии.

Метод для создания тиковой серии:

//+------------------------------------------------------------------+
//| Создаёт список-серию тиковых данных                              |
//+------------------------------------------------------------------+
int CTickSeries::Create(const uint required=0)
  {
//--- Если тиковая серия не используется - сообщаем об этом и выходим
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol,": ",CMessage::Text(MSG_TICKSERIES_TEXT_IS_NOT_USE));
      return false;
     }
//--- Объявим массив ticks[], в который будем получать исторические тиковые данные,
//--- очистим список объектов тиковых данных и установим ему флаг сортировки по времени в миллисекундах
   MqlTick ticks_array[];
   this.m_list_ticks.Clear();
   this.m_list_ticks.Sort(SORT_BY_TICK_TIME_MSC);
   ::ResetLastError();
   int err=ERR_SUCCESS;
//--- Рассчитаем время начала дня в миллисекундах, от которого необходимо скопировать тики
   MqlDateTime date_str={0};
   datetime date=::iTime(m_symbol,PERIOD_D1,this.m_required);
   ::TimeToStruct(date,date_str);
   date_str.hour=date_str.min=date_str.sec=0;
   date=::StructToTime(date_str);
   long date_from=(long)date*1000;
   if(date_from<1) date_from=1;
//--- Получим в массив tick[] исторические данные структуры MqlTick,
//--- от рассчитанной даты до текущего времени и сохраним полученное количество в m_amount.
//--- Если получить данные не удалось - выводим об этом сообщение и возвращаем ноль
   this.m_amount=::CopyTicksRange(m_symbol,ticks_array,COPY_TICKS_ALL,date_from);
   if(this.m_amount<1)
     {
      err=::GetLastError();
      ::Print(DFUN,CMessage::Text(MSG_TICKSERIES_ERR_GET_TICK_DATA),": ",CMessage::Text(err),CMessage::Retcode(err));
      return 0;
     }
//--- Исторические данные получены в массив ticks[]
//--- В цикле по массиву ticks[]
   for(int i=0; i<(int)this.m_amount; i++)
     {
      //--- создаём новый объект тиковых данных из данных текущей структуры MqlTick из массива ticks[] по индексу цикла
      ::ResetLastError();
      CDataTick* tick=new CDataTick(this.m_symbol,ticks_array[i]);
      if(tick==NULL)
        {
         ::Print
           (
            DFUN,CMessage::Text(MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ)," ",this.Header()," ",::TimeMSCtoString(ticks_array[i].time_msc),". ",
            CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError())
           );
         continue;
        }
      //--- Если не удалось добавить новый объект тиковых данных в список
      //--- выводим об этом сообщение в журнал с описанием ошибки
      //--- и удаляем вновь созданный объект
      if(!this.m_list_ticks.Add(tick))
        {
         err=::GetLastError();
         ::Print(DFUN,CMessage::Text(MSG_TICKSERIES_FAILED_ADD_TO_LIST)," ",tick.Header()," ",
                      CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(err),CMessage::Retcode(err));
         delete tick;
        }
     }
//--- Возвращаем размер созданного списка объектов-баров
   return this.m_list_ticks.Total();
  }
//+------------------------------------------------------------------+

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

На этом создание списка тиковых данных завершено.


Тестирование создания списка и получения данных

Для тестирования просто создадим список объектов-тиков для текущего символа за текущий день при старте программы. В полученном списке найдём тик с наибольшим значением цены Ask и наименьшим значением цены Bid и выведем данные найденных объектов-тиков в журнал. Для этого возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part60\ под новым именем TestDoEasyPart60.mq5.

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

//|                                             TestDoEasyPart60.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>
#include <DoEasy\Objects\Ticks\TickSeries.mqh>
//--- enums

В области глобальных переменных программы объявим объект-список тиковых данных:

//--- global variables
CEngine        engine;
SDataButt      butt_data[TOTAL_BUTT];
string         prefix;
double         lot;
double         withdrawal=(InpWithdrawal<0.1 ? 0.1 : InpWithdrawal);
ushort         magic_number;
uint           stoploss;
uint           takeprofit;
uint           distance_pending;
uint           distance_stoplimit;
uint           distance_pending_request;
uint           bars_delay_pending_request;
uint           slippage;
bool           trailing_on;
bool           pressed_pending_buy;
bool           pressed_pending_buy_limit;
bool           pressed_pending_buy_stop;
bool           pressed_pending_buy_stoplimit;
bool           pressed_pending_close_buy;
bool           pressed_pending_close_buy2;
bool           pressed_pending_close_buy_by_sell;
bool           pressed_pending_sell;
bool           pressed_pending_sell_limit;
bool           pressed_pending_sell_stop;
bool           pressed_pending_sell_stoplimit;
bool           pressed_pending_close_sell;
bool           pressed_pending_close_sell2;
bool           pressed_pending_close_sell_by_buy;
bool           pressed_pending_delete_all;
bool           pressed_pending_close_all;
bool           pressed_pending_sl;
bool           pressed_pending_tp;
double         trailing_stop;
double         trailing_step;
uint           trailing_start;
uint           stoploss_to_modify;
uint           takeprofit_to_modify;
int            used_symbols_mode;
string         array_used_symbols[];
string         array_used_periods[];
bool           testing;
uchar          group1;
uchar          group2;
double         g_point;
int            g_digits;

//--- Объект "Новый тик"
CNewTickObj    check_tick;
//--- Объект данных тиковой серии текущего символа
CTickSeries    tick_series;
//+------------------------------------------------------------------+

В OnTick() удалим блок кода для работы с объектами тиковых данных — это осталось от прошлой статьи, и здесь мы не будем создавать никаких объектов:

//--- Создаём временный список для хранения объектов "Данные тика",
//--- переменную для получения данных тика и
//--- переменную для подсчёта поступающих тиков
   static int tick_count=0;
   CArrayObj list;
   MqlTick tick_struct;
//--- Проверяем новый тик на текущем символе
   if(check_tick.IsNewTick())
     {
      //--- Если цены получить не удалось - уходим
      if(!SymbolInfoTick(Symbol(),tick_struct))
         return;
      //--- Создаём новый объект тиковых данных
      CDataTick *tick_obj=new CDataTick(Symbol(),tick_struct);
      if(tick_obj==NULL)
         return;
      //--- Увеличиваем счётчик тиков (просто для вывода на экран - другого смысла нету)
      tick_count++;
      //--- Ограничиваем количество тиков в подсчёте ста тысячами (тоже без какого-либо смысла)
      if(tick_count>100000) tick_count=1;
      //--- Выводим в комментарии на графике номер тика и его краткое описание
      Comment("--- №",IntegerToString(tick_count,5,'0'),": ",tick_obj.Header());
      //--- Если это первый тик (следующий за первым запуском советника), выведем его полное описание в журнал
      if(tick_count==1)
         tick_obj.Print();
      //--- Если созданный объект тиковых данных не удалось разместить в списке - удалим его
      if(!list.Add(tick_obj))
         delete tick_obj;
     }

Таким образом, обработчик OnTick() примет такой вид:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Обработка события NewTick в библиотеке
   engine.OnTick(rates_data);

//--- Если работа в тестере
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer(rates_data);   // Работа в таймере
      PressButtonsControl();        // Контроль нажатия кнопок
      engine.EventsHandling();      // Работа с событиями
     }

//--- Если установлен флаг трейлинга
   if(trailing_on)
     {
      TrailingPositions();          // Трейлинг позиций
      TrailingOrders();             // Трейлинг отложенных ордеров
     }
  }
//+------------------------------------------------------------------+

А список мы создадим в обработчике OnInit() внутри функции инициализации библиотеки DoEasy, в блоке кода:

//+------------------------------------------------------------------+
//| Инициализация библиотеки DoEasy                                  |
//+------------------------------------------------------------------+
void OnInitDoEasy()
  {
//--- Проверка на выбор работы с полным списком
   used_symbols_mode=InpModeUsedSymbols;
   if((ENUM_SYMBOLS_MODE)used_symbols_mode==SYMBOLS_MODE_ALL)
     {
      int total=SymbolsTotal(false);
      string ru_n="\nКоличество символов на сервере "+(string)total+".\nМаксимальное количество: "+(string)SYMBOLS_COMMON_TOTAL+" символов.";
      string en_n="\nThe number of symbols on server "+(string)total+".\nMaximal number: "+(string)SYMBOLS_COMMON_TOTAL+" symbols.";
      string caption=TextByLanguage("Внимание!","Attention!");
      string ru="Выбран режим работы с полным списком.\nВ этом режиме первичная подготовка списков коллекций символов и таймсерий может занять длительное время."+ru_n+"\nПродолжить?\n\"Нет\" - работа с текущим символом \""+Symbol()+"\"";
      string en="Full list mode selected.\nIn this mode, the initial preparation of lists of symbol collections and timeseries can take a long time."+en_n+"\nContinue?\n\"No\" - working with the current symbol \""+Symbol()+"\"";
      string message=TextByLanguage(ru,en);
      int flags=(MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2);
      int mb_res=MessageBox(message,caption,flags);
      switch(mb_res)
        {
         case IDNO : 
           used_symbols_mode=SYMBOLS_MODE_CURRENT; 
           break;
         default:
           break;
        }
     }
//--- Установим начало отсчёта счётчика для замера примерного времени инициализации библиотеки
   ulong begin=GetTickCount();
   Print(TextByLanguage("--- Инициализация библиотеки \"DoEasy\" ---","--- Initializing the \"DoEasy\" library ---"));
//--- Заполнение массива используемых символов
   CreateUsedSymbolsArray((ENUM_SYMBOLS_MODE)used_symbols_mode,InpUsedSymbols,array_used_symbols);
//--- Установка типа используемого списка символов в коллекции символов и заполнение списка таймсерий символов
   engine.SetUsedSymbols(array_used_symbols);
//--- Отображение в журнале выбранного режима работы с коллекцией объектов-символов
   string num=
     (
      used_symbols_mode==SYMBOLS_MODE_CURRENT ? ": \""+Symbol()+"\"" : 
      TextByLanguage(". Количество используемых символов: ",". The number of symbols used: ")+(string)engine.GetSymbolsCollectionTotal()
     );
   Print(engine.ModeSymbolsListDescription(),num);
//--- Вывод списка используемых символов сделаем только для MQL5 - в MQL4 нет функции ArrayPrint()
#ifdef __MQL5__
   if(InpModeUsedSymbols!=SYMBOLS_MODE_CURRENT)
     {
      string array_symbols[];
      CArrayObj* list_symbols=engine.GetListAllUsedSymbols();
      for(int i=0;i<list_symbols.Total();i++)
        {
         CSymbol *symbol=list_symbols.At(i);
         if(symbol==NULL)
            continue;
         ArrayResize(array_symbols,ArraySize(array_symbols)+1,SYMBOLS_COMMON_TOTAL);
         array_symbols[ArraySize(array_symbols)-1]=symbol.Name();
        }
      ArrayPrint(array_symbols);
     }
#endif   
//--- Установка используемых таймфреймов
   CreateUsedTimeframesArray(InpModeUsedTFs,InpUsedTFs,array_used_periods);
//--- Отображение выбранного режима работы с коллекцией объектов-таймсерий
   string mode=
     (
      InpModeUsedTFs==TIMEFRAMES_MODE_CURRENT   ? 
         TextByLanguage("Работа только с текущим таймфреймом: ","Work only with the current Period: ")+TimeframeDescription((ENUM_TIMEFRAMES)Period())   :
      InpModeUsedTFs==TIMEFRAMES_MODE_LIST      ? 
         TextByLanguage("Работа с заданным списком таймфреймов:","Work with a predefined list of Periods:")                                              :
      TextByLanguage("Работа с полным списком таймфреймов:","Work with the full list of all Periods:")
     );
   Print(mode);
//--- Вывод списка используемых таймфреймов сделаем только для MQL5 - в MQL4 нет функции ArrayPrint()
#ifdef __MQL5__
   if(InpModeUsedTFs!=TIMEFRAMES_MODE_CURRENT)
      ArrayPrint(array_used_periods);
#endif 
//--- Создание таймсерий всех используемых символов
   engine.SeriesCreateAll(array_used_periods);

//--- Проверка созданных таймсерий - выводим в журнал описания всех созданных таймсерий
//--- (true - только созданные, false - созданные и объявленные)
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания
   //engine.GetTimeSeriesCollection().Print(true);      // Полные описания

//--- Блок кода для проверки создания списка тиков и работы с ним
   Print("");
//--- Так как объект тиковой серии создан с конструктором по умолчанию, то
//--- установим символ, флаг использования и количество дней (1 по умолчанию) для копирования тиков
//--- Создадим тиковую серию и распечатываемые данные в журнал
   tick_series.SetSymbol(Symbol());
   tick_series.SetAvailable(true);
   tick_series.SetRequiredUsedDays();
   tick_series.Create();
   tick_series.Print();
   
   Print("");
//--- Получим и выведем в журнал данные объекта с максимальной ценой Ask в дневном диапазоне цен
   int index_max=CSelect::FindTickDataMax(tick_series.GetList(),TICK_PROP_ASK);
   CDataTick *tick_max=tick_series.GetList().At(index_max);
   if(tick_max!=NULL)
      tick_max.Print();
//--- Получим и выведем в журнал данные объекта с минимальной ценой Bid в дневном диапазоне цен
   int index_min=CSelect::FindTickDataMin(tick_series.GetList(),TICK_PROP_BID);
   CDataTick *tick_min=tick_series.GetList().At(index_min);
   if(tick_min!=NULL)
      tick_min.Print();

//--- Создание тестовых файлов ресурсов
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_01",TextByLanguage("Звук упавшей монетки 1","The sound of a falling coin 1"),sound_array_coin_01);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_02",TextByLanguage("Звук упавших монеток","Sound fallen coins"),sound_array_coin_02);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_03",TextByLanguage("Звук монеток","Sound of coins"),sound_array_coin_03);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_04",TextByLanguage("Звук упавшей монетки 2","The sound of a falling coin 2"),sound_array_coin_04);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_01",TextByLanguage("Звук щелчка по кнопке 1","Click on the button sound 1"),sound_array_click_01);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_02",TextByLanguage("Звук щелчка по кнопке 2","Click on the button sound 1"),sound_array_click_02);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_03",TextByLanguage("Звук щелчка по кнопке 3","Click on the button sound 1"),sound_array_click_03);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_cash_machine_01",TextByLanguage("Звук кассового аппарата","The sound of the cash machine"),sound_array_cash_machine_01);
   engine.CreateFile(FILE_TYPE_BMP,"img_array_spot_green",TextByLanguage("Изображение \"Зелёный светодиод\"","Image \"Green Spot lamp\""),img_array_spot_green);
   engine.CreateFile(FILE_TYPE_BMP,"img_array_spot_red",TextByLanguage("Изображение \"Красный светодиод\"","Image \"Red Spot lamp\""),img_array_spot_red);

//--- Передача в основной класс библиотеки всех имеющихся коллекций
   engine.CollectionOnInit();

//--- Установка магика по умолчанию для всех используемых инструментов
   engine.TradingSetMagic(engine.SetCompositeMagicNumber(magic_number));
//--- Установка синхронной передачи приказов для всех используемых символов
   engine.TradingSetAsyncMode(false);
//--- Установка количества торговых попыток при ошибке
   engine.TradingSetTotalTry(InpTotalAttempts);
//--- Установка корректных типов истечения и заливки ордера всем торговым объектам
   engine.TradingSetCorrectTypeExpiration();
   engine.TradingSetCorrectTypeFilling();

//--- Установка стандартных звуков торговым объектам всех используемых символов
   engine.SetSoundsStandart();
//--- Установка общего флага использования звуков
   engine.SetUseSounds(InpUseSounds);
//--- Установка множителя спреда торговым объектам символов в коллекции символов
   engine.SetSpreadMultiplier(InpSpreadMultiplier);
      
//--- Установка контрольных значений для символов
   //--- Получаем список всех символов коллекции
   CArrayObj *list=engine.GetListAllUsedSymbols();
   if(list!=NULL && list.Total()!=0)
     {
      //--- В цикле по списку устанавливаем нужные значения для отслеживаемых свойств символов
      //--- По умолчанию всем свойствам установлены значения LONG_MAX, что означает "Не отслеживать данное свойство" 
      //--- Включить или выключить (задать величину меньше LONG_MAX или наоборот - установить значение LONG_MAX) можно в любое время в любом месте программы
      /*
      for(int i=0;i<list.Total();i++)
        {
         CSymbol* symbol=list.At(i);
         if(symbol==NULL)
            continue;
         //--- Установка контроля увеличения цены символа на 100 пунктов
         symbol.SetControlBidInc(100000*symbol.Point());
         //--- Установка контроля уменьшения цены символа на 100 пунктов
         symbol.SetControlBidDec(100000*symbol.Point());
         //--- Установка контроля увеличения спреда символа на 40 пунктов
         symbol.SetControlSpreadInc(400);
         //--- Установка контроля уменьшения спреда символа на 40 пунктов
         symbol.SetControlSpreadDec(400);
         //--- Установка контроля размера спреда по значению 40 пунктов
         symbol.SetControlSpreadLevel(400);
        }
      */
     }
//--- Установка контрольных значений для текущего аккаунта
   CAccount* account=engine.GetAccountCurrent();
   if(account!=NULL)
     {
      //--- Установка контроля увеличения значения прибыли на 10
      account.SetControlledValueINC(ACCOUNT_PROP_PROFIT,10.0);
      //--- Установка контроля увеличения значения средств на 15
      account.SetControlledValueINC(ACCOUNT_PROP_EQUITY,15.0);
      //--- Установка контрольного уровня прибыли на 20
      account.SetControlledValueLEVEL(ACCOUNT_PROP_PROFIT,20.0);
     }
//--- Получим конец отсчёта времени инициализации библиотеки и выведем его в журнал
   ulong end=GetTickCount();
   Print(TextByLanguage("Время инициализации библиотеки: ","Library initialization time: "),TimeMSCtoString(end-begin,TIME_MINUTES|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

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

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

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

Счёт 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10426.13 USD, 1:100, Hedge, Демонстрационный счёт MetaTrader 5
--- Инициализация библиотеки "DoEasy" ---
Работа только с текущим символом: "EURUSD"
Работа только с текущим таймфреймом: H4
Таймсерия символа EURUSD: 
- Таймсерия "EURUSD" H4: Запрошено: 1000, Фактически: 1000, Создано: 1000, На сервере: 6336

Тиковая серия "EURUSD": Запрошенное количество дней: 1, Создано исторических данных: 276143

============= Начало списка параметров (Тик "EURUSD" 2021.01.06 14:25:32.156) =============
Время последнего обновления цен в миллисекундах: 2021.01.06 14:25:32.156
Время последнего обновления цен: 2021.01.06 14:25:32
Объем для текущей цены Last: 0
Флаги: 134
Изменённые данные на тике:
 - Изменение цены Ask
 - Изменение цены Bid
------
Цена Bid: 1.23494
Цена Ask: 1.23494
Цена Last: 0.00000
Объем для текущей цены Last c повышенной точностью: 0.00
Спред: 0.00000
------
Символ: "EURUSD"
============= Конец списка параметров (Тик "EURUSD" 2021.01.06 14:25:32.156) =============

============= Начало списка параметров (Тик "EURUSD" 2021.01.07 12:51:40.632) =============
Время последнего обновления цен в миллисекундах: 2021.01.07 12:51:40.632
Время последнего обновления цен: 2021.01.07 12:51:40
Объем для текущей цены Last: 0
Флаги: 134
Изменённые данные на тике:
 - Изменение цены Ask
 - Изменение цены Bid
------
Цена Bid: 1.22452
Цена Ask: 1.22454
Цена Last: 0.00000
Объем для текущей цены Last c повышенной точностью: 0.00
Спред: 0.00002
------
Символ: "EURUSD"
============= Конец списка параметров (Тик "EURUSD" 2021.01.07 12:51:40.632) =============

Время инициализации библиотеки: 00:00:12.828

На инициализацию было затрачено 12.8 секунд — время на подгрузку исторических тиковых данных.

Что дальше

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

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

К содержанию

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

Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа
Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа
Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам
Работа с таймсериями в библиотеке DoEasy (Часть 38): Коллекция таймсерий - реалтайм обновление и доступ к данным из программы
Работа с таймсериями в библиотеке DoEasy (Часть 39): Индикаторы на основе библиотеки - подготовка данных и события таймсерий
Работа с таймсериями в библиотеке DoEasy (Часть 40): Индикаторы на основе библиотеки - реалтайм обновление данных
Работа с таймсериями в библиотеке DoEasy (Часть 41): Пример мультисимвольного мультипериодного индикатора
Работа с таймсериями в библиотеке DoEasy (Часть 42): Класс объекта абстрактного индикаторного буфера
Работа с таймсериями в библиотеке DoEasy (Часть 43): Классы объектов индикаторных буферов
Работа с таймсериями в библиотеке DoEasy (Часть 44): Класс-коллекция объектов индикаторных буферов
Работа с таймсериями в библиотеке DoEasy (Часть 45): Мультипериодные индикаторные буферы
Работа с таймсериями в библиотеке DoEasy (Часть 46): Мультипериодные, мультисимвольные индикаторные буферы
Работа с таймсериями в библиотеке DoEasy (Часть 47): Мультипериодные мультисимвольные стандартные индикаторы
Работа с таймсериями в библиотеке DoEasy (Часть 48): Мультипериодные мультисимвольные индикаторы на одном буфере в подокне
Работа с таймсериями в библиотеке DoEasy (Часть 49): Мультипериодные мультисимвольные многобуферные стандартные индикаторы
Работа с таймсериями в библиотеке DoEasy (Часть 50): Мультипериодные мультисимвольные стандартные индикаторы со смещением
Работа с таймсериями в библиотеке DoEasy (Часть 51): Составные мультипериодные мультисимвольные стандартные индикаторы
Работа с таймсериями в библиотеке DoEasy (Часть 52): Кроссплатформенность мультипериодных мультисимвольных однобуферных стандартных индикаторов
Работа с таймсериями в библиотеке DoEasy (Часть 53): Класс абстрактного базового индикатора
Работа с таймсериями в библиотеке DoEasy (Часть 54): Классы-наследники абстрактного базового индикатора
Работа с таймсериями в библиотеке DoEasy (Часть 55): Класс-коллекция индикаторов
Работа с таймсериями в библиотеке DoEasy (Часть 56): Объект пользовательского индикатора, получение данных от объектов-индикаторов в коллекции
Работа с таймсериями в библиотеке DoEasy (Часть 57): Объект данных буфера индикатора
Работа с таймсериями в библиотеке DoEasy (Часть 58): Таймсерии данных буферов индикаторов
Работа с ценами в библиотеке DoEasy (Часть 59): Объект для хранения данных одного тика