Trabajando con las series temporales en la biblioteca DoEasy (Parte 40): Indicadores basados en la biblioteca - actualización de datos en tiempo real

31 julio 2020, 17:04
Artyom Trishkin
0
481

Contenido


Concepto

En el artículo pasado, comenzamos a trabajar el funcionamiento de la biblioteca DoEasy dentro de los indicadores. Para este tipo de programas, necesitamos un enfoque un tanto distinto en cuanto a la construcción y la actualización de las series temporales, debido a ciertas peculiaridades del cálculo económico en los indicadores y a las limitaciones en la obtención de los datos del símbolo y periodo actuales en el gráfico del indicador iniciado en este mismo gráfico.

Ya hemos logrado implementar correctamente la solicitud y la carga de los datos históricos, y ahora necesitamos crear la funcionalidad para la actualización en tiempo real de todos los datos de todas las series temporales utilizadas en el indicador (vamos a considerar que el indicador es de periodo múltiple y obtiene los datos para trabajar con los marcos temporales del gráfico en el que está iniciado).

Al construir los datos del búfer de indicador, nos desplazábamos por un ciclo desde la barra con la profundidad de datos históricos establecida hasta la barra actual (cero). Lo más sencillo en esta situación era tomar los datos según el índice del ciclo, ya que estos datos ya habían sido creados en el objeto de serie temporal de la biblioteca, y nada nos impedía obtenerlos según el índice. Pero estas son solo tres construcciones de datos estáticos. Con los problemas de indexación según el número de la barra nos encontramos al intentar actualizar en tiempo real los datos en las listas de series temporales. Al añadir una nueva barra a una lista de serie temporal, esta nueva barra tiene el índice 0, ya que es precisamente la nueva barra la que se convierte en barra cero (actual), mientras que en todas las barras construidas anteriormente en la serie temporal, todos sus índices deberán aumentar en 1 unidad. De esta forma, cada vez que se abra una nueva barra en el gráfico, deberemos añadir esta barra recién aparecida a la lista de serie temporal y aumentar en una unidad el número de las demás barras en la lista de serie temporal actualizada.

Y esto es del todo irracional. Por eso, debemos rediseñar el enfoque sobre la indiexación de las barras en la lista y su obtención desde la misma para que se implemente de acuerdo con la hora de las barras en la serie temporal: la hora de apertura de cada barra en la lista de serie temporal permanecerá inalterable, y será precisamente la hora de la barra desde donde partiremos al invocar cualquiera de las barras en la colección de series temporales de la biblioteca. Aun así, dejaremos la indexación según el número de la barra, si bien no obtendremos este desde las propiedades de las barras: al solicitar una barra según el número de la serie temporal, calcularemos la hora de la barra según el índice solicitado, y ya según la hora calculada, obtendremos la barra necesaria de la lista de serie temporal para su posterior uso.

Mejorando las clases de las series temporales

En el archivo \MQL5\Include\DoEasy\Defines.mqh de la enumeración de propiedades de tipo entero del objeto de barra, eliminamos la propiedad del índice de la barra:

//+------------------------------------------------------------------+
//| Bar integer properties                                           |
//+------------------------------------------------------------------+
enum ENUM_BAR_PROP_INTEGER
  {
   BAR_PROP_INDEX = 0,                                      // Bar index in timeseries
   BAR_PROP_TYPE,                                           // Bar type (from the ENUM_BAR_BODY_TYPE enumeration)

En su lugar, trasladamos la propiedad "Hora de la barra" y reducimos en 1 el número de propiedades de tipo entero del objeto de barra (de 14 a 13):

//+------------------------------------------------------------------+
//| Bar integer properties                                           |
//+------------------------------------------------------------------+
enum ENUM_BAR_PROP_INTEGER
  {
   BAR_PROP_TIME = 0,                                       // Bar period start time
   BAR_PROP_TYPE,                                           // Bar type (from the ENUM_BAR_BODY_TYPE enumeration)
   BAR_PROP_PERIOD,                                         // Bar period (timeframe)
   BAR_PROP_SPREAD,                                         // Bar spread
   BAR_PROP_VOLUME_TICK,                                    // Bar tick volume
   BAR_PROP_VOLUME_REAL,                                    // Bar exchange volume

   BAR_PROP_TIME_DAY_OF_YEAR,                               // Bar day serial number in a year
   BAR_PROP_TIME_YEAR,                                      // A year the bar belongs to
   BAR_PROP_TIME_MONTH,                                     // A month the bar belongs to
   BAR_PROP_TIME_DAY_OF_WEEK,                               // Bar week day
   BAR_PROP_TIME_DAY,                                       // Bar day of month (number)
   BAR_PROP_TIME_HOUR,                                      // Bar hour
   BAR_PROP_TIME_MINUTE,                                    // Bar minute
  }; 
#define BAR_PROP_INTEGER_TOTAL (13)                         // Total number of integer bar properties
#define BAR_PROP_INTEGER_SKIP  (0)                          // Number of bar properties not used in sorting
//+------------------------------------------------------------------+

Por consiguiente, en la enumeración de los posibles criterios de clasificación de las barras, debemos eliminar exactamente de la misma forma la calsificación según el índice, y poner en su lugar la clasificación según la hora de la barra:

//+------------------------------------------------------------------+
//| Possible bar sorting criteria                                    |
//+------------------------------------------------------------------+
#define FIRST_BAR_DBL_PROP          (BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP)
#define FIRST_BAR_STR_PROP          (BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP+BAR_PROP_DOUBLE_TOTAL-BAR_PROP_DOUBLE_SKIP)
enum ENUM_SORT_BAR_MODE
  {
//--- Sort by integer properties
   SORT_BY_BAR_TIME = 0,                                    // Sort by bar period start time
   SORT_BY_BAR_TYPE,                                        // Sort by bar type (from the ENUM_BAR_BODY_TYPE enumeration)
   SORT_BY_BAR_PERIOD,                                      // Sort by bar period (timeframe)
   SORT_BY_BAR_SPREAD,                                      // Sort by bar spread
   SORT_BY_BAR_VOLUME_TICK,                                 // Sort by bar tick volume
   SORT_BY_BAR_VOLUME_REAL,                                 // Sort by bar exchange volume
   SORT_BY_BAR_TIME_DAY_OF_YEAR,                            // Sort by bar day number in a year
   SORT_BY_BAR_TIME_YEAR,                                   // Sort by a year the bar belongs to
   SORT_BY_BAR_TIME_MONTH,                                  // Sort by a month the bar belongs to
   SORT_BY_BAR_TIME_DAY_OF_WEEK,                            // Sort by a bar week day
   SORT_BY_BAR_TIME_DAY,                                    // Sort by a bar day
   SORT_BY_BAR_TIME_HOUR,                                   // Sort by a bar hour
   SORT_BY_BAR_TIME_MINUTE,                                 // Sort by a bar minute
//--- Sort by real properties
   SORT_BY_BAR_OPEN = FIRST_BAR_DBL_PROP,                   // Sort by bar open price
   SORT_BY_BAR_HIGH,                                        // Sort by the highest price for the bar period
   SORT_BY_BAR_LOW,                                         // Sort by the lowest price for the bar period
   SORT_BY_BAR_CLOSE,                                       // Sort by a bar close price
   SORT_BY_BAR_CANDLE_SIZE,                                 // Sort by a candle price
   SORT_BY_BAR_CANDLE_SIZE_BODY,                            // Sort by a candle body size
   SORT_BY_BAR_CANDLE_BODY_TOP,                             // Sort by a candle body top
   SORT_BY_BAR_CANDLE_BODY_BOTTOM,                          // Sort by a candle body bottom
   SORT_BY_BAR_CANDLE_SIZE_SHADOW_UP,                       // Sort by candle upper wick size
   SORT_BY_BAR_CANDLE_SIZE_SHADOW_DOWN,                     // Sort by candle lower wick size
//--- Sort by string properties
   SORT_BY_BAR_SYMBOL = FIRST_BAR_STR_PROP,                 // Sort by a bar symbol
  };
//+------------------------------------------------------------------+

Rediseñamos la clase CBar en el archivo \MQL5\Include\DoEasy\Objects\Series\Bar.mqh para que trabaje con la hora de la barra.

Antes, el método SetSymbolPeriod() asignaba a un objeto de barra el símbolo, el periodo del gráfico y el índice indicados. Ahora, en lugar del índice, estableceremos la hora de la barra:

//--- Set (1) bar symbol, timeframe and time, (2) bar object parameters
   void              SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   void              SetProperties(const MqlRates &rates);

También corregiremos la implementación del método:

//+------------------------------------------------------------------+
//| Set bar symbol, timeframe and index                              |
//+------------------------------------------------------------------+
void CBar::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time)
  {
   this.SetProperty(BAR_PROP_TIME,time);
   this.SetProperty(BAR_PROP_SYMBOL,symbol);
   this.SetProperty(BAR_PROP_PERIOD,timeframe);
   this.m_digits=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
   this.m_period_description=TimeframeDescription(timeframe);
  }
//+------------------------------------------------------------------+

En el primer constructor paramétrico de la clase, en lugar del índice de la barra, transmitimos la hora de la barra, y, para obtener más información respecto al lugar desde donde se ha llamado el constructor de clase CBar, añadimos una variable con la que transmitiremos al constructor la descripción del método de clase en el que se llama la creación del objeto de barra:

//--- Constructors
                     CBar(){;}
                     CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time,const string source);
                     CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const MqlRates &rates);

Asimismo, corregimos la implementación del constructor: en lugar del índice de la barra, ahora usamos su hora, concretamente, añadiremos a la variable que indica el método de clase desde el que se ha llamado el constructor un texto que describa el error de obtención de los datos históricos:

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

La adición de la variable source al mensaje mostrado sobre el error de obtención de datos históricos nos permitirá encontrar la clase y el método desde el que se ha realizado el intento de creación preciso (de un nuevo objeto de barra) cuya ejecución ha causado el error de obtención de la historia.

Ahora, el segundo constructor paramétrico también opera con la hora de la barra, en lugar de usar su índice:

//+------------------------------------------------------------------+
//| Constructor 2                                                    |
//+------------------------------------------------------------------+
CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,rates.time);
   ::ResetLastError();
//--- If 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,"(2) ",symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(rates.time),": ",
         CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ",
         CMessage::Text(MSG_LIB_SYS_ERROR),"> ",CMessage::Text(err_code)," ",
         CMessage::Retcode(err_code)
        );
      //--- Set the requested bar time to the structure with zero fields
      MqlRates err={0};
      err.time=rates.time;
      this.SetProperties(err);
      return;
     }
//--- Set the bar properties
   this.SetProperties(rates);
  }
//+------------------------------------------------------------------+

En la sección pública de la clase, en el bloque de métodos de acceso simplificado a las propiedades del objeto de barra, renombramos el método Period() como Timeframe() y eliminamos el método Index() que retorna esta propiedad (ya quitada) de la barra:

//+------------------------------------------------------------------+
//| Methods of simplified access to bar object properties            |
//+------------------------------------------------------------------+
//--- Return the (1) type, (2) period, (3) spread, (4) tick, (5) exchange volume,
//--- (6) bar period start time, (7) year, (8) month the bar belongs to
//--- (9) week number since the year start, (10) week number since the month start
//--- (11) day, (12) hour, (13) minute
   ENUM_BAR_BODY_TYPE TypeBody(void)                                    const { return (ENUM_BAR_BODY_TYPE)this.GetProperty(BAR_PROP_TYPE);  }
   ENUM_TIMEFRAMES   Timeframe(void)                                    const { return (ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD);   }
   int               Spread(void)                                       const { return (int)this.GetProperty(BAR_PROP_SPREAD);               }
   long              VolumeTick(void)                                   const { return this.GetProperty(BAR_PROP_VOLUME_TICK);               }
   long              VolumeReal(void)                                   const { return this.GetProperty(BAR_PROP_VOLUME_REAL);               }
   datetime          Time(void)                                         const { return (datetime)this.GetProperty(BAR_PROP_TIME);            }
   long              Year(void)                                         const { return this.GetProperty(BAR_PROP_TIME_YEAR);                 }
   long              Month(void)                                        const { return this.GetProperty(BAR_PROP_TIME_MONTH);                }
   long              DayOfWeek(void)                                    const { return this.GetProperty(BAR_PROP_TIME_DAY_OF_WEEK);          }
   long              DayOfYear(void)                                    const { return this.GetProperty(BAR_PROP_TIME_DAY_OF_YEAR);          }
   long              Day(void)                                          const { return this.GetProperty(BAR_PROP_TIME_DAY);                  }
   long              Hour(void)                                         const { return this.GetProperty(BAR_PROP_TIME_HOUR);                 }
   long              Minute(void)                                       const { return this.GetProperty(BAR_PROP_TIME_MINUTE);               }
   long              Index(void)                                        const { return this.GetProperty(BAR_PROP_INDEX);                     }

Ahora, el método Index() no retornará la propiedad inexistente del objeto de barra, sino el valor calculado según la hora de la barra:

//--- Return bar symbol
   string            Symbol(void)                                       const { return this.GetProperty(BAR_PROP_SYMBOL);                    }
//--- Return bar index on the specified timeframe the bar time falls into
   int               Index(const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT)  const
                       { return ::iBarShift(this.Symbol(),(timeframe>PERIOD_CURRENT ? timeframe : this.Timeframe()),this.Time());            }  
//+------------------------------------------------------------------+

El método retorna el índice de la barra de la serie temporal actual para el marco temporal indicado en el parámetro de entrada del método calculado por la función iBarShift().

En el método que retorna la denominación breve del objeto de barra, llamamos ahora al método que acabamos de analizar con el valor PERIOD_CURRENT por defecto, lo cual retorna el índice para la serie temporal a la que pertenece el objeto de barra:

//+------------------------------------------------------------------+
//| Return the bar object short name                                 |
//+------------------------------------------------------------------+
string CBar::Header(void)
  {
   return
     (
      CMessage::Text(MSG_LIB_TEXT_BAR)+" \""+this.GetProperty(BAR_PROP_SYMBOL)+"\" "+
      TimeframeDescription((ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD))+"["+(string)this.Index()+"]"
     );
  }
//+------------------------------------------------------------------+

En el método que retorna la descripción de una propiedad de tipo entero del objeto de barra, eliminamos el bloque que retorna la descripción del índice de la barra:

//+------------------------------------------------------------------+
//| Return the description of the bar integer property               |
//+------------------------------------------------------------------+
string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER property)
  {
   return
     (
      property==BAR_PROP_INDEX               ?  CMessage::Text(MSG_LIB_TEXT_BAR_INDEX)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_TYPE                ?  CMessage::Text(MSG_ORD_TYPE)+

En su lugar, colocamos un bloque de código que retorna la hora de la barra (listado completo del método):

//+------------------------------------------------------------------+
//| Return the description of the bar integer property               |
//+------------------------------------------------------------------+
string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER property)
  {
   return
     (
      property==BAR_PROP_TIME                ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+::TimeToString(this.GetProperty(property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
         )  :
      property==BAR_PROP_TYPE                ?  CMessage::Text(MSG_ORD_TYPE)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.BodyTypeDescription()
         )  :
      property==BAR_PROP_PERIOD              ?  CMessage::Text(MSG_LIB_TEXT_BAR_PERIOD)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.m_period_description
         )  :
      property==BAR_PROP_SPREAD              ?  CMessage::Text(MSG_LIB_TEXT_BAR_SPREAD)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_VOLUME_TICK         ?  CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_TICK)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_VOLUME_REAL         ?  CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_REAL)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==BAR_PROP_TIME_YEAR           ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_YEAR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.Year()
         )  :
      property==BAR_PROP_TIME_MONTH          ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MONTH)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+MonthDescription((int)this.Month())
         )  :
      property==BAR_PROP_TIME_DAY_OF_YEAR    ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_YEAR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.DayOfYear(),3,'0')
         )  :
      property==BAR_PROP_TIME_DAY_OF_WEEK    ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_WEEK)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+DayOfWeekDescription((ENUM_DAY_OF_WEEK)this.DayOfWeek())
         )  :
      property==BAR_PROP_TIME_DAY            ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Day(),2,'0')
         )  :
      property==BAR_PROP_TIME_HOUR           ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_HOUR)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Hour(),2,'0')
         )  :
      property==BAR_PROP_TIME_MINUTE         ?  CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MINUTE)+
         (!this.SupportProperty(property)    ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)::IntegerToString(this.Minute(),2,'0')
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Con esto, podemos dar por finalizados los cambios en la clase del objeto de barra.

Si miramos atentamente la lista de clases de la biblioteca estándar, veremos que en la dirección MQL5\Include\Indicators\ hay dos archivos: Series.mqh y TimeSeries.mqh.
En la biblioteca, también tenemos los archivos de clase homónimos, y eso no es correcto. Vamos a renombrar nuestras dos clases: para ello, añadiremos a sus nombres y a los nombres de sus archivos la abreviatura DE (de DoEasy) y corregiremos su nombre en todos los lugares donde se encuentren las invocaciones a estos archivos y clases. Estos cambios han afectado a tres archivos: Series.mqh (renombrado ahora como SeriesDE.mqh y la clase CSeriesDE), TimeSeries.mqh (renombrado ahora como TimeSeriesDE.mqh y la clase CTimeSeriesDE) y TimeSeriesCollection.mqh (usa ambas clases renombradas). Vamos a analizar todos estos archivos y sus clases por orden.

El archivo Series.mqh ahora se guarda con el nuevo nombre \MQL5\Include\DoEasy\Objects\Series\SeriesDE.mqh, y el nombre de la clase se ha convertido de la forma correspondiente:

//+------------------------------------------------------------------+
//| Timeseries class                                                 |
//+------------------------------------------------------------------+
class CSeriesDE : public CBaseObj
  {
private:

Por consiguiente, también el método que retorna el objeto de esta clase tiene un nuevo tipo de clase:

public:
//--- Return (1) oneself and (2) the timeseries list
   CSeriesDE        *GetObject(void)                                    { return &this;         }

El método público que retorna un objeto de barra según su índice, al igual que en la serie temporal GetBarBySeriesIndex, ahora ha sido renombrado simplemente como GetBar(). Asimismo, añadiremos otro método igual para retornar un objeto de barra según su hora de apertura en la serie temporal:

//--- Return the bar object by (1) a real index in the list, (2) an index as in the timeseries, (3) time and (4) the real list size
   CBar             *GetBarByListIndex(const uint index);
   CBar             *GetBar(const uint index);
   CBar             *GetBar(const datetime time);
   int               DataTotal(void)                                       const { return this.m_list_series.Total();               }

De esa forma, ahora tendremos dos métodos sobrecargados para retornar un objeto de barra: según la hora y según el índice.

Implementación del método para retornar un objeto de barra según su hora de apertura:

//+------------------------------------------------------------------+
//| Return the bar object by time in the timeseries                  |
//+------------------------------------------------------------------+
CBar *CSeriesDE::GetBar(const datetime time)
  {
   CBar *obj=new CBar(this.m_symbol,this.m_timeframe,time,DFUN_ERR_LINE); 
   if(obj==NULL)
      return NULL;
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   int index=this.m_list_series.Search(obj);
   delete obj;
   CBar *bar=this.m_list_series.At(index);
   return bar;
  }
//+------------------------------------------------------------------+

Transmitimos al método la hora según la cual debemos encontrar y retornar el objeto de barra correspondiente.
Asimismo, creamos un objeto de barra temporal para la serie temporal actual con una propiedad de hora igual a la transmitida al método.
Establecemos la bandera de clasificación de la lista de objetos de barra según la hora
y buscamos en la lista un objeto de barra con una propiedad de hora igual a la transmitida al método.
Como resultado de la búsqueda, se nos retornará el índice de la barra en la lista, si este es localizado, o bien -1, si no existiera.
Eliminamos el objeto de barra temporal y obtenemos la barra necesaria de la lista según el índice obtenido. Si el índice es inferior a cero, el método At() de la clase CArrayObj retornará NULL.
Retornamos del método, o bien el objeto de barra, si el objeto ha sido localizado según la hora, o bien NULL
.

Implementación del método para retornar un objeto de barra según su índice:

//+------------------------------------------------------------------+
//| Return the bar object by index as in the timeseries              |
//+------------------------------------------------------------------+
CBar *CSeriesDE::GetBar(const uint index)
  {
   datetime time=::iTime(this.m_symbol,this.m_timeframe,index);
   if(time==0)
      return NULL;
   return this.GetBar(time);
  }
//+------------------------------------------------------------------+

Transmitimos al método el índice de la barra buscada.
Usando la función iTime(), obtenemos la hora de la barra según el índice
y retornamos el resultado del funcionamiento del método GetBar() (que analizamos anteriormente), encargado de retornar un objeto de barra según la hora obtenida.

En la sección pública de la clase, junto con los métodos que retornan las propiedades principales de una barra según su índice, declaramos los métodos que retornan estas mismas propiedades según la hora de la barra:

//--- Return (1) Open, (2) High, (3) Low, (4) Close, (5) time, (6) tick volume, (7) real volume, (8) bar spread by index
   double            Open(const uint index,const bool from_series=true);
   double            High(const uint index,const bool from_series=true);
   double            Low(const uint index,const bool from_series=true);
   double            Close(const uint index,const bool from_series=true);
   datetime          Time(const uint index,const bool from_series=true);
   long              TickVolume(const uint index,const bool from_series=true);
   long              RealVolume(const uint index,const bool from_series=true);
   int               Spread(const uint index,const bool from_series=true);

//--- Return (1) Open, (2) High, (3) Low, (4) Close, (5) time, (6) tick volume, (7) real volume, (8) bar spread by index
   double            Open(const datetime time);
   double            High(const datetime time);
   double            Low(const datetime time);
   double            Close(const datetime time);
   datetime          Time(const datetime time);
   long              TickVolume(const datetime time);
   long              RealVolume(const datetime time);
   int               Spread(const datetime time);

Un poco más tarde, analizaremos la implementación de los métodos declarados.

En ese mismo lugar —en la sección pública de la clase—, declaramos el método que permite registrar los datos indicados de un objeto de barra en la matriz transmitida al método:

//--- (1) Create and (2) update the timeseries list
   int               Create(const uint required=0);
   void              Refresh(SDataCalculate &data_calculate);
//--- Copy the specified double property of the timeseries to the array
//--- Regardless of the array indexing direction, copying is performed the same way as copying to a timeseries array
   bool              CopyToBufferAsSeries(const ENUM_BAR_PROP_DOUBLE property,double &array[],const double empty=EMPTY_VALUE);

//--- Create and send the "New bar" event to the control program chart
   void              SendEvent(void);


Supongamos que necesitamos registrar de una sola vez en un búfer de indicador los datos de una serie temporal. El objeto de barra puede contener multitud de propiedades diferentes, tanto de tipo entero, como de tipo real. Con la ayuda de este método, podemos registrar en la matriz cualquiera de las propiedades de tipo real del objeto de barra enumeradas. En este caso, además, todos los datos se registrarán en la matriz como en una matriz de serie temporal: los datos de la barra actual que se guardan en el objeto de serie temporal al final de la lista se registrarán en el índice cero de la matriz receptora, es decir, el registro se realizará de adelante hacia atrás.

Veamos su implementación:

//+------------------------------------------------------------------+
//| Copy the specified double property of the timeseries to the array|
//+------------------------------------------------------------------+
bool CSeriesDE::CopyToBufferAsSeries(const ENUM_BAR_PROP_DOUBLE property,double &array[],const double empty=EMPTY_VALUE)
  {
//--- Get the number of bars in the timeseries list
   int total=this.m_list_series.Total();
   if(total==0)
      return false;
//--- If a dynamic array is passed to the method and its size is not equal to that of the timeseries list,
//--- set the new size of the passed array equal to that of the timeseries list
   if(::ArrayIsDynamic(array) && ::ArraySize(array)!=total)
      if(::ArrayResize(array,total,this.m_required)==WRONG_VALUE)
         return false;
//--- In the loop from the very last timeseries list element (from the current bar)
   int n=0;
   for(int i=total-1;i>WRONG_VALUE && !::IsStopped();i--)
     {
      //--- get the next bar object by the loop index,
      CBar *bar=this.m_list_series.At(i);
      //--- calculate the index, based on which the bar property is saved to the passed array
      n=total-1-i;
      //--- write the value of the obtained bar property using the calculated index
      //--- if the bar is not received or the property is equal to zero, write the value passed to the method as "empty" to the array
      array[n]=(bar==NULL ? empty : (bar.GetProperty(property)>0 && bar.GetProperty(property)<EMPTY_VALUE ? bar.GetProperty(property) : empty));
     }
   return true;
  }
//+------------------------------------------------------------------+

Como podemos ver, aquí se calcula el índice de la matriz receptora de tal forma que el último valor de la matriz fuente se encuentre en la celda cero de la matriz receptora. De esta forma, nuestra lista de serie temporal (la propiedad solicitada de la barra) se registrará en la matriz (por ejemplo, el búfer de indicador) en el orden de numeración en el gráfico del símbolo, mientras que los objetos de barra en la lista de series temporales se ubicarán en orden inverso: la barra con la última hora (barra actual) se ubicará al final de la lista. Esto nos permitirá copiar rápidamente en el búfer de indicador las propiedades de todas las barras de la lista de series temporales, en el caso de que el marco temporal de la serie temporal copiada coincida con el marco temporal del gráfico para el cual copiamos la serie temporal en el búfer con la ayuda de este método.

En ambos constructores de clase, establecemos la bandera de clasificación de la lista de serie temporal según la hora de las barras:

//+------------------------------------------------------------------+
//| Constructor 1 (current symbol and period timeseries)             |
//+------------------------------------------------------------------+
CSeriesDE::CSeriesDE(void) : m_bars(0),m_amount(0),m_required(0),m_sync(false)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   this.SetSymbolPeriod(NULL,(ENUM_TIMEFRAMES)::Period());
   this.m_period_description=TimeframeDescription(this.m_timeframe);
  }
//+------------------------------------------------------------------+
//| Constructor 2 (specified symbol and period timeseries)           |
//+------------------------------------------------------------------+
CSeriesDE::CSeriesDE(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0) : m_bars(0), m_amount(0),m_required(0),m_sync(false)
  {
   this.m_list_series.Clear();
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
   this.SetSymbolPeriod(symbol,timeframe);
   this.m_sync=this.SetRequiredUsedData(required,0);
   this.m_period_description=TimeframeDescription(this.m_timeframe);
  }
//+------------------------------------------------------------------+

En el método para crear la lista de series temporales, cambiamos el tipo de clasificación, usando la clasificación según la hora, en lugar de la clasificación según el índice y completamos el texto mostrado al cometer errores de creación del objeto de barra y su adición a la lista de serie temporal:

//+------------------------------------------------------------------+
//| 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
      if(!this.m_list_series.Add(bar))
        {
         err=::GetLastError();
         ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_ADD_TO_LIST)," ",bar.Header()," ",::TimeToString(rates[i].time),". ",
                      CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(err),CMessage::Retcode(err));
        }
     }
//--- Return the size of the created bar object list
   return this.m_list_series.Total();
  }
//+------------------------------------------------------------------+

El método de actualización de la lista y los datos de la serie temporal también lo hemos mejorado un poco:

//+------------------------------------------------------------------+
//| Update timeseries list and data                                  |
//+------------------------------------------------------------------+
void CSeriesDE::Refresh(SDataCalculate &data_calculate)
  {
//--- If the timeseries is not used, exit
   if(!this.m_available)
      return;
   MqlRates rates[1];
//--- Set the flag of sorting the list of bars by time
   this.m_list_series.Sort(SORT_BY_BAR_TIME);
//--- If a new bar is present on a symbol and period,
   if(this.IsNewBarManual(data_calculate.rates.time))
     {
      //--- create a new bar object and add it to the end of the list
      CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,this.m_new_bar_obj.TimeNewBar(),DFUN_ERR_LINE);
      if(new_bar==NULL)
         return;
      if(!this.m_list_series.InsertSort(new_bar))
        {
         delete new_bar;
         return;
        }
      //--- Write the very first date by a period symbol at the moment and the new time of opening the last bar by a period symbol 
      this.SetServerDate();
      //--- if the timeseries exceeds the requested number of bars, remove the earliest bar
      if(this.m_list_series.Total()>(int)this.m_required)
         this.m_list_series.Delete(0);
      //--- save the new bar time as the previous one for the subsequent new bar check
      this.SaveNewBarTime(data_calculate.rates.time);
     }
     
//--- Get the bar index with the maximum time (zero bar) and bar object from the list by the obtained index
   int index=CSelect::FindBarMax(this.GetList(),BAR_PROP_TIME);
   CBar *bar=this.m_list_series.At(index);
   if(bar==NULL)
      return;
//--- if the work is performed in an indicator and the timeseries belongs to the current symbol and timeframe,
//--- copy price parameters (passed to the method from the outside) to the bar price structure
   int copied=1;
   if(this.m_program==PROGRAM_INDICATOR && this.m_symbol==::Symbol() && this.m_timeframe==(ENUM_TIMEFRAMES)::Period())
     {
      rates[0].time=data_calculate.rates.time;
      rates[0].open=data_calculate.rates.open;
      rates[0].high=data_calculate.rates.high;
      rates[0].low=data_calculate.rates.low;
      rates[0].close=data_calculate.rates.close;
      rates[0].tick_volume=data_calculate.rates.tick_volume;
      rates[0].real_volume=data_calculate.rates.real_volume;
      rates[0].spread=data_calculate.rates.spread;
     }
//--- otherwise, get data to the bar price structure from the environment
   else
      copied=::CopyRates(this.m_symbol,this.m_timeframe,0,1,rates);
//--- If the prices are obtained, set the new properties from the price structure for the bar object
   if(copied==1)
      bar.SetProperties(rates[0]);
  }
//+------------------------------------------------------------------+

Aquí, la clasificación de la lista también se ha establecido según la hora. Al crear un nuevo objeto de barra, transmitimos al constructor de clase la hora de la barra desde el objeto "Nueva barra", ya que añadimos una nueva barra a la lista solo en el momento en que se determina el hecho de la apertura de una nueva barra, y en el objeto "Nueva barra" ya se encuentra la hora de apertura de esta barra, que precisamente transmitimos al constructor. Y para completar, trasnmitimos al constructor la descripción del método en el que se crea el nuevo objeto de barra. Si el objeto de barra se crea de forma errónea, desde el constructor del objeto se mostrará un mensaje en el diario, en el que se escribirá el método CSeriesDE::Refresh y la línea de código desde la que se ha llamado el constructor de la clase CBar.
Para obtener con toda certeza de la lista de serie temporal la última barra (actual), la localizaremos según la hora máxima de todos los objetos de barra que se hallan en la lista de serie temporal. Para ello, primero encontraremos el índice del objeto de barra con la hora máxima con la ayuda del método FindBarMax() de la clase CSelect, y ya según el índice obtenido, tomaremos de la lista la última barra: ella será la actual. Si por algún motivo no obtenemos el índice de la barra actual, el valor del índice será -1, mientras que al obtener un elemento de la lista con el método At(), si se da un índice negativo, se nos retornará el valor NULL, y si verdaderamente es tal, simplemente saldremos del método.

Métodos para retornar las propiedades descritas de un objeto de barra según la hora:

//+------------------------------------------------------------------+
//| Return bar's Open by time                                        |
//+------------------------------------------------------------------+
double CSeriesDE::Open(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Open() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar's High by time                                        |
//+------------------------------------------------------------------+
double CSeriesDE::High(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.High() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar's Low by time                                         |
//+------------------------------------------------------------------+
double CSeriesDE::Low(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Low() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar's Close by time                                       |
//+------------------------------------------------------------------+
double CSeriesDE::Close(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Close() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar time by time                                          |
//+------------------------------------------------------------------+
datetime CSeriesDE::Time(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Return bar tick volume by time                                   |
//+------------------------------------------------------------------+
long CSeriesDE::TickVolume(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.VolumeTick() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar real volume by time                                   |
//+------------------------------------------------------------------+
long CSeriesDE::RealVolume(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.VolumeReal() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Return bar spread by time                                        |
//+------------------------------------------------------------------+
int CSeriesDE::Spread(const datetime time)
  {
   CBar *bar=this.GetBar(time);
   return(bar!=NULL ? bar.Spread() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+

Todos están construidos de la misma forma:
obtenemos el objeto de barra de la lista de serie temporal según la hora
y retornamos el valor de la propiedad correspondiente teniendo en cuenta el error de obtención del objeto de barra.

El método de creación y envío del evento "Nueva barra" al gráfico del programa de control también ha sido mejorado, considerando que necesitamos obtener el objeto de barra actual según la hora:

//+------------------------------------------------------------------+
//| Create and send the "New bar" event                              |
//| to the control program chart                                     |
//+------------------------------------------------------------------+
void CSeriesDE::SendEvent(void)
  {
   int index=CSelect::FindBarMax(this.GetList(),BAR_PROP_TIME);
   CBar *bar=this.m_list_series.At(index);
   if(bar==NULL)
      return;
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,bar.Time(),this.Timeframe(),this.Symbol());
  }
//+------------------------------------------------------------------+

Aquí, exactamente de la misma forma que en el método Refresh(), obtenemos el objeto de barra actual de la lista de serie temporal; después, transmitimos la hora de esta barra al parámetro lparam al enviar el evento de usuario al gráfico del programa de control.

Ya hemos terminado con la clase de serie temporal. Ahora, vamos a mejorar la clase de todas las series temporales de un símbolo.

Como ya hemos mencionado antes, esta clase CTimeSerirs puede entrar en conflicto con la clase homónima de la biblioteca estándar. Por eso, ya la hemos renombrado como CTimeSerirsDE. Por consiguiente, dentro del listado de la clase, hemos sustituido todas las entradas de la línea "CTimeSerirs" por la línea "CTimeSerirsDE", y también todas las entradas de la línea "CSerirs" por la línea "CSerirsDE"; no vamos a analizar estos cambios aquí. Tan solo, como ejemplo:

//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "SeriesDE.mqh"
#include "..\Ticks\NewTickObj.mqh"
//+------------------------------------------------------------------+
//| Symbol timeseries class                                          |
//+------------------------------------------------------------------+
class CTimeSeriesDE : public CBaseObjExt
  {
private:

En la sección pública de la clase, declaramos el método para copiar en la matriz transmitida la propiedad de tipo real indicada de las barras de la serie temporal indicada:

//--- Copy the specified double property of the specified timeseries to the array
//--- Regardless of the array indexing direction, copying is performed the same way as copying to a timeseries array
   bool              CopyToBufferAsSeries(const ENUM_TIMEFRAMES timeframe,
                                          const ENUM_BAR_PROP_DOUBLE property,
                                          double &array[],
                                          const double empty=EMPTY_VALUE);

//--- Compare CTimeSeriesDE 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
                     CTimeSeriesDE(void){;}
                     CTimeSeriesDE(const string symbol);
  };
//+------------------------------------------------------------------+

Este método ya lo hemos visto antes, al mejorar la clase CSeriesDE. Vamos a analizar la implementación del método:

//+------------------------------------------------------------------+
//| Copy the specified double property of the specified timeseries   |
//+------------------------------------------------------------------+
bool CTimeSeriesDE::CopyToBufferAsSeries(const ENUM_TIMEFRAMES timeframe,
                                         const ENUM_BAR_PROP_DOUBLE property,
                                         double &array[],
                                         const double empty=EMPTY_VALUE)
  {
   CSeriesDE *series=this.GetSeries(timeframe);
   if(series==NULL)
      return false;
   return series.CopyToBufferAsSeries(property,array,empty);
  }
//+------------------------------------------------------------------+

Aquí, todo es sencillo: primero obtenemos la serie temporal necesaria según el marco temporal indicado, y después retornamos el resultado de la llamada de este método desde el objeto de serie temporal obtenido.

En el método que retorna el índice de la serie temporal en la lista de todas las series temporales del símbolo según el marco temporal, introducimos la comprobación del marco temporal indicado para la búsqueda:

//+------------------------------------------------------------------+
//| Return the timeframe index in the list                           |
//+------------------------------------------------------------------+
int CTimeSeriesDE::IndexTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   const CSeriesDE *obj=new CSeriesDE(this.m_symbol,(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe));
   if(obj==NULL)
      return WRONG_VALUE;
   this.m_list_series.Sort();
   int index=this.m_list_series.Search(obj);
   delete obj;
   return index;
  }
//+------------------------------------------------------------------+

Al crear un objeto temporal para la búsqueda, comprobamos el valor introducido del marco temporal, y si se ha introducido el valor CURRENT_PERIOD, para la búsqueda usaremos el marco temporal actual.

En el método de actualización de la lista de serie temporal indicada, al añadir un nuevo evento a la lista de eventos, usaremos la hora de apertura de la nueva barra de la estructura data_calculate como valor del parámetro lparam:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the timeseries event flag and clear the list of all timeseries events
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the timeseries from the list by its timeframe
   CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Update the timeseries list
   series_obj.Refresh(data_calculate);
//--- If the timeseries object features the New bar event
   if(series_obj.IsNewBar(data_calculate.rates.time))
     {
      //--- send the "New bar" event to the control program chart
      series_obj.SendEvent();
      //--- set the values of the first date in history on the server and in the terminal
      this.SetTerminalServerDate();
      //--- add the "New bar" event to the list of timeseries events
      //--- in case of successful addition, set the event flag for the timeseries
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(data_calculate.rates.time),series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+

Ya hemos terminado con la clase CTimeSeriesDE. Vamos a pasar a la clase del objeto de colección de los objetos de todas las series temporales de todos los símbolos CTimeSeriesCollection.

En estos momentos, hemos renombrado dos clases: CSeriesDE y CTimeSerirsDE. Por consiguiente, dentro del listado de la clase CTimeSeriesCollection, sustituimos todas las entradas de la línea "CTimeSerirs" por la línea "CTimeSerirsDE", y todas las entradas de la línea "CSerirs" por la línea "CSerirsDE".
No vamos a analizar aquí estas sustituciones. Tan solo, como ejemplo:

//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Objects\Series\TimeSeriesDE.mqh"
#include "..\Objects\Symbols\Symbol.mqh"
//+------------------------------------------------------------------+
//| Symbol timeseries collection                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CBaseObjExt
  {
private:
   CListObj                m_list;                    // List of applied symbol timeseries
//--- Return the timeseries index by symbol name
   int                     IndexTimeSeries(const string symbol);
public:
//--- Return (1) oneself and (2) the timeseries list
   CTimeSeriesCollection  *GetObject(void)            { return &this;         }
   CArrayObj              *GetList(void)              { return &this.m_list;  }
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
   CTimeSeriesDE          *GetTimeseries(const string symbol);
   CSeriesDE              *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

//--- Create the symbol timeseries list collection

En la sección pública de la clase, declaramos los tres nuevos métodos:
un método que retorna el objeto de barra de la serie temporal indicada del símbolo indicado según la hora de apertura de la barra,
y dos métodos que retornan el objeto de barra de la serie temporal que se corresponde con la hora de apertura de esta barra en otra serie temporal según el índice de la barra y la hora de la barra:

//--- Return the bar object of the specified timeseries of the specified symbol of the specified position (1) by index, (2) by time
//--- bar object of the first timeseries corresponding to the bar open time on the second timeseries (3) by index, (4) by time
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true);
   CBar                   *GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime bar_time);
   CBar                   *GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                             const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);
   CBar                   *GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime first_bar_time,
                                                             const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);

Asimismo, declaramos en la sección pública otros dos métodos: el método para actualizar todas las series temporales del símbolo indicado y el método que copia en una matriz la propiedad double indicada de la serie temporal indicada del símbolo indicado:

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of the specified symbol, (3) all timeseries of all symbols
   void                    Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void                    Refresh(const string symbol,SDataCalculate &data_calculate);
   void                    Refresh(SDataCalculate &data_calculate);

//--- Get events from the timeseries object and add them to the list
   bool                    SetEvents(CTimeSeriesDE *timeseries);

//--- Display (1) the complete and (2) short collection description in the journal
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
//--- Copy the specified double property of the specified timeseries of the specified symbol to the array
//--- Regardless of the array indexing direction, copying is performed the same way as copying to a timeseries array
   bool                    CopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                                const ENUM_BAR_PROP_DOUBLE property,
                                                double &array[],
                                                const double empty=EMPTY_VALUE);
//--- Constructor
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

Implementación del método que retorna el objeto de barra de la serie temporal indicada del símbolo indicado de la posición indicada según la hora:

//+------------------------------------------------------------------+
//| Return the bar object of the specified timeseries                |
//| of the specified symbol of the specified position by time        |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime bar_time)
  {
   CSeriesDE *series=this.GetSeries(symbol,timeframe);
   if(series==NULL)
      return NULL;
   return series.GetBar(bar_time);
  }
//+------------------------------------------------------------------+

Al método se transmiten el símbolo y el marco temporal de la serie temporal de la que debemos obtener la barra con la hora de apertura indicada.

Obtenemos el objeto de serie temporal con el símbolo y marco temporal indicados y retornamos el objeto de barra tomado de la serie temporal obtenida según la hora de la barra.
Si no hemos logrado obtener la barra, retornamos NULL.

Implementación del método que retorna el objeto de barra de la primera serie temporal según el índice que se corresponde con la hora de apertura de la barra en la segunda serie temporal:

//+------------------------------------------------------------------+
//| Return the bar object of the first timeseries by index           |
//| corresponding to the bar open time on the second timeseries      |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                               const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
  {
   CBar *bar_first=this.GetBar(symbol_first,timeframe_first,index);
   if(bar_first==NULL)
      return NULL;
   CBar *bar_second=this.GetBar(symbol_second,timeframe_second,bar_first.Time());
   return bar_second;
  }
//+------------------------------------------------------------------+

Al método se transmiten el símbolo y el marco temporal del primer gráfico, el índice de la barra en el primer gráfico, y el símbolo y el periodo del segundo gráfico.

Obtenemos el primer objeto de barra de la serie temporal del primer símbolo-periodo según el índice establecido,
y obtenemos y retornamos el segundo objeto de barra del segundo símbolo-periodosegún la hora de la primera barra obtenida
.

El método permite obtener la posición de la barra indicada según el índice en el primer símbolo-periodo indicado del gráfico, que coincida según la hora de apertura con la posición de la barra en el segundo símbolo-periodo indicado del gráfico.
¿Y qué nos da eso? Podemos, por ejemplo, marcar con rapidez en el gráfico М15 todas las barras Н1.
Basta con transmitir al método el símbolo actual, el period del gráfico М15, la posición de la barra según su índice en el gráfico (supongamos, el índice del ciclo de cálculo del indicador), el símbolo actual y el periodo Н1. Y el método retornará el objeto de barra del gráfico del símbolo actual y el periodo Н1, cuya hora de apertura incluye la hora de apertura de la primera barra indicada.

Implementación del método que retorna el objeto de barra de la primera serie temporal según la hora que se corresponde con la hora de apertura de la barra en la segunda serie temporal:

//+------------------------------------------------------------------+
//| Return the bar object of the first timeseries by time            |
//| corresponding to the bar open time on the second timeseries      |
//+------------------------------------------------------------------+
CBar *CTimeSeriesCollection::GetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime first_bar_time,
                                                               const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
  {
   CBar *bar_first=this.GetBar(symbol_first,timeframe_first,first_bar_time);
   if(bar_first==NULL)
      return NULL;
   CBar *bar_second=this.GetBar(symbol_second,timeframe_second,bar_first.Time());
   return bar_second;
  }
//+------------------------------------------------------------------+

El método es idéntico al método de obtención del objeto de barra según su índice, que acabamos de analizar. Aquí, en lugar del índice de la barra en la serie temporal, se indica su hora de apertura en la primera serie temporal indicada.

Como podemos notar, a ambos métodos se transmiten no solo los periodos de los dos gráficos, sino también sus símbolos. Y esto significa que estos métodos pueden retornar el objeto de barra de cualquier símbolo-periodo que se corresponda con el objeto de barra del primer símbolo-periodo con su posición indicada en la serie temporal. Esto permite confrontar fácilmente dos barras de cualquier símbolo-periodo para comparar cualquiera de sus propiedades del objeto de barra.

Vamos a añadir al método de actualización de la serie temporal indicada del símbolo indicado la comprobación de "símbolo ajeno":

//+------------------------------------------------------------------+
//| Update the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the object of all symbol timeseries by a symbol name
   CTimeSeriesDE *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- If a symbol is non-native and there is no new tick on the timeseries object symbol, exit
   if(symbol!=::Symbol() && !timeseries.IsNewTick())
      return;
//--- Update the required object timeseries of all symbol timeseries
   timeseries.Refresh(timeframe,data_calculate);
//--- If the timeseries has the enabled event flag,
//--- get events from symbol timeseries, write them to the collection event list
//--- and set the event flag in the collection
   if(timeseries.IsEvent())
      this.m_is_event=this.SetEvents(timeseries);
  }
//+------------------------------------------------------------------+

¿Y para qué necesitamos esta propiedad? La cosa es que nosotros actualizamos en el temporizador de la biblioteca todas las series temporales que no pertenecen al símbolo-periodo actual. Y la actualización de las series temporales que pertenecen al símbolo en el que está iniciado el programa se debe realizar desde el manejador del evento Start, NewTick o Calculate del programa. Por eso, para no comprobar en el temporizador el evento de un nuevo tick para el símbolo actual (la serie temporal del símbolo actual ya se actualiza según el tick), vamos a comparar si el símbolo de la serie temporal coincide con su símbolo actual y a comprobar el evento de serie temporal "nuevo tick" solo si la serie temporal pertenece al símbolo actual.

Implementación del método para actualizar todas las series temporales del símbolo indicado:

//+------------------------------------------------------------------+
//| Update all timeseries of the specified symbol                    |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the object of all symbol timeseries by a symbol name
   CTimeSeriesDE *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- If a symbol is non-native and there is no new tick on the timeseries object symbol, exit
   if(symbol!=::Symbol() && !timeseries.IsNewTick())
      return;
//--- Update all object timeseries of all symbol timeseries
   timeseries.RefreshAll(data_calculate);
//--- If the timeseries has the enabled event flag,
//--- get events from symbol timeseries, write them to the collection event list
//--- and set the event flag in the collection
   if(timeseries.IsEvent())
      this.m_is_event=this.SetEvents(timeseries);
  }
//+------------------------------------------------------------------+

Aquí, cada línea de la lógica va acompañada de comentarios al código, por lo que, esperamos, todo resultará comprensible para el lector.

Implementación del método que registra los datos indicados de tipo entero de la barra del objeto de serie temporal indicado en la matriz transmitida al método:

//+------------------------------------------------------------------+
//| Copy the specified double property to the array                  |
//| for a specified timeseries of a specified symbol                 |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::CopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,
                                                 const ENUM_BAR_PROP_DOUBLE property,
                                                 double &array[],
                                                 const double empty=EMPTY_VALUE)
  {
   CSeriesDE *series=this.GetSeries(symbol,timeframe);
   if(series==NULL)
      return false;
   return series.CopyToBufferAsSeries(property,array,empty);
  }
//+------------------------------------------------------------------+

Ya analizamos anteriormente el funcionamiento del método, al mejorar la clase CSeriesDE.
Aquí, nos limitaremos a obtener el objeto de serie temporal necesario según el símbolo y periodo indicados, y retornar el resultado de la llamada del método homónimo de la serie temporal obtenida.

Ya hemos terminado con la clase de colecciones de series temporales.

Ahora, debemos proporcionar acceso a los nuevos métodos creados desde los programas que funcionan usando como base la biblioteca. Y será el objeto principal de la biblioteca CEngine el que nos ofrezca este acceso.

Abrimos el archivo en la dirección \MQL5\Include\DoEasy\Engine.mqh y sustituimos en él todas las entradas de la línea "CSerirs" por la línea "CSerirsDE", y todas las entradas de la línea "CTimeSerirs" por la línea "CTimeSerirsDE".

En la sección privada de la clase, declaramos la variable de miembro de clase para guardar el nombre del programa:

//+------------------------------------------------------------------+
//| 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_time_series;                  // Timeseries collection
   CResourceCollection  m_resource;                      // Resource list
   CTradingControl      m_trading;                       // Trading management object
   CPause               m_pause;                         // Pause 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
   string               m_name;                          // Program name

En el constructor de la clase, asignamos a esta variable el valor del nombre del programa:

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

En la sección pública de la clase, añadimos un método que retorna el objeto de barra de la serie temporal indicada del símbolo indicado de la posición indicada según la hora de la barra,
dos métodos que retornan el objeto de barra de la primera serie temporal que se corresponde con la hora de apertura de la barra en la segunda serie temporal según el índice y según la hora,
un método para actualizar todas las series temporales del símbolo indicado,
los métodos que retornan las propiedades básicas de la barra según la hora,
un método para copiar en la matriz la propiedad double de la serie temporal indicada del símbolo indicado y
un método que retorna el nombre del programa
que funciona usando como base la biblioteca.

//--- Return the bar object of the specified timeseries of the specified symbol of the specified position (1) by index, (2) by time
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const bool from_series=true)
                          { return this.m_time_series.GetBar(symbol,timeframe,index,from_series);                 }
   CBar                *SeriesGetBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time)
                          { return this.m_time_series.GetBar(symbol,timeframe,time);                              }
//--- Return the bar object of the first timeseries corresponding to the bar open time on the second timeseries (1) by index, (2) by time
   CBar                *SeriesGetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const int index,
                                                                const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
                          { return this.m_time_series.GetBarSeriesFirstFromSeriesSecond(symbol_first,timeframe_first,index,symbol_second,timeframe_second); }
   
   CBar                *SeriesGetBarSeriesFirstFromSeriesSecond(const string symbol_first,const ENUM_TIMEFRAMES timeframe_first,const datetime time,
                                                                const string symbol_second=NULL,const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT)
                          { return this.m_time_series.GetBarSeriesFirstFromSeriesSecond(symbol_first,timeframe_first,time,symbol_second,timeframe_second); }

//--- 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_time_series.IsNewBar(symbol,timeframe,time);                            }

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of the specified symbol, (3) all timeseries of all symbols
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(const string symbol,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,data_calculate);                                    }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }

//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
   CTimeSeriesDE       *SeriesGetTimeseries(const string symbol)
                          { return this.m_time_series.GetTimeseries(symbol);                                      }
   CSeriesDE           *SeriesGetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
                          { return this.m_time_series.GetSeries(symbol,timeframe);                                }
//--- Return (1) an empty, (2) partially filled timeseries
   CSeriesDE           *SeriesGetSeriesEmpty(void)       { return this.m_time_series.GetSeriesEmpty();            }
   CSeriesDE           *SeriesGetSeriesIncompleted(void) { return this.m_time_series.GetSeriesIncompleted();      }

//--- Return (1) Open, (2) High, (3) Low, (4) Close, (5) Time, (6) TickVolume,
//--- (7) RealVolume, (8) Spread of the bar, specified by index, 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);
   
//--- Return (1) Open, (2) High, (3) Low, (4) Close, (5) Time, (6) TickVolume,
//--- (7) RealVolume, (8) Spread of the bar, specified by time, of the specified symbol of the specified timeframe
   double               SeriesOpen(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   double               SeriesHigh(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   double               SeriesLow(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   double               SeriesClose(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   datetime             SeriesTime(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   long                 SeriesTickVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   long                 SeriesRealVolume(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   int                  SeriesSpread(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time);
   
//--- Copy the specified double property of the specified timeseries of the specified symbol to the array
//--- Regardless of the array indexing direction, copying is performed the same way as copying to a timeseries array
   bool                 SeriesCopyToBufferAsSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const ENUM_BAR_PROP_DOUBLE property,
                                                   double &array[],const double empty=EMPTY_VALUE)
                          { return this.m_time_series.CopyToBufferAsSeries(symbol,timeframe,property,array,empty);}

...

//--- Return the program name
   string               Name(void)                                const { return this.m_name;                                 }

Todos los métodos implementados en el cuerpo de la clase devuelven el resultado de la llamada de los métodos homónimos de la colección de series temporales TimeSeriesCollection, que hemos analizado anteriormente.

Implementación de los métodos que retornan las propiedades básicas de las barras según la hora:

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

Aquí, todo es simple:
obtenemos el objeto de barra de la clase de colección de series temporales mediante el método GetBar() -indicando el símbolo y el periodo de la serie temporal- y la hora de apertura de la barra solicitada en este serie temporal, y retornamos el valor de la propiedad existente de la barra obtenida teniendo en cuenta el error de obtención de la barra de la serie temporal.

En el manejador del evento NewTick del símbolo actual, añadimos la actualización de todas las series temporales del símbolo actual:

//+------------------------------------------------------------------+
//| NewTick event handler                                            |
//+------------------------------------------------------------------+
void CEngine::OnTick(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not a EA, exit
   if(this.m_program!=PROGRAM_EXPERT)
      return;
//--- Re-create empty timeseries and update the current symbol timeseries
   this.SeriesSync(data_calculate,required);
   this.SeriesRefresh(NULL,data_calculate);
//--- end
  }
//+------------------------------------------------------------------+

Esto nos permitirá realizar la actualización justo después de intentar sincronizar todos las series temporales utilizadas del símbolo actual en los asesores: de esta forma, no tendremos que esperar la actualización de las series temporales del símbolo actual en el temporizador de la biblioteca, ya que esto a veces provoca que los datos se desincronicen, cuando la actualización de los datos en el temporizador se llama después de llegar un nuevo tick en el símbolo actual.

En el manejador del evento Calculate del símbolo actual, añadimos la actualización de todas las series temporales del símbolo actual después de sincronizar todas las series temporales:

//+------------------------------------------------------------------+
//| Calculate event handler                                          |
//+------------------------------------------------------------------+
int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not an indicator, exit
   if(this.m_program!=PROGRAM_INDICATOR)
      return data_calculate.rates_total;
//--- Re-create empty timeseries
//--- If at least one of the timeseries is not synchronized, return zero
   if(!this.SeriesSync(data_calculate,required))
     {
      return 0;
     }
//--- Update the timeseries of the current symbol and return rates_total
   this.SeriesRefresh(NULL,data_calculate);
   return data_calculate.rates_total;
  }
//+------------------------------------------------------------------+

Aquí, existe una diferencia respecto al manejador OnTick(): hasta que no se sincronicen todas las series temporales del símbolo actual, el método retornará cero, lo cual, a su vez, comunicará al manejador OnCalculate() del indicador la necesidad de recalcular por completo los datos históricos.

Por consiguiente, el método que sincroniza los datos de todas las series temporales ahora debe retornar valores booleanos:

//+------------------------------------------------------------------+
//| Synchronize timeseries data with the server                      |
//+------------------------------------------------------------------+
bool CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If the timeseries data is not calculated, try re-creating the timeseries
//--- Get the pointer to the empty timeseries
   CSeriesDE *series=this.SeriesGetSeriesEmpty();
//--- If there is an empty timeseries
   if(series!=NULL)
     {
      //--- Display the empty timeseries data as a chart comment and try synchronizing the timeseries with the server data
      ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC));
      ::ChartRedraw(::ChartID());
      //--- if the data has been synchronized
      if(series.SyncData(required,data_calculate.rates_total))
        {
         //--- if managed to re-create the timeseries
         if(this.m_time_series.ReCreateSeries(series.Symbol(),series.Timeframe(),data_calculate.rates_total))
           {
            //--- display the chart comment and the journal entry with the re-created timeseries data
            ::Comment(series.Header(),": OK");
            ::ChartRedraw(::ChartID());
            Print(series.Header()," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK),":");
            series.PrintShort();
            return true;
           }
        }
      //--- Data is not yet synchronized or failed to re-create the timeseries
      return false;
     }
//--- There are no empty timeseries - all is synchronized, delete all comments
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+

Por el momento, hemos terminado con la clase CEngine.

Ahora, vamos a intentar comprobar cómo funciona todo esto en los indicadores. Ya que estamos usando varias series temporales distintas en un solo indicador, y que tenemos la posibilidad de obtener datos de una sola barra que se correspondan con los datos de otra barra con una hora que entre en los límites de la primera barra, pero con otras series temporales, lo primero que viene a la cabeza es crear un indicador que muestre en el gráfico actual las líneas de las barras OHLC de otros marcos temporales.

Creación y prueba del indicador multiperiodo

Para realizar la prueba, tomaremos el indicador que creamos en el artículo anterior
y lo guardaremos en la carpeta \MQL5\Indicators\TestDoEasy\Part40\ con el nuevo nombre TestDoEasyPart40.mq5.

¿Cómo lo haremos? Podemos utilizar 21 series temporales, según el número de periodos estándar representados del gráfico. En los ajustes, encontraremos la selección estándar de marcos temporales para la biblioteca, mientras que en el gráfico mostraremos los marcos que se corresponden con los marcos temporales utilizados que se han seleccionado en los ajustes. Para no poner demasiadas trabas en los ajustes del indicador y, por consiguiente, en el código, y ocuparnos con solvencia de los búferes de indicador, simplemente vincularemos los búferes de indicador a cada uno de los periodos del gráfico ya disponibles en el terminal con la ayuda de una matriz de estructuras.
Activaremos/desactivaremos la visibilidad de la línea de búfer y sus datos en la ventana de datos del indicador activando/desactivando el botón correspondiente. Para cada marco temporal, se designarán dos búferes: uno de dibujado, y otro de cálculo. En el búfer de cálculo, podremos guardar los datos intermedios de la serie temporal que le corresponde. No obstante, en nuestra ejecución no usaremos búfer de cálculo. Y para no registrar los 42 búferes (21 de dibujado y 21 de cálculo), hemos creado una estructura en la que se guardarán los parámetros para cada uno de los marcos temporales:

  • Una matriz que se asigna al búfer de indicador de dibujado
  • Una matriz que se asigna al búfer de indicador de cálculo
  • Un identificador de búfer (el marco temporal de la serie temporal cuyos datos mostrará el búfer)
  • Un índice del búfer de indicador conectado con la matriz del búfer de dibujado
  • Un índice del búfer de indicador conectado con la matriz del búfer de cálculo
  • Una bandera de uso del búfer en el indicador (botón pulsado/no pulsado)
  • Una bandera de representación del búfer en el indicador antes de la activación/desactivación de la representación del búfer con un botón en el gráfico

La elección de usar o no cada uno de los marcos temporales, y, por consiguiente, la serie temporal seleccionada/no seleccionada, la implemetaremos en los ajustes del indicador. Con los botones del gráfico, construidos de acuerdo con las series temporales utilizadas, activaremos/desactivaremos la representación de los búferes de indicador correspondientes en el gráfico. Necesitamos la bandera de representación del búfer en el indicador antes de la activación/desactivación de su representación con un botón para tomar la decisión sobre la eliminación o la representación de los datos del búfer en el gráfico solo en el momento en que se pulsa el botón correspondiente.

Vamos a escribir todos los parámetros de cada búfer de indicador (podríamos establecerlos de forma programática, pero así será más rápido):

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

//--- classes

Como podemos ver, hemos establecido el número total de búferes en 43, mientras que hay 21 búferes de dibujado. Dado que hemos acordado añadir un búfer de cálculo a cada uno de los búferes dibujados, tendremos 21+21=42. ¿Y de dónde sale el búfer sobrante? Y lo necesitamos para guardar los datos sobre la hora de la matriz time[] OnCalculate(). Dado que en ciertas funciones necesitaremos la hora de la barra según el índice, mientras que la matriz time[] solo existe en el ámbito del manejador OnCalculate(), la solución más simple consistirá en disponer de los datos de tiempo de cada barra del marco temporal actual: es decir, guardar la matriz time[] en uno de los búferes de cálculo del indicador. Precisamente para ello, hemos establecido un búfer adicional.

En el indicador, tendremos la posibilidad de representar los cuatro precios de la barra: Open, High, Low y Close. El objeto de barra tiene más propiedades de tipo real:

  • El precio de apertura de la barra (Open)
  • El mayor precio en el periodo de la barra (High)
  • El menor precio en el periodo de la barra (Low)
  • El precio de cierre de la barra (Close)
  • El tamaño de la vela
  • El tamaño del cuerpo de la vela
  • La parte superior del cuerpo de la vela
  • La parte inferior del cuerpo de la vela
  • El tamaño de la sombra superior de la vela
  • El tamaño de la sombra inferior de la vela

Por ese motivo, no podemos usar en los ajustes el valor de esta enumeración (ENUM_BAR_PROP_DOUBLE); así que crearemos otra enumeración en la que escribiremos las propiedades necesarias, equiparadas a las propiedades de la enumeración de propiedades de tipo real del objeto de barra ENUM_BAR_PROP_DOUBLE, que se podrán seleccionar en los ajustes de representación. Asimismo, estableceremos una macrosustitución con el número total de periodos disponibles del gráfico:

//--- classes

//--- enums
enum ENUM_BAR_PRICE
  {
   BAR_PRICE_OPEN    =  BAR_PROP_OPEN,    // Bar Open
   BAR_PRICE_HIGH    =  BAR_PROP_HIGH,    // Bar High
   BAR_PRICE_LOW     =  BAR_PROP_LOW,     // Bar Low
   BAR_PRICE_CLOSE   =  BAR_PROP_CLOSE,   // Bar Close
  };
//--- defines
#define PERIODS_TOTAL   (21)              // Total amount of available chart periods
//--- structures

Ahora, vamos a crear la estructura de datos del búfer dibujado y del búfer de cálculo asignados a la serie temporal (periodo del gráfico):

//--- structures
struct SDataBuffer
  {
private:
   int               m_buff_id;           // Buffer ID (timeframe)
   int               m_buff_data_index;   // The index of the indicator buffer related to the Data[] array
   int               m_buff_tmp_index;    // The index of the indicator buffer related to the Temp[] array
   bool              m_used;              // The flag of using the buffer in the indicator
   bool              m_show_data;         // The flag of displaying the buffer on the chart before enabling/disabling its display
public:
   double            Data[];              // The array assigned as INDICATOR_DATA by the indicator buffer
   double            Temp[];              // The array assigned as INDICATOR_CALCULATIONS by the indicator buffer
//--- Set indices for the drawn and calculated buffers assigned to the timeframe
   void              SetIndex(const int index)
                       {
                        this.m_buff_data_index=index;
                        this.m_buff_tmp_index=index+PERIODS_TOTAL;
                       }
//--- Methods of setting and returning values of the private structure members
   void              SetID(const int id)              { this.m_buff_id=id;             }
   void              SetUsed(const bool flag)         { this.m_used=flag;              }
   void              SetShowData(const bool flag)     { this.m_show_data=flag;         }
   int               IndexDataBuffer(void)      const { return this.m_buff_data_index; }
   int               IndexTempBuffer(void)      const { return this.m_buff_tmp_index;  }
   int               ID(void)                   const { return this.m_buff_id;         }
   bool              IsUsed(void)               const { return this.m_used;            }
   bool              GetShowDataFlag(void)      const { return this.m_show_data;       }
   void              Print(void);
  };
//--- Display structure data to the journal
void SDataBuffer::Print(void)
  {
   ::Print
     (
      "Buffer[",this.IndexDataBuffer(),"], ID: ",(string)this.ID(),
      " (",TimeframeDescription((ENUM_TIMEFRAMES)this.ID()),
      "), temp buffer index: ",(string)this.IndexTempBuffer(),
      ", used: ",this.IsUsed()
     );
  }
//--- input variables

Esta estructura guardará todos los datos para trabajar con un marco temporal. A cada uno de los marcos temporales del indicador se le asignará su propia estructura de este tipo. Y la solución óptima para conseguirlo, será crear una matriz de estas estructuras. La implementaremos en el bloque encargado de determinar los búferes de indicador.

Vamos a escribir los parámetros de entrada del indicador:

//--- input variables
/*sinput*/ENUM_SYMBOLS_MODE   InpModeUsedSymbols=  SYMBOLS_MODE_CURRENT;            // Mode of used symbols list
/*sinput*/string              InpUsedSymbols    =  "EURUSD,AUDUSD,EURAUD,EURCAD,EURGBP,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";  // List of used symbols (comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs    =  TIMEFRAMES_MODE_LIST;            // Mode of used timeframes list
sinput   string               InpUsedTFs        =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator)
sinput   ENUM_BAR_PRICE       InpBarPrice       =  BAR_PRICE_OPEN;                  // Applied bar price
sinput   bool                 InpShowBarTimes   =  false;                           // Show bar time comments
sinput   uint                 InpControlBar     =  1;                               // Control bar
sinput   uint                 InpButtShiftX     =  0;    // Buttons X shift 
sinput   uint                 InpButtShiftY     =  10;   // Buttons Y shift 
sinput   bool                 InpUseSounds      =  true; // Use sounds
//--- indicator buffers

Aquí, todo resulta bastante estandarizado, como en todos los asesores e indicadores de prueba que implementamos en cada artículo. Dado que hoy vamos a poner a prueba el trabajo solo con el símbolo actual, comentaremos en los ajustes del símbolo los modificadores sinput que indican que la variable es un parámetro de entrada del indicador (el modificador sinputindica la prohibición de la optimización de los parámetros). De esta forma, no podremos seleccionar estos parámetros en los ajustes, debido a que no se encontrarán allí, y a la variable InpModeUsedSymbols se le asignará el valor SYMBOLS_MODE_CURRENT, que permite trabajar solo con el símbolo actual.
La variable InpShowBarTimes permite activar/desactivar la representación de comentarios en el gráfico: la representación de la correspondencia de la barra en el periodo actual del gráfico respecto a la barra con la misma hora en los gráficos de las series temporales simuladas. Por otro lado, la variable InpControlBar sirve para indicar el número de barra cuyo valor se podrá controlar en los comentarios en el gráfico.

Y, finalmente, escribiremos los búferes de indicador y las variables globales:

//--- indicator buffers
SDataBuffer    Buffers[PERIODS_TOTAL];          // Array of the indicator buffer data structures assigned to the timeseries
double         BufferTime[];                    // The calculated buffer for storing and passing data from the time[] array
//--- global variables
CEngine        engine;                          // CEngine library main object
string         prefix;                          // Prefix of graphical object names
bool           testing;                         // Flag of working in the tester
int            used_symbols_mode;               // Mode of working with symbols
string         array_used_symbols[];            // Array of used symbols
string         array_used_periods[];            // Array of used timeframes
//+------------------------------------------------------------------+

Como podemos ver, hemos establecido la matriz de estructuras -que hemos analizado anteriormente- como definición de los búferes de indicador. Al inicializar el indicador, asignaremos los datos a las propiedades de la estructura y vincularemos las matrices de la estructura a los búferes de indicador. Aquí mismo, se determina el búfer de cálculo para guardar y transmitir la hora a las funciones del indicador.
Las variables globales del indicador están comentadas, por lo que, a nuestro parecer, no requieren de explicaciones adicionales.

En el manejador OnInit() del indicador, primero creamos un panel con iconos que se correspondan con los marcos temporales que hemos seleccionado en los ajustes para el trabajo; a continuación, asignamos todos los búferes de indicador y establecemos todos los parámetros de los búferes de indicador para las estructuras ubicadas en la matriz de estructuras de los búferes de indicador:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set indicator global variables
   prefix=engine.Name()+"_";
   testing=engine.IsTester();
   ZeroMemory(rates_data);
   
//--- Initialize DoEasy library
   OnInitDoEasy();

//--- Check and remove remaining indicator graphical objects
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

//--- Create the button panel
   if(!CreateButtons(InpButtShiftX,InpButtShiftY))
      return INIT_FAILED;

//--- Check playing a standard sound using macro substitutions
   engine.PlaySoundByDescription(SND_OK);
//--- Wait for 600 milliseconds
   engine.Pause(600);
   engine.PlaySoundByDescription(SND_NEWS);

//--- indicator buffers mapping

   //--- In the loop by the total number of available timeframes,
   for(int i=0;i<PERIODS_TOTAL;i++)
     {
      //--- get the next timeframe
      ENUM_TIMEFRAMES timeframe=TimeframeByEnumIndex(uchar(i+1));
      //--- Bind the drawn indicator buffer by the buffer index equal to the loop index with the structure Data[] array
      SetIndexBuffer(i,Buffers[i].Data);
      //--- set "the empty value" for the Data[] buffer, 
      //--- set the name of the graphical series displayed in the data window for the Data[] buffer
      //--- set the direction of indexing the Data[] drawn buffer as in the timeseries
      PlotIndexSetDouble(i,PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetString(i,PLOT_LABEL,"Buffer "+TimeframeDescription(timeframe));
      ArraySetAsSeries(Buffers[i].Data,true);
      //--- Setting the drawn buffer according to the button status
      bool state=false;
      //--- Set the name of the button correspondign to the buffer with the loop index and its timeframe
      string name=prefix+"BUTT_"+TimeframeDescription(timeframe);
      //--- If not in the tester, while the chart features the button with the specified name,
      if(!engine.IsTester() && ObjectFind(ChartID(),name)==0)
        {
         //--- set the name of the terminal global variable for storing the button status
         string name_gv=(string)ChartID()+"_"+name;
         //--- if no global variable with such a name is found, create it set to 'false',
         if(!GlobalVariableCheck(name_gv))
            GlobalVariableSet(name_gv,false);
         //--- get the button status from the terminal global variable
         state=GlobalVariableGet(name_gv);
        }
      //--- Set the values for all structure fields
      Buffers[i].SetID(timeframe);
      Buffers[i].SetIndex(i);
      Buffers[i].SetUsed(state);
      Buffers[i].SetShowData(state);
      //--- Set the button status
      ButtonState(name,state);
      //--- Depending on the button status, specify whether the buffer data should be displayed should be displayed in the data window
      PlotIndexSetInteger(i,PLOT_SHOW_DATA,state);
      //--- Bind the calculated indicator buffer by the buffer index from IndexTempBuffer() with the Temp[] array of the structure
      SetIndexBuffer(Buffers[i].IndexTempBuffer(),Buffers[i].Temp,INDICATOR_CALCULATIONS);
      //--- set the direction of indexing the Temp[] calculated buffer as in the timeseries
      ArraySetAsSeries(Buffers[i].Temp,true);
     }
   //--- Bind the calculated indicator buffer by the PERIODS_TOTAL*2 buffer index with the BufferTime[] array of the indicator
   SetIndexBuffer(PERIODS_TOTAL*2,BufferTime,INDICATOR_CALCULATIONS);
   //--- set the direction of indexing the BufferTime[] calculated buffer as in the timeseries
   ArraySetAsSeries(BufferTime,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Aquí, hemos comentado todas las líneas de código del ciclo en el que tiene lugar la vinculación de los búferes de indicador con el índice del ciclo según la matriz de estructuras, así como el establecimiento de los demás parámetros de cada estructura guardada en la siguiente celda de la matriz de estructuras. Si al lector le surge alguna duda al respecto, podrá formularla en los comentarios al artículo.

Funciones para trabajar con los botones:

//+------------------------------------------------------------------+
//| Create the buttons panel                                         |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int total=ArraySize(array_used_periods);
   uint w=30,h=20,x=InpButtShiftX+1, y=InpButtShiftY+h+1;
   //--- In the loop by the amount of used timeframes
   for(int i=0;i<total;i++)
     {
      //--- create the name of the next button
      string butt_name=prefix+"BUTT_"+array_used_periods[i];
      //--- create a new button with the offset by ((button width + 1) * loop index)
      if(!ButtonCreate(butt_name,x+(w+1)*i,y,w,h,array_used_periods[i],clrGray))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),array_used_periods[i]);
         return false;
        }
     }   
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+
//| Create the button                                                |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Set the terminal's global variable value                         |
//+------------------------------------------------------------------+
bool SetGlobalVariable(const string gv_name,const double value)
  {
//--- If the variable name length exceeds 63 symbols, return 'false'
   if(StringLen(gv_name)>63)
      return false;
   return(GlobalVariableSet(gv_name,value)>0);
  }
//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Return the button status by the timeframe name                   |
//+------------------------------------------------------------------+
bool ButtonState(const ENUM_TIMEFRAMES timeframe)
  {
   string name=prefix+"BUTT_"+TimeframeDescription(timeframe);
   return ButtonState(name);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
   else
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
  }
//+------------------------------------------------------------------+
//| Track the buttons' status                                        |
//+------------------------------------------------------------------+
void PressButtonsControl(void)
  {
   int total=ObjectsTotal(0,0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
//--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
//--- Create the button name for the terminal's global variable
   string name_gv=(string)ChartID()+"_"+prefix+button;
//--- Get the button status (pressed/released). If not in the tester,
//--- write the status to the button global variable (1 or 0)
   bool state=ButtonState(button_name);
   if(!engine.IsTester())
      SetGlobalVariable(name_gv,state);
//--- Get the timeframe from the button string ID and
//--- the drawn buffer index by timeframe
   ENUM_TIMEFRAMES timeframe=TimeframeByDescription(StringSubstr(button,5));
   int buffer_index=IndexBuffer(timeframe);
//--- Set the button color depending on its status, 
//--- write its status to the buffer structure depending on the button status (used/not used)
//--- initialize the buffer corresponding to the button timeframe by the buffer index received earlier
   ButtonState(button_name,state);
   Buffers[buffer_index].SetUsed(state);
   if(Buffers[buffer_index].GetShowDataFlag()!=state)
     {
      InitBuffer(buffer_index);
      BufferFill(buffer_index);
      Buffers[buffer_index].SetShowData(state);
     }

//--- Here you can add additional handling of button pressing:
//--- If the button is pressed
   if(state)
     {
      //--- If M1 button is pressed
      if(button=="BUTT_M1")
        {
         
        }
      //--- If button M2 is pressed
      else if(button=="BUTT_M2")
        {
         
        }
      //---
      // Remaining buttons ...
      //---
     }
   //--- Not pressed
   else 
     {
      //--- M1 button
      if(button=="BUTT_M1")
        {
         
        }
      //--- M2 button
      if(button=="BUTT_M2")
        {
         
        }
      //---
      // Remaining buttons ...
      //---
     }
//--- re-draw the chart
   ChartRedraw();
  }
//+------------------------------------------------------------------+

Todas estas funciones son bastante sencillas y comprensibles, además de tener algunas de sus líneas comentadas, por lo que no creemos que planteen mayores dificultades.

Vamos a echar un vistazo al manejador OnCalculate() del indicador:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//+------------------------------------------------------------------+
//| OnCalculate code block for working with the library:             |
//+------------------------------------------------------------------+
   
//--- Pass the current symbol data from OnCalculate() to the price structure
   CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread);

//--- Handle the Calculate event in the library
   engine.OnCalculate(rates_data);

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

//+------------------------------------------------------------------+
//| OnCalculate code block for working with the indicator:           |
//+------------------------------------------------------------------+
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Setting buffer arrays as timeseries

//--- Check for the minimum number of bars for calculation
   if(rates_total<2 || Point()==0) return 0;
   
//--- Display reference data on bar open time
   if(InpShowBarTimes)
     {
      string txt="";
      int total=ArraySize(array_used_periods);
      //--- In the loop by the amount of used timeframes
      for(int i=0;i<total;i++)
        {
         //--- get the next timeframe, buffer index and timeseries object by timeframe
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_used_periods[i]);
         int buffer_index=IndexBuffer(timeframe);
         CSeriesDE *series=engine.SeriesGetSeries(NULL,timeframe);
         //--- If failed to get the timeseries or the buffer is not used (the button is released), move on to the next one
         if(series==NULL || !Buffers[buffer_index].IsUsed())
            continue;
         //--- Get the reference bar from the timeseries list
         CBar *bar=series.GetBar(InpControlBar);
         if(bar==NULL)
            continue;
         //--- Collect data for the comment text
         string t1=TimeframeDescription((ENUM_TIMEFRAMES)Period());
         string t2=TimeframeDescription(bar.Timeframe());
         string t3=(string)InpControlBar;
         string t4=TimeToString(bar.Time());
         string t5=(string)bar.Index((ENUM_TIMEFRAMES)Period());
         //--- Set the comment text depending on the terminal language
         string tn=TextByLanguage
           (
            "Бар на "+t1+", соответствующий бару "+t2+"["+t3+"] со временеи открытия "+t4+", расположен на баре "+t5,
            "The bar on "+t1+", corresponding to the "+t2+"["+t3+"] bar since the opening time of "+t4+", is located on bar "+t5
           );
         txt+=tn+"\n";
        }
      //--- Display the comment on the chart
      Comment(txt);
     }

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;

//--- Recalculate the entire history
   if(limit>1)
     {
      limit=rates_total-1;
      InitBuffersAll();
     }
//--- Prepare data

//--- Calculate the indicator
   for(int i=limit; i>WRONG_VALUE && !IsStopped(); i--)
     {
      BufferTime[i]=(double)time[i];
      CalculateSeries((ENUM_BAR_PROP_DOUBLE)InpBarPrice,i,time[i]);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Si el parámetro "Show bar time comments" (variable InpShowBarTimes) en los ajustes ha sido establecido en true, este bloque de código mostrará en el gráfico la información sobre la barra indicada en la variable InpControlBar ("ControlBar") en el gráfico actual acerca de la correspondencia de dicha barra con la barra en los marcos temporales de todas las series temporales utilizadas.

Si el valor limit calculado es superior a la unidad (lo cual indicará la necesidad de redibujar toda la historia, dado que han surgido cambios en la misma), estableceremos un limit igual al inicio de la historia en el gráfico actual y llamaremos a la función de inicialización de todos los búferes de indicador.

El indicador se calcula partiendo del valor limit (en condiciones normales, su valor es 1 (nueva barra), o cero, es decir, se calcula la barra actual) hasta cero.
En el ciclo principal de cálculo del indicador, rellenamos el búfer de cálculo de la hora desde la matriz time[] (necesitamos el búfer de tiempo en otras funciones del indicador donde es necesario obtener la hora según el índice, pero la matriz time[] no está disponible), y llamamos a la función de cálculo de la barra para todos los búferes de indicador utilizados.

Funciones de inicialización de los búferes de indicador:

//+------------------------------------------------------------------+
//| Initialize the timeseries and the appropriate buffers by index   |
//+------------------------------------------------------------------+
bool InitBuffer(const int buffer_index)
  {
//--- Leave if the wrong index is passed
   if(buffer_index==WRONG_VALUE)
      return false;
//--- Initialize the variables using the "Not rendered" drawing style and disable the display in the data window
   int draw_type=DRAW_NONE;
   bool show_data=false;
//--- If the buffer is used (button pressed)
//--- Set the "Line" drawing style for variables and enable display in the data window
   if(Buffers[buffer_index].IsUsed())
     {
      draw_type=DRAW_LINE;
      show_data=true;
     }
//--- Set the drawing style and display in the data window for the buffer by its index
   PlotIndexSetInteger(Buffers[buffer_index].IndexDataBuffer(),PLOT_DRAW_TYPE,draw_type);
   PlotIndexSetInteger(Buffers[buffer_index].IndexDataBuffer(),PLOT_SHOW_DATA,show_data);
//--- Initialize the calculated buffer using zero, while the drawn one is initialized using the "empty" value 
   ArrayInitialize(Buffers[buffer_index].Temp,0);
   ArrayInitialize(Buffers[buffer_index].Data,EMPTY_VALUE);
   return true;
  }
//+------------------------------------------------------------------+
//|Initialize the timeseries and the appropriate buffers by timeframe|
//+------------------------------------------------------------------+
bool InitBuffer(const ENUM_TIMEFRAMES timeframe)
  {
   return InitBuffer(IndexBuffer(timeframe));
  }
//+------------------------------------------------------------------+
//| Initialize all timeseries and the appropriate buffers            |
//+------------------------------------------------------------------+
void InitBuffersAll(void)
  {
//--- Initialize the next buffer in the loop by the total number of chart periods
   for(int i=0;i<PERIODS_TOTAL;i++)
      if(!InitBuffer(i))
         continue;
  }
//+------------------------------------------------------------------+

Función para calcular una barra establecida de todos los búferes de indicador utilizados (para los cuales el botón está pulsado):

//+------------------------------------------------------------------+
//| Calculating a single bar of all active buffers                   |
//+------------------------------------------------------------------+
void CalculateSeries(const ENUM_BAR_PROP_DOUBLE property,const int index,const datetime time)
  {
//--- Get the next buffer in the loop by the total number of chart periods
   for(int i=0;i<PERIODS_TOTAL;i++)
     {
      //--- if the buffer is not used (the button is released), move on to the next one
      if(!Buffers[i].IsUsed())
         continue;
      //--- get the timeseries object by the buffer timeframe
      CSeriesDE *series=engine.SeriesGetSeries(NULL,(ENUM_TIMEFRAMES)Buffers[i].ID());
      //--- if the timeseries is not received
      //--- or the bar index passed to the function is beyond the total number of bars in the timeseries, move on to the next buffer
      if(series==NULL || index>series.GetList().Total()-1)
         continue;
      //--- get the bar object from the timeseries corresponding to the one passed to the bar time function on the current chart
      CBar *bar=engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,NULL,series.Timeframe());
      if(bar==NULL)
         continue;
      //--- get the specified property from the obtained bar and
      //--- call the function of writing the value to the buffer by i index
      double value=bar.GetProperty(property);
      SetBufferData(i,value,index,bar);
     }
  }
//+------------------------------------------------------------------+

Función para registrar el valor de una propiedad del objeto de barra en el búfer de indicador según varios índices de las barras en el gráfico actual:

//+------------------------------------------------------------------+
//| Write data on a single bar to the specified buffer               |
//+------------------------------------------------------------------+
void SetBufferData(const int buffer_index,const double value,const int index,const CBar *bar)
  {
//--- Get the bar index by its time falling within the time limits on the current chart
   int n=iBarShift(NULL,PERIOD_CURRENT,bar.Time());
//--- If the passed index on the current chart (index) is less than the calculated time of bar start on another timeframe
   if(index<n)
      //--- in the loop from the n bar on the current chart to zero
      while(n>WRONG_VALUE && !IsStopped())
        {
         //--- fill in the n index buffer with the 'value' passed to the function (0 - EMPTY_VALUE)
         //--- and decrease the n value
         Buffers[buffer_index].Data[n]=(value>0 ? value : EMPTY_VALUE);
         n--;
        }
//--- If the passed index on the current chart (index) is not less than the calculated time of bar start on another timeframe
//--- Set 'value' for the buffer by the 'index' passed to the function (0 - EMPTY_VALUE)
   else
      Buffers[buffer_index].Data[index]=(value>0 ? value : EMPTY_VALUE);
  }
//+------------------------------------------------------------------+

Para representar correctamente los datos de una barra de otro marco temporal en el gráfico actual, deberemos encontrar el comienzo del periodo de la vela (barra) indicada en el gráfico actual, y luego rellenar todos los índices del búfer en el gráfico actual con el valor de la barra en otro periodo. De eso precisamente se encarga esta función.

Al pulsar los botones de activación de algún marco temporal, deberemos, o bien rellenar el búfer representado correspondiente con un valor vacío (si el botón no está pulsado), o bien recalcular por completo todos los datos del búfer indicado por el botón (si este está pulsado). La función de inicialización del búfer se encarga del borrado de datos; en cambio, del rellenado del propio búfer con los datos de la serie temporal indicada, se encarga la siguiente función:

//+------------------------------------------------------------------+
//| Fill in the entire buffer with historical data                   |
//+------------------------------------------------------------------+
void BufferFill(const int buffer_index)
  {
//--- Leave if the wrong index is passed
   if(buffer_index==WRONG_VALUE)
      return;
//--- Leave if the buffer is not used (the button is released)
   if(!Buffers[buffer_index].IsUsed())
      return;
//--- Get the timeseries object by the buffer timeframe
   CSeriesDE *series=engine.SeriesGetSeries(NULL,(ENUM_TIMEFRAMES)Buffers[buffer_index].ID());
   if(series==NULL)
      return;
//--- If the buffer belongs to the current chart, copy the bar data from the timeseries to the buffer
   if(Buffers[buffer_index].ID()==Period())
      series.CopyToBufferAsSeries((ENUM_BAR_PROP_DOUBLE)InpBarPrice,Buffers[buffer_index].Data,EMPTY_VALUE);
//--- Otherwise, calculate each next timeseries bar and write it to the buffer in the loop by the number of the current chart bars
   else 
      for(int i=rates_data.rates_total-1;i>WRONG_VALUE && !IsStopped();i--)
         CalculateSeries((ENUM_BAR_PROP_DOUBLE)InpBarPrice,i,(datetime)BufferTime[i]);
  }
//+------------------------------------------------------------------+

Podrá ver el código completo del indicador en los archivos adjuntos al artículo.

Queremos destacar que este indicador de prueba ha sido desarrollado en MQL5. En MQL4, el indicador también funciona sin correcciones adicionales, aunque no correctamente al cien por cien: al pulsar el botón correspondiente, el periodo actual del gráfico no se representa, pero comienza a representarse al activar otro marco temporal más. Si en los ajustes establecemos periodos de gráficos que no sean estándar para MetaTrader 4, el indicador siempre esperará su sincronización.
Tampoco se representan correctamente los datos en la ventana de datos del terminal: se muestran absolutamente todos los búferes de indicador, incluso los de cálculo, lo cual es natural, ya que no todas las funciones de MQL5 funcionan en MQL4, y deberemos sustuirlas por análogos en MQL4.
Por si fuera poco, el indicador no siempre procesa correctamente en MetaTrader 5 los cambios en los datos históricos, lo cual es igualmente natural, ya que solo estamos ante una versión de prueba para comprobar el trabajo en el modo de periodo múltiple; todos los defectos mencionados serán paulatinamente corregidos en los próximos artículos. Y solo cuando todo funcione correctamente en MetaTrader 5, corregiremos el funcionamiento de la biblioteca en los indicadores en MetaTrader 4.

Compilamos el indicador y lo iniciamos en el gráfico en el terminal:


Podemos ver que en el gráfico М15, el búfer de datos con М5 muestra los precios de cierre de las barras М5 en un tercio de las velas del gráfico actual, lo cual es lógico, ya que en una barra de М15 tenemos tres barras de М5, y precisamente el precio de cierre de la barra М5 se representa en la barra М15.

Vamos a inicializar el indicador en el simulador con el parámetro establecido de representación de datos de las series temporales en el periodo actual del gráfico:



¿Qué es lo próximo?

En el próximo artículo, continuaremos desarrollando el tema del trabajo con los objetos de series temporales de la biblioteca en los 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
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
Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales



Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/7771

Archivos adjuntos |
MQL5.zip (3700.41 KB)
MQL4.zip (3700.4 KB)
Creando un EA gradador multiplataforma: simulación del asesor multidivisa Creando un EA gradador multiplataforma: simulación del asesor multidivisa

En un solo mes, los mercados han caído más de un 30%. ¿Acaso no se trata del mejor momento para simular asesores basados en cuadrículas y martingale? Este artículo es una continuación de la serie de artículos "Creando un EA gradador multiplataforma" cuya publicación, en principio, no estaba planeada. Pero, si el propio mercado nos ofrece la posibilidad de organizar un test de estrés para el asesor gradador, ¿por qué no aprovechar la oportunidad? Pongámonos manos a la obra.

Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura

Describiendo la creación de la optimización móvil automática, al fin hemos llegado a la estructura interna del propio optimizador automático. Este artículo puede resultar útil a aquellos que deseen mejorar el proyecto creado, o bien quieran simplemente analizar la lógica de funcionamiento del programa. En el presente artículo, mostraremos con la ayuda de diagramas UML la estructura interna del proyecto y la interacción de los objetos. Asimismo, analizaremos el proceso de iniciación de las optimizaciones, aunque, por el momento, sin describir el proceso de implementación del optimizador.

Optimización móvil continua (Parte 7): Encajando la parte lógica del optimizador automático con la parte gráfica y el control de la misma desde el programa Optimización móvil continua (Parte 7): Encajando la parte lógica del optimizador automático con la parte gráfica y el control de la misma desde el programa

Este artículo es el penúltimo de la serie, y describe cómo encajar la parte gráfica del programa del optimizador automático con su parte lógica. En él, analizaremos el proceso de inicio y optimización, comenzando por la pulsación del botón y terminando el redireccionamiento al gestor de optimizaciones.

Una rápida inmersión en MQL5 Una rápida inmersión en MQL5

¿Ha decidido estudiar el lenguaje de programación de estrategias de trading MQL5 pero no sabe nada sobre él? Hemos intentado describir el terminal de MQL5 y Meta Trader 5 desde el punto de vista de una persona iniciada y para ello hemos escrito este corto artículo introductorio. En este artículo encontrará una breve descripción de las posibilidades de este lenguaje, así como algunos consejos sobre cómo trabajar con MetaEditor 5 y el terminal.