Objektorientierter Ansatz zum Aufbau von Panels mit mehreren Timeframes und Währungen

Marcin Konieczny | 26 Januar, 2016


Einleitung

In diesem Beitrag wird die Nutzung der objektorientierten Programmierung zum Erstellen von Panels mit mehreren Timeframes und Währungen für MetaTrader 5 beschrieben. Das Hauptziel ist der Aufbau eines universell einsatzfähigen Panels, das für die Darstellung verschiedener Arten von Daten genutzt werden kann – beispielsweise Preise, Preisänderungen, Indikatorwerte oder benutzerdefinierte Kauf-/Verkaufsbedingungen –, ohne den Code des Panels selbst verändern zu müssen. Auf diese Art ist nur ein sehr geringer Codieraufwand erforderlich, um das Panel an unsere Bedürfnisse anzupassen.

Die Lösung, die ich beschreiben werde, funktioniert in zwei Modi:

  1. Multi-Timeframe-Modus – ermöglicht die Darstellung der Tabelleninhalte, die für das aktuelle Symbol berechnet wurden, in verschiedenen Timeframes;
  2. Mehrwärhungsmodus – ermöglicht die Darstellung der Tabelleninhalte, die für das aktuelle Timeframe berechnet wurden, in verschiedenen Symbolen.

Die folgenden Abbildungen zeigen das Panel in diesen zwei Modi.

Die erste Abbildung zeigt die folgenden Daten im Multi-Timeframe-Modus:

  1. Aktueller Preis;
  2. Preisänderung im aktuellen Bar;
  3. Preisänderung im aktuellen Bar in Prozent;
  4. Preisänderung im aktuellen Bar als Pfeil (nach oben/unten);
  5. Indikatorwert RSI(14);
  6. Indikatorwert RSI(10);
  7. Benutzerdefinierte Bedingung: SMA(20) < aktueller Preis.

Abb. 1 Multi-Timeframe-Modus

Abbildung 1. Multi-Timeframe-Modus


Die zweite Abbildung zeigt die folgenden Daten im Mehrwährungsmodus:

  1. Aktueller Preis;
  2. Preisänderung im aktuellen Bar;
  3. Preisänderung im aktuellen Bar in Prozent;
  4. Preisänderung im aktuellen Bar als Pfeil;
  5. Indikatorwert RSI(10);
  6. Indikatorwert RSI(14);
  7. Benutzerdefinierte Bedingung: SMA(20) < aktueller Preis.

Abb. 2 Mehrwährungsmodus

Abbildung 2. Mehrwährungsmodus


1. Umsetzung

Das folgende Klassendiagramm illustriert die Umsetzung des Panels.

Abb. 3 Klassendiagramm des Panels.

Abbildung 3. Klassendiagramm des Panels.


Lassen Sie mich die Bestandteile des Diagramms beschreiben:

  1. CTable. Kernklasse des Diagramms. Sie ist für die Zeichnung des Panels und die Verwaltung seiner Bestandteile zuständig.
  2. SpyAgent. Ein Indikator, der für die 'Beobachtung' anderer Symbole (Instrumente) zuständig ist. Für jedes Symbol wird ein Agent erstellt und zugewiesen. Der Agent reagiert auf OnCalculate-Ereignisse, wenn ein neuer Tick für das Symboldiagramm eintrifft, und sendet ein CHARTEVENT_CUSTOM-Ereignis, um das CTable-Objekt darüber zu informieren, dass es aktualisiert werden muss. Die Idee hinter diesem Ansatz basiert auf dem Beitrag "Die Umsetzung des Mehrwährungsmodus in MetaTrader 5". Dort finden Sie alle technischen Details.
  3. CRow. Basisklasse für alle Indikatoren und Bedingungen, die für die Erstellung des Panels genutzt werden. Durch die Erweiterung dieser Klasse können alle notwendigen Bestandteile des Panels erstellt werden.
  4. CPriceRow. Einfache Klasse, die CRow erweitert und zur Darstellung des aktuellen Bid-Preises genutzt wird.
  5. CPriceChangeRow. Klasse, die CRow erweitert und zur Darstellung der Preisänderung des aktuellen Bars genutzt wird. Sie kann Preisänderungen, prozentuale Änderungen oder Pfeile darstellen.
  6. CRSIRow. Klasse, die CRow erweitert und für die Darstellung des Wertes des RSI-Indikators genutzt wird.
  7. CPriceMARow. Klasse, die CRow erweitert und eine benutzerdefinierte Bedingung darstellt: SMA > aktueller Preis.

Die Klassen CTable und CRow sowie der SpyAgent-Indikator sind die wichtigsten Bestandteile des Panels. CPriceRow, CPriceChangeRow, CRSIRow und CPriceMARow sind tatsächliche Inhalte des Panels. Die CRow-Klasse lässt sich durch zahlreiche neue Klassen erweitern, um das gewünschte Ergebnis zu erhalten. Die vier abgeleiteten Klassen, die hier vorgestellt wurden, dienen nur als einfache Beispiele dafür, was möglich ist und wie es bewerkstelligt werden kann.


2. SpyAgent

Fangen wir beim SpyAgent-Indikator an. Er wird nur im Multi-currency mode (Mehrwährungsmodus) verwendet und wird benötigt, um das Panel korrekt zu aktualisieren, wenn ein neuer Tick für andere Diagramme eingeht. Ich werde nicht zu sehr ins Detail gehen. Die Details werden im Beitrag "Die Umsetzung des Mehrwährungsmodus in MetaTrader 5" beschrieben.

Der SpyAgent-Indikator wird auf dem Diagramm des angegebenen Symbols ausgeführt und sendet zwei Ereignisse: Initialisierungsereignis und neues Tick-Ereignis. Beide Ereignisse gehören zum Typen CHARTEVENT_CUSTOM. Um diese Ereignisse zu verarbeiten, müssen wir den Handler OnChartEvent(...) verwenden (wir gehen später auf diesen ein).

Sehen wir uns den Code des SpyAgents an:

//+------------------------------------------------------------------+
//|                                                     SpyAgent.mq5 |
//|                                                 Marcin Konieczny |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Marcin Konieczny"
#property indicator_chart_window
#property indicator_plots 0

input long   chart_id=0;        // chart id
input ushort custom_event_id=0; // event id
//+------------------------------------------------------------------+
//| Indicator iteration function                                     |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {

   if(prev_calculated==0)
      EventChartCustom(chart_id,0,0,0.0,_Symbol); // sends initialization event
   else
      EventChartCustom(chart_id,(ushort)(custom_event_id+1),0,0.0,_Symbol); // sends new tick event

   return(rates_total);
  }
Er ist ziemlich simpel. Alles, was er tut, ist neue Ticks empfangen und CHARTEVENT_CUSTOM-Ereignisse senden.


3. CTable

CTable ist die zentrale Klasse des Panels. Sie speichert Informationen über die Einstellungen des Panels und verwaltet seine Komponenten. Bei Bedarf aktualisiert sie das Panel (zeichnet es neu).

Sehen wir uns die Deklarierung von CTable an:

//+------------------------------------------------------------------+
//| CTable class                                                     |
//+------------------------------------------------------------------+
class CTable
  {
private:
   int               xDistance;    // distance from right border of the chart
   int               yDistance;    // distance from top of the chart
   int               cellHeight;   // table cell height
   int               cellWidth;    // table cell width
   string            font;         // font name
   int               fontSize;
   color             fontColor;

   CList            *rowList;      // list of row objects
   bool              tfMode;       // is in multi-timeframe mode?

   ENUM_TIMEFRAMES   timeframes[]; // array of timeframes for multi-timeframe mode
   string            symbols[];    // array of currency pairs for multi-currency mode

   //--- private methods
   //--- sets default parameters of the table
   void              Init();
   //--- draws text label in the specified table cell
   void              DrawLabel(int x,int y,string text,string font,color col);
   //--- returns textual representation of given timeframe
   string            PeriodToString(ENUM_TIMEFRAMES period);

public:
   //--- multi-timeframe mode constructor
                     CTable(ENUM_TIMEFRAMES &tfs[]);
   //--- multi-currency mode constructor
                     CTable(string &symb[]);
   //--- destructor
                    ~CTable();
   //--- redraws table
   void              Update();
   //--- methods for setting table parameters
   void              SetDistance(int xDist,int yDist);
   void              SetCellSize(int cellW,int cellH);
   void              SetFont(string fnt,int size,color clr);
   //--- appends CRow object to the of the table
   void              AddRow(CRow *row);
  };

Wie Sie sehen, sind alle Komponenten (Zeilen) des Panels als Liste von CRow-Pointern gespeichert, also muss jede Komponente, die wir dem Panel hinzufügen möchten, die CRow-Klasse erweitern. CRow kann als Kontaktpunkt zwischen dem Panel und seinen Komponenten betrachtet werden. CTable enthält keine Codes für die Neuberechnung ihrer Zellen. Dafür sind Klassen verantwortlich, die CRow erweitern. CTable ist nur eine Struktur, die CRow-Komponenten enthält und sie bei Bedarf neu zeichnet.

Sehen wir uns die Methoden von CTable an. Die Klasse hat zwei Konstruktoren. Der erste wird für den Multi-Timeframe-Modus genutzt und ist ziemlich simpel. Wir müssen ihm nur einen Bereich von Timeframes zur Verfügung stellen, die er darstellen soll.

//+------------------------------------------------------------------+
//| Multi-timeframe mode constructor                                 |
//+------------------------------------------------------------------+
CTable::CTable(ENUM_TIMEFRAMES &tfs[])
  {
//--- copy all timeframes to own array
   ArrayResize(timeframes,ArraySize(tfs),0);
   ArrayCopy(timeframes,tfs);
   tfMode=true;
   
//--- fill symbols array with current chart symbol
   ArrayResize(symbols,ArraySize(tfs),0);
   for(int i=0; i<ArraySize(tfs); i++)
      symbols[i]=Symbol();

//--- set default parameters
   Init();
  }

Der zweite Konstruktor wird für den Mehrwährungsmodus verwendet und benötigt einen Bereich von Symbolen (Instrumenten). Dieser Konstruktor sendet auch SpyAgents. Er hängt sie einen nach dem anderen an die entsprechenden Diagramme an.

//+------------------------------------------------------------------+
//| Multi-currency mode constructor                                  |
//+------------------------------------------------------------------+
CTable::CTable(string &symb[])
  {
//--- copy all symbols to own array
   ArrayResize(symbols,ArraySize(symb),0);
   ArrayCopy(symbols,symb);
   tfMode=false;
   
//--- fill timeframe array with current timeframe
   ArrayResize(timeframes,ArraySize(symb),0);
   ArrayInitialize(timeframes,Period());

//--- set default parameters
   Init();

//--- send SpyAgents to every requested symbol
   for(int x=0; x<ArraySize(symbols); x++)
      if(symbols[x]!=Symbol()) // don't send SpyAgent to own chart
         if(iCustom(symbols[x],0,"SpyAgent",ChartID(),0)==INVALID_HANDLE)
           {
            Print("Error in setting of SpyAgent on "+symbols[x]);
            return;
           }
  }

Die Init-Methode erstellt die Liste von Zeilen (als CList-Objekt – CList ist eine dynamische Liste aus CObject-Typen) und legt die Standardwerte für die internen Variablen von CTable fest (Schriftart, Schriftgröße, Farbe, Zellengröße und Abstand von der rechten oberen Ecke des Diagramms).

//+------------------------------------------------------------------+
//| Sets default parameters of the table                             |
//+------------------------------------------------------------------+
CTable::Init()
  {
//--- create list for storing row objects
   rowList=new CList;

//--- set defaults
   xDistance = 10;
   yDistance = 10;
   cellWidth = 60;
   cellHeight= 20;
   font="Arial";
   fontSize=10;
   fontColor=clrWhite;
  }

Der Destruktor ist ziemlich simpel. Er löscht die Listen von Zeilen und alle Diagrammobjekte (Labels), die vom Panel erstellt wurden.

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTable::~CTable()
  {
   int total=ObjectsTotal(0);

//--- remove all text labels from the chart (all object names starting with nameBase prefix)
   for(int i=total-1; i>=0; i--)
      if(StringFind(ObjectName(0,i),nameBase)!=-1)
         ObjectDelete(0,ObjectName(0,i));

//--- delete list of rows and free memory
   delete(rowList);
  }

Die AddRow-Methode hängt die neue Zeile an die Liste von Zeilen an. Beachten Sie, dass rowList ein CList-Objekt ist, das seine Größe automatisch ändert. Diese Methode ruft auch die Init-Methode für jedes hinzugefügte CRow-Objekt auf. Dem Objekt muss die korrekte Initialisierung seiner internen Variablen genehmigt werden. Beispielsweise kann es den Init-Aufruf nutzen, um einen Indikator oder Datei-Handles zu erstellen.

//+------------------------------------------------------------------+
//| Appends new row to the end of the table                          |
//+------------------------------------------------------------------+
CTable::AddRow(CRow *row)
  {
   rowList.Add(row);
   row.Init(symbols,timeframes);
  }

Die Update-Methode ist schon etwas komplizierter. Sie wird verwendet, um das Panel neu zu zeichnen.

Grundsätzlich besteht sie aus den folgenden drei Teilen:

Es sollte beachtet werden, dass jede Komponente ihren eigenen Wert basierend auf dem zur Verfügung gestellten Symbol und Timeframe berechnen soll. Wir lassen die Komponenten auch entscheiden, welche Schriftart und Farbe verwendet werden soll.

//+------------------------------------------------------------------+
//| Redraws the table                                                |
//+------------------------------------------------------------------+
CTable::Update()
  {
   CRow *row;
   string symbol;
   ENUM_TIMEFRAMES tf;

   int rows=rowList.Total(); // number of rows
   int columns;              // number of columns

   if(tfMode)
      columns=ArraySize(timeframes);
   else
      columns=ArraySize(symbols);

//--- draw first column (names of rows)
   for(int y=0; y<rows; y++)
     {
      row=(CRow*)rowList.GetNodeAtIndex(y);
      //--- note: we ask row object to return its name
      DrawLabel(columns,y+1,row.GetName(),font,fontColor);
     }

//--- draws first row (names of timeframes or currency pairs)
   for(int x=0; x<columns; x++)
     {
      if(tfMode)
         DrawLabel(columns-x-1,0,PeriodToString(timeframes[x]),font,fontColor);
      else
         DrawLabel(columns-x-1,0,symbols[x],font,fontColor);
     }

//--- draws inside table cells
   for(int y=0; y<rows; y++)
      for(int x=0; x<columns; x++)
        {
         row=(CRow*)rowList.GetNodeAtIndex(y);

         if(tfMode)
           {
            //--- in multi-timeframe mode use current symbol and different timeframes
            tf=timeframes[x];
            symbol=_Symbol;
           }
         else
           {
            //--- in multi-currency mode use current timeframe and different symbols
            tf=Period();
            symbol=symbols[x];
           }

         //--- note: we ask row object to return its font, 
         //--- color and current calculated value for given timeframe and symbol
         DrawLabel(columns-x-1,y+1,row.GetValue(symbol,tf),row.GetFont(symbol,tf),row.GetColor(symbol,tf));
        }

//--- forces chart to redraw
   ChartRedraw();
  }

Die DrawLabel-Methode wird verwendet, um Textlabels in der angegebenen Zelle des Panels zu zeichnen. Als Erstes überprüft sie, ob es bereits ein Label für diese Zelle gibt. Falls nicht, erstellt sie ein neues.

Anschließend legt sie alle erforderlichen Label-Eigenschaften und seinen Text fest.

//+------------------------------------------------------------------+
//| Draws text label in the specified cell of the table              |
//+------------------------------------------------------------------+  
CTable::DrawLabel(int x,int y,string text,string font,color col)
  {
//--- create unique name for this cell
   string name=nameBase+IntegerToString(x)+":"+IntegerToString(y);

//--- create label
   if(ObjectFind(0,name)<0)
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);

//--- set label properties
   ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_XDISTANCE,xDistance+x*cellWidth);
   ObjectSetInteger(0,name,OBJPROP_YDISTANCE,yDistance+y*cellHeight);
   ObjectSetString(0,name,OBJPROP_FONT,font);
   ObjectSetInteger(0,name,OBJPROP_COLOR,col);
   ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize);

//--- set label text
   ObjectSetString(0,name,OBJPROP_TEXT,text);
  }

Andere Methoden werden hier nicht vorgestellt, da sie sehr simpel und nicht so wichtig sind. Der vollständige Code kann unten im Beitrag heruntergeladen werden.


4. Erweiterung von CRow

CRow ist eine Basisklasse für alle Komponenten, die von dem Panel genutzt werden können.

Sehen wir uns den Code der CRow-Klasse an:

//+------------------------------------------------------------------+
//| CRow class                                                       |
//+------------------------------------------------------------------+
//| Base class for creating custom table rows                        |
//| one or more methods of CRow should be overriden                  |
//| when creating own table rows                                     |
//+------------------------------------------------------------------+
class CRow : public CObject
  {
public:
   //--- default initialization method
   virtual void Init(string &symb[],ENUM_TIMEFRAMES &tfs[]) { }

   //--- default method for obtaining string value to display in the table cell
   virtual string GetValue(string symbol,ENUM_TIMEFRAMES tf) { return("-"); }

   //--- default method for obtaining color for table cell
   virtual color GetColor(string symbol,ENUM_TIMEFRAMES tf) { return(clrWhite); }
   
   //--- default method for obtaining row name
   virtual string GetName() { return("-"); }

   //--- default method for obtaining font for table cell
   virtual string GetFont(string symbol,ENUM_TIMEFRAMES tf) { return("Arial"); }
  };

Sie erweitert CObject, da nur CObjects in einer CList-Struktur gespeichert werden können. Sie hat fünf Methoden, die beinahe leer sind. Genauer gesagt geben die meisten von ihnen nur Standardwerte aus. Diese Methoden sollen bei der Erweiterung von CRow überschrieben werden. Wir müssen sie nicht alle überschreiben, sondern nur diejenigen, die wir wollen.

Erstellen wir beispielsweise die einfachste mögliche Panel-Komponente: die Komponente für den aktuellen Bid-Preis. Sie kann im Mehrwährungsmodus verwendet werden, um die aktuellen Preise verschiedener Instrumente anzuzeigen.

Dazu erstellen wir eine CPriceRow-Klasse, die so aussieht:

//+------------------------------------------------------------------+
//| CPriceRow class                                                  |
//+------------------------------------------------------------------+
class CPriceRow : public CRow
  {
public:
   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

  };
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CPriceRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   MqlTick tick;

//--- gets current price
   if(!SymbolInfoTick(symbol,tick)) return("-");

   return(DoubleToString(tick.bid,(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
string CPriceRow::GetName()
  {
   return("Price");
  }

Die Methoden, die wir in diesem Fall überschreiben, sind GetValue und GetName. GetName gibt einfach den Namen dieser Zeile aus, der in der ersten Spalte des Panels angezeigt wird. GetValue erhält den aktuellsten Tick des angegebenen Symbols und gibt den aktuellsten Bid-Preis aus. Das ist alles, was wir brauchen.

Das war ein einfaches Beispiel. Versuchen wir etwas anderes. Wir erstellen nun eine Komponente, die den aktuellen RSI-Wert anzeigt.

Der Code ist dem vorherigen ähnlich:

//+------------------------------------------------------------------+
//| CRSIRow class                                                    |
//+------------------------------------------------------------------+
class CRSIRow : public CRow
  {
private:
   int               rsiPeriod;        // RSI period
   string            symbols[];        // symbols array
   ENUM_TIMEFRAMES   timeframes[];     // timeframes array
   int               handles[];        // array of RSI handles

   //--- finds the indicator handle for a given symbol and timeframe
   int               GetHandle(string symbol,ENUM_TIMEFRAMES tf);

public:
   //--- constructor
                     CRSIRow(int period);

   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

   //--- overrides default Init(..) method from CRow
   virtual void      Init(string &symb[],ENUM_TIMEFRAMES &tfs[]);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRSIRow::CRSIRow(int period)
  {
   rsiPeriod=period;
  }
//+------------------------------------------------------------------+
//| Overrides default Init(..) method from CRow                      |
//+------------------------------------------------------------------+
void CRSIRow::Init(string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   int size=ArraySize(symb);
   
   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);
   
//--- copies arrays contents into own arrays
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);
  
//--- gets RSI handles for all used symbols or timeframes
   for(int i=0; i<ArraySize(symbols); i++)
      handles[i]=iRSI(symbols[i],timeframes[i],rsiPeriod,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CRSIRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double value[1];

//--- gets RSI indicator handle
   int handle=GetHandle(symbol,tf);

   if(handle==INVALID_HANDLE) return("err");

//--- gets current RSI value
   if(CopyBuffer(handle,0,0,1,value)<0) return("-");

   return(DoubleToString(value[0],2));
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
string CRSIRow::GetName()
  {
   return("RSI("+IntegerToString(rsiPeriod)+")");
  }
//+------------------------------------------------------------------+
//| finds the indicator handle for a given symbol and timeframe      |
//+------------------------------------------------------------------+
int CRSIRow::GetHandle(string symbol,ENUM_TIMEFRAMES tf)
  {
   for(int i=0; i<ArraySize(timeframes); i++)
      if(symbols[i]==symbol && timeframes[i]==tf)
         return(handles[i]);

   return(INVALID_HANDLE);
  }

Hier haben wir ein paar neue Methoden. Der Konstruktor ermöglicht die Bereitstellung des RSI-Zeitraums und speichert ihn als Mitgliedsvariable. Die Init-Methode wird für die Erstellung der Handles des RSI-Indikators genutzt. Diese Handles werden im handles[]-Array gespeichert. Die GetValue-Methode kopiert den letzten Wert aus dem RSI-Puffer und gibt ihn aus. Die private GetHandle-Methode wird genutzt, um das korrekte Indikator-Handle im handles[]-Array zu finden. GetName ist selbsterklärend.

Wie wir sehen können, ist das Erstellen von Panel-Komponenten ziemlich einfach. Auf die gleiche Art können wir Komponenten für die meisten anderen benutzerdefinierten Bedingungen erstellen. Es muss nicht zwangsläufig der Indikatorwert sein. Im Folgenden finden Sie eine benutzerdefinierte Bedingung auf Basis von SMA. Sie prüft, ob der aktuelle Preis über dem gleitenden Mittelwert liegt, und zeigt 'Yes' oder 'No' an.

//+------------------------------------------------------------------+
//| CPriceMARow class                                                |
//+------------------------------------------------------------------+
class CPriceMARow : public CRow
  {
private:
   int               maPeriod; // period of moving average
   int               maShift;  // shift of moving average
   ENUM_MA_METHOD    maType;   // SMA, EMA, SMMA or LWMA
   string            symbols[];        // symbols array
   ENUM_TIMEFRAMES   timeframes[];     // timeframes array
   int               handles[];        // array of MA handles

   //--- finds the indicator handle for a given symbol and timeframe
   int               GetHandle(string symbol,ENUM_TIMEFRAMES tf);

public:
   //--- constructor
                     CPriceMARow(ENUM_MA_METHOD type,int period,int shift);

   //--- overrides default GetValue(..) method of CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   // overrides default GetName() method CRow
   virtual string    GetName();

   //--- overrides default Init(..) method from CRow
   virtual void      Init(string &symb[],ENUM_TIMEFRAMES &tfs[]);
  };
//+------------------------------------------------------------------+
//| CPriceMARow class constructor                                    |
//+------------------------------------------------------------------+
CPriceMARow::CPriceMARow(ENUM_MA_METHOD type,int period,int shift)
  {
   maPeriod= period;
   maShift = shift;
   maType=type;
  }
//+------------------------------------------------------------------+
//| Overrides default Init(..) method from CRow                      |
//+------------------------------------------------------------------+
void CPriceMARow::Init(string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   int size=ArraySize(symb);
   
   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);
   
//--- copies arrays contents into own arrays
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);
  
//--- gets MA handles for all used symbols or timeframes
   for(int i=0; i<ArraySize(symbols); i++)
      handles[i]=iMA(symbols[i],timeframes[i],maPeriod,maShift,maType,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method of CRow                    |
//+------------------------------------------------------------------+
string CPriceMARow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double value[1];
   MqlTick tick;

//--- obtains MA indicator handle
   int handle=GetHandle(symbol,tf);

   if(handle==INVALID_HANDLE) return("err");

//--- gets the last MA value
   if(CopyBuffer(handle,0,0,1,value)<0) return("-");
//--- gets the last price
   if(!SymbolInfoTick(symbol,tick)) return("-");

//--- checking the condition: price > MA
   if(tick.bid>value[0])
      return("Yes");
   else
      return("No");
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method of CRow                       |
//+------------------------------------------------------------------+
string CPriceMARow::GetName()
  {
   string name;

   switch(maType)
     {
      case MODE_SMA: name = "SMA"; break;
      case MODE_EMA: name = "EMA"; break;
      case MODE_SMMA: name = "SMMA"; break;
      case MODE_LWMA: name = "LWMA"; break;
     }

   return("Price>"+name+"("+IntegerToString(maPeriod)+")");
  }
//+------------------------------------------------------------------+
//| finds the indicator handle for a given symbol and timeframe      |
//+------------------------------------------------------------------+
int CPriceMARow::GetHandle(string symbol,ENUM_TIMEFRAMES tf)
  {
   for(int i=0; i<ArraySize(timeframes); i++)
      if(symbols[i]==symbol && timeframes[i]==tf)
         return(handles[i]);

   return(INVALID_HANDLE);
  }

Der Code ist länger, weil Moving Average drei Parameter hat: Zeitraum, Verschiebung und Typ. GetName ist etwas komplizierter, da diese Methode basierend auf MA-Typ und -Zeitraum den Namen aufbaut. GetValue funktioniert fast auf die gleiche Weise wie bei CRSIRow, doch anstatt den Indikatorwert auszugeben, gibt die Methode 'Yes' aus, wenn der Preis über dem SMA liegt, oder 'No', wenn er darunter liegt.

Das letzte Beispiel ist etwas komplexer. Es handelt sich um die CPriceChangeRow-Klasse, die die Preisänderung des aktuellen Bars anzeigt. Sie funktioniert auf drei Arten:

Der Code sieht so aus:

//+------------------------------------------------------------------+
//| CPriceChangeRow class                                            |
//+------------------------------------------------------------------+
class CPriceChangeRow : public CRow
  {
private:
   bool              percentChange;
   bool              useArrows;

public:
   //--- constructor
                     CPriceChangeRow(bool arrows,bool percent=false);

   //--- overrides default GetName() method from CRow
   virtual string    GetName();

   //--- overrides default GetFont() method from CRow
   virtual string    GetFont(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetValue(..) method from CRow
   virtual string    GetValue(string symbol,ENUM_TIMEFRAMES tf);

   //--- overrides default GetColor(..) method from CRow
   virtual color     GetColor(string symbol,ENUM_TIMEFRAMES tf);

  };
//+------------------------------------------------------------------+
//| CPriceChangeRow class constructor                                |
//+------------------------------------------------------------------+
CPriceChangeRow::CPriceChangeRow(bool arrows,bool percent=false)
  {
   percentChange=percent;
   useArrows=arrows;
  }
//+------------------------------------------------------------------+
//| Overrides default GetName() method from CRow                     |
//+------------------------------------------------------------------+
 string CPriceChangeRow::GetName()
  {
   return("PriceChg");
  }
//+------------------------------------------------------------------+
//| Overrides default GetFont() method from CRow                     |
//+------------------------------------------------------------------+
string CPriceChangeRow::GetFont(string symbol,ENUM_TIMEFRAMES tf)
  {
//--- we use Wingdings font to draw arrows (up/down)
   if(useArrows)
      return("Wingdings");
   else
      return("Arial");
  }
//+------------------------------------------------------------------+
//| Overrides default GetValue(..) method from CRow                  |
//+------------------------------------------------------------------+
string CPriceChangeRow::GetValue(string symbol,ENUM_TIMEFRAMES tf)
  {
   double close[1];
   double open[1];

//--- gets open and close of current bar
   if(CopyClose(symbol,tf,0, 1, close) < 0) return(" ");
   if(CopyOpen(symbol, tf, 0, 1, open) < 0) return(" ");

//--- current bar price change
   double change=close[0]-open[0];

   if(useArrows)
     {
      if(change > 0) return(CharToString(233)); // returns up arrow code
      if(change < 0) return(CharToString(234)); // returns down arrow code
      return(" ");
        }else{
      if(percentChange)
        {
         //--- calculates percent change
         return(DoubleToString(change/open[0]*100.0,3)+"%");
           }else{
         return(DoubleToString(change,(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
        }
     }
  }
//+------------------------------------------------------------------+
//| Overrides default GetColor(..) method from CRow                  |
//+------------------------------------------------------------------+
color CPriceChangeRow::GetColor(string symbol,ENUM_TIMEFRAMES tf)
  {
   double close[1];
   double open[1];

//--- gets open and close of current bar
   if(CopyClose(symbol,tf,0, 1, close) < 0) return(clrWhite);
   if(CopyOpen(symbol, tf, 0, 1, open) < 0) return(clrWhite);

   if(close[0] > open[0]) return(clrLime);
   if(close[0] < open[0]) return(clrRed);
   return(clrWhite);
  }

Der Konstruktor hat zwei Parameter. Der erste entscheidet, ob Pfeile angezeigt werden. Falls er true ist, wird der zweite Parameter verworfen. Falls er false ist, entscheidet der zweite Parameter, ob Preisänderungen absolut oder in Prozent angezeigt werden.

Bei dieser Klasse habe ich mich für das Überschreiben von vier Methoden von CRow entschieden: GetName, GetValue, GetColor und GetFont. GetName ist die einfachste und gibt nur den Namen aus. GetFont wird verwendet, weil es uns die Möglichkeit gibt, Pfeile oder andere Zeichen aus der Schriftart Wingdings zu nutzen. GetColor gibt die Farbe Lime aus, wenn der Preis steigt, und Red, wenn er fällt. Falls der Preis unverändert bleibt oder Fehler auftreten, wird die Farbe White ausgegeben. GetValue ruft die Öffnungs- und Schließungspreise des letzten Bars ab, berechnet die Differenz und gibt diese aus. Im Pfeil-Modus gibt er Wingdings-Zeichencodes von Pfeilen nach oben und unten aus.


5. Wie das alles genutzt wird

Um das Panel zu nutzen, müssen wir einen neuen Indikator erstellen. Wir nennen ihn TableSample.

Die Ereignisse, die wir verarbeiten müssen, sind:

Wir brauchen auch einen Pointer zum CTable-Objekt, das dynamisch in OnInit() erstellt wird. Zuerst müssen wir entscheiden, welchen Modus wir nutzen werden (Multi-Timeframe oder Mehrwährungsmodus). Das folgende Codebeispiel zeigt den Mehrwährungsmodus, aber alles, was für den Multi-Timeframe-Modus benötigt wird, ist ebenfalls in den Kommentaren enthalten. Für den Mehrwährungsmodus müssen wir einen Bereich von Symbolen erstellen und an den CTable-Konstruktor übergeben. Für den Multi-Timeframe-Modus würden wir einen Bereich von Timeframes erstellen und an den zweiten CTable-Konstruktor übergeben.

Anschließend müssen wir alle erforderlichen Komponenten erstellen und mithilfe der AddRow-Methode zum Panel hinzufügen. Parameter des Panels können bei Bedarf angepasst werden. Danach müssen wir das Panel zum ersten Mal zeichnen, also rufen wir Update am Ende von OnInit() auf. OnDeinit ist simpel. Diese Methode löscht nur das CTable-Objekt, wodurch der CTable-Destruktor aufgerufen wird.

OnCalculate(...) und OnChartEvent(...) sind identisch. Sie rufen nur die Update-Methode auf. OnChartEvent(...) ist nur erforderlich, wenn das Panel im Mehrwährungsmodus genutzt wird. In diesem Modus verarbeitet diese Methode Ereignisse, die von SpyAgents aufgerufen werden. Im Multi-Timeframe-Modus wird nur OnCalculate(...) benötigt, weil wir nur das aktuelle Diagrammsymbol überwachen müssen.

//+------------------------------------------------------------------+
//|                                                  TableSample.mq5 |
//|                                                 Marcin Konieczny |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Marcin Konieczny"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0

#include <Table.mqh>
#include <PriceRow.mqh>
#include <PriceChangeRow.mqh>
#include <RSIRow.mqh>
#include <PriceMARow.mqh>

CTable *table; // pointer to CTable object
//+------------------------------------------------------------------+
//| Indicator initialization function                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- timeframes used in table (in multi-timeframe mode)
   ENUM_TIMEFRAMES timeframes[4]={PERIOD_M1,PERIOD_H1,PERIOD_D1,PERIOD_W1};

//--- symbols used in table (in multi-currency mode)
   string symbols[4]={"EURUSD","GBPUSD","USDJPY","AUDCHF" };
//-- CTable object creation 
//   table = new CTable(timeframes); // multi-timeframe mode
   table=new CTable(symbols); // multi-currency mode

//--- adding rows to the table
   table.AddRow(new CPriceRow());               // shows current price
   table.AddRow(new CPriceChangeRow(false));     // shows change of price in the last bar
   table.AddRow(new CPriceChangeRow(false,true)); // shows percent change of price in the last bar
   table.AddRow(new CPriceChangeRow(true));      // shows change of price as arrows
   table.AddRow(new CRSIRow(14));                // shows RSI(14)
   table.AddRow(new CRSIRow(10));                // shows RSI(10)
   table.AddRow(new CPriceMARow(MODE_SMA,20,0));  // shows if SMA(20) > current price

//--- setting table parameters
   table.SetFont("Arial",10,clrYellow);  // font, size, color
   table.SetCellSize(60, 20);           // width, height
   table.SetDistance(10, 10);           // distance from upper right chart corner

   table.Update(); // forces table to redraw

   return(0);
  }
//+------------------------------------------------------------------+
//| Indicator deinitialization function                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- calls table destructor and frees memory
   delete(table);
  }
//+------------------------------------------------------------------+
//| Indicator iteration function                                     |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
//--- update table: recalculate/repaint
   table.Update();
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnChartEvent handler                                             |
//| Handles CHARTEVENT_CUSTOM events sent by SpyAgent indicators     |
//| nedeed only in multi-currency mode!                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   table.Update(); // update table: recalculate/repaint
  }
//+------------------------------------------------------------------+

Nachdem wir diesen Indikator an das Diagramm angehängt haben, wird er aktualisiert und anschließend sehen wir, dass das Panel funktioniert.


6. Installation

Alle Dateien müssen kompiliert werden. SpyAgent und TableSample sind Indikatoren und müssen nach Datenordner_des_Terminals\MQL5\Indicators kopiert werden. Die restlichen Dateien sind Include-Dateien und müssen in Datenordner_des_Terminals\MQL5\Include abgelegt werden. Um das Panel auszuführen, hängen Sie den Indikator TableSample an ein beliebiges Diagramm an. SpyAgents müssen nicht angehängt werden. Sie werden automatisch gestartet.


Fazit

Dieser Beitrag erklärt die objektorientierte Implementierung eines Multi-Timeframe- und Mehrwährungs-Panels für MetaTrader 5. Er zeigt, wie man ein Design schafft, das sich leicht erweitern lässt und den Aufbau benutzerdefinierter Panels mit wenig Aufwand ermöglicht.

Alle in diesem Beitrag vorgestellten Codes können im Anhang heruntergeladen werden.