Wie man den Berechnungsblock eines Indikators in den Code eines Expert Advisors überträgt

18 Juli 2018, 12:35
Dmitriy Gizlyk
0
185

Inhalt

Einleitung

Wenn ein Programmierer einen Expert Advisor erstellt, der Signale von Indikatoren empfängt, wird er jedes Mal mit der Frage konfrontiert, ob einen Aufruf des Indikators zu verwenden oder den Code des Indikators in den Expert Advisor zu übertragen. Dafür kann es unterschiedliche Gründe geben: der Wunsch, verwendete Indikatoren und die Strategie als Ganze geheim zu halten; die Notwendigkeit, den Expert Advisor als eine Datei zu verteilen; der Wunsch, die Anzahl der Operationen zu reduzieren, wenn nicht alle Signale/Indikatorpuffer verwendet werden usw. Natürlich bin ich nicht der Erste und nicht der Letzte, der diese Frage stellt. Nikolay Kositsin beschäftigte sich bereits mit diesem Thema in Bezug auf MetaTrader 4. Schauen wir, wie man das auf der Plattform MetaTrader 5 lösen kann.

1. Grundsätze der Übertragung des Codes

Bevor wir anfangen, klären wir die Unterschiede zwischen Indikatoren und Expert Advisors. Schauen wir uns die leere Vorlage eines Indikators an.

//+------------------------------------------------------------------+
//|                                                        Blanc.mq5 |
//|                                             Copyright 2018, DNG® |
//|                                 http://www.mql5.com/ru/users/dng |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, DNG®"
#property link      "http://www.mql5.com/ru/users/dng"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Buffer
#property indicator_label1  "Buffer"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double         BufferBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferBuffer,INDICATOR_DATA);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 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[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Am Anfang des Codes des Indikators werden Puffer-Arrays für den Datenaustausch mit anderen Programmen deklariert. Diese Arrays stellen Zeitreihen dar, ihre Elemente sind mit Preisbalken verbunden. Diese Verbindung wird durch das Terminal unterstützt. Der Indikator speichert die Ergebnisse der Berechnungen in diesen Arrays, ohne sich um die Änderung deren Größe und die Übertragung von Daten bei der Erscheinung einer neuen Kerze zu kümmern. In einem Expert Advisor gibt es solche Arrays nicht, d.h. sie müssen bei der Übertragung des Codes des Indikators in den Expert Advisor erstellt werden. Neben dem Berechnungsblock muss noch eine Verbindung zwischen den Elementen des Arrays und den Balken auf dem Chart des Finanzinstruments erstellt werden. Im Expert Advisor gibt es aber die Möglichkeit, Berechnungen nicht anhand der ganzen vorhandenen Historie durchzuführen (was in einem Indikator der Fall ist), sondern nur die verwendeten Daten neu zu berechnen.

Im Expert Advisor müssen also Indikatorpuffer erstellt werden. Dabei muss man daran denken, dass der Indikator nicht nur Puffer für die Ausgabe der Informationen auf den Chart, sondern auch Hilfspuffer für Zwischenberechnungen haben kann. Sie müssen auch erstellt werden. Puffer für die Farbe des Zeichnens können vernachlässigt werden, wenn die Änderung der Farbe von Indikatorlinien in der Strategie des Expert Advisors nicht vorgesehen ist.

Ein weiterer Unterschied eines Indikators von einem Expert Advisor hinsichtlich der Architektur besteht in der Funktion der Verarbeitung von Ticks. Im Gegensatz zu MetaTrader 4 sind in MetaTrader 5 die Handlers eingehender Ticks für Indikatoren und Expert Advisors getrennt. In einem Indikator wird beim Eintreffen eines neuen Ticks die Funktion OnCalculate aufgerufen. In den Parametern erhält sie die Gesamtzahl der Balken auf dem Chart, die Anzahl der Balken beim vorherigen Aufruf und die Zeitreihen, die für die Berechnung des Indikators benötigt werden. In einem Expert Advisor werden neue Ticks in der Funktion OnTick verarbeitet, die keine Parameter hat. Deshalb müssen wir den Zugriff auf Zeitreihen selbst programmieren und das Verfolgen der Änderungen auf dem Chart sichern.

2. Erstellen einer Klasse für die Berechnung des Indikators

In den Strategien von Expert Advisors wird häufig ein Indikator mit unterschiedlichen Parametern verwendet, deshalb macht es meiner Meinung Sinn, die Möglichkeiten der objektorientierten Programmierung zu nutzen und unseren Indikator in der Klasse CIndicator zu platzieren.

Fassen wir kurz zusammen. Für die Übertragung des Berechnungsblocks eines Indikators in einen Expert Advisor müssen wir Folgendes tun:

  1. Die Arbeit der Indikatorpuffer organisieren. Dafür erstellen wir die Klasse CArrayBuffer, und in dieser Klasse — Methoden für das Speichern von Daten und für den Zugriff auf sie. Später erstellen wir einen Array für solche Klassen basierend auf der Anzahl der Puffer im Indikator.
  2. Den Berechnungsblock des Indikators übertragen wir aus der Funktion OnCalculate in die Funktion Calculate unserer Klasse.
  3. Den Zugriff auf Zeitreihen erhält der Indikator aus den Parametern der Funktion OnCalculate, was die Funktionen des Expert Advisors nicht haben. Deshalb platzieren wir das Herunterladen der benötigten Zeitreihen in der Funktion LoadHistory.
  4. Um den Zugriff auf die neu berechneten Daten des Indikators zu vereinheitlichen, erstellen wir die Funktion CopyBuffer mit den benötigten Parametern in der Klasse CIndicator. 

Die bevorstehende Arbeit kann wie folgt zusammengefasst werden.


Wenn ich weiter vom Indikator spreche, meine ich damit die Instanz des Indikators, die im Code des Expert Advisors erstellt wurde.

2.1. Erstellen des Indikatorpuffers

Für die Erstellung der Indikatorpuffer nutzen wir die Klasse CArrayDouble. Auf ihrer Basis erstellen wir die neue Klasse CArrayBuffer.

class CArrayBuffer   :  public CArrayDouble
  {
public:
                     CArrayBuffer(void);
                    ~CArrayBuffer(void);
//---
   int               CopyBuffer(const int start, const int count, double &double_array[]);
   int               Initilize(void);
   virtual bool      Shift(const int shift);
  };

Erstellen wir die Methode CopyBuffer, damit das Erhalten der Daten von der Form her ähnlich dem Standardaufruf des Indikators ist. Des Weiteren fügen wir zwei Methoden hinzu: Initilize — für das Säubern der Pufferdaten und Shift — für die Verschiebung der Daten im Puffer bei der Erscheinung einer neuen Kerze. Der Code der Funktionen kann in den beigefügten Dateien studiert werden.

2.2. Basisklasse für zukünftige Indikatoren

Als nächstes erstellen wir einen Prototyp des Indikators in der Basisklasse CIndicator.

class CIndicator
  {
private:
//---
   datetime             m_last_load;
public:
                        CIndicator(void);
                       ~CIndicator(void);
   virtual bool         Create(const string symbol=NULL, const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
//--- Set indicator's main settings
   virtual bool         SetBufferSize(const int bars);
//--- Get indicator's data
   virtual int          CopyBuffer(const uint buffer_num,const uint start, const uint count, double &double_array[]);
   virtual double       GetData(const uint buffer_num,const uint shift);

protected:
   double               m_source_data[];
   CArrayBuffer         ar_IndBuffers[];
   int                  m_buffers;
   int                  m_history_len;
   int                  m_data_len;
//---
   string               m_Symbol;
   ENUM_TIMEFRAMES      m_Timeframe;
   ENUM_APPLIED_PRICE   m_Price;      
//--- Set indicator's main settings
   virtual bool         SetHistoryLen(const int bars=-1);
//---
   virtual bool         LoadHistory(void);
   virtual bool         Calculate()                         {  return true;   }
  };

Diese Klasse beinhaltet 6 öffentliche Methoden:

  • Konstruktor,
  • Destruktor,
  • Methode der Initialisierung der Klasse,
  • Methode für die Angabe der Größe des Indikatorpuffers
  • zwei Methoden für den Zugriff auf die Daten des Indikators: eine — für das Herunterladen aller Daten auf einmal (Paketdownload), und die zweite — für den Zugriff auf ein konkretes Element.

Die meisten Mitglieder der Klasse deklarieren wir als protected. Hier deklarieren wir:

  • Array der Ausgangsdaten für die Berechnung (m_source_data);
  • Array der Indikatorpuffer (ar_IndBuffers);
  • Variablen für das Speichern der Anzahl der Indikatorpuffer (m_buffers), der Tiefe der benötigten Historie der Ausgangsdaten (m_history_len) und der Tiefe der benötigten Historie der Indikatorwerten (m_data_len);
  • das verwendete Symbol (m_Symbol) und den Zeitrahmen (m_Timeframe);
  • den Preistyp für die Berechnung des Indikators (m_Price);
  • Methoden zum Setzen der Tiefe der Ausgangsdaten (SetHistoryLen); zum Herunterladen der historischen Daten der Zeitreihen (LoadHistory); zur Berechnung des Indikators (Calculate). 

Alle Methoden werden als virtuell erstellt, damit man sie später an einen konkreten Indikator anpassen kann. Im Konstruktor der Klasse initialisieren wir die Variablen und löschen die Arrays.

CIndicator::CIndicator()   :  m_buffers(0),
                              m_Symbol(_Symbol),
                              m_Timeframe(PERIOD_CURRENT),
                              m_Price(PRICE_CLOSE),
                              m_last_load(0)
  {
   m_data_len=m_history_len  =  Bars(m_Symbol,m_Timeframe)-1;
   ArrayFree(ar_IndBuffers);
   ArrayFree(m_source_data);
  }

In der Funktion für die Initialisierung der Klasse prüfen wir zuerst, ob das angegebene Symbol verwendet werden kann. Dafür prüfen wir, ob es in der Marktübersicht vorhanden ist, wenn nicht, versuchen wir es auszuwählen. Wenn das Symbol nicht verwendet werden kann, gibt die Funktion false zurück. Wenn es verwendet werden kann, speichern wir das Symbol, den Zeitrahmen und den Preis in den entsprechenden Variablen.

bool CIndicator::Create(const string symbol=NULL,const ENUM_TIMEFRAMES timeframe=0,const ENUM_APPLIED_PRICE price=1)
  {
   m_Symbol=(symbol==NULL ? _Symbol : symbol);
   if(!SymbolInfoInteger(m_Symbol,SYMBOL_SELECT))
      if(!SymbolSelect(m_Symbol,true))
         return false;
//---
   m_Timeframe=timeframe;
   m_Price=price;
//---
   return true;
  }

Die Methode, die die Größe des Indikatorpuffers setzt, hat nur einen Parameter — die Größe. Wenn wir die ganze verfügbare Historie verwenden wollen, reicht es uns, der Funktion eine Zahl zu übergeben, die kleiner als oder gleich "0" ist. In der Funktion speichern wir den Wert des übergebenen Parameters in der entsprechenden Variablen. Danach prüfen wir, ob es ausreichend historische Daten für Zeitreihen gibt, um die angegebene Historie des Indikators zu erhalten. Wenn die Ausgangsdaten nicht ausreichend sind, steigt die Größe der herunterzuladenden Daten. Am Ende der Funktion löschen wir alle Indikatorpuffer und ändern ihre Größe.

bool CIndicator::SetBufferSize(const int bars)
  {
   if(bars>0)
      m_data_len  =  bars;
   else
      m_data_len  =  Bars(m_Symbol,m_Timeframe);
//---
   if(m_data_len<=0)
     {
      for(int i=0;i<m_buffers;i++)
         ar_IndBuffers[i].Shutdown();
      return false;
     }
//---
   if(m_history_len<m_data_len)
      if(!SetHistoryLen(m_data_len))
         return false;
//---
   for(int i=0;i<m_buffers;i++)
     {
      ar_IndBuffers[i].Shutdown();
      if(!ar_IndBuffers[i].Resize(m_data_len))
         return false;
     }
//---
   return true;
  }

Für die Abfrage der historischen Daten der Zeitreihen wird die Funktion LoadHistory verwendet. Sie hat keine Parameter, Ausgansgdaten erhält sie aus den Daten, die in den vorherigen Funktionen gespeichert wurden.

Häufig ändert sich der aktuelle Wert des Indikators, während sich eine Kerze bildet, und dies kann zur Entstehung falscher Signale führen. Aus diesem Grund verwenden viele Strategien basierend auf Indikatoren die Daten der bereits geschlossenen Kerzen. Wenn wir davon ausgehen, müssen historische Daten nur einmal beim Bilden einer neuen Kerze heruntergeladen werden. Deshalb prüfen wir am Anfang der Funktion, ob ein neuer Balken entsteht. Wenn kein neuer Balken entsteht, und die Daten bereits heruntergeladen wurden, verlassen wir die Funktion. Wenn die Daten heruntergeladen werden müssen, wechseln wir zum nächsten Block der Funktion. Wenn für die Berechnung des Indikators eine Zeitreihe ausreichend ist, laden wir die notwendigen Daten in unseren Array für Ausgangsdaten herunter. Wenn der Indikator den Medianpreis, typischen oder gewichteten Durchschnittspreis verwendet, laden wir historische Daten zuerst ins Array der Strukturen MqlRates herunter, und danach berechnen wir den benötigten Preis in der Schleife. Die Berechnungsergebnisse werden im Array der Ausgangsdaten gespeichert.

bool CIndicator::LoadHistory(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
   if(m_last_load>=cur_date && ArraySize(m_source_data)>=m_history_len)
      return true;
//---
   MqlRates rates[];
   int total=0,i;
   switch(m_Price)
     {
      case PRICE_CLOSE:
        total=CopyClose(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
        break;
      case PRICE_OPEN:
        total=CopyOpen(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_HIGH:
        total=CopyHigh(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_LOW:
        total=CopyLow(m_Symbol,m_Timeframe,1,m_history_len,m_source_data);
      case PRICE_MEDIAN:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low)/2;
        break;
      case PRICE_TYPICAL:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low+rates[i].close)/3;
        break;
      case PRICE_WEIGHTED:
        total=CopyRates(m_Symbol,m_Timeframe,1,m_history_len,rates);
        if(total!=ArraySize(m_source_data))
           total=ArrayResize(m_source_data,total);
        for(i=0;i<total;i++)
           m_source_data[i]=(rates[i].high+rates[i].low+2*rates[i].close)/4;
        break;
     }
//---
   if(total<=0)
      return false;
//---
   m_last_load=cur_date;
   return (total>0);
  }

Wenn während der Ausführung der Funktion keine Daten heruntergeladen wurden, gibt die Funktion false zurück.

Die Methode für die Abfrage der Daten des Indikators machen wir gleich einem Standardzugriff auf die Indikatorpuffer. Dafür erstellen wir die Funktion CopyBuffer, in deren Parametern wir die Nummer des Puffers, die Position, ab welcher die Daten kopiert werden, die Anzahl der benötigten Elemente und den Array für die Abfrage der Daten übergeben werden. Nach der Ausführung gibt die Funktion die Anzahl der kopierten Elemente zurück.

int CIndicator::CopyBuffer(const uint buffer_num,const uint start,const uint count,double &double_array[])
  {
   if(!Calculate())
      return -1;
//---
   if((int)buffer_num>=m_buffers)
     {
      ArrayFree(double_array);
      return -1;
     }
//---
   return ar_IndBuffers[buffer_num].CopyBuffer(start,count,double_array);
  }

Damit der Nutzer immer aktuelle Daten erhält, rufen wir am Anfang der Funktion die Funktion der Neuberechnung des Indikators auf (in dieser Klasse deklarieren wir nur die virtuelle Funktion, und die Berechnung selbst führen wir in der finalen Klasse des Indikators). Nach der Neuberechnung der Indikatorwerte prüfen wir, ob er den angegebenen Puffer beinhaltet. Wenn die Nummer des Puffers falsch angegeben wurde, säubern wir die Daten im Empfänger-Array und verlassen die Funktion mit dem Ergebnis "-1". Bei einer erfolgreichen Prüfung der Puffernummer rufen wir die Methode CopyBuffer des entsprechenden Pufferarrays auf.

Gleich ist die Funktion des Zugriffs auf konkrete Daten konzipiert.

Den vollständigen Code der Klasse und aller ihrer Funktionen entnehmen Sie bitte dem Anhang.

2.3. Indikatorklasse des gleitenden Durchschnitts

Für die Demonstration der Technologie habe ich den Indikator des gleitenden Durchschnitts (MA) ausgewählt. Das ist kein Zufall. Dieser Indikator der technischen Analyse wird von Tradern sowhol in der klassischen Variante als auch für das Zeichnen anderer Indikatoren verwendet. Dazu gehören MACD, Alligator und viele mehr. Darüber hinaus gibt es im Standardpaket ein Beispiel für den MA-Indikator, von dem wir die Daten über die Funktion iCustom erhalten können, um die Geschwindigkeit des Zugriffs auf den Indikator mit der Geschwindigkeit der Berechnung von Daten im Expert Advisor zu vergleichen.

Den MA berechnen wir in der Klasse CMA. Unsere Klasse erhält vier öffentliche Methoden: Konstruktor, Destruktor, Methode der Initialisierung (Create) und eine Methode für das Setzen der Tiefe historischer Daten des Indikators (die wir umschreiben). Die Methoden für den Zugriff auf Indikatordaten leitet unsere Klasse von der Basisklasse ab.

class CMA : public CIndicator
  {
private:
   int               m_Period;
   int               m_Shift;
   ENUM_MA_METHOD    m_Method;
   datetime          m_last_calculate;
   
public:
                     CMA();
                    ~CMA();
   bool              Create(const string symbol, const ENUM_TIMEFRAMES timeframe, const int ma_period, const int ma_shift, const ENUM_MA_METHOD ma_method, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   virtual bool      SetBufferSize(const int bars);
   
protected:
   virtual bool      Calculate();
   virtual double    CalculateSMA(const int shift);
   virtual double    CalculateEMA(const int shift);
   virtual double    CalculateLWMA(const int shift);
   virtual double    CalculateSMMA(const int shift);
  };

Aus dem Header der Klasse ist es ersichtlich, dass die Elemente für die Berechnung des Indikators in dieser Phase entstehen. Das sind private Variablen für das Speichern des Zeitraums, der Verschiebung und der Berechnungsmethode des Indikators. Im Block protected schreiben wir die virtuelle Funktion für die Berechnung des Indikators Calculate um. Je nach der angegebenen Methode für die Berechnung des Indikators wird sie die Funktion CalculateSMA, CalculateEMA, CalculateLWMA oder CalculateSMMA aufrufen.

Im Konstruktor der Klasse initialisieren wir die Variablen, geben die Anzahl der Puffer des Indikators an und erstellen einen Indikatorpuffer.

CMA::CMA()  :  m_Period(25),
               m_Shift(0),
               m_Method(MODE_SMA)
  {
   m_buffers=1;
   ArrayResize(ar_IndBuffers,1);
  }

In den Parametern der Funktion für die Initialisierung der Klasse geben wir die erforderlichen Werte an: Symbol, Zeitrahmen und Parameter für die Berechnung des Indikators. In der Funktion selbst rufen wir die Funktion der Initialisierung der Basisklasse auf. Danach prüfen wir, ob der angegebene Mittelungszeitraum gültig ist (er muss positiv sein). Danach speichern wir die Parameter des Indikators in den entsprechenden Variablen der Klasse und setzen die Tiefe der Historie für den Indikatorpuffer und die herunterzuladenden Daten der Zeitreihen. Wenn ein Fehler auftritt, gibt die Funktion false zurück. Nach einer erfolgreichen Initialisierung gibt die Funktion true zurück.

bool CMA::Create(const string symbol,const ENUM_TIMEFRAMES timeframe,const int ma_period,const int ma_shift,const ENUM_MA_METHOD ma_method,const ENUM_APPLIED_PRICE price=1)
  {
   if(!CIndicator::Create(symbol,timeframe,price))
      return false;
//---
   if(ma_period<=0)
      return false;
//---
   m_Period=ma_period;
   m_Shift=ma_shift;
   m_Method=ma_method;
//---
   if(!SetBufferSize(ma_period))
      return false;
   if(!SetHistoryLen(2*ma_period+(m_Shift>0 ? m_Shift : 0)))
      return false;
//---
   return true;
  }

Die Funktion Calculate wird den Indikator berechnen. Bei der Erstellung der Elternklasse haben wir festgelegt, dass wir historische Daten der Zeitreihen beim Erscheinen einer neuen Kerze herunterladen. Die Daten des Indikators werden mit der gleichen Periodizität neu berechnet. Dafür prüfen wir am Anfang der Funktion, ob eine neue Kerze erscheint. Wenn der aktuelle Balken bereits berechnet wurde, verlassen wir die Funktion mit dem Ergebnis true.

Weiter, wenn ein neuer Balken vorhanden ist, rufen wir die Funktion für das Herunterladen der Daten der Zeitreihen auf. Wenn die historischen Daten erfolgreich heruntergeladen wurden, prüfen wir die Anzahl der Kerzen, die sich nach der letzten Berechnung des Indikators gebildet haben. Wenn die Anzahl der neuen Kerzen größer als der Indikatorpuffer ist, initialisieren wir ihn erneut. Wenn die neuen Kerzen weniger sind, verschieben wir die Daten im Puffer um die Anzahl der entstandenen Balken. Danach berechnen wir nur die neuen Elemente neu.

Nun organisieren wir eine Schleife für die Berechnung der neuen Elemente des Indikatorpuffers. Bitte beachten: wenn ein berechnetes Element die Größe des aktuellen Indikatorpuffers übersteigt (das ist beim ersten Start der Berechnung oder bei der Berechnung nach einer Verbindungsunterbrechung möglich, wenn die Anzahl der neuen Kerzen die Größe des Puffers übersteigt), werden die Daten mithilfe der Methode Add zum Puffer hinzugefügt. Wenn das neu berechnete Element in die Größe des existierenden Puffers passt, wird der Wert des Elements durch die Methode Update aktualisiert. Die Werte des Indikators werden in Funktionen berechnet, die der Mittelungsmethode entsprechen. Die Logik der Berechnung wurde dem Indikator Custom Moving Average.mq5 aus dem Standardpaket MetaTrader 5 entnommen.

Nach einer erfolgreichen Neuberechnung des Indikatorpuffers speichern wir die Zeit der letzten Berechnung und verlassen die Funktion mit dem Ergebnis true.

bool CMA::Calculate(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
   if(m_last_calculate==cur_date && ArraySize(m_source_data)==m_history_len)
      return true;
//---
   if(!LoadHistory())
      return false;
//---
   int shift=Bars(m_Symbol,m_Timeframe,m_last_calculate,cur_date)-1;
   if(shift>m_data_len)
     {
      ar_IndBuffers[0].Initilize();
      shift=m_data_len;
     }
   else
      ar_IndBuffers[0].Shift(shift);
//---
   for(int i=(m_data_len-shift);i<m_data_len;i++)
     {
      int data_total=ar_IndBuffers[0].Total();
      switch(m_Method)
        {
         case MODE_SMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateSMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateSMA(i+m_Shift));
           break;
         case MODE_EMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateEMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateEMA(i+m_Shift));
           break;
         case MODE_SMMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateSMMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateSMMA(i+m_Shift));
           break;
         case MODE_LWMA:
           if(i>=data_total)
              ar_IndBuffers[0].Add(CalculateLWMA(i+m_Shift));
           else
              ar_IndBuffers[0].Update(i,CalculateLWMA(i+m_Shift));
           break;
        }
     }
//---
   m_last_calculate=cur_date;
   m_data_len=ar_IndBuffers[0].Total();
//---
   return true;
  }

Darüber hinaus schreiben wir die virtuelle Funktion der Elternklasse in dieser Klasse um, die die benötigte Größe des Indikatorpuffers setzt. Das wird benötigt, um die Tiefe des Indikatorpuffers und die Tiefe der historischen Daten der Zeitreihen zu prüfen. In der Elternklasse haben wir angegeben, dass die Anzahl der Elemente in der Zeitreihe nicht kleiner als die Anzahl der Elemente im Indikatorpuffer sein darf. Und um МА zu berechnen, muss die Anzahl der Elemente in der Zeitreihe mindestens um den Mittelungszeitraum größer als die Größe des Indikatorpuffers sein.

3. Beispiel für das Hinzufügen einer Indikatorklasse zu einem Expert Advisor

Als ich diesen Artikel plante, war eines meiner Ziele die Geschwindigkeit der Datenverarbeitung in einem Expert Advisor und der Abfrage der Daten von einem Indikator zu vergleichen. Um die Arbeit der Klasse zu demonstrieren, habe ich mich entschieden, einen vollständigen Handelsroboter nicht zu erstellen. Ich präsentiere den Entwurf eines Expert Advisors, in dem Sie Ihre eigene Logik der Verarbeitung von Signalen des Indikators schreiben können.

Erstellen wir eine neue Datei des Expert Advisors Test_Class.mq5. Seine Inputparameter sind gleich den Parametern des verwendeten Indikators.

input int                  MA_Period   =  25;
input int                  MA_Shift    =  0;
input ENUM_MA_METHOD       MA_Method   =  MODE_SMA;
input ENUM_APPLIED_PRICE   MA_Price    =  PRICE_CLOSE;

Deklarieren wir eine Instanz unserer Indikatorklasse und den Array für das Erhalten der Indikatordaten als global.

CMA   *MA;
double c_data[];

In der Funktion OnInit müssen wir die Instanz der Indikatorklasse initialisieren und ihr die Ausgangsdaten übergeben.

int OnInit()
  {
//---
   MA=new CMA;
   if(CheckPointer(MA)==POINTER_INVALID)
      return INIT_FAILED;
//---
   if(!MA.Create(_Symbol,PERIOD_CURRENT,MA_Period,MA_Shift,MA_Method,MA_Price))
      return INIT_FAILED;
   MA.SetBufferSize(3);
//---
   return(INIT_SUCCEEDED);
  }

Nach dem Ende der Arbeit des Expert Advisors muss man den Speicher säubern und die Instanz der Klasse löschen. Das tun wir in der Funktion OnDeinit.

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(MA)!=POINTER_INVALID)
      delete MA;
  }

Jetzt ist die Klasse bereit für die Arbeit. Es bleibt nur noch der Funktion OnTick das Erhalten von Indikatordaten hinzuzufügen. Am Anfang der Funktion prüfen wir die Erscheinung eines neuen Balkens, danach rufen wir die Methode CopyBuffer unserer Klasse auf. Danach folgt Ihr eigener Code für die Verarbeitung von Signalen und die Ausführung von Trades.

void OnTick()
  {
//---
   static datetime last_bar=0;
   datetime cur_date=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);
   if(last_bar==cur_date)
      return;
   last_bar=cur_date;
//---
   if(!MA.CopyBuffer(MAIN_LINE,0,3,c_data))
      return;

//---
//     Hier wird Ihr Code der Verarbeitung von Signalen und Handelsoperationen hinzugefügt
//---
   return;
  }

Den vollständigen Code aller Programme und Klassen können Sie im Anhang finden.

4. Der "Preis" der Verwendung des übertragenen Indikators

Und eine weitere wichtige Frage: wie wird sich die Übertragung des Codes des Indikators auf die Arbeit des Expert Advisros auswirken? Um die Frage zu beantworten, führen wir einige Experimente durch.

4.1. Experiment 1

Wie bereits erwähnt, habe ich mich absichtlich für den MA-Indikator entschieden. Jetzt können wir die Geschwindigkeit des Erhaltens von Daten auf drei Weisen prüfen:

  • über die Funktion eines ins Terminal integrierten Indikators (iMA);
  • durch den Aufruf eines ähnlichen benutzerdefinierten Indikators (iCustom);
  • durch die Berechnung im Expert Advisor.

Das Erste, was einem einfällt, ist die Funktion des Profilings im MetaEditor zu verwenden. Dafür erstellen wir einen nicht handelnden Expert Advisor, der die Daten aus allen drei Quellen gleichzeitig erhalten wird. Hier führe ich keine vollständige Beschreibung der Arbeit dieses Expert Advisors an, seinen Code finden Sie im Anhang. Alle drei Datenquellen wurden nur beim Öffnen einer neuen Kerze aufgerufen.

Das Profiling wurde im Strategietester durchgeführt, 15 Monate, Zeitrahmen М15. Als Ergebnis des Experiments habe ich folgende Daten erhalten.

Funktion Durchschnittliche Ausführungszeit, Mikrosekunden Anteil an der Gesamtzeit
OnTick
99.14%
Prüfung des Öffnens eines neuen Balkens 0.528 67,23%
Interne Berechnung
21.524 2.36%
      darunter CopyClose 1.729  0.19%
 iMA  2.231  0.24%
 iCustom  0.748  0.08%
 OnInit  241439  0.86%
 Abfrage des Handles iCustom  235676  0.84%

Das Erste, was einem auffällt, ist der große Zeitaufwand für die Abfrage des Handles des Indikators mithilfe der Funktion iCustom. Die Zeit übersteigt die Zeit für die Initialisierung der Indikatorklasse und die Abfrage des Handles des Indikators über die Funktion iMA um das Dutzendfache. Gleichzeitig erfolgt die Abfrage der Daten von einem Indikator, der durch die Funktion iCustom initialisiert wurde, um das Dreifache schneller, als vom Indikator iMA, und um das 30-fache schneller, als bei der Berechnung der Indikatorwerte in der Klasse.

Durchschnittliche Zeit der Ausführung der Operationen

Betrachten wir die Zeit der Ausführung verschiedener Funktionen unserer Indikatorklasse. Ich möchte Sie darauf hinweisen, dass die Zeit der Abfrage der historischen Daten durch die Funktion CopyClose mit der Zeit der Abfrage der Indikatordaten vergleichbar ist. Braucht denn der Indikator kaum Zeit für die Berechnung? In der Wirklichkeit sieht es ein bisschen anders aus. Die Architektur des MetaTrader 5 bietet einen asynchronen Zugriff auf die Werte der Indikatoren. Mit anderen Worten: bei der Abfrage des Handles des Indikators wird er zum Chart hinzugefügt. Weiter führt der Indikator seine Berechnungen außerhalb des Threads des Expert Advisors durch. Sie interagieren nur in der Phase des Datenaustausches, genauso wie beim Erhalten der Daten der Zeitreihen. Deswegen ist die Zeit für die Ausführung dieser Operationen vergleichbar.

Fassen wir das in diesem Experiment Gesagte zusammen: wir haben bewiesen, dass es falsch ist, die Profiling-Funktion des MetaEditor für die Einschätzung des Zeitaufwands für die Berechnung der in Expert Advisors verwendeten Indikatoren zu verwenden.

4.2. Experiment 2

Erstellen wir vier separate Expert Advisors.

  1. Einen leeren Referenz-Expert Advisor, der keine Funktionen hat. Er wird der Einschätzung des Zeitaufwandes dienen, den das Terminal für das Durchlaufen der Historie braucht.
  2. Einen Expert Advisor, der Daten durch Berechnungen in der Indikatorklasse erhält.
  3. Einen Expert Advisor, der Daten vom iMA Indikator erhält.
  4. Einen Expert Advisor, der Daten von einem benutzerdefinierten Indikator erhält.

Danach starten wir die Optimeirung auf 11 Durchläufen im Strategietester und vergleichen wir die durchschnittliche Zeit eines Durchlaufs.

Experiment 2Experiment 2

Die Testergebnisse haben eine Zeiteinsparung bei der Verwendung der Berechnungen im Expert Advisor gezeigt. Besonders zeitaufwendig ist es, die Daten von einem benutzerdefinierten Indikator zu erhalten.

Die Ergebnisse des Experiments 2

Bitte beachten: im Experiment wurde der МА nach dem Close-Preis berechnet. Der Berechnungsblock eines solchen Indikators ist relativ einfach. Es stellt sich die Frage: wird sich die Situation bei komplizierteren Berechnungen ändern? Finden wir es heraus, indem wir noch ein Experiment durchführen.

4.3. Experiment 3

Dieses Experiment wiederholt das vorherige, aber um die Last auf den Berechnungsblock zu erhöhen, wurden die Indikatoren als linear gewichteter gleitender Durchschnitt vom gewichteten Durchschnittspreis berechnet.

Experiment 3Experiment 3

Wir sehen, dass der Zeitaufwand für das Erhalten von Daten bei allen Methoden gestiegen ist. Dabei steigt die Zeit eines Durchlaufs proportional, was im Großen und Ganzen die Ergebnisse des vorherigen Experiments bestätigt.

Die Ergebnisse des Experiments 3

Fazit

Der Artikel demonstriert die Technologie der Übertragung des Berechnungsblocks eines Indikators in einen Expert Advisor. Die objektorientierte Programmierung erlaubt es, den Zugriff auf die Daten des Indikators möglichst ähnlich der standardmäßigen Abfrage der Daten von Indikatorpuffern zu machen. Der Quellcode des Expert Advisors wird minimal geändert.

Nach den Ergebnissen der durchgeführten Experimente kann solcher Ansatz beim Testen und bei der Optimierung von Expert Advisors Zeit sparen. Bei der Arbeit des Expert Advisors in Echtzeit kann dieser Vorteil durch die Multi-Thread-Architektur des MetaTrader 5 nivelliert werden.

Die im Artikel verwendeten Programme:

#
 Name
Typ 
Beschreibung 
1 Indicarot.mqh  Klassenbibliothek  Basisklasse für die Übertragung der Indikatoren
2 MA.mqh  Klassenbibliothek  Klasse für die Berechnung des MA-Indikators innerhalb des Expert Advisors
3 Test.mq5  Expert Advisor  Expert Advisor für die Durchführung des 1. Experiments
4 Test_Class.mq5  Expert Advisor  Expert Advisor mit der Berechnung des Indikators innerhalb des Expert Advisors (Experimente 2 und 3)
5 Test_iMA.mq5  Expert Advisor  Expert Advisor, der Indikatordaten über iMA erhält
6 Test_iCustom.mq5  Expert Advisor  Expert Advisor, der Indikatordaten über iCustom erhält


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

Beigefügte Dateien |
MQL5.zip (164.68 KB)
Die Behandlung der Ergebnisse der Optimierung mit einem grafischen Interface Die Behandlung der Ergebnisse der Optimierung mit einem grafischen Interface

Dies ist eine Fortsetzung der Idee der Verarbeitung und Analyse von Optimierungsergebnissen. Diesmal geht es darum, die 100 besten Optimierungsergebnisse auszuwählen und in einer GUI-Tabelle darzustellen. Der Benutzer kann eine Zeile in der Optimierungsergebnistabelle auswählen und erhält ein Saldo mehrerer Symbole und eine Drawdown-Grafik auf einer eigenen Seite.

Random Decision Forest und Reinforcement-Learning Random Decision Forest und Reinforcement-Learning

Random Forest (RF) mit dem Einsatz von Bagging ist eine der leistungsfähigsten maschinellen Lernmethoden, die dem Gradienten-Boosting etwas unterlegen ist. Dieser Artikel versucht, ein selbstlernendes Handelssystem zu entwickeln, das Entscheidungen basierend auf den Erfahrungen aus der Interaktion mit dem Markt trifft.

Die Entwicklung eines oszillierenden ZigZag-Indikator Beispiel für die Durchführung der Anforderungsspezifikationen Die Entwicklung eines oszillierenden ZigZag-Indikator Beispiel für die Durchführung der Anforderungsspezifikationen

Der Artikel demonstriert die Entwicklung des ZigZag-Indikators gemäß der im Artikel "Wie man eine Anforderungsspezifikation bei der Bestellung eines Indikators erstellt" beschriebenen Beispiele. Der Indikator wird durch Extrema gebildet, die mit Hilfe eines Oszillators definiert werden. Es besteht die Möglichkeit, einen von fünf Oszillatoren zu verwenden: WPR, CCI, Chaikin, RSI oder die Stochastik.

Ein visueller Strategieentwickler Erstellen eines Handelsroboters ohne zu programmieren Ein visueller Strategieentwickler Erstellen eines Handelsroboters ohne zu programmieren

Dieser Artikel stellt einen visuellen Strategieentwickler vor. Es wird gezeigt, wie jeder Nutzer einen Handelsroboter oder ein Hilfsprogramm ohne zu programmieren, erstellen kann. Der erstellte Expert Advisor ist voll funktionsfähig und kann im Strategie-Tester getestet, in der Cloud optimiert oder auf einem realen Konto ausgeführt werden.