Trabalhando com séries temporais na biblioteca DoEasy (Parte 40): indicadores com base na biblioteca - atualização de dados em tempo real

11 agosto 2020, 10:14
Artyom Trishkin
0
645

Sumário


Ideia

No artigo anterior começamos a aprender a trabalhar com a Biblioteca DoEasy como parte de indicadores. Este tipo de programa requer uma abordagem um pouco diferente na hora de construir e atualizar séries temporais, uma vez que existem algumas particularidades ao realizar cálculos de maneira reduzida em indicadores, bem como algumas limitações ao obter dados sobre o símbolo e período gráfico atual do indicador em execução.

Nós construímos uma consulta e um carregamento de dados históricos, tudo da maneira correta, e agora precisamos criar uma funcionalidade que permita atualizar em tempo real todos os dados vindos de todas as séries temporais usadas no indicador (assumimos que o indicador é multiperíodo e recebe dados a partir dos períodos gráficos especificados).

Ao construir os dados do buffer do indicador, executamos um loop desde a barra que tem uma determinada profundidade de dados históricos até a barra atual (zero). Em tal caso, o mais simples é pegar os dados pelo índice do loop, uma vez que esses dados já estão criados no objeto-série temporal da biblioteca, e ninguém nos impede de obtê-los de acordo com o índice. Porém, fazemos isso apenas ao criar dados estáticos. Ademais, enfrentamos problemas de indexação por número de barra ao tentar atualizar dados em tempo real nas listas-séries temporais. Quando uma nova barra é adicionada à lista-série temporal, tal barra possui o índice 0: afinal, é a nova barra que se torna zero (atual) e os índices de todas as barras construídas anteriormente na série temporal aumentam em 1. Assim, sempre que uma nova barra é aberta no gráfico, precisamos adicioná-la à lista-série temporal e aumentar em 1 os números de todas as outras barras na lista-série temporal atualizada.

Isso é extremamente irracional. Por esse motivo, vamos modificar a maneira de fazer/obter a indexação das barras na/da lista para ser como a usada ao trabalhar com o tempo das barras na lista-série temporal: o tempo de abertura de cada barra na lista-série temporal permanece sempre inalterado, e o que necessitamos é usar como ponto de referência o tempo da barra ao acessar qualquer uma delas na coleção de séries temporais da biblioteca. Contudo, manteremos a indexação por número de barra, mas não obteremos o número da barra a partir de propriedades: ao consultar a barra pelo índice da série temporal, calcularemos o tempo da barra segundo o índice solicitado, e segundo esse cálculo teremos a barra necessária desde a lista-série temporal, barra essa que usaremos no futuro.

Modificando classes de séries temporais

No arquivo \MQL5\Include\DoEasy\Defines.mqh, da enumeração de propriedades inteiras do objeto-barra removemos a propriedade índice de 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)

Em seu lugar colocamos a propriedade hora da barra e reduzimos em 1 a quantidade de propriedades inteiras do objeto-barra (de 14 para 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
//+------------------------------------------------------------------+

Assim, na lista de possíveis critérios para classificação de barras, precisamos da mesma maneira remover a classificação por índice e em seu lugar colocar a classificação por tempo de 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
  };
//+------------------------------------------------------------------+

Reconstruímos a classe CBar no arquivo \MQL5\Include\DoEasy\Objects\Series\Bar.mqh de modo que trabalhe com o tempo da barra.

Anteriormente, o método SetSymbolPeriod() definia o símbolo especificado, período gráfico e índice de barra, para o objeto-barra. Agora, em vez do índice, definiremos o tempo da 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);

Consertamos a implementação do 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);
  }
//+------------------------------------------------------------------+

Em vez do índice de barra, agora vamos transferir para o primeiro construtor paramétrico da classe o tempo da barra, e para obter mais informações de onde é chamado o construtor da classe CBar, adicionamos uma variável por meio da qual transferiremos ao construtor a descrição do método de classe no qual é chamada a criação do objeto-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);

Corrigimos a implementação do construtor: em vez do índice, agora usamos o tempo da barra, e ao texto que descreve o erro de obtenção de dados históricos adicionamos uma variável que aponta para o método de classe desde o qual é chamado o construtor:

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

Após adicionar o valor da variável source à mensagem de erro de histórico, conseguiremos encontrar a classe e o método desde o qual é realizada a tentativa de criar o novo objeto-barra que provoca o falha ao obter o histórico.

O segundo construtor paramétrico agora, em vez de índice de barra, também opera com o tempo de barra:

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

À seção pública da classe, no bloco de métodos de acesso simplificado às propriedades do objeto-barra renomeamos o método Period() para Timeframe() e removemos o método Index(), que retorna esta propriedade (já removida) da 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);                     }

Agora o método Index() o valor calculado com base no tempo da 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());            }  
//+------------------------------------------------------------------+

O método retorna o índice da barra da série temporal atual para timeframe especificado no parâmetro de entrada do método calculado pela função iBarShift().

No método que retorna o nome abreviado do objeto-barra, agora chamamos o método recém visto com o valor padrão PERIOD_CURRENT, que retorna o índice para a série temporal à qual pertence o objeto-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()+"]"
     );
  }
//+------------------------------------------------------------------+

Do método que retorna uma descrição da propriedade inteira do objeto-barra removemos o bloco que retorna a descrição do índice da 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)+

Em vez dele colocamos um bloco de código que retorna o tempo da barra (listagem completa do 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')
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Isso conclui as mudanças na classe do objeto-barra.

Se dermos uma olhada nas listas de classes da biblioteca padrão, no caminho MQL5\Include\Indicators\ veremos dois arquivos: Series.mqh e TimeSeries.mqh.
Também temos na biblioteca arquivos de classe com o mesmo nome. Isso não é correto. Renomeamos nossas duas classes, para isso, ao seu nome e ao dos seus arquivos atribuiremos a abreviação DE (de DoEasy), modificando todos os nomes, quando encontremos uma chamada para esses arquivos e classes. Essas alterações afetaram três arquivos: Series.mqh (agora renomeado para SeriesDE.mqh e classe CSeriesDE), TimeSeries.mqh (agora renomeado para TimeSeriesDE.mqh e classe CTimeSeriesDE) e TimeSeriesCollection.mqh (usa as duas classes renomeadas). Vamos examinar todos esses arquivos e suas classes em ordem.

O arquivo Series.mqh agora está salvo com o novo nome \MQL5\Include\DoEasy\Objects\Series\SeriesDE.mqh, e agora o nome da classe corresponde:

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

Assim, o método que retorna um objeto desta classe agora tem um novo tipo de classe:

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

Agora renomeamos o método público, que retorna o objeto-barra pelo índice como na série temporal GetBarBySeriesIndex, para apenas GetBar(), e adicionamos um método semelhante, para retornar um objeto-barra por tempo de abertura na série 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();               }

Assim, agora teremos dois métodos sobrecarregados para retornar o objeto-barra (por tempo e por índice).

Implementação do método para retornar um objeto-barra por seu tempo de abertura:

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

Ao método é transferido o tempo pelo qual precisamos encontrar e retornar o objeto-barra correspondente.
Criamos um objeto-barra temporário para a série temporal atual com uma propriedade de tempo igual àquela passada para o método.
Definimos o sinalizador para classificar a lista de objetos-barra por tempo
e pesquisamos na lista o objeto-barra com uma propriedade de tempo igual àquela passada para o método.
No final da pesquisa, será retornado o índice da barra na lista, se ele for encontrado, caso contrário, será devolvido - 1.
Removemos o objeto-barra e obtemos a devida barra desde a lista pelo índice recebido. Se o índice for menor que zero, o método At() da classe CArrayObj retornará NULL.
Se o objeto for encontrado com base no tempo, retornamos do método o objeto-barra, caso contrário, NULL
.

Implementação do método para retornar um objeto-barra por í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);
  }
//+------------------------------------------------------------------+

Ao método é transferido o índice da barra procurada.
Através da função iTime() obtemos o tempo da barra pelo índice
e retornamos o resultado do trabalho do método GetBar(), discutido acima, que retorna o objeto-barra pelo tempo recebido.

Na seção pública da classe, junto com os métodos que retornam as propriedades principais da barra por índice, declaramos os métodos que retornam as mesmas propriedades com base no tempo da 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);

Consideraremos a implementação dos métodos declarados um pouco mais tarde.

No mesmo local dessa classe, declaramos o método que permite gravar os dados especificados do objeto-série temporal na matriz passada para o 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);


Digamos que precisamos gravar os dados da série temporal no buffer do indicador de uma vez. Nosso objeto-barra pode conter muitas propriedades diferentes (inteiras e reais). Podemos escrever qualquer uma das propriedades reais do objeto-barra numa matriz usando este método. Neste caso, todos os dados serão gravados na matriz como na matriz-série temporal: os dados da barra atual armazenados no objeto-série temporal no final da lista serão gravados no índice zero da matriz de destino, ou seja, a gravação será feita de trás e para frente.

Consideremos sua implementação:

//+------------------------------------------------------------------+
//| 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, para o último valor da matriz de origem caia na célula zero da matriz de destino, aqui é calculado o índice desta última. Assim, nossa lista-série temporal (propriedade da barra solicitada) será gravada na matriz (por exemplo, o buffer de indicador) de acordo com a ordem de numeração no gráfico do símbolo, enquanto os objetos-barras na lista-série temporal estarão localizados na ordem inversa — barra com o tempo mais recente (barra atual) estará localizada no final da lista. Isso nos permitirá copiar rapidamente as propriedades de todas as barras desde a lista-série temporal no buffer de indicador se o timeframe da série temporal copiada (para o buffer por meio deste método) coincidir com o timeframe do gráfico.

Em ambos os construtores da classe definimos o sinalizador para classificar a lista-séries temporal com base no tempo das 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);
  }
//+------------------------------------------------------------------+

No método para criar uma lista-série temporal alteramos a classificação por índice para a classificação por tempo e adicionamos o texto exibido para erros ao criar um objeto-barra e o adicionamos à lista-série 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();
  }
//+------------------------------------------------------------------+

O método para atualizar a lista e os dados da série temporal também foi ligeiramente aprimorado:

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

Aqui também agora a classificação da lista é definida pelo tempo, assim, ao criar um novo objeto-barra transferimos ao construtor da classe o tempo da barra desde o objeto "Nova barra" — afinal, o que estamos fazendo é adicionar uma nova barra à lista somente quando definimos a abertura de uma nova barra, e o objeto "Nova barra" já contém o tempo de abertura desta barra, portanto, vamos passá-lo para o construtor. E, além disso, passamos ao construtor a descrição do método, em que é criado o novo objeto-barra. Se houver uma falha ao criar um novo objeto-barra, uma mensagem gerada desde seu construtor aparecerá no log, em que será gravado o método CSeriesDE::Refresh e a string desde a qual é chamado o construtor da classe CBar.
Para obter desde a lista-série temporal a barra mais recente (atual), vamos encontrá-la pelo tempo máximo de todos os objetos-barras na lista-série temporal. Para fazer isso, primeiro encontramos o índice do objeto-barra com o tempo máximo por meio do método FindBarMax() da classe CSelect, e pelo índice obtido, da lista pegamos na barra mais recente (a atual). Se por algum motivo não obtivermos o índice da barra atual, o valor do índice será -1, e ao obter um item de lista usando o método At() com um índice negativo, retornaremos o valor NULL, se estiver, simplesmente saímos do método de atualização.

Métodos para retornar as propriedades principais de um objeto-barra com base no tempo:

//+------------------------------------------------------------------+
//| 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 eles funcionam da mesma maneira:
obtemos o objeto-barra desde a lista-série temporal com base no tempo
e retornamos o valor da devida propriedade levando em consideração o erro de obtenção do objeto-barra.

O método para criar e enviar o evento "Nova barra" para o gráfico do programa de controle também foi aprimorado levando em consideração a necessidade de receber o objeto-barra atual com base no tempo:

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

Aqui, exatamente da mesma maneira que no método Refresh(), obtemos o objeto-barra atual desde a lista-série temporal, e transferimos o tempo desta barra ao parâmetro lparam ao enviar o evento personalizado para o gráfico do programa de controle.

Assim concluímos a série temporal. Agora modificamos a classe de todas as séries temporais de um símbolo.

Conforme mencionado anteriormente, esta classe CTimeSerirs pode entrar em conflito com a classe de mesmo nome na biblioteca padrão. Por isso, já a renomeamos para CTimeSerirsDE. Assim, dentro da listagem da classe são substituídas todas as ocorrências da linha "CTimeSerirs" pela linha "CTimeSerirsDE" e "CSerirs" por "CSerirsDE", e aqui não consideraremos essas substituições. Apenas como exemplo:

//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "SeriesDE.mqh"
#include "..\Ticks\NewTickObj.mqh"
//+------------------------------------------------------------------+
//| Symbol timeseries class                                          |
//+------------------------------------------------------------------+
class CTimeSeriesDE : public CBaseObjExt
  {
private:

Na seção pública da classe declaramos um método para copiar na matriz transferida a propriedade real das barras da série temporal especificada:

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

Consideramos esse método acima ao finalizar a classe CSeriesDE. Vejamos a implementação do 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);
  }
//+------------------------------------------------------------------+

Neste caso, tudo é simples: primeiro, obtemos a série temporal do timeframe especificado, e depois retornamos o resultado da chamada deste método desde o objeto-série temporal resultante.

No método que retorna o índice da série temporal na lista contendo todas as séries temporais do símbolo de acordo com o timeframe, introduzimos uma verificação de timeframe especificado para pesquisa:

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

Ao criar um objeto temporário para pesquisa, verificamos o valor do timeframe inserido e se inserido CURRENT_PERIOD, para realizar a pesquisa usamos o timeframe atual.

No método para atualizar a lista-série temporal especificada ao adicionar um novo evento à lista de eventos usaremos o tempo de abertura da nova barra desde a estrutura data_calculate como valor do 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;
     }
  }
//+------------------------------------------------------------------+

Isso conclui a classe CTimeSeriesDE. Passamos para a classe de objeto-coleção de objetos de todas as séries temporais de todos os símbolos CTimeSeriesCollection.

No momento, renomeamos duas classes: CSeriesDE e CTimeSerirsDE. Assim, dentro da listagem da classe CTimeSeriesCollection substituímos todas as ocorrências da linha "CTimeSerirs" pela linha "CTimeSerirsDE", e "CSerirs" por "CSerirsDE".
Neste caso, não consideraremos essas substituições. Apenas como exemplo:

//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#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

Na seção pública da classe, declararemos três novos métodos:
um método que retorna um objeto-barra da série temporal do símbolo com base no tempo de abertura da barra,
e dois métodos que retornam um objeto-barra de uma série temporal correspondendo ao tempo de abertura desta barra em outra série temporal de acordo com o índice e com o tempo da 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);

Além disso, declaramos na seção pública mais dois métodos: um para atualizar todas as séries temporais de um símbolo e outro que copie para a matriz a propriedade double da série temporal de um símbolo:

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

Implementação de um método que retorna um objeto-barra de uma série temporal do símbolo da posição especificada com base no tempo:

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

Ao método é transferido o símbolo e o timeframe da série temporal, desde a qual é necessário obter a barra com o tempo de abertura especificado.

Obtemos o objeto-série temporal com o símbolo e o timeframe especificados e retornamos o objeto-barra obtido da série temporal resultante com base no tempo da barra.
Se a barra não puder ser recebida, ela retornará NULL.

Implementação do método que retorna o objeto-barra (da primeira série temporal com base no índice) que corresponde ao tempo de abertura da barra na segunda série 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;
  }
//+------------------------------------------------------------------+

Ao método são transferidos o símbolo e o timeframe do primeiro gráfico, o índice da barra no primeiro gráfico, o símbolo e o período do segundo gráfico.

Obtemos o primeiro objeto-barra desde a série temporal do primeiro símbolo-período com base no índice,
obtemos e retornamos o segundo objeto-barra do segundo símbolo-período com base no tempo da primeira barra obtida
.

O método permite obter a posição da barra com base no índice no primeiro período-símbolo especificado, posição essa cujo tempo coincide com o da posição da barra no segundo período-símbolo.
O que isso faz? Como exemplo, podemos marcar rapidamente no gráfico M15 todas as barras H1.
Basta ao método transferir o símbolo atual, o período gráfico М15, a posição da barra com base no seu índice no gráfico (digamos que seja o índice do loop do cálculo do indicador), o símbolo atual e o período Н1. E o método retornará o objeto-barra desde o gráfico do símbolo atual e o período H1, cujo tempo de abertura inclui o da primeira barra especificada.

Implementação do método que retorna a barra-objeto da primeira série temporal com base no tempo correspondente ao tempo de abertura da barra na segunda série 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;
  }
//+------------------------------------------------------------------+

O método é idêntico ao método de obtenção de um objeto-barra com base no índice que acabamos de considerar. Aqui, em vez do índice da barra na série temporal, é indicada a sua hora de abertura na primeira série temporal especificada.

Como podemos ver, não apenas os períodos dos dois gráficos, mas também seus símbolos são transferidos para ambos os métodos. Isso significa que esses métodos podem retornar um objeto-barra (de qualquer símbolo-período) correspondente ao do primeiro símbolo-período com sua posição especificada na série temporal. Isso torna mais fácil comparar qualquer uma das propriedades de duas barras de qualquer símbolo-período.

Ao método para atualizar a série temporal do símbolo especificado adicionamos uma verificação de "símbolo não nativo":

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

Por que isso é necessário? Acontece que atualizamos todas as séries temporais que não pertencem ao símbolo-período atual no temporizador da biblioteca. E a atualização das séries temporais pertencentes ao símbolo no qual o programa está sendo executado deve ser feita a partir do manipulador de eventos Start, NewTick ou Calculate do programa. Por isso, para não verificar no temporizador o novo evento de tick para o símbolo atual (as séries temporais do símbolo atual são atualizadas com base no tick), comparamos o símbolo da série temporal com o símbolo atual e verificamos o evento de série temporal "novo tick" apenas se as séries temporais não pertencerem ao símbolo atual.

Implementação do método para atualizar todas as séries temporais do símbolo especificado:

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

Aqui, o código é comentado e, portanto, espero que tudo esteja claro.

Implementação do método que grava os dados reais especificados da barra do objeto-série temporal especificado na matriz passada para o 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);
  }
//+------------------------------------------------------------------+

Já vimos a operação do método ao modificar a classe CSeriesDE.
Aqui apenas obtemos o devido objeto-série temporal com base no símbolo e período especificados e retornamos o resultado da chamada do método com o mesmo nome da série temporal obtida.

Assim concluímos a classe-coleção de séries temporais.

Agora precisamos fornecer acesso aos métodos recém-criados desde os programas baseados na biblioteca. O principal objeto da biblioteca CEngine é responsável por isso.

Abrimos o arquivo em \MQL5\Include\DoEasy\Engine.mqh e substituímos todas as ocorrências da string "CSerirs" por "CSerirsDE" e todas as ocorrências da string "CTimeSerirs" por "CTimeSerirsDE".

Na seção privada da classe declaramos uma variável-membro de classe para armazenar o nome do 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

No construtor da classe atribuímos a esta variável o valor do nome do 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);
   
...

Na seção pública da classe, escrevemos um método que retorna um objeto-barra da série temporal do símbolo da posição especificada com base no tempo da barra,
dois métodos que retornam o objeto-barra da primeira série temporal correspondente ao tempo de abertura da barra na segunda série temporal com base no índice e com base no tempo,
um método que atualiza todas as séries temporais do símbolo especificado,
métodos que retornam propriedades básicas da barra com base no tempo,
um método para copiar a propriedade double da série temporal do símbolo especificado numa matriz e
um método que retorna o nome do programa
que trabalha com base na 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 os métodos, cuja implementação está escrita no corpo da classe, retornam o resultado da chamada dos métodos com o mesmo nome da coleção de séries temporais TimeSeriesCollection discutidas acima.

Implementação de métodos que retornam propriedades básicas de barras com base no tempo:

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

Aqui tudo é simples:
obtemos o objeto-barra desde a classe-coleção de séries temporais por meio do método GetBar() especificando o símbolo e o período da série temporal e o tempo de abertura da barra solicitada nesta série temporal, e retornamos o valor da devida propriedade da barra obtida, levando em consideração o erro de falha ao obter a barra desde a série temporal.

No manipulador de eventos NewTick do símbolo atual escrevemos a atualização de todas as séries temporais do símbolo atual:

//+------------------------------------------------------------------+
//| 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
  }
//+------------------------------------------------------------------+

Isso permitirá imediatamente, após uma tentativa de sincronização, atualizar nos EAs todas as séries temporais do símbolo atual, de modo a não esperar por tal atualização no temporizador da biblioteca, uma vez que isso às vezes leva à dessincronização dos dados, quando a atualização de dados no temporizador é chamada após a chegada de um novo tick com base no símbolo atual.

No manipulador de eventos Calculate do símbolo atual escrevemos a atualização de todas as séries temporais do símbolo atual após sua sincronização:

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

Neste caso, existem diferenças do manipulador OnTick(): até que todas as séries temporais usadas do símbolo atual estejam sincronizadas, o método retornará zero, que por sua vez informará o manipulador OnCalculate() do indicador sobre a necessidade de recalcular completamente os dados históricos.

Conseqüentemente, o método para sincronizar dados de todas as séries temporais agora deve 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;
  }
//+------------------------------------------------------------------+

Assim fica concluída a classe CEngine, por enquanto.

Agora vamos tentar verificar como funciona tudo nos indicadores. Como usamos séries temporais diferentes num indicador e podemos receber dados de uma barra que corresponde aos de outra com o tempo caindo nos limites da primeira barra, mas de outras séries temporais, a primeira coisa que vem à mente é criar um indicador exibindo linhas OHLC de barras de outros timeframes no gráfico atual.

Criando e testando o indicador multiperíodo

Para realizar o teste, vamos pegar o indicador que criamos no último artigo
e vamos salvá-lo na nova pasta \MQL5\Indicators\TestDoEasy\Part40\ com o novo nome TestDoEasyPart40.mq5.

Como proceder...? Podemos usar 21 séries temporais, de acordo com o número de timeframes padrão disponíveis. Nas configurações, haverá uma seleção padrão dos timeframes usados para a biblioteca, e já no gráfico exibiremos os botões correspondentes aos timeframes nas configurações. Para não limitar demais as configurações e, consequentemente, o código de manutenção dos buffers, simplesmente vinculamos os buffers do indicador a cada um dos timeframes disponíveis no terminal usando uma matriz de estruturas.
Vamos ativar/desativar a visibilidade da linha de buffer no gráfico e seus dados na janela de dados do indicador, ativando/desativando o botão correspondente. Para cada timeframe, teremos dois buffers: um a ser desenhado e outro a ser calculado. No buffer de cálculo, será possível armazenar dados intermediários das séries temporais correspondentes. Mas, nesta versão, não usaremos o buffer de cálculo. Para não escrever todos os 42 buffers (21 plotados e 21 calculados), criaremos uma estrutura na qual serão armazenados os parâmetros para cada um dos timeframes:

  • Matriz atribuída pelo buffer indicador plotado
  • Matriz atribuída pelo buffer indicador calculado
  • Identificador de buffer (timeframe da série temporal cujos dados o buffer processará)
  • Índice do buffer de indicador associado à matriz do buffer plotado
  • Índice do buffer do indicador associado à matriz do buffer de cálculo
  • Sinalizador de uso de buffer no indicador (botão pressionado/não pressionado)
  • Sinalizador de exibição de buffer no indicador antes de ativar/desativar a exibição do buffer através do botão no gráfico

Não faremos a escolha de timeframe(s) ou séries temporais nas configurações do indicador. Habilitaremos/desabilitaremos a exibição dos devidos buffers do indicador no gráfico usando os botões que serão plotados de acordo com as séries temporais usadas. Precisamos do sinalizador de exibição de buffer no indicador antes de ativá-lo/desativá-lo com o botão, porque necessitamos tomar uma decisão sobre excluir ou exibir dados do buffer no gráfico apenas ao pressionar o devido botão.

Escrevemos todos os parâmetros de cada buffer do indicador (poderia ter sido definido programaticamente, mas é mais rápido desta forma):

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart40.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/pt/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/pt/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, nós temos que o número total de buffers é definido como 43, enquanto os buffers plotados são 21. Como concordamos em adicionar um calculado a cada um dos buffers exibidos, então 21+21=42. Por que vem um extra? Ele é necessário para armazenar dados de tempo da matriz time[] OnCalculate(). Como em algumas funções precisamos do tempo da barra segundo o índice, e a matriz time[] existe apenas no escopo do manipulador OnCalculate(), a solução mais simples para ter dados de tempo do timeframe atual é salvar a matriz time[] num dos buffers de cálculo do indicador. É por isso que definimos mais um buffer.

No indicador, poderemos e exibir os quatro preços da barra: Open, High, Low e Close. O objeto-barra tem mais propriedades reais:

  • Preço de abertura da barra (Open)
  • Preço mais alto durante o período (High)
  • Preço mais baixo durante o período (Low)
  • Preço da fechamento da barra (Close)
  • Tamanho da vela
  • Tamanho do corpo da vela
  • Parte superior do corpo da vela
  • Parte inferior do corpo da vela
  • Tamanho de sombra superior da vela
  • Tamanho da sombra inferior da vela

Por isso, não podemos usar o valor desta enumeração (ENUM_BAR_PROP_DOUBLE) nas configurações, e criamos mais uma enumeração, na qual escreveremos as propriedades necessárias, equiparadas às propriedades da enumeração das propriedades reais do objeto-barra ENUM_BAR_PROP_DOUBLE, que pode ser selecionado nas configurações de exibição e defini,os uma substituição de macro com o número total de períodos gráficos disponíveis:

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

Agora vamos criar uma estrutura de dados de um buffer plotado e de um de cálculo, atribuídos a uma série temporal (a um período 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

Essa estrutura armazenará todos os dados para trabalhar com um timeframe. Cada um dos timeframes do indicador usado terá sua própria estrutura. A melhor solução para isso é a matriz dessas estruturas. Vamos criá-la no bloco para definir os buffers do indicador.

Escrevemos os parâmetros de entrada do 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

Tudo é padrão aqui, como em todos os EA de teste e indicadores que fazemos para cada artigo. Como hoje testaremos apenas com base no símbolo atual, comentaremos os modificadores sinput nas configurações do símbolo indicando que a variável é um parâmetro de entrada do indicador (modificador sinput indica que a otimização dos parâmetros desta variável é proibida). Assim, estes parâmetros não podem ser selecionados nas configurações uma vez que estão ausentes, e à variável InpModeUsedSymbols será atribuído o valor SYMBOLS_MODE_CURRENT (trabalho apenas com o símbolo atual).
A variável InpShowBarTimes permite ativar/desativar a exibição de comentários no gráfico — exibição da correspondência entre a barra do timeframe atual e a das séries temporais testadas. Já a variável InpControlBar serve para especificar o número da barra, cujo valor pode ser controlado nos comentários do gráfico.

Finalmente, escrevemos os buffers do indicador e as variáveis globais:

//--- 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, como setup de buffers de indicador, definimos uma matriz de estruturas, discutida acima. Ao inicializar o indicador, atribuiremos dados às propriedades da estrutura e vincularemos as matrizes da estrutura aos buffers de indicador. Aqui é definido o buffer calculado para armazenar e transmitir o tempo na função do indicador.
As variáveis globais do indicador são todas assinadas, e acho que não suscitam dúvidas.

No manipulador OnInit() do indicador, primeiro criamos um painel com botões que corresponda aos timeframes selecionados nas configurações, em seguida, atribuímos todos os buffers do indicador e definimos todos os seus parâmetros para estruturas localizadas na sua matriz de estruturas:

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

Aqui são comentadas todas as strings do loop em que são vinculados os buffers do indicador ao índice do loop com base na matriz de estrutura e são definidos os parâmetros restantes para cada estrutura armazenada na célula seguinte. Se você tiver alguma dúvida, pode sempre perguntar na discussão do artigo.

Funções para trabalhar com botões:

//+------------------------------------------------------------------+
//| 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 essas funções são bastante simples e claras, além disso, algumas de suas strings são comentadas, e creio que não geraram dúvidas.

Vejamos o manipulador OnCalculate() do 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);
  }
//+------------------------------------------------------------------+

Se nas configurações o parâmetro "Show bar time comments" (variável InpShowBarTimes) estiver definido como true, este bloco de código exibirá no gráfico informações sobre a barra especificada na variável InpControlBar ("ControlBar"), bem como sobre sua correspondência com a barra nos timeframes de todas as séries temporais usadas.

Se o valor calculado limit for mais de um (o que significa que é necessário redesenhar todo o histórico, uma vez que houve mudanças nele), definiremos limit como igual ao início do histórico no gráfico atual e chamaremos a função de inicialização de todos os buffers do indicador.

O indicador é calculado desde o valor limit (em condições normais, seu valor é 1 (nova barra) ou zero (é calculada a barra atual)) até zero.
No ciclo principal do cálculo do indicador preenchemos o buffer de tempo calculado desde a matriz time[] (precisamos de um buffer de tempo em outras funções do indicador, onde é necessário obter o tempo por índice e a matriz time[] não está disponível), e chamamos a função para calcular uma barra para todos os buffers do indicador usados.

Funções de inicialização para buffers do 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;
  }
//+------------------------------------------------------------------+

Função de cálculo de uma barra especificada para todos os buffers do indicador usados (para os quais o botão é pressionado):

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

Função de registro do valor da propriedade de um objeto-barra no buffer do indicador com base em vários índices de barra no gráfico atual:

//+------------------------------------------------------------------+
//| 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 exibir corretamente os dados de uma barra de um timeframe diferente no gráfico atual, precisamos encontrar o início do período da vela especificada (barra) no gráfico atual e preencher todos os índices de buffer com o valor da barra no outro período gráfico. E é esta função que toma conta disso.

Quando pressionamos o botão para ativar qualquer período gráfico, precisamos preencher o devido buffer exibido com um valor vazio (se o botão for liberado) ou recalcular todos os dados no buffer especificado pelo botão (se o botão for pressionado). A função de inicialização do buffer é responsável por apagar os dados, enquanto a função a seguir é responsável por preencher o buffer com os dados da série temporal especificada:

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

O código completo do indicador pode ser encontrado nos arquivos anexados ao artigo.

Gostaria de salientar que este indicador de teste foi desenvolvido em MQL5. Ele também funciona em MQL4 sem necessidade de modificações, mas não totalmente da maneira correta, já que o período gráfico atual não é exibido quando o devido botão é pressionado, em vez disso, começa a ser exibido quando outro timeframe é ativado. Se definirmos nas configurações não padrão para os períodos gráficos do MetaTrader 4, o indicador sempre aguardará sua sincronização.
Além disso, os dados na janela de dados do terminal são exibidos incorretamente, mostrando absolutamente todos os buffers do indicador, mesmo os calculados, o que é natural, porque nem todas as funções MQL5 funcionam em MQL4, e é por isso que precisamos substituí-las por análogos MQL4.
Além disso, o indicador no MetaTrader 5 nem sempre processa corretamente as alterações nos dados históricos, o que é natural, pois esta é apenas uma versão de teste para verificar a operação no modo multiperíodo. Iremos resolver gradualmente todas as deficiências identificadas nos próximos artigos. E só quando tudo estiver funcionando corretamente no MetaTrader 5, corrigiremos o trabalho da biblioteca em indicadores no MetaTrader 4.

Compilamos o indicador e o iniciamos no gráfico do terminal:


Pode-se ver que no gráfico M15, o buffer de dados de M5 mostra os preços de fechamento das barras M5 num terço das velas no gráfico atual, o que é natural, porque numa barra M15 temos três barras M5, e é o preço de fechamento da barra M5 que é exibido na barra M15.

Iniciamos o indicador no testador com o parâmetro definido para exibir os dados das séries temporais no período gráfico atual:



O que vem agora?

No próximo artigo, continuaremos a desenvolver o tópico acerca do trabalho com objetos-séries temporais da biblioteca aplicado a indicadores.

Abaixo estão anexados todos os arquivos da versão atual da biblioteca e os arquivos do EA de teste. Você pode baixá-los e testar tudo sozinho.
Se você tiver perguntas, comentários e sugestões, poderá expressá-los nos comentários do artigo.

Complementos

Artigos desta série:

Trabalhando com séries temporais na biblioteca DoEasy (Parte 35): Objeto "Barra" e lista-série temporal do símbolo
Trabalhando com séries temporais na biblioteca DoEasy (Parte 36): objeto das séries temporais de todos os períodos usados do símbolo
Trabalhando com séries temporais na biblioteca DoEasy (Parte 37): coleção de séries temporais - banco de dados de séries temporais para símbolos e períodos
Trabalhando com séries temporais na biblioteca DoEasy (Parte 38): coleção de séries temporais - atualização em tempo real e acesso aos dados do programa
Trabalhando com séries temporais na biblioteca DoEasy (Parte 39): indicadores com base na biblioteca - preparação de dados e eventos das séries temporais



Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7771

Arquivos anexados |
MQL5.zip (3700.41 KB)
MQL4.zip (3700.4 KB)
Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático

Anteriormente, nós consideramos a criação da otimização walk forward automática. Desta vez, nós prosseguiremos para a estrutura interna da ferramenta de otimização automática. O artigo será útil para todos aqueles que desejam continuar trabalhando com o projeto criado e modificá-lo, bem como para aqueles que desejam entender a lógica do programa. O artigo atual contém diagramas UML que apresentam a estrutura interna do projeto e os relacionamentos entre seus objetos. Ele também descreve o processo de início da otimização, mas não contém a descrição do processo de implementação do otimizador.

Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte II Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte II

Neste artigo continuaremos testando um novo conceito, em particular a descrição da interface de programas MQL usando as construções da linguagem MQL. A criação automática de GUIs com base no layout MQL fornece funcionalidade adicional para armazenamento em cache e geração dinâmica de elementos, gerenciamento de estilos e novos esquemas de manipulação de eventos. Incluímos uma versão aprimorada da biblioteca de controles padrão.

Criando um EA gradador multiplataforma: testando um EA multimoeda Criando um EA gradador multiplataforma: testando um EA multimoeda

No mês, os mercados caíram mais de 30%. Estamos no momento oportuno para testar Expert Advisors gradadores e martingale. Este artigo é uma continuação da série de artigos "Criando um EA gradador multiplataforma", cuja publicação não tinha sido planejada. Mas, uma vez que o próprio mercado nós dá uma oportunidade para fazer um teste de estresse do EA gradador, é bom aproveitá-la. Então, vamos direto ao assunto.

Otimização Walk Forward Contínua (Parte 7): Vinculação da parte lógica do Otimizador Automático com a parte gráfica e o controle do mesmo no programa Otimização Walk Forward Contínua (Parte 7): Vinculação da parte lógica do Otimizador Automático com a parte gráfica e o controle do mesmo no programa

Este artigo descreve a vinculação da parte gráfica do programa do otimizador automático com a sua parte lógica. Ele considera o processo de inicialização da otimização, pelo clique de um botão até o redirecionamento da tarefa ao gerenciador de otimização.