Timeseries in DoEasy library (part 41): Sample multi-symbol multi-period indicator

7 August 2020, 07:39
Artyom Trishkin
0
9 769

Contents


Concept

During the two previous articles, we were working on the library's ability to work with indicators. In particular, we have implemented correct download of historical data and real-time update of the current data for the library timeseries. In the previous article, we have set the indicator buffers to the data structure for displaying data on the screen. A single structure describes a single drawn indicator buffer. If multiple drawn buffers are to be implemented, each buffer is defined by a single structure and each buffer structure is placed to the array.

In the current article, I will continue refining the concept of working with the indicator buffers in the structures and create a multi-symbol multi-period indicator drawing a candle price chart of one of the specified pairs with the specified chart period. Besides, we will gradually come to understand the need to create classes of the indicator buffers.

The library features the class of messages allowing selection of the language of messages displayed by the library, as well as easily adding any number of custom languages for displaying the library messages in one of the specified languages. Currently, we do not have the means to select a language for translating input descriptions. After compilation, all input descriptions are displayed in a language a user applied to write the input description in their program.
Here we do not have much freedom of choice in implementing the ability to select a language to be used to describe program inputs — either we use a single language or create a similar set of inputs for each of the necessary compilation languages.

Let's choose the second option and create a separate file featuring the necessary enumerations for inputs in two possible languages of the enumeration constants description — Russian and English. Thus, a user should translate the enumeration constant descriptions from Russian into a necessary language. Since English is required to publish products in the Market service, it should remain at all times.

Improving library classes and data

Let's do some restructuring of data location in the library files.

The structure used to pass the current bar data to the library from the OnCalculate() handler is located in \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                                                     |
//+------------------------------------------------------------------+

But this structure does not apply to predefined variables and static values. It is more suitable for the definition of "Data". Therefore, let's remove it from Defines.mqh and define it in \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                                                        |
//+------------------------------------------------------------------+

We have already mentioned a separate file with enumerations for program inputs. The file is not created yet but its inclusion has already been set to avoid editing the Datas.mqh file again.

We have also added two arrays to the new block for arrays — these arrays will be available from the library-based program. The arrays are to contain the lists of used symbols and timeframes selected in the program inputs.

Now let's create the file \MQL5\Include\DoEasy\InpDatas.mqh to store enumerations for program inputs:

//+------------------------------------------------------------------+
//|                                                     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,                              // Работа только с текущим символом 
   SYMBOLS_MODE_DEFINES,                              // Работа с заданным списком символов 
   SYMBOLS_MODE_MARKET_WATCH,                         // Работа с символами из окна "Обзор рынка" 
   SYMBOLS_MODE_ALL                                   // Работа с полным списком символов 
  };
//+------------------------------------------------------------------+
//| Mode of working with timeframes                                  |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Работа только с текущим таймфреймом 
   TIMEFRAMES_MODE_LIST,                              // Работа с заданным списком таймфреймов 
   TIMEFRAMES_MODE_ALL                                // Работа с полным списком таймфреймов 
  };
#endif 
//+------------------------------------------------------------------+

Here all is simple: set a macro substitution. If it does not exist, the compilation is performed with the enumerations featuring English language descriptions. If the macro substitution does not exist (the string with its declaration is commented out), the compilation is performed with the constant enumerations featuring descriptions in Russian (or any other language set by a user for enumeration constants).

New enumerations will be added to the file whenever required.

Fix the error in the method of adding the object of all symbol timeseries to the list sometimes causing the error of accessing a non-existent pointer in \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh of the CTimeSeries class:

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

After getting the error of adding an object to the list, remove the 'series' object and attempt accessing it for setting the flag of its usage. In this case, we get the error since the pointer to the object has already been removed.

To fix this, simply move setting the flag prior to verifying the result of adding an object to the list in the code:

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

It is not always possible to place the "New bar" event to the list of events with the correct event time (new bar open time) in the methods of updating the specified timeseries list and all timeseries lists. Sometimes, the time becomes equal to zero.

To fix this, create the new variable for storing time. If the program type is "indicator" and the work is performed on the current symbol and chart period, write the time to the variable from the structure of prices received from OnCalculate(), otherwise get the time from the value returned by the LastBarDate() method of the timeseriesd object. Use the obtained time when adding an event to the list of all object events of all symbol timeseries:

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

To update all timeseries, we need to separate the place the timeseries update is called from for the current symbol and the rest. Any other timeseries are updated in the timer, while timeseries of the current symbol are updated in OnCalculate(). This is done to avoid excessive use of the current symbol timeseries in the timer in search of a new tick since the update of the current symbol timeseries is called in OnCalculate() upon a new tick arrival.

To work in the timer, declare yet another method in the file of the timeseries collection class \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);

The method calls the methods of updating all timeseries except for the current symbol (implementing the method):

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

In the file of the library service functions \MQL5\Include\DoEasy\Services\DELib.mqh, add the function returning the number of bars of the second specified period inside one bar of the first specified chart period:

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

Since the PeriodSeconds() function returns the number of seconds in the period, it is enough to divide the number of seconds of a greater period into the number of seconds of a lower one to define the number of bars of a single (smaller) period within a single bar of another (greater) period. This is exactly what we do here.

We are able to set the list of used symbols in our programs. The list is set in the library in the SetUsedSymbols() method of the symbol collection class in \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh. If we set the list of used symbols without the current symbol in our program, the library creates the timeseries of all symbols specified in the symbol settings in the timeseries collection excluding the current symbol. But we need it since it is constantly accessed when positioning data on the screen. Therefore, we need to fix this.

In the SetUsedSymbols() method of the symbol collection class, add the current symbol to the list. It will be added to the list provided that the current symbol is not present in the list of working symbols specified by a user in the program settings. If the symbol is already present there, no new symbol with the same name is added:
//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

In \MQL5\Include\DoEasy\Engine.mqh of the CEngine library main object, declare three private methods:

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

The methods are needed to write the list of symbols and timeframes to previously declared arrays in the Datas.mqh file, as well as to return the flag of a symbol presence in the array of used symbol names and the timeframe presence in the array of used timeframes.

Implementing the methods returning symbol and timeframe presence flags in the appropriate arrays:

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

Get the next array element in a loop by the appropriate array and compare it with the value passed to the method. If the value of the next array element matches the one passed to the method, return true. Upon completion of the entire loop, return false — no matches found among element values in the array and the one passed to the method.

Implementing the method of writing used symbols and timeframes to the array:

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

The method views all created timeseries for each symbol used in the program and fills the arrays of used symbols and timeframes with data from the timeseries collection. All method listing strings are commented in detail and are easy to understand. If you have any questions, feel free to ask them in the comments below.

In the block of the timeseries update methods, add the protected method for updating all timeseries except for the current symbol timeseries:

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

We considered the necessity of such a method earlier. Here the method simply calls the timeseries collection class method of the same name we have considered above.

In the block of handling timeseries collection of the class timer, call this method for updating all timeseries except the current one:

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

In the Calculate event handler (namely, in the OnCalculate() method of the CEngine library main object), return zero in case not all timeseries are created yet, and rates_total if all used timeseries are fully created:

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

Previously, rates_total passed to the method via the current bar price structure was immediately returned. However, we need to manage the value returned from the method to handle timeseries synchronization correctly. Zero returned for launching re-calculation of the entire history, while rates_total is used only to calculate data that has not yet been calculated (usually, this is 0 — current bar calculation or 1 — calculation of previous and current bars at the moment of opening a new bar).

Add writing to the arrays of all used symbols and timeseries in the method of creating all timeseries for all used symbols:

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

After calling the method from the library initialization function in the program, calling the method prepares two arrays to be used in programs if necessary — the array of all used symbols and the array of all used timeframes. The method has been considered above.

This concludes the improvement of the library classes.

Today, we will create a test indicator to see how library works with indicators in multi-symbol and multi-period modes.
The indicator is to provide the ability to set four used symbols and all possible timeseries. A symbol and timeframe the indicator is to work with are selected using the buttons. The chart displays up to four buttons with the names of symbols specified in the settings. The list of buttons with available timeframes is displayed opposite to the symbol whose button is pressed.
Only one symbol button and one timeframe button of the symbol can be pressed at any single moment.

This allows us to select a symbol the indicator is to work with and a timeframe whose data is to be displayed in the indicator subwindow on the chart. Looking ahead, I should note that the implementation of working with the buttons in procedural style turned out to be quite inconvenient for me. Therefore, the code for managing the button status has much room for improvement. Anyway, it is still sufficient for testing the timeseries operation in the multi-symbol multi-period indicator. This is only a test indicator after all.

Creating a test indicator

The idea behind the previous test indicator and the currently developed one lies not only in testing and checking its work in indicators with the library timeseries but also in test running the indicator buffer structure. We will arrange the set of indicator buffer classes based on obtained knowledge of using the structure. Today I will supplement the buffer structure making it a drawing buffer in candlestick style.

To create a test indicator, use the indicator from the previous article and save it in \MQL5\Indicators\TestDoEasy\Part41\ under the name TestDoEasyPart41.mq5.

First, specify drawing the indicator in a separate window, describe all necessary indicator buffers and add one more macro substitution indicating the maximum number of used symbols (and accordingly, the number of drawn indicator buffers):

//+------------------------------------------------------------------+
//|                                             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 arrays (Open[] High[] Low[] Close[] Color[]) * 4 drawn buffers + 1 BufferTime[] calculated buffer
#property indicator_plots   4       // 1 candlesticks buffer consisting of 5 arrays (Open[] High[] Low[] Close[] Color[]) * 4 symbols
//--- 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

Why is the number of indicator buffers equal to 21?
The answer is simple: the DRAW_COLOR_CANDLES drawing style implies there are five arrays associated with it:

  1. Open price array
  2. High price array
  3. Low price array
  4. Close price array
  5. Color array

The maximum number of symbols equal to 4 is to be used in the indicator. Accordingly, four drawn buffers with five associated arrays mean 20 indicator buffers. One more buffer is necessary to store the bar time in it. The time is to be passed to the functions. Total: 21 indicator buffers, four of them are drawn.

Write the candlesticks buffer structure:

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

The structure has the variables for storing values of the buffer indices related to the appropriate OHLC and Color arrays. We are always able to access the necessary buffer by index. The next free index for binding the new indicator buffer to the structure arrays can always be obtained from the m_buff_next_index variable using the IndexNextBuffer() method returning the index following the color buffer in the current structure.

According to the listing, the structure features all methods for setting and returning all values defined in the private structure section, as well as the method for printing all structure data in the journal: OHLC and color buffer indices data, the next free index for binding a new array and the flag of using the buffer data are used to generate the array. Next, all this loop data is passed to the journal from the array.

For example, this method is used to send the data on four drawn buffers set in the indicator settings to the journal (AUDUSD buffer is displayed on the chart):

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

As we can see, the Open buffer index of each subsequent candlesticks buffer matches the "Next buffer index" index of the previous candlesticks buffer. The next free buffer has the index of 20. This index can be used to assign the next one, for example the calculated indicator buffer. This is exactly what is done for the calculated buffer storing the bar time of the current chart.

Add the block of the indicator inputs:

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

The enumeration for selecting the mode of using symbols ENUM_SYMBOLS_MODE in Defines.mqh features two unnecessary modes "Work with Market Watch window symbols" and "Work with the full list of symbols":

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

... then in order to avoid selecting two modes in the settings, make the InpModeUsedSymbols variable non-external by commenting out its sinput modifier. Thus, the mode of working with symbols in the indicator is always equal to "Work with the specified symbol list" and the first four symbols from the list specified by the InpUsedSymbols input are to be used.

Let's write the definition of indicator buffers and the block of global variables:

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

We declared the array of candlesticks buffer structures as drawn buffers. This is much more convenient than defining four similar buffers. Besides, it is much more convenient to access each buffer by the index of its location in the array matching the button: if you need to select the buffer by the first button, select the buffer located first in the array, if you need to select the buffer assigned for the last button, select the last buffer in the array, etc.

One of the calculation buffers — the time buffer — is necessary for passing the bar time in the indicator function from the time[] predefined array of the indicator's OnCalculate() handler.

We are already familiar with the block containing global variables I have been using in the test EAs and indicators in almost all articles. All variables are accompanied with comments, so there is no point in thoroughly analyzing them here.
We need the minimum number of bars necessary for calculating the indicator to define if the bars are sufficient for calculating the timeseries, so that the indicator data from the higher timeframe is displayed correctly on the lower one.

For example, if we are on М15, while the display data is taken from Н1 chart, we need at least 4 bars for correct display of all bars since a single one-hour bar contains four fifteen-minute ones.

Depending on the applied period, the calculation of the required number of current chart bars is performed by the previously considered NumberBarsInTimeframe() function included in the DELib.mqh file of the library service functions.

I have already mentioned the difficulties I faced when writing the indicator in procedural style — I had to create additional auxiliary functions for searching, setting and monitoring button and buffer states. If the buttons and buffers have been written as objects, this would greatly simplify access to their properties and setting their modes. But I am not going to change anything yet. Introducing the test in procedural style seems faster. Besides, the test indicator requires no temporary objects. They will be of no use later.

Let's consider the newly developed auxiliary functions.

The function for setting the states of the drawn indicator buffers:

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

The function passes the index of the buffer whose status should be set. The status is passed by the input as well.

One feature should be kept in mind when working with the buffers of indicators requiring several related arrays to be displayed.

For example, if the indicator buffer requires 2 arrays, the indices of the arrays related to that buffers will be equal to 0 and 1. These values are set using the SetIndexBuffer() function. Applying one drawn buffer using two data arrays causes no issues in understanding the access to the drawn buffer — simply specify the buffer with the index 0 for accessing its properties.

But if we need two or more drawn buffers using two arrays, then there may be a misunderstanding about which index to use to access the second, third and subsequent drawn buffers.

Let's consider an example of three drawn buffers with two arrays each, as well as the numbers of indices of drawn buffers and their arrays:
  • Drawn buffer 1 — drawn buffer 0 index
    • Array 1 — buffer 0 index
    • Array 2 — buffer 1 index
  • Drawn buffer 2 — drawn buffer 1 index
    • Array 1 — buffer 2 index
    • Array 2 — buffer 3 index
  • Drawn buffer 3 — drawn buffer 2 index
    • Array 1 — buffer 4 index
    • Array 2 — buffer 5 index

It may seem that there are six arrays for three drawn buffers and, in order to access the second drawn array, we should access the index 2 (since 0 and 1 are occupied by the first buffer arrays). However, this is not the case. To access the second drawn buffer, we need to access the indices of drawn buffers, rather than all arrays assigned as buffers for each drawn buffer, i.e. by index 1.

Thus, in order to bind the array with the buffer using the SetIndexBuffer() function, the serial numbers of all arrays meant for using as indicator buffers should be specified. However, in order to get data from the drawn buffer using the PlotIndexGetInteger() function or set data for the drawn buffer using the PlotIndexSetDouble(), PlotIndexSetInteger() and PlotIndexSetString() functions, set the index of each drawn buffer, rather than the array number. In the current example, the index is 0 for the first drawn buffer, 1 for the second one and 2 for the third one. This should be taken into account.

The function returning the flag of using a symbol specified in the settings:

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

If a symbol is present in the array of used symbols, the function returns true, otherwise — false. Sometimes, we may not specify the current symbol in the list of used symbols, but it is present at all times — its data is necessary for performing internal library calculations. The function returns the flag indicating that the current symbol is not specified in the settings and should be skipped.

The function returning the index of the drawn buffer by symbol:

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

The function receives a symbol name, whose buffer index should be returned. In the loop by all buffers, search for a buffer with the symbol and return the loop index in case of match. If there is no buffer with such a symbol, return -1.

The function returning the number of the first free index the next drawn indicator buffer can be assigned to:

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

In the loop by all drawn buffers from the buffer structure array, check the value of the next free buffer.
If it exceeds the previous one, remember the new value. Upon the loop completion, return the written value from the num variable.

The functions written for performing auxiliary actions for installing and searching button and buffer states:

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

The function of handling button pressing has been slightly revised, since we are able to press only one symbol button and one period button corresponding to the symbol button:

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

The function for creating button panels:

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

The functions for initializing drawn indicator buffers:

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

The function for calculating a single bar of all active indicator buffers:

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

The function writing values of the passed bar object to the specified drawn buffer:

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


Now let's consider the indicator's OnInit() handler where the buttons are created and all indicator buffers are prepared:

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


Let's have a look at the indicator's OnCalculate() handler:

//+------------------------------------------------------------------+
//| 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 next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

I tried to describe everything we do in each function in the comments of the listing of all provided functions.
I hope, they are easy to understand. If you have any questions, feel free to ask them in the comments below.

The remaining functions are inherited by the indicator from its previous version. They are largely unchanged.
The full indicator code can be viewed in the files attached below.

Compile the indicator and launch it on EURUSD M15 chart:


We can see the four buttons with the first four symbols. The buttons of selecting periods to be displayed are displayed for them until any of the buttons is pressed. As soon as a symbol button is pressed, the list of the period selection buttons is opened for it. After selecting the period, the candles of the selected symbol and period are displayed on the chart. Now the status of selected buttons is written to the terminal global variables. After relaunching the indicator or pressing another symbol button and then returning to the previous one, the period buttons are displayed for it accompanied by the previously used period button already selected.

We have checked the concept of constructing indicator buffers by storing them in the structures. However, working with them from an indicator is still not very convenient. Therefore, starting from the next article, I am going to develop the classes of indicator buffers allowing for more easy and convenient indicator development.

Over the course of the last two articles, we have got acquainted with the methods of easy development of multi-symbol multi-period indicators using the library timeseries.

What's next?

Starting with the next article, I am going to develop indicator buffer classes.

All files of the current version of the library are attached below together with the test EA files for you to test and download.
Leave your questions, comments and suggestions in the comments.
Please keep in mind that here I have developed the MQL5 test indicator for MetaTrader 5.
The attached files are intended only for MetaTrader 5. The current library version has not been tested in MetaTrader 4.
The current buffer drawing type (DRAW_COLOR_CANDLES) is not supported in the fourth version. However, I will try to implement some MQL5 features in MetaTrader 4 when creating indicator buffer classes.

Back to contents

Previous articles within the series:

Timeseries in DoEasy library (part 35): Bar object and symbol timeseries list
Timeseries in DoEasy library (part 36): Object of timeseries for all used symbol periods
Timeseries in DoEasy library (part 37): Timeseries collection - database of timeseries by symbols and periods
Timeseries in DoEasy library (part 38): Timeseries collection - real-time updates and accessing data from the program
Timeseries in DoEasy library (part 39): Library-based indicators - preparing data and timeseries events
Timeseries in DoEasy library (part 40): Library-based indicators - updating data in real time

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/7804

Attached files |
MQL5.zip (3705.42 KB)
Continuous Walk-Forward Optimization (Part 7): Binding Auto Optimizer's logical part with graphics and controlling graphics from the program Continuous Walk-Forward Optimization (Part 7): Binding Auto Optimizer's logical part with graphics and controlling graphics from the program

This article describes the connection of the graphical part of the auto optimizer program with its logical part. It considers the optimization launch process, from a button click to task redirection to the optimization manager.

Timeseries in DoEasy library (part 40): Library-based indicators - updating data in real time Timeseries in DoEasy library (part 40): Library-based indicators - updating data in real time

The article considers the development of a simple multi-period indicator based on the DoEasy library. Let's improve the timeseries classes to receive data from any timeframes to display it on the current chart period.

Native Twitter Client for MT4 and MT5 without DLL Native Twitter Client for MT4 and MT5 without DLL

Ever wanted to access tweets and/or post your trade signals on Twitter ? Search no more, these on-going article series will show you how to do it without using any DLL. Enjoy the journey of implementing Tweeter API using MQL. In this first part, we will follow the glory path of authentication and authorization in accessing Twitter API.

Timeseries in DoEasy library (part 42): Abstract indicator buffer object class Timeseries in DoEasy library (part 42): Abstract indicator buffer object class

In this article, we start the development of the indicator buffer classes for the DoEasy library. We will create the base class of the abstract buffer which is to be used as a foundation for the development of different class types of indicator buffers.