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

Artyom Trishkin | 17 julho, 2020

Sumário


Ideia

Tudo o que fizemos até hoje esteve relacionado a EA e scripts, não tendo a ver nada com indicadores. No entanto, as séries temporais podem ser usadas ativamente como fonte de dados para vários cálculos em indicadores. E, na minha opinião, é hora de consolidar a amizade entre a biblioteca e os indicadores.

Ao contrário dos EAs, os indicadores têm uma arquitetura completamente diferente, e cada um deles é executado num único fluxo de um símbolo. Isso significa que, se iniciarmos indicadores diferentes em vários gráficos do mesmo símbolo, todos eles serão executados no mesmo fluxo do símbolo ao qual todos esses gráficos pertencem.
Assim, se um dos indicadores tiver uma arquitetura mal concebida, congelará todo o fluxo do símbolo. Nesse caso, todos os outros indicadores feitos corretamente, mas trabalhando no mesmo fluxo que o indicador "pesado", serão forçados a travar, à espera de o fluxo "descongelar".

Para evitar atrasos ao solicitar dados históricos quando trabalhamos com indicadores, o terminal fornece o retorno sequencial dos dados solicitados, isto é, funções que ativam o carregamento de dados históricos retornam imediatamente o resultado da função.

Ao solicitar dados de qualquer série temporal de qualquer símbolo com funções Copy, o indicador e o EA terão um comportamento diferente quando o terminal retornar dados históricos:

Ao solicitar dados do indicador, se as séries temporais solicitadas ainda não estiverem construídas ou precisarem ser carregadas do servidor, a função retornará imediatamente -1, sendo que será iniciado o próprio processo de carregamento/construção.

Ao solicitar dados desde um EA ou script, o carregamento será inicializando a partir do servidor, se o terminal não tiver esses dados localmente, começará a construção das séries temporais necessárias, se os dados puderem ser construídos a partir do histórico local, mas ainda não estiverem prontos. A função retornará a quantidade de dados que estará pronta quando o tempo limite expirar, sendo que o carregamento do histórico continuará e, se houver uma próxima solicitação semelhante, a função retornará mais dados.

Assim, vemos que, ao solicitar dados a partir de um EA, o próprio terminal começará a carregar dados (se ainda não houver dados solicitados localmente ou ainda não forem suficientes) e, após algum tempo (tempo limite), a função retornará a quantidade de histórico existente para no momento em que é concluído o carregamento, ou seja, o terminal tentará fornecer imediatamente o histórico solicitado e, se localmente não for suficiente, tentará carregá-lo na quantidade certa.
Nosso programa, nesse momento, estará aguardando o carregamento de dados.

Nos indicadores é inaceitável aguardar, portanto, o terminal nos fornece o que há (ou informa que não há nada) e, se não houver histórico localmente ou não houver dados suficientes na primeira solicitação, será iniciado seu carregamento. Além disso, não há expectativa de carregar dados ausentes antes do tempo limite.
Nosso próprio programa deve sair de sua parte calculada antes do próximo tick nesta situação. No próximo início do manipulador OnCalculate() do indicadores num novo tique, os dados já podem estar parcial ou totalmente carregados e disponíveis para cálculos. Aqui precisamos decidir quantos dados serão suficientes para o algoritmo do programa funcionar sem problemas.

E mais: o indicador não deve tentar carregar seus próprios dados, os dados com base no símbolo e o período em que está em execução. Caso contrário, essa solicitação pode causar um erro. O subsistema do terminal está envolvido no carregamento desses dados para indicadores e fornece todos os dados sobre sua quantidade e estado nas variáveis rates_total e prev_calculated do manipulador OnCalculate().

Com base nesses requisitos mínimos, precisamos corrigir algumas classes para trabalhar com séries temporais e realizar o carregamento inicial correto dos dados necessários para os cálculos em nossos indicadores.

Hoje, corrigiremos as classes que já foram criadas, realizaremos o carregamento inicial correto de dados de todas as séries temporais usadas em nossos programas e enviaremos ao gráfico do programa de controle eventos de todas as séries temporais usadas durante a atualização em tempo real.

Modificando classes para trabalhar com indicadores, criando eventos de séries temporais

Primeiro, ao arquivo Datas.mqh adicionamos novas mensagens da biblioteca, isto é, os índices das mensagens:

   MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,        // Failed to prepare array of used symbols. Error 
   MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY,              // Failed to get array of used symbols.
   MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING,            // Error. The string of predefined periods is empty and is to be used

...

//--- CBar
   MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA,              // Failed to receive bar data
   MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE,           // Failed to write time to time structure
   MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA,           // Failed to receive timeseries data

...

   MSG_LIB_TEXT_TS_TEXT_SYMBOL_TERMINAL_FIRSTDATE,    // The very first date in history by a symbol in the client terminal
   MSG_LIB_TEXT_TS_TEXT_CREATED_OK,                   // successfully created
   MSG_LIB_TEXT_TS_TEXT_NOT_CREATED,                  // not created
   MSG_LIB_TEXT_TS_TEXT_IS_SYNC,                      // synchronized
   MSG_LIB_TEXT_TS_TEXT_ATTEMPT,                      // Attempt:
   MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC,                // Waiting for data synchronization ...

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

bem como os textos das mensagens correspondentes aos índices adicionados recentemente:

   {"Не удалось подготовить массив используемых символов. Ошибка ","Failed to create an array of used symbols. Error "},
   {"Не удалось получить массив используемых символов","Failed to get array of used symbols"},
   {"Ошибка. Строка предопределённых периодов пустая, будет использоваться ","Error. String of predefined periods is empty, the Period will be used: "},

...

   {"Не удалось получить данные бара","Failed to get bar data"},
   {"Не удалось записать время в структуру времени","Failed to write time to datetime structure"},
   {"Не удалось получить данные таймсерии","Failed to get timeseries data"},

...

   {"Самая первая дата в истории по символу в клиентском терминале","Very first date in history of symbol in client terminal"},
   {"создана успешно","created successfully"},
   {"не создана","not created"},
   {"синхронизирована","synchronized"},
   {"Попытка: ","Attempt: "},
   {"Ожидание синхронизации данных ...","Waiting for data synchronization ..."},
   
  };
//+---------------------------------------------------------------------+

No construtor da classe de objeto base de todos os objetos da biblioteca CBaseObj, no arquivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh é alterada a inicialização da variável m_available — imediatamente após a criação, todos os objetos-herdeiros da classe base CBaseObj terão o estado "usado" (true) que é uma propriedade indicando que podemos trabalhar com eles no programa. Anteriormente, durante a inicialização o valor era definido como "não usado" false:

//--- Constructor
                     CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                                  m_global_error(ERR_SUCCESS),
                                  m_log_level(LOG_LEVEL_ERROR_MSG),
                                  m_chart_id_main(::ChartID()),
                                  m_chart_id(::ChartID()),
                                  m_folder_name(DIRECTORY),
                                  m_sound_name(""),
                                  m_name(__FUNCTION__),
                                  m_type(0),
                                  m_use_sound(false),
                                  m_available(true),
                                  m_first_start(true) {}
  };
//+------------------------------------------------------------------+

Também foi alterado o nome do método que define o sinalizador que indica que no objeto foi registrado um evento, na classe do objeto base estendido de todos os objetos da biblioteca CBaseObjExt no arquivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh:

//--- Set/return the occurred event flag to the object data
   void              SetEventFlag(const bool flag)                   { this.m_is_event=flag;                   }

Anteriormente, o método era chamado SetEvent(), e isso gerava alguma confusão ao desenvolver novos objetos, pois SetEvent pode significar criar, instalar, enviar etc. de algum evento e não definir o sinalizador que indica presença de evento.

Assim, também foram feitas alterações nos arquivos de classe nos quais foi usado esse método, substituímos a chamada para o método SetEvent() pela chamada para SetEventFlag(), que está explicada nos arquivos anexados ao artigo.

Como nos indicadores são proibidas as funções de negociação, faremos alterações nas classes dos objetos de negociação.
Na classe do objeto de negociação multiplataforma no arquivo \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh no início de todos os métodos de negociação, inserimos uma verificação do tipo de programa e, se for um indicador ou um serviço, abandonaremos o método retornando true:

//+------------------------------------------------------------------+
//| Open a position                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::OpenPosition(const ENUM_POSITION_TYPE type,
                             const double volume,
                             const double sl=0,
                             const double tp=0,
                             const ulong magic=ULONG_MAX,
                             const string comment=NULL,
                             const ulong deviation=ULONG_MAX,
                             const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePosition(const ulong ticket,
                              const string comment=NULL,
                              const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position partially                                       |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionPartially(const ulong ticket,
                                       const double volume,
                                       const string comment=NULL,
                                       const ulong deviation=ULONG_MAX)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Close a position by an opposite one                              |
//+------------------------------------------------------------------+
bool CTradeObj::ClosePositionBy(const ulong ticket,const ulong ticket_by)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Modify a position                                                |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyPosition(const ulong ticket,const double sl=WRONG_VALUE,const double tp=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Set an order                                                     |
//+------------------------------------------------------------------+
bool CTradeObj::SetOrder(const ENUM_ORDER_TYPE type,
                         const double volume,
                         const double price,
                         const double sl=0,
                         const double tp=0,
                         const double price_stoplimit=0,
                         const ulong magic=ULONG_MAX,
                         const string comment=NULL,
                         const datetime expiration=0,
                         const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                         const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Remove an order                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::DeleteOrder(const ulong ticket)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

...

//+------------------------------------------------------------------+
//| Modify an order                                                  |
//+------------------------------------------------------------------+
bool CTradeObj::ModifyOrder(const ulong ticket,
                            const double price=WRONG_VALUE,
                            const double sl=WRONG_VALUE,
                            const double tp=WRONG_VALUE,
                            const double price_stoplimit=WRONG_VALUE,
                            const datetime expiration=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                            const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      return true;
   ::ResetLastError();

Exatamente as mesmas alterações foram feitas em todos os métodos de negociação com o mesmo nome da classe de negociação principal da biblioteca
no arquivo \MQL5\Include\DoEasy\Trading.mqh.

Essa saída dos métodos de negociação, em primeiro lugar, não permitirá chamar funções de negociação em programas onde são proibidas e, em segundo lugar, retornará a execução bem-sucedida do método, o que impedirá de processar erros da biblioteca.

Agora consideraremos as alterações que afetam diretamente as classes dos objetos das séries temporais.

Na classe do objeto-barra, foram ligeiramente alterados os textos derivados dos construtores da classe no caso de erro na obtenção de dados históricos ao criar o objeto-barra. Ao texto exibido foram adicionados o número do construtor, símbolo e período gráfico da série temporal para a qual o objeto-barra está sendo criado.
No construtor da primeira forma a verificação de erros de obtenção de dados e o registro de tempo numa estrutura de tempo foram colocados em blocos separados:

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

Essas ações nos fornecem mais dados ao surgir o erro de criar um objeto-barra.

Como, para solicitar dados sobre o número de barras e seus valores no símbolo-período atual, precisamos usar as matrizes de séries temporais fornecidas pelo manipulador OnCalculate(), devemos, de alguma forma, passar essas matrizes e valores para as classes da biblioteca.
Para fazer isso, criamos uma estrutura no arquivo \MQL5\Include\DoEasy\Defines.mqh, na qual serão armazenadas as variáveis, através das quais transferiremos todos os dados necessários, calculados para a série temporal atual, na série temporal da biblioteca:

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                                 // size of input time series
   int         prev_calculated;                             // number of handled bars at the previous call
   int         begin;                                       // where significant data start
   double      price;                                       // current array value for calculation
   MqlRates    rates;                                       // Price structure
  } rates_data;
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Search and sorting data                                          |
//+------------------------------------------------------------------+

Como você pode ver, a estrutura contém todos os campos necessários para transferir dados para a biblioteca durante qualquer implementação do manipulador OnCalculate() do indicador.

Para a primeira forma de manipulador

int OnCalculate(
   const int        rates_total,       // price[] array size
   const int        prev_calculated,   // number of handled bars at the previous call
   const int        begin,             // index number in the price[] array meaningful data starts from
   const double&    price[]            // array of values for calculation
   );

são usadas as variáveis de estrutura rates_total, prev_calculated, begin e price.

Para a segunda forma do manipulador

int OnCalculate(
   const int        rates_total,       // size of input time series
   const int        prev_calculated,   // number of handled bars at the previous call
   const datetime&  time{},            // Time array
   const double&    open[],            // Open array
   const double&    high[],            // High array
   const double&    low[],             // Low array
   const double&    close[],           // Close array
   const long&      tick_volume[],     // Tick Volume array
   const long&      volume[],          // Real Volume array
   const int&       spread[]           // Spread array
   );

são usadas as variáveis das estruturas rates_total, prev_calculated e a estrutura MqlRates rates para armazenar os valores das matrizes.

Essa implementação da estrutura é adequada para transferir apenas um valor de barra para a biblioteca.

Na classe CSeries, no arquivo \MQL5\Include\DoEasy\Objects\Series\Series.mqh, aos métodos para definir o símbolo e período gráfico adicionamos o sinalizador de definição de datas do servidor:

//--- Set (1) symbol, (2) timeframe, (3) symbol and timeframe, (4) amount of applied timeseries data
   void              SetSymbol(const string symbol,const bool set_server_date=false);
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false);

Por padrão, o sinalizador está desmarcado, o que não permite definir as datas do servidor ao chamar o método, pois para chamar o método de configuração da datas do servidor primeiro é verificado o estado do sinalizador em questão:

//+------------------------------------------------------------------+
//| Set a symbol                                                     |
//+------------------------------------------------------------------+
void CSeries::SetSymbol(const string symbol,const bool set_server_date=false)
  {
   if(this.m_symbol==symbol)
      return;
   this.m_symbol=(symbol==NULL || symbol==""   ? ::Symbol() : symbol);
   this.m_new_bar_obj.SetSymbol(this.m_symbol);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+
//| Set a timeframe                                                  |
//+------------------------------------------------------------------+
void CSeries::SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false)
  {
   if(this.m_timeframe==timeframe)
      return;
   this.m_timeframe=(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe);
   this.m_new_bar_obj.SetPeriod(this.m_timeframe);
   this.m_period_description=TimeframeDescription(this.m_timeframe);
   if(set_server_date)
      this.SetServerDate();
  }
//+------------------------------------------------------------------+

Isso é feito assim para não definir datas do servidor várias vezes ao chamar o método que permite definir simultaneamente o símbolo e o período gráfico:

//+------------------------------------------------------------------+
//| Set a symbol and timeframe                                       |
//+------------------------------------------------------------------+
void CSeries::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_symbol==symbol && this.m_timeframe==timeframe)
      return;
   this.SetSymbol(symbol);
   this.SetTimeframe(timeframe,true);
  }
//+------------------------------------------------------------------+

Aqui: primeiro é chamado o método de definição do símbolo (sinalizador desmarcado), em seguida, é chamado o método de configuração do período gráfico com o sinalizador definido para chamar o método de configuração de datas do servidor a partir do método de configuração do período gráfico.

Ao método de atualização de dados da série temporal agora é transferida uma nova estrutura de dados do manipulador OnCalculate() em vez de sua lista completa de matrizes:

//--- (1) Create and (2) update the timeseries list
   int               Create(const uint required=0);
   void              Refresh(SDataCalculate &data_calculate);
                            
//--- Create and send the "New bar" event to the control program chart
   void              SendEvent(void);

Assim, na implementação do método Refresh() agora, em vez de usar as matrizes, são acessados os dados desta estrutura:

//+------------------------------------------------------------------+
//| Update timeseries list and data                                  |
//+------------------------------------------------------------------+
void CSeries::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 index
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
//--- 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,0);
      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 object from the list by the terminal timeseries index (zero bar)
   CBar *bar=this.GetBarBySeriesIndex(0);
//--- 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]);
  }
//+------------------------------------------------------------------+

Para poder pesquisar na lista de objetos-séries temporais por seus períodos gráficos, agora está implementado o método virtual para comparar dois objetos-séries temporais:

//--- Comparison method to search for identical timeseries objects by timeframe
   virtual int       Compare(const CObject *node,const int mode=0) const 
                       {   
                        const CSeries *compared_obj=node;
                        return(this.Timeframe()>compared_obj.Timeframe() ? 1 : this.Timeframe()<compared_obj.Timeframe() ? -1 : 0);
                       } 
//--- Constructors
                     CSeries(void);
                     CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
  };
//+------------------------------------------------------------------+

O método compara a propriedade "timeframe" de dois objetos-séries temporais (atual e passado para o método) e, se for igual, retornará zero.
Já examinamos muitas vezes a lógica de métodos semelhantes para pesquisar e classificar vários objetos-herdeiros do objeto base da biblioteca padrão CObject. O método é definido no objeto base da biblioteca padrão como virtual, por isso, sua implementação deve ser executada nos objetos-herdeiros e o método deve retornar zero se for igual ou 1/-1 se o valor da propriedade comparada do objeto atual for maior/menor que o valor dessa propriedade do objeto comparado.

Como o primeiro uso das funções que retornam dados históricos ativa o carregamento de dados se eles estiverem ausentes ou não forem suficientes localmente, ao início do método de configuração da quantidade necessária de dados necessários (ele é chamado antes de criar o objeto-série temporal) adicionaremos a consulta de dados históricos necessários (basta solicitarmos a data da barra atual). Isso iniciará o carregamento de dados necessários (se não houver localmente):

//+------------------------------------------------------------------+
//| Set the number of required data                                  |
//+------------------------------------------------------------------+
bool CSeries::SetRequiredUsedData(const uint required,const uint rates_total)
  {
   this.m_required=(required<1 ? SERIES_DEFAULT_BARS_COUNT : required);
//--- Launch downloading historical data
   if(this.m_program!=PROGRAM_INDICATOR || (this.m_program==PROGRAM_INDICATOR && (this.m_symbol!=::Symbol() || this.m_timeframe!=::Period())))
     {
      datetime array[1];
      ::CopyTime(this.m_symbol,this.m_timeframe,0,1,array);
     }
//--- Set the number of available timeseries bars


Quando criamos o objeto que armazenava listas de todas as séries temporais de um símbolo (classe CTimeSeries), fizemos com que este objeto sempre tivesse uma lista na qual fosse gravado o conjunto completo de todos os possíveis períodos gráficos no terminal. Além disso, as listas-séries temporais eram imediatamente adicionadas a essa lista, mas não eram criadas. Eles eram geradas conforme necessário. Quanto ao acesso aos ponteiros para uma lista-série temporal necessária, nós o criamos segundo um índice desconhecido que correspondia à posição do índice do período gráfico da lista na enumeração ENUM_TIMEFRAMES com deslocamento de 1 (descrito no artigo).

Isso foi feito para acelerar o acesso ao ponteiro para o objeto-série temporal desejado na lista. Mas, como se viu, junto com o acesso instantâneo ao índice, tivemos problemas ao trabalhar no testador, pois o testador visual criava gráficos de todos os períodos gráficos, independentemente de se fossem realmente usados no programa e de suas listas-séries temporais fossem na verdade criadas.

Também tivemos mais um problema ao alterar o período de agendamento durante a operação do programa - as listas criadas anteriormente não foram recriadas e o programa continuou a rastrear eventos de objetos já inexistentes, substituindo-os por outros.

Bem, para evitar a futura acumulação de erros e o trabalho de procurar causas, decidi que seria melhor armazenar no objeto da classe CTimeSeries (que armazena as listas-séries temporais de todos os períodos gráficos usados) os ponteiros apenas para as listas-séries temporais realmente criadas e usadas, em outras palavras, à lista adicionaremos os ponteiros para cada série temporal de cada período gráfico apenas se no programa estiver especificado que ela deve ser usada e que tal objeto-série temporal está criado fisicamente.

Abrimos o arquivo \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh e fazemos as alterações necessárias.

Agora a classe de todas as séries temporais de um símbolo será herdada da classe de objeto base estendida de todos os objetos de biblioteca.
Isso é feito para poder usar a funcionalidade de evento da classe CBaseObjExt:

//+------------------------------------------------------------------+
//| Symbol timeseries class                                          |
//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {

O método que retorna o índice de série temporal na lista pelo nome do período gráfico agora apenas é declarado na seção privada da classe:

//+------------------------------------------------------------------+
class CTimeSeries : public CBaseObjExt
  {
private:
   string            m_symbol;                                             // Timeseries symbol
   CNewTickObj       m_new_tick;                                           // "New tick" object
   CArrayObj         m_list_series;                                        // List of timeseries by timeframes
   datetime          m_server_firstdate;                                   // The very first date in history by a server symbol
   datetime          m_terminal_firstdate;                                 // The very first date in history by a symbol in the client terminal
//--- Return (1) the timeframe index in the list and (2) the timeframe by the list index
   int               IndexTimeframe(const ENUM_TIMEFRAMES timeframe);
   ENUM_TIMEFRAMES   TimeframeByIndex(const uchar index)             const { return TimeframeByEnumIndex(uchar(index+1));                       }
//--- Set the very first date in history by symbol on the server and in the client terminal
   void              SetTerminalServerDate(void)
                       {
                        this.m_server_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE);
                        this.m_terminal_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE);
                       }
public:

A implementação deste método agora é realizada fora do corpo da classe:

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

O período é passado para o método cujo ponteiro é necessário retornar para a série temporal.
Em seguida, criamos um objeto-série temporal temporário com o período gráfico desejado.
Para a lista de objetos-séries temporais definimos o sinalizador de lista classificada
e
obtemos o índice do objeto-série temporal na lista para a qual o período é igual ao período do objeto temporário
.
Se tal objeto existir na lista, será obtido seu índice, caso contrário, WRONG_VALUE (-1).
Removemos o objeto temporário e retornamos o índice obtido.

Em vez dos métodos Create() e CreateAll() declaramos os métodos para adicionar a série temporal especificada à lista e o método para criar o objeto-série temporal especificado,
enquanto os métodos de atualização de listas-séries temporais agora obtêm a estrutura dos valores dos parâmetros e matrizes OnCalculate() em vez da lista completa de matrizes:

//--- (1) Add the specified timeseries list to the list and create (2) the specified timeseries list
   bool              AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool              CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
//--- Update (1) the specified timeseries list and (2) all timeseries lists
   void              Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void              RefreshAll(SDataCalculate &data_calculate);

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

Do construtor de classe excluímos o ciclo de criação da listas de séries temporais:

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

Agora, as séries temporais necessárias serão criadas após a criação de uma matriz de séries temporais usadas no manipulador OnInit() do programa. Quaisquer alterações no número de períodos gráficos usados no programa farão com que o EA seja reinicializado ou o indicador seja recriado, causando uma recriação completa da lista de objetos-séries temporais usadas e, mais tarde, seu recálculo.

Nos métodos para definir a profundidade do histórico de todas as séries temporais usadas SetRequiredAllUsedData() e para retornar o sinalizador de sincronização de todas as séries temporais usadas SyncAllData(), substituímos o ciclo percorrendo o número total de períodos gráficos possíveis

//+------------------------------------------------------------------+
//| Set the history depth of all applied symbol timeseries           |
//+------------------------------------------------------------------+
bool CTimeSeries::SetRequiredAllUsedData(const uint required=0,const int rates_total=0)
  {
   if(this.m_symbol==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL));
      return false;
     }
   bool res=true;
   for(int i=0;i<21;i++)
     {
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL)
         continue;
      res &=series_obj.SetRequiredUsedData(required,rates_total);
     }
   return res;
  }
//+------------------------------------------------------------------+

por um ciclo pelo número de objetos-séries temporais reais na lista:

   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)

Isso é compreensível, pois agora nossa lista consiste apenas em objetos-séries temporais realmente criados e realizamos ciclos de acordo com seu número real.

Implementação do método de adição do objeto-série temporaç especificado à lista:

//+------------------------------------------------------------------+
//| Add the specified timeseries list to the list                    |
//+------------------------------------------------------------------+
bool CTimeSeries::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeries *series=new CSeries(this.m_symbol,timeframe,required);
   if(series==NULL)
      return res;
   this.m_list_series.Sort();
   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   if(!res)
      delete series;
   series.SetAvailable(true);
   return res;
  }
//+------------------------------------------------------------------+

Ao método é transferido o período gráfico da série temporal que é necessário adicionar à lista das séries temporais do símbolo.

Criamos o objeto-série temporal com o período gráfico cujo valor é transferido ao método.
Para o lista de séries temporais definimos o sinalizador indicando lista classificada e procuramos na lista um objeto-série temporal igual ao recém-criado.
Se ainda não houver tal objeto na lista (a pesquisa retornar -1), adicionaremos o objeto-série temporal criado à lista.
Caso contrário,
excluímos o objeto criado, pois tal objeto-série temporal já está na lista.
Como estamos criando uma série temporal, é necessário definirmos um sinalizador indicando seu uso no programa e
retornarmos o resultado da adição de série temporal à lista
.
Se for bem sucedido, será retornado o valor true, caso contrário, false.

Na biblioteca, para enviar eventos que ocorrem com seus diversos objetos, no objeto estendido de todos os objetos da biblioteca foi criada uma funcionalidade de evento. Examinamos os princípios e a lógica de trabalho com eventos da biblioteca no artigo 16 e continuamos a desenvolver esse tópico no artigo 17.

Em resumo, cada objeto herdado do objeto base da biblioteca CBaseObj (agora já de CBaseObjExt) possui uma lista na qual são registrados todos os eventos que podem ocorrer com o objeto durante uma execução do programa, num tick ou numa iteração do temporizador.
Ao identificar qualquer evento de um objeto, para ele é definido o sinalizador que indica evento ocorrido. Em seguida, nas classes-coleções, são exibidas as listas de objetos de coleções e, por sua vez, são verificados esses sinalizadores. Se for encontrado um objeto com o sinalizador de evento marcado, a classe-coleção desses objetos receberá uma lista de todos os eventos do objeto com o sinalizador de evento marcado e enviará todos os eventos dessa lista para o temporizado do programa de controle.
No próprio programa, foi criada uma funcionalidade para processar todos os eventos recebidos. Além disso, no testador, todos os eventos são processados por ticks, enquanto, fora do testador, no manipulador OnChartEvent().

Na classe do objeto de todas as séries temporais do mesmo símbolo CTimeSeries, que estamos considerando, é o local adequando para determinar os eventos de todas as suas listas-séries temporais, pois este é o método de atualização para as séries temporizadas especificadas Refresh() e o método de atualização para todas as séries temporais do símbolo RefreshAll().

Vejamos a implementação de métodos para atualizar listas-séries temporais:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeries::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
   CSeries *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(0),series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeries::RefreshAll(SDataCalculate &data_calculate)
  {
//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal
//--- and the timeseries event flag, and clear the list of all timeseries events
   bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by the list of all used timeseries,
   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next timeseries object by the loop index
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0)
         continue;
      //--- 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 flag indicating the necessity to set the first date in history on the server and in the terminal
         upd=true;
         //--- 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(0),series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled,
//--- set the values of the first date in history on the server and in the terminal
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

Aqui, comentei cada linha do código do método, e espero que tenham ficado claras. De qualquer forma, na discussão do artigo podem ser feitas todas as perguntas que surgirem.

Assim concluímos a classe de objeto de todas as séries temporais de um mesmo símbolo CTimeSeries.

A próxima classe é uma classe-coleção de objetos de séries temporais de símbolos CTimeSeriesCollection que, da mesma forma, deve ser dotada de funcionalidade orientada a eventos, pois é responsável por receber listas de eventos de todos os objetos que armazenam todas as séries temporais de cada símbolo usado no programa.

Abrimos o arquivo \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh e o herdamos da classe base estendida de todos os objetos de biblioteca:

//+------------------------------------------------------------------+
//| Symbol timeseries collection                                     |
//+------------------------------------------------------------------+
class CTimeSeriesCollection : public CBaseObjExt
  {

Na seção pública da classe, declaramos um método para retornar o objeto de todas as séries temporais do símbolo especificado e outro para retornar um objeto-série temporal do símbolo e período especificados:

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
   CTimeSeries            *GetTimeseries(const string symbol);
   CSeries                *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

Fora do corpo da classe, escrevemos imediatamente sua implementação.
Método que retorna o objeto de séries temporais do símbolo especificado:

//+------------------------------------------------------------------+
//| Return the timeseries object of the specified symbol             |
//+------------------------------------------------------------------+
CTimeSeries *CTimeSeriesCollection::GetTimeseries(const string symbol)
  {
   int index=this.IndexTimeSeries(symbol);
   if(index==WRONG_VALUE)
      return NULL;
   CTimeSeries *timeseries=this.m_list.At(index);
   return timeseries;
  }
//+------------------------------------------------------------------+

Aqui, obtemos o índice do objeto de séries temporais pelo nome do símbolo através do método IndexTimeSeries(), que revisamos na parte 37, que descrevia a criação de uma biblioteca. De acordo com o índice recebido obtemos o objeto de séries temporais a partir da lista. Se um índice ou objeto da lista não for obtido com sucesso, será retornado o valor NULL, caso contrário, um ponteiro para o objeto solicitado na lista.

Método que retorna um objeto-série temporal do símbolo/período especificado:

//+------------------------------------------------------------------+
//| Return the timeseries object of the specified symbol/period      |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return NULL;
   CSeries *series=timeseries.GetSeries(timeframe);
   return series;
  }
//+-----------------------------------------------------------------------+

Aqui, obtemos o objeto de séries temporais usando o método GetTimeseries() discutido acima de acordo com o símbolo passado para o método.
Do objeto de séries temporais recebido, obtemos a lista-série temporal pelo período especificado e retornamos um ponteiro para o objeto-série temporal recebido.

Para retornar a série temporal necessária, o método GetSeries() do objeto de séries temporais usa o método IndexTimeframe() discutido acima. O método GetSeries() do objeto de séries temporais CTimeSeries fica assim:

CSeries *GetSeries(const ENUM_TIMEFRAMES timeframe) { return this.m_list_series.At(this.IndexTimeframe(timeframe)); }

Da seção pública da classe removemos os três métodos para criar séries temporais, deixando apenas um para criar as séries temporais especificadas do símbolo especificado:

//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0);
   bool                    CreateSeries(const string symbol,const uint required=0);
   bool                    CreateSeries(const uint required=0);
//--- Update (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols,
//--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols and (5) all timeseries except for the current symbol

Até agora, três métodos remotos parecem redundantes e, em vez deles, declararemos três novos métodos: para recriar as séries temporais especificadas, para retornar uma série temporal vazia e para retornar as séries temporais incompletas:

//--- (1) Create and (2) re-create a specified timeseries of a specified symbol
   bool                    CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
   bool                    ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0);
//--- Return (1) an empty, (2) partially filled timeseries
   CSeries                *GetSeriesEmpty(void);
   CSeries                *GetSeriesIncompleted(void);

Por que é necessário recriar uma série temporal? Ao inicializar a biblioteca e criar todas as séries temporais de todos os símbolos usados, passamos às funções que iniciam o carregamento de dados históricos. Mas, como dissemos mais de uma vez, se o programa for um indicador e tentar usar o símbolo e período gráfico em que estará sendo executado, poderemos obter um erro. Portanto, essas situações são ignoradas e, após concluir a inicialização e entrar no manipulador OnCalculate(), a primeira coisa que devemos fazer é examinar a série temporal criada, obter uma vazia (ignorada durante a inicialização) e recriá-la usando os dados da variável rates_total em OnCalculate().

Agora aos métodos de atualização de séries temporais não transferiremos dados das matrizes de séries temporais desde OnCalculate(), em vez disso, passaremos a estrutura desses dados, e declararemos o método para obter eventos desde o objeto de séries temporais e para adicioná-los na lista de eventos todos os objetos da coleção de séries temporais dos símbolos:

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

//--- Get events from the timeseries object and add them to the list
   bool                    SetEvents(CTimeSeries *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);
   
//--- Constructor
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

Implementação de métodos que retornam uma série temporal vazia e incompleta:

//+------------------------------------------------------------------+
//|Return the empty (created but not filled with data) timeseries    |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesEmpty(void)
  {
//--- In the loop by the timeseries object list
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all symbol timeseries by the loop index
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- get the list of timeseries objects from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- in the loop by the symbol timeseries list
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- if the timeseries has no bar objects,

         //--- return the pointer to the timeseries
         if(series.DataTotal()==0)
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+
//| Return partially filled timeseries                               |
//+------------------------------------------------------------------+
CSeries *CTimeSeriesCollection::GetSeriesIncompleted(void)
  {
//--- In the loop by the timeseries object list
   int total_timeseries=this.m_list.Total();
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all symbol timeseries by the loop index
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL || !timeseries.IsAvailable())
         continue;
      //--- get the list of timeseries objects from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- in the loop by the symbol timeseries list
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries
         CSeries *series=list_series.At(j);
         if(series==NULL || !series.IsAvailable())
            continue;
         //--- if the timeseries has bar objects,
         //--- but their number is not equal to the requested and available one for the symbol,
         //--- return the pointer to the timeseries
         if(series.DataTotal()>0 && series.AvailableUsedData()!=series.DataTotal())
            return series;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

Cada linha dos métodos é comentada, os métodos são exatamente os mesmos, exceto aquele que verifica se a série está vazia e incompleta.

Métodos que retornam as primeiras séries temporais que atendem às condições de pesquisa. Isso foi feito de propósito para em cada tick receber todas as séries temporais vazias ou incompletas possíveis (entrada em OnCalculate), o que corresponde às recomendações da MetaQuotes para o processamento correto da falta de dados nos indicadores (sair do manipulador e verificar os dados no próxima tick).

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

//+------------------------------------------------------------------+
//| Create the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.AddSeries(timeframe,required))
      return false;
   if(!timeseries.SyncData(timeframe,required,rates_total))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

O método adiciona ao objeto das séries temporais do símbolo uma nova série temporal com o período gráfico especificado.
Ao método são transferidos o símbolo e o período da série temporal necessária.
Obtemos o objeto de séries temporais e adicionamos a ele uma nova série temporal do período gráfico especificado.
Solicitamos dados do símbolo/período e definimos a quantidade de dados necessária na série temporal.
Se todas as etapas anteriores forem bem-sucedidas, retornaremos o resultado da criação de uma nova série temporal e da adição de dados a ela.

Todos esses métodos foram considerados por nós em artigos anteriores, no entanto, ao contrário do lógica descrita no artigo 37, aqui apenas criamos uma lógica para criar a série temporal necessária do símbolo/período em questão.

Implementação do método de reconstrução das séries temporais especificadas do símbolo especificado:

//+------------------------------------------------------------------+
//| Re-create a specified timeseries of a specified symbol           |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
  {
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return false;
   if(!timeseries.SyncData(timeframe,rates_total,required))
      return false;
   return timeseries.CreateSeries(timeframe,required);
  }
//+------------------------------------------------------------------+

Aqui tudo é exatamente o mesmo, com apenas uma diferença, isto é, a série temporal já está criada, portanto, a etapa de adicionar novas séries temporais ao objeto de todas as séries temporais do símbolo é ignorada.

Implementação de um método que recebe eventos a partir de um objeto de séries temporais e sua adição à lista de eventos da coleção de séries temporais:

//+------------------------------------------------------------------+
//| Get events from the timeseries object and add them to the list   |
//+------------------------------------------------------------------+
bool CTimeSeriesCollection::SetEvents(CTimeSeries *timeseries)
  {
//--- Set the flag of successfully adding an event to the list and
//--- get the list of symbol timeseries object events
   bool res=true;
   CArrayObj *list=timeseries.GetListEvents();
   if(list==NULL)
      return false;
//--- In the loop by the obtained list of events,
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next event by the loop index and
      CEventBaseObj *event=timeseries.GetEvent(i);
      if(event==NULL)
         continue;
      //--- add the result of adding the obtained event to the flag value
      //--- from the symbol timeseries list to the timeseries collection list
      res &=this.EventAdd(event.ID(),event.LParam(),event.DParam(),event.SParam());
     }
//--- Return the result of adding events to the list
   return res;
  }
//+------------------------------------------------------------------+

Ao método é transferido o ponteiro para um objeto de séries temporais do símbolo, e, num ciclo pela lista de eventos desse objeto, todos os seus eventos são adicionados à lista de eventos da coleção de séries temporais.

Implementação do método para atualizar a série temporal especificada do símbolo especificado e adição de seus eventos à lista de eventos da coleção de séries temporais:

//+------------------------------------------------------------------+
//| 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
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- If there is no new tick on the timeseries object symbol, exit
   if(!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);
  }
//+------------------------------------------------------------------+

Implementação do método de atualização de todas as séries temporais de todos os símbolos e inclusão de seus eventos na lista de eventos da coleção de séries temporais:

//+------------------------------------------------------------------+
//| Update all timeseries of all symbols                             |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(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();
//--- In the loop by all symbol timeseries objects in the collection,
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next symbol timeseries object
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(!timeseries.IsNewTick())
         continue;
      //--- Update all symbol timeseries
      timeseries.RefreshAll(data_calculate);
      //--- If the event flag enabled for the symbol timeseries object,
      //--- 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);
     }
  }
//+------------------------------------------------------------------+

Todos esses métodos são comentados em detalhes, portanto não acho que sua lógica crie confusões.

Assim concluímos a modificação de todas as classes de séries temporais nesta fase.

Agora vamos modificar o objeto principal da biblioteca CEngine (\MQL5\Include\DoEasy\Engine.mqh) para trabalhar com uma coleção de séries temporais desde programas.

Na seção privada da classe declaramos um objeto-pausa:

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

À seção pública da classe adicionamos um método que retorne um sinalizador indicando presença de evento na coleção de séries temporais:

//--- Return the (1) hedge account, (2) working in the tester, (3) account event, (4) symbol event and (5) trading event flag
   bool                 IsHedge(void)                             const { return this.m_is_hedge;                             }
   bool                 IsTester(void)                            const { return this.m_is_tester;                            }
   bool                 IsAccountsEvent(void)                     const { return this.m_accounts.IsEvent();                   }
   bool                 IsSymbolsEvent(void)                      const { return this.m_symbols.IsEvent();                    }
   bool                 IsTradeEvent(void)                        const { return this.m_events.IsEvent();                     }
   bool                 IsSeriesEvent(void)                       const { return this.m_time_series.IsEvent();                }

O método retorna o resultado de funcionamento do método IsEvent () do objeto-coleção de séries temporais.

Agora como para os métodos de atualização de séries temporais precisamos enviar dados de matrizes a partir do manipulador OnCalculate() do indicador para processar os dados da série temporal atual, aos métodos de processamento de eventos Timer e Tick adicionaremos a transferência de estruturas de dados das matrizes OnCalculate() e, ao mesmo tempo, declararemos um método para processar o evento Calculate:

//--- (1) Timer, (2) NewTick event handler and (3) Calculate event handler
   void                 OnTimer(SDataCalculate &data_calculate);
   void                 OnTick(SDataCalculate &data_calculate,const uint required=0);
   int                  OnCalculate(SDataCalculate &data_calculate,const uint required=0);

Na mesma seção pública da classe escrevemos um método que retorne uma lista de eventos de séries temporais:

//--- Return (1) the timeseries collection and (2) the list of timeseries from the timeseries collection and (3) the list of timeseries events
   CTimeSeriesCollection *GetTimeSeriesCollection(void)                       { return &this.m_time_series;                                     }
   CArrayObj           *GetListTimeSeries(void)                               { return this.m_time_series.GetList();                            }
   CArrayObj           *GetListSeriesEvents(void)                             { return this.m_time_series.GetListEvents();                      }

O método retorna um ponteiro para a lista de eventos da coleção de séries temporais com ajuda do método de coleção de séries temporais GetListEvents()

Na seção pública da classe, temos quatro métodos para criar diferentes séries temporais. Vamos remover três métodos que ainda não são necessários:

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

e em vez deles escrevemos a declaração do método para criar todas as séries temporais de todos os símbolos usados da coleção, escrevemos um método para recriar a série temporal especificada e declaramos um método para solicitar que as séries temporais sejam sincronizadas com o servidor:

//--- Create (1) the specified timeseries of the specified symbol and (2) all used timeseries of all used symbols
   bool                 SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.CreateSeries(symbol,timeframe,rates_total,required);        }
   bool                 SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0);
//--- Re-create a specified timeseries of a specified symbol
   bool                 SeriesReCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0)
                          { return this.m_time_series.ReCreateSeries(symbol,timeframe,rates_total,required);      }
//--- Synchronize timeseries data with the server
   void                 SeriesSync(SDataCalculate &data_calculate,const uint required=0);

Agora, temos quatro métodos para atualizar a coleção de séries temporais.
Vamos deixar apenas dois métodos, o usado para atualizar a série temporal especificada e aquele para atualizar todas as séries temporais da coleção:

//--- Update (1) the specified timeseries of the specified symbol, (2) 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(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }

Agora, em vez de valores de matriz OnCalculate(), para os métodos é passada uma estrutura com dados de variáveis e de matrizes OnCalculate().

E adicionamos mais quatro métodos, um para retornar um ponteiro para um objeto de séries temporais do símbolo especificado, outro para o objeto-série temporal especificado, bem como métodos para retornar ponteiros para séries temporais vazias e incompletas:

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

Métodos retornam o resultado do retorno dos métodos da coleção de séries temporais com o mesmo nome, que examinamos acima.

O método TradingOnInit() que transfere à classe de negociação os ponteiros para todas as coleções necessárias foi renomeado para CollectionOnInit(), nele produziremos as inicializações necessárias de todas as classes-coleções.

No final do código do corpo da classe vamos inserir um bloco com métodos para trabalhar com o objeto-pausa:

//--- Set the new (1) pause countdown start time and (2) pause in milliseconds
   void                 PauseSetTimeBegin(const ulong time)             { this.m_pause.SetTimeBegin(time);                    }
   void                 PauseSetWaitingMSC(const ulong pause)           { this.m_pause.SetWaitingMSC(pause);                  }
//--- Return (1) the time passed from the pause countdown start in milliseconds, (2) waiting completion flag
//--- (3) pause countdown start time, (4) pause in milliseconds
   ulong                PausePassed(void)                         const { return this.m_pause.Passed();                       }
   bool                 PauseIsCompleted(void)                    const { return this.m_pause.IsCompleted();                  }
   ulong                PauseTimeBegin(void)                      const { return this.m_pause.TimeBegin();                    }
   ulong                PauseTimeWait(void)                       const { return this.m_pause.TimeWait();                     }
//--- Return the description (1) of the time passed till the countdown starts in milliseconds,
//--- (2) pause countdown start time, (3) pause in milliseconds
   string               PausePassedDescription(void)              const { return this.m_pause.PassedDescription();            }
   string               PauseTimeBeginDescription(void)           const { return this.m_pause.TimeBeginDescription();         }
   string               PauseWaitingMSCDescription(void)          const { return this.m_pause.WaitingMSCDescription();        }
   string               PauseWaitingSECDescription(void)          const { return this.m_pause.WaitingSECDescription();        }
//--- Launch the new pause countdown
   void                 Pause(const ulong pause_msc,const datetime time_start=0)
                          {
                           this.PauseSetWaitingMSC(pause_msc);
                           this.PauseSetTimeBegin(time_start*1000);
                           while(!this.PauseIsCompleted() && !IsStopped()){}
                          }

//--- Constructor/destructor
                        CEngine();
                       ~CEngine();

A classe "Pausa" foi revisada por nós no artigo 30, e destina-se a organizar pausas em vez da função Sleep(), que não funciona em indicadores.

Aqui, juntamente com os métodos já descritos pela classe CPause que são chamados a partir desses métodos, adicionamos outro método Pause(), que imediatamente permite iniciar uma nova pausa pendente sem primeiro inicializar seus parâmetros, pois todos os parâmetros são passados para o método, e dentro do método é realizado um ciclo que espera que o número de milissegundos de pausa seja concluído, milissegundos esses que são passados para o método como um parâmetro de entrada. Esses métodos podem ser úteis em programas para realizar pausas em indicadores.

Você não deve esquecer que esse objeto-pausa também atrasa o fluxo principal no qual o indicador está sendo executado, como a função Sleep(),
e necessário aplicar essa pausa nos indicadores onde for justificado.

O temporizador da classe CEngine foi modificado. Anteriormente para cada manipulador de cada coleção, era verificado onde estávamos trabalhando (no testador ou não). Isso obrigava cada manipulador de todas as coleções a realizar essas verificações, o que não faz muito sentido.
Agora, primeiro é verificado se estamos trabalhando não no testador ou no testador, e depois dentro dos blocos, para não testador e testador, é realizado o processamento de todas as coleções:

//+------------------------------------------------------------------+
//| CEngine timer                                                    |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//--- If this is not a tester, work with collection events by timer
   if(!this.IsTester())
     {
   //--- Timer of the collections of historical orders and deals, as well as of market orders and positions
      int index=this.CounterIndex(COLLECTION_ORD_COUNTER_ID);
      CTimerCounter* cnt1=this.m_list_counters.At(index);
      if(cnt1!=NULL)
        {
         //--- If unpaused, work with the order, deal and position collections events
         if(cnt1.IsTimeDone())
            this.TradeEventsControl();
        }
   //--- Account collection timer
      index=this.CounterIndex(COLLECTION_ACC_COUNTER_ID);
      CTimerCounter* cnt2=this.m_list_counters.At(index);
      if(cnt2!=NULL)
        {
         //--- If unpaused, work with the account collection events
         if(cnt2.IsTimeDone())
            this.AccountEventsControl();
        }
   //--- Timer 1 of the symbol collection (updating symbol quote data in the collection)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID1);
      CTimerCounter* cnt3=this.m_list_counters.At(index);
      if(cnt3!=NULL)
        {
         //--- If the pause is over, update quote data of all symbols in the collection
         if(cnt3.IsTimeDone())
            this.m_symbols.RefreshRates();
        }
   //--- Timer 2 of the symbol collection (updating all data of all symbols in the collection and tracking symbl and symbol search events in the market watch window)
      index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID2);
      CTimerCounter* cnt4=this.m_list_counters.At(index);
      if(cnt4!=NULL)
        {
         //--- If the pause is over
         if(cnt4.IsTimeDone())
           {
            //--- update data and work with events of all symbols in the collection
            this.SymbolEventsControl();
            //--- When working with the market watch list, check the market watch window events
            if(this.m_symbols.ModeSymbolsList()==SYMBOLS_MODE_MARKET_WATCH)
               this.MarketWatchEventsControl();
           }
        }
   //--- Trading class timer
      index=this.CounterIndex(COLLECTION_REQ_COUNTER_ID);
      CTimerCounter* cnt5=this.m_list_counters.At(index);
      if(cnt5!=NULL)
        {
         //--- If unpaused, work with the list of pending requests
         if(cnt5.IsTimeDone())
            this.m_trading.OnTimer();
        }
   //--- Timeseries collection timer
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- If unpaused, work with the timeseries list
         if(cnt6.IsTimeDone())
            this.SeriesRefresh(data_calculate);
        }
     }
//--- If this is a tester, work with collection events by tick
   else
     {
      //--- work with events of collections of orders, deals and positions by tick
      this.TradeEventsControl();
      //--- work with events of collections of accounts by tick
      this.AccountEventsControl();
      //--- update quote data of all collection symbols by tick
      this.m_symbols.RefreshRates();
      //--- work with events of all symbols in the collection by tick
      this.SymbolEventsControl();
      //--- work with the list of pending orders by tick
      this.m_trading.OnTimer();
      //--- work with the timeseries list by tick
      this.SeriesRefresh(data_calculate);
     }
  }
//+------------------------------------------------------------------+

O manipulador agora se tornou mais compacto e com uma lógica mais compreensível, e é dispensado de verificações repetidas desnecessárias.

Método que sincroniza os dados vazios das séries temporais com o servidor e recria uma série temporal vazia:

//+------------------------------------------------------------------+
//| Synchronize timeseries data with the server                      |
//+------------------------------------------------------------------+
void 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
   CSeries *series=this.SeriesGetSeriesEmpty();
   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(0,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();
           }
        }
     }
//--- Delete all comments
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
     }
  }
//+------------------------------------------------------------------+

Este método é a pedra angular para o carregamento correto dos dados históricos de qualquer série temporal usada, para quaisquer símbolos e períodos gráficos.

O método recupera a partir da coleção de séries temporais a primeira série temporal não preenchida, o que significa que não havia dados disponíveis anteriormente e imediatamente é feita uma tentativa de sincronizar os dados dessas séries temporais com os dados no servidor. Se não der certo, sairemos do método antes do próximo tick. Se os dados forem sincronizados, essas séries temporais serão recriadas, pois elas serão preenchidas com todas as barras disponíveis (mas não mais que a quantidade solicitada) do histórico.

E assim, a cada tick, obtemos as próximas séries temporais vazias, sincronizamos e recriamos, até que não existam séries temporais vazias.

Implementação de manipuladores de eventos NewTick e Calculate:

//+------------------------------------------------------------------+
//| 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
   this.SeriesSync(data_calculate,required);
//--- end
  }
//+------------------------------------------------------------------+
//| 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
   this.SeriesSync(data_calculate,required);
//--- return rates_total
   return data_calculate.rates_total;
  }
//+------------------------------------------------------------------+

Nos dois métodos é chamado um método para recriar séries temporais vazias.
Mas os métodos em si devem ser chamados a partir dos manipuladores de programas com o mesmo nome, trabalhando com base nessa biblioteca.

Implementação do método para criar todas as séries temporais usadas de todos os símbolos usados:

//+------------------------------------------------------------------+
//| Create all applied timeseries of all used symbols                |
//+------------------------------------------------------------------+
bool CEngine::SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0)
  {
//--- Set the flag of successful creation of all timeseries of all symbols
   bool res=true;
//--- Get the list of all used symbols
   CArrayObj* list_symbols=this.GetListAllUsedSymbols();
   if(list_symbols==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY));
      return false;
     }
   //--- In the loop by the total number of symbols
   for(int i=0;i<list_symbols.Total();i++)
     {
      //--- get the next symbol object
      CSymbol *symbol=list_symbols.At(i);
      if(symbol==NULL)
        {
         ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
         continue;
        }
      //--- In the loop by the total number of used timeframes,
      int total_periods=::ArraySize(array_periods);
      for(int j=0;j<total_periods;j++)
        {
         //--- create the timeseries object of the next symbol.
         //--- Add the timeseries creation result to the res variable
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]);
         res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required);
        }
     }
//--- Return the result of creating all timeseries for all symbols
   return res;
  }
//+------------------------------------------------------------------+

O método deve ser chamado quando o programa for inicializado após a criação de uma lista de todos os símbolos usados.
Ao método é transferida a matriz criada durante a inicialização com os nomes dos períodos do gráfico usado, bem como parâmetros para criar séries temporais, isto é, o número de barras das séries temporais atuais (apenas para indicadores - rates_total) e a profundidade necessária do histórico para séries temporais criadas (por padrão, 1000, mas não mais que o valor de Bars() do símbolo, para indicadores, não mais que rates_total).

Essas são todas as modificações necessárias para trabalhar com séries temporais hoje.


Testando o funcionamento de séries temporais e seus eventos em indicadores

Para testar o funcionamento da classe-coleção de séries temporais nos indicadores, no diretório de indicadores do terminal criaremos uma nova pasta
\MQL5\Indicators\TestDoEasy\ e, nela, uma subpasta Part39\, nela geraremos um novo indicador chamado de TestDoEasyPart39.mq5.

O número e o tipo de buffers de indicador plotados até agora não importam para nós, pois não vamos desenhar nada nele. Mas para o futuro, defino dois buffers desenháveis com um tipo de plotagem DRAW_LINE.

Os parâmetros de entrada necessários do indicador para definir os símbolos e períodos desejados, e alguns outros,
foram transferidos desde o EA de teste do artigo anterior. Este foi o resultado:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart39.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- enums
//--- defines
//--- structures
//--- properties
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Label2
#property indicator_label2  "Label2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGreen
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- indicator buffers
double         Buffer1[];
double         Buffer2[];
//--- 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   bool              InpUseSounds         =  true; // Use sounds
//--- 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
//+------------------------------------------------------------------+

No manipulador OnInit() do indicador, escrevemos a definição das variáveis globais do indicador e uma chamada para a função de inicialização da biblioteca:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,Buffer1,INDICATOR_DATA);
   SetIndexBuffer(1,Buffer2,INDICATOR_DATA);

//--- Set indicator global variables
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   testing=engine.IsTester();
   ZeroMemory(rates_data);
   
//--- Initialize DoEasy library
   OnInitDoEasy();

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

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

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Tomamos o manipulador OnDeinit() do indicador a partir do EA de teste do artigo anterior:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Remove indicator graphical objects by an object name prefix
   ObjectsDeleteAll(0,prefix);
   Comment("");
  }
//+------------------------------------------------------------------+

Também tomamos os manipuladores OnTimer() e OnChartEvent() a partir do EA:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Launch the library timer (only not in the tester)
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer(rates_data);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Handling mouse events
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Handling pressing the buttons in the panel
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Handling DoEasy library events
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     } 
  }
//+------------------------------------------------------------------+

Para preencher as estruturas de dados de matrizes e variáveis do primeiro e segundo formulário OnCalculate() do indicador, criamos duas funções:

//+------------------------------------------------------------------+
//| Copy data from the first OnCalculate() form to the structure     |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              const int rates_total,
              const int prev_calculated,
              const int begin,
              const double &price[])
  {
//--- Get the array indexing flag as in the timeseries. If failed,
//--- set the indexing direction for the array as in the timeseries
   bool as_series_price=ArrayGetAsSeries(price);
   if(!as_series_price)
      ArraySetAsSeries(price,true);
//--- Copy the array zero bar to the OnCalculate() SDataCalculate data structure
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.begin=begin;
   data_calculate.price=price[0];
//--- Return the array's initial indexing direction
   if(!as_series_price)
      ArraySetAsSeries(price,false);
  }
//+------------------------------------------------------------------+
//| Copy data from the second OnCalculate() form to the structure    |
//+------------------------------------------------------------------+
void CopyData(SDataCalculate &data_calculate,
              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[])
  {
//--- Get the array indexing flags as in the timeseries. If failed,
//--- set the indexing direction or the arrays as in the timeseries
   bool as_series_time=ArrayGetAsSeries(time);
   if(!as_series_time)
      ArraySetAsSeries(time,true);
   bool as_series_open=ArrayGetAsSeries(open);
   if(!as_series_open)
      ArraySetAsSeries(open,true);
   bool as_series_high=ArrayGetAsSeries(high);
   if(!as_series_high)
      ArraySetAsSeries(high,true);
   bool as_series_low=ArrayGetAsSeries(low);
   if(!as_series_low)
      ArraySetAsSeries(low,true);
   bool as_series_close=ArrayGetAsSeries(close);
   if(!as_series_close)
      ArraySetAsSeries(close,true);
   bool as_series_tick_volume=ArrayGetAsSeries(tick_volume);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,true);
   bool as_series_volume=ArrayGetAsSeries(volume);
   if(!as_series_volume)
      ArraySetAsSeries(volume,true);
   bool as_series_spread=ArrayGetAsSeries(spread);
   if(!as_series_spread)
      ArraySetAsSeries(spread,true);
//--- Copy the arrays' zero bar to the OnCalculate() SDataCalculate data structure
   data_calculate.rates_total=rates_total;
   data_calculate.prev_calculated=prev_calculated;
   data_calculate.rates.time=time[0];
   data_calculate.rates.open=open[0];
   data_calculate.rates.high=high[0];
   data_calculate.rates.low=low[0];
   data_calculate.rates.close=close[0];
   data_calculate.rates.tick_volume=tick_volume[0];
   data_calculate.rates.real_volume=(#ifdef __MQL5__ volume[0] #else 0 #endif);
   data_calculate.rates.spread=(#ifdef __MQL5__ spread[0] #else 0 #endif);
//--- Return the arrays' initial indexing direction
   if(!as_series_time)
      ArraySetAsSeries(time,false);
   if(!as_series_open)
      ArraySetAsSeries(open,false);
   if(!as_series_high)
      ArraySetAsSeries(high,false);
   if(!as_series_low)
      ArraySetAsSeries(low,false);
   if(!as_series_close)
      ArraySetAsSeries(close,false);
   if(!as_series_tick_volume)
      ArraySetAsSeries(tick_volume,false);
   if(!as_series_volume)
      ArraySetAsSeries(volume,false);
   if(!as_series_spread)
      ArraySetAsSeries(spread,false);
  }
//+------------------------------------------------------------------+

Também transferiremos desde o EA de teste a função de processamento de eventos da biblioteca DoEasy:

//+------------------------------------------------------------------+
//| Handling DoEasy library events                                   |
//+------------------------------------------------------------------+
void OnDoEasyEvent(const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
  {
   int idx=id-CHARTEVENT_CUSTOM;
//--- Retrieve (1) event time milliseconds, (2) reason and (3) source from lparam, as well as (4) set the exact event time
   ushort msc=engine.EventMSC(lparam);
   ushort reason=engine.EventReason(lparam);
   ushort source=engine.EventSource(lparam);
   long time=TimeCurrent()*1000+msc;
   
//--- Handling symbol events
   if(source==COLLECTION_SYMBOLS_ID)
     {
      CSymbol *symbol=engine.GetSymbolObjByName(sparam);
      if(symbol==NULL)
         return;
      //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol
      int digits=(idx<SYMBOL_PROP_INTEGER_TOTAL ? 0 : symbol.Digits());
      //--- Event text description
      string id_descr=(idx<SYMBOL_PROP_INTEGER_TOTAL ? symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_INTEGER)idx) : symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_DOUBLE)idx));
      //--- Property change text value
      string value=DoubleToString(dparam,digits);
      
      //--- Check event reasons and display its description in the journal
      if(reason==BASE_EVENT_REASON_INC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     }   
     
//--- Handling account events
   else if(source==COLLECTION_ACCOUNT_ID)
     {
      CAccount *account=engine.GetAccountCurrent();
      if(account==NULL)
         return;
      //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol
      int digits=int(idx<ACCOUNT_PROP_INTEGER_TOTAL ? 0 : account.CurrencyDigits());
      //--- Event text description
      string id_descr=(idx<ACCOUNT_PROP_INTEGER_TOTAL ? account.GetPropertyDescription((ENUM_ACCOUNT_PROP_INTEGER)idx) : account.GetPropertyDescription((ENUM_ACCOUNT_PROP_DOUBLE)idx));
      //--- Property change text value
      string value=DoubleToString(dparam,digits);
      
      //--- Checking event reasons and handling the increase of funds by a specified value,
      
      //--- In case of a property value increase
      if(reason==BASE_EVENT_REASON_INC)
        {
         //--- Display an event in the journal
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
         //--- if this is an equity increase
         if(idx==ACCOUNT_PROP_EQUITY)
           {
            //--- Get the list of all open positions for the current symbol
            CArrayObj* list_positions=engine.GetListMarketPosition();
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_SYMBOL,Symbol(),EQUAL);
            //--- Select positions with the profit exceeding zero
            list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_PROFIT_FULL,0,MORE);
            if(list_positions!=NULL)
              {
               //--- Sort the list by profit considering commission and swap
               list_positions.Sort(SORT_BY_ORDER_PROFIT_FULL);
               //--- Get the position index with the highest profit
               int index=CSelect::FindOrderMax(list_positions,ORDER_PROP_PROFIT_FULL);
               if(index>WRONG_VALUE)
                 {
                  COrder* position=list_positions.At(index);
                  if(position!=NULL)
                    {
                     //--- Get a ticket of a position with the highest profit and close the position by a ticket
                     engine.ClosePosition(position.Ticket());
                    }
                 }
              }
           }
        }
      //--- Other events are simply displayed in the journal
      if(reason==BASE_EVENT_REASON_DEC)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_MORE_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_LESS_THEN)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
      if(reason==BASE_EVENT_REASON_EQUALS)
        {
         Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits));
        }
     } 
     
//--- Handling market watch window events
   else if(idx>MARKET_WATCH_EVENT_NO_EVENT && idx<SYMBOL_EVENTS_NEXT_CODE)
     {
      //--- Market Watch window event
      string descr=engine.GetMWEventDescription((ENUM_MW_EVENT)idx);
      string name=(idx==MARKET_WATCH_EVENT_SYMBOL_SORT ? "" : ": "+sparam);
      Print(TimeMSCtoString(lparam)," ",descr,name);
     }
     
//--- Handling timeseries events
   else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE)
     {
      //--- "New bar" event
      if(idx==SERIES_EVENTS_NEW_BAR)
        {
         Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam));
        }
     }
     
//--- Handling trading events
   else if(idx>TRADE_EVENT_NO_EVENT && idx<TRADE_EVENTS_NEXT_CODE)
     {
      //--- Get the list of trading events
      CArrayObj *list=engine.GetListAllOrdersEvents();
      if(list==NULL)
         return;
      //--- get the event index shift relative to the end of the list
      //--- in the tester, the shift is passed by the lparam parameter to the event handler
      //--- outside the tester, events are sent one by one and handled in OnChartEvent()
      int shift=(testing ? (int)lparam : 0);
      CEvent *event=list.At(list.Total()-1-shift);
      if(event==NULL)
      return;
      //--- Accrue the credit
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CREDIT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Additional charges
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CHARGE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Correction
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CORRECTION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Enumerate bonuses
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BONUS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Additional commissions
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Daily commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Monthly commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Daily agent commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Monthly agent commission
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Interest rate
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_INTEREST)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Canceled buy deal
      if(event.TypeEvent()==TRADE_EVENT_BUY_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Canceled sell deal
      if(event.TypeEvent()==TRADE_EVENT_SELL_CANCELLED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Dividend operations
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Accrual of franked dividend
      if(event.TypeEvent()==TRADE_EVENT_DIVIDENT_FRANKED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Tax charges
      if(event.TypeEvent()==TRADE_EVENT_TAX)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Replenishing account balance
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_REFILL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Withdrawing funds from balance
      if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      
      //--- Pending order placed
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_PLASED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order removed
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_REMOVED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order activated by price
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Pending order partially activated by price
      if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position opened
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position opened partially
      if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by an opposite one
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed by TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by a new deal (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by partial market order execution (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position reversal by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by a new deal (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by partial execution of a market order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by activating a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Added volume to a position by partial activation of a pending order (netting)
      if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position partially closed by an opposite one
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially by StopLoss
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Position closed partially by TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- StopLimit order activation
      if(event.TypeEvent()==TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order and StopLoss price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order, StopLoss and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's StopLoss and TakeProfit price
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's StopLoss
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing order's TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position's StopLoss and TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position StopLoss
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL)
        {
         Print(DFUN,event.TypeEventDescription());
        }
      //--- Changing position TakeProfit
      if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_TP)
        {
         Print(DFUN,event.TypeEventDescription());
        }
     }
  }
//+------------------------------------------------------------------+

Também transferiremos desde o EA a função para trabalhar com eventos da biblioteca no testador:

//+------------------------------------------------------------------+
//| Working with events in the tester                                |
//+------------------------------------------------------------------+
void EventsHandling(void)
  {
//--- If a trading event is present
   if(engine.IsTradeEvent())
     {
      //--- Number of trading events occurred simultaneously
      int total=engine.GetTradeEventsTotal();
      for(int i=0;i<total;i++)
        {
         //--- Get the next event from the list of simultaneously occurred events by index
         CEventBaseObj *event=engine.GetTradeEventByIndex(i);
         if(event==NULL)
            continue;
         long   lparam=i;
         double dparam=event.DParam();
         string sparam=event.SParam();
         OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
        }
     }
//--- If there is an account event
   if(engine.IsAccountsEvent())
     {
      //--- Get the list of all account events occurred simultaneously
      CArrayObj* list=engine.GetListAccountEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- If there is a symbol collection event
   if(engine.IsSymbolsEvent())
     {
      //--- Get the list of all symbol events occurred simultaneously
      CArrayObj* list=engine.GetListSymbolsEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
//--- If there is a timeseries collection event
   if(engine.IsSeriesEvent())
     {
      //--- Get the list of all timeseries events occurred simultaneously
      CArrayObj* list=engine.GetListSeriesEvents();
      if(list!=NULL)
        {
         //--- Get the next event in a loop
         int total=list.Total();
         for(int i=0;i<total;i++)
           {
            //--- take an event from the list
            CEventBaseObj *event=list.At(i);
            if(event==NULL)
               continue;
            //--- Send an event to the event handler
            long lparam=event.LParam();
            double dparam=event.DParam();
            string sparam=event.SParam();
            OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Por enquanto, é possível não transferir as funções para trabalhar com botões do painel de negociação, porém, para o futuro, para poder usar alguns botões no indicador, também transferiremos desde o EA essas funções com pequenas alterações (dois botões):

//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
//--- Button 1
   if(name=="BUTT_1")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
//--- Button 2
   if(name=="BUTT_2")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'255,220,90');
      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));
   //--- If the button is pressed
   if(ButtonState(button_name))
     {
      //--- If button 1 is pressed
      if(button=="BUTT_1")
        {

        }
      //--- If button 2 is pressed
      else if(button=="BUTT_2")
        {

        }
      //--- Wait for 1/10 of a second
      engine.Pause(100);
      //--- "Unpress" the button (if this is neither a trailing button, nor the buttons enabling pending requests)
      ButtonState(button_name,false);
      //--- re-draw the chart
      ChartRedraw();
     }
   //--- Not pressed
   else 
     {
      //--- button 1
      if(button=="BUTT_1")
        {
         ButtonState(button_name,false);
        }
      //--- button 2
      if(button=="BUTT_2")
        {
         ButtonState(button_name,false);
        }
      //--- re-draw the chart
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

Como se pode ver, a maioria das funções do EA pode ser usada em indicadores, sem problemas e alterações. Isso faz pensar que todas as funções realmente necessárias para trabalhar com a biblioteca de EAs e indicadores precisarão ser transferidas para o arquivo incluído da biblioteca, e, a partir daí, começar a usar. Mas, isso será depois. Agora precisamos criar o manipulador OnCalculate() do indicador.

O manipulador consistirá no bloco de código necessário para preparar dados da biblioteca e num bloco de código opcional (hoje) para trabalhar com o 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:           |
//+------------------------------------------------------------------+
//--- Arrange resource-saving indicator calculations
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Setting buffer arrays as timeseries
   ArraySetAsSeries(Buffer1,true);
   ArraySetAsSeries(Buffer2,true);

//--- Check for the minimum number of bars for calculation
   if(rates_total<2 || Point()==0) return 0;

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;
   if(limit>1)
     {
      limit=rates_total-1;
      ArrayInitialize(Buffer1,EMPTY_VALUE);
      ArrayInitialize(Buffer2,EMPTY_VALUE);
     }
//--- Prepare data
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // the code for preparing indicator calculation buffers
     }

//--- Calculate the indicator
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      Buffer1[i]=high[i];
      Buffer2[i]=low[i];
     }

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Como podemos ver, tudo relacionado ao trabalho da biblioteca se encaixa num pequeno bloco de código no manipulador OnCalculate() e, de fato, a única diferença com o EA é que preenchemos previamente a estrutura de preços dos dados atuais das matrizes desde OnCalculate() com a função CopyData(), e tudo o restante é absolutamente idêntico ao trabalho no EA - a biblioteca funciona no temporizador se o indicador estiver sendo executado no gráfico de símbolos e no OnCalculate() por ticks, se o indicador estiver sendo executado no testador.
Os buffers do indicador na parte calculada do OnCalculate() são simplesmente preenchidos com os dados das matrizes high[] e low[].

O código completo do indicador pode ser visualizado nos arquivos anexados no final do artigo.

Compilaremos o indicador e o iniciaremos no gráfico do símbolo, com o qual não trabalhamos há muito tempo, depois definir o símbolo atual nas configurações e selecionar a lista de períodos gráficos desejada. A execução de símbolos longos não utilizados fará com que o indicador carregue os dados ausentes e relate isso no log e no gráfico:


Aqui vemos que, a cada novo tick, as próximas séries temporais vazias foram sincronizadas e criadas. Nesse caso, as seguintes entradas foram gravadas no log:

Account 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10425.23 USD, 1:100, Hedge, MetaTrader 5 demo
--- Initializing "DoEasy" library ---
Working with the current symbol only: "USDCAD"
Working with the specified timeframe list:
"M1"  "M5"  "M15" "M30" "H1"  "H4"  "D1"  "W1"  "MN1"
USDCAD symbol timeseries: 
- Timeseries "USDCAD" M1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M5: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M15: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" M30: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" H1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" H4: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" D1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" W1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
- Timeseries "USDCAD" MN1: Requested: 1000, Actual: 0, Created: 0, On the server: 0
Library initialization time: 00:00:01.406
"USDCAD" M1 timeseries created successfully:
- Timeseries "USDCAD" M1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5001
"USDCAD" M5 timeseries created successfully:
- Timeseries "USDCAD" M5: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5741
"USDCAD" M15 timeseries created successfully:
- Timeseries "USDCAD" M15: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5247
"USDCAD" M30 timeseries created successfully:
- Timeseries "USDCAD" M30: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5123
"USDCAD" H1 timeseries created successfully:
- Timeseries "USDCAD" H1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6257
"USDCAD" H4 timeseries created successfully:
- Timeseries "USDCAD" H4: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6232
"USDCAD" D1 timeseries created successfully:
- Timeseries "USDCAD" D1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5003
"USDCAD" W1 timeseries created successfully:
- Timeseries "USDCAD" W1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 1403
"USDCAD" MN1 timeseries created successfully:
- Timeseries "USDCAD" MN1: Requested: 1000, Actual: 323, Created: 323, On the server: 323
New bar on USDCAD M1: 2020.03.19 12:18
New bar on USDCAD M1: 2020.03.19 12:19
New bar on USDCAD M1: 2020.03.19 12:20
New bar on USDCAD M5: 2020.03.19 12:20

Aqui vemos que ao inicializar a biblioteca todas as séries temporais solicitadas foram criadas, mas não foram preenchidas com dados devido à sua ausência. No primeiro acesso aos dados solicitados, o carregamento de dados foi iniciado pelo terminal. E após a chegada de cada tick, obtivemos o seguinte objeto-série temporal vazio, sincronizamos seus dados com o servidor e preenchemos o objeto-série temporal com os dados das barras na quantidade solicitada. Somente 323 barras estão realmente disponíveis em MN1, uma vez que todas foram adicionadas à lista-série temporal.

Agora execute o indicador no modo visual do testador com as mesmas configurações:


O testador carrega o histórico necessário para todos os períodos usados, a biblioteca relata a criação de todas as séries temporais, exceto a atual, e, na primeira entrada em OnCalculate(), as séries temporais para o símbolo e o período atuais são recriadas com êxito e, após limpar o testador, vemos como no testador são processados os eventos "Nova Barra" das séries temporais usadas.

Tudo funciona como esperado.

O que vem agora?

No próximo artigo, continuaremos trabalhando com séries temporais em indicadores, e testaremos o uso de séries temporais criadas para exibir informações no gráfico.

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