Prices in DoEasy library (part 60): Series list of symbol tick data

Artyom Trishkin | 17 March, 2021

Contents


Concept

In the previous article, I started creating the functionality for working with tick data. In particular, I created the class of tick data object. Here, I will create the list for storing such objects. Such lists will be available for each symbol used in the program. By default, the size of tick data lists for a symbol is to cover the current day. Naturally, it will be possible to set the number of days the program will have a set of tick data for.

Why do we need custom tick data lists if MQL5 allows obtaining them at any time? They allow us to search for the necessary data, as well as compare and receive data quickly and easily. This opportunity is provided by the very concept of building lists in the library and working with them.

I will combine the lists created for each symbol applied by the program into the collection of tick data that makes it more convenient to work with tick data of any symbol and perform any analysis of tick streams of different symbols.


Improving library classes

First of all, let's add new library messages to \MQL5\Include\DoEasy\Data.mqh. Add indices of new messages:

//--- CTick
   MSG_TICK_TEXT_TICK,                                // Tick
   MSG_TICK_TIME_MSC,                                 // Time of the last update of prices in milliseconds
   MSG_TICK_TIME,                                     // Time of the last update of prices
   MSG_TICK_VOLUME,                                   // Volume for the current Last price
   MSG_TICK_FLAGS,                                    // Flags
   MSG_TICK_VOLUME_REAL,                              // Volume for the current Last price with greater accuracy
   MSG_TICK_SPREAD,                                   // Spread
   MSG_LIB_TEXT_TICK_CHANGED_DATA,                    // Changed data on tick:
   MSG_LIB_TEXT_TICK_FLAG_BID,                        // Bid price change
   MSG_LIB_TEXT_TICK_FLAG_ASK,                        // Ask price change
   MSG_LIB_TEXT_TICK_FLAG_LAST,                       // Last deal price change
   MSG_LIB_TEXT_TICK_FLAG_VOLUME,                     // Volume change
   
//--- CTickSeries
   MSG_TICKSERIES_TEXT_TICKSERIES,                    // Tick series
   MSG_TICKSERIES_ERR_GET_TICK_DATA,                  // Failed to get tick data
   MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ,        // Failed to create tick data object
   MSG_TICKSERIES_FAILED_ADD_TO_LIST,                 // Failed to add tick data object to list
   MSG_TICKSERIES_TEXT_IS_NOT_USE,                    // Tick series not used. Set the flag using SetAvailable()
   MSG_TICKSERIES_REQUIRED_HISTORY_DAYS,              // Requested number of days
   
  };
//+------------------------------------------------------------------+

and text messages corresponding to newly added indices:

//--- 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
   {"Тиковая серия","Tick series"},
   {"Ошибка получения тиковых данных","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: "},
   
  };
//+---------------------------------------------------------------------+

The tick data is to be stored for the current day by default. In \MQL5\Include\DoEasy\Defines.mqh, let's introduce the new constant (macro substitution) to set the number of days the library is to store ticks for:

//--- Timeseries parameters
#define SERIES_DEFAULT_BARS_COUNT      (1000)                     // Required default amount of timeseries data
#define PAUSE_FOR_SYNC_ATTEMPTS        (16)                       // Amount of pause milliseconds between synchronization attempts
#define ATTEMPTS_FOR_SYNC              (5)                        // Number of attempts to receive synchronization with the server
//--- Tick series parameters
#define TICKSERIES_DEFAULT_DAYS_COUNT  (1)                        // Required number of days for tick data in default series

While re-checking the timeseries class code in \MQL5\Include\DoEasy\Objects\Series\SeriesDE.mqh, I noticed a mistake I made — if a created bar object is not added to the list for some reason, it is not removed. This may lead to a memory leak. Therefore, let's add an object removal in case it was not added to the list:

//+------------------------------------------------------------------+
//| Create the timeseries list                                       |
//+------------------------------------------------------------------+
int CSeriesDE::Create(const uint required=0)
  {
//--- If the required history depth is not set for the list yet,
//--- display the appropriate message and return zero,
   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;
     }
//--- otherwise, if the passed 'required' value exceeds zero and is not equal to the one already set, 
//--- while being lower than the available bar number,
//--- set the new value of the required history depth for the list
   else if(required>0 && this.m_amount!=required && required<this.m_bars)
     {
      //--- If failed to set a new value, return zero
      if(!this.SetRequiredUsedData(required,0))
         return 0;
     }
//--- For the rates[] array we are to receive historical data to,
//--- set the flag of direction like in the timeseries,
//--- clear the bar object list and set the flag of sorting by bar index
   MqlRates rates[];
   ::ArraySetAsSeries(rates,true);
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   ::ResetLastError();
//--- Get historical data of the MqlRates structure to the rates[] array starting from the current bar in the amount of m_amount,
//--- if failed to get data, display the appropriate message and return zero
   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;
     }
//--- Historical data is received in the rates[] array
//--- In the rates[] array loop,
   for(int i=0; i<copied; i++)
     {
      //--- create a new bar object out of the current MqlRates structure by the loop index
      ::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 failed to add bar object to the list,
      //--- display the appropriate message with the error description in the journal
      //--- and remove the newly created object
      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 the size of the created bar object list
   return this.m_list_series.Total();
  }
//+------------------------------------------------------------------+

To be able to search, sort and select tick objects from the created list, add the methods for working with this list and with tick data to \MQL5\Include\DoEasy\Services\Select.mqh.

Include the file of the tick data object class:

//+------------------------------------------------------------------+
//|                                                       Select.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#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"
//+------------------------------------------------------------------+

At the end of the class body, add declarations of methods of working with tick data:

//+------------------------------------------------------------------+
//| Methods of work with indicator data                              |
//+------------------------------------------------------------------+
   //--- Return the list of indicator data with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion
   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);
   //--- Return the indicator data index in the list with the maximum value of (1) integer, (2) real and (3) string property of data
   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);
   //--- Return the indicator data index in the list with the minimum value of (1) integer, (2) real and (3) string property of data
   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);
//+------------------------------------------------------------------+
//| Methods of working with tick data                                |
//+------------------------------------------------------------------+
   //--- Return the list of tick data with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion
   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);
   //--- Return the tick data index in the list with the maximum value of (1) integer, (2) real and (3) string property of data
   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);
   //--- Return the tick data index in the list with the minimum value of (1) integer, (2) real and (3) string property of data
   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);
//---
  };
//+------------------------------------------------------------------+

Implement declared methods outside the class body:

//+------------------------------------------------------------------+
//| Methods of working with tick data lists                          |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Return the list of tick data with one of integer                 |
//| property meeting the specified criterion                         |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the list of tick data with one of real                    |
//| property meeting the specified criterion                         |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the list of tick data with one of string                  |
//| property meeting the specified criterion                         |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the maximum integer property value                          |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the maximum real property value                             |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the maximum string property value                           |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the minimum integer property value                          |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the minimum real property value                             |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Return the tick data in the list                                 |
//| with the minimum string property value                           |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

I have described the work of such methods multiple times. Find more info on them in part 3.


Tick data series object class

Now let's write the list object class of tick data. Just like everything else in the library, the list is to be an array based on the class of dynamic array of pointers to the instances of CObject class and its descendants.

I will calculate the start of the necessary day depending on the specified number of days the tick history should be stored for. All existing ticks are to be added to the list using CopyTicksRange() from that day. In the next article, I will arrange the real time update of these lists to always have the relevant tick database in the collection.

The structure of the tick data list object class is similar to the symbol timeseries list class. The only difference is that a tick object is used here as a minimum data storage unit instead of a bar object. The class composition is to be standard for the library. So let's consider its body in full and clarify some details and methods afterwards:

//+------------------------------------------------------------------+
//|                                                   TickSeries.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\..\Services\Select.mqh"
#include "NewTickObj.mqh"
#include "DataTick.mqh"
//+------------------------------------------------------------------+
//| "Tick data series" class                                         |
//+------------------------------------------------------------------+
class CTickSeries : public CBaseObj
  {
private:
   string            m_symbol;                                          // Symbol
   uint              m_amount;                                          // Amount of applied tick series data
   uint              m_required;                                        // Required number of days for tick series data
   CArrayObj         m_list_ticks;                                      // List of tick data
   CNewTickObj       m_new_tick_obj;                                    // "New tick" object

public:
//--- Return (1) itself, (2) list of tick data and (3) "New tick" object of the tick series
   CTickSeries      *GetObject(void)                                    { return &this;               }
   CArrayObj        *GetList(void)                                      { return &m_list_ticks;       }
   CNewTickObj      *GetNewTickObj(void)                                { return &this.m_new_tick_obj;}

//--- Return the list of tick objects by selected (1) double, (2) integer and (3) string property fitting a compared condition
   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); }
//--- Return the object of tick data by (1) index in the list, (2) time and (4) list size
   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();       }

//--- The comparison method for searching identical tick series objects by a symbol
   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);
                       } 
//--- Return the tick series name
   string            Header(void);
//--- Display (1) the tick series description and (2) the tick series short description in the journal
   void              Print(void);
   void              PrintShort(void);

//--- Constructors
                     CTickSeries(void){;}
                     CTickSeries(const string symbol,const uint required=0);

//+------------------------------------------------------------------+ 
//| Methods of working with objects and accessing their properties   |
//+------------------------------------------------------------------+
//--- Set (1) a symbol and (2) a number of used tick series data
   void              SetSymbol(const string symbol);                     
   void              SetRequiredUsedBars(const uint required=0);

//--- Return (1) symbol, (2) number of used, (3) requested tick data and (4) new tick flag
   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(); }

//--- Return (1) Bid, (2) Ask, (3) Last, (4) volume with increased accuracy,
//--- (5) spread, (6) volume, (7) tick flags, (8) time, (9) time in milliseconds by index in the list
   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);
   
//--- Return (1) Bid, (2) Ask, (3) Last, (4) volume with increased accuracy,
//--- (5) spread, (6) volume, (7) tick flags by tick time in milliseconds
   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);
   
//--- Return (1) Bid, (2) Ask, (3) Last, (4) volume with increased accuracy,
//--- (5) spread, (6) volume and (7) tick flags by tick time
   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) Create and (2) update the timeseries list
   int               Create(const uint required=0);
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

Previously prepared "New tick" and "Tick data" object class listings are included into the class. We will need the "New tick" object when updating the tick data lists in the next article, while the "Tick data" object is the class of the objects that are to be placed to the list.

All the necessary class member variables are declared in the private section of the class. These variables are to store the values of the object parameters, the object of the object list class (which is the tick list itself) and the "New tick" object that will be required in the next article during the real time list update.

The public section of the class features the object operation methods that are standard for library objects, the methods for accessing the tick data list object properties, the methods for simplified access to the properties of the specified tick data object located in the list and the methods for creating and updating the list.

Let's consider the object methods.

In the parametric class constructor, clear the list, set the flag of the sorted list by tick time in milliseconds and the required number of days, for which we need to have the tick history in the list using the SetRequiredUsedDays() method.

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

The method of setting a tick list symbol:

//+------------------------------------------------------------------+
//| Set a symbol                                                     |
//+------------------------------------------------------------------+
void CTickSeries::SetSymbol(const string symbol)
  {
   if(this.m_symbol==symbol)
      return;
   this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);
  }
//+------------------------------------------------------------------+

If a symbol passed to the method is already set, exit the method, otherwise, if NULL or empty string is passed, set the current symbol. Otherwise, set a symbol passed to the method.

The method for setting the number of days the tick data is needed for:

//+------------------------------------------------------------------+
//| Set the number of required tick data                             |
//+------------------------------------------------------------------+
void CTickSeries::SetRequiredUsedDays(const uint required=0)
  {
   this.m_required=(required<1 ? TICKSERIES_DEFAULT_DAYS_COUNT : required);
  }
//+------------------------------------------------------------------+

If zero or less than zero is passed to the method, set the default number of days, otherwise — the passed number of days.

The method returning the tick object from the list by the list index:

//+------------------------------------------------------------------+
//| Return the tick object by its index in the list                  |
//+------------------------------------------------------------------+
CDataTick *CTickSeries::GetTickByListIndex(const uint index)
  {
   return this.m_list_ticks.At(index);
  }
//+------------------------------------------------------------------+

Simply return the object located in the list by the index passed to the method. Keep in mind that the At() method returns NULL in case the invalid index is specified or the list is empty.

The method returning the tick object from the list by its time:

//+------------------------------------------------------------------+
//| Return the last tick object by its time                          |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Get the list of tick objects with their time corresponding to the one passed to the method and return the last of them.
Different ticks may have the same time, therefore the last of them is returned.

The method returning the tick object from the list by its time in milliseconds:

//+------------------------------------------------------------------+
//| Return the last tick object by its time in milliseconds          |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Get the list of tick objects with their millisecond time corresponding to the one passed to the method and return the last of them.
Like in the method just considered, different ticks may have the same millisecond time, therefore the last of them is returned as it is considered to be the most relevant.

Subsequent methods for getting tick objects will be identical to each other and have three overloads — getting the list by index, by time and by time in milliseconds. Let's consider these three methods for getting the Bid tick price and show the listing for other identical methods.

The method returning the tick object Bid price by its index in the list:

//+------------------------------------------------------------------+
//| Return tick's Bid by index in the list                           |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Obtain the object from the list by the index passed to the method and return either Bid or 0 from the obtained object (if failed to get the object)

The method returning the Bid price of the last tick object in the list by its time in milliseconds:

//+------------------------------------------------------------------+
//| Return tick's Bid by time in milliseconds                        |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Obtain the last object by the time passed to the method in milliseconds (the obtaining method has been considered above) and return either Bid from the obtained object, or 0 (if failed to obtain the object)

The method returning the Bid price of the last tick object by its time in the list:

//+------------------------------------------------------------------+
//| Return tick's Bid by time                                        |
//+------------------------------------------------------------------+
double CTickSeries::Bid(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Bid() : 0);
  } 
//+------------------------------------------------------------------+

Obtain the last object by the time passed to the method (the obtaining method has been considered above) and return either Bid from the obtained object, or 0 (if failed to obtain the object).

The remaining methods of obtaining the values of tick object properties from the list are identical to the three methods considered above. Leave their analysis for independent study:

//+------------------------------------------------------------------+
//| Return tick's Ask by index in the list                           |
//+------------------------------------------------------------------+

double CTickSeries::Ask(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's Ask by time in milliseconds                        |
//+------------------------------------------------------------------+
double CTickSeries::Ask(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's Ask by time                                        |
//+------------------------------------------------------------------+
double CTickSeries::Ask(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Ask() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's Last by index in the list                          |
//+------------------------------------------------------------------+
double CTickSeries::Last(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's Last by time in milliseconds                       |
//+------------------------------------------------------------------+
double CTickSeries::Last(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's Last by time                                       |
//+------------------------------------------------------------------+
double CTickSeries::Last(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Last() : 0);
  } 
//+-------------------------------------------------------------------------+
//| Return the volume with the increased tick accuracy by index in the list |
//+-------------------------------------------------------------------------+
double CTickSeries::VolumeReal(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+--------------------------------------------------------------------------+
//|Return the volume with the increased tick accuracy by time in milliseconds|
//+--------------------------------------------------------------------------+
double CTickSeries::VolumeReal(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the volume with the increased tick accuracy by time       |
//+------------------------------------------------------------------+
double CTickSeries::VolumeReal(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.VolumeReal() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the tick spread by index in the list                      |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's spread by time in milliseconds                     |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's spread by time                                     |
//+------------------------------------------------------------------+
double CTickSeries::Spread(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Spread() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the tick volume by index in the list                      |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's volume by time in milliseconds                     |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Return tick's volume by time                                     |
//+------------------------------------------------------------------+
long CTickSeries::Volume(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Volume() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the tick flags by index in the list                       |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const uint index)
  {
   CDataTick *tick=this.GetTickByListIndex(index);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the tick flags by time in milliseconds                    |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const ulong time_msc)
  {
   CDataTick *tick=this.GetTick(time_msc);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+
//| Return the tick flags by time                                    |
//+------------------------------------------------------------------+
uint CTickSeries::Flags(const datetime time)
  {
   CDataTick *tick=this.GetTick(time);
   return(tick!=NULL ? tick.Flags() : 0);
  } 
//+------------------------------------------------------------------+

Each of the methods is represented by three identical overloaded methods allowing to get the tick object by its index in the list and by time both in the date format and in milliseconds.

The method returning the string name of the tick series:

//+------------------------------------------------------------------+
//| Return the tick series name                                      |
//+------------------------------------------------------------------+
string CTickSeries::Header(void)
  {
   return CMessage::Text(MSG_TICKSERIES_TEXT_TICKSERIES)+" \""+this.m_symbol+"\"";
  }
//+------------------------------------------------------------------+

Return the string in the format

Tickseries "symbol name"

For example:

Tick series "EURUSD"

The method displaying the full description of the tick series in the journal:

//+------------------------------------------------------------------+
//| Display the tick series description in the journal               |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

The method creates the string showing the description of the number of days the ticks are stored in the series for and the number of ticks that are actually stored in the list.
The header of the tick series and the created string are displayed afterwards. For example:

Tick series "EURUSD": Requested number of days: 1, Historical data created: 256714

The method displaying the brief description of the tick series in the journal:

//+------------------------------------------------------------------+
//| Display the brief tick series description in the journal         |
//+------------------------------------------------------------------+
void CTickSeries::PrintShort(void)
  {
   ::Print(this.Header());
  }
//+------------------------------------------------------------------+

Display the string name of the tick series in the journal.

The method for creating the tick series:

//+------------------------------------------------------------------+
//| Create the series list of tick data                              |
//+------------------------------------------------------------------+
int CTickSeries::Create(const uint required=0)
  {
//--- If the tick series is not used, inform of that and exit
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol,": ",CMessage::Text(MSG_TICKSERIES_TEXT_IS_NOT_USE));
      return false;
     }
//--- Declare the ticks[] array we are to receive historical data to,
//--- clear the list of tick data objects and set the flag of sorting by time in milliseconds
   MqlTick ticks_array[];
   this.m_list_ticks.Clear();
   this.m_list_ticks.Sort(SORT_BY_TICK_TIME_MSC);
   ::ResetLastError();
   int err=ERR_SUCCESS;
//--- Calculate the day start time in milliseconds the ticks should be copied from
   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;
//--- Get historical data of the MqlTick structure to the tick[] array
//--- from the calculated date to the current time and save the obtained number in m_amount.
//--- If failed to get data, display the appropriate message and return zero
   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;
     }
//--- Historical data is received in the rates[] array
//--- In the ticks[] array loop
   for(int i=0; i<(int)this.m_amount; i++)
     {
      //--- create a new object of tick data out of the current MqlTick structure data from the ticks[] array by the loop index
      ::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 failed to add a new tick data object to the list
      //--- display the appropriate message with the error description in the journal
      //--- and remove the newly created object
      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 the size of the created bar object list
   return this.m_list_ticks.Total();
  }
//+------------------------------------------------------------------+

The method operation is described in details in the code listing. In brief:
calculate the start time of the day we should copy the ticks from and request the ticks from the calculated date for the array. If the ticks are calculated successfully, get the next tick in the MqlTick structure format in a loop by the obtained tick array. Use the array to create a new tick object to be placed to the list in case of its successful creation. Upon the loop completion, return the number of ticks placed in the tick data list.

This concludes creation of the tick data list.


Testing the list creation and data retrieval

For test purposes, simply create a list of tick objects for the current symbol for the current day during the program start. In the obtained list, find the tick with the highest Ask and lowest Bid prices, and display the data of detected tick objects in the journal. To do this, take the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part60\ as TestDoEasyPart60.mq5.

Since this is simply a test of the tick data list and it cannot be directly accessed from the library, include the tick data list object class to the EA file:

//|                                             TestDoEasyPart60.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
#include <DoEasy\Objects\Ticks\TickSeries.mqh>
//--- enums

In the area of the program global variables, declare the tick data list object:

//--- 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;

//--- "New tick" object
CNewTickObj    check_tick;
//--- Object of the current symbol tick series data
CTickSeries    tick_series;
//+------------------------------------------------------------------+

In OnTick(), remove the code block to work with tick data objects. It has been left here since the previous article. We will not create any objects here:

//--- Create a temporary list for storing “Tick data” objects,
//--- a variable for obtaining tick data and
//--- a variable for calculating incoming ticks
   static int tick_count=0;
   CArrayObj list;
   MqlTick tick_struct;
//--- Check a new tick on the current symbol
   if(check_tick.IsNewTick())
     {
      //--- If failed to get the price - exit
      if(!SymbolInfoTick(Symbol(),tick_struct))
         return;
      //--- Create a new tick data object
      CDataTick *tick_obj=new CDataTick(Symbol(),tick_struct);
      if(tick_obj==NULL)
         return;
      //--- Increase tick counter (simply to display on the screen, no other purpose is provided)
      tick_count++;
      //--- Limit the number of ticks in the counting as one hundred thousand (again, no purpose is provided)
      if(tick_count>100000) tick_count=1;
      //--- In the comment on the chart display the tick number and its short description
      Comment("--- #",IntegerToString(tick_count,5,'0'),": ",tick_obj.Header());
      //--- If this is the first tick (which follows the first launch of EA) display its full description in the journal
      if(tick_count==1)
         tick_obj.Print();
      //--- Remove if failed to put the created tick data object in the list
      if(!list.Add(tick_obj))
         delete tick_obj;
     }

Thus, the OnTick() handler will look like this:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Handle the NewTick event in the library
   engine.OnTick(rates_data);

//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer(rates_data);   // Working in the timer
      PressButtonsControl();        // Button pressing control
      engine.EventsHandling();      // Working with events
     }

//--- If the trailing flag is set
   if(trailing_on)
     {
      TrailingPositions();          // Trailing positions
      TrailingOrders();             // Trailing pending orders
     }
  }
//+------------------------------------------------------------------+

The list is to be created in the OnInit() handler inside the DoEasy library initialization function, namely, in the code block:

//+------------------------------------------------------------------+
//| Initializing DoEasy library                                      |
//+------------------------------------------------------------------+
void OnInitDoEasy()
  {
//--- Check if working with the full list is selected
   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="\nNumber of symbols on server "+(string)total+".\nMaximum 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;
        }
     }
//--- Set the counter start point to measure the approximate library initialization time
   ulong begin=GetTickCount();
   Print(TextByLanguage("--- Инициализация библиотеки \"DoEasy\" ---","--- Initializing the \"DoEasy\" library ---"));
//--- Fill in the array of used symbols
   CreateUsedSymbolsArray((ENUM_SYMBOLS_MODE)used_symbols_mode,InpUsedSymbols,array_used_symbols);
//--- Set the type of the used symbol list in the symbol collection and fill in the list of symbol timeseries
   engine.SetUsedSymbols(array_used_symbols);
//--- Displaying the selected mode of working with the symbol object collection in the journal
   string num=
     (
      used_symbols_mode==SYMBOLS_MODE_CURRENT ? ": \""+Symbol()+"\"" : 
      TextByLanguage(". Количество используемых символов: ",". The number of symbols used: ")+(string)engine.GetSymbolsCollectionTotal()
     );
   Print(engine.ModeSymbolsListDescription(),num);
//--- Implement displaying the list of used symbols only for MQL5 - MQL4 has no ArrayPrint() function
#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   
//--- Set used timeframes
   CreateUsedTimeframesArray(InpModeUsedTFs,InpUsedTFs,array_used_periods);
//--- Display the selected mode of working with the timeseries object collection
   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);
//--- Implement displaying the list of used timeframes only for MQL5 - MQL4 has no ArrayPrint() function
#ifdef __MQL5__
   if(InpModeUsedTFs!=TIMEFRAMES_MODE_CURRENT)
      ArrayPrint(array_used_periods);
#endif 
//--- Create timeseries of all used symbols
   engine.SeriesCreateAll(array_used_periods);

//--- Check created timeseries - display descriptions of all created timeseries in the journal
//--- (true - only created ones, false - created and declared ones)
   engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions
   //engine.GetTimeSeriesCollection().Print(true);      // Full descriptions

//--- Code block for checking the tick list creation and working with it
   Print("");
//--- Since the tick series object is created with the default constructor,
//--- set a symbol, usage flag and the number of days (the default is 1) to copy the ticks
//--- Create the tick series and printed data in the journal
   tick_series.SetSymbol(Symbol());
   tick_series.SetAvailable(true);
   tick_series.SetRequiredUsedDays();
   tick_series.Create();
   tick_series.Print();
   
   Print("");
//--- Get and display in the journal the data of an object with the highest Ask price in the daily price range
   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();
//--- Get and display in the journal the data of an object with the lowest Bid price in the daily price range
   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();

//--- Create resource text files
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_01",TextByLanguage("Звук упавшей монетки 1","Falling coin 1"),sound_array_coin_01);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_02",TextByLanguage("Звук упавших монеток","Falling coins"),sound_array_coin_02);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_03",TextByLanguage("Звук монеток","Coins"),sound_array_coin_03);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_04",TextByLanguage("Звук упавшей монетки 2","Falling coin 2"),sound_array_coin_04);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_01",TextByLanguage("Звук щелчка по кнопке 1","Button click 1"),sound_array_click_01);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_02",TextByLanguage("Звук щелчка по кнопке 2","Button click 2"),sound_array_click_02);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_03",TextByLanguage("Звук щелчка по кнопке 3","Button click 3"),sound_array_click_03);
   engine.CreateFile(FILE_TYPE_WAV,"sound_array_cash_machine_01",TextByLanguage("Звук кассового аппарата","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);

//--- Pass all existing collections to the main library class
   engine.CollectionOnInit();

//--- Set the default magic number for all used symbols
   engine.TradingSetMagic(engine.SetCompositeMagicNumber(magic_number));
//--- Set synchronous passing of orders for all used symbols
   engine.TradingSetAsyncMode(false);
//--- Set the number of trading attempts in case of an error
   engine.TradingSetTotalTry(InpTotalAttempts);
//--- Set correct order expiration and filling types to all trading objects
   engine.TradingSetCorrectTypeExpiration();
   engine.TradingSetCorrectTypeFilling();

//--- Set standard sounds for trading objects of all used symbols
   engine.SetSoundsStandart();
//--- Set the general flag of using sounds
   engine.SetUseSounds(InpUseSounds);
//--- Set the spread multiplier for symbol trading objects in the symbol collection
   engine.SetSpreadMultiplier(InpSpreadMultiplier);
      
//--- Set controlled values for symbols
   //--- Get the list of all collection symbols
   CArrayObj *list=engine.GetListAllUsedSymbols();
   if(list!=NULL && list.Total()!=0)
     {
      //--- In a loop by the list, set the necessary values for tracked symbol properties
      //--- By default, the LONG_MAX value is set to all properties, which means "Do not track this property" 
      //--- It can be enabled or disabled (by setting the value less than LONG_MAX or vice versa - set the LONG_MAX value) at any time and anywhere in the program
      /*
      for(int i=0;i<list.Total();i++)
        {
         CSymbol* symbol=list.At(i);
         if(symbol==NULL)
            continue;
         //--- Set control of the symbol price increase by 100 points
         symbol.SetControlBidInc(100000*symbol.Point());
         //--- Set control of the symbol price decrease by 100 points
         symbol.SetControlBidDec(100000*symbol.Point());
         //--- Set control of the symbol spread increase by 40 points
         symbol.SetControlSpreadInc(400);
         //--- Set control of the symbol spread decrease by 40 points
         symbol.SetControlSpreadDec(400);
         //--- Set control of the current spread by the value of 40 points
         symbol.SetControlSpreadLevel(400);
        }
      */
     }
//--- Set controlled values for the current account
   CAccount* account=engine.GetAccountCurrent();
   if(account!=NULL)
     {
      //--- Set control of the profit increase to 10
      account.SetControlledValueINC(ACCOUNT_PROP_PROFIT,10.0);
      //--- Set control of the funds increase to 15
      account.SetControlledValueINC(ACCOUNT_PROP_EQUITY,15.0);
      //--- Set profit control level to 20
      account.SetControlledValueLEVEL(ACCOUNT_PROP_PROFIT,20.0);
     }
//--- Get the end of the library initialization time counting and display it in the journal
   ulong end=GetTickCount();
   Print(TextByLanguage("Время инициализации библиотеки: ","Library initialization time: "),TimeMSCtoString(end-begin,TIME_MINUTES|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

The code block for creating the tick series of the current symbol for the current day and searching for the two necessary tick objects in it to display their data in the journal has been commented on in details. So, I believe, no questions should arise when studying it. If you have any questions, feel free to ask them in the comments below.

This function is calculated from the OnInit() handler. Therefore, the list is created once when launching the program. Two tick data objects featuring the highest Ask and the lowest Bid for the current day will immediately be found in it. Displaying data requires some time. If no tick data is presented locally, their download is activated.

Compile the EA and launch it on any symbol chart having preliminary defined in the settings to use the current symbol and the current timeframe. When initializing the EA, data on EA parameters, on created timeseries, as well as (a bit later) on created tick series are to be displayed. The data on two found ticks with the highest Ask and lowest Bid for the day are to be displayed below:

Account 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10426.13 USD, 1:100, Hedge, MetaTrader 5 demo
--- Initializing "DoEasy" library ---
Working with the current symbol only: "EURUSD"
Working with the current timeframe only: H4
EURUSD symbol timeseries: 
- Timeseries "EURUSD" H4: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6336

Tick series "EURUSD": Requested number of days: 1, Historical data created: 276143

============= Beginning of parameter list (Tick "EURUSD" 2021.01.06 14:25:32.156) =============
Last price update time in milliseconds: 2021.01.06 14:25:32.156
Last price update time: 2021.01.06 14:25:32
Volume for the current Last price: 0
Flags: 134
Changed data on the tick:
 - Ask price change
 - Bid price change
------
Bid price: 1.23494
Ask price: 1.23494
Last price: 0.00000
Volume for the current Last price with greater accuracy: 0.00
Spread: 0.00000
------
Symbol: "EURUSD"
============= End of parameter list (Tick "EURUSD" 2021.01.06 14:25:32.156) =============

============= Beginning of parameter list (Tick "EURUSD" 2021.01.07 12:51:40.632) =============
Last price update time in milliseconds: 2021.01.07 12:51:40.632
Last price update time: 2021.01.07 12:51:40
Volume for the current Last price: 0
Flags: 134
Changed data on the tick:
 - Ask price change
 - Bid price change
------
Bid price: 1.22452
Ask price: 1.22454
Last price: 0.00000
Volume for the current Last price with greater accuracy: 0.00
Spread: 0.00002
------
Symbol: "EURUSD"
============= End of parameter list (Tick "EURUSD" 2021.01.07 12:51:40.632) =============

Library initialization time: 00:00:12.828

Initialization took 12.8 seconds — time for uploading historical tick data.

What's next?

In the next article, we will create the collection class of tick data of all symbols used in the program and implement the update of all created lists in real time.

All files of the current version of the library are attached below together with the test EA file for MQL5 for you to test and download.
The tick data classes are under development, therefore their use in custom programs at this stage is strongly not recommended.
Leave your questions and suggestions in the comments.

Back to contents

Previous articles within the series:

Timeseries in DoEasy library (part 35): Bar object and symbol timeseries list
Timeseries in DoEasy library (part 36): Object of timeseries for all used symbol periods
Timeseries in DoEasy library (part 37): Timeseries collection - database of timeseries by symbols and periods
Timeseries in DoEasy library (part 38): Timeseries collection - real-time updates and accessing data from the program
Timeseries in DoEasy library (part 39): Library-based indicators - preparing data and timeseries events
Timeseries in DoEasy library (part 40): Library-based indicators - updating data in real time
Timeseries in DoEasy library (part 41): Sample multi-symbol multi-period indicator
Timeseries in DoEasy library (part 42): Abstract indicator buffer object class
Timeseries in DoEasy library (part 43): Classes of indicator buffer objects
Timeseries in DoEasy library (part 44): Collection class of indicator buffer objects
Timeseries in DoEasy library (part 45): Multi-period indicator buffers
Timeseries in DoEasy library (part 46): Multi-period multi-symbol indicator buffers
Timeseries in DoEasy library (part 47): Multi-period multi-symbol standard indicators
Timeseries in DoEasy library (part 48): Multi-period multi-symbol indicators on one buffer in a subwindow
Timeseries in DoEasy library (part 49): Multi-period multi-symbol multi-buffer standard indicators
Timeseries in DoEasy library (part 50): Multi-period multi-symbol standard indicators with a shift
Timeseries in DoEasy library (part 51): Composite multi-period multi-symbol standard indicators
Timeseries in DoEasy library (part 52): Cross-platform nature of multi-period multi-symbol single-buffer standard indicators
Timeseries in DoEasy library (part 53): Abstract base indicator class
Timeseries in DoEasy library (part 54): Descendant classes of abstract base indicator
Timeseries in DoEasy library (part 55): Indicator collection class
Timeseries in DoEasy library (part 56): Custom indicator object, get data from indicator objects in the collection
Timeseries in DoEasy library (part 57): Indicator buffer data object
Timeseries in DoEasy library (part 58): Timeseries of indicator buffer data
Prices in DoEasy library (part 59): Object to store data of one tick