Zeitreihen in der Bibliothek DoEasy (Teil 41): Beispiel eines Multisymbol- und Mehrperiodenindikators

10 August 2020, 09:31
Artyom Trishkin
0
310

Inhalt


Konzept

In der beiden vorangegangenen Artikel haben wir an der Fähigkeit der Bibliothek gearbeitet, mit Indikatoren zu arbeiten. Insbesondere haben wir das korrekte Herunterladen der historischen Daten und die Echtzeit-Aktualisierung der aktuellen Daten für die Bibliothekszeitreihen implementiert. Im vorigen Artikel haben wir die Indikatorpuffer auf die Datenstruktur für die Anzeige der Daten auf dem Bildschirm gesetzt. Eine einzige Struktur beschreibt einen einzelnen zu zeichnenden Indikatorpuffer. Wenn mehrere zu zeichnende Puffer implementiert werden sollen, wird jeder Puffer durch eine einzige Struktur definiert, und jede Pufferstruktur wird in das Array gestellt.

Im aktuellen Artikel werde ich das Konzept der Arbeit mit den Indikatorpuffern in den Strukturen weiter verfeinern und einen Multisymbol-Mehrperiodenindikator erstellen, der ein Kerzenpreisdiagramm eines der angegebenen Paare mit der angegebenen Diagrammperiode zeichnet. Außerdem werden wir allmählich die Notwendigkeit verstehen, Klassen von Indikatorpuffern zu erstellen.

Die Bibliothek verfügt über die Funktionen die Klasse der Meldungen, die es ermöglichen, die Sprache der von der Bibliothek angezeigten Meldungen zu wählen und auf einfache Weise eine beliebige Anzahl von benutzerdefinierten Sprachen für die Anzeige der Bibliotheksmeldungen in einer der angegebenen Sprachen hinzuzufügen. Derzeit haben wir nicht die Möglichkeit, eine Sprache für die Übersetzung von Eingabebeschreibungen auszuwählen. Nach der Kompilierung werden alle Eingabebeschreibungen in einer Sprache angezeigt, die ein Benutzer verwendet hat, um die Eingabebeschreibung in seinem Programm zu schreiben.
Hier haben wir keine große Wahlfreiheit bei der Implementierung der Möglichkeit, eine Sprache für die Beschreibung der Programmeingaben zu wählen — entweder verwenden wir eine einzige Sprache oder wir erstellen einen ähnlichen Satz von Eingaben für jede der erforderlichen Kompilationssprachen.

Wählen wir die zweite Option und erstellen wir eine separate Datei mit den erforderlichen Aufzählungen für Eingaben in zwei möglichen Sprachen der Beschreibung der Aufzählungskonstanten — Russisch und Englisch. Ein Nutzer müsste also die Beschreibungen der Enumerationskonstanten aus dem Russischen in eine notwendige Sprache übersetzen. Da Englisch erforderlich ist, um Produkte im Service Market zu veröffentlichen, sollte es immer Englisch bleiben.

Verbesserungen der Bibliotheks-Klassen und -Daten

Lassen Sie uns den Speicherort der Daten in den Bibliotheksdateien etwas umstrukturieren.

Die Struktur, die verwendet wird, um die aktuellen Balkendaten vom OnCalculate() an die Bibliothek zu übergeben, befindet sich 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                                                     |
//+------------------------------------------------------------------+

Diese Struktur gilt jedoch nicht für vordefinierte Variablen und statische Werte. Sie ist eher für die Definition von "Daten" geeignet. Daher entfernen wir sie aus Defines.mqh und definieren sie 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                                                        |
//+------------------------------------------------------------------+

Wir haben bereits eine separate Datei mit Aufzählungen für Programmeingaben erwähnt. Die Datei ist noch nicht erstellt aber sie wurde bereits eingebunden, um ein erneutes Editieren der Datei Datas.mqh zu vermeiden.

Wir haben auch zwei Arrays zum neuen Block für Arrays hinzugefügt — diese Arrays werden im bibliotheksbasierten Programm verfügbar sein. Die Arrays sollen die Listen der verwendeten Symbole und die in den Programmeingaben ausgewählten Zeitrahmen enthalten.

Lassen Sie uns nun die Datei \MQL5\Include\DoEasy\InpDatas.mqh erstellen, um Enumerationen für Programmeingaben zu speichern:

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

Hier ist alles einfach: nur eine Makro-Substitution setzen. Sollte sie nicht existieren, wird die Zusammenstellung mit den Enumerationen mit englischsprachigen Beschreibungen durchgeführt. Wenn die Makro-Substitution nicht existiert (die Zeile mit ihrer Deklaration ist auskommentiert), wird die Kompilierung mit den Enumerationen der Konstanten durchgeführt, die Beschreibungen in Russisch (oder einer anderen vom Nutzer für Aufzählungskonstanten festgelegten Sprache) enthalten.

Neue Enumerationen können der Datei bei Bedarf hinzugefügt werden.

Beheben wir den Fehler in der Methode zum Hinzufügen des Objekts aller Symbol-Zeitreihen zur Liste, der manchmal das Problem verursachte, auf einen nicht existierenden Zeiger in \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh der CTimeSeries-Klasse zuzugreifen:

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

Nachdem der Fehler beim Hinzufügen eines Objekts zur Liste aufgetreten ist, entfernen wir das Objekt 'series' und versuchen, auf das Objekt zuzugreifen, um das Flag seiner Verwendung zu setzen. In diesem Fall erhalten wir den Fehler, da der Zeiger auf das Objekt bereits entfernt wurde.

Um dies zu beheben, setzen wir einfach des Flags, bevor wir das Ergebnis des Hinzufügens eines Objekts zur Liste im Code verifizieren:

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

Bei den Methoden zur Aktualisierung der angegebenen Zeitreihenliste und aller Zeitreihenlisten ist es nicht immer möglich, das Ereignis "New bar" (neuer Balken) in der Liste der Ereignisse mit der korrekten Ereigniszeit (Öffnungszeit des neuen Balkens) zu platzieren. Manchmal wird die Zeit gleich Null.

Um dies zu beheben, erstellen wir die neue Variable zur Speicherung der Zeit. Wenn der Programmtyp "Indikator" ist und die Arbeit an der aktuellen Symbol- und Chart-Periode ausgeführt wird, schreiben wir die Zeit in die Variable aus der Struktur der von OnCalculate() erhaltenen Preise, andernfalls holen wir die Zeit aus dem Wert, der von der Methode LastBarDate() des Zeitrahmenobjekts zurückgegeben wird. Verwenden wir die erhaltene Zeit beim Hinzufügen eines Ereignisses zur Liste aller Objektereignisse aller Symbolzeitreihen:

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

Um alle Zeitreihen zu aktualisieren, müssen wir den Ort, von dem aus die Zeitreihenaktualisierung aufgerufen wird, für das aktuelle Symbol und den Rest trennen. Alle anderen Zeitreihen werden im Timer aktualisiert, während die Zeitreihen des aktuellen Symbols in OnCalculate() aktualisiert werden. Dies geschieht, um eine übermäßige Verwendung der Zeitreihen des aktuellen Symbols im Timer bei der Suche nach einem neuen Tick zu vermeiden, da die Aktualisierung der Zeitreihen des aktuellen Symbols bei Ankunft eines neuen Ticks in OnCalculate() aufgerufen wird.

Um im Timer zu arbeiten, deklarieren wir eine weitere Methode in der Datei der Kollektionsklasse der Zeitreihen \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);

Die Methode ruft die Methoden zur Aktualisierung aller Zeitreihen außer dem aktuellen Symbol auf (Implementierung der Methode):

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

Fügen wir in der Datei der Bibliotheks-Servicefunktionen \MQL5\Include\DoEasy\Services\DELib.mqh die Funktion hinzu, die die Anzahl der Balken der zweiten angegebenen Periode innerhalb eines Balkens der ersten angegebenen Diagrammperiode zurückgibt:

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

Da die Funktion PeriodeSekunden() die Anzahl der Sekunden der Periodenlänge zurückgibt, reicht es aus, die Anzahl der Sekunden einer größeren Periode in die Anzahl der Sekunden einer kleineren zu teilen, um die Anzahl der Balken einer einzelnen (kleineren) Periode innerhalb eines einzelnen Taktes einer anderen (größeren) Periode zu definieren. Genau das machen wir hier.

Wir sind in der Lage, die Liste der in unseren Programmen verwendeten Symbole festzulegen. Die Liste wird in der Bibliothek von der Methode SetUsedSymbols() der Kollektionsklasse der Symbole in \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh festgelegt. Wenn wir in unserem Programm die Liste der verwendeten Symbole ohne das aktuelle Symbol einstellen, erzeugt die Bibliothek die Zeitreihe aller in den Symboleinstellungen angegebenen Symbole in der Zeitreihenkollektion mit Ausnahme des aktuellen Symbols. Aber wir brauchen sie, da bei der Positionierung der Daten auf dem Bildschirm ständig darauf zugegriffen wird. Deshalb müssen wir dies beheben.

Fügen Sie in der Methode SetUsedSymbols() der Kollektionsklasse der Symbole das aktuelle Symbol zur Liste hinzu. Es wird der Liste hinzugefügt, vorausgesetzt, dass das aktuelle Symbol nicht in der Liste der Arbeitssymbole vorhanden ist, die von einem Nutzer in den Programmeinstellungen festgelegt wurde. Wenn das Symbol dort bereits vorhanden ist, wird kein neues Symbol mit dem gleichen Namen hinzugefügt:
//+------------------------------------------------------------------+
//| 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 des Hauptobjekts der Bibliothek CEngine, deklarieren wir drei private Methoden:

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

Die Methoden werden benötigt, um die Liste der Symbole und Zeitrahmen in zuvor deklarierte Arrays in der Datei Datas.mqh zu schreiben, sowie um das Flag einer Symbolpräsenz im Array der verwendeten Symbolnamen und die Zeitrahmenpräsenz im Array der verwendeten Zeitrahmen zurückzugeben.

Implementieren der Methoden, die Flags für Symbol- und Zeitrahmenpräsenz in den entsprechenden Arrays zurückgeben:

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

Holen wir uns das nächste Array-Element in einer Schleife über das entsprechende Array und vergleichen es mit dem an die Methode übergebenen Wert. Wenn der Wert des nächsten Arrayelements mit dem an die Methode übergebenen übereinstimmt, geben wir true zurück. Nach Abschluss der gesamten Schleife geben wir false zurück — keine Übereinstimmung zwischen den Elementwerten im Array und dem an die Methode übergebenen Wert gefunden.

Implementieren der Methode zum Schreiben der verwendeten Symbole und Zeitrahmen in das 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);
  }
//+------------------------------------------------------------------+

Die Methode zeigt alle erstellten Zeitreihen für jedes im Programm verwendete Symbol an und füllt die Arrays der verwendeten Symbole und Zeitrahmen mit Daten aus der Zeitreihensammlung. Alle Zeilen des Codes der Methode sind ausführlich kommentiert und leicht verständlich. Wenn Sie Fragen haben, können Sie diese gerne in den Kommentaren unten stellen.

Fügen wir im Block der Zeitreihen-Aktualisierungsmethoden die 'protected' Methode zur Aktualisierung aller Zeitreihen mit Ausnahme der aktuellen Symbolzeitreihe hinzu:

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

Wir haben die Notwendigkeit einer solchen Methode bereits früher in Betracht gezogen. Hier ruft die Methode einfach die gleichnamige Methode der Klasse der Zeitreihenkollektion auf, die wir oben besprochen haben.

Im Block zur Behandlung der Zeitreihenkollektion der Klassen-Timer rufen wir diese Methode auf, um alle Zeitreihen außer der aktuellen zu aktualisieren:

//+------------------------------------------------------------------+
//| 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 der Ereignisbehandlung Calculate (d.h. in der Methode OnCalculate() des Hauptobjekts der CEngine-Bibliothek) geben wir Null für den Fall zurück, dass noch nicht alle Zeitreihen erstellt sind, und rates_total für den Fall, dass alle verwendeten Zeitreihen vollständig sind:

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

Zuvor wurde rates_total über die aktuelle Balkenpreisstruktur an die Methode übergeben und sofort zurückgegeben. Wir müssen jedoch den von der Methode zurückgegebenen Wert verwalten, um die Zeitreihensynchronisation korrekt zu handhaben. Null wird zurückgegeben, um die Neuberechnung der gesamten Historie zu starten, während rates_total nur zur Berechnung von Daten verwendet wird, die noch nicht berechnet wurden (normalerweise ist dies 0 — Berechnung des aktuellen Balkens oder 1 — Berechnung der vorherigen und aktuellen Balken zum Zeitpunkt der Eröffnung eines neuen Balkens).

Hinzufügen aller verwendeten Symbole und Zeitreihen zum Array in der Methode zur Erstellung aller Zeitreihen für alle verwendeten Symbole:

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

Nach dem Aufruf der Methode aus der Bibliotheksinitialisierungsfunktion im Programm werden durch den Aufruf der Methode zwei Arrays vorbereitet, die bei Bedarf in Programmen verwendet werden können — das Array aller verwendeten Symbole und das Array aller verwendeten Zeitrahmen. Die Methode wurde oben besprochen.

Damit ist die Verbesserung der Bibliotheksklassen abgeschlossen.

Jetzt werden wir noch einen Testindikator erstellen, um zu sehen, wie die Bibliothek mit Indikatoren im Multisymbol- und Mehrperiodenmodus arbeitet.
Der Indikator soll die Möglichkeit bieten, vier verwendete Symbole und alle möglichen Zeitreihen festzulegen. Ein Symbol und ein Zeitrahmen, mit dem der Indikator arbeiten soll, werden mit Hilfe der Schaltflächen ausgewählt. Im Diagramm werden bis zu vier Schaltflächen mit den Namen der in den Einstellungen festgelegten Symbole angezeigt. Die Liste der Schaltflächen mit den verfügbaren Zeitreihen wird gegenüber dem Symbol angezeigt, dessen Schaltfläche gedrückt wird.
Es kann zu jedem Zeitpunkt nur eine Symboltaste und eine Zeitrahmen-Taste des Symbols gedrückt werden.

Dies ermöglicht uns die Auswahl eines Symbols, mit dem der Indikator arbeiten soll, und eines Zeitrahmens, dessen Daten im Indikator-Unterfenster auf dem Diagramm angezeigt werden sollen. Mit Blick auf die Zukunft möchte ich anmerken, dass sich die Implementierung der Arbeit mit den Schaltflächen im prozeduralen Stil für mich als ziemlich unbequem erwiesen hat. Daher ist der Code für die Verwaltung des Schaltflächenstatus noch stark verbesserungsfähig. Auf jeden Fall ist er immer noch ausreichend, um den Zeitreihenbetrieb in der Multisymbol-Mehrperiodenanzeige zu testen. Dies ist schließlich nur ein Testindikator.

Erstellen eines Testindikators

Die Idee hinter und dem derzeit entwickelten liegt nicht nur im Testen und Überprüfen seiner Arbeit an Indikatoren mit der Bibliothekszeitreihe, sondern auch im Testlauf der Indikator-Pufferstruktur. Wir werden den Satz von Indikator-Pufferklassen auf der Grundlage der gewonnenen Erkenntnisse über die Verwendung der Struktur zusammenstellen. Jetzt werde ich die Pufferstruktur ergänzen, indem ich sie zu einem Zeichenpuffer im Kerzen-Stil mache.

Um einen Testindikator zu erstellen, verwenden wir den Indikator aus dem vorherigen Artikel und speichern ihn in \MQL5\Indikatoren\TestDoEasy\Part41\ unter dem Namen TestDoEasyPart41.mq5.

Zuerst spezifizieren wir das Zeichnen des Indikators in einem separaten Fenster, beschreiben Sie alle notwendigen Indikatorpuffer und fügen eine weitere Makrosubstitution hinzu, die die maximale Anzahl der verwendeten Symbole (und entsprechend die Anzahl der zu zeichnenden Indikatorpuffer) angibt:

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

Warum ist die Anzahl der Indikatorpuffer gleich 21?
Die Antwort ist einfach: Der Zeichenstil DRAW_COLOR_CANDLES impliziert, dass ihm fünf Felder zugeordnet sind:

  1. Array der Eröffnungspreise
  2. Array der Höchstpreise
  3. Array der Tiefstpreise
  4. Array der Schlusskurse
  5. Array der Farben

Für den Indikator ist die maximale Anzahl von Symbolen gleich 4 zu verwenden. Dementsprechend bedeuten vier zu zeichnende Puffer mit fünf zugehörigen Arrays 20 Indikatorpuffer. Ein weiterer Puffer ist notwendig, um die Balkenzeiten darin zu speichern. Die Zeit ist an die Funktionen zu übergeben. Insgesamt: 21 Indikatorpuffer, davon werden vier gezeichnet.

Schreiben wir die Pufferstruktur der Kerzen:

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

Die Struktur verfügt über die Variablen zur Speicherung der Werte der Pufferindizes, die sich auf die entsprechenden OHLC- und Farb-Arrays beziehen. Über den Index können wir jederzeit auf den erforderlichen Puffer zugreifen. Der nächste freie Index für die Bindung des neuen Indikator-Puffers an die Struktur-Arrays kann immer aus der Variable m_buff_next_index unter Verwendung der Methode IndexNextBuffer(), die den auf den Farbpuffer in der aktuellen Struktur folgenden Index zurückgibt, gewonnen werden.

Entsprechend der Auflistung enthält die Struktur alle Methoden zum Setzen und Abrufen aller Werte, die im Abschnitt 'private' Struktur definiert sind, sowie die Methode zum Drucken aller Strukturdaten in der Zeitschrift: OHLC- und Farbpuffer-Indexdaten, der nächste freie Index zum Binden eines neuen Arrays und das Flag zur Verwendung der Pufferdaten werden zur Generierung des Arrays verwendet. Als Nächstes werden alle diese Schleifendaten aus dem Array an das Journal übergeben.

Diese Methode wird z.B. verwendet, um die Daten der vier zu zeichnenden Puffern, die in den Indikatoreinstellungen festgelegt wurden, an das Journal zu senden (AUDUSD-Puffer wird im Diagramm angezeigt):

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

Wie wir sehen können, stimmt der Pufferindex von Open jedes nachfolgenden Kerzen-Puffers mit dem Index "Nächster Pufferindex" des vorhergehenden Kerzen-Puffers überein. Der nächste freie Puffer hat den Index 20. Dieser Index kann verwendet werden, um den nächsten zuzuweisen, zum Beispiel den berechneten Indikator-Puffer. Genau dies geschieht für den berechneten Puffer, der die Balkenzeit des aktuellen Charts speichert.

Fügen wir den Block der Indikatoreingaben hinzu:

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

Die Enumeration für die Auswahl des Modus für die Verwendung der Symbole ENUM_SYMBOLS_MODE in Defines.mqh enthält zwei unnötige Modi "Work with Market Watch window symbols" (Arbeiten mit Symbolen im Fenster des Market Watch) und "Work with the full list of symbols" (Mit der vollständigen Liste der Symbole arbeiten):

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

... dann, um die Auswahl von zwei Modi in den Einstellungen zu vermeiden, machen wir die Variable InpModeUsedSymbols nicht-extern durch das Auskommentieren ihres Modifikators sinput. Somit ist der Modus der Arbeit mit den Symbolen im Indikator immer gleich "Work with the specified symbol list" und die ersten vier Symbole aus der durch die Eingabe InpUsedSymbols angegebenen Liste sind zu verwenden.

Schreiben wir die Definition von Indikatorpuffern und der Block der globalen Variablen:

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

Wir deklarierten das Array der Kerzen-Pufferstrukturen als zu zeichnende Puffer. Dies ist viel bequemer als die Definition von vier ähnlichen Puffern. Außerdem ist es viel bequemer, auf jeden Puffer über den Index seiner Position im Array zuzugreifen, der mit der Schaltfläche übereinstimmt: Wenn wir den Puffer über die erste Schaltfläche auswählen müssen, wählen wir den Puffer, der sich als erster im Array befindet, und wenn wir den für die letzte Schaltfläche zugewiesenen Puffer auswählen müssen, wählen wir den letzten Puffer im Array, usw.

Einer der Berechnungspufferder Zeitpuffer — ist notwendig, um die Balkenzeit in der Indikatorfunktion aus dem vordefinierten Array time[] von OnCalculate() des Indikators zu übergeben.

Wir sind bereits mit dem Block vertraut, der globale Variablen enthält, den ich in den Test EAs und Indikatoren in fast allen Artikeln verwendet habe. Alle Variablen sind mit Kommentaren versehen, so dass es keinen Sinn macht, sie hier gründlich zu analysieren.
Wir benötigen die für die Berechnung des Indikators erforderliche Mindestanzahl von Balken, um festzulegen, ob die Balken für die Berechnung der Zeitreihen ausreichen, damit die Indikatorendaten aus dem höheren Zeitrahmen korrekt auf dem unteren angezeigt werden.

Wenn wir zum Beispiel auf М15 sind, während die Anzeigedaten aus dem Chart Н1 entnommen werden, benötigen wir mindestens 4 Balken für die korrekte Anzeige aller Balken, da ein einziger Ein-Stunden-Balken vier Fünfzehn-Minuten-Balken enthält.

Abhängig von der angewandten Periode wird die Berechnung der erforderlichen Anzahl aktueller Diagrammbalken durch die zuvor in Betracht gezogene Funktion NumberBarsInTimeframe() durchgeführt, die in der Datei DELib.mqh der Bibliotheks-Servicefunktionen enthalten ist.

Ich habe bereits die Schwierigkeiten erwähnt, auf die ich beim Schreiben des Indikators im prozeduralen Stil gestoßen bin — ich musste zusätzliche Hilfsfunktionen für die Suche, Einstellung und Überwachung von Schaltflächen- und Pufferzuständen erstellen. Wenn die Schaltflächen und Puffer als Objekte geschrieben worden wären, würde dies den Zugriff auf ihre Eigenschaften und das Einstellen ihrer Modi erheblich vereinfachen. Aber ich werde noch nichts daran ändern. Die Einführung des Tests im prozeduralen Stil scheint schneller zu gehen. Außerdem benötigt der Testindikator keine temporären Objekte. Sie werden später nicht mehr von Nutzen sein.

Wenden wir uns den neu entwickelten Hilfsfunktionen zu.

Die Funktion zum Einstellen der Zustände der zu zeichnenden Indikatorpuffer:

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

Die Funktion übergibt den Index des Puffers, dessen Status gesetzt werden soll. Der Status wird ebenfalls über die Eingabe übergeben.

Bei der Arbeit mit den Puffern von Indikatoren, die die Anzeige mehrerer verwandter Arrays erfordern, ist eine Eigenschaft zu beachten.

Wenn der Indikatorpuffer beispielsweise 2 Arrays benötigt, sind die Indizes der Arrays, die sich auf diesen Puffer beziehen, gleich 0 und 1. Diese Werte werden mit der Funktion IndexPuffer() gesetzt. Die Anwendung eines zu zeichnenden Puffers unter Verwendung von zwei Daten-Arrays verursacht keine Probleme beim Verständnis des Zugriffs auf den zu zeichnenden Puffer — wir geben einfach den Puffer mit dem Index 0 für den Zugriff auf seine Eigenschaften an.

Wenn wir jedoch zwei oder mehr zu zeichnende Puffer unter Verwendung von zwei Arrays benötigen, kann es zu einem Missverständnis darüber kommen, welcher Index für den Zugriff auf den zweiten, dritten und nachfolgenden zu zeichnenden Puffer verwendet werden soll.

Betrachten wir ein Beispiel von drei zu zeichnenden Puffern mit jeweils zwei Arrays sowie die Anzahl der Indizes der zu zeichnenden Puffer und ihrer Arrays:
  • Drawn buffer 1 — zu zeichnender Puffer mit Index 0
    • Array 1 — Puffer mit Index 0
    • Array 2 — Puffer mit Index 1
  • Drawn buffer 2 — zu zeichnender Puffer mit Index 2
    • Array 1 — Puffer mit Index 2
    • Array 2 — Puffer mit Index 3
  • Drawn buffer 3 — zu zeichnender Puffer mit Index 2
    • Array 1 — Puffer mit Index 4
    • Array 2 — Puffer mit Index 5

Es mag den Anschein haben, dass es sechs Arrays für drei zu zeichnende Puffer gibt, und um auf das zweite zu zeichnende Array zuzugreifen, sollten wir auf den Index 2 zugreifen (da 0 und 1 von den ersten Puffer-Arrays belegt sind). Dies ist jedoch nicht der Fall. Um auf den zweiten zu zeichnenden Puffer zuzugreifen, müssen wir auf die Indizes von zu zeichnenden Puffer zugreifen, und nicht auf alle Arrays, die als Puffer für jeden gezogenen Puffer, d.h. durch Index 1, zugewiesen wurden.

Um das Array mit Hilfe der Funktion SetIndexBuffer() an den Puffer zu binden, sollten daher die Seriennummern aller Arrays, die als Indikatorpuffer verwendet werden sollen, angegeben werden. Um jedoch Daten aus dem zu zeichnenden Puffer mit der Funktion PlotIndexGetInteger() zu erhalten oder Daten für den zu zeichnenden Puffer mit den Funktionen PlotIndexSetDouble(), PlotIndexSetInteger() und PlotIndexSetString() zu setzen, setzen wir den Index jedes zu zeichnenden Puffers und nicht die Array-Nummer. Im aktuellen Beispiel ist der Index 0 für den ersten zu zeichnenden Puffer, 1 für den zweiten und 2 für den dritten. Dies sollte berücksichtigt werden.

Die Funktion gibt das Flag der Verwendung eines in den Einstellungen angegebenen Symbols zurück:

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

Wenn ein Symbol in dem Array der verwendeten Symbole vorhanden ist, gibt die Funktion true zurück, andernfalls — false. Manchmal kann es vorkommen, dass wir das aktuelle Symbol nicht in der Liste der verwendeten Symbole angeben, aber es ist immer vorhanden — seine Daten sind für die Durchführung von bibliotheksinternen Berechnungen notwendig. Die Funktion gibt das Flag zurück, das anzeigt, dass das aktuelle Symbol in den Einstellungen nicht spezifiziert ist und übersprungen werden sollte.

Die Funktion gibt den Index des zu zeichnenden Puffers nach dem Symbol zurück:

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

Die Funktion erhält einen Symbolnamen, dessen Pufferindex zurückgegeben werden soll. Wir suchen in der Schleife über alle Puffer nach einem Puffer mit dem Symbol und geben bei Übereinstimmung den Schleifenindex zurück. Wenn es keinen Puffer mit einem solchen Symbol gibt, geben wir -1 zurück.

Die Funktion, die die Nummer des ersten freien Index zurückgibt, dem der nächste gezogene Indikatorpuffer zugeordnet werden kann:

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

Überprüfen wir in der Schleife über alle zu zeichnenden Puffer aus dem Pufferstruktur-Array den Wert des nächsten freien Puffers.
Wenn er den vorherigen Wert überschreitet, merken wir uns den neuen Wert. Nach dem Ende der Schleife geben wir den eingetragenen Wert der Variablen num zurück.

Die Funktionen, die für die Durchführung von Hilfsaktionen zur Installation und Suche von Schaltflächen- und Pufferzuständen geschrieben wurden:

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

Die Funktion zur Handhabung des Drückens der Taste wurde leicht überarbeitet, da wir nur noch eine Symboltaste und eine der Symboltaste entsprechende Periodentaste drücken können:

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

Die Funktion zum Erstellen der Panels für die Schaltflächen:

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

Die Funktionen zum Initialisieren von zu zeichnenden Indikatorpuffern:

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

Die Funktion zur Berechnung eines einzelnen Balkens aller aktiven Indikatorpuffer:

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

Die Funktion, die Werte des übergebenen Balkenobjekts in den angegebenen, zu zeichnenden Puffers schreibt:

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


Kommen wir nun zu OnInit() des Indikators, in dem die Schaltflächen erstellt und alle Indikatorpuffer vorbereitet werden:

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


Lassen Sie uns einen Blick auf OnCalculate() des Indikators werfen:

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

Ich habe versucht, alles, was wir in jeder Funktion tun, in den Kommentaren des Codes aller bereitgestellten Funktionen zu beschreiben.
Ich hoffe, sie sind leicht zu verstehen. Wenn Sie Fragen haben, können Sie diese gerne in den Kommentaren unten stellen.

Die übrigen Funktionen werden durch den Indikator übernommen. Sie sind weitgehend unverändert.
Der vollständige Indikatorcode kann in den unten angehängten Dateien eingesehen werden.

Kompilieren Sie den Indikator und starten Sie ihn auf dem EURUSD M15-Chart:


Wir können die vier Schaltflächen mit den ersten vier Symbolen sehen. Für sie werden die Schaltflächen zur Auswahl der anzuzeigenden Perioden angezeigt, bis eine der Schaltflächen gedrückt wird. Sobald ein Symbolknopf gedrückt wird, wird für ihn die Liste der Periodenauswahlknöpfe geöffnet. Nach der Auswahl der Periode werden die Kerzen des gewählten Symbols und der gewählten Periode auf dem Diagramm angezeigt. Jetzt wird der Status der ausgewählten Schaltflächen in die globalen Terminalvariablen geschrieben. Nach dem Neustart des Indikators oder nach dem Drücken einer anderen Symbolschaltfläche und der Rückkehr zur vorherigen werden die Periodenschaltflächen für diesen Indikator zusammen mit der bereits ausgewählten, zuvor verwendeten Periodenschaltfläche angezeigt.

Wir haben das Konzept der Konstruktion von Indikatorpuffern überprüft, indem wir sie in den Strukturen gespeichert haben. Allerdings ist die Arbeit mit ihnen von einem Indikator aus immer noch nicht sehr komfortabel. Deshalb werde ich ab dem nächsten Artikel die Klassen von Indikatorpuffern entwickeln, die eine einfachere und bequemere Entwicklung von Indikatoren ermöglichen.

Im Laufe der letzten beiden Artikel haben wir uns mit den Methoden der einfachen Entwicklung von Multisymbol- und Mehrperiodenindikatoren unter Verwendung der Bibliothekszeitreihen vertraut gemacht.

Was kommt als Nächstes?

Beginnend mit dem nächsten Artikel werde ich Indikator-Pufferklassen entwickeln.

Alle Dateien der aktuellen Version der Bibliothek sind unten zusammen mit den Dateien der Test-EAs angehängt, die Sie testen und herunterladen können.
Hinterlassen Sie Ihre Fragen, Kommentare und Anregungen in den Kommentaren.
Bitte bedenken Sie, dass ich hier den MQL5-Testindikator für MetaTrader 5 entwickelt habe.
Die angehängten Dateien sind nur für MetaTrader 5 bestimmt. Die aktuelle Bibliotheksversion wurde nicht mit dem MetaTrader 4 getestet.
Der aktuelle Pufferzeichnungstyp (DRAW_COLOR_CANDLES) wird in der vierten Version nicht unterstützt. Ich werde jedoch versuchen, einige MQL5-Funktionen in MetaTrader 4 zu implementieren, wenn ich Indikator-Pufferklassen erstelle.

Zurück zum Inhalt

Frühere Artikel dieser Serie:

Zeitreihen in der Bibliothek DoEasy (Teil 35): das Balkenobjekt und die Liste der Zeitreihen eines Symbols
Zeitreihen in der Bibliothek DoEasy (Teil 36): Objekt der Zeitreihe für alle verwendeten Symbolperioden
Zeitreihen in der Bibliothek DoEasy (Teil 37): Kollektion von Zeitreihen - Datenbank der Zeitreihen nach Symbolen und Zeitrahmen
Zeitreihen in der Bibliothek DoEasy (Teil 38): Kollektion von Zeitreihen - Aktualisierungen in Echtzeit und Datenzugriff aus dem Programm

Zeitreihen in der Bibliothek DoEasy (Teil 40): Bibliotheksbasierte Indikatoren - Aktualisierung der Daten in Echtzeit

Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/7804

Beigefügte Dateien |
MQL5.zip (3705.42 KB)
Kontinuierliche Walk-Forward-Optimierung (Teil 7): Einbinden des logischen Teils des Auto-Optimizer mit Grafiken und Steuerung Kontinuierliche Walk-Forward-Optimierung (Teil 7): Einbinden des logischen Teils des Auto-Optimizer mit Grafiken und Steuerung

Dieser Artikel beschreibt die Verbindung des grafischen Teils des Auto-Optimizers mit seinem logischen Teil. Er betrachtet den Prozess des Optimierungsstarts, von einem Tastenklick bis zur Aufgabenumleitung zum Optimierungsmanager.

Entwicklung eines plattformübergreifenden Grid-EAs: Testen eines Mehrwährungs-EA Entwicklung eines plattformübergreifenden Grid-EAs: Testen eines Mehrwährungs-EA

Die Märkte brachen innerhalb eines Monats um mehr als 30% ein. Dies scheint der beste Zeitpunkt für die Prüfung von Expertenberatern mit Grid- und Martingal-Basis zu sein. Dieser Artikel ist eine ungeplante Fortsetzung der Serie "Entwicklung eines plattformübergreifenden Grid-EAs". Der aktuelle Markt bietet eine Gelegenheit, einen Stresstest für den Grid-EA zu arrangieren. Lassen Sie uns also diese Gelegenheit nutzen und unseren Expert Advisor testen.

Zeitreihen in der Bibliothek DoEasy (Teil 42): Abstrakte Objektklasse der Indikatorpuffer Zeitreihen in der Bibliothek DoEasy (Teil 42): Abstrakte Objektklasse der Indikatorpuffer

In diesem Artikel beginnen wir mit der Entwicklung der Indikatorpufferklassen für die DoEasy-Bibliothek. Wir werden die Basisklasse des abstrakten Puffers erstellen, die als Grundlage für die Entwicklung verschiedener Klassentypen von Indikatorpuffern verwendet werden soll.

Die Handelssignale mehrerer Währungen überwachen (Teil 5): Signalkombinationen Die Handelssignale mehrerer Währungen überwachen (Teil 5): Signalkombinationen

Im fünften Artikel, der sich auf die Schaffung eines Handelssignalmonitors bezieht, werden wir zusammengesetzte Signale betrachten und die notwendige Funktionalität implementieren. In früheren Versionen verwendeten wir einfache Signale, wie RSI, WPR und CCI, und wir führten auch die Möglichkeit ein, nutzerdefinierte Indikatoren zu verwenden.