Komplexe Indikatoren mit Objekten vereinfachen

Manuel Alejandro Cercos Perez | 11 August, 2022

1. Einführung

Wenn Sie schon einmal versucht haben, einen komplexen Indikator zu erstellen oder zu modifizieren, kennen Sie vielleicht einige der Probleme, die entstehen, wenn man die Anzahl der Puffer erhöht: Sie müssen Unmengen von double-Arrays für die Puffer deklarieren, sie als Puffer festlegen, alle konfigurieren...

Dann kommen die Darstellung: Sie müssen angeben, welche Art Sie verwenden, alle Eigenschaften konfigurieren und dann sicherstellen, dass alles richtig passt und dass Sie die richtige Anzahl von Puffern und Darstellungen angegeben haben (wenn es weniger sind, dann würden Sie einen Array Out Of Range-Fehler oder einen unsichtbaren Darstellung bekommen, bis Sie es bemerken).

Schließlich kommt der Umgang mit den Daten in den Puffern: Wenn Sie Daten aus einer großen Anzahl von Puffern kombinieren wollen (z. B. den Durchschnitt/Maximum/Minimum von 10 Puffern in einem weiteren), dann müssen Sie sehr lange Zeilen mit sich wiederholendem Code schreiben, der jeden Puffer einzeln vergleicht/kombiniert, oder sich auf einige clevere Tricks mit Makros oder Funktionen verlassen, um etwas Platz zu sparen. Das Ergebnis ist wahrscheinlich dasselbe: ein sehr komplexes, fehleranfälliges Durcheinander mit vielen Zeilen und sich wiederholenden Funktionen. Wenn Sie irgendwo einen Tippfehler gemacht haben, wäre es ein Albtraum, diesen zu entdecken und zu korrigieren!

Diese Art von Situationen kann Anfänger (und sogar fortgeschrittene Programmierer) davon abhalten, komplexe Indikatoren zu erstellen, sei es in Bezug auf die Funktionalität oder die visuelle Gestaltung. Es gibt jedoch einen kleinen Trick, der nicht für jeden ersichtlich ist und der die Kodierung von Indikatoren schneller und einfacher machen könnte:

Sie können Arrays, die innerhalb von Objekten enthalten sind, als Puffer festlegen.

In diesem Artikel werden wir die Möglichkeiten untersuchen, die uns dieser Trick bietet, und Lösungen finden, um diesen Trick in jeder anderen Situation mit objektorientierter Programmierung anwenden zu können.


2. Ein erstes Beispiel

Bevor wir mit der Erstellung des Indikators beginnen, sehen wir uns an, wie die einfachste Form des Objekts aussieht, das die Puffer-Arrays enthalten wird:

class CIndicatorPlot
{
public:
   double            array[];
};

Er hat nur ein ‚public‘ Array. Im Moment ist es wichtig, dass er public (öffentlich) ist, damit wir auf ihn zugreifen können, wenn wir ihn als Puffer festlegen oder auf die Daten, die er haben wird, zugreifen können (wie bei jedem anderen Indikator).

Kommen wir nun zum Indikator: Um einige der Konzepte anzuwenden, werden wir einen Indikator erstellen, der 10 RSI mit verschiedenen Periodenlängen und seinem Durchschnitt anzeigt. Wir beginnen mit den Eigenschaften, Eingaben und der OnInit-Funktion.

#property indicator_buffers 11
#property indicator_plots 11

input int firstPeriod = 6;
input int increment = 2;

CIndicatorPlot indicators[];
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{

   ArrayResize(indicators, 11);
//--- indicator buffers mapping

   for (int i=0; i<11; i++)
   {
      SetIndexBuffer(i, indicators[i].array, INDICATOR_DATA);
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
   }

   for (int i=0; i<10; i++)
      PlotIndexSetInteger(i, PLOT_LINE_COLOR, clrRed);


   PlotIndexSetInteger(10, PLOT_LINE_COLOR, clrCyan);
   PlotIndexSetInteger(10, PLOT_LINE_STYLE, STYLE_DASH);
   PlotIndexSetInteger(10, PLOT_LINE_WIDTH, 2);

   ArrayResize(handles, 10);
   for (int i=0; i<10; i++)
      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);


//---
   return(INIT_SUCCEEDED);
}

Beachten Sie, dass wir nur 2 Eigenschaften verwendet haben: indicator_buffers und indicator_plots. Abgesehen von den üblichen (Copyright, Link, Version, separates/Chartfenster...) werden diese 2 immer benötigt. Die anderen Eigenschaften (Linienfarbe, Zeichentyp...) sind optional, aber um einen kompakteren Code zu haben, werden wir sie mit PlotIndexSetInteger in einer Schleife konfigurieren.
Für diesen Indikator benötigen wir 10 Puffer für jeden RSI mit verschiedenen Periodenlängen und einen weiteren für ihren Durchschnitt. Wir werden sie alle in ein Array packen. Außerdem erstellen wir die Indikator-Handles in OnInit. 

Nun ein paar Berechnungen und Datenkopien...

//+------------------------------------------------------------------+
//| 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[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
   {
      if (limit==0)
         CopyBuffer(handles[i], 0, 0, rates_total-limit, indicators[i].array);
      else
      {
         double newValues[];
         CopyBuffer(handles[i], 0, 0, rates_total-limit, newValues);

         for (int k=0; k<rates_total-limit; k++)
         {
            indicators[i].array[limit+k] = newValues[k];
         }
      }
   }

   for (int i=limit; i<rates_total; i++)
   {
      indicators[10].array[i] = 0.0;
      for (int j=0; j<10; j++)                            
         indicators[10].array[i] +=indicators[j].array[i];

      indicators[10].array[i]/=10.0;
   }


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

Beachten Sie, dass die Berechnung des Durchschnitts aus allen Puffern nun so einfach ist wie die Ausführung einer Schleife. Wenn jeder Puffer im globalen Bereich als double Array (wie üblich) deklariert wäre, wäre das Zusammenzählen nicht so einfach oder in so wenigen Zeilen möglich.

Und vergessen wir nicht, die Handles wieder freizugeben...

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
}
//+------------------------------------------------------------------+

Und das ist unser Endergebnis:

Für die Länge des Codes ist das gar nicht so schlecht, aber es gibt noch Raum für Verbesserungen, wie wir im folgenden Abschnitt sehen werden.


3. Hinzufügen weiterer Optionen

Obwohl wir durch die Verwendung dieser Klasse und die Konfiguration der Eigenschaften bei der Initialisierung (anstelle von #property) etwas Platz gespart haben, mussten wir die Puffer und die Plots immer noch manuell konfigurieren, und das ist nicht jedes Mal so einfach. Gibt es keine effektivere Methode, dies zu tun? Die Antwort lautet: Ja, und zwar indem wir die Funktionalität an die Klasse delegieren.

Zunächst fügen wir der Klasse einige zusätzliche Funktionen hinzu, die wir später benötigen werden.

class CIndicatorPlot
{
private:
   int               indicator_plot;

public:
   double            array[];

   void              SetBuffer(int &buffer, int &plot);
   void              SetLineWidth(int width);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineColor(color line_color);
   void              SetLabel(string label);
};

Mit der Funktion SetBuffer wird der Indikatorpuffer gesetzt und gezeichnet. Wenn zwei Variablen per Referenz übergeben werden, spiegeln sich die Änderungen, die ein Objekt an ihnen vornimmt, in den nächsten Aufrufen durch andere Objekte wider. Der Plotindex wird gespeichert, um andere Eigenschaften einzustellen.

Der Rest der Set-Funktionen setzen einfache die anderen Plot-Eigenschaften.

//+------------------------------------------------------------------+
void CIndicatorPlot::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);

   buffer++; //Increment for other steps (One buffer in this case)
   plot++;   //Increment one plot in any case
}

//+------------------------------------------------------------------+
void CIndicatorPlot::SetLineWidth(int width)
{
   PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width);
}

//---
//...

Um den Indikator optisch interessanter zu gestalten, werden wir auch eine Funktion zur Interpolation von Farben erstellen, die wir später verwenden werden:

//+------------------------------------------------------------------+
//| Function to linearly interpolate 2 colors                        |
//+------------------------------------------------------------------+
color InterpolateColors(color colorA, color colorB, double factor)
{
   if (factor<=0.0) return colorA;
   if (factor>=1.0) return colorB;

   int result = 0;

   for (int i=0; i<3; i++) //R-G-B
   {
      int subcolor = int(
                        ((colorA>>(8*i))&(0xFF))*(1.0-factor) +
                        ((colorB>>(8*i))&(0xFF))*factor
                     );

      subcolor = subcolor>0xFF?0xFF:(
                    subcolor<0x00?0x00:
                    subcolor);

      result |= subcolor<<(8*i);
   }
   return (color)result;
}

Die OnInit-Funktion sieht nun wie folgt aus:

CIndicatorPlot* indicators[];
CIndicatorPlot average;
int handles[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);
   ArrayResize(handles, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
        indicators[i] = new CIndicatorPlot();
   
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE);
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

//---
   return(INIT_SUCCEEDED);
}

Beachten Sie, dass wir nirgendwo auf einen Puffer oder ein Chart mit seiner Nummer verwiesen haben: Die Klassen haben dieses Problem gelöst. Jetzt ist es einfacher, jede Darstellungseigenschaft korrekt zu setzen oder die Reihenfolge der Puffer zu ändern, da wir uns auf sie mit einem Objekt statt mit einem Index beziehen können. Wir haben auch die Gelegenheit genutzt, um die Darstellung mit Farben und Beschriftungen zu versehen.

In diesem Beispiel haben wir auch die Struktur des Indikators geändert, indem wir ein Array von Zeigern für die RSIs verwendet haben (um zu beweisen, dass es auch möglich ist, dynamisch erstellte Objekte zu verwenden) und den Durchschnitt vom Array getrennt haben. Aus diesem Grund müssen wir die Referenzen zu den Durchschnitten in OnCalculate ändern und die Indikatoren im Array der Zeiger in OnDeInit löschen.

void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      IndicatorRelease(handles[i]);
   for (int i=0; i<10; i++)
        delete indicators[i];
}

Jetzt sieht es so aus:

Die einzige visuelle Änderung betrifft die Farben (und die Beschriftungen im Datenfenster). Intern haben wir unseren Arbeitsablauf verbessert, indem wir den Umgang mit den Plots und Puffern vereinfacht haben, aber es gibt noch mehr Raum, um die Organisation im Inneren zu verbessern.

Wenn Sie einen genaueren Blick darauf werfen, werden Sie sehen, dass jedes Handle nur von einem der Puffer verwendet wird: Jeder RSI-Puffer kann unabhängig berechnet werden, sodass wir die Klasse dazu bringen können, dies innerhalb der Klasse zu tun (anstatt die Berechnungen direkt in OnCalculate durchzuführen). Der Durchschnitt muss auf den Rest der Puffer zugreifen, aber diese Berechnungen können auch an die Klasse delegiert werden. Mit Hilfe der Vererbung können wir bestimmte Funktionen hinzufügen, ohne die Funktionen zu ändern oder Bedingungen in der Basisklasse hinzuzufügen.

Zunächst fügen wir der Basisklasse virtuelle leere Ereignishandler hinzu:

class CIndicatorPlot
{
   //...

public:
   
   //...

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(const int start, const int rates_total) { }
};

Wie bereits in diesem Beispiel gesehen, benötigt Update nur die start und rates_total zur Durchführung der Berechnungen, sodass die restlichen Werte weggelassen werden.

Jetzt werden wir die Klasse Individual RSI erstellen. Er kann den benötigten Handle erstellen und löschen. Außerdem haben wir eine Funktion zum Festlegen der Periode dieses Handles eingefügt, aber es ist auch möglich, diesen Parameter in Init() einzufügen.

class CRSIIndividual : public CIndicatorPlot
{
private:
   int               handle;
   int               rsi_period;

public:

   void              SetPeriodRSI(int period);

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIIndividual::SetPeriodRSI(int period)
{
   rsi_period = period;
}

//+------------------------------------------------------------------+
void CRSIIndividual::Init(void)
{
   handle = iRSI(NULL, PERIOD_CURRENT, rsi_period, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CRSIIndividual::Update(const int start,const int rates_total)
{
   if (start==0)
      CopyBuffer(handle, 0, 0, rates_total-start, array);
   else
   {
      double newValues[];
      CopyBuffer(handle, 0, 0, rates_total-start, newValues);

      for (int k=0; k<rates_total-start; k++)
      {
         array[start+k] = newValues[k];
      }
   }
}

//+------------------------------------------------------------------+
void CRSIIndividual::DeInit(void)
{
   IndicatorRelease(handle);
}

Für die Klasse Average müssen wir Zeiger für den Zugriff auf die übrigen Indikatorplot-Objekte (die einzelnen RSI) speichern. In diesem Fall werden Init() und DeInit() nicht benötigt.

class CRSIAverage : public CIndicatorPlot
{
private:
   CRSIIndividual*   rsi_indicators[];

public:
   void              SetRSIPointers(const CRSIIndividual &rsi_objects[]);

   virtual void      Update(const int start, const int rates_total);
};

//+------------------------------------------------------------------+
void CRSIAverage::SetRSIPointers(const CRSIIndividual &rsi_objects[])
{
   int total = ArraySize(rsi_objects);
   ArrayResize(rsi_indicators, total);

   for (int i=0; i<total; i++)
      rsi_indicators[i] = (CRSIIndividual*)GetPointer(rsi_objects[i]);
}

//+------------------------------------------------------------------+
void CRSIAverage::Update(const int start,const int rates_total)
{
   for (int i=start; i<rates_total; i++)
   {
      array[i] = 0.0;
      for (int j=0; j<10; j++)
         array[i] +=rsi_indicators[j].array[i];

      array[i]/=10.0;
   }
}

Die Erstellung eines Arrays von Zeigern mag sich wie eine Überkomplizierung des Problems anfühlen, wenn man direkt aus dem globalen Bereich auf die Objekte zugreifen könnte, aber dies erleichtert die Wiederverwendung der Klasse in anderen Indikatoren, ohne weitere Änderungen vorzunehmen. Beachten Sie auch, dass wir in diesem Beispiel wieder ein Array von Objekten anstelle von Zeigern für die RSI-Indikatoren verwenden werden, sodass wir die Zeiger von ihnen erhalten müssen.

Schließlich wird OnInit (und die obigen Objektdeklarationen) wie folgt aussehen...

CRSIIndividual indicators[];
CRSIAverage average;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   ArrayResize(indicators, 10);


   int index=0, plot=0;

   for (int i=0; i<10; i++)
   {
      indicators[i].SetBuffer(index, plot);
      indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/9.0));
      indicators[i].SetLabel("RSI ("+IntegerToString(firstPeriod+i*increment)+")");

      indicators[i].SetPeriodRSI(firstPeriod+i*increment);
      indicators[i].Init();                               
   }

   average.SetBuffer(index, plot);
   average.SetLineColor(clrBlue);
   average.SetLineStyle(STYLE_DASH);
   average.SetLineWidth(2);
   average.SetLabel("Average");

   average.SetRSIPointers(indicators);                    

//---
   return(INIT_SUCCEEDED);
}

... und wir werden in der Lage sein, die anderen Funktionen zur Ereignisbehandlung viel sauberer zu gestalten:

//+------------------------------------------------------------------+
//| 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[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   for (int i=0; i<10; i++)
      indicators[i].Update(limit, rates_total);

   average.Update(limit, rates_total);

//--- return value of prev_calculated for next call
   return(rates_total);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   for (int i=0; i<10; i++)
      indicators[i].DeInit();
}
//+------------------------------------------------------------------+

Optisch sieht der Indikator genau so aus wie im zweiten Beispiel.



4. Ausweitung der Klasse

Bis jetzt können alle diese Klassen gut ihre Aufgabe erfüllen, aber sie sind sehr spezifisch für die Art von Indikator, mit dem wir uns beschäftigt haben: Wir haben nur ein paar Zeichen-Eigenschaften und nur Linienzeichnungen verwendet, aber was ist, wenn ich eine Darstellung mit Farbpuffern verwenden möchte? Oder ein Histogramm, oder ein Zickzack...? Um das, was wir gemacht haben, wiederverwenden zu können, müssen wir die Klassen verallgemeinern. Dafür müssen wir 3 Bedingungen erfüllen:

In diesem Sinne werden wir zunächst erklären, wie die Klassen implementiert werden und wie die Vererbung strukturiert ist.

Zunächst sind die Klassen wie folgt aufgebaut:

Ein paar wichtige Punkte:

Zusammengefasst sieht die Umsetzung folgendermaßen aus:

//+------------------------------------------------------------------+
//| Base class for plots and calculation buffers                     |
//+------------------------------------------------------------------+
class CIndicatorBufferBase
{
public:
   virtual void      SetBuffer(int &buffer, int &plot)=NULL;
   virtual void      SetAsSeries(bool set)=NULL;

   virtual void      Init() { }
   virtual void      DeInit() { }
   virtual void      Update(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[]) { }
};

//+------------------------------------------------------------------+
//| Calculations Buffer (with no plot)                               |
//+------------------------------------------------------------------+
class CIndicatorCalculations : public CIndicatorBufferBase
{
public:
   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetBuffer(int &index, int &plot)
{
   SetIndexBuffer(index, array, INDICATOR_CALCULATIONS);

   index++;
//No plot is used
}

//+------------------------------------------------------------------+
void CIndicatorCalculations::SetAsSeries(bool set)
{
   ArraySetAsSeries(array, set);
}

//+------------------------------------------------------------------+
//| Base indicator plot class                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotBase : public CIndicatorBufferBase
{
protected:

   int               indicator_plot;

   virtual void      SetDrawType()=NULL; //Implicit in each class

public:

   void              SetArrow(uchar arrow);
   void              SetArrowShift(int shift);
   void              SetDrawBegin(int begin);
   void              SetShowData(bool show);
   void              SetShift(int shift);
   void              SetLineStyle(ENUM_LINE_STYLE style);
   void              SetLineWidth(int width);
   void              SetColorIndexes(color &color_array[]);
   void              SetLineColor(color line_color);
   void              SetLineColor(color line_color, int index);
   void              SetEmptyValue(double empty);
   void              SetLabel(string label);

   int               GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, int property_modifier=0);
};

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer                           |
//+------------------------------------------------------------------+
class CIndicator_1Data : public CIndicatorPlotBase
{
public:

   double            array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data::SetBuffer(int &buffer,int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, array, INDICATOR_DATA);
   SetDrawType();

   buffer++;
   plot++;
}

//...

//+------------------------------------------------------------------+
//| Plot Line (1 data buffer)                                        |
//+------------------------------------------------------------------+
class CIndicatorPlotLine : public CIndicator_1Data
{
protected:

   virtual void      SetDrawType() final;
};

//+------------------------------------------------------------------+
void CIndicatorPlotLine::SetDrawType(void)
{
   PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE);
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 2 Data Buffers                          |
//+------------------------------------------------------------------+
class CIndicator_2Data : public CIndicatorPlotBase
{
public:

   double            first_array[];
   double            second_array[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};


//+------------------------------------------------------------------+
void CIndicator_2Data::SetBuffer(int &buffer, int &plot)
{
   indicator_plot = plot;

   SetIndexBuffer(buffer, first_array, INDICATOR_DATA);
   SetIndexBuffer(buffer+1, second_array, INDICATOR_DATA);
   SetDrawType();

   buffer+=2;
   plot++;
}

//...

//...

//+------------------------------------------------------------------+
//| Base for indicators with 1 Data Buffer & 1 Color Buffer          |
//+------------------------------------------------------------------+
class CIndicator_1Data1Color : public CIndicator_1Data
{
public:

   double            color_buffer[];

   virtual void      SetBuffer(int &buffer, int &plot);
   virtual void      SetAsSeries(bool set);
};

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetBuffer(int &buffer, int &plot)
{
   CIndicator_1Data::SetBuffer(buffer, plot);

   SetIndexBuffer(buffer, color_buffer, INDICATOR_COLOR_INDEX);

   buffer++; //Add color buffer
}

//+------------------------------------------------------------------+
void CIndicator_1Data1Color::SetAsSeries(bool set)
{
   CIndicator_1Data::SetAsSeries(set);
   ArraySetAsSeries(color_buffer, set);
}

//...

Jede Klasse enthält (und setzt) die benötigte Anzahl von Puffern. CIndicatorBufferBase hat die Ereignisbehandlung, die optional von jeder Art von Pufferklasse überschrieben werden können, CIndicatorPlotBase enthält Setter für alle Darstellungseigenschaften (und einen Getter), jede Basisdatenklasse (mit oder ohne Farbe) enthält die Array-Deklarationen und Setzfunktionen für die Puffer, und jede spezifische Klasse überschreibt die Funktion SetDrawType() und deklariert sie als final, damit sie nicht erneut überschrieben werden kann (wenn Sie eine Klasse mit undefiniertem Zeichnungstyp benötigen, könnten Sie von der entsprechenden Basisdatenklasse erben und diese Funktion überschreiben).

In dieser Implementierung hat Update alle Werte, die im OnCalculate-Ereignis verwendet werden, aber sie können mit weniger Parametern überschrieben werden, wenn Sie keine Polymorphie verwenden müssen.

ArraySetAsSeries wurde ebenfalls aufgenommen, da es sich um eine sehr verbreitete Funktion handelt, die fast immer voraussetzt, dass alle Puffer auf dieselbe Weise gesetzt werden.


Jetzt, da wir die Klassen haben, können wir einen Indikator erstellen. Wir werden als Beispiel einige Dinge zusammenzählen:

Zunächst deklarieren wir die Eingaben und binden die Dateien für die Indikatorklassen und die Farbinterpolation ein, die wir in Abschnitt 3 erstellt haben:

#property indicator_buffers 19
#property indicator_plots 13

#include <OOPIndicators/IndicatorClass.mqh>
#include <OOPIndicators/ColorLerp.mqh>

input int atr_period = 10; //ATR Period
input double atr_band_multiplier = 0.8; //ATR Multiplier for bands
input bool show_bands = true; //Show Bands
input bool show_data = false; //Show Extra Data

input int ma_faster_period = 14; //MA Faster Period
input int ma_step = 2; //MA Step
input ENUM_MA_METHOD ma_method = MODE_SMA; //MA Method

Wir haben bereits die benötigte Anzahl von Puffern und Darstellungen festgelegt. Man sollte nicht von vornherein wissen müssen, wie viele von jedem benötigt werden, aber wie wir weiter unten sehen werden, kann es einfacher sein, die Werte zu ermitteln (in OnInit()).

Dann erstellen wir die Klassen für jeden Teil des Indikators.

Beginnend mit den ATR-Bändern:

//+------------------------------------------------------------------+
//| ATR Bands class (inherit from Filling Plot)                      |
//+------------------------------------------------------------------+
class CATRBand : public CIndicatorPlotFilling
{
private:

   int               handle;

public:

   virtual void      Init();
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total, const double &close[]);
};

//+------------------------------------------------------------------+
void CATRBand::Init(void)
{
   handle = iATR(NULL, PERIOD_CURRENT, atr_period);
}

//+------------------------------------------------------------------+
void CATRBand::Update(const int limit,const int rates_total,const double &close[])
{
   double atr[];
   CopyBuffer(handle, 0, 0, rates_total-limit, atr);

   for (int i=limit; i<rates_total; i++)
   {
      first_array[i] = close[i]+atr[i-limit]*atr_band_multiplier;
      second_array[i] = close[i]-atr[i-limit]*atr_band_multiplier;
   }
}

//+------------------------------------------------------------------+
void CATRBand::DeInit(void)
{
   IndicatorRelease(handle);
}

Die Klasse MA, die in Init() Parameter für den Zeitraum und die Methode hat:

//+------------------------------------------------------------------+
//| Moving Averages class (inherit from Line Plot)                   |
//+------------------------------------------------------------------+
class CMA : public CIndicatorPlotLine
{
private:

   int               handle;

public:
   virtual void      Init(int period, ENUM_MA_METHOD mode);
   virtual void      DeInit();
   virtual void      Update(const int limit, const int rates_total);
};

//+------------------------------------------------------------------+
void CMA::Init(int period, ENUM_MA_METHOD mode)
{
   handle = iMA(NULL, PERIOD_CURRENT, period, 0, mode, PRICE_CLOSE);
}

//+------------------------------------------------------------------+
void CMA::Update(const int limit,const int rates_total)
{
   if (limit==0) CopyBuffer(handle, 0, 0, rates_total, array);
   else
   {
      double newVals[];
      CopyBuffer(handle, 0, 0, rates_total-limit, newVals);

      for (int i=limit; i<rates_total; i++)
         array[i] = newVals[i-limit];
   }
}

//+------------------------------------------------------------------+
void CMA::DeInit(void)
{
   IndicatorRelease(handle);
}

Und die Klasse der Kerzen. In diesem Fall und um zusätzliche Komplexität für das Beispiel zu vermeiden, werden wir auf Objekte aus dem globalen Bereich zugreifen. Dies ist jedoch nicht empfehlenswert, wenn wir eine Klasse wiederverwenden wollen.

Sie enthält auch Makros, die wir weiter unten ebenfalls deklarieren werden. Hinweis: Die Funktionen befinden sich unter dem Makro im Code, aber die Funktionen wurden neu geordnet, um hier gezeigt zu werden.

//+------------------------------------------------------------------+
//| Color Candles class (inherit from Color Candles Plot)            |
//+------------------------------------------------------------------+
class CColorCandles : public CIndicatorPlotColorCandles
{
public:
   virtual void      Update(const int limit,
                            const int rates_total,
                            const double &open[],
                            const double &high[],
                            const double &low[],
                            const double &close[]);
};

//+------------------------------------------------------------------+
void CColorCandles::Update(const int limit,
                           const int rates_total,
                           const double &open[],
                           const double &high[],
                           const double &low[],
                           const double &close[])
{
   for (int i=limit; i<rates_total; i++)
   {
      open_array[i] = open[i];
      high_array[i] = high[i];
      low_array[i] = low[i];
      close_array[i] = close[i];

      int count_ma = TOTAL_MA;

      for (int m=0; m<TOTAL_MA; m++)
      {
         if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++;
         if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--;
      }

      color_buffer[i] = count_ma;

      //Update inside of this other object (to avoid making an extra inheritance, or an external loop)
      showIndex.array[i] = TOTAL_MA - count_ma;
   }
}

Jetzt müssen wir die Objekte deklarieren und die Puffer und die Anzeige der Darstellungen konfigurieren:

#define TOTAL_MA 10

CMA maIndicators[TOTAL_MA];
CATRBand bands;
CColorCandles candles;
CIndicatorPlotNone showIndex; //To show MAs above/below

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
   int buffer=0, plot=0;

   bands.SetBuffer(buffer, plot);
   candles.SetBuffer(buffer, plot);
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].SetBuffer(buffer, plot);
   showIndex.SetBuffer(buffer, plot);

//Print("Buffers: ", buffer, "  Plots: ", plot);

//--- plot settings
   if (show_bands) bands.SetLineColor(clrDarkSlateGray);
   else bands.SetLineColor(clrNONE);
   bands.SetShowData(show_data);
   if (show_data)
      bands.SetLabel("Close + ATR;Close - ATR");


   for (int i=0; i<TOTAL_MA; i++)
   {
      maIndicators[i].SetLineColor(InterpolateColors(clrAqua, clrRoyalBlue, i/(TOTAL_MA-1.0)));
      maIndicators[i].SetLabel("MA("+IntegerToString(ma_faster_period+i*ma_step)+")");
      maIndicators[i].SetShowData(show_data);
      if (i>0 && i <TOTAL_MA-1) maIndicators[i].SetLineStyle(STYLE_DOT);
      else maIndicators[i].SetLineWidth(2);
   }

   color arrow_colors[TOTAL_MA*2+1];

   for (int i=0; i<TOTAL_MA; i++)
      arrow_colors[i] = InterpolateColors(clrGreenYellow, clrGray, i/double(TOTAL_MA));
   arrow_colors[TOTAL_MA] = clrGray;
   for (int i=TOTAL_MA+1; i<TOTAL_MA*2+1; i++)
      arrow_colors[i] = InterpolateColors(clrGray, clrOrange, (i-TOTAL_MA)/double(TOTAL_MA));

   candles.SetColorIndexes(arrow_colors);
   candles.SetLabel("Open;High;Low;Close");
   candles.SetShowData(false);

   showIndex.SetLabel("MAs above/below");
   showIndex.SetShowData(true);

//--- initialize classes
   bands.Init();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Init(ma_faster_period+i*ma_step, ma_method);

   return(INIT_SUCCEEDED);
}

In dieser Reihenfolge werden zunächst die Puffer konfiguriert, dann die Darstellungseigenschaften und anschließend die Teilindikatoren initialisiert (wie in ihren Klassen angegeben).

Wie bereits erwähnt, können wir die Anzahl der Puffer und Darstellungen, die wir benötigen, leicht ermitteln, indem wir die Werte der Variablen Puffer und Darstellungen ausdrucken. Dann können wir die Eigenschaften richtig einstellen (zu Beginn können wir sie auch auf eine höhere Zahl als nötig einstellen, um Fehler zu vermeiden).

Vergessen wir nicht, dass wir eine Instanz der Klasse „Plot None“ eingefügt haben.  Dieses Objekt wird durch das Kerzenobjekt aktualisiert, sodass es keine speziellen Ereignisbehandler benötigt. Sie zeigt die Anzahl der MAs an, die über oder unter den Bändern im Datenfenster liegen.

Schließlich gibt es nicht viel Funktionalität in den anderen Event-Handlern, da sich alles innerhalb der Objekte befindet, es wird nur benötigt, um die Funktionen der Objekte in der richtigen Reihenfolge aufzurufen:

//+------------------------------------------------------------------+
//| 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[])
{
//---
   int limit = MathMax(0, prev_calculated-1);

   bands.Update(limit, rates_total, close);

   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].Update(limit, rates_total);

   candles.Update(limit, rates_total, open, high, low, close);

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

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   bands.DeInit();
   for (int i=0; i<TOTAL_MA; i++)
      maIndicators[i].DeInit();
}

Das Endergebnis des Indikators sieht wie folgt aus:


5. Einschränkungen dieser Methode

Obwohl es bequemer ist, bringt die Verwendung von Funktionen anstelle von Eigenschaften einige Nachteile mit sich: der bemerkenswerteste ist die Störung beim Ändern der Farben/Stils einer Darstellung, manchmal bleiben sie erhalten und manchmal werden sie bei der Initialisierung neu geschrieben.

Dieses Problem kann vermieden werden, indem man Eingaben für die Farben verwendet (anstatt sie in der Registerkarte Farben zu ändern) oder prüft, ob es eine andere Farbe als Schwarz (0x000000) gibt, die die Standardfarbe ist. Das würde allerdings mit allen Farben außer Schwarz funktionieren.

if (obj.GetInteger(PLOT_LINE_COLOR)==clrBlack)
   obj.SetLineColor(clrYellow);

Außerdem haben wir in diesem Artikel die Auswirkungen der Verwendung dieser Klassen auf die Leistung nicht analysiert. Theoretisch sollte die direkte Verwendung von Eigenschaften und weniger Funktionen schneller sein, aber in den meisten Fällen nicht allzu bedeutsam.

Wie Sie vielleicht schon bemerkt haben, enthalten die Klassen keine Ereignisbehandlung von Chartereignisse oder OnTimer. Der Grund dafür ist, dass Chartereignisse besser direkt in OnChartEvent verarbeitet werden und die spezifischen Funktionen erst danach aufgerufen werden (anstatt jedes Mal, wenn ein Ereignis auftritt, einen Handler für jeden Indikator aufzurufen und jedes Ereignis mehrfach zu verarbeiten). Für den Timer können wir den Update-Handler anders verwenden, wenn unser Indikator Multi-Timeframe oder Multi-Currency ist (wir haben dann keinen direkten Zugriff auf die OnCalculate-Arrays). Andere Design-Entscheidung, die einige vielleicht nicht mittragen, war die Deklaration der Arrays, die als Puffer mit öffentlicher Sichtbarkeit verwendet werden: Es ist möglich, die Arrays mit ‚protected‘ Sichtbarkeit zu deklarieren und der Indikator würde immer noch funktionieren, aber es müssen möglicherweise Getter hinzugefügt werden, um in der Lage sein, auf die Daten von außen zuzugreifen.


6. Schlussfolgerung

In diesem Artikel haben wir eine Methode entwickelt, um komplexe Indikatoren einfacher und mit weniger Zeilen zu erstellen. Wir begannen mit kleinen organisatorischer Tricks für einen bestimmten Fall, implementierten dann eine Klassenstruktur, die die Wiederverwendung und Anpassung von Funktionen ermöglicht, und fügten schließlich alles zu einem Beispielindikator zusammen, der die meisten der in diesem Artikel beschriebenen Funktionen nutzt.