Anwenden von Indikatoren auf Indikatoren

MetaQuotes | 11 Januar, 2016

Einleitung

Die Aufgabe ist, einen Indikator, der auf die Werte eines anderen Indikators angewendet wird, zu verbessern. In diesem Artikel arbeiten wir weiterhin mit dem True Strength Index (TSI), der in dem vorherigen Beitrag "MQL5: Erstellen Ihres eigenen Indikators" erstellt und erläutert wurde.

Benutzerdefinierter Indikator auf Basis der Werte eines anderen Indikators

Beim Schreiben eines Indikators, der die kurze Form des Aufrufs der OnCalculate()-Funktion nutzt, könnten Sie übersehen, dass ein Indikator nicht nur anhand von Preisdaten berechnet werden kann, sondern auch anhand der Daten eines anderen Indikators (unabhängig davon, ob dieser Indikator ein integrierter oder benutzerdefinierter ist).

Machen wir ein einfaches Experiment: Hängen Sie den integrierten RSI-Indikator mit Standardeinstellungen an ein Diagramm an und ziehen Sie den benutzerdefinierten Indikator True_Strength_Index_ver2.mq5 in das Fenster des RSI-Indikators. Geben Sie in der Registerkarte Parameters an, dass der Indikator auf Previous Indicator's Data (Daten des vorherigen Indikators) (RSI(14)) angewendet werden soll.

Das Ergebnis unterscheidet sich stark von dem, was wir erwartet hätten. Die zusätzliche Linie des TSI-Indikators erschien nicht im Fenster des RSI-Indikators und seine Werte im DataWindow sind ebenfalls unklar.

Obwohl die RSI-Werte fast in der gesamten Historie definiert sind, fehlen die Werte von TSI (angewendet auf RSI-Daten) entweder vollständig (am Anfang) oder betragen immer -100:

Dieses Verhalten wird dadurch verursacht, dass der Wert des Parameters begin an keiner Stelle in der Funktion OnCalculate() in unserer True_Strength_Index_ver2.mq5 benutzt wird. Der Parameter begin gibt die Anzahl leerer Werte im Eingabeparameter price[] an. Diese leeren Werte können in der Berechnung von Indikatorwerten nicht genutzt werden. Rufen wir uns die Definition der ersten Form des Aufrufs der OnCalculate()-Funktion ins Gedächtnis.
int OnCalculate (const int rates_total,      // price[] array length
                 const int prev_calculated,  // number of bars calculated after previous call
                 const int begin,            // start index of meaningful data
                 const double& price[]       // array for calculation
   );

Als wir den Indikator auf Preisdaten anwendeten, die eine der Preiskonstanten festlegen, war der Parameter begin gleich 0, weil es einen festgelegten Preistyp für jedes Bar gibt. Deshalb hat das Eingabe-Array price[] immer korrekte Daten ab seinem ersten Element price[0]. Dies ist allerdings nicht länger garantiert, wenn wir Daten eines anderen Indikators als Quelle für Berechnungen angeben.

Der Parameter begin der OnCalculate()-Funktion

Überprüfen wir die Daten im Array price[], wenn die Berechnung anhand der Daten eines anderen Indikators durchgeführt wird. Dazu fügen wir in der OnCalculate()-Funktion etwas Code hinzu, der die Werte ausgibt, die wir überprüfen wollen. Nun sieht der Anfang der OnCalculate()-Funktion so aus:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // price[] array length;
                 const int prev_calculated,// number of available bars after previous call;
                 const int begin,          // start index of meaningful data in price[] array 
                 const double &price[])    // data array, that will be used for calculations;
  {
//--- flag for single output of price[] values
   static bool printed=false;
//--- if begin isn't zero, then there are some values that we shouldn't take into account

   if(begin>0 && !printed)
     {
      //--- let's output them
      Print("Data for calculation begin from index equal to ",begin,
            "   price[] array length =",rates_total);

      //--- let's show the values that we shouldn't take into account for calculation
      for(int i=0;i<=begin;i++)
        {
         Print("i =",i,"  value =",price[i]);
        }
      //--- set printed flag to confirm that we have already logged the values
      printed=true;
     }

Ziehen wir die veränderte Version unseres Indikators erneut in das RSI(14)-Fenster und geben die Daten des vorherigen Indikators für die Berechnung an. Nun sehen wir die Werte, die nicht dargestellt werden und für Berechnungen, in denen die Werte des Indikators RSI(14) verwendet werden, nicht berücksichtigt werden sollen.


Leere Werte in Indikatorpuffern und DBL_MAX

Die ersten 14 Elemente des Arrays price[] mit den Indizes 0 bis einschließlich 13 haben den gleichen Wert: 1.797693134862316e+308. Sie werden diese Zahl sehr oft zu Gesicht bekommen, da es sich um den numerischen Wert der integrierten Konstante EMPTY_VALUE handelt, die zum Kennzeichnen leerer Werte in einem Indikatorpuffer verwendet wird.

Das Füllen leerer Werte mit Nullen ist keine universelle Lösung, da dieser Wert das Ergebnis der Berechnung von anderen Indikatoren sein kann. Aus diesem Grund geben alle integrierten Indikatoren des Client-Terminals diese Zahl für leere Werte aus. Der Wert 1.797693734862316e+308 wurde gewählt, da er der höchste mögliche Wert des Typen double ist und praktischerweise als Konstante DBL_MAX in MQL5 dargestellt wird.

Um zu prüfen, ob eine bestimmte Zahl des Typen double leer ist oder nicht, können Sie sie mit den Konstanten EMPTY_VALUE oder DBL_MAX vergleichen. Beide Varianten sind gleich, aber die Verwendung der Konstante EMPTY_VALUE ist eindeutiger.

//+------------------------------------------------------------------+
//| returns true for "empty" values                                  |
//+------------------------------------------------------------------+
bool isEmptyValue(double value_to_check)
  {
//--- if the value is equal DBL_MAX, it has an empty value
   if(value_to_check==EMPTY_VALUE) return(true);
//--- it isn't equal DBL_MAX
   return(false);
  }

DBL_MAX ist eine sehr große Zahl und der RSI-Indikator kann solche Werte nicht ausgeben! Außerdem hat nur das fünfzehnte Element des Arrays (mit dem Index 14) einen vernünftigen Wert gleich 50. Also können wir mithilfe des Parameters begin die Datenverarbeitung für solche Fälle ordentlich organisieren, auch wenn wir nichts über den Indikator als Quelle für zu berechnende Werte wissen. Genauer gesagt, müssen wir in unseren Berechnungen die Verwendung dieser leeren Werte vermeiden.

Beziehung zwischen Parameter begin und Eigenschaft PLOT_DRAW_BEGIN

Der Parameter begin, der an die OnCalculate()-Funktion übertragen wird, und die Eigenschaft PLOT_DRAW_BEGIN, die die Anzahl der Ausgangs-Bars ohne Zeichnung definiert, stehen in einer engen Beziehung zueinander. Betrachten wir den Quellcode von RSI aus dem MetaTrader5-Standardpaket, sehen wir den folgenden Code in der OnInit()-Funktion:
//--- sets first bar from what index will be drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,ExtPeriodRSI);

Das bedeutet, dass die grafische Darstellung mit Index 0 erst ab dem Bar beginnt, dessen Index ExtPeriodRSI entspricht (das ist eine Eingabevariable, die den Zeitraum des RSI-Indikators festlegt). Frühere Bars werden nicht dargestellt.

In der MQL5-Sprache wird die Berechnung eines Indikators A basierend auf den Daten von Indikator B immer anhand der Werte des Nullpuffers des Indikators B durchgeführt. Die Werte des Nullpuffers von Indikator B werden als Eingabeparameter price[] an die OnCalculate()-Funktion des Indikators A übertragen. Dabei wird der Nullpuffer immer der grafischen Null-Darstellung mithilfe der SetIndexBuffer()-Funktion zugeordnet. Deshalb:

Regel zum Übertragen der Eigenschaft PLOT_DRAW_BEGIN an den Parameter begin: Bei Berechnungen des benutzerdefinierten Indikators A auf Basis der Daten eines anderen (Basis-)Indikators B ist der Wert des Eingabeparameters begin in der OnCalculate()-Funktion immer gleich dem Wert der Eigenschaft PLOT_DRAW_BEGIN der grafischen Null-Darstellung des Basisindikators B.

Wenn wir also einen RSI-Indikator (Indikator B) mit Zeitraum 14 und dann unseren benutzerdefinierten Indikator True Strength Index (Indikator A) auf Basis seiner Daten erstellt haben, gilt:

Denken Sie daran, dass der TSI-Indikator nicht ab dem Anfang des Diagramms gezeichnet wird, weil der Indikatorwert in einigen der ersten Bars nicht festgelegt ist. Der Index des ersten Bars, das im TSI-Indikator als Linie dargestellt wird, entspricht r+s-1. Dabei ist:

Für Bars mit Indizes unter r+s-1 gibt es keine Werte, die im TSI-Indikator dargestellt werden können. Deshalb haben die Daten für die endgültigen Arrays EMA2_MTMBuffer[] und EMA2_AbsMTMBuffer[], die für die Berechnung des TSI-Indikators verwendet werden, ein zusätzliches Offset und beginnen beim Index r+s-1. Weitere Informationen finden Sie im Beitrag "MQL5: Erstellen Ihres eigenen Indikators".

Die OnInit()-Funktion enthält eine Anweisung zum Deaktivieren der Zeichnung der ersten r+s-1-Bars:

//--- first bar to draw
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);

Da der Beginn der Eingabedaten durch begin-Bars nach vorne verschoben wurde, müssen wir dies berücksichtigen und die Anfangsposition der Datenzeichnung durch die begin-Bars in der OnCalculate()-Funktion erhöhen:

   if(prev_calculated==0)
     { 
      //--- let's increase beginning position of data by begin bars,
      //--- because we use other indicator's data for calculation
      if(begin>0)PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,begin+r+s-1);
     }
Nun berücksichtigen wir den Parameter begin bei der Berechnung der Werte des TSI-Indikators. Außerdem wird der Parameter begin korrekt übertragen, wenn ein anderer Indikator TSI-Werte für die Berechnung nutzt: beginanderer_Indikator=beginunser_Indikator+r+s-1. Somit können wir die Regel zum Auferlegen eines Indikators auf Werte eines anderen Indikators formulieren:

Regeln zum Auferlegen von Indikatoren: Wenn ein Indikator A ab der Position Na (die ersten Na-Werte werden nicht dargestellt) gezeichnet wird und auf Daten eines anderen Indikators B basiert, der ab der Position Nb gezeichnet wird, wird der daraus entstehende Indikator A{B} ab der Position Nab=Na+Nb gezeichnet. Dabei bedeutet A{B}, dass der Indikator A anhand der Werte des Nullpuffers des Indikators B berechnet wird.

Somit bedeutet TSI (25,13) {RSI (14)}, dass der Indikator TSI (25,13) aus Werten des Indikators RSI (14) besteht. Aufgrund der Auferlegung beginnen die Daten nun bei (25+13-1)+14=51. Also beginnt die Zeichnung des Indikators ab dem 52. Bar (die Indizierung der Bars beginnt bei 0).

Hinzufügen von begin-Werten zu Indikatorberechnungen

Wir wissen nun genau, dass sinnvolle Werte des Arrays price[] immer ab der Position beginnen, die durch den Parameter begin festgelegt ist. Verändern wir unseren Code Schritt für Schritt. Als Erstes kommt der Code, der die Werte der Arrays MTMBuffer[] und AbsMTMBuffer[] berechnet. Ohne den Parameter begin begann die Ausfüllung der Arrays mit dem Index 1.

//--- calculate values for mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling MTMBuffer[] and AbsMTMBuffer[] arrays from 1st index 
   else start=prev_calculated-1;    // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Jetzt beginnen wir bei der Position (begin+1) und der veränderte Code sieht so aus (Code-Änderungen sind fett formatiert):

//--- calculate values for mtm and |mtm|
   int start;
   if(prev_calculated==0) start=begin+1;  // start filling MTMBuffer[] and AbsMTMBuffer[] arrays from begin+1 index 
   else start=prev_calculated-1;           // set start equal to the last array index
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Da die Werte von price[0] bis price[begin-1] nicht für Berechnungen verwendet werden können, beginnen wir bei price[begin]. Die ersten Werte, die für die Arrays MTMBuffer[] und AbsMTMBuffer[] berechnet werden, sehen wie folgt aus:

      MTMBuffer[begin+1]=price[begin+1]-price[begin];
      AbsMTMBuffer[begin+1]=fabs(MTMBuffer[begin+1]);

Aus diesem Grund hat die Variable start im Zyklus for nun den Ausgangswert start=begin+1 anstatt 1.

Berücksichtigen von begin bei abhängigen Arrays

Nun folgt die exponentielle Glättung der Arrays MTMBuffer[] und AbsMTMBuffer[]. Die Regel ist ebenfalls einfach: Wenn die Ausgangsposition des Basis-Arrays um begin Bars erhöht wurde, sollte auch die Ausgangsposition aller abhängigen Arrays um begin erhöht werden.

//--- calculating the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,               // index of the starting element in array 
                         r,               // period of exponential average
                         MTMBuffer,       // source buffer for average
                         EMA_MTMBuffer);  // target buffer
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Hier sind MTMBuffer[] und AbsMTMBuffer[] die Basis-Arrays und die berechneten Werte in diesen Arrays beginnen nun bei einem Index, der um begin größer ist. Also fügen wir diese Verschiebung einfach zur ExponentialMAOnBuffer()-Funktion hinzu.

Jetzt sieht dieser Block so aus:

//--- calculating the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,        // index of the starting element in array 
                         r,               // period for exponential average
                         MTMBuffer,       // source buffer for average
                         EMA_MTMBuffer);  // target buffer
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Wie Sie sehen können, diente die gesamte Anpassung dem Zweck, die Erhöhung der Startposition der Daten, die durch den Parameter begin definiert ist, zu berücksichtigen. Nichts Kompliziertes. Auf die gleiche Weise ändern wir den zweiten Glättungsblock.

Vorher:

//--- calculating the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Nachher:

//--- calculating the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Auf die gleiche Weise ändern wir den letzten Berechnungsblock.

Vorher:

//--- calculating values of our indicator
   if(prev_calculated==0) start=r+s-1; // set initial index for input arrays
   else start=prev_calculated-1;       // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

Nachher:

//--- calculating values of our indicator
   if(prev_calculated==0) start=begin+r+s-1; // set initial index for input arrays
   else start=prev_calculated-1;              // set start equal to last array index
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

Die Feinabstimmung des Indikators ist abgeschlossen. Nun werden die ersten leeren begin-Werte des Eingabe-Arrays price[] in der OnCalculate()-Funktion übersprungen und wir berücksichtigen die von dieser Auslassung verursachte Verschiebung. Doch wir müssen daran denken, dass bestimmte andere Indikatoren die TSI-Werte für Berechnungen nutzen können. Aus diesem Grund setzen wir die leeren Werte unseres Indikators auf EMPTY_VALUE.

Ist die Initialisierung von Indikatorpuffern notwendig?

In MQL5 werden Arrays standardmäßig nicht durch beliebige definierte Werte initialisiert. Das gilt auch für Arrays, die durch die SetIndexBuffer()-Funktion als Indikatorpuffer definiert sind. Wird ein Array als Indikatorpuffer verwendet, hängt seine Größe vom Wert des Parameters rates_total in der OnCalculate()-Funktion ab.

Sie könnten versucht sein, alle Indikatorpuffer mit EMPTY_VALUE-Werten mithilfe der ArrayInitialize()-Funktion zu initialisieren, zum Beispiel sofort am Anfang von OnCalculate():

//--- if it is the first call of OnCalculate() 
   if(prev_calculated==0)
     {
      ArrayInitialize(TSIBuffer,EMPTY_VALUE);
     }

Doch das ist aus dem folgenden Grund nicht empfehlenswert: Während das Client Terminal ausgeführt wird, gehen neue Angebote für das Symbol ein, deren Daten zur Berechnung des Indikators verwendet werden. Nach einiger Zeit steigt die Anzahl von Bars und das Client Terminal reserviert deshalb zusätzlichen Speicher für die Indikatorpuffer.

Doch die Werte neuer ("angehängter") Array-Elemente können beliebig sein, da die Initialisierung während der Neuzuweisung des Speichers für keines der Arrays durchgeführt wird. Die erste Initialisierung kann Ihnen die falsche Gewissheit vermitteln, dass alle Array-Elemente, die nicht explizit definiert wurden, mit Werten ausgefüllt werden, die während der Initialisierung festgelegt wurden. Das ist natürlich falsch und Sie sollten nicht den Eindruck erhalten, dass der numerische Wert von Variablen oder Array-Elementen mit Werten, die wir benötigen, initialisiert wird.

Sie sollten den Wert für jedes Element des Indikatorpuffers einstellen. Wenn die Werte bestimmter Bars nicht durch den Algorithmus des Indikators definiert sind, sollten Sie sie explizit als leere Werte definieren. Wird beispielsweise ein Wert des Indikatorpuffers durch eine Division berechnet, kann der Divisor in einigen Fällen Null sein.

Wir wissen, dass die Division durch Null in MQL5 ein kritischer Laufzeitfehler ist und zur sofortigen Beendigung eines mql5-Programms führt. Anstatt die Division durch Null durch eine Bearbeitung dieses Sonderfalls in einem Code zu vermeiden, muss der Wert für dieses Pufferelement definiert werden. Möglicherweise ist es besser, den Wert zu verwenden, den wir für diesen Zeichenstil als leer definiert haben.

Beispielsweise haben wir für einen bestimmten Zeichenstil Null als leeren Wert mithilfe der PlotIndexSetDouble()-Funktion definiert:

   PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,0);   

Dann ist es für alle leeren Werte des Indikatorpuffers in dieser Zeichnung erforderlich, den Nullwert explizit zu definieren:

   if(divider==0)
      IndicatorBuffer[i]=0;
   else
      IndicatorBuffer[i]=... 

Zudem werden alle Elemente des Indikatorpuffers mit Indizes von 0 bis DRAW_BEGIN automatisch mit Nullen ausgefüllt, wenn DRAW_BEGIN für eine Zeichnung festgelegt wurde.

Fazit

Fassen wir kurz zusammen. Es gibt einige verpflichtende Bedingungen, damit ein Indikator korrekt auf Basis der Daten eines anderen Indikators berechnet werden kann (und damit er für die Verwendung in anderen mql5-Programmen geeignet ist):

  1. Leere Werte in integrierten Indikatoren werden mit den Werten der Konstante EMPTY_VALUE ausgefüllt, die genau dem Maximalwert für den Typ double entspricht (DBL_MAX).
  2. Um Details über den Startindex sinnvoller Werte eines Indikators zu finden, sollten Sie den Eingabeparameter begin der Kurzform von OnCalculate() analysieren.
  3. Um die Darstellung der ersten N-Werte für den festgelegten Zeichenstil zu verbieten, stellen Sie den Parameter DRAW_BEGIN mithilfe des folgenden Codes ein:
    PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,N);
  4. Falls DRAW_BEGIN für eine Zeichnung festgelegt wurde, werden alle Elemente des Indikatorpuffers mit Indizes von 0 bis DRAW_BEGIN automatisch mit leeren Werten ausgefüllt (Standard ist EMPTY_VALUE).
  5. Fügen Sie in der OnCalculate()-Funktion eine zusätzliche Verschiebung durch begin-Bars ein, damit andere Indikatordaten in Ihrem eigenen Indikator korrekt verwendet werden können:
    //--- if it's the first call 
       if(prev_calculated==0)
         { 
          //--- increase position of data beginning by begin bars, 
          //--- because of other indicator's data use      
          if(begin>0)PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,begin+N);
         }
    
  6. Mithilfe des folgenden Codes können Sie Ihren eigenen leeren Wert festlegen, der sich von EMPTY_VALUE in der OnInit()-Funktion unterscheidet:
    PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,your_empty_value);
  7. Verlassen Sie sich nicht auf eine einmalige Initialisierung der Indikatorpuffer mithilfe des folgenden Codes:
    ArrayInitialize(buffer_number,value);
        
    Sie sollten alle Werte des Indikatorpuffers für die OnCalculate()-Funktion explizit und konsistent festlegen, einschließlich leerer Werte.

Natürlich werden Sie in der Zukunft, wenn Sie Erfahrung beim Schreiben von Indikatoren gesammelt haben, auf Fälle stoßen, die außerhalb des Umfangs dieses Artikels sind. Dennoch hoffe ich, dass Sie dies mit den Kenntnissen von MQL5, die Sie bis dahin sammeln werden, lösen können.