PairPlot basiert auf CGraphic dient der Analyse von Korrelationen zwischen Datenarrays (Zeitreihen)

Dmitriy Gizlyk | 4 Oktober, 2018

Inhalt

Einführung

Wie wir alle wissen, führen Devisenhändler Transaktionen auf Basis der Kurse von Währungspaaren durch, bei denen der Währungswert eines Landes in der Währung eines anderen Landes ausgedrückt wird. Es ist leicht zu erkennen, dass sich die gleiche Währung in mehreren Währungspaaren befindet. Da der Wert einer Währung durch den wirtschaftlichen Zustand ihres Landes bestimmt wird, können wir fragen, ob sich Veränderungen in der Wirtschaft des Landes in verschiedenen Währungspaaren einheitlich auf ihre Währung auswirken. Eine positive Antwort scheint hier unvermeidlich. Aber es ist nur in einem perfekten Fall gerechtfertigt, wenn sich die Wirtschaftslage nur eines Landes ändert. Die Wahrheit des Lebens ist, dass sich unsere Welt ständig verändert. Eine Veränderung der Wirtschaft eines Landes bedeutet direkt oder indirekt eine Veränderung der Weltwirtschaft.

Im Internet findet man viele Informationen über die Analyse der Entwicklung eines Währungspreises innerhalb verschiedener Paare und die Suche nach Korrelationen zwischen verschiedenen Währungspaaren. Auf dieser Website gibt es Artikel über den Handel mit Währungskörben[1, 2, 3]. Dennoch bleibt die Frage der Analyse der Korrelationen zwischen den Zeitreihen offen. In diesem Artikel schlage ich vor, ein Werkzeug zur grafischen Analyse von Korrelationen zwischen Zeitreihen zu entwickeln, das es ermöglicht, Korrelationen zwischen Zeitreihen von Kursen analysierter Währungspaare zu visualisieren.


1. Erklärung des Problems

Bevor wir anfangen zu arbeiten, definieren wir unsere Ziele. Welche Art von Werkzeug wollen wir am Ende erhalten? Zunächst einmal sollte es sich um ein grafisches Panel handeln, das grafische Darstellungen von Korrelationen zwischen vergangenen Zeitreihen enthält. Das Tool sollte vielseitig genug sein und mit einer unterschiedlichen Anzahl von Zeitreihen arbeiten können.

Um die Zeitreihen auf dem Panel zu analysieren, werden wir ein Verteilungshistogramm für jede Zeitreihe erstellen. Wir werden auch Streudiagramme erstellen, die paarweise für analysierte Zeitreihen angezeigt werden, um nach einer Korrelation zu suchen. Trendlinien werden in Streudiagrammen als visuelle Referenz hinzugefügt.

Die Anordnung der Diagramme in Form einer Kreuztabelle verbessert die Lesbarkeit des gesamten Werkzeugs. Dieser Ansatz vereint die Datenpräsentation und vereinfacht die visuelle Wahrnehmung. Das Layout des vorgeschlagenen Tools ist unten aufgeführt.

Layout


2. Erstellen der Basisklassen

2.1. "Die Basis"

Bei der Entwicklung eines solchen Instruments sollten wir bedenken, dass die Nutzer mit einer unterschiedlichen Anzahl von Handelsinstrumenten arbeiten können. Ich glaube, die perfekte visuelle Lösung ist eine Darstellung in einem Block, bei der Standardgrafiken als "Bausteine" zum Aufbau einer gemeinsamen Korrelationstabelle verwendet werden.

Wir werden mit der Entwicklung unseres Tools beginnen, indem wir die Grundlage für die Erstellung von Charts schaffen. Die Grundausstattung des MetaTrader 5 beinhaltet die Klasse CGraphic, die für die Erstellung wissenschaftlicher Grafiken gedacht ist. Dieser Artikel[4] beschreibt diese Klasse ausführlich. Wir werden es als Grundlage für die Erstellung unserer Grafiken verwenden. Erstellen wir die Basisklasse CPlotBase und weisen wir sie als diejenige zu, die von der CGraphic Standardklasse abgeleitet ist. In dieser Klasse werden wir Methoden zur Erstellung einer Grafik-Leinwand erstellen. Es gibt zwei solche Methoden: die erste ist das Plotten eines quadratischen Diagrammfeldes mit einer gegebenen Seitenabmessung und die zweite ist das Konstruieren eines rechteckigen Bereichs mit vorgegebenen Koordinaten. Wir werden auch die Methoden zur Anzeige eines Textes auf den Seiten der Grafik hinzufügen (sie helfen uns bei der Anzeige der Namen der Instrumente). Außerdem fügen wir das Verfahren zum Ändern der Anzeigefarbe im Zeitreihendiagramm hinzu.

Wir sollten auch bedenken, dass die verwendete Basisklasse CGraphic nicht von der Klasse CObject abgeleitet ist und keine Methoden zum Verschieben, Verstecken und Anzeigen eines Objekts in einem Graphen enthält. Ähnliche Methoden werden in grafischen Panels häufig eingesetzt. Daher müssen wir diese Methoden zur erstellten Klasse hinzufügen, um die Kompatibilität unseres Tools mit den Standardklassen für den Aufbau von grafischen Panels zu gewährleisten.

class CPlotBase : public CGraphic
  {
protected:
   long              m_chart_id;                // chart ID
   int               m_subwin;                  // chart subwindow

public:
                     CPlotBase();
                    ~CPlotBase();
//--- Objekterstellen
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int size);
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
//--- Farbwechsel der Bezeichnung der Zeitreihen
   virtual bool      SetTimeseriesColor(uint clr, uint timeserie=0);
//--- Text auf den Chart schreiben
   virtual void      TextUp(string text, uint clr);
   virtual void      TextDown(string text, uint clr);
   virtual void      TextLeft(string text, uint clr);
   virtual void      TextRight(string text, uint clr);
//--- Geometrie
   virtual bool      Shift(const int dx,const int dy);
//--- Status
   virtual bool      Show(void);
   virtual bool      Hide(void);
  };

Im Konstruktor der Klasse entfernen wir die Darstellung der Legende und setzen die Mindestanzahl von Kennzeichnungen der Achsen.

CPlotBase::CPlotBase()
  {
   HistoryNameWidth(0);
   HistorySymbolSize(0);
   m_x.MaxLabels(3);
   m_y.MaxLabels(3);
  }

Der gesamte Code aller Klassenmethoden befindet sich in der Anlage.

2.2. Streudiagramm

Als nächstes entwickeln wir die Klasse CScatter für die Darstellung der Streudiagramme. Diese Klasse wird nur zwei Methoden zum Erstellen und Aktualisieren der Zeitreihendaten enthalten.

class CScatter : public CPlotBase
  {

public:
                     CScatter();
                    ~CScatter();
//---
   int               AddTimeseries(const double &timeseries_1[],const double &timeseries_2[]);
   bool              UpdateTimeseries(const double &timeseries_1[],const double &timeseries_2[],uint timeserie=0);

  };

Zwei Arrays der Zeitreihen der analysierten Instrumenten, auf denen das Streudiagramm basiert, werden der Methode AddTimeseries für die Erstellung der Kurve zu übergeben. Die Standardklasse CGraphic ist in der Lage, ein Punktdiagramm basierend auf zwei Datenfeldern anzuzeigen. Wir werden diese Funktion nutzen. Zu Beginn der Methode erstellen wir eine Punktkurve, die auf zwei Datenfeldern basiert. Wenn die Kurvenerstellung fehlschlägt, verlassen wir die Funktion mit dem Ergebnis "-1". Wenn die Kurve erfolgreich erstellt wurde, stellen wir die Größe der Kurvenpunkte ein und setzen das Trendlinienanzeige-Flag. Nachdem alle Operationen durchgeführt wurden, gibt das Verfahren den Index der erstellten Kurve zurück.

int CScatter::AddTimeseries(const double &timeseries_1[],const double &timeseries_2[])
  {
   CCurve *curve=CGraphic::CurveAdd(timeseries_1,timeseries_2,CURVE_POINTS);
   if(curve==NULL)
      return -1;
   curve.PointsSize(2);
   curve.TrendLineVisible(true);
   return (m_arr_curves.Total()-1);
  }

Um die Kurvendaten zu aktualisieren, erstellen wir die Methode UpdateTimeseries. Zwei Datenfelder zur Erstellung der Kurve und des Index der Kurve, deren Daten geändert werden sollen, werden ihr übergeben. Wir überprüfen zu Beginn der Funktion die Gültigkeit der angegebenen Kurvennummer. Wenn eine falsche Nummer angegeben wurde, verlassen wir die Funktion mit 'false'.

Dann vergleichen wir dann die Größe der empfangenen Zeitreihen. Wenn die Größen der Arrays unterscheiden oder die Arrays leer sind, beenden wir die Funktion mit 'false'.

Der nächste Schritt ist die Spezifikation des Pointers auf ein Kurvenobjekt nach Index. Ist der Pointer falsch, beenden wir die Funktion mit 'false'.

Nach allen Prüfungen übergeben wir die Zeitreihe an die Kurve und beenden die Funktion mit 'true'.

bool CScatter::UpdateTimeseries(const double &timeseries_1[],const double &timeseries_2[], uint timeserie=0)
  {
   if((int)timeserie>=m_arr_curves.Total())
      return false;
   if(ArraySize(timeseries_1)!=ArraySize(timeseries_2) || ArraySize(timeseries_1)==0)
      return false;
//---
   CCurve *curve=m_arr_curves.At(timeserie);
   if(CheckPointer(curve)==POINTER_INVALID)
      return false;
//---
   curve.Update(timeseries_1,timeseries_2);
//---
   return true;
  }

2.3. Histogramme

Das Histogramm ist ein weiterer "Baustein" für den Aufbau unseres Werkzeugs. Wir benötigen die Klasse CHistogram, um sie zu erstellen. Wie CScatter erhält die Klasse ihre eigenen Methoden zur Erzeugung und Aktualisierung von Kurvendaten. Im Gegensatz zu ihrem Vorgänger wendet die aktuelle Klasse jedoch eine Zeitreihe an, um die Kurve zu erstellen. Die Prinzipien der Konstruktion dieser Methoden sind vergleichbar mit den Methoden der vorherigen Klasse.

Wir dürfen nicht vergessen, dass die Basisklasse CGraphic das Histogramm nur in seiner Standardform aufbauen kann. Um die Möglichkeit der Erstellung eines vertikalen Histogramms vom Typ Marktprofil hinzuzufügen, müssen wir die Methode HistogramPlot neu schreiben. Außerdem sollten wir die Variable e_orientation zum Speichern des Histogramm-Konstruktionstyps hinzufügen und die Methoden zur Erstellung der Hintergründe der Graphen neu schreiben, indem wir ihnen die Möglichkeit hinzufügen, den Histogrammtyp anzugeben.

Ein weiterer Unterschied zwischen unserer Klasse und der Basisklasse in CGpraphic ist die Art der Ausgangsdaten, die wir erhalten. In der Basisklasse wird ein Array von erhaltenen Werten für die direkte Ausgabe in das Diagramm verwendet. Unsere Klasse erhält eine Zeitreihe, und bevor sie das Histogramm verarbeitet, ist es notwendig, die erhaltenen Daten zu verarbeiten. Die Vorbereitung der Daten für die Erstellung des Histogramms erfolgt durch die Methode CalculateHistogramArray, die Spaltenzahl des Histogramms wird durch die Methode SetCells festgelegt und in der Variablen i_cells gespeichert.

class CHistogram : public CPlotBase
  {
private:
   ENUM_HISTOGRAM_ORIENTATION    e_orientation;
   uint                          i_cells;

public:
                                 CHistogram();
                                ~CHistogram();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int size, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);
   int               AddTimeserie(const double &timeserie[]);
   bool              UpdateTimeserie(const double &timeserie[],uint timeserie=0);
   bool              SetCells(uint value)    {  i_cells=value; }

protected:
   virtual void      HistogramPlot(CCurve *curve);
   bool              CalculateHistogramArray(const double &data[],double &intervals[],double &frequency[], 
                                             double &maxv,double &minv);
};

Die Methode CalculateHistogramArray basiert auf dem Algorithmus, der in der MQL5 Referenz mit einem kleinen Zusatz angeboten wird. Zu Beginn des Verfahrens überprüfen wir die Angemessenheit der Anfangsdaten für die Darstellung des Histogramms, bestimmen die Minimal- und Maximalwerte, berechnen Sie die Bereichsbreite jedes Intervalls und bereiten Arrays zum Speichern von Intervallen und Frequenzen vor.

Danach werden die Zentren der Intervalle in der Schleife gesetzt und das Frequenzarray auf Null gesetzt.

In der nächsten Schleife durchlaufen wir die Zeitreihe und zählen die Werte, die dem entsprechenden Intervall entsprechen.

Schließlich normieren wir die Frequenzen, die die genannten Anpassungen in Prozent der Gesamtzahl der Elemente in der Zeitreihe umwandeln.

bool CHistogram::CalculateHistogramArray(const double &data[],double &intervals[],double &frequency[], 
                             double &maxv,double &minv) 
  { 
   int size=ArraySize(data); 
   if(size<(int)i_cells*10) return (false); 
   minv=data[ArrayMinimum(data)]; 
   maxv=data[ArrayMaximum(data)]; 
   double range=maxv-minv; 
   double width=range/i_cells; 
   if(width==0) return false; 
   ArrayResize(intervals,i_cells); 
   ArrayResize(frequency,i_cells); 
//--- Setzen der Zentren der Intervalle 
   for(uint i=0; i<i_cells; i++) 
     { 
      intervals[i]=minv+(i+0.5)*width; 
      frequency[i]=0; 
     } 
//--- Ausfüllen des Intervalls mit den passenden Häufigkeiten 
   for(int i=0; i<size; i++) 
     { 
      uint ind=int((data[i]-minv)/width); 
      if(ind>=i_cells) ind=i_cells-1; 
      frequency[ind]++; 
     } 
//--- Normalisierung der Häufigkeiten in Prozentsätze
   for(uint i=0; i<i_cells; i++) 
      frequency[i]*=(100.0/(double)size); 
   return (true); 
  } 

Das Histogramm wird mit der Methode HistogramPlot auf dem Diagramm dargestellt. Diese Funktion basiert auf dem Algorithmus der Basisklasse CGraphic, der für die Verwendung von Zeitreihen und die Histogramm-Konstruktionsorientierung korrigiert wurde.

Zu Beginn bereiten wir des Verfahrens die Daten für die Darstellung des Histogramms vor. Dazu erhalten wir eine Zeitreihe aus den Kurvendaten und rufen die Methode CalculateHistogramArray auf. Nachdem die Funktion erfolgreich ausgeführt wurde, erhalten wir die Breite der Histogrammblöcke und überprüfen die Größe des Konstruktionsdaten-Arrays.

Anschließend formatieren wir die Werte nach Achsen entsprechend der Darstellungsart des Histogramms.

Schließlich ordnen wir die Schleife für die Anzeige der Diagrammspalten im Grafikfeld an.

CHistogram::HistogramPlot(CCurve *curve)
  {
   double data[],intervals[],frequency[];
   double max_value, min_value;
   curve.GetY(data);
   if(!CalculateHistogramArray(data,intervals,frequency,max_value,min_value))
      return;
//--- Parameter des Histogramms
   int histogram_width=fmax(curve.HistogramWidth(),2);
//--- check
   if(ArraySize(frequency)==0 || ArraySize(intervals)==0)
      return;
//---
   switch(e_orientation)
     {
      case HISTOGRAM_HORIZONTAL:
        m_y.AutoScale(false);
        m_x.Min(intervals[ArrayMinimum(intervals)]);
        m_x.Max(intervals[ArrayMaximum(intervals)]);
        m_x.MaxLabels(3);
        m_x.ValuesFormat("%.0f");
        m_y.Min(0);
        m_y.Max(frequency[ArrayMaximum(frequency)]);
        m_y.ValuesFormat("%.2f");
        break;
      case HISTOGRAM_VERTICAL:
        m_x.AutoScale(false);
        m_y.Min(intervals[ArrayMinimum(intervals)]);
        m_y.Max(intervals[ArrayMaximum(intervals)]);
        m_y.MaxLabels(3);
        m_y.ValuesFormat("%.0f");
        m_x.Min(0);
        m_x.Max(frequency[ArrayMaximum(frequency)]);
        m_x.ValuesFormat("%.2f");
        break;
     }
//---
   CalculateXAxis();
   CalculateYAxis();
//--- Berechnen des Originals von y
   int originalY=m_height-m_down;
   int originalX=m_width-m_right;
   int yc0=ScaleY(0.0);
   int xc0=ScaleX(0.0);
//--- Abfrage der Kurvenfarbe
   uint clr=curve.Color();
//--- Zeichnen 
   for(uint i=0; i<i_cells; i++)
     {
      //--- Prüfen der Koordinaten
      if(!MathIsValidNumber(frequency[i]) || !MathIsValidNumber(intervals[i]))
         continue;
      if(e_orientation==HISTOGRAM_HORIZONTAL)
        {
         int xc=ScaleX(intervals[i]);
         int yc=ScaleY(frequency[i]);
         int xc1 = xc - histogram_width/2;
         int xc2 = xc + histogram_width/2;
         int yc1 = yc;
         int yc2 = (originalY>yc0 && yc0>0) ? yc0 : originalY;
         //---
         if(yc1>yc2)
            yc2++;
         else
            yc2--;
         //---
         m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
        }
      else
        {
         int yc=ScaleY(intervals[i]);
         int xc=ScaleX(frequency[i]);
         int yc1 = yc - histogram_width/2;
         int yc2 = yc + histogram_width/2;
         int xc1 = xc;
         int xc2 = (originalX>xc0 && xc0>0) ? xc0 : originalX;
         //---
         if(xc1>xc2)
            xc2++;
         else
            xc2--;
         //---
         m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
        }
     }
//---
  }

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.

2.4. Die Klasse für die Arbeit mit Zeitreihen

Um das Tool zu entwickeln, benötigen wir noch einen weiteren "Baustein", der die benötigten historischen Daten herunterlädt und die Zeitreihen für die grafische Darstellung vorbereitet. Diese Arbeiten werden in der Klasse CTimeserie durchgeführt. Der Name des Handelsinstruments, der Zeitrahmen und der angewandte Preis sind bei der Initialisierung der Klasse zu übergeben. Außerdem sollen die Methoden zur nachträglichen Änderung des Namens des Handelsinstruments, des Zeitrahmens, des Anwendungspreises und der Historientiefe erstellt werden.

class CTimeserie :  public CObject
  {
protected:
   string               s_symbol;
   ENUM_TIMEFRAMES      e_timeframe;
   ENUM_APPLIED_PRICE   e_price;
   double               d_timeserie[];
   int                  i_bars;
   datetime             dt_last_load;
   
public:
                     CTimeserie(void);
                    ~CTimeserie(void);
   bool              Create(const string symbol=NULL, const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
//--- Ändern der Einstellungen der Zeitreihes
   void              SetBars(const int value)            {  i_bars=value;  }
   void              Symbol(string value)                {  s_symbol=value;      dt_last_load=0;  }
   void              Timeframe(ENUM_TIMEFRAMES value)    {  e_timeframe=value;   dt_last_load=0;  }
   void              Price(ENUM_APPLIED_PRICE value)     {  e_price=value;       dt_last_load=0;  }
//---
   string            Symbol(void)                        {  return s_symbol;     }
   ENUM_TIMEFRAMES   Timeframe(void)                     {  return e_timeframe;  }
   ENUM_APPLIED_PRICE Price(void)                        {  return e_price;      }
//--- Laden der Daten
   virtual bool      UpdateTimeserie(void);
   bool              GetTimeserie(double &timeserie[])   {  return ArrayCopy(timeserie,d_timeserie)>0;   }
  };

Die wichtigsten Datenaufbereitungsarbeiten werden in der Methode UpdateTimeserie durchgeführt. Überprüfen wir zu Beginn der Methode, ob die notwendigen Informationen zuvor auf der aktuellen Kerze geladen wurden. Wenn die Informationen bereit sind, verlassen wir die Funktion mit 'true'. Wenn es notwendig ist, die Daten aufzubereiten, laden wir die erforderliche Historie entsprechend dem angegebenen Preis. Wenn es nicht möglich ist, die Informationen zuladen, verlassen wir die Funktion mit 'false'. Vergessen wir aber nicht, dass wir nicht am Preis selbst interessiert sind, sondern an seiner Veränderung. Daher überschreiten die zu ladende Daten der Historie den angegebenen Betrag um 1 Bar. Im nächsten Schritt berechnen wir die Preisänderung für jeden Bar neu und speichern sie in einer Schleife im Array. Später kann ein Nutzer diese Informationen mit der Methode GetTimeserie abrufen.

bool CTimeserie::UpdateTimeserie(void)
  {
   datetime cur_date=(datetime)SeriesInfoInteger(s_symbol,e_timeframe,SERIES_LASTBAR_DATE);
   if(dt_last_load>=cur_date && ArraySize(d_timeserie)>=i_bars)
      return true;
//---
   MqlRates rates[];
   int bars=0,i;
   double data[];
   switch(e_price)
     {
      case PRICE_CLOSE:
        bars=CopyClose(s_symbol,e_timeframe,1,i_bars+1,data);
        break;
      case PRICE_OPEN:
        bars=CopyOpen(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_HIGH:
        bars=CopyHigh(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_LOW:
        bars=CopyLow(s_symbol,e_timeframe,1,i_bars+1,data);
      case PRICE_MEDIAN:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low)/2;
        break;
      case PRICE_TYPICAL:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+rates[i].close)/3;
        break;
      case PRICE_WEIGHTED:
        bars=CopyRates(s_symbol,e_timeframe,1,i_bars+1,rates);
        bars=ArrayResize(data,bars);
        for(i=0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+2*rates[i].close)/4;
        break;
     }
//---
   if(bars<=0)
      return false;
//---
   dt_last_load=cur_date;
//---
   if(ArraySize(d_timeserie)!=(bars-1) && ArrayResize(d_timeserie,bars-1)<=0)
      return false;
   double point=SymbolInfoDouble(s_symbol,SYMBOL_POINT);
   for(i=0;i<bars-1;i++)
      d_timeserie[i]=(data[i+1]-data[i])/point;
//---
   return true;
  }

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.


3. Zusammensetzen von PairPlot

Nachdem wir die "Bausteine" erstellt haben, können wir mit der Entwicklung unseres Tools beginnen. Erstellen wir die Klasse CPairPlot, die von der Klasse CWndClient abgeleitet ist. Dieser Ansatz wird es einfacher machen, unser Tool in grafischen Panels zu verwenden, die mit der Standardklasse CAppDialog erstellt wurden (die Details zu seiner Anwendung befinden sich in den Artikeln[5,6]).

Deklarieren wir jetzt im Block 'private' unserer Klasse das Array der Pointer auf die Objekte CPlotBase zum Speichern von Pointer auf die Graphen, das Klassenobjekt CArrayObj zum Speichern von Pointer auf Zeitreihenobjekte sowie die Variablen zum Speichern des angewandten Zeitrahmens, des Preises, der Histogrammausrichtung, der Historientiefe und -farbe zum Anzeigen des Instrumentennamens im Diagramm.

class CPairPlot : public CWndClient
  {
private:
   CPlotBase                    *m_arr_graphics[];
   CArrayObj                     m_arr_symbols;
   ENUM_TIMEFRAMES               e_timeframe;
   ENUM_APPLIED_PRICE            e_price;
   int                           i_total_symbols;
   uint                          i_bars;
   ENUM_HISTOGRAM_ORIENTATION    e_orientation;
   uint                          i_text_color;
      
public:
                     CPairPlot();
                    ~CPairPlot();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10, const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   bool              Refresh(void);
   bool              HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value);
   ENUM_HISTOGRAM_ORIENTATION    HistogramOrientation(void)    {  return e_orientation;   }
   bool              SetTextColor(color value);
//--- Geometrie
   virtual bool      Shift(const int dx,const int dy);
//--- Status
   virtual bool      Show(void);
   virtual bool      Hide(void);
  };

Deklarieren wir die Klassenmethoden im Block 'public'. Die Klasseninitialisierung wird von der Methode Create durchgeführt, die bei ihrem Aufruf die Graph-ID, den Objektnamen, den angewandten Subfensterindex, die Konstruktionskoordinaten, die Anordnung der verwendeten Symbole, die angewandten Zeitrahmen, den Preis, die Historientiefe und die Anzahl der Histogrammspalten empfängt.

Überprüfen wir zu Beginn der Methode das Array der übergebenen Gerätenamen und die Tiefe der angegebenen Historie. Wenn sie nicht unseren Mindestanforderungen entsprechen, verlassen wir die Funktion mit 'false'. Dann speichern wir die Werte der Eingabeparameter für die grafische Darstellung.

bool CPairPlot::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2, const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10, const ENUM_APPLIED_PRICE price=PRICE_CLOSE)
  {
   i_total_symbols=0;
   int total=ArraySize(symbols);
   if(total<=1 || bars<100)
      return false;
//---
   e_timeframe=timeframe;
   i_bars=bars;
   e_price=price;

Als nächstes erstellen wir in einer Schleife Instanzen der Klasse CTimeserie für jedes Handelsinstrument. Wenn für jedes angegebene Handelsinstrument eine Zeitreihe nicht erstellt werden konnte, verlassen wir die Funktion mit 'false'.

   for(int i=0;i<total;i++)
     {
      CTimeserie *temp=new CTimeserie;
      if(temp==NULL)
         return false;
      temp.SetBars(i_bars);
      if(!temp.Create(symbols[i],e_timeframe,e_price))
         return false;
      if(!m_arr_symbols.Add(temp))
         return false;
     }
   i_total_symbols=m_arr_symbols.Total();
   if(i_total_symbols<=1)
      return false;

Nach erfolgreichem Abschluss der Vorarbeiten geht es weiter zur direkten Erstellung der grafischen Objekte. Rufen wir zunächst die Methode Create der übergeordneten Klasse auf. Dann passen wir die Größe des Arrays m_arr_graphics (zum Speichern von Zeigern auf Graphen) in Übereinstimmung mit der Anzahl der analysierten Instrumente an. Wir berechnen die Breite und Höhe jedes Diagramms basierend auf der Größe des gesamten Instruments und der Anzahl der analysierten Instrumente.

Danach ordnen wir zwei verschachtelte Schleifen an, um über alle analysierten Instrumente zu iterieren und die Tabelle mit grafischen Objekten zu erstellen. Wir erstellen jetzt die Histogramme an der Kreuzung von gleichnamigen Instrumenten und in anderen Fällen Streudiagrammen. Wenn alle Objekte erfolgreich angelegt wurden, verlassen wir die Methode mit 'true'.

   if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(ArraySize(m_arr_graphics)!=(i_total_symbols*i_total_symbols))
      if(ArrayResize(m_arr_graphics,i_total_symbols*i_total_symbols)<=0)
         return false;
   int width=Width()/i_total_symbols;
   int height=Height()/i_total_symbols;
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie1=m_arr_symbols.At(i);
      if(timeserie1==NULL)
         continue;
      for(int j=0;j<i_total_symbols;j++)
        {
         string obj_name=m_name+"_"+(string)i+"_"+(string)j;
         int obj_x1=m_rect.left+j*width;
         int obj_x2=obj_x1+width;
         int obj_y1=m_rect.top+i*height;
         int obj_y2=obj_y1+height;
         if(i==j)
           {
            CHistogram *temp=new CHistogram();
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2,e_orientation))
               return false;
            m_arr_graphics[i*i_total_symbols+j]=temp;
            temp.SetCells(cells);
           }
         else
           {
            CScatter *temp=new CScatter();
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2))
               return false;
            CTimeserie *timeserie2=m_arr_symbols.At(j);
            if(timeserie2==NULL)
               continue;
            m_arr_graphics[i*i_total_symbols+j]=temp;
           }
        }
     }
//---
   return true;
  }

Die Methode Refresh wird verwendet, um die Zeitreihendaten zu aktualisieren und Daten im Diagramm anzuzeigen. Am Anfang des Verfahrens steht die Schleife zur Aktualisierung der Daten aller Zeitreihen. Wir dürfen nicht vergessen, dass beim Erstellen eines Verteilungsgraphen Zeitreihen paarweise verwendet werden. Daher sollten die Daten vergleichbar sein. Um die Datenkompatibilität zu gewährleisten, werden grafische Objektdaten nicht aktualisiert und die Methode gibt 'false' zurück, falls bei der Aktualisierung mindestens einer der Zeitreihen ein Fehler auftritt.

Nach der Aktualisierung der Zeitreihendaten wird die Schleife zur Übergabe aktualisierter Zeitreihen an grafische Objekte eingerichtet. Achten wir darauf, dass die Update-Methode eines grafischen Objekts mit dem Parameter 'false' aufgerufen wird. Ein solcher Aufruf stellt sicher, dass das grafische Objekt aktualisiert wird, ohne den Graphen zu aktualisieren, auf dem die Anwendung läuft. Dieser Ansatz schließt Grafik-Updates nach der Aktualisierung jedes grafischen Objekts aus, wodurch die Belastung des Terminals reduziert und die Ausführungszeit der Funktionen verkürzt wird. Der Graph wird einmalig aktualisiert, nachdem alle grafischen Elemente aktualisiert wurden, bevor die Funktion beendet wird.

bool CPairPlot::Refresh(void)
  {
   bool updated=true;
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie=m_arr_symbols.At(i);
      if(timeserie==NULL)
         continue;
      updated=(updated && timeserie.UpdateTimeserie());
     }
   if(!updated)
      return false;
//---
   for(int i=0;i<i_total_symbols;i++)
     {
      CTimeserie *timeserie1=m_arr_symbols.At(i);
      if(CheckPointer(timeserie1)==POINTER_INVALID)
         continue;
      double ts1[];
      if(!timeserie1.GetTimeserie(ts1))
         continue;
//---
      for(int j=0;j<i_total_symbols;j++)
        {
         if(i==j)
           {
            CHistogram *temp=m_arr_graphics[i*i_total_symbols+j];
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            if(temp.CurvesTotal()==0)
              {
               if(temp.AddTimeserie(ts1)<0)
                  continue;
              }
            else
              {
               if(!temp.UpdateTimeserie(ts1))
                  continue;
              }
            if(!temp.CurvePlotAll())
               continue;
            if(i==0)
               temp.TextUp(timeserie1.Symbol(),i_text_color);
            if(i==(i_total_symbols-1))
               temp.TextDown(timeserie1.Symbol(),i_text_color);
            if(j==0)
               temp.TextLeft(timeserie1.Symbol(),i_text_color);
            if(j==(i_total_symbols-1))
               temp.TextRight(timeserie1.Symbol(),i_text_color);
            temp.Update(false);
           }
         else
           {
            CScatter *temp=m_arr_graphics[i*i_total_symbols+j];
            if(CheckPointer(temp)==POINTER_INVALID)
               return false;
            CTimeserie *timeserie2=m_arr_symbols.At(j);
            if(CheckPointer(timeserie2)==POINTER_INVALID)
               continue;
            double ts2[];
            if(!timeserie2.GetTimeserie(ts2))
               continue;
            if(temp.CurvesTotal()==0)
              {
               if(temp.AddTimeseries(ts1,ts2)<0)
                  continue;
              }
            else
               if(!temp.UpdateTimeseries(ts1,ts2))
                  continue;
            if(!temp.CurvePlotAll())
               continue;
            if(i==0)
               temp.TextUp(timeserie2.Symbol(),i_text_color);
            if(i==(i_total_symbols-1))
               temp.TextDown(timeserie2.Symbol(),i_text_color);
            if(j==0)
               temp.TextLeft(timeserie1.Symbol(),i_text_color);
            if(j==(i_total_symbols-1))
               temp.TextRight(timeserie1.Symbol(),i_text_color);
            temp.Update(false);
           }
        }
     }
//---
   ChartRedraw(m_chart_id);
//---
   return true;
  }

Ich habe bereits erwähnt, dass die grafischen Elemente auf der Klasse CGraphic basieren, die nicht von der Klasse CObject abgeleitet ist. Aus diesem Grund haben wir die Basisklasse CPlotBase um die Methoden Shift, Hide und Show erweitert. Aus dem gleichen Grund müssen wir auch die entsprechenden Methoden in der Klasse CPairPlot neu schreiben. Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.


4. Ein Beispiel für die Verwendung der Klasse CPairPlot

Jetzt, nach so viel Arbeit, ist es an der Zeit, einen Blick auf die Ergebnisse zu werfen. Um das Werkzeug in Aktion zu demonstrieren, machen wir einen Indikator, der die Korrelationsgraphen, z.B. für die letzten 1000 Kerzen, in jedem neuen Takt anzeigt.

Wie bereits oben erwähnt, ist das Tool für den Einsatz in grafischen Panels konzipiert. Erstellen wir daher zunächst die Klasse CPairPlotDemo, die von der Klasse CAppDialog abgeleitet ist. Details zur Arbeit mit der Klasse CAppDialog findet sich in den Artikeln[5, 6]. An dieser Stelle möchte ich nur auf die Besonderheiten der Nutzung des Tools hinweisen.

Wir deklarieren die Instanz der Klasse CPairPlot im Block 'private'. Im Block 'public' deklarieren wir die Methode Create mit allen Eingabeparametern, die für die Initialisierung und den Betrieb des Tools erforderlich sind. Hier werden wir auch die Methoden Refresh und HistogramOrientation deklarieren, die die entsprechenden Methoden unseres Tools aufrufen.

class CPairPlotDemo : public CAppDialog
  {
private:
   CPairPlot         m_PairPlot;
public:
                     CPairPlotDemo();
                    ~CPairPlotDemo();
//---
   bool              Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2,const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10);
   bool              Refresh(void);
//---
   bool              HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value)   {  return m_PairPlot.HistogramOrientation(value);   }
   ENUM_HISTOGRAM_ORIENTATION    HistogramOrientation(void)                   {  return m_PairPlot.HistogramOrientation();   }
   };

In der Methode Create rufen wir zunächst die entsprechende Methode der übergeordneten Klasse auf, rufen dann die gleiche Methode der Elementinstanz auf und fügen den Pointer auf die Instanz der Klasse CPairPlot zur Sammlung von Steuerelementen hinzu.

bool CPairPlotDemo::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2,const string &symbols[],const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, const int bars=1000, const uint cells=10)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
   if(!m_PairPlot.Create(m_chart_id,m_name+"PairPlot",m_subwin,0,0,ClientAreaWidth(),ClientAreaHeight(),symbols,timeframe,bars,cells))
      return false;
   if(!Add(m_PairPlot))
      return false;
//---
   return true;
  }

Erstellen wir jetzt den Indikator. Die Zeichenkette mit den kommagetrennten Namen der verwendeten Instrumente, die Tiefe der analysierten Historie in Balken, die Anzahl der Spalten und die Histogrammausrichtung sind als Eingabeparameter unserer Anzeige zu verwenden.

input string   i_Symbols   =  "EURUSD, GBPUSD, EURGBP";
input uint     i_Bars      =  1000;
input uint     i_Cells     =  50;
input ENUM_HISTOGRAM_ORIENTATION i_HistogramOrientation  =  HISTOGRAM_HORIZONTAL;

Deklarieren der Klasse CPairPlotDemo als globale Variable.

CPairPlotDemo     *PairPlot;

In der OnInit-Funktion erstellen wir das Array der angewandten Instrumente aus der Zeichenkette der externen Parameter des Indikators. Dann erstellen wir eine Instanz der Klasse CPairPlotDemo, übergeben ihr die angegebene Histogrammausrichtung und rufen ihre Methode Create auf. Nach erfolgreicher Initialisierung starten wir die Klassenausführung durch die Methode Run und aktualisieren die Daten mit der Methode Refresh.

int OnInit()
  {
//---
   string symbols[];
   int total=StringSplit(i_Symbols,',',symbols);
   if(total<=0)
      return INIT_FAILED;
   for(int i=0;i<total;i++)
     {
      StringTrimLeft(symbols[i]);
      StringTrimRight(symbols[i]);
     }
//---
   PairPlot=new CPairPlotDemo;
   if(CheckPointer(PairPlot)==POINTER_INVALID)
      return INIT_FAILED;
//---
   if(!PairPlot.HistogramOrientation(i_HistogramOrientation))
      return INIT_FAILED;
   if(!PairPlot.Create(0,"Pair Plot",0,20,20,620,520,symbols,PERIOD_CURRENT,i_Bars,i_Cells))
      return INIT_FAILED;
   if(!PairPlot.Run())
      return INIT_FAILED;
   PairPlot.Refresh();
//---
   return INIT_SUCCEEDED;
  }

In der Funktion OnCalculate rufen wir bei jedem neuen Takt die Methode Refresh auf. Die entsprechenden Klassenmethoden werden von den Funktionen OnChartEvent und OnDeinit aufgerufen.

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.

Unten sieht man, wie der Indikator arbeitet.

PairPlot Funktionsweise


Schlussfolgerung

In diesem Artikel haben wir ein recht interessantes Werkzeug vorgestellt. Es ermöglicht Händlern, schnell und einfach das Vorhandensein einer Korrelation zwischen fast einer beliebigen Anzahl von Handelsinstrumenten zu visualisieren. Der Hauptzweck des Instruments liegt in der Analyse von Handelsinstrumenten und in der Entwicklung verschiedener Arbitrage-Strategien. Natürlich reicht dieses Instrument allein nicht aus, um ein vollwertiges Handelssystem zu entwickeln, aber es kann die erste Phase der Entwicklung eines Handelssystems - die Suche nach korrelierten Instrumenten und deren Abhängigkeiten - erheblich erleichtern.


Referenzen

  1. Arbeiten mit Währungskörben im Forexmarkt
  2. Testen von Mustern, die beim Handel mit Währungskörben entstehen. Teil I
  3. Testen von Mustern, die beim Handel mit Währungskörben entstehen. Teil II
  4. Visualisierung! Eine grafische MQL5 Bibliothek ähnlich 'plot' der Sprache R
  5. Wie erstellt man ein grafisches Panel beliebiger Komplexität?
  6. Panels verbessern: Transparenz hinzufügen, Hintergrundfarbe ändern und von CAppDialog/CWndClient übernehmen


Die dieses Artikels

#
 Name
Typ 
Beschreibung 
1  PlotBase.mqh  Klassenbibliothek  Basisklasse für das Erstellen von Graphen
2  Scatter.mqh  Klassenbibliothek  Klasse zum Erstellen von Streudiagrammen
3  Histogram.mqh  Klassenbibliothek  Klasse zum Erstellen von Histogrammen
4  PairPlot.mqh  Klassenbibliothek  PairPlot Hilfsklasse
5  PairPlotDemo.mqh  Klassenbibliothek  Klasse zur Demonstration der Verbindung
6  PairPlot.mq5  Indikator  Demonstration der Arbeitsweise des Indikators