Zeitreihen in der Bibliothek DoEasy (Teil 39): Bibliotheksbasierte Indikatoren - Vorbereitung der Daten und Zeitreihen

Artyom Trishkin | 20 Juli, 2020

Inhalt


Konzept

Alles, was wir bis zu diesem Zeitpunkt taten, bezog sich nur auf EA und Skripte. Es hatte in keiner Weise mit Indikatoren zu tun. Die Zeitreihe kann jedoch auch aktiv als Datenquelle für verschiedene Berechnungen in Indikatoren verwendet werden. Es ist also an der Zeit, auch sie in Betracht zu ziehen.

Im Gegensatz zu einem EA weisen Indikatoren eine völlig andere Architektur auf. Jeder Indikator wird mit dem einzigen Strom des einzigen Symbols ausgeführt, für den er gestartet wird. Das bedeutet, dass, wenn wir verschiedene Indikatoren auf mehreren Charts desselben Symbols starten, sie alle im selben Symbolstrang ausgeführt werden, zu dem alle diese Charts gehören.
Dementsprechend verlangsamt sich die gesamte Symbolbearbeitung, wenn einer der Indikatoren eine fehlerhafte Architektur aufweist. In diesem Fall frieren alle übrigen Indikatoren, die im selben Thread arbeiten, ein, während sie auf den "langsamen" Indikator warten.

Um Verzögerungen beim Warten auf historische Daten bei der Arbeit mit Indikatoren zu vermeiden, verfügt das Terminal über eine sequentielle Rückgabe der angeforderten Daten — die Funktionen, die das Laden der historischen Daten aktivieren, liefern das Funktionsergebnis sofort zurück, ohne zu warten.

Bei der Anforderung von Daten einer beliebigen Zeitreihe eines beliebigen Symbols mit Kopierfunktionen zeigen ein Indikator und ein EA ein unterschiedliches Verhalten, wenn das Terminal historische Daten sendet:

Wenn Daten von einem Indikator angefordert werden, gibt die Funktion sofort -1 zurück, wenn die angeforderten Zeitreihen noch nicht konstruiert sind oder sie vom Server heruntergeladen werden sollen, das Laden/Konstruieren selbst aber eingeleitet wird.

Wenn Daten von einem EA oder einem Skript angefordert werden, wird der Download vom Server initiiert, wenn das Terminal nicht über die entsprechenden Daten lokal verfügt, oder es wird mit dem Aufbau der notwendigen Zeitreihen begonnen, wenn die Daten aus der lokalen Historie aufgebaut werden können, aber noch nicht fertig sind. Die Funktion gibt die Menge zurück, die zum Zeitpunkt des Ablaufs der Zeitüberschreitung bereit sein wird, wobei das Herunterladen der Historie jedoch fortgesetzt wird, und die Funktion gibt bei der nächsten ähnlichen Anfrage weitere Daten zurück.

Wir können also sehen, dass das Terminal bei der Datenanforderung eines EAs mit dem Herunterladen von Daten beginnt (wenn noch keine lokal angeforderten Daten vorhanden sind oder diese nicht ausreichen). Nach Ablauf der Zeitüberschreitung gibt die Funktion die Menge an Historie zurück, die zum Zeitpunkt des Wartens auf den Abschluss des History-Downloads bereits vorhanden ist — das Terminal versucht sofort, uns die angeforderte Historie zur Verfügung zu stellen. Wenn die lokalen Daten nicht ausreichen, versucht er sie in der erforderlichen Menge herunterzuladen.
In der Zwischenzeit wartet das Programm darauf, dass die Daten heruntergeladen werden.

Im Falle von Indikatoren können wir nicht warten, also sendet uns das Terminal, was es hat (oder meldet, dass es nichts hat). Wenn es lokal keine Kurshistorie gibt oder diese bei der ersten Datenanforderung nicht ausreicht, beginnt der Download. Hier wartet das System vor dem Timeout nicht, bis die fehlenden Daten heruntergeladen sind.
In der aktuellen Situation sollte das Programm seinen Berechnungsteil vor dem nächsten Tick verlassen. Beim nächsten Aufruf von OnCalculate() des Indikators durch einen neuen Tick können die Daten bereits teilweise oder vollständig geladen und für Berechnungen verfügbar sein. Hier sollten wir entscheiden, wie viele Daten ausreichen werden, um den Programmalgorithmus ohne Unterbrechung auszuführen.

Außerdem sollte der Indikator nicht versuchen, seine eigenen Daten herunterzuladen — die Daten, auf deren Symbol und Periode er gestartet wird. Andernfalls kann eine solche Anfrage zu einem Konflikt führen. Das Terminal-Subsystem lädt solche Daten für Indikatoren herunter. Es liefert uns alle Daten über ihre Menge und ihren Status in den Variablen rates_total und prev_calculated von OnCalculate().

Auf der Grundlage dieser Mindestanforderungen müssen wir einige Klassen für die Arbeit mit Zeitreihen anpassen und das korrekte anfängliche Laden der für die Berechnungen in unseren Indikatoren erforderlichen Daten veranlassen.

Im vorliegenden Artikel werden wir die bereits erstellten Klassen anpassen, das korrekte initiale Laden der Daten aller verwendeten Zeitreihen in unsere Programme veranlassen und alle Ereignisse aller verwendeten Zeitreihen während ihrer Echtzeit-Aktualisierung an die Kontrollprogrammkarte senden.

Verbesserung der Klassen für die Arbeit mit Indikatoren, Erstellen von Zeitreihen-Ereignissen

Fügen wir zunächst die neuen Nachrichten in die Datei Daten.mqh — den Nachrichtenindizes — ein:

   MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,        // Failed to prepare array of used symbols. Error 
   MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY,              // Failed to get array of used symbols.
   MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING,            // Error. Die Zeichenfolge der vordefinierten Perioden ist leer und soll verwendet werden

...

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

...

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

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

und die Nachrichtentexte entsprechend den neu hinzugefügten Indizes:

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

...

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

...

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

Im Klassenkonstruktor des Basisobjekts CBaseObj aller Bibliotheksobjekte in \MQL5\Include\DoEasy\Objects\BaseObj.mqh< habe ich die Initialisierung der Variablen m_available geändert. Alle vom Basisobjekt CBaseObj abgeleiteten Objekte weisen direkt bei der Erstellung die Verfügbarkeitseigenschaft für die Arbeit im Programm mit dem Status "used" (true) auf. Zuvor wurde der Wert bei der Initialisierung in den Status "not used" (false) installiert:

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

Der Name der Methode zum Setzen des Flags eines im Objekt erkannten Ereignisses wurde in der Klasse des erweiterten Basisobjekts aller Bibliotheksobjekte CBaseObjExt in \MQL5\Include\DoEasy\Objects\BaseObj.mqh geändert:

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

Zuvor hieß die Methode SetEvent(), was verwirrend sein könnte, da SetEvent die Erzeugung, das Setzen, das Senden usw. eines beliebigen Ereignisses bedeuten kann, anstatt ein Signalflag für das Vorhandensein des Ereignisses zu setzen.

Daher wurden auch die Dateien der Klassen, die die Methode anwenden, geändert — der Aufruf der Methode SetEvent() wurde durch SetEventFlag() ersetzt. Die Details finden Sie in den angehängten Dateien.

Da die Handelsfunktionen in den Indikatoren deaktiviert sind, nehmen wir Änderungen in den Handelsobjektklassen vor.
Geben Sie in der plattformübergreifenden Handelsobjektklasse in \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh zu Beginn aller Handelsmethoden ein Häkchen für den Programmtyp ein. Wenn es sich um einen Indikator oder eine Dienstleistung handelt, verlassen wir die Methode und geben true zurück:

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

...

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

...

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

...

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

...

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

...

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

...

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

...

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

Die gleichen Änderungen wurden in allen gleichnamigen Handelsmethoden der Haupthandelsklasse der Bibliothek in \MQL5\Include\DoEasy\Trading.mqh vorgenommen.

Das Beenden von Handelsmethoden auf diese Weise erlaubt es nicht, Handelsfunktionen in Programmen aufzurufen, in denen sie deaktiviert sind, und gibt die erfolgreiche Ausführung der Methode zurück, wodurch die Behandlung von Bibliotheksfehlern verhindert wird.

Lassen Sie uns nun die Änderungen betrachten, die sich direkt auf die Klassen von Zeitreihenobjekten auswirkten.

In der Objektklasse der Bar habe ich die Texte, die vom Klassenkonstruktor im Falle eines Fehlers beim Empfang historischer Daten beim Erstellen eines Bar-Objekts angezeigt werden, leicht geändert. Der angezeigte Text enthält jetzt auch Konstruktornummer, Symbol und Zeitrahmen der Zeitreihe, für die das Balkenobjekt erstellt wurde.
Im ersten Konstruktor wird auf Datenabfragefehlern geprüft und Schreibzeit in die Zeitstruktur wurden in getrennten Blöcken gesetzt:

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

Diese Aktionen liefern uns im Falle eines Fehlers bei der Erstellung eines Balkenobjektes mehr Daten.

Da wir Zeitreihen-Arrays verwenden müssen, die von OnCalculate() bereitgestellt werden, um Daten über die Anzahl der Balken und ihre Werte auf dem aktuellen Periodensymbol anzufordern, müssen wir diese Arrays und Werte irgendwie an die Bibliotheksklassen übergeben.
Erstellen wir dazu die Struktur in \MQL5\Include\DoEasy\Defines.mqh. Die Struktur soll Variablen speichern, die verwendet werden, um alle notwendigen Daten, die für die aktuelle Zeitreihe berechnet wurden, an die Bibliothekszeitreihe zu übergeben:

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

Wie wir sehen können, enthält die Struktur alle notwendigen Felder für die Datenübergabe an die Bibliothek für jede Implementierung des OnCalculate()-Handlers des Indikators.

Die erste Form der Funktion

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

Es werden die Variablen rates_total, prev_calculated, begin und price verwendet.

Die zweite Form der Funktion

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

Es werden die Variablen rates_total und prev_calculated sowie die Struktur MqlRates rates zur Speicherung von Array-Werten verwendet.

Die aktuelle Strukturimplementierung ist geeignet, um den Wert von nur einem einzelnen Balken an die Bibliothek zu übergeben.

In der Klasse CSeries in \MQL5\Include\DoEasy\Objects\Series\\Series.mqh fügen wir das Flag zum Setzen von Serverdaten zu den Methoden zum Setzen eines Symbols und eines Zeitrahmens:

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

Standardmäßig ist das Flag deaktiviert. Dies verhindert das Setzen von Server-Datumsangaben beim Aufruf der Methode, da zum Aufruf der Methode zum Setzen von Server-Datumsangaben der Flag-Status zuerst geprüft wird:

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

Dies wurde getan, um ein mehrfaches Zurücksetzen der Serverdaten bei gleichzeitigem Aufruf der Methode zum Setzen eines Symbols und eines Zeitrahmens zu vermeiden:

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

Hier wird die Methode zum Setzen des Symbols zuerst aufgerufen (Flag deaktiviert), gefolgt von der Methode zum Setzen eines Zeitrahmens mit dem aktivierten Flag zum Aufruf der Methode zum Setzen von Serverdaten aus der Methode zum Setzen des Zeitrahmens.

Die Methode zur Aktualisierung der Zeitreihendaten übergeht jetzt die neue Struktur der Daten von OnCalculate() statt der vollständigen Liste ihrer Arrays:

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

So bietet die Implementation der Methode Refresh() jetzt den Zugriff auf die Strukturdaten statt auf die Arrays:

//+------------------------------------------------------------------+
//| Update timeseries list and data                                  |
//+------------------------------------------------------------------+
void CSeries::Refresh(SDataCalculate &data_calculate)
  {
//--- If the timeseries is not used, exit
   if(!this.m_available)
      return;
   MqlRates rates[1];
//--- Set the flag of sorting the list of bars by index
   this.m_list_series.Sort(SORT_BY_BAR_INDEX);
//--- If a new bar is present on a symbol and period,
   if(this.IsNewBarManual(data_calculate.rates.time))
     {
      //--- create a new bar object and add it to the end of the list
      CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,0);
      if(new_bar==NULL)
         return;
      if(!this.m_list_series.InsertSort(new_bar))
        {
         delete new_bar;
         return;
        }
      //--- Write the very first date by a period symbol at the moment and the new time of opening the last bar by a period symbol 
      this.SetServerDate();
      //--- if the timeseries exceeds the requested number of bars, remove the earliest bar
      if(this.m_list_series.Total()>(int)this.m_required)
         this.m_list_series.Delete(0);
      //--- save the new bar time as the previous one for the subsequent new bar check
      this.SaveNewBarTime(data_calculate.rates.time);
     }
//--- Get the bar object from the list by the terminal timeseries index (zero bar)
   CBar *bar=this.GetBarBySeriesIndex(0);
//--- if the work is performed in an indicator and the timeseries belongs to the current symbol and timeframe,
//--- copy price parameters (passed to the method from the outside) to the bar price structure
   int copied=1;
   if(this.m_program==PROGRAM_INDICATOR && this.m_symbol==::Symbol() && this.m_timeframe==(ENUM_TIMEFRAMES)::Period())
     {
      rates[0].time=data_calculate.rates.time;
      rates[0].open=data_calculate.rates.open;
      rates[0].high=data_calculate.rates.high;
      rates[0].low=data_calculate.rates.low;
      rates[0].close=data_calculate.rates.close;
      rates[0].tick_volume=data_calculate.rates.tick_volume;
      rates[0].real_volume=data_calculate.rates.real_volume;
      rates[0].spread=data_calculate.rates.spread;
     }
//--- otherwise, get data to the bar price structure from the environment
   else
      copied=::CopyRates(this.m_symbol,this.m_timeframe,0,1,rates);
//--- If the prices are obtained, set the new properties from the price structure for the bar object
   if(copied==1)
      bar.SetProperties(rates[0]);
  }
//+------------------------------------------------------------------+

Eine Suche in der Liste der Zeitreihenobjekte nach Zeitrahmen kann nun über die virtuelle Methode des Vergleichs zweier Zeitreihenobjekte durchgeführt werden:

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

Die Methode vergleicht die Eigenschaft "timeframe" der beiden verglichenen Zeitreihenobjekte (das aktuelle und das der Methode übergebene) und gibt Null zurück, wenn sie gleich sind.
Wir haben bereits die Logik ähnlicher Methoden zum Suchen und Sortieren verschiedener Objekte untersucht, die von dem Standard-Bibliotheksbasisobjekt CObjekt abgeleitet sind. Die Methode ist als virtuelle Methode im Basisobjekt der Standardbibliothek definiert. Daher sollte sie in nachkommenden Objekten implementiert werden, und die Methode sollte bei Gleichheit null oder 1/-1 zurückgeben, wenn der Wert der verglichenen Eigenschaft des aktuellen Objekts größer/kleiner ist als der Eigenschaftswert des verglichenen Objekts.

Da der erste Zugriff auf die Funktionen, die historische Daten zurückgeben, das Herunterladen von Daten aktiviert, falls diese lokal nicht vorhanden/unzureichend sind, Zugriff auf die erforderlichen historischen Daten (einfach das aktuelle Balkendatum abfragen) ganz am Anfang der Methode zur Einstellung der erforderlichen Datenmenge. Dies startet den Download der erforderlichen Daten (falls diese lokal nicht vorhanden sind):

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


Als wir das Objekt zur Speicherung der Listen aller Zeitreihen eines einzelnen Symbols (Klasse CTimeSeries) erstellt haben, haben wir dafür gesorgt, dass dieses Objekt immer eine Liste mit dem vollständigen Satz aller im Terminal möglichen Zeitreihen enthält. Die Liste der Zeitreihen werden der Liste sofort hinzugefügt. Sie werden jedoch nur bei Bedarf erstellt. Der Zugriff auf die Zeiger auf die notwendigen Zeitreihen erfolgte über den konstanten Index, der der Position des Zeitreihenindex der Liste in der Enumeration ENUM_TIMEFRAMES mit dem Offset 1 (wie im Artikel beschrieben) entspricht.

Dies wurde getan, um den Zugriff auf den Zeiger auf das notwendige Zeitreihenobjekt in der Liste zu beschleunigen. Es stellte sich jedoch heraus, dass der sofortige Zugriff auf den Zeiger von Problemen im Tester begleitet wird — der visuelle Tester erstellte Charts von absolut allen Zeitrahmen, unabhängig davon, ob sie tatsächlich im Programm verwendet wurden und ob ihre Zeitreihenlisten erstellt wurden.

Außerdem haben wir ein weiteres Problem beim Umschalten des Chartperiode während des Programmbetriebs — zuvor erstellte Listen werden nicht neu erstellt, und das Programm nimmt die Verfolgung von Ereignissen nicht vorhandener Objekte wieder auf und ersetzt sie durch andere.

Um eine weitere Häufung von versteckten Fehlern und eine langwierige Suche nach ihren Ursachen zu vermeiden, habe ich mich entschieden, Zeiger auf tatsächlich verwendete und erzeugte Zeitreihenlisten nur im Klassenobjekt CTimeSeries zu speichern, das Zeitreihenlisten aller verwendeten Zeitrahmen speichert. Mit anderen Worten, die Zeiger auf jede Zeitreihe jeder Chartperiode werden der Liste nur dann hinzugefügt, wenn das Programm explizit auf die Notwendigkeit ihrer Verwendung hinweist und ein solches Zeitreihenobjekt physisch erzeugt wird.

Öffnen wir \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh und fügen die notwendigen Verbesserungen hinzu.

Jetzt ist die Klasse der Zeitreihen eines einzelnen Symbols abgeleitet von der Klasse des erweiterten Basisobjekts aller Bibliotheksobjekte.
Dies geschieht, um die Ereignisfunktionalität der Klasse CBaseObjExt nutzen zu können:

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

Die Methode, die den Zeitreihenindex in der Liste nach Zeitreihennamen zurückgibt, wird jetzt einfach im 'private' Teil der Klasse deklariert:

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

Die Methode ist jetzt außerhalb des Klassenkörpers implementiert:

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

Die Methode erhält einen Zeitrahmen. Der Zeiger auf die Zeitreihe des Zeitrahmens sollte zurückgegeben werden.
Als Nächstes erstellen wir ein temporäres Zeitreihenobjekt mit dem erforderlichen Zeitrahmen.
Setzen wir das Flag für die sortierte Liste für die Liste der Zeitreihenobjekte
und holen den Zeitreihenobjektindex in der Liste, dessen Zeitrahmen gleich dem Zeitrahmen Temporäres Objekt ist.
Wenn ein solches Objekt in der Liste existiert, wird sein Index empfangen, andernfalls — WRONG_VALUE (-1).
Entfernen wir das temporäre Objekt und geben den erhaltenen Index zurück.

Anstelle der Methoden Create() und CreateAll() deklarieren wir die Methoden zum Hinzufügen der angegebenen Zeitreihe zu der Liste und die Methode zum Erstellen des angegebenen Zeitreihenobjekts, während die Methoden zum Aktualisieren der Zeitreihenlisten jetzt die Struktur der Parameterwerte und OnCalculate()-Arrays anstelle der vollständigen Liste der Arrays erhalten:

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

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

Entfernen wir die Schleife zur Erstellung von Zeitreihenlisten aus dem Klassenkonstruktor:

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

Jetzt werden die erforderlichen Zeitreihen erstellt, nachdem das Array der verwendeten Zeitreihen im OnInit()-Handler des Programms erstellt wurde. Jede Änderung in der Anzahl der im Programm verwendeten Chartperioden bewirkt eine EA-Neuinitialisierung oder die Neuerstellung eines Indikators, was zu einer vollständigen Neuerstellung der Liste der verwendeten Zeitreihenobjekte und ihrer korrekten Verrechnung in der Zukunft führt.

In den Methoden zur Einstellung der Historientiefe aller verwendeten Zeitreihen SetRequiredAllUsedData() und zur Rückgabe des Synchronisationsflags aller verwendeten Zeitreihen SyncAllData(), ersetzen wir die Schleife für die Gesamtzahl aller möglichen Zeitreihen

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

mit der Schleife für die Anzahl der realen Zeitreihenobjekte in der Liste:

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

Nun besteht die Liste aus tatsächlich erzeugten Zeitreihenobjekten, und die Schleifen werden entsprechend ihrer tatsächlichen Anzahl ausgeführt.

Implementation der Methode zum Hinzufügen des angegebenen Zeitreihenobjekts zu der Liste:

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

Die Methode erhält die Zeitreihen-Chartperiode, die der Symbol-Zeitreihenliste hinzugefügt werden soll.

Erstellen Sie ein Zeitreihen-Objekt mit einem Zeitrahmen, dessen Wert an die Methode übergeben wird.
Setzen Sie das Flag für die sortierte Liste für die Zeitreihenliste und suchen Sie in der Liste nach einem Zeitreihenobjekt, das dem neu angelegten entspricht.
Wenn die Liste kein solches Objekt enthält (die Suche ergibt -1), soll das erzeugte Zeitreihen-Objekt zur Liste hinzugefügt werden.
Andernfalls
entfernen wir das angelegte Objekt, da ein solches Zeitreihenobjekt bereits auf der Liste steht.
Wir setzen das Flag für die Verwendung der Zeitreihe im Programm und geben das Ergebnis des Hinzufügens der Zeitreihe zur Liste zurück.
Eine erfolgreiche Addition gibt true, anderfalls false zurück.

Die Bibliothek bietet die Ereignisfunktionalität im erweiterten Objekt aller Bibliotheksobjekte zum Senden von Ereignissen, die an die verschiedenen Objekte der Bibliothek auftreten. In den Artikeln 16 und 17 haben wir die Prinzipien und die Logik der Arbeit mit Bibliotheksereignissen besprochen.

Kurz gesagt, jedes vom Basisobjekt der Bibliothek CBaseObj abgeleitete Objekt (derzeit CBaseObjExt) verfügt über eine Liste, in der alle Ereignisse registriert werden, die innerhalb einer Schleife der Programmoperation bei einem einzigen Tick oder einer einzigen Timer-Iteration auf das Objekt einwirken können.
Bei der Identifizierung eines Objektereignisses wird das Flag eines eingetretenen Ereignisses für dieses Ereignis gesetzt. Anschließend können die Listen der Kollektionsobjekte in den Kollektionsklassen angezeigt werden. In den Listen werden wiederum die Flags geprüft. Wenn ein Objekt mit dem aktivierten Ereignisflag gefunden wird, empfängt die Kollektionsklasse dieser Objekte die Liste aller Objektereignisse mit aktiviertem Ereignisflag und sendet alle Ereignisse aus der Liste an die Kontrollprogrammkarte.
Das Programm selbst bietet die Funktionalität zur Behandlung aller eingehenden Ereignisse. Im Tester werden alle Ereignisse durch Ticks behandelt. Außerhalb des Testers werden sie in OnChartEvent() verarbeitet.

In der betrachteten Objektklasse aller Zeitreihen eines einzelnen Symbols CTimeSeries ist der beste Ort für die Definition von Ereignissen aller ihrer Zeitreihenlisten eine Methode zur Aktualisierung der angegebenen Refresh()-Zeitreihe und die Methode zur Aktualisierung aller Symbolzeitreihen RefreshAll().

Betrachten wir die Implementierung der Methoden zur Aktualisierung der Listen der Zeitreihen:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the timeseries event flag and clear the list of all timeseries events
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the timeseries from the list by its timeframe
   CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Update the timeseries list
   series_obj.Refresh(data_calculate);
//--- If the timeseries object features the New bar event
   if(series_obj.IsNewBar(data_calculate.rates.time))
     {
      //--- send the "New bar" event to the control program chart
      series_obj.SendEvent();
      //--- set the values of the first date in history on the server and in the terminal
      this.SetTerminalServerDate();
      //--- add the "New bar" event to the list of timeseries events
      //--- in case of successful addition, set the event flag for the timeseries
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeries::RefreshAll(SDataCalculate &data_calculate)
  {
//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal
//--- and the timeseries event flag, and clear the list of all timeseries events
   bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by the list of all used timeseries,
   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next timeseries object by the loop index
      CSeries *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0)
         continue;
      //--- update the timeseries list
      series_obj.Refresh(data_calculate);
      //--- If the timeseries object features the New bar event
      if(series_obj.IsNewBar(data_calculate.rates.time))
        {
         //--- send the "New bar" event to the control program chart,
         series_obj.SendEvent();
         //--- set the flag indicating the necessity to set the first date in history on the server and in the terminal
         upd=true;
         //--- add the "New bar" event to the list of timeseries events
         //--- in case of successful addition, set the event flag for the timeseries
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled,
//--- set the values of the first date in history on the server and in the terminal
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

Hier habe ich jede Codezeile der Methoden kommentiert, es sollte also alles klar sein. Wenn Sie Fragen haben, können Sie diese gerne in den Kommentaren unten stellen.

Dies vervollständigt die Klasse CTimeSeries des Objekts aller Zeitreihen für ein einzelnes Symbol.

Die nächste Klasse ist die CTimeSeriesCollection Kollektionsklasse der Symbol-Zeitreihen-Objekte. Sie sollte auch die Ereignisfunktionalität aufweisen, da sie "verantwortlich" ist für den Erhalt von Listen mit Ereignissen aus allen Objekten, die alle Zeitreihen jedes im Programm verwendeten Symbols speichern.

Öffnen Sie \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh und leiten Sie es von der erweiterten Basisklasse aller Bibliotheksobjekte ab:

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

Wir deklarieren im 'public' Teil der Klasse zwei Methoden zur Rückgabe des Objekts aller Zeitreihen des angegebenen Symbols und zur Rückgabe des Zeitreihenobjekts des angegebenen Symbols und der Periode:

public:
//--- Return (1) oneself and (2) the timeseries list
   CTimeSeriesCollection  *GetObject(void)            { return &this;         }
   CArrayObj              *GetList(void)              { return &this.m_list;  }
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period
   CTimeSeries            *GetTimeseries(const string symbol);
   CSeries                *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);

Schreiben wir die Implementierung außerhalb des Klassenkörpers.
Die Methode, die das Zeitreihen-Objekt des angegebenen Symbols zurückgibt:

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

Hier erhalten wir den Index des Zeitreihenobjekts für die Benennung eines Symbols unter Verwendung der IndexTimeSeries()-Methode, die wir im Teil 37 besprochen haben. Der erhaltene Index wird verwendet, um das Zeitreihenobjekt aus der Liste zu erhalten. Wenn es nicht möglich war, den Index oder ein Objekt aus der Liste zu erhalten, wird NULL zurückgegeben. Andernfalls erhalten wir den Zeiger auf das angeforderte Objekt in der Liste.

Die Methode, die das Zeitreihenobjekt des angegebenen Symbols/Periode zurückgibt:

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

Hier wir erhalten das Zeitreihen-Objekt mit der Methode GetTimeseries() (oben besprochen) durch ein der Methode übergebenes Symbol.
Aus dem erhaltenen Zeitreihen-Objekt holen wir uns die Zeitreihen-Liste in einem bestimmten Zeitrahmen und geben den Zeiger auf das erhaltene Zeitreihen-Objekt zurück.

Die Methode GetSeries() des Zeitreihenobjekts verwendet die oben erwähnte Methode IndexTimeframe(), um die erforderliche Zeitreihe zurückzugeben, während die Methode GetSeries() des Zeitreihenobjekts CTimeSeries wie folgt aussieht:

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

Im 'public' Teil der Klasse entfernen wir drei Methoden zur Erstellung von Zeitreihen und lassen nur eine zur Erstellung der angegebenen Zeitreihe des angegebenen Symbols übrig:

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

Drei entfernte Methoden scheinen hier bisher überflüssig zu sein. Lassen Sie uns also stattdessen drei neue Methoden deklarieren — für die Neuerstellung einer bestimmten Zeitreihe, für die Rückgabe einer leeren Zeitreihe und für die Rückgabe einer teilweise gefüllten Zeitreihe:

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

Warum müssen wir eine Zeitreihe neu erstellen? Bei der Initialisierung der Bibliothek und der Erstellung aller angewandten Zeitreihen aller Symbole verwenden wir die Funktionen, die den Download der historischen Daten initiieren. Wie ich bereits mehr als einmal gesagt habe, kann es zu Konflikten kommen, wenn ein Programm ein Indikator ist und sich auf ein Symbol und einen Zeitrahmen bezieht, in dem es gestartet wird. Deshalb werden solche Situationen übersprungen. Nach dem Ende des Starts und dem Einstieg in OnCalculate() sollten wir zunächst die erstellten Zeitreihen überarbeiten, die leer sind (bei der Initialisierung übersprungene) und sie unter Verwendung von Daten der Variablen rates_total in OnCalculate() neu erstellen.

Anstatt nun Zeitreihen-Arraydaten von OnCalculate() zu erhalten, erhält die Zeitreihen-Aktualisierungsmethoden die Datenstruktur. Deklarieren wir die Methode zum Abrufen von Ereignissen aus dem Zeitreihen-Objekt und fügen sie zur Ereignisliste aller Objekte der Symbol-Zeitreihensammlung hinzu:

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

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

//--- Display (1) the complete and (2) short collection description in the journal
   void                    Print(const bool created=true);
   void                    PrintShort(const bool created=true);
   
//--- Constructor
                           CTimeSeriesCollection();
  };
//+------------------------------------------------------------------+

Implementation der Methoden, die leere und teilweise gefüllt Zeitreihen zurückgeben:

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

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

Jede Zeile in der Methode ist kommentiert, und die Methoden sind ähnlich, mit Ausnahme von leeren und teilweise ausgefüllten Zeitreihen.

Die Methoden geben die erste kommende Zeitreihe zurück, die die Suchbedingungen erfüllt. Dies wurde getan, um nacheinander alle möglichen leeren oder teilweise gefüllten Zeitreihen bei jedem folgenden Tick (Eingabe von OnCalculate) zu erhalten. Dies entspricht den Empfehlungen von MetaQuotes für den korrekten Umgang mit unzureichenden Daten in Indikatoren — Verlassen der Funktion und Überprüfung des Vorhandenseins der Daten beim nächsten Tick.

Implementation der Methode zum Erstellen der angegebenen Zeitreihen des angegebenen Symbols:

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

Die Methode fügt dem Zeitreihenobjekt eines einzelnen Symbols die Daten hinzu — einer neuen Zeitreihe mit der angegebenen Chartperiode.
Die Methode erhält ein Symbol und die erforderliche Zeitreihenperiode.
Holen Sie das Zeitreihenobjekt und fügen Sie es der neuen Zeitreihe der spezifizierten Chartperiode hinzu.
Fordern Sie die Symbol-/Periodendaten an und stellen Sie die erforderliche Datenmenge in der Zeitreihe ein.
Wenn alle vorherigen Aktionen erfolgreich waren, geben Sie das Ergebnis der Erstellung einer neuen Zeitreihe und das Hinzufügen von Daten zu dieser zurück.

Wir haben all diese Methoden in den vorangegangenen Artikeln berücksichtigt. Hier habe ich eine neue Logik zur Erstellung der erforderlichen Symbol/Periodenzeitreihen eingeführt. Die Logik unterscheidet sich von der im Artikel 37 beschriebenen Logik.

Implementation der Methode zur Neuerstellung der angegebenen Zeitreihe des angegebenen Symbols:

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

Hier ist alles genau gleich, mit nur einem Unterschied — die Zeitreihe wurde bereits erstellt, sodass der Schritt des Hinzufügens einer neuen Zeitreihe zum Objekt aller Symbolzeitreihen übersprungen wird.

Implementation der Methode, die Ereignisse aus dem Zeitreihenobjekt empfängt und sie der Liste der Ereignisse der Zeitreihensammlung hinzufügt:

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

Die Methode empfängt den Zeiger auf das Symbol-Zeitreihen-Objekt. Alle seine Ereignisse werden der Liste der Ereignisse der Kollektion der Liste der Zeitreihen in einer Schleife von der Ereignisliste des Objekts hinzugefügt.

Implementieren der Methode zum Aktualisieren einer angegebenen Zeitreihe des angegebenen Symbols und Hinzufügen seiner Ereignisse zur Liste der Ereignisse der Zeitreihenkollektion:

//+------------------------------------------------------------------+
//| Update the specified timeseries of the specified symbol          |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the object of all symbol timeseries by a symbol name
   CTimeSeries *timeseries=this.GetTimeseries(symbol);
   if(timeseries==NULL)
      return;
//--- If there is no new tick on the timeseries object symbol, exit
   if(!timeseries.IsNewTick())
      return;
//--- Update the required object timeseries of all symbol timeseries
   timeseries.Refresh(timeframe,data_calculate);
//--- If the timeseries has the enabled event flag,
//--- get events from symbol timeseries, write them to the collection event list
//--- and set the event flag in the collection
   if(timeseries.IsEvent())
      this.m_is_event=this.SetEvents(timeseries);
  }
//+------------------------------------------------------------------+

Implementierung der Methode zur Aktualisierung aller Zeitreihen aller Symbole und Hinzufügen ihrer Ereignisse zur Liste der Ereignisse der Zeitreihensammlung:

//+------------------------------------------------------------------+
//| Update all timeseries of all symbols                             |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::Refresh(SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by all symbol timeseries objects in the collection,
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next symbol timeseries object
      CTimeSeries *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(!timeseries.IsNewTick())
         continue;
      //--- Update all symbol timeseries
      timeseries.RefreshAll(data_calculate);
      //--- If the event flag enabled for the symbol timeseries object,
      //--- get events from symbol timeseries, write them to the collection event list
      //--- and set the event flag in the collection
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }
//+------------------------------------------------------------------+

Alle diese Methoden werden ausführlich kommentiert und ihre Logik ist leicht verständlich.

Damit ist die Verbesserung aller Zeitreihenklassen in der aktuellen Phase abgeschlossen.

Lassen Sie uns nun das Hauptobjekt der Bibliothek CEngine (\MQL5\Include\DoEasy\Engine.mqh) verbessern, um mit der Zeitreihenkollektion von Programmen zu arbeiten.

Im 'private' Teil der Klasse deklarieren wir das Pausenobjekt:

class CEngine
  {
private:
   CHistoryCollection   m_history;                       // Collection of historical orders and deals
   CMarketCollection    m_market;                        // Collection of market orders and deals
   CEventsCollection    m_events;                        // Event collection
   CAccountsCollection  m_accounts;                      // Account collection
   CSymbolsCollection   m_symbols;                       // Symbol collection
   CTimeSeriesCollection m_time_series;                  // Timeseries collection
   CResourceCollection  m_resource;                      // Resource list
   CTradingControl      m_trading;                       // Trading management object
   CPause               m_pause;                         // Pause object

Fügen wir im 'public' Teil der Klasse die Methode hinzu, die das Flag der Ereignispräsenz in der Zeitreihensammlung zurückgibt:

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

Die Methode gibt das Ergebnis der Operation der Methode IsEvent() mit dem Kollektionsobjekt der Zeitreihen zurück.

Da die Array-Daten von OnCalculate() des Indikators nun an die Zeitreihenaktualisierungsmethoden zur Behandlung der aktuellen Zeitreihendaten gesendet werden sollen, fügen wir die Übergabe der Array-Datenstruktur an OnCalculate() zu den Ereignisbehandlungsmethoden Timer und Tick hinzu sowie die Deklaration der Methode zur Behandlung des Ereignisses Calculate:

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

Fügen wir im gleichen 'public' Teil der Klasse die Methode hinzu, die die Zeitreihen-Ereignisliste zurückgibt:

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

Die Methode gibt den Zeiger auf die Liste der Ereignisse der Zeitreihenkollektion mit der entsprechenden Methode GetListEvents()zurück.

Der 'public' Teile der Klasse enthält die vier Methoden zum Erstellen verschiedener Zeitreihen. Lassen Sie uns vorübergehend drei Methoden entfernen, die wir noch nicht benötigen:

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

und ersetzen sie durch die Deklaration der Methode zur Erstellung aller Zeitreihen aller verwendeten Kollektionssymbole. Schreiben Sie außerdem die Methode zur Neuerstellung der angegebenen Zeitreihen und deklarieren Sie die Methode zur Anforderung der Zeitreihensynchronisation mit dem Server:

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

Dort haben wir auch vier Methoden zur Aktualisierung der Zeitreihenkollektion.
Es bleiben nur zwei Methoden — die erste zur Aktualisierung der angegebenen Zeitreihen und die zweite zur Aktualisierung aller Zeitreihenkollektion:

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

Die Struktur mit den Daten über Variablen und Array von OnCalculate() wird an die Methoden anstelle der Arraywerte OnCalculate() übergeben.

Fügen wir vier neue Methoden hinzu — für die Rückgabe des Zeigers auf das Zeitreihenobjekt des angegebenen Symbols, für das angegebene Zeitreihenobjekt, sowie die Methoden zur Rückgabe der Zeiger auf eine leere und teilweise gefüllte Zeitreihe:

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

Die Methoden liefern das Ergebnis der Rückgabe gleichnamiger Methoden der Zeitreihensammlung, die wir oben betrachtet haben.

Die Methode TradingOnInit(), die die Zeiger auf alle notwendigen Sammlungen in die Handelsklasse übergibt, wurde in CollectionOnInit() umbenannt, da ein solcher Name dafür besser geeignet ist, da darin notwendige Initialisierungen aller Kollektionsklassen durchgeführt werden.

Fügen wir am Ende des Klassenkörpers den Block mit den Methoden für die Arbeit mit dem Pausenobjekt hinzu:

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

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

Die Klasse für die Pause wurde im Artikel 30 beschrieben. Die Klasse ist dafür gedacht, Pausen anstelle der Funktion Sleep() einzufügen, die in den Indikatoren nicht funktioniert.

Zusätzlich zu den bereits beschriebenen Methoden der CPause-Klasse, die von diesen Methoden aus aufgerufen werden, haben wir eine weitere Pause()-Methode hinzugefügt, die es uns erlaubt, eine neue Wartepause ohne vorherige Initialisierung ihrer Parameter zu starten — alle Parameter werden an die Methode übergeben, während die Methode das Warten auf die Beendigung der Pause in Millisekunden, die als Eingabe an die Methode übergeben wird, beinhaltet. Diese Methoden können in Programmen zur Organisation von Pausen in Indikatoren nützlich sein.

Beachten Sie, dass dieses Pausenobjekt den Hauptthread verzögert, auf dem der Indikator gestartet wurde, genau wie die Funktion Sleep().
Diese Pause sollte bei Bedarf in den Indikatoren angewandt werden.

Der Zeitgeber der CEngine-Klasse wurde neu angeordnet — zuvor haben wir überprüft, wo jeder Handler arbeitet — im Tester oder nicht. Jede Funktion aller Kollektionen musste solche Überprüfungen durchführen, was unzumutbar war.
Nun prüfen wir zunächst, wo die Arbeit getan wird — nicht im Tester oder im Tester. Die Bearbeitung aller Kollektionen erfolgt dann innerhalb der Blöcke (nicht im Tester und im Tester):

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

Die Methode ist kompakter geworden und verfügt über eine verständlichere Logik. Außerdem wird die nun von unnötigen Wiederholungsprüfungen entlastet.

Die Methode synchronisiert leere Zeitreihendaten mit dem Server und erzeugt die leere Zeitreihe neu:

//+------------------------------------------------------------------+
//| Synchronize timeseries data with the server                      |
//+------------------------------------------------------------------+
void CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If the timeseries data is not calculated, try re-creating the timeseries
//--- Get the pointer to the empty timeseries
   CSeries *series=this.SeriesGetSeriesEmpty();
   if(series!=NULL)
     {
      //--- Display the empty timeseries data as a chart comment and try synchronizing the timeseries with the server data
      ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC));
      ::ChartRedraw(::ChartID());
      //--- if the data has been synchronized
      if(series.SyncData(0,data_calculate.rates_total))
        {
         //--- if managed to re-create the timeseries
         if(this.m_time_series.ReCreateSeries(series.Symbol(),series.Timeframe(),data_calculate.rates_total))
           {
            //--- display the chart comment and the journal entry with the re-created timeseries data
            ::Comment(series.Header(),": OK");
            ::ChartRedraw(::ChartID());
            Print(series.Header()," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK),":");
            series.PrintShort();
           }
        }
     }
//--- Delete all comments
   else
     {
      ::Comment("");
      ::ChartRedraw(::ChartID());
     }
  }
//+------------------------------------------------------------------+

Die Methode ist ein Eckpfeiler für das korrekte Laden der historischen Daten jeder verwendeten Zeitreihe — aller Symbole und aller Perioden der Charts.

Die Methode erhält die ersten ungefüllten Zeitreihen aus der Zeitreihensammlung, d.h. sie hatte zuvor keine Daten, die einen Tick zuvor angekreuzt waren. Der Versuch, die Zeitreihendaten mit den Serverdaten zu synchronisieren, wird sofort ausgeführt. Wenn der Versuch fehlschlägt, beenden Sie die Methode bis zum nächsten Tick. Wenn die Daten synchronisiert worden sind, wird die Zeitreihe neu erstellt — gefüllt mit allen verfügbaren (aber nicht mehr als die angeforderte Menge) Balken aus der Historie.

Der Prozess wird bei jedem Tick ausgeführt — wir erhalten die nächste leere Zeitreihe, synchronisieren sie und erstellen sie neu, bis keine leere Zeitreihe mehr übrig bleibt.

Implementierung der Ereignisbehandlung durch NewTick und Calculate:

//+------------------------------------------------------------------+
//| NewTick event handler                                            |
//+------------------------------------------------------------------+
void CEngine::OnTick(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not a EA, exit
   if(this.m_program!=PROGRAM_EXPERT)
      return;
//--- Re-create empty timeseries
   this.SeriesSync(data_calculate,required);
//--- end
  }
//+------------------------------------------------------------------+
//| Calculate event handler                                          |
//+------------------------------------------------------------------+
int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not an indicator, exit
   if(this.m_program!=PROGRAM_INDICATOR)
      return data_calculate.rates_total;
//--- Re-create empty timeseries
   this.SeriesSync(data_calculate,required);
//--- return rates_total
   return data_calculate.rates_total;
  }
//+------------------------------------------------------------------+

Die Methode für das Neuerstellen leerer Zeitreihen wird in beiden Methoden aufgerufen.
Die Methoden selbst sind von gleichnamigen Programm-Funktionen auf Basis der Bibliothek aufzurufen.

Implementation der Methoden zum Erstellen aller angewandten Zeitreihen aller 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);
        }
     }
//--- Return the result of creating all timeseries for all symbols
   return res;
  }
//+------------------------------------------------------------------+

Die Methode ist bei der Programminitialisierung nach Erstellung der Liste aller verwendeten Symbole aufzurufen.
Die Methode empfängt das bei der Initialisierung erstellte Array. Das Array enthält die Namen der verwendeten Chartperioden und Parameter für die Erstellung von Zeitreihen — die Anzahl der aktuellen Zeitreihenbalken (nur für Indikatoren — rates_total) und die notwendige Historientiefe für die erstellten Zeitreihen (der Standard ist 1000, aber nicht mehr als der Wert des Symbols Bars() und nicht mehr als rates_total für Indikatoren).

Dies sind derzeit alle notwendigen Verbesserungen für die Arbeit mit Zeitreihen.


Testzeitreihen und ihre Ereignisse in Indikatoren

Um die Arbeit der Zeitreihensammlungsklasse in Indikatoren zu testen, erstellen wir einen neuen Ordner im Terminal-Indikator-Verzeichnis \MQL5\Indikatoren\TestDoEasy\. Legen wir dort einen neuen Unterordner Part39\ mit einem neuen Indikator TestDoEasyPart39.mq5 darin an.

Die Anzahl und Art der gezeichneten Indikatorpuffer spielt für uns bisher keine Rolle, da wir darin nichts zeichnen werden. Ich habe jedoch zwei gezeichnete Puffer des Zeichnungstyps DRAW_LINE zur zukünftigen Verwendung eingerichtet.

Die notwendigen Indikatoreingaben für die Festlegung der notwendigen Symbole und Zeitrahmen sowie einige andere Eingaben wurden aus dem im vorherigen Artikel beschriebenen Test EA entnommen. So sieht es nun aus:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart39.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- enums
//--- defines
//--- structures
//--- properties
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Label2
#property indicator_label2  "Label2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGreen
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- indicator buffers
double         Buffer1[];
double         Buffer2[];
//--- input variables
sinput   ENUM_SYMBOLS_MODE InpModeUsedSymbols   =  SYMBOLS_MODE_CURRENT;            // Mode of used symbols list
sinput   string            InpUsedSymbols       =  "EURUSD,AUDUSD,EURAUD,EURCAD,EURGBP,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";  // List of used symbols (comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs    =  TIMEFRAMES_MODE_LIST;            // Mode of used timeframes list
sinput   string            InpUsedTFs           =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator)
sinput   bool              InpUseSounds         =  true; // Use sounds
//--- global variables
CEngine        engine;                          // CEngine library main object
string         prefix;                          // Prefix of graphical object names
bool           testing;                         // Flag of working in the tester
int            used_symbols_mode;               // Mode of working with symbols
string         array_used_symbols[];            // Array of used symbols
string         array_used_periods[];            // Array of used timeframes
//+------------------------------------------------------------------+

Implementieren Sie in OnInit() des Indikators das Setzen von globalen Variablen des Indikators und den Aufruf der Bibliotheksinitialisierungsfunktion:

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

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

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

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

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

OnDeinit() des Indikators stammt aus dem im vorigen Artikel beschriebenen Test EA:

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

Ebenso wie OnTimer() und OnChartEvent() aus dem EA:

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

Erstellen wir zwei Funktionen zum Ausfüllen von der Struktur von Array und variablen Daten aus den ersten und zweiten Version von OnCalculate() des Indikators:

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

Verschieben wir der Funktion zur Behandlung von DoEasy-Bibliotheksereignissen aus dem Test-EA:

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

Die Funktion, die mit den Bibliotheksereignissen im Tester des EAs arbeitet:

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

Wir müssen die EA-Funktionen für die Arbeit mit den Schaltflächen des Handelspanels nicht verlagern. Das machen wir aber trotzdem mit einigen geringfügigen Änderungen, um die Schaltflächen im Indikator verwenden zu können (es sollen zwei Knöpfe implementiert werden):

//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
//--- Button 1
   if(name=="BUTT_1")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
//--- Button 2
   if(name=="BUTT_2")
     {
      if(state)
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'255,220,90');
      else
         ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240');
     }
  }
//+------------------------------------------------------------------+
//| Track the buttons' status                                        |
//+------------------------------------------------------------------+
void PressButtonsControl(void)
  {
   int total=ObjectsTotal(0,0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- If the button is pressed
   if(ButtonState(button_name))
     {
      //--- If button 1 is pressed
      if(button=="BUTT_1")
        {

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

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

Wie wir sehen können, können die meisten EA-Funktionen in Indikatoren verwendet werden, ohne dass Anpassungen erforderlich sind. Dies legt nahe, dass alle notwendigen Funktionen für die Arbeit mit der Bibliothek von EA und Indikatoren in die Bibliotheks-Include-Datei verschoben werden sollten. Dies wird jedoch zu einem späteren Zeitpunkt erfolgen. Derzeit müssen wir OnCalculate() des Indikators erstellen.

Der Handler soll aus dem wesentlichen Code-Block für die Vorbereitung der Bibliotheksdaten und dem optionalen (vorerst) Code-Block für die Arbeit mit dem Indikator bestehen:

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

//--- Handle the Calculate event in the library
   engine.OnCalculate(rates_data);

//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER)) 
     {
      engine.OnTimer(rates_data);   // Working in the timer
      PressButtonsControl();        // Button pressing control
      EventsHandling();             // Working with events
     }

//+------------------------------------------------------------------+
//| OnCalculate code block for working with the indicator:           |
//+------------------------------------------------------------------+
//--- Arrange resource-saving indicator calculations
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

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

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

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

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

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

Wie wir sehen können, passt alles, was mit der Bibliotheksoperation zusammenhängt, in einen kleinen Code-Block in OnCalculate(). Tatsächlich besteht der Unterschied zu einem EA darin, dass wir die Preisstruktur der aktuellen Array-Daten aus OnCalculate() mit der Funktion CopyData() ausfüllen, während alles andere absolut identisch mit der Arbeit in einem EA ist — die Bibliothek arbeitet im Timer, wenn der Indikator auf einem Symbolchart gestartet wird, und in OnCalculate() durch Ticks, wenn der Indikator im Tester gestartet wird.
Füllen Sie die Indikatorpuffer im Berechnungsteil OnCalculate() mit High[] und Low[] Array-Daten.

Der vollständige Indikatorcode kann in den unten angehängten Dateien eingesehen werden.

Kompilieren Sie den Indikator und starten Sie ihn auf dem Symbolchart, mit dem wir lange Zeit nicht gearbeitet haben (während Sie zuvor in den Einstellungen die Arbeit mit dem aktuellen Symbol eingestellt haben) und wählen Sie die Arbeit mit der angegebenen Zeitrahmenliste. Durch das Starten des Indikators auf lange Zeit unbenutzte Symbole wird der Indikator zum Herunterladen fehlender Daten und zur Information darüber im Journal und auf dem Chart:


Hier können wir sehen, dass jede nächste leere Zeitreihe bei jedem neuen Tick synchronisiert und erstellt wurde. Die folgenden Einträge wurden im Journal angezeigt:

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

Hier können wir sehen, dass alle angeforderten Zeitreihen bei der Initialisierung der Bibliothek erstellt wurden. Aufgrund des Fehlens dieser Daten wurden sie jedoch nicht mit Daten gefüllt. Beim ersten Zugriff auf die angeforderten Daten wurde der Daten-Download durch das Terminal initiiert. Bei jedem folgenden Tick haben wir ein weiteres leeres Zeitreihenobjekt erhalten, seine Daten mit dem Server synchronisiert und das Zeitreihenobjekt mit Balken-Daten in der angeforderten Menge gefüllt. Nur 323 Balken sind tatsächlich für MN1 verfügbar. Alle wurden der Zeitreihenliste hinzugefügt.

Lassen Sie uns nun den Indikator im visuellen Tester-Modus mit den gleichen Einstellungen starten:


Der Tester lädt die gesamte notwendige Historie für alle verwendeten Zeitrahmen, die Bibliothek informiert über die Erstellung aller Zeitreihen außer der aktuellen. Die Zeitreihe für das aktuelle Symbol und die aktuelle Periode wird beim ersten Eintrag in OnCalculate() erfolgreich neu erstellt. Nach dem Aufheben der Pause des Testers können wir sehen, wie die Ereignisse "New bar" (Neuer Balken) der verwendeten Zeitreihen im Tester ausgelöst werden.

Alles funktioniert wie erwartet.

Was kommt als Nächstes?

Im nächsten Artikel werden wir unsere Arbeit mit Indikatorzeitreihen fortsetzen und testen, wie die erstellten Zeitreihen für die Anzeige von Informationen in einem Chart verwendet werden können.

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.
Schreiben Sie Ihre Fragen und Vorschläge in den Kommentaren.

Zurück zum Inhalt

Frühere Artikel dieser Serie:

Zeitreihen in der Bibliothek DoEasy (Teil 35): das Bar-Objekt 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