Trabalhando com séries temporais na biblioteca DoEasy (Parte 41): exemplo de indicador multissímbolo multiperíodo

3 setembro 2020, 14:08
Artyom Trishkin
0
594

Sumário


Ideia

Nos dois artigos anteriores, "criamos uma amizade" entre a biblioteca e os indicadores por etapas. Em particular, implementamos o carregamento correto de dados históricos e a atualização em tempo real de dados para as séries temporais da biblioteca. No artigo anterior, para exibir os dados na tela, colocamos os buffers do indicador numa estrutura de dados. Uma estrutura descreve um buffer de indicador plotado. Se planejarmos plotar muitos buffers no indicador, cada buffer será definido por uma estrutura, e cada estrutura do buffer será colocada num array.

Hoje, continuaremos a ver a ideia de trabalhar com buffers de indicador em estruturas, criaremos um indicador multissímbolo multiperíodo que desenhe numa subjanela um gráfico do par de moedas especificado para operação com o período gráfico selecionado na forma de velas japonesas e, gradualmente, compreenderemos a necessidade de criar classes de buffers de indicadores.

A biblioteca tem uma classe de mensagens que permite não só selecionar o idioma das mensagens exibidas por ela, senão também adicionar facilmente qualquer quantidade de idiomas personalizados para levar a cabo a escolha do mesmo. Mas até agora não temos uma opção para escolher o idioma para traduzir as descrições dos parâmetros de entrada. Após a compilação elas são exibidas apenas no idioma em que o usuário escreve o texto da descrição do parâmetro de entrada no programa.
Aqui, ao criar tal opção de idioma para descrever as variáveis de entrada do programa, não temos muita liberdade de escolha, uma vez que tem de ser apenas uma ou criar o mesmo conjunto de parâmetros de entrada para cada um dos idiomas de compilação exigidos.

Vamos pelo segundo caminho e criaremos um arquivo separado no qual colocaremos as enumerações necessárias que os parâmetros de entrada precisam para descrever constantes em dois idiomas, nomeadamente em russo e em inglês. Assim, para corrigir a descrição das constantes de enumeração, o usuário precisará traduzir independentemente as descrições das constantes do russo para o idioma de que precisa. Como o inglês é o mesmo idioma exigido para publicar produtos no serviço Mercado, deve permanecer sempre.

Modificando as classes e dados de biblioteca

Vamos fazer uma reestruturação da localização dos dados nos arquivos da biblioteca.

A estrutura através da qual transferimos os dados da barra atual do manipulador OnCalculate() desde os indicadores para a biblioteca
estão no arquivo \MQL5\Include\DoEasy\Defines.mqh.

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

Mas essa estrutura não se aplica a variáveis pré-definidas e valores estáticos, ela se encaixa mais na definição de "Dados". Por isso, vamos removê-la de Defines.mqh e defini-la no arquivo \MQL5\Include\DoEasy\Datas.mqh:

//+------------------------------------------------------------------+
//|                                                        Datas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "InpDatas.mqh"
//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define INPUT_SEPARATOR                (",")    // Separator in the inputs string
#define TOTAL_LANG                     (2)      // Number of used languages
//+------------------------------------------------------------------+
//| Structures                                                        |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                     // size of input timeseries
   int         prev_calculated;                 // number of handled bars at the previous call
   int         begin;                           // where significant data starts
   double      price;                           // current array value for calculation
   MqlRates    rates;                           // Price structure
  } rates_data;
//+------------------------------------------------------------------+
//| Arrays                                                           |
//+------------------------------------------------------------------+
string            ArrayUsedSymbols[];           // Array of used symbols' names
ENUM_TIMEFRAMES   ArrayUsedTimeframes[];        // Array of used timeframes
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+ 
//| Data sets                                                        |
//+------------------------------------------------------------------+

Acima, já falamos sobre um arquivo separado com enumerações para as variáveis de entrada dos programas. Ainda não criamos o arquivo, mas sua anexação já está escrita aqui, para não voltar a editar o arquivo Datas.mqh.

Aqui também adicionamos dois arrays no novo bloco de arrays: esses arrays estarão disponíveis num programa baseado na biblioteca e conterão listas dos símbolos e dos períodos gráficos que foram selecionados nos parâmetros de entrada do programa.

Agora vamos criar o novo arquivo \MQL5\Include\DoEasy\InpDatas.mqh para armazenar enumerações para as variáveis de entrada do programa:

//+------------------------------------------------------------------+
//|                                                     InpDatas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
//#define COMPILE_EN // Comment out the string for compilation in Russian 
//+------------------------------------------------------------------+
//| Input enumerations                                               |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| English language inputs                                          |
//+------------------------------------------------------------------+
#ifdef COMPILE_EN
//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work only with the current Symbol
   SYMBOLS_MODE_DEFINES,                              // Work with a given list of Symbols
   SYMBOLS_MODE_MARKET_WATCH,                         // Working with Symbols from the "Market Watch" window
   SYMBOLS_MODE_ALL                                   // Work with a complete list of Symbols
  };
//+------------------------------------------------------------------+
//| Mode of working with timeframes                                  |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Work only with the current timeframe
   TIMEFRAMES_MODE_LIST,                              // Work with a given list of timeframes
   TIMEFRAMES_MODE_ALL                                // Work with a complete list of timeframes
  };
//+------------------------------------------------------------------+
//| Russian language inputs                                          |
//+------------------------------------------------------------------+
#else  
//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work with the current symbol only
   SYMBOLS_MODE_DEFINES,                              // Work with the specified symbol list
   SYMBOLS_MODE_MARKET_WATCH,                         // Work with the Market Watch window symbols
   SYMBOLS_MODE_ALL                                   // Work with the full symbol list
  };
//+------------------------------------------------------------------+
//| Mode of working with timeframes                                  |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Work with the current timeframe only
   TIMEFRAMES_MODE_LIST,                              // Work with the specified timeframe list
   TIMEFRAMES_MODE_ALL                                // Work with the full timeframe list
  };
#endif 
//+------------------------------------------------------------------+

Aqui tudo é simples, definimos uma substituição de macros, e se ela existir, a compilação será realizada com enumerações cujas constantes estarão assinadas em inglês. Se a substituição de macros não existir (sua linha de declaração está comentada), a compilação será realizada com enumerações cujas constantes estarão assinadas em russo (ou em qualquer outro idioma que o usuário escolha para corrigir as descrições em russo das constantes das enumeração).

Adicionaremos novas enumerações a este arquivo conforme necessário.

No arquivo \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh da classe CTimeSeries corrigimos o erro no método que serve para adicionar o objeto de séries temporais à lista, erro esse de acesso a um ponteiro inexistente:

//+------------------------------------------------------------------+
//| Add the specified timeseries list to the list                    |
//+------------------------------------------------------------------+
bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeriesDE *series=new CSeriesDE(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;
  }
//+------------------------------------------------------------------+

Depois de obter o erro de adição de um objeto à lista, excluímos o objeto criado series e em seguida, tentamos acessá-lo para definir seu sinalizador de uso. Em tal situação, aparecerá um erro, porque o ponteiro para o objeto já foi excluído.

A correção é simples, basta mover a definição do sinalizador no código para a verificação do resultado da adição de objeto à lista:

   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   series.SetAvailable(true);
   if(!res)
      delete series;
   return res;
  }
//+------------------------------------------------------------------+

Nos métodos de atualização da lista-série temporal especificada e de todas as listas-séries temporais, nem sempre era possível colocar o evento "Nova barra" na lista de eventos com a hora correta do evento (tempo de abertura de uma nova barra). Em algumas situações, o tempo era zero.

Para corrigir isso, criamos uma nova variável para armazenar o tempo, e se o programa for um indicador e o trabalho for no símbolo e período gráfico atuais, escreveremos o tempo numa variável a partir da estrutura de preço obtida em OnCalculate(), caso contrário, obtemos o tempo a partir do valor retornado pelo método LastBarDate() do objeto-série temporal. Usamos o tempo obtido ao adicionar um evento à lista de eventos do objeto das séries temporais do símbolo:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                                  |
//+------------------------------------------------------------------+
void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the timeseries event flag and clear the list of all timeseries events
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the timeseries from the list by its timeframe
   CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Update the timeseries list
   series_obj.Refresh(data_calculate);
   datetime time=
     (
      this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
      data_calculate.rates.time : 
      series_obj.LastBarDate()
     );
//--- If the timeseries object features the New bar event
   if(series_obj.IsNewBar(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,time,series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeriesDE::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
      CSeriesDE *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);
      datetime time=
        (
         this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
         data_calculate.rates.time : 
         series_obj.LastBarDate()
        );
      //--- If the timeseries object features the New bar event
      if(series_obj.IsNewBar(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,time,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();
  }
//+------------------------------------------------------------------+

Para atualizar todas as séries temporais, precisamos separar o local de onde é chamada a atualização da série temporal para o símbolo atual e os restantes. Atualizamos qualquer outra série temporal no temporizador, enquanto a do símbolo atual, em OnCalculate(). Isso é feito para não manipular as séries temporais do símbolo atual no temporizador em busca de um novo tick, quando em OnCalculate() é chamada uma atualização das séries temporais do símbolo atual ao entrar um novo tick.

Para trabalho com o temporizador declaramos mais um método no arquivo da classe da coleção das séries temporais \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh:

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

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

O método chamará os métodos de atualização de todas as séries temporais exceto para o símbolo atual (implementação do método):

//+------------------------------------------------------------------+
//| Update all timeseries except the current one                     |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::RefreshAllExceptCurrent(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
      CTimeSeriesDE *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- if the timeseries symbol is equal to the current chart symbol or
      //--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(timeseries.Symbol()==::Symbol() || !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);
     }
  }
//+------------------------------------------------------------------+

Ao arquivo de funções de serviço da biblioteca \MQL5\Include\DoEasy\Services\DELib.mqh adicionamos uma função que retorna o número de barras do segundo período especificado dentro de uma barra do primeiro período especificado do gráfico:

//+------------------------------------------------------------------+
//| Return the number of bars of one period in a single bar of another one  |
//+------------------------------------------------------------------+
int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT)
  {
   return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period);
  }
//+------------------------------------------------------------------+

Como a função PeriodSeconds() retorna o número de segundos num período, então, para determinar o número de barras de um período (menor) numa barra de outro período (maior), basta dividir o número de segundos de um período maior pelo número de segundos de um período menor. É o que estamos fazendo aqui.

Em nossos programas, podemos definir a lista de símbolos usados. Definimos essa lista para a biblioteca no método SetUsedSymbols() da classe-coleção de símbolos no arquivo \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh. Se especificarmos a lista de símbolos usados sem o símbolo atual, a biblioteca criará todos os símbolos especificados nas configurações da coleção de séries temporais, mas dispensando o símbolo atual. Porém, ele é constantemente acessado para calcular o posicionamento dos dados na tela. Por conseguinte, precisamos corrigir tal situação em que não se especifica o símbolo atual.

Para fazer isso, no método SetUsedSymbols() da classe de coleção de símbolos adicionamos o símbolo atual à lista. Desde que o nome do símbolo atual não esteja na lista de símbolos de trabalho especificada pelo usuário nas configurações do programa, o símbolo será adicionado à lista. Se já estiver, um novo símbolo com este nome não será adicionado:
//+------------------------------------------------------------------+
//| Set the list of used symbols                                     |
//+------------------------------------------------------------------+
bool CSymbolsCollection::SetUsedSymbols(const string &symbol_used_array[])
  {
   ::ArrayResize(this.m_array_symbols,0,1000);
   ::ArrayCopy(this.m_array_symbols,symbol_used_array);
   this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols);
   this.m_list_all_symbols.Clear();
   this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW);
   //--- Use only the current symbol
   if(this.m_mode_list==SYMBOLS_MODE_CURRENT)
     {
      string name=::Symbol();
      ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
      return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
     }
   else
     {
      bool res=true;
      //--- Use the pre-defined symbol list
      if(this.m_mode_list==SYMBOLS_MODE_DEFINES)
        {
         int total=::ArraySize(this.m_array_symbols);
         for(int i=0;i<total;i++)
           {
            string name=this.m_array_symbols[i];
            ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
            bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
            res &=add;
            if(!add) 
               continue;
           }
         //--- Create the new current symbol (if it is already in the list, it is not re-created)
         res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL));
         return res;
        }
      //--- Use the full list of the server symbols
      else if(this.m_mode_list==SYMBOLS_MODE_ALL)
        {
         return this.CreateSymbolsList(false);
        }
      //--- Use the symbol list from the Market Watch window
      else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH)
        {
         this.MarketWatchEventsControl(false);
         return true;
        }
     }
   return false;
  }
//+------------------------------------------------------------------+

No arquivo \MQL5\Include\DoEasy\Engine.mqh do objeto principal da biblioteca CEngine adicionamos três métodos privados:

//--- Set the list of used symbols in the symbol collection and create the collection of symbol timeseries
   bool                 SetUsedSymbols(const string &array_symbols[]);
private:
//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
   void                 WriteSymbolsPeriodsToArrays(void);
//--- Check the presence of a (1) symbol in the ArrayUsedSymbols array, (2) the presence of a timeframe in the ArrayUsedTimeframes array
   bool                 IsExistSymbol(const string symbol);
   bool                 IsExistTimeframe(const ENUM_TIMEFRAMES timeframe);
public:
//--- Create a resource file

Os métodos são necessários para gravar uma lista de símbolos e períodos gráficos nas matrizes declaradas anteriormente no arquivo Datas.mqh, bem como para retornar o sinalizador que indica a existência do símbolo na matriz de nomes de símbolos usados e o sinalizador que indica a existência do período gráfico na matriz de timeframes usados.

Implementação de métodos que retornam sinalizadores que indicam a presença de um símbolo e período gráfico nas matrizes correspondentes:

//+------------------------------------------------------------------+
//| Check if a symbol is present in the array                        |
//+------------------------------------------------------------------+
bool CEngine::IsExistSymbol(const string symbol)
  {
   int total=::ArraySize(ArrayUsedSymbols);
   for(int i=0;i<total;i++)
      if(ArrayUsedSymbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+
//| Check if a timeframe is present in the array                     |
//+------------------------------------------------------------------+
bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   int total=::ArraySize(ArrayUsedTimeframes);
   for(int i=0;i<total;i++)
      if(ArrayUsedTimeframes[i]==timeframe)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

Simplesmente num loop pelo devido array, obtemos o próximo elemento do array e o comparamos com o valor passado ao método. Se o valor do seguinte elemento da matriz corresponder ao passado para o método, retornamos true. No final de todo o loop, retornamos false , uma vez que nenhuma correspondência foi encontrada entre os valores dos elementos na matriz e aqueles passados para o método.

Implementação do método de gravação em matrizes de símbolos e períodos gráficos usados:

//+------------------------------------------------------------------+
//| Write all used symbols and timeframes                            |
//| to the ArrayUsedSymbols and ArrayUsedTimeframes arrays           |
//+------------------------------------------------------------------+
void CEngine::WriteSymbolsPeriodsToArrays(void)
  {
//--- Get the list of all created timeseries (created by the number of used symbols)
   CArrayObj *list_timeseries=this.GetListTimeSeries();
   if(list_timeseries==NULL)
      return;
//--- Get the total number of created timeseries
   int total_timeseries=list_timeseries.Total();
   if(total_timeseries==0)
      return;
//--- Set the size of the array of used symbols equal to the number of created timeseries, while
//--- the size of the array of used timeframes is set equal to the maximum possible number of timeframes in the terminal
   if(::ArrayResize(ArrayUsedSymbols,total_timeseries,1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,21,21)!=21)
      return;
//--- Set both arrays to zero
   ::ZeroMemory(ArrayUsedSymbols);
   ::ZeroMemory(ArrayUsedTimeframes);
//--- Reset the number of added symbols and timeframes to zero and,
//--- in a loop by the total number of timeseries,
   int num_symbols=0,num_periods=0;
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all timeseries of a single symbol
      CTimeSeriesDE *timeseries=list_timeseries.At(i);
      if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol()))
         continue;
      //--- increase the number of used symbols and (num_symbols variable), and
      //--- write the timeseries symbol name to the array of used symbols by the num_symbols-1 index
      num_symbols++;
      ArrayUsedSymbols[num_symbols-1]=timeseries.Symbol();
      //--- Get the list of all its timeseries from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- In the loop by the total number of symbol timeseries,
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries object
         CSeriesDE *series=list_series.At(j);
         if(series==NULL || this.IsExistTimeframe(series.Timeframe()))
            continue;
         //--- increase the number of used timeframes and (num_periods variable), and
         //--- write the timeseries timeframe value to the array of used timeframes by num_periods-1 index
         num_periods++;
         ArrayUsedTimeframes[num_periods-1]=series.Timeframe();
        }
     }
//--- Upon the loop completion, change the size of both arrays to match the exact number of added symbols and timeframes
   ::ArrayResize(ArrayUsedSymbols,num_symbols,1000);
   ::ArrayResize(ArrayUsedTimeframes,num_periods,21);
  }
//+------------------------------------------------------------------+

O método examina todas as séries temporais criadas para cada símbolo usado no programa e preenche as matrizes de símbolos e períodos gráficos usados com dados a partir da coleção de séries temporais. Todas as linhas da listagem do método são comentadas em detalhes e, espero, não dificultem o entendimento da lógica. Em qualquer caso, todas as perguntas podem ser feitas na discussão do artigo.

No bloco de métodos para atualizar séries temporais adicionamos um método protegido para atualizar todas as séries temporais exceto as do símbolo atual:

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of a specified symbol,
//--- (3) all timeseries of all symbols, (4) all timeseries except the current one
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(const string symbol,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,data_calculate);                                    }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }
protected:
   void                 SeriesRefreshAllExceptCurrent(SDataCalculate &data_calculate)
                          { this.m_time_series.GetObject().RefreshAllExceptCurrent(data_calculate);               }
public  
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period

Já vimos anteriormente qual a necessidade de ter tal método. Aqui, ele simplesmente chama o método da classe-coleção das séries temporais de mesmo nome, que consideramos acima.

No temporizador da classe no bloco de processamento de coleção de séries temporais vamos chamar este método para atualizar todas as séries temporais, exceto a atual:

//+------------------------------------------------------------------+
//| CEngine timer                                                   |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//
// here I have removed some code not needed for the current example
//
   //--- Timeseries collection timer
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- If the pause is over, work with the timeseries list (update all except the current one)
         if(cnt6.IsTimeDone())
            this.SeriesRefreshAllExceptCurrent(data_calculate);
        }
     }
//--- If this is a tester, work with collection events by tick
   else
     {
//
// here I have removed some code not needed for the current example
//
     }
  }

No manipulador de eventos Calculate, no método OnCalculate() do objeto principal da biblioteca CEngine, retornaremos zero se ainda não foram criadas todas as séries temporais, caso contrário, rates_total, isto é, se já foram criadas todas as séries do tempo usadas:

//+------------------------------------------------------------------+
//| 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 0;
//--- Re-create empty timeseries
//--- If at least one of the timeseries is not synchronized, return zero
   if(!this.SeriesSync(data_calculate,required))
      return 0;
//--- Update the timeseries of the current symbol (not in the tester) and
//--- return either 0 (in case there are empty timeseries), or rates_total
   if(!this.IsTester())
      this.SeriesRefresh(NULL,data_calculate);
   return(this.SeriesGetSeriesEmpty()==NULL ? data_calculate.rates_total : 0);
  }
//+------------------------------------------------------------------+

Anteriormente, retornávamos imediatamente rates_total, passado para o método por meio da estrutura de preços da barra atual. Mas, para o tratamento correto da sincronização das séries temporais, precisamos controlar o valor retornado desde o método. Zero é retornado para iniciar o recálculo de todo o histórico, e rates_total, apenas para calcular dados que ainda não foram calculados (geralmente 0 quando o cálculo é da barra atual ou 1 quando é de barras anteriores e atuais no momento da abertura de uma nova barra).

No método de criação de todas séries temporais para todos os símbolos usados adicionamos uma entrada às matrizes de todos os símbolos e séries temporais 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);
        }
     }
//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
   this.WriteSymbolsPeriodsToArrays();
//--- Return the result of creating all timeseries for all symbols
   return res;
  }
//+------------------------------------------------------------------+

Depois de chamar esse método a partir da função de inicialização da biblioteca no programa, serão preparadas, se necessário, duas matrizes para uso em programas, nomeadamente uma matriz de todos os símbolos usados e uma matriz de todos os períodos gráficos usados. O método foi discutido por nós acima.

Assim fica concluída a modificação das classes da biblioteca.

Hoje criaremos um indicador de teste, no qual verificaremos o funcionamento da biblioteca em indicadores nos modos multissímbolo e multiperíodo.
No indicador será possível definir os quatro símbolos a serem usados e todas as séries temporais possíveis. A escolha do símbolo e do período gráfico, com os quais funciona o indicador num determinado momento, será feita por meio dos botões. No gráfico serão exibidos até quatro botões com os nomes dos símbolos especificados nas configurações, e já ao lado do símbolo cujo botão está pressionado, exibiremos uma lista de botões com os períodos disponíveis.
Apenas um botão do símbolo e um botão do período gráfico podem ser pressionados por vez.

Assim, podemos selecionar o símbolo com o qual trabalham o indicador e o período gráfico, símbolo esse cujos dados serão exibidos numa subjanela do indicador. Olhando para o futuro, direi que acabou sendo bastante inconveniente para mim escrever trabalhos com botões num estilo procedimental. O código para controlar o estado dos botões resultou abaixo do ideal. Mas, para testar o funcionamento das séries temporais num indicador multissímbolo multiperíodo, deixaremos o código para trabalhar com botões em segundo plano. Ainda por cima, este é apenas um indicador de teste.

Criando um indicador de teste

O propósito do indicador de teste anterior e o do que vamos fazer hoje não é apenas testar e mostrar o trabalho em indicadores com séries temporais de biblioteca, mas também testar a estrutura do buffer do indicador - com base no conhecimento adquirido sobre o uso dessa estrutura, faremos um conjunto de classes-buffers de indicadores. Hoje vamos complementar a estrutura do buffer, implementando o estilo "Vela japonesa".

Para criar um indicador de teste, pegamos o indicador do artigo anterior e o salvamos numa nova pasta \MQL5\Indicators\TestDoEasy\Part41\ com o novo nome TestDoEasyPart41.mq5.

Para iniciar especificamos o desenho do indicador numa janela separada, descrevemos todos os buffers de indicador necessários e adicionamos mais uma substituição de macros indicando o número máximo de símbolos usados (e consequentemente, o número de buffers de indicador plotados):

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart41.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>
//--- properties
#property indicator_separate_window
#property indicator_buffers 21      // 5 массивов (Open[] High[] Low[] Close[] Color[]) * 4 рисуемых буфера + 1 расчётный буфер BufferTime[]
#property indicator_plots   4       // 1 буфер "Японские свечи", состоящий из 5 массивов (Open[] High[] Low[] Close[] Color[]) * 4 символа
//--- plot Pair1
#property indicator_label1  "Pair 1"
#property indicator_type1   DRAW_COLOR_CANDLES
#property indicator_color1  clrLimeGreen,clrRed,clrDarkGray
//--- plot Pair2
#property indicator_label2  "Pair 2"
#property indicator_type2   DRAW_COLOR_CANDLES
#property indicator_color2  clrDeepSkyBlue,clrFireBrick,clrDarkGray
//--- plot Pair3
#property indicator_label3  "Pair 3"
#property indicator_type3   DRAW_COLOR_CANDLES
#property indicator_color3  clrMediumPurple,clrDarkSalmon,clrGainsboro
//--- plot Pair4
#property indicator_label4  "Pair 4"
#property indicator_type4   DRAW_COLOR_CANDLES
#property indicator_color4  clrMediumAquamarine,clrMediumVioletRed,clrGainsboro

//--- classes

//--- enums

//--- defines
#define PERIODS_TOTAL   (21)              // Total amount of available chart periods
#define SYMBOLS_TOTAL   (4)               // Maximum number of drawn symbol buffers
//--- structures

Bem, por que obtivemos um número de buffers de indicador igual a 21?
A resposta é simples, o estilo de plotagem DRAW_COLOR_CANDLES implica que existem cinco matrizes associadas a ele:

  1. array de preços Open
  2. array de preços High
  3. array de preços Low
  4. array de preços Close
  5. array de preços (Color)

Como número máximo de símbolos no indicador usaremos 4. Por tanto, quatro buffers plotados de cinco matrizes associadas eles já somam 20 buffers de indicadores, e mais um buffer que é necessário para armazenar o tempo de barra que iremos transferir para a função. No total, temos 21 buffers de indicador, quatro dos quais são plotados.

Vamos escrever a estrutura do buffer Vela japonesa:

//--- structures
struct SDataBuffer                        // Candlesticks buffer structure
  {
private:
   ENUM_TIMEFRAMES   m_buff_timeframe;    // Buffer timeframe
   string            m_buff_symbol;       // Buffer symbol
   int               m_buff_open_index;   // The index of the indicator buffer related to the Open[] array
   int               m_buff_high_index;   // The index of the indicator buffer related to the High[] array
   int               m_buff_low_index;    // The index of the indicator buffer related to the Low[] array
   int               m_buff_close_index;  // The index of the indicator buffer related to the Close[] array
   int               m_buff_color_index;  // The index of the color buffer related to the Color[] array
   int               m_buff_next_index;   // The index of the next free indicator buffer
   bool              m_used;              // The flag of using the buffer in the indicator
   bool              m_show_data;         // The flag of displaying the buffer on the chart before enabling/disabling its display
public:
   double            Open[];              // The array assigned as INDICATOR_DATA by the Open indicator buffer
   double            High[];              // The array assigned as INDICATOR_DATA by the High indicator buffer
   double            Low[];               // The array assigned as INDICATOR_DATA by the Low indicator buffer
   double            Close[];             // The array assigned as INDICATOR_DATA by the Close indicator buffer
   double            Color[];             // The array assigned as INDICATOR_COLOR_INDEX by the Color indicator buffer
//--- Set indices for the drawn OHLC and Color buffers
   void              SetIndexes(const int index_first)
                       {
                        this.m_buff_open_index=index_first;
                        this.m_buff_high_index=index_first+1;
                        this.m_buff_low_index=index_first+2;
                        this.m_buff_close_index=index_first+3;
                        this.m_buff_color_index=index_first+4;
                        this.m_buff_next_index=index_first+5;
                       }
//--- Methods of setting and returning values of the private structure members
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe)   { this.m_buff_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); }
   void              SetSymbol(const string symbol)                  { this.m_buff_symbol=symbol;        }
   void              SetUsed(const bool flag)                        { this.m_used=flag;                 }
   void              SetShowDataFlag(const bool flag)                { this.m_show_data=flag;            }
   int               IndexOpenBuffer(void)                     const { return this.m_buff_open_index;    }
   int               IndexHighBuffer(void)                     const { return this.m_buff_high_index;    }
   int               IndexLowBuffer(void)                      const { return this.m_buff_low_index;     }
   int               IndexCloseBuffer(void)                    const { return this.m_buff_close_index;   }
   int               IndexColorBuffer(void)                    const { return this.m_buff_color_index;   }
   int               IndexNextBuffer(void)                     const { return this.m_buff_next_index;    }
   ENUM_TIMEFRAMES   Timeframe(void)                           const { return this.m_buff_timeframe;     }
   string            Symbol(void)                              const { return this.m_buff_symbol;        }
   bool              IsUsed(void)                              const { return this.m_used;               }
   bool              GetShowDataFlag(void)                     const { return this.m_show_data;          }
   void              Print(void);
  };
//--- Display structure data to the journal
void SDataBuffer::Print(void)
  {
   string array[8];
   array[0]="Buffer "+this.Symbol()+" "+TimeframeDescription(this.Timeframe())+":";
   array[1]=" Open buffer index: "+(string)this.IndexOpenBuffer();
   array[2]=" High buffer index: "+(string)this.IndexHighBuffer();
   array[3]=" Low buffer index: "+(string)this.IndexLowBuffer();
   array[4]=" Close buffer index: "+(string)this.IndexCloseBuffer();
   array[5]=" Color buffer index: "+(string)this.IndexColorBuffer();
   array[6]=" Next buffer index: "+(string)this.IndexNextBuffer();
   array[7]=" Used: "+(string)(bool)this.IsUsed();
   for(int i=0;i<ArraySize(array);i++)
      ::Print(array[i]);
  }
//--- input variables

A estrutura possui variáveis para armazenar os valores dos índices dos buffers associados aos arrays correspondentes OHLC e Color; sempre podemos usar o índice para acessar o buffer desejado. Outro índice livre para vincular um novo buffer de indicador a matrizes de estrutura pode sempre ser obtido de uma variável m_buff_next_index usando o método IndexNextBuffer(), que retorna o índice que vem depois do buffer de cor na estrutura atual.

Na listagem, pode ser visto que a estrutura possui todos os métodos para definir e retornar todos os valores definidos na sua seção privada, e também que há um método para imprimir todos os dados da estrutura no log: a uma matriz são adicionados os dados de índices de buffers OHLC e cores, o próximo índice livre para vinculação de uma nova matriz e um sinalizador que indica o uso deste buffer. Em seguida, todos esses dados no loop são exibidos desde a matriz para o log.

Por exemplo, é assim que os dados dos quatro buffers plotados especificados nas configurações do indicador serão exibidos no log (o buffer AUDUSD é exibido no gráfico):

2020.04.08 21:55:21.528 Buffer EURUSD H1:
2020.04.08 21:55:21.528  Open buffer index: 0
2020.04.08 21:55:21.528  High buffer index: 1
2020.04.08 21:55:21.528  Low buffer index: 2
2020.04.08 21:55:21.528  Close buffer index: 3
2020.04.08 21:55:21.528  Color buffer index: 4
2020.04.08 21:55:21.528  Next buffer index: 5
2020.04.08 21:55:21.528  Used: false
2020.04.08 21:55:21.530 Buffer AUDUSD H1:
2020.04.08 21:55:21.530  Open buffer index: 5
2020.04.08 21:55:21.530  High buffer index: 6
2020.04.08 21:55:21.530  Low buffer index: 7
2020.04.08 21:55:21.530  Close buffer index: 8
2020.04.08 21:55:21.530  Color buffer index: 9
2020.04.08 21:55:21.530  Next buffer index: 10
2020.04.08 21:55:21.530  Used: true
2020.04.08 21:55:21.532 Buffer EURAUD H1:
2020.04.08 21:55:21.532  Open buffer index: 10
2020.04.08 21:55:21.532  High buffer index: 11
2020.04.08 21:55:21.532  Low buffer index: 12
2020.04.08 21:55:21.532  Close buffer index: 13
2020.04.08 21:55:21.532  Color buffer index: 14
2020.04.08 21:55:21.532  Next buffer index: 15
2020.04.08 21:55:21.532  Used: false
2020.04.08 21:55:21.533 Buffer EURGBP H1:
2020.04.08 21:55:21.533  Open buffer index: 15
2020.04.08 21:55:21.533  High buffer index: 16
2020.04.08 21:55:21.533  Low buffer index: 17
2020.04.08 21:55:21.533  Close buffer index: 18
2020.04.08 21:55:21.533  Color buffer index: 19
2020.04.08 21:55:21.533  Next buffer index: 20
2020.04.08 21:55:21.533  Used: false

Vemos que o índice do buffer Open de cada buffer consecutivo "Vela japonesa" coincide com o índice "Next buffer index" do buffer anterior "Vela japonesa". Pode-se de ver que o próximo buffer livre tem um índice de 20, a este índice pode ser atribuído, por exemplo, o próximo buffer do indicador calculado, que, a propósito, será feito para o buffer calculado que armazena o tempo das barras do gráfico atual.

Adicionamos o bloco de parâmetros de entrada do indicador:

//--- input variables
/*sinput*/ ENUM_SYMBOLS_MODE  InpModeUsedSymbols=  SYMBOLS_MODE_DEFINES;            // Mode of used symbols list
sinput   string               InpUsedSymbols    =  "EURUSD,AUDUSD,EURAUD,EURGBP,EURCAD,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   uint                 InpButtShiftX     =  0;    // Buttons X shift 
sinput   uint                 InpButtShiftY     =  10;   // Buttons Y shift 
sinput   bool                 InpUseSounds      =  true; // Use sounds
//--- indicator buffers

Uma vez que a enumeração para selecionar o modo de uso dos símbolos ENUM_SYMBOLS_MODE localizados no arquivo Defines.mqh contém dois modos que não precisamos aqui, "Trabalhar com símbolos da janela Observação do mercado" e "Trabalhar com a lista completa de símbolos":

//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work with the current symbol only
   SYMBOLS_MODE_DEFINES,                              // Work with the specified symbol list
   SYMBOLS_MODE_MARKET_WATCH,                         // Work with the Market Watch window symbols
   SYMBOLS_MODE_ALL                                   // Work with the full symbol list
  };
//+------------------------------------------------------------------+

... assim, para evitar a escolha desses dois modos nas configurações, faremos a variável InpModeUsedSymbols não seja externa, comentando seu modificador sinput. Assim, o modo de trabalho com os símbolos no indicador será sempre "Trabalhar com uma determinada lista de símbolos", e serão usados os primeiros quatro símbolos da lista especificada pela variável de entrada InpUsedSymbols.

Vamos escrever a definição dos buffers do indicador e o bloco de variáveis globais:

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

Como buffers plotados do indicador declaramos a matriz de estruturas do buffer "Velas japonesas", isso é muito mais conveniente do que definir quatro buffers idênticos, e é mais fácil acessar cada buffer pelo seu índice de localização na matriz correspondente ao botão, basta selecionar o buffer do primeiro botão, selecionamos o buffer que é o primeiro na matriz, precisamos selecionar o buffer atribuído ao último botão, selecionamos o último buffer na matriz, etc.

Precisamos de um buffer de cálculo - buffer de tempo - para transferir à função o indicador de tempo das barras desde a matriz predefinida time[] do manipulador OnCalculate() do indicador.

Já estamos familiarizados com o bloco de variáveis globais graças aos EAs de teste e indicadores de quase todos os artigos que descrevem a biblioteca, todas as variáveis são assinadas e não iremos analisá-las completamente aqui.
Precisamos do número mínimo de barras para calcular o indicador para determinar se há suficientes as barras para calcular a série temporal, para exibir corretamente os dados do indicador do período gráfico mais alto no mais baixo atual.

Por exemplo, se estamos no período gráfico M15 e pegamos os dados para exibição do gráfico H1, então, para a exibição correta de todas as barras, precisamos ter pelo menos 4 barras disponíveis, afinal, uma barra horária contém 4 barras de quinze minutos.

O cálculo do número de barras necessário no gráfico atual, dependendo do período usado para os cálculos, será realizado pela função NumberBarsInTimeframe(), que escrevemos anteriormente no arquivo de funções de serviço da biblioteca DELib.mqh e discutida acima.

Acima, já escrevi que tive dificuldades para escrever um indicador usando um estilo procedural, até porque tive que criar funções auxiliares adicionais para encontrar, definir e monitorar os estados de botões e buffers. Se os botões e buffers fossem escritos como objetos, seria muito mais fácil acessar suas propriedades e definir seus modos. Até agora fizemos como tem sido feito embora parecesse que seria mais rápido escrever um teste com um estilo procedural, já para o indicador de teste não há necessidade de criar objetos temporários que não serão úteis mais tarde.

Vejamos as funções auxiliares escritas.

Função para definir os estados dos buffers plotados do indicador:

//+------------------------------------------------------------------+
//| Set the state for drawn buffers                                  |
//+------------------------------------------------------------------+
void SetPlotBufferState(const int buffer_index,const bool state)
  {
//--- Depending on a passed status, define whether data should be displayed in the data window (state==true) or not (state==false)
   PlotIndexSetInteger(buffer_index,PLOT_SHOW_DATA,state);
//--- Create the buffer description consisting of a symbol and timeframe and set the buffer description by its buffer_index index
   string params=Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe());
   string label=params+" Open;"+params+" High;"+params+" Low;"+params+" Close";
   PlotIndexSetString(buffer_index,PLOT_LABEL,(state ? label : NULL));
//--- If the buffer is active (drawn), set a short name for the indicator with the displayed symbol and timeframe
   if(state)
      IndicatorSetString(INDICATOR_SHORTNAME,engine.Name()+" "+Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe()));
  }  
//+------------------------------------------------------------------+

A função recebe o índice do buffer para o qual precisa ser definido o estado que também passado pelo parâmetro de entrada.

Observe que existe um recurso que deve ser levado em consideração ao trabalhar com buffers de indicadores que exigem vários arrays vinculados para exibição.

Por exemplo, se o buffer do indicador requer 2 arrays, os índices dos arrays vinculados a esses buffers terão os valores 0 e 1. Esses valores são definidos usando a função SetIndexBuffer(). Ao usar um buffer plotado usando duas matrizes de dados, não há problemas especiais em entender o acesso ao buffer plotado, porque apenas especificamos o buffer com índice 0 para acessar suas propriedades.

Mas se precisarmos de dois ou mais buffers plotados usando duas matrizes, pode haver um mal-entendido sobre qual índice usar para acessar segundo, terceiro e subsequente buffer plotado.

Consideremos um exemplo de três buffers plotados com duas matrizes cada, e o números dos índices dos buffers plotados e suas matrizes:
  • Buffer plotado №1 — índice do buffer plotado 0
    • Matriz №1 — índice de buffer 0
    • Matriz №2 — índice de buffer 1
  • Buffer plotado №2 — índice de buffer plotado 1
    • Matriz №1 — índice de buffer 2
    • Matriz №2 — índice de buffer 3
  • Buffer plotado №3 — índice de buffer plotado 2
    • Matriz №1 — índice de buffer plotado 4
    • Matriz №2 — índice de buffer 5

Aparentemente existem seis matrizes para três buffers plotados, sendo que para acessar a segunda matriz plotada é necessário clicar no índice 2 (afinal, 0 e 1 são ocupados pelas matrizes do primeiro buffer). Mas não é bem assim. Para acessar o segundo buffer plotado, precisamos acessar os índices precisamente dos buffers plotados, no entanto, nem todas as matrizes são atribuídas como buffers para cada buffer plotado, isto él, para o índice 1.

Assim, para vincular uma matriz a um buffer através a função SetIndexBuffer(), precisamos especificar o número de sequência de todas as matrizes destinadas a serem usadas como buffers de indicador, mas para receber dados do buffer plotado pela função PlotIndexGetInteger() ou definir dados para o buffer plotado por meio das funções PlotIndexSetDouble(), PlotIndexSetInteger(), PlotIndexSetString(), precisamos especificar o índice do buffer plotado desejado, não o número da matriz. Neste exemplo, o índice para o primeiro buffer plotado será 0, para o segundo, 1 e para o terceiro, 2. Isso deve ser sabido e levado em consideração.

Função que retorna o sinalizador que indica o uso do símbolo especificado nas configurações:

//+------------------------------------------------------------------+
//| Return the flag of using a symbol specified in the settings      |
//+------------------------------------------------------------------+
bool IsUsedSymbolByInput(const string symbol)
  {
   int total=ArraySize(array_used_symbols);
   for(int i=0;i<total;i++)
      if(array_used_symbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

Se o símbolo estiver presente na matriz de símbolos usados, a função retornará true, caso contrário, false. Às vezes, podemos não indicar o símbolo atual na lista de símbolos usados, mas ele estará sempre nela, pois seus dados são necessários para realizar cálculos internos da biblioteca. Esta função retorna um sinalizador de que o símbolo atual não está especificado nas configurações e deve ser ignorado.

Função que retorna o índice do buffer plotado do símbolo:

//+------------------------------------------------------------------+
//| Return the structure drawn buffer index by symbol                |
//+------------------------------------------------------------------+
int IndexBuffer(const string symbol)
  {
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].Symbol()==symbol)
         return i;
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

O nome do símbolo é passado para a função, cujo índice do buffer deve ser retornado. Num loop percorrendo todos os buffers, procuramos um buffer com tal símbolo e retornamos o índice do loop se ele corresponder. Se não houver buffer com este símbolo, retornaremos -1.

Função que retorna o número do primeiro índice livre, ao qual pode ser atribuído o próximo buffer de indicador plotado:

//+------------------------------------------------------------------+
//| Return the first free index of the drawn buffer                  |
//+------------------------------------------------------------------+
int FirstFreePlotBufferIndex(void)
  {
   int num=WRONG_VALUE,total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].IndexNextBuffer()>num)
         num=Buffers[i].IndexNextBuffer();
   return num;
  }
//+------------------------------------------------------------------+

Num loop percorrendo todos os buffers plotados desde a matriz de estruturas de buffers, verificamos o valor do próximo buffer livre.
Se for maior que o anterior, lembramos o novo valor. No final do loop, retornamos o valor escrito desde a variável num.

Funções escritas para realizar ações auxiliares para definir e encontrar os estados dos botões e buffers:

//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void SetButtonState(const string button_name,const bool state)
  {
//--- Set the button status and its color depending on the status
   ObjectSetInteger(0,button_name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'220,255,240');
   else
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'240,240,240');
//--- If not in the tester, 
//--- set the status to the terminal global variable
   if(!engine.IsTester())
      GlobalVariableSet((string)ChartID()+"_"+button_name,state);
  }
//+------------------------------------------------------------------+
//| Set the symbol button status                                     |
//+------------------------------------------------------------------+
void SetButtonSymbolState(const string button_symbol_name,const bool state)
  {
//--- Set the symbol button status
   SetButtonState(button_symbol_name,state);
//--- Detect wrong names if the timeframe button status is not specified
//--- Write the button status to the global variable only if its name contains no "PERIOD_CURRENT" substring
   if(StringFind(button_symbol_name,"PERIOD_CURRENT")==WRONG_VALUE)
      GlobalVariableSet((string)ChartID()+"_"+button_symbol_name,state);
//--- Set the visibility for all period buttons corresponding to the symbol button
   SetButtonPeriodVisible(button_symbol_name,state);
  }
//+------------------------------------------------------------------+
//| Set the period button status                                     |
//+------------------------------------------------------------------+
void SetButtonPeriodState(const string button_period_name,const bool state)
  {
//--- Set the button status and write it to the terminal global variable
   SetButtonState(button_period_name,state);
   GlobalVariableSet((string)ChartID()+"_"+button_period_name,state);
  }
//+------------------------------------------------------------------+
//| Set the "visibility" of period buttons for the symbol button     |
//+------------------------------------------------------------------+
void SetButtonPeriodVisible(const string button_symbol_name,const bool state_symbol)
  {
//--- In the loop by the amount of used timeframes
   int total=ArraySize(array_used_periods);
   for(int j=0;j<total;j++)
     {
      //--- create the name of the next period button
      string butt_name_period=button_symbol_name+"_"+EnumToString(ArrayUsedTimeframes[j]);
      //--- Set the status and visibility for the period button depending on the symbol button status
      ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
     }   
  }
//+------------------------------------------------------------------+
//| Reset the states of the remaining symbol buttons                 |
//+------------------------------------------------------------------+
void ResetButtonSymbolState(const string button_symbol_name)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is a period button, move on to the next one
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")>0)
         continue;
      //--- Reset the symbol button status by object name
      SetButtonSymbolState(name,false);
     }
  }
//+------------------------------------------------------------------+
//| Reset the states of the remaining symbol period buttons          |
//+------------------------------------------------------------------+
void ResetButtonPeriodState(const string button_period_name,const string symbol)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_period_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- Reset the period button status by object name
      SetButtonPeriodState(name,false);
     }
  }
//+------------------------------------------------------------------+
//| Return the name of the pressed period button corresponding to the symbol  |
//+------------------------------------------------------------------+
string GetNamePressedTimeframe(const string button_symbol_name,const string symbol)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- If the button is pressed, return the name of the pressed button graphic object
      if(ButtonState(name))
         return name;
     }
//--- Return NULL if no symbol period buttons are pressed
   return NULL;
  }
//+------------------------------------------------------------------+
//| Set the buffer states, 'true' - only for the specified one       |
//+------------------------------------------------------------------+
void SetAllBuffersState(const string symbol)
  {
   int total=ArraySize(Buffers);
//--- Get the specified buffer index
   int index=IndexBuffer(symbol);
//--- In a loop by the number of drawn buffers
   for(int i=0;i<total;i++)
     {
      //--- if the loop index is equal to the specified buffer index,
      //--- set the flag of its usage to 'true', otherwise - to 'false'
      //--- forcibly set the flag indicating that pressing the button for the buffer has already been handled
      Buffers[i].SetUsed(i!=index ? false : true);
      Buffers[i].SetShowDataFlag(false);
     }
  }
//+------------------------------------------------------------------+

Função de manipulação de clique de botão foi ligeiramente refeita, pois aqui só podemos ter um botão de símbolo pressionado e um botão de período em conformidade com este último:

//+------------------------------------------------------------------+
//| Handle pressing the buttons                                                            |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
//--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
      //--- Get the index of the drawn buffer by timeframe, its symbol and index
   int index=StringFind(button,"_PERIOD_");
   string symbol=StringSubstr(button,5,index-5);
   int buffer_index=IndexBuffer(symbol);
//--- Create the button name for the terminal's global variable
   string name_gv=(string)ChartID()+"_"+prefix+button;
//--- Get the button status (pressed/released). If not in the tester,
//--- write the status to the button global variable (1 or 0)
   bool state=ButtonState(button_name);
   if(!engine.IsTester())
      GlobalVariableSet(name_gv,state);
//--- Set the button color depending on its status, 
//--- write its status to the buffer structure depending on the button status (used/not used)
//--- initialize the buffer corresponding to the button timeframe by the buffer index received earlier
   if(StringFind(button_name,"_PERIOD_")==WRONG_VALUE)
     {
      SetButtonSymbolState(button_name,state);
      ResetButtonSymbolState(button_name);
     }
   else
     {
      SetButtonPeriodState(button_name,state);
      ResetButtonPeriodState(button_name,symbol);
     }
//--- Get the timeframe from the pressed symbol timeframe button
   string pressed_period=GetNamePressedTimeframe(button_name,symbol);
   ENUM_TIMEFRAMES timeframe=
     (
      StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
      TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
      TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
     );
//--- Set the states of all buffers, 'true' - for the pressed button symbol buffer, the rest are 'false'
   SetAllBuffersState(symbol);
//--- Set the displayed timeframe for the buffer
   Buffers[buffer_index].SetTimeframe(timeframe);
//--- If the button pressing is not handled yet
   if(Buffers[buffer_index].GetShowDataFlag()!=state)
     {
      //--- Initialize all indicator buffers
      InitBuffersAll();
      //--- If the buffer is active, fill it with historical data
      if(state)
         BufferFill(buffer_index);
      //--- Set the flag indicating that pressing the button has already been handled
      Buffers[buffer_index].SetShowDataFlag(state);
     }

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

Função para criar uma barra de botões:

//+------------------------------------------------------------------+
//| Create the buttons panel                                                              |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int total_symbols=ArraySize(array_used_symbols);
   int total_periods=ArraySize(ArrayUsedTimeframes);
   uint ws=48,hs=18,w=26,h=16,shift_h=2,x=InpButtShiftX+1, y=InpButtShiftY+h+1;
   //--- In the loop by the number of used symbols,
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- create the name of the next symbol button
      string butt_name_symbol=prefix+"BUTT_"+array_used_symbols[i];
      //--- create the next symbol button with a shift calculated as
      //--- ((button height + 2) * loop index)
      uint ys=y+(hs+shift_h)*i;
      if(ButtonCreate(butt_name_symbol,x,ys,ws,hs,array_used_symbols[i],clrGray))
        {
         bool state_symbol=(engine.IsTester() && i==0 ? true : false);
         //--- If not in the tester,
         if(!engine.IsTester())
           {
            //--- set the name of the terminal global variable for storing the symbol button status
            string name_gv_symbol=(string)ChartID()+"_"+butt_name_symbol;
            //--- if there is no global variable with the symbol name, create it set to 'false',
            if(!GlobalVariableCheck(name_gv_symbol))
               GlobalVariableSet(name_gv_symbol,false);
            //--- get the symbol button status from the terminal global variable
            state_symbol=GlobalVariableGet(name_gv_symbol);
           }
         //--- Set the status for the symbol button
         SetButtonState(butt_name_symbol,state_symbol);
         
         //--- In the loop by the amount of used timeframes
         for(int j=0;j<total_periods;j++)
           {
            //--- create the name of the next period button
            string butt_name_period=butt_name_symbol+"_"+EnumToString(ArrayUsedTimeframes[j]);
            uint yp=ys-(hs-h)/2;
            //--- create the next period button with a shift calculated as
            //--- (symbol button width + (period button width + 1) * loop index)
            if(ButtonCreate(butt_name_period,x+ws+2+(w+1)*j,yp,w,h,TimeframeDescription(ArrayUsedTimeframes[j]),clrGray))
               ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS));
            else
              {
               Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_period,"\"");
               return false;
              }
            bool state_period=(engine.IsTester() && ArrayUsedTimeframes[j]==Period() ? true : false);
            //--- If not in the tester,
            if(!engine.IsTester())
              {
               //--- set the name of the terminal global variable for storing the period button status
               string name_gv_period=(string)ChartID()+"_"+butt_name_period;
               //--- if there is no global variable with the period name, create it set to 'false',
               if(!GlobalVariableCheck(name_gv_period))
                  GlobalVariableSet(name_gv_period,false);
               //--- get the period button status from the terminal global variable
               state_period=GlobalVariableGet(name_gv_period);
              }
            //--- Set the status and visibility for the period button depending on the symbol button status
            SetButtonState(butt_name_period,state_period);
            ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
           }   
        }
      else
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_symbol,"\"");
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

Funções para inicializar os buffers plotados do indicador:

//+------------------------------------------------------------------+
//| Initialize the timeseries and the appropriate buffers by index   |
//+------------------------------------------------------------------+
bool InitBuffer(const int buffer_index)
  {
//--- Leave if the wrong index is passed
   if(buffer_index==WRONG_VALUE)
      return false;
//--- Initialize drawn OHLC buffers using the "empty" value, while Color is initialized using zero
   ArrayInitialize(Buffers[buffer_index].Open,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].High,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Low,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Close,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Color,0);
//--- Set the flag of the buffer display in the data window by index
   SetPlotBufferState(buffer_index,Buffers[buffer_index].IsUsed());
   return true;
  }
//+------------------------------------------------------------------+
//| Initialize all timeseries and the appropriate buffers            |
//+------------------------------------------------------------------+
void InitBuffersAll(void)
  {
//--- Initialize the next buffer in the loop by the total number of chart periods
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      InitBuffer(i);
  }
//+------------------------------------------------------------------+

Função para calcular a barra de todos os buffers de indicador ativos:

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

Função que grava os valores do objeto-barra passado para o buffer plotado especificado:

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


Agora vamos considerar o manipulador do indicador OnInit(), no qual são criados os botões e são preparados todos os buffers do indicador:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize DoEasy library
   OnInitDoEasy();

//--- Set indicator global variables
   prefix=engine.Name()+"_";
   //--- Get the index of the maximum used timeframe in the array,
   //--- calculate the number of bars of the current period fitting in the maximum used period
   //--- Use the obtained value if it exceeds 2, otherwise use 2
   int index=ArrayMaximum(ArrayUsedTimeframes);
   int num_bars=NumberBarsInTimeframe(ArrayUsedTimeframes[index]);
   min_bars=(index>WRONG_VALUE ? (num_bars>2 ? num_bars : 2) : 2);
//--- Check and remove remaining indicator graphical objects
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

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

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

//--- indicator buffers mapping
   //--- In the loop by the total number of all symbols
   int total_symbols=ArraySize(array_used_symbols);
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- get the next symbol
      //--- if the loop index is less than the size of used symbols array, the symbol name is taken from the array,
      //--- otherwise, this is an empty (unused) buffer, and the buffer symbol name is "EMPTY "+loop index
      string symbol=(i<total_symbols ? array_used_symbols[i] : "EMPTY "+string(i+1));
      //--- Increase the size of the buffer structures array, set the buffer symbol,
      ArrayResize(Buffers,ArraySize(Buffers)+1,SYMBOLS_TOTAL);
      Buffers[i].SetSymbol(symbol);
      //--- set the values of all indices for binding the indicator buffers with the structure arrays and
      //--- specify the next buffer index
      int index_first=(i==0 ? i : Buffers[i-1].IndexNextBuffer());
      Buffers[i].SetIndexes(index_first);
      
      //--- Setting the drawn buffer according to the button status
      //--- Set the symbol button status. The first button is active in the tester
      bool state_symbol=(engine.IsTester() && i==0 ? true : false);
      //--- Set the name of the symbol button corresponding to the buffer with the loop index and its timeframe
      string name_butt_symbol=prefix+"BUTT_"+Buffers[i].Symbol();
      string name_butt_period=name_butt_symbol+"_PERIOD_"+TimeframeDescription(Buffers[i].Timeframe());
      //--- If not in the tester, while the chart features the button with the specified name,
      if(!engine.IsTester() && ObjectFind(ChartID(),name_butt_symbol)==0)
        {
         //--- set the name of the terminal global variable for storing the button status
         string name_gv_symbol=(string)ChartID()+"_"+name_butt_symbol;
         string name_gv_period=(string)ChartID()+"_"+name_butt_period;
         //--- get the symbol button status from the terminal global variable
         state_symbol=GlobalVariableGet(name_gv_symbol);
        }
      
      //--- Get the timeframe from pressed symbol timeframe buttons
      string pressed_period=GetNamePressedTimeframe(name_butt_symbol,symbol);
      //--- Convert button name into its string ID
      string button=StringSubstr(name_butt_symbol,StringLen(prefix));
      ENUM_TIMEFRAMES timeframe=
        (
         StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
         TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
         TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
        );
      
      //--- Set the values for all structure fields
      Buffers[i].SetTimeframe(timeframe);
      Buffers[i].SetUsed(state_symbol);
      Buffers[i].SetShowDataFlag(state_symbol);
      
      //--- Bind drawn indicator buffers by the buffer index equal to the loop index with the structure bar price arrays (OHLC)
      SetIndexBuffer(Buffers[i].IndexOpenBuffer(),Buffers[i].Open,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexHighBuffer(),Buffers[i].High,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexLowBuffer(),Buffers[i].Low,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexCloseBuffer(),Buffers[i].Close,INDICATOR_DATA);
      //--- Bind the color buffer by the buffer index equal to the loop index with the corresponding structure arrays
      SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX);
      //--- set the "empty value" for all structure buffers, 
      PlotIndexSetDouble(Buffers[i].IndexOpenBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexHighBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexLowBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexCloseBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexColorBuffer(),PLOT_EMPTY_VALUE,0);
      //--- set the drawing type
      PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES);
      //--- Depending on the button status, set the graphical series name
      //--- and specify whether the buffer data in the data window is displayed or not
      SetPlotBufferState(i,state_symbol);
      //--- set the direction of indexing of all structure buffers as in the timeseries
      ArraySetAsSeries(Buffers[i].Open,true);
      ArraySetAsSeries(Buffers[i].High,true);
      ArraySetAsSeries(Buffers[i].Low,true);
      ArraySetAsSeries(Buffers[i].Close,true);
      ArraySetAsSeries(Buffers[i].Color,true);
      
      //--- Print data on the next buffer in the journal
      //Buffers[i].Print();
     }
   //--- Bind the calculated indicator buffer by the FirstFreePlotBufferIndex() buffer index with the BufferTime[] array of the indicator
   //--- set the direction of indexing the BufferTime[] calculated buffer as in the timeseries
   int buffer_temp_index=FirstFreePlotBufferIndex();
   SetIndexBuffer(buffer_temp_index,BufferTime,INDICATOR_CALCULATIONS);
   ArraySetAsSeries(BufferTime,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Vejamos o manipulador OnCalculate() do indicador:

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

//--- Check for the minimum number of bars for calculation
   if(rates_total<min_bars || Point()==0) return 0;
   
//--- Handle the Calculate event in the library
//--- If the OnCalculate() method of the library returns zero, not all timeseries are ready - leave till the next tick
   if(engine.0)
      return 0;
   
//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER)) 
     {
      engine.OnTimer(rates_data);   // Working in the timer
      PressButtonsControl();        // Button pressing control
      EventsHandling();             // Working with events
     }
//+------------------------------------------------------------------+
//| OnCalculate code block for working with the indicator:           |
//+------------------------------------------------------------------+
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Check and calculate the number of calculated bars
//--- If limit = 0, there are no new bars - calculate the current one
//--- If limit = 1, a new bar has appeared - calculate the first and the current ones
//--- If limit > 1, there are changes in history - the full recalculation of all data
   int limit=rates_total-prev_calculated;
   
//--- Recalculate the entire history
   if(limit>1)
     {
      limit=rates_total-1;
      InitBuffersAll();
     }
//--- Prepare data

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

Em geral, tentei descrever tudo o que fazemos em todas as funções nos comentários da listagem de todas as funções apresentadas.
Espero que não surjam dúvidas. Em qualquer caso, você pode perguntar na discussão do artigo, eu, pela minha parte, ficarei feliz em responder.

Todas as outras funções herdadas pelo indicador de sua versão anterior permanecem sem mudanças significativas.
O código completo do indicador pode ser encontrado nos arquivos anexados ao final do artigo.

Vamos compilar o indicador e executá-lo no gráfico EURUSD M15:


O que vemos é que são exibidos quatro botões com os primeiros quatro símbolos. Até que qualquer um dos botões seja pressionado, eles não exibem os botões de seleção de período. Vale a pena clicar no botão de símbolo, para ele é aberta uma lista desde os botões de seleção de período. Selecionamos o período e vemos como as velas do símbolo e o período selecionados são exibidas no gráfico. Agora o estado dos botões selecionados é escrito nas variáveis globais do terminal. Após reiniciar o indicador ou pressionar o botão de outro símbolo e, em seguida, retornar ao anterior, os botões dos períodos com o botão selecionado do período usado anteriormente já serão exibidos para este último.

Testamos a ideia de construir buffers de indicadores, armazenando-os em estruturas. Porém, trabalhar com eles a partir do indicador ainda não é muito conveniente. Por isso, a partir do próximo artigo, começaremos a criar classes de buffers de indicadores, que permitirão que criemos nossos próprios indicadores de forma mais conveniente e fácil, com menos esforço.

Bem, ao longo dos últimos dois artigos, nos familiarizamos com as maneiras de criar facilmente indicadores multissímbolos multiperíodos usando as séries temporais da biblioteca.

O que vem agora?

No próximo artigo, começaremos a desenvolver classes de buffers de indicadores.

Abaixo estão anexados todos os arquivos da versão atual da biblioteca e os arquivos do EA de teste. Você pode baixá-los e testar tudo sozinho.
Se você tiver dúvidas, comentários e sugestões, pode expressá-los nos comentários do artigo.
Gostaria de chamar sua atenção para o fato de que neste artigo fizemos um indicador de teste em MQL5 para MetaTrader 5.
Os arquivos anexados estão destinados apenas ao MetaTrader 5 e a versão atual da biblioteca ainda não foi testada no MetaTrader 4.
Para a quarta versão, o tipo de buffer de plotagem usado hoje (DRAW_COLOR_CANDLES) não é suportado, mas ao criar classes de buffers de indicadores, tentaremos implementar algumas coisas do MQL5 para MetaTrader 4.

Complementos

Artigos desta série:

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

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

Arquivos anexados |
MQL5.zip (3705.42 KB)
Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos

No quinto artigo relacionado à criação de um monitor de sinal de negociação, nós consideraremos os sinais compostos e implementaremos a funcionalidade necessária. Em versões anteriores, nós usamos os sinais simples, como o RSI, WPR e CCI, e também introduzimos a possibilidade de usar os indicadores personalizados.

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

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

Linguagem MQL como um meio de marcação da interface gráfica de programas MQL (Parte 3). Designer de formulários Linguagem MQL como um meio de marcação da interface gráfica de programas MQL (Parte 3). Designer de formulários

Este artigo complementa a descrição da ideia de como construir uma interface de programa MQL com ajuda das construções da linguagem MQL. Um editor gráfico especial nos permitirá configurar interativamente um layout consistindo nas principais classes de elementos da GUI e, em seguida, as exportará para uma descrição MQL que será usada em nosso projeto MQL. Aqui são apresentados detalhes internos do editor e o manual do usuário. Códigos fonte estão anexados ao artigo.

Trabalhando com séries temporais na biblioteca DoEasy (Parte 42): classe de um objeto de buffer abstrato de indicador Trabalhando com séries temporais na biblioteca DoEasy (Parte 42): classe de um objeto de buffer abstrato de indicador

Com este artigo começaremos a criar classes de buffers de indicador para a biblioteca DoEasy. Hoje, criaremos uma classe base de buffer abstrato que será o alicerce para a criação de diversos tipos de classes de buffer de indicador.