Trabajando con las series temporales en la biblioteca DoEasy (Parte 38): Colección de series temporales - Actualización en tiempo real y acceso a los datos desde el programa

Artyom Trishkin | 2 julio, 2020

Contenido


Concepto

En los anteriores artículos dedicados a la creación de series temporales de cualquier periodo del gráfico de cualquier símbolo, creamos una clase de colección de series temporales completa de todos los símbolos utilizados en el programa y aprendimos a rellenar las series temporales con datos históricos para la búsqueda rápida y la clasificación de estos datos.
Disponiendo de este instrumento, podremos en lo sucesivo buscar y comparar diferentes combinaciones de datos de precio en la historia. Pero también tenemos que pensar en la actualización de los datos que debemos realizar en cada nuevo tick para cada símbolo utilizado.

En principio, y como variante más simple, podemos actualizar todas las series temporales en el manejador de milisegundos OnTimer() del programa, y esto funcionará. Pero aquí surgen la pregunta: ¿es siempre necesario actualizar los datos de alguna serie temporal exactamente según el contador del temporizador? Y es que los datos cambian en el programa cuando llega un nuevo tick, y no es completamente correcto actualizar los datos independientemente de si ha llegado este nuevo tick o aún no lo ha hecho: resultaría irracional desde el punto de vista del rendimiento.

Si en el símbolo actual siempre podemos estar al tanto de la llegada de un nuevo tick en los manejadores OnTick() del experto o en OnCalculate() del indicador para comprender si "ha habido un nuevo tick", en cualquier otro símbolo que deba ser monitoreado por el programa iniciado en otro símbolo que no sea este, deberemos monitorear los eventos necesarios en el experto o el indicador.

La variante más sencilla de todas las posibles capaces de satisfacer las necesidades de la biblioteca teniendo en cuenta su estado actual, es la simple comparación de la hora del tick pasado con la hora del actual: si la hora del tick pasado se distingue de la hora del actual, consideraremos que ha llegado un nuevo tick en el símbolo monitoreado por el programa, pero que no es "propio" para el programa, es decir, que este programa no está iniciado en el gráfico de este símbolo.

Antes de comenzar a crear la clase "Nuevo tick" y la implementación de la actualización en tiempo real de todas las series temporales utilizadas en el programa, vamos a retocar las clases ya creadas.


Mejorando las clases de las series temporales

En primer lugar, como ya venimos haciendo, añadimos al archivo Datas.mqh el índice del nuevo mensaje de la biblioteca:

//--- CTimeSeries
   MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL,             // First, set a symbol using SetSymbol()
   MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE,                   // Timeseries is not used. Set the flag using SetAvailable()
   MSG_LIB_TEXT_TS_TEXT_UNKNOWN_TIMEFRAME,            // Unknown timeframe

y el texto del mensaje que se corresponde con el índice nuevamente añadido:

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

La clase de objeto básico de todos los objetos de la biblioteca CBaseObj se compone de dos variables:

//+------------------------------------------------------------------+
//| Base object class for all library objects                        |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   ENUM_LOG_LEVEL    m_log_level;                              // Logging level
   ENUM_PROGRAM_TYPE m_program;                                // Program type
   bool              m_first_start;                            // First launch flag
   bool              m_use_sound;                              // Flag of playing the sound set for an object
   bool              m_available;                              // Flag of using a descendant object in the program
   int               m_global_error;                           // Global error code
   long              m_chart_id_main;                          // Control program chart ID
   long              m_chart_id;                               // Chart ID
   string            m_name;                                   // Object name
   string            m_folder_name;                            // Name of the folder storing CBaseObj descendant objects 
   string            m_sound_name;                             // Object sound file name
   int               m_type;                                   // Object type (corresponds to the collection IDs)

public:

La variable m_chart_id_main guarda el identificador del gráfico del programa de control: se trata del gráfico del símbolo en el que se ha iniciado el programa. El gráfico deberá enviar a esta biblioteca todos los eventos que se registran en las colecciones y objetos de la biblioteca.
La variable m_chart_id, por su parte, sirve para guardar el identificador del gráfico con el que de alguna forma se relaciona el objeto que es heredero de la clase CBaseObj. Por el momento, esta propiedad no se utiliza en ninguna parte, pero la necesitaremos en el futuro.

No obstante, dado que hemos añadido la variable m_chart_id_main un poco más tarde que la variable m_chart_id, todos los mensajes se envían al identificador del gráfico registrado en la variable m_chart_id. Esta discordancia ha sido corregida: ahora, todos los identificadores del gáfico actual se registran en la variable m_chart_id_main, como debía ser. Además, se han realizado correcciones en todas las clases en las que existe el envío de mensajes desde la biblioteca al gráfico del programa de control; todas las entradas del texto "m_chart_id" han sido sustituidas por "m_chart_id_main".
Estos cambios han sido introducidos en todas las clases de eventos de la carpeta \MQL5\Include\DoEasy\Objects\Events\, así como en los archivos de las clases de las colecciones AccountsCollection.mqh, EventsCollection.mqh y SymbolsCollection.mqh.
Para ahorrar especio en el artículo, no vamos a describir estos cambios aquí: el lector podrá analizarlos en los archivos adjuntos al artículo.

Para poder mostrar los datos de la barra indicada de la colección de series temporales, vamos a añadir la descripción textual de los parámetros de la barra de la clase CBar
en el archivo \MQL5\Include\DoEasy\Objects\Series\Bar.mqh.

En el bloque de código con la descripción de las propiedades del objeto, declaramos el método para crear la descripción textual de los parámetros de la barra:

//+------------------------------------------------------------------+
//| Descriptions of bar object properties                            |
//+------------------------------------------------------------------+
//--- Get description of a bar's (1) integer, (2) real and (3) string properties
   string            GetPropertyDescription(ENUM_BAR_PROP_INTEGER property);
   string            GetPropertyDescription(ENUM_BAR_PROP_DOUBLE property);
   string            GetPropertyDescription(ENUM_BAR_PROP_STRING property);

//--- Return the bar type description
   string            BodyTypeDescription(void)  const;
//--- Send description of bar properties to the journal (full_prop=true - all properties, false - only supported ones)
   void              Print(const bool full_prop=false);
//--- Display a short bar description in the journal
   virtual void      PrintShort(void);
//--- Return the (1) short name and (2) description of bar object parameters
   virtual string    Header(void);
   string            ParameterDescription(void);
//---
  };
//+------------------------------------------------------------------+

Implementamos fuera del cuerpo de la clase el método para crear la descripción textual de los parámetros de la barra, y cambiamos la implementación del método que muestra en el diario la descripción breve de la barra:

//+------------------------------------------------------------------+
//| Return the description of the bar object parameters              |
//+------------------------------------------------------------------+
string CBar::ParameterDescription(void)
  {
   int dg=(this.m_digits>0 ? this.m_digits : 1);
   return
     (
      ::TimeToString(this.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+", "+
      "O: "+::DoubleToString(this.Open(),dg)+", "+
      "H: "+::DoubleToString(this.High(),dg)+", "+
      "L: "+::DoubleToString(this.Low(),dg)+", "+
      "C: "+::DoubleToString(this.Close(),dg)+", "+
      "V: "+(string)this.VolumeTick()+", "+
      (this.VolumeReal()>0 ? "R: "+(string)this.VolumeReal()+", " : "")+
      this.BodyTypeDescription()
     );
  }
//+------------------------------------------------------------------+
//| Display a short bar description in the journal                   |
//+------------------------------------------------------------------+
void CBar::PrintShort(void)
  {
   ::Print(this.Header(),": ",this.ParameterDescription());
  }
//+------------------------------------------------------------------+

Aquí, simplemente hemos sacado del método que muestra en el diario los parámetros de la barra el código de construcción de la descripción textual de los parámtros, ubicándolo en el nuevo método encargado de retornar el mensaje de texto creado. Al mostrar en el diario los parámetros de la barra, mostramos un mensaje complejo, que consta de la denominación breve del objeto de barra y sus parámetros, cuya descripción textual ahora se crea en el nuevo método ParameterDescription().

Para actualizar los datos de las series temporales "ajenas" (que no son aquellas en las que está iniciado el programa), hemos decidido crear la clase "Nuevo tick" y actualizar los datos de estos símbolos solo al darse el evento "Nuevo tick" para cada símbolo utilizado en el programa.

La clase "Nuevo tick" y la actualización de datos

Creamos en el directorio de la biblioteca \MQL5\Include\DoEasy\Objects\ la nueva carpeta Ticks\, y en ella, el nuevo archivo NewTickObj.mqh de la clase CNewTickObj heredada de la clase del objeto básico de todos los objetos de la biblioteca CBaseObj, cuyo archivo está incluido en el archivo de la clase; luego, lo rellenamos de inmediato con los datos necesarios:

//+------------------------------------------------------------------+
//|                                                   NewTickObj.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "..\..\Objects\BaseObj.mqh"
//+------------------------------------------------------------------+
//| "New tick" class                                                 |
//+------------------------------------------------------------------+
class CNewTickObj : public CBaseObj
  {
private:
   MqlTick           m_tick;                          // Structure of the current prices
   MqlTick           m_tick_prev;                     // Structure of the current prices during the previous check
   string            m_symbol;                        // Object symbol
   bool              m_new_tick;                      // New tick flag
public:
//--- Set a symbol
   void              SetSymbol(const string symbol)   { this.m_symbol=symbol;             }
//--- Return the new tick flag
   bool              IsNewTick(void);
//--- Update price data in the tick structure and set the "New tick" event flag if necessary
   void              Refresh(void)                    { this.m_new_tick=this.IsNewTick(); }
//--- Constructors
                     CNewTickObj(void){;}
                     CNewTickObj(const string symbol);
  };
//+------------------------------------------------------------------+

En la variable m_tick, guardaremos los datos de precio del último tick recibido.

En la variable m_tick_prev, guardaremos los datos de precio del tick anterior.

En la variable m_symbol, guardaremos el símbolo cuyo nuevo tick vamos a monitorear.

La bandera de nuevo tick en la variable m_new_tick se usará en lo sucesivo.
En estos momentos (para las actualidades necesidades de la biblioteca), determinaremos el evento "Nuevo tick" en el símbolo con la ayuda del método IsNewTick():

//+------------------------------------------------------------------+
//| Return the new tick flag                                         |
//+------------------------------------------------------------------+
bool CNewTickObj::IsNewTick(void)
  {
//--- If failed to get the current prices to the tick structure, return 'false'
   if(!::SymbolInfoTick(this.m_symbol,this.m_tick))
      return false;
//--- If this is the first launch, copy data of the obtained tick to the previous tick data
//--- reset the first launch flag and return 'false'
   if(this.m_first_start)
     {
      this.m_tick_prev=this.m_tick;
      this.m_first_start=false;
      return false;
     }
//--- If the time of a new tick is not equal to the time of a tick during the previous check -
//--- copy data of the obtained tick to the previous tick data and return 'true'
   if(this.m_tick.time_msc!=this.m_tick_prev.time_msc)
     {
      this.m_tick_prev=this.m_tick;
      return true;
     }
//--- In all other cases, return 'false'
   return false;
  }
//+------------------------------------------------------------------+

Para la clase, se han determinado dos constructores:

//+------------------------------------------------------------------+
//| Parametric constructor CNewTickObj                               |
//+------------------------------------------------------------------+
CNewTickObj::CNewTickObj(const string symbol) : m_symbol(symbol)
  {
//--- Reset the structures of the new and previous ticks
   ::ZeroMemory(this.m_tick);
   ::ZeroMemory(this.m_tick_prev);
//--- If managed to get the current prices to the tick structure,
//--- copy data of the obtained tick to the previous tick data and reset the first launch flag
  if(::SymbolInfoTick(this.m_symbol,this.m_tick))
     { 
      this.m_tick_prev=this.m_tick;
      this.m_first_start=false;
     }
  }
//+------------------------------------------------------------------+

Aquí tenemos la clase completa del objeto de nuevo tick. La idea es simple: obtenemos los precios en la estructura del tick y comparamos la hora del tick que ha llegado con la hora del tick pasado.
Si las horas no son iguales, significará que ha llegado un nuevo tick.

En los asesores, los ticks pueden omitirse, pero esto aquí no importa. Lo que importa es que, de esta forma, podemos monitorear en el temporizador el hecho de la llegada de un nuevo tick en un símbolo "ajeno", para actualizar los datos solo en el momento de llegada de un nuevo tick, y no constantemente según el temporizador.
En los indicadores donde se monitorean todos los ticks, y los ticks pueden llegar por paquetes para el símbolo en el que está iniciado el indicador, la actualización de los datos de la serie temporal actual debe tener lugar en el manejador OnCalculate(), mientras que en el temporizador se monitorean los nuevos ticks solo para los símbolos "ajenos" (y los eventos de nuevo tick para un símbolo "ajeno" en OnOnCalculate() no se pueden obtener): por eso, también en los indicadores, nos bastará con monitorear solo la diferencia de la hora del tick nuevo y el pasado para los símbolos "ajenos", con objeto de actualizar a tiempo los datos de estas series temporales.

Lo haremos así para que el objeto de serie temporal CSeries pueda enviar al programa de control su evento de "Nueva barra": esto nos dará la oportunidad de obtener en el programa estos eventos de cualquier serie temporal, y también reaccionar a los mismos.

Añadimos al final del listado del archivo Defines.mqh una nueva enumeración con los posibles eventos del objeto de serie temporal:

//+------------------------------------------------------------------+
//| List of possible timeseries events                               |
//+------------------------------------------------------------------+
enum ENUM_SERIES_EVENT
  {
   SERIES_EVENTS_NO_EVENT = SYMBOL_EVENTS_NEXT_CODE,        // no event
   SERIES_EVENTS_NEW_BAR,                                   // "New bar" event
  };
#define SERIES_EVENTS_NEXT_CODE  (SERIES_EVENTS_NEW_BAR+1)  // Code of the next event after the "New bar" event
//+------------------------------------------------------------------+

Por el momento, aquí solo tenemos dos estados para los eventos de serie temporal: "No hay eventos" y el evento "Nueva barra". Necesitamos las constantes de esta enumeración para ejecutar la búsqueda por las propiedades establecidas del objeto de barra en la lista de colección de barras (en la serie temporal CSeries).

Dado que los objetos de las series temporales se actualizarán en el temporizador de la biblioteca, vamos a añadir al listado del archivo Defines.mqh los parámetros del temporizador de actualización de la colección de objetos de series temporales y el identidicador de la lista de colección de series temporales:

//--- Trading class timer parameters
#define COLLECTION_REQ_PAUSE           (300)                      // Trading class timer pause in milliseconds
#define COLLECTION_REQ_COUNTER_STEP    (16)                       // Trading class timer counter increment
#define COLLECTION_REQ_COUNTER_ID      (5)                        // Trading class timer counter ID
//--- Parameters of the timeseries collection timer
#define COLLECTION_TS_PAUSE            (32)                       // Timeseries collection timer pause in milliseconds
#define COLLECTION_TS_COUNTER_STEP     (16)                       // Account timer counter increment
#define COLLECTION_TS_COUNTER_ID       (6)                        // Timeseries timer counter ID
//--- Collection list IDs
#define COLLECTION_HISTORY_ID          (0x777A)                   // Historical collection list ID
#define COLLECTION_MARKET_ID           (0x777B)                   // Market collection list ID
#define COLLECTION_EVENTS_ID           (0x777C)                   // Event collection list ID
#define COLLECTION_ACCOUNT_ID          (0x777D)                   // Account collection list ID
#define COLLECTION_SYMBOLS_ID          (0x777E)                   // Symbol collection list ID
#define COLLECTION_SERIES_ID           (0x777F)                   // Timeseries collection list ID
//--- Data parameters for file operations

Ya vimos los parámetros del temporizador de las colecciones al crear el objeto básico de la biblioteca CEngine, y la designación de los identificadores de las colecciones al reorganizar la estructura de la biblioteca: si hemos olvidado algo, podremos regresar y repasar el material ya analizado.

Vamos a asignar de inmediato al objeto de barra el identificador de la colección de series temporales, dado que el objeto de serie temporal es una lista que contiene los punteros a los objetos de barra que pertenecen a esta lista.
Abrimos de nuevo el archivo \MQL5\Include\DoEasy\Objects\Series\Bar.mqh e introducimos la indicación del tipo de objeto en ambos constructores:

//+------------------------------------------------------------------+
//| Constructor 1                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   this.m_type=COLLECTION_SERIES_ID;
   MqlRates rates_array[1];
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- If failed to write bar data to the MqlRates array by index or set the time to the time structure,
//--- display an error message, create and fill the structure with zeros, and write it to the rates_array array
   if(::CopyRates(symbol,timeframe,index,1,rates_array)<1 || !::TimeToStruct(rates_array[0].time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code));
      MqlRates err={0};
      rates_array[0]=err;
     }
//--- Set the bar properties
   this.SetProperties(rates_array[0]);
  }
//+------------------------------------------------------------------+
//| Constructor 2                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
//--- If failed to set time to the time structure, display the error message,
//--- create and fill the structure with zeros, set the bar properties from this structure and exit
   if(!::TimeToStruct(rates.time,this.m_dt_struct))
     {
      int err_code=::GetLastError();
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code));
      MqlRates err={0};
      this.SetProperties(err);
      return;
     }
//--- Set the bar properties
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+


Ahora, mejoramos la clase del objeto de serie temporal CSeries ubicado en la dirección \MQL5\Include\DoEasy\Objects\Series\Series.mqh.

En la sección pública de la clase, declaramos el nuevo método de envío del evento al gráfico del programa de control:

//--- (1) Create and (2) update the timeseries list
   int               Create(const uint required=0);
   void              Refresh(const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);
                             
//--- Create and send the "New bar" event to the control program chart
   void              SendEvent(void);

//--- Return the timeseries name
   string            Header(void);
//--- Display (1) the timeseries description and (2) the brief timeseries description in the journal
   void              Print(void);
   void              PrintShort(void);


//--- Constructors
                     CSeries(void);
                     CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
  };
//+------------------------------------------------------------------+

Al final del listado de la clase, implementamos el método declarado:

//+------------------------------------------------------------------+
//| Create and send the "New bar" event                              |
//| to the control program chart                                     |
//+------------------------------------------------------------------+
void CSeries::SendEvent(void)
  {
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(0),this.Timeframe(),this.Symbol());
  }
//+------------------------------------------------------------------+

Aquí, creamos y enviamos al gráfico del programa de control un evento que consta de:
el identificador del gráfico receptor del evento,
el identificador del evento (Nueva barra),
como parámetro long del evento, enviamos la hora de apertura de la nueva barra,
como parámetro double del evento, enviamos el marco temporal del gráfico
en el que ha sucedido el evento, y
como parámetro string, enviamos el nombre del símbolo en cuya serie temporal ha tenido lugar el evento.

Añadimos al método de sincronización de la serie temporal la comprobación del uso de esta serie temporal en el programa :

//+------------------------------------------------------------------+
//|Synchronize symbol and timeframe data with server data            |
//+------------------------------------------------------------------+
bool CSeries::SyncData(const uint required,const uint rates_total)
  {
//--- If the timeseries is not used, notify of that and exit
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      return false;
     }
//--- If managed to obtain the available number of bars in the timeseries

Es decir, si para la serie temporal no se ha establecido la bandera para su uso en el programa, tampoco tiene sentido sincronizarla. No obstante, podría suceder que la serie temporal fuera necesaria, pero hubiéramos olvidado poner en algún lugar de nuestro programa la bandera de su uso: por eso, en el diario se mostraría un mensaje informando de que la serie temporal no se usa.

Vamos a añadir una comprobación exactamente igual en el método de creación de la serie temporal:

//+------------------------------------------------------------------+
//| Create the timeseries list                                       |
//+------------------------------------------------------------------+
int CSeries::Create(const uint required=0)
  {
//--- If the timeseries is not used, notify of that and return zero
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      return 0;
     }
//--- If the required history depth is not set for the list yet,

Asimismo, hemos rediseñado en la clase el método que retorna un objeto de barra según el índice de la serie temporal. Antes, el método era así:

//+------------------------------------------------------------------+
//| Return the bar object by index in the timeseries                 |
//+------------------------------------------------------------------+
CBar *CSeries::GetBarBySeriesIndex(const uint index)
  {
   CArrayObj *list=this.GetList(BAR_PROP_INDEX,index);
   return(list==NULL || list.Total()==0 ? NULL : list.At(0));
  }
//+------------------------------------------------------------------+

Es decir, se creaba una nueva lista que contenía una copia de la barra buscada, y esta copia se retornaba. Esto es suficiente para obtener simplemente los datos de la barra solicitada, pero, si debemos modificar las propiedades de la barra, no lo conseguiremos: se cambiarán las propiedades de la copia de la barra, y no las del objeto original.

Dado que necesitamos implementar la actualización en tiempo real de la barra actual cuando llega un nuevo tick, hemos rediseñado el método de tal forma que retorne el puntero al objeto de barra original que se encuentra en la lista de colección de barras, y no la barra de la copia de esta lista:

//+------------------------------------------------------------------+
//| Return the bar object by index in the timeseries                 |
//+------------------------------------------------------------------+
CBar *CSeries::GetBarBySeriesIndex(const uint index)
  {
   CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index); 
   if(tmp==NULL)
      return NULL;
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
   int idx=this.m_list_series.Search(tmp);
   delete tmp;
   CBar *bar=this.m_list_series.At(idx);
   return(bar!=NULL ? bar : NULL);
  }
//+------------------------------------------------------------------+

Aquí, creamos un objeto de barra temporal con el símbolo y el periodo del gráfico del objeto de serie temporal actual y el índice de barra transmitido al método. Precisamente necesitamos el índice de la barra en la serie temporal del gráfico para buscar en la lista de serie temporal el mismo objeto clasficado según el índice de las barras. Como resultado de la búsqueda de la barra con ese índice de serie temporal, obtenemos su índice en la lista, consiguiendo posteriormente según dicho índice el puntero al objeto de barra en la lista y retornando el puntero a este objeto.
Ahora, el método retornará el puntero al objeto de barra original en la lista de serie temporal, y podremos modificarlo al actualizar los datos en tiempo real.

Ahora, vamos a mejorar la clase del objeto de serie temporal CTimeSeries para monitorear los nuevos ticks y actualizar los datos al determinarse dicho evento. El objeto de clase supone un conjunto de series temporales con todos los periodos del gráfico de un mismo símbolo utilizados en el programa. Y esto significa que este objeto es el lugar ideal para el objeto de la clase "Nuevo tick", dado que la obtención de un nuevo tick del símbolo de objeto de series temporales CTimeSeries iniciará el proceso de actualización de los datos de los objetos de series temporales CSeries de todos los periodos que pertenezcan a este objeto.

Incluimos en el archivo de clase del objeto de series temporales el archivo de clase del objeto "Nuevo tick", y definimos en la sección privada de la clase el objeto de clase "Nuevo tick".
Asimismo, añadimos a la sección pública de la clase el método que retorna la bandera de nuevo tick en el símbolo del actual objeto de series temporales:

//+------------------------------------------------------------------+
//|                                                   TimeSeries.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "Series.mqh"
#include "..\Ticks\NewTickObj.mqh"
//+------------------------------------------------------------------+
//| Symbol timeseries class                                          |
//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObj
  {
private:
   string            m_symbol;                                             // Timeseries symbol
   CNewTickObj       m_new_tick;                                           // "New tick" object
   CArrayObj         m_list_series;                                        // List of timeseries by timeframes
   datetime          m_server_firstdate;                                   // The very first date in history by a server symbol
   datetime          m_terminal_firstdate;                                 // The very first date in history by a symbol in the client terminal
//--- Return (1) the timeframe index in the list and (2) the timeframe by the list index
   char              IndexTimeframe(const ENUM_TIMEFRAMES timeframe) const { return IndexEnumTimeframe(timeframe)-1;                            }
   ENUM_TIMEFRAMES   TimeframeByIndex(const uchar index)             const { return TimeframeByEnumIndex(uchar(index+1));                       }
//--- Set the very first date in history by symbol on the server and in the client terminal
   void              SetTerminalServerDate(void)
                       {
                        this.m_server_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE);
                        this.m_terminal_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE);
                       }
public:
//--- Return (1) oneself, (2) the full list of timeseries, (3) specified timeseries object and (4) timeseries object by index
   CTimeSeries      *GetObject(void)                                       { return &this;                                                      }
   CArrayObj        *GetListSeries(void)                                   { return &this.m_list_series;                                        }
   CSeries          *GetSeries(const ENUM_TIMEFRAMES timeframe)            { return this.m_list_series.At(this.IndexTimeframe(timeframe));      }
   CSeries          *GetSeriesByIndex(const uchar index)                   { return this.m_list_series.At(index);                               }
//--- Set/return timeseries symbol
   void              SetSymbol(const string symbol)                        { this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);  }
   string            Symbol(void)                                    const { return this.m_symbol;                                              }
//--- Set the history depth (1) of a specified timeseries and (2) of all applied symbol timeseries
   bool              SetRequiredUsedData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool              SetRequiredAllUsedData(const uint required=0,const int rates_total=0);
//--- Return the flag of data synchronization with the server data of the (1) specified timeseries, (2) all timeseries
   bool              SyncData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool              SyncAllData(const uint required=0,const int rates_total=0);
//--- Return the very first date in history by symbol (1) on the server, (2) in the client terminal and (3) the new tick flag
   datetime          ServerFirstDate(void)                           const { return this.m_server_firstdate;                                    }
   datetime          TerminalFirstDate(void)                         const { return this.m_terminal_firstdate;                                  }
   bool              IsNewTick(void)                                       { return this.m_new_tick.IsNewTick();                                }
//--- Create (1) the specified timeseries list and (2) all timeseries lists
   bool              Create(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool              CreateAll(const uint required=0);
//--- Update (1) the specified timeseries list and (2) all timeseries lists
   void              Refresh(const ENUM_TIMEFRAMES timeframe,
                             const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);
   void              RefreshAll(const datetime time=0,
                             const double open=0,
                             const double high=0,
                             const double low=0,
                             const double close=0,
                             const long tick_volume=0,
                             const long volume=0,
                             const int spread=0);

//--- Compare CTimeSeries objects (by symbol)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Display (1) description and (2) short symbol timeseries description in the journal
   void              Print(const bool created=true);
   void              PrintShort(const bool created=true);
   
//--- Constructors
                     CTimeSeries(void){;}
                     CTimeSeries(const string symbol);
  };
//+------------------------------------------------------------------+

El método IsNewTick() retorna el resultado de la solicitud de los datos sobre el nuevo tick desde el objeto "Nuevo tick" m_new_tick.

Por consiguiente, para que el objeto de clase "Nuevo tick" comprenda sobre qué símbolo retornar los datos, en el constructor de la clase, deberemos asignar el símbolo al objeto de la clase "Nuevo tick" y de inmediato actualizar los datos para leer los precios del tick actual:

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

En los métodos que retornan la bandera de sincronización de los datos, comprobamos ahora la bandera de uso de la serie temporal, y si la bandera está quitada, la serie temporal no se usará en el programa, por lo que no deberemos procesarla:

//+------------------------------------------------------------------+
//| Return the flag of data synchronization                          |
//| with the server data                                             |
//+------------------------------------------------------------------+
bool CTimeSeries::SyncData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0)
  {
   if(this.m_symbol==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL));
      return false;
     }
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe));
      return false;
     }
   if(!series_obj.IsAvailable())
      return false;
   return series_obj.SyncData(required,rates_total);
  }
//+------------------------------------------------------------------+
//| Return the flag of data synchronization                          |
//| of all timeseries with the server data                           |
//+------------------------------------------------------------------+
bool CTimeSeries::SyncAllData(const uint required=0,const int rates_total=0)
  {
   if(this.m_symbol==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL));
      return false;
     }
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable())
         continue;
      res &=series_obj.SyncData(required,rates_total);
     }
   return res;
  }
//+------------------------------------------------------------------+

En los mismos métodos de creación de la serie temporal, estableceremos forzosamente la bandera de uso de la serie temporal, ya que si la creamos, significará que tenemos intención de usarla:

//+------------------------------------------------------------------+
//| Create a specified timeseries list                               |
//+------------------------------------------------------------------+
bool CTimeSeries::Create(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe));
      return false;
     }
   if(series_obj.RequiredUsedData()==0)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_TEXT_FIRS_SET_AMOUNT_DATA));
      return false;
     }
   series_obj.SetAvailable(true);
   return(series_obj.Create(required)>0);
  }
//+------------------------------------------------------------------+
//| Create all timeseries lists                                      |
//+------------------------------------------------------------------+
bool CTimeSeries::CreateAll(const uint required=0)
  {
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || series_obj.RequiredUsedData()==0)
         continue;
      series_obj.SetAvailable(true);
      res &=(series_obj.Create(required)>0);
     }
   return res;
  }
//+------------------------------------------------------------------+

En los métodos de actualización de la serie temporal, en el caso de que se detecte en esta el evento "Nueva barra", añadimos el envío de un mensaje sobre el evento al gráfico del programa de control con la ayuda del método SendEvent() del objeto de serie temporal CSeries que hemos analizado anteriormente:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,
                          const datetime time=0,
                          const double open=0,
                          const double high=0,
                          const double low=0,
                          const double close=0,
                          const long tick_volume=0,
                          const long volume=0,
                          const int spread=0)
  {
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0)
      return;
   series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread);
   if(series_obj.IsNewBar(time))
     {
      series_obj.SendEvent();
      this.SetTerminalServerDate();
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeries::RefreshAll(const datetime time=0,
                          const double open=0,
                          const double high=0,
                          const double low=0,
                          const double close=0,
                          const long tick_volume=0,
                          const long volume=0,
                          const int spread=0)
  {
   bool upd=false;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || series_obj.DataTotal()==0)
         continue;
      series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread);
      if(series_obj.IsNewBar(time))
        {
         series_obj.SendEvent();
         upd &=true;
        }
     }
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+


Vamos a mejorar la clase de colección de series temporales CTimeSeriesCollection en el archivo \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh.

Tenemos que convertir el tipo de la lista de colección de series temporales en el tipo de la clase CListObj.
Para ello, incluimos el archivo de la clase CListObj y cambiamos el tipo de la lista de colección de CArrayObj a CListObj:

//+------------------------------------------------------------------+
//|                                         TimeSeriesCollection.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Objects\Series\TimeSeries.mqh"
#include "..\Objects\Symbols\Symbol.mqh"
//+------------------------------------------------------------------+
//| Symbol timeseries collection                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CObject
  {
private:
   CListObj                m_list;                    // List of applied symbol timeseries
//--- Return the timeseries index by symbol name
   int                     IndexTimeSeries(const string symbol);
public:

En la sección pública de la clase, declaramos el método para retornar la barra indicada de la serie temporal según el índice de la serie temporal del gráfico, así como el método que retorna la bandera de apertura de una nueva barra de la serie temporal indicada y el método para actualizar las series temporales que no pertenezcan al símbolo actual:

//--- Return the flag of data synchronization with the server data of the (1) specified timeseries of the specified symbol,
//--- (2) the specified timeseries of all symbols, (3) all timeseries of the specified symbol and (4) all timeseries of all symbols
   bool                    SyncData(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool                    SyncData(const ENUM_TIMEFRAMES timeframe,const uint required=0,const int rates_total=0);
   bool                    SyncData(const string symbol,const uint required=0,const int rates_total=0);
   bool                    SyncData(const uint required=0,const int rates_total=0);

//--- Return the bar of the specified timeseries of the specified symbol of the specified position 
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true);
//--- Return the flag of opening a new bar of the specified timeseries of the specified symbol
   bool                    IsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0);

//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols

   void                    RefreshOther(void);

//--- Display (1) the complete and (2) short collection description in the journal
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
   
//--- Constructor
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

En el constructor de la clase, asignamos el identificador de la colección de series temporales a la lista de objetos de series temporales:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTimeSeriesCollection::CTimeSeriesCollection()
  {
   this.m_list.Clear();
   this.m_list.Sort();
   this.m_list.Type(COLLECTION_SERIES_ID);
  }
//+------------------------------------------------------------------+

Implementación de los métodos para retornar un objeto de barra según el índice de la serie temporal y el evento de nueva barra de la lista de la serie temporal indicada:

//+-----------------------------------------------------------------------+
//| Return the bar of the specified timeseries                            |
//| of the specified symbol of the specified position                     |
//| from_series=true - by the timeseries index, false - by the list index |
//+-----------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
  {
//--- Get the timeseries object index in the timeseries collection list by a symbol name
   int idx=this.IndexTimeSeries(symbol);
   if(idx==WRONG_VALUE)
      return NULL;
//--- Get the pointer to the timeseries object from the collection list of timeseries objects by the obtained index
   CTimeSeries *timeseries=this.m_list.At(idx);
   if(timeseries==NULL)
      return NULL;
//--- Get the specified timeseries from the symbol timeseries object by the specified timeframe
   CSeries *series=timeseries.GetSeries(timeframe);
   if(series==NULL)
      return NULL;
//--- Depending on the from_series flag, return the pointer to the bar
//--- either by the chart timeseries index or by the bar index in the timeseries list
   return(from_series ? series.GetBarBySeriesIndex(index) : series.GetBarByListIndex(index));
  }
//+------------------------------------------------------------------+
//| Return new bar opening flag                                      |
//| for a specified timeseries of a specified symbol                 |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::IsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0)
  {
//--- Get the timeseries object index in the timeseries collection list by a symbol name
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return false;
//--- Get the pointer to the timeseries object from the collection list of timeseries objects by the obtained index
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return false;
//--- Get the specified timeseries from the symbol timeseries object by the specified timeframe
   CSeries *series=timeseries.GetSeries(timeframe);
   if(series==NULL)
      return false;
//--- Return the result of checking the new bar of the specified timeseries
   return series.IsNewBar(time);
  }
//+------------------------------------------------------------------+

Implementación del método para actualizar todas las series temporales, excepto la serie temporal del símbolo actual:

//+------------------------------------------------------------------+
//| Update all timeseries except the current symbol                  |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::RefreshOther(void)
  {
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick())
         continue;
      timeseries.RefreshAll();
     }
  }
//+------------------------------------------------------------------+

En un ciclo por la lista con todos los obejtos de series temporales, obtenemos el siguiente objeto de series temporales, y si el símbolo del objeto es igual al símbolo del gráfico en el que está iniciado el programa, omitiremos este objeto de series temporales.

Tanto a este método como a los métodos de actualización de las series temporales presentados más abajo, les hemos añadido la comprobación de la bandera de nuevo tick: si no hay un tick nuevo, omitiremos la serie temporal y no actualizaremos sus datos:

//+------------------------------------------------------------------+
//| Update the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return;
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return;
   if(!timeseries.IsNewTick())
      return;
   timeseries.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread);
  }
//+------------------------------------------------------------------+
//| Update the specified timeseries of all symbols                   |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const ENUM_TIMEFRAMES timeframe,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      if(!timeseries.IsNewTick())
         continue;
      timeseries.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread);
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries of the specified symbol                    |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,
                                    const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return;
   CTimeSeries *timeseries=this.m_list.At(index);
   if(timeseries==NULL)
      return;
   if(!timeseries.IsNewTick())
      return;
   timeseries.RefreshAll(time,open,high,low,close,tick_volume,volume,spread);
  }
//+------------------------------------------------------------------+
//| Update all timeseries of all symbols                             |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const datetime time=0,
                                    const double open=0,
                                    const double high=0,
                                    const double low=0,
                                    const double close=0,
                                    const long tick_volume=0,
                                    const long volume=0,
                                    const int spread=0)
  {
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      if(!timeseries.IsNewTick())
         continue;
      timeseries.RefreshAll(time,open,high,low,close,tick_volume,volume,spread);
     }
  }
//+------------------------------------------------------------------+


En la etapa final, debemos introducir las mejoras necesarias en el archivo de la clase del objeto principal de la biblioteca CEngine.

Abrimos el archivo de la clase en la dirección \MQL5\Include\DoEasy\Engine.mqh.
En la sección privada de la clase, declaramos la variable para guardar el tipo del programa que funciona usando como base la biblioteca:

//+------------------------------------------------------------------+
//| Library basis class                                              |
//+------------------------------------------------------------------+
class CEngine
  {
private:
   CHistoryCollection   m_history;                       // Collection of historical orders and deals
   CMarketCollection    m_market;                        // Collection of market orders and deals
   CEventsCollection    m_events;                        // Event collection
   CAccountsCollection  m_accounts;                      // Account collection
   CSymbolsCollection   m_symbols;                       // Symbol collection
   CTimeSeriesCollection m_series;                       // Timeseries collection
   CResourceCollection  m_resource;                      // Resource list
   CTradingControl      m_trading;                       // Trading management object
   CArrayObj            m_list_counters;                 // List of timer counters
   int                  m_global_error;                  // Global error code
   bool                 m_first_start;                   // First launch flag
   bool                 m_is_hedge;                      // Hedge account flag
   bool                 m_is_tester;                     // Flag of working in the tester
   bool                 m_is_market_trade_event;         // Account trading event flag
   bool                 m_is_history_trade_event;        // Account history trading event flag
   bool                 m_is_account_event;              // Account change event flag
   bool                 m_is_symbol_event;               // Symbol change event flag
   ENUM_TRADE_EVENT     m_last_trade_event;              // Last account trading event
   int                  m_last_account_event;            // Last event in the account properties
   int                  m_last_symbol_event;             // Last event in the symbol properties
   ENUM_PROGRAM_TYPE    m_program;                       // Program type

En la sección pública de la clase, declaramos el método para procesar los eventos de los expertos NewTick:

//--- (1) NewTick event timer and (2) handler
   void                 OnTimer(void);
   void                 OnTick(void);

También en la sección pública, declaramos el método que retorna el objeto de barra de la serie temporal indicada del símbolo indicado según el índice de la serie temporal del gráfico,
y el método que retorna la bandera de apertura de una nueva barra de la serie temporal indicada del símbolo indicado:

//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,timeframe,required);          }
   bool                 SeriesCreate(const ENUM_TIMEFRAMES timeframe,const uint required=0)
                          { return this.m_series.CreateSeries(timeframe,required);                 }
   bool                 SeriesCreate(const string symbol,const uint required=0)
                          { return this.m_series.CreateSeries(symbol,required);                    }
   bool                 SeriesCreate(const uint required=0)
                          { return this.m_series.CreateSeries(required);                           }

//--- Return the bar of the specified timeseries of the specified symbol of the specified position
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
                          { return this.m_series.GetBar(symbol,timeframe,index,from_series);                   }
//--- Return the flag of opening a new bar of the specified timeseries of the specified symbol
   bool                 SeriesIsNewBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time=0)
                          { return this.m_series.IsNewBar(symbol,timeframe,time);                  }

//--- Update (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols

Y en la misma sección de la clase, declaramos los métodos que retornan las propiedades estándar de las barras para el símbolo indicado, la serie temporal y su posición en la serie temporal del gráfico (índice de la barra):

//--- Return (1) Open, (2) High, (3) Low, (4) Close, (5) Time, (6) TickVolume,
//--- (7) RealVolume, (8) Spread of the specified bar of the specified symbol of the specified timeframe
   double               SeriesOpen(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   double               SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   datetime             SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   long                 SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   long                 SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);
   int                  SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index);

//--- Set the following for the trading classes:
//--- (1) correct filling policy, (2) filling policy,
//--- (3) correct order expiration type, (4) order expiration type,
//--- (5) magic number, (6) comment, (7) slippage, (8) volume, (9) order expiration date,
//--- (10) the flag of asynchronous sending of a trading request, (11) logging level, (12) number of trading attempts

En el constructor de la clase, establecemos el tipo de programa ejecutado y creamos el contador del temporizador de la colección de series temporales:

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

En el manejador OnTimer() de la biblioteca, añadimos el trabajo con el temporizador de la colección de series temporales (hemos quitado el código sobrante):

//+------------------------------------------------------------------+
//| CEngine timer                                                    |
//+------------------------------------------------------------------+
void CEngine::OnTimer(void)
  {
//--- Timer of the collections of historical orders and deals, as well as of market orders and positions
//...

//--- Account collection timer
//...
     
//--- Timer 1 of the symbol collection (updating symbol quote data in the collection)
//...

//--- Timer 2 of the symbol collection (updating all data of all symbols in the collection and tracking symbl and symbol search events in the market watch window)
//...

//--- Trading class timer
//...
     
//--- Timeseries collection timer
   index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
   if(index>WRONG_VALUE)
     {
      CTimerCounter* counter=this.m_list_counters.At(index);
      if(counter!=NULL)
        {
         //--- If this is not a tester
         if(!this.IsTester())
           {
            //--- If the pause is over, work with the timeseries list (except for the current symbol timeseries)
            if(counter.IsTimeDone())
               this.m_series.RefreshOther();
           }
         //--- In case of the tester, work with the timeseries list by tick (except for the current symbol timeseries)
         else
            this.m_series.RefreshOther();
        }
     }
  }
//+------------------------------------------------------------------+

Ya analizamos el trabajo con los contadores de los temporizadores de las series temporales al crear el objeto principal de la biblioteca CEngine; en cuanto a lo demás, todo está descrito en los comentarios al código.
Una observación: en el temporizador se procesan solo aquellas series temporales cuyo símbolo no coincide con el símbolo del gráfico en el que está iniciado el programa.
Dado que aquí — en el temporizador — actualizamos las series temporales al registrar los eventos "Nuevo tick" para los símbolos "no propios", vamos a detectar precisamente estos eventos en el temporizador.
Y para actualizar las series temporales del símbolo actual, hemos creado el método OnTick(), que iniciaremos desde el manejador OnTick() del experto:

//+------------------------------------------------------------------+
//| NewTick event handler                                            |
//+------------------------------------------------------------------+
void CEngine::OnTick(void)
  {
//--- If this is not a EA, exit
   if(this.m_program!=PROGRAM_EXPERT)
      return;
//--- Update the current symbol timeseries
   this.SeriesRefresh(NULL,PERIOD_CURRENT);
  }
//+------------------------------------------------------------------+

Implementación de los métodos de obtención de las propiedades básicas de la barra indicada de la serie temporal indicada:

//+------------------------------------------------------------------+
//| Return the specified bar's Open                                  |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
double CEngine::SeriesOpen(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Open() : 0);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's High                                  |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
double CEngine::SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.High() : 0);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's Low                                   |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
double CEngine::SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Low() : 0);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's Close                                 |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
double CEngine::SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Close() : 0);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's Time                                  |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
datetime CEngine::SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's TickVolume                            |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
long CEngine::SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.VolumeTick() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's RealVolume                            |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
long CEngine::SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.VolumeReal() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return the specified bar's Spread                                |
//| of the specified symbol of the specified timeframe               |
//+------------------------------------------------------------------+
int CEngine::SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index)
  {
   CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
   return(bar!=NULL ? bar.Spread() : INT_MIN);
  }
//+------------------------------------------------------------------+

Aquí, todo es muy simple: obtenemos el objeto de barra según el símbolo y el marco temporal de la serie temporal a partir del índice establecido de la serie temporal del gráfico (0 — barra actual), y retornamos después la propiedad correspondiente de la barra.

Estas son todas las mejoras que necesitábamos hoy para implementar la actualización automática de los datos de precio de las series temporales que se usan en el programa, enviar los eventos al gráfico del programa de control y obtener los datos de las series temporales creadas en el programa.

Simulación

Vamos a comprobar el funcionamiento de la siguiente manera:
creamos tres series temporales para los marcos temporales actuales de los tres símbolos, obtenemos el objeto de la barra cero (clase CBar) desde el objeto de colección de series temporales (CTimeSeriesCollection) y mostramos en el comentario del gráfico los datos de esta barra con la ayuda de sus métodos encargados de retornar la denominación corta del objeto de barra + la descripción de los parámetros del objeto de barra. Con la segunda línea del comentario, mostramos los datos de la barra cero en un formato similar, pero creando estos con la ayuda de los métodos del objeto principal de la biblioteca CEngine que retornan los datos de la barra indicada del símbolo indicado del marco temporal indicado.

Estos datos deberán actualizarse en tiempo real en el simulador y en el gráfico en el que está iniciado el asesor.
Asimismo, vamos a implementar el procesamiento de la obtención de los eventos de los objetos de la clase CSeries encargados de enviar los eventos "Nueva barra" al gráfico del programa de control, observando que se obtengan correctamente estos eventos en el programa iniciado en el gráfico del símbolo.

Para la simulación, vamos a tomar el asesor del artículo anterior y guardarlo en la nueva carpeta
\MQL5\Experts\TestDoEasy\Part38\ con el nuevo nombre TestDoEasyPart38.mq5.

Cambiamos el manejador OnTick() del asesor de esta forma:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer();       // Working in the timer
      PressButtonsControl();  // Button pressing control
      EventsHandling();       // Working with events
     }
//--- Handle the NewTick event in the library
   engine.OnTick();

//--- If the trailing flag is set
   if(trailing_on)
     {
      TrailingPositions();    // Trailing positions
      TrailingOrders();       // Trailing of pending orders
     }
   
//--- Bet the zero bar of the current timeseries
   CBar *bar=engine.SeriesGetBar(NULL,PERIOD_CURRENT,0);
   if(bar==NULL)
      return;
//--- Create a string of parameters of the current bar similar to the one
//--- displayed by the bar object description:
//--- bar.Header()+": "+bar.ParameterDescription()
   string parameters=
     (TextByLanguage("Бар \"","Bar \"")+Symbol()+"\" "+TimeframeDescription((ENUM_TIMEFRAMES)Period())+"[0]: "+TimeToString(bar.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+
      ", O: "+DoubleToString(engine.SeriesOpen(NULL,PERIOD_CURRENT,0),Digits())+
      ", H: "+DoubleToString(engine.SeriesHigh(NULL,PERIOD_CURRENT,0),Digits())+
      ", L: "+DoubleToString(engine.SeriesLow(NULL,PERIOD_CURRENT,0),Digits())+
      ", C: "+DoubleToString(engine.SeriesClose(NULL,PERIOD_CURRENT,0),Digits())+
      ", V: "+(string)engine.SeriesTickVolume(NULL,PERIOD_CURRENT,0)+
      ", Real: "+(string)engine.SeriesRealVolume(NULL,PERIOD_CURRENT,0)+
      ", Spread: "+(string)engine.SeriesSpread(NULL,PERIOD_CURRENT,0)
     );
//--- Display the data received from the bar object in the first line of the chart comment,
//--- while the second line contains the methods of receiving timeseries price data
   Comment(bar.Header(),": ",bar.ParameterDescription(),"\n",parameters);
  }
//+------------------------------------------------------------------+

Aquí, todo es muy simple: este bloque de código supone la plantilla estándar usada al trabajar con la biblioteca DoEasy. En la implementación de hoy, hemos añadido la llamada del manejador del evento NewTick procesada por la biblioteca en cada tick. Dicho manejador se usará para actualizar las series temporales creadas. Todas las series temporales ausentes (declaradas, pero no creadas por los métodos Create()), son omitidas, es decir, no son actualizadas por la biblioteca. La llamada de este método desde el manejador OnTick() para los asesores, será obligatoria en lo sucesivo para actualizar los datos de la serie temporal actual.

A continuación, obtenemos el objeto de barra de la serie temporal del símbolo y periodo actuales, creamos la línea de descripción de los datos de la barra obtenida y mostrammos en los comentarios dos líneas:
la primera de ellas se muestra con la ayuda de los métodos del objeto de barra,
la segunda, es una línea compuesta por los datos obtenidos con la ayuda de los métodos del objeto principal de la biblioteca que retornan los datos de la barra solicitada.

En la función de inicialización de la biblioteca OnInitDoEasy(), hemos cambiado un poco el bloque de código para crear las series temporales de todos los símbolos utilizados:

//--- 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
   CArrayObj *list_timeseries=engine.GetListTimeSeries();
   if(list_timeseries!=NULL)
     {
      int total=list_timeseries.Total();
      for(int i=0;i<total;i++)
        {
         CTimeSeries *timeseries=list_timeseries.At(i);
         int total_periods=ArraySize(array_used_periods);
         for(int j=0;j<total_periods;j++)
           {
            ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_used_periods[j]);
            timeseries.SyncData(timeframe);
            timeseries.Create(timeframe);
           }
        }
     }
//--- Check created timeseries - display descriptions of all created timeseries in the journal
//--- (true - only created ones, false - created and declared ones)
   engine.GetTimeSeriesCollection().PrintShort(true); // Short descriptions
   //engine.GetTimeSeriesCollection().Print(true);      // Full descriptions


Aquí, obtenemos la lista con todas las series temporales y, en un ciclo por la lista de series temporales, obtenemos el siguiente objeto de serie temporal según el índice del ciclo. A continuación, en un ciclo por el número de marcos temporales utilizados, creamos el objeto de serie temporal necesario, realizando preliminarmente la sincronización de los datos de la serie temporal y los datos históricos.

En la función de procesamiento de eventos de la biblioteca OnDoEasyEvent(), añadimos un bloque de código para procesar los eventos de las series temporales (hemos quitado el código sobrante):

//+------------------------------------------------------------------+
//| Handling DoEasy library events                                   |
//+------------------------------------------------------------------+
void OnDoEasyEvent(const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
  {
   int idx=id-CHARTEVENT_CUSTOM;
//--- Retrieve (1) event time milliseconds, (2) reason and (3) source from lparam, as well as (4) set the exact event time
   ushort msc=engine.EventMSC(lparam);
   ushort reason=engine.EventReason(lparam);
   ushort source=engine.EventSource(lparam);
   long time=TimeCurrent()*1000+msc;
   
//--- Handling symbol events
//...  
     
//--- Handling account events
//...
     
//--- Handling market watch window events
//...
     
//--- Handling timeseries events
   else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE)
     {
      //--- "New bar" event
      if(idx==SERIES_EVENTS_NEW_BAR)
        {
         Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam));
        }
     }
     
//--- Handling trading events
//...

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

Aquí, si el identificador del evento obtenido se encuentra dentro de los identificadores de los eventos de las series temporales, y si se trata del evento "Nueva barra", mostramos un mensaje sobre el evento en el diario del terminal.

Compilamos el asesor y establecemos en sus parámetros:


Iniciamos el asesor en el gráfico. Transcurrido cierto tiempo, en el diario se muestran los mensajes sobre el evento "Nueva barra" en los símbolos utilizados para el gráfico de símbolos actual:

New bar on EURUSD M5: 2020.03.11 12:55
New bar on EURAUD M5: 2020.03.11 12:55
New bar on AUDUSD M5: 2020.03.11 12:55
New bar on EURUSD M5: 2020.03.11 13:00
New bar on AUDUSD M5: 2020.03.11 13:00
New bar on EURAUD M5: 2020.03.11 13:00

Iniciamos el asesor en el modo visual del simulador en el gráfico de uno de los símbolos seleccionados en los ajustes, por ejemplo en EURUSD, y miramos cómo cambian los datos de la barra cero en los comentarios del gráfico:


Como podemos ver, ambas líneas, cuyos datos han sido recibidos de forma distinta, tienen valores idénticos para las propiedades obtenidas de la barra cero, y se actualizan en tiempo real en cada tick.

¿Qué es lo próximo?

En el próximo artículo, corregiremos algunos errores de la versión actual de la biblioteca, que detectamos una vez finalizado el artículo, y también continuaremos desarrollando el concepto de trabajo con las series temporales, preparando la biblioteca para funcionar dentro de indicadores.

Más abajo se adjuntan todos los archivos de la versión actual de la biblioteca y los archivos del asesor de prueba. Puede descargarlo todo y ponerlo a prueba por sí mismo.
Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.

Volver al contenido

Artículos de esta serie:

Trabajando con las series temporales en la biblioteca DoEasy (Parte 35): El objeto "Barra" y la lista de serie temporal del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 36): El objeto de series temporales de todos los periodos utilizados del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 37): Colección de series temporales - Base de datos de series temporales según el símbolo y el periodo