Horizontale Diagramm auf den Charts des MеtaTrader 5

Andrei Novichkov | 27 Februar, 2019

Einführung

Horizontale Diagramme sind in den Terminalcharts nicht üblich, können aber dennoch für eine Reihe von Aufgaben nützlich sein, z.B. bei der Entwicklung von Indikatoren, die Volumen- oder Preisverteilung für einen bestimmten Zeitraum anzeigen, bei der Erstellung verschiedener Versionen der Markttiefe usw. Es kann exotischere Aufgaben bei der Verteilung der Werte bezeichnend von benutzerdefinierten (Standard-)Indikatoren geben. Aber auf jeden Fall haben sie alle ein gemeinsames Merkmal — Diagramme, die erstellt, platziert, skaliert, verschoben und gelöscht werden müssen. Lassen Sie uns die folgenden Punkte hervorheben:

  1. Es kann mehrere Diagramme geben (dies ist in den meisten Fällen der Fall).
  2. Die meisten Diagramme, die uns interessieren, bestehen aus Balken.
  3. Die Diagrammbalken sind horizontal angeordnet.

Lassen Sie uns an einem bekannten Beispiel sehen, wie solche Diagramme aussehen könnten:



Unten ist ein weiteres Beispiel. Gleiche Diagramme gezeichnet mit grafischen Primitiven:


In diesem Fall ist dies der Indikator, der die Verteilung der Tick-Volumina eines Tages anzeigt. Dieses Beispiel zeigt ganz deutlich, welche Aufgaben der Entwickler zu lösen hat:

Nochmals, wir sollten beachten, dass es mehrere Diagramme gibt, die es uns weiterhin ermöglichen, über eine Reihe von Diagrammen zu sprechen.

Nachfolgend ein letztes Beispiel:

Hier sehen wir eine komplexere Version der Verteilung von Tick-Volumina. Obwohl das Diagramm einheitlich aussieht, besteht es aus drei verschiedenen Diagrammen, die sich an einer Stelle befinden, nämlich:

  • "Tick-Volumina der Verkäufe"
  • "Tick-Volumina der Käufe"
  • "Gesamtzahl der Tick-Volumina"

Sie werden vielleicht fragen: "Gibt es einfachere Möglichkeiten, die Daten anzuzeigen? Müssen wir eine so große Anzahl von grafischen Primitiven verwalten?" Es können in der Tat einfachere Methoden gefunden werden, und ihre Effizienz sollte analysiert werden. Der einfachste Weg, die am Anfang des Artikels genannten Aufgaben zu lösen, besteht jedoch darin, die in den Beispielen dargestellten horizontalen Diagramme zu verwenden.

Erklärung des Problems

Lassen Sie uns unseren Plan in zwei Hauptteile unterteilen:

  1. Da alle grafischen Primitive Koordinaten von Bindungen in Form von Zeit und Preis haben, ist es offensichtlich, dass wir die Bindungen erhalten sollten, die es ermöglichen, Diagramme auf einem Diagramm zu positionieren.
  2. Unter Verwendung der in der ersten Stufe erhaltenen Arrays ist es notwendig, die Diagramme anzuzeigen und zu verwalten.

Lassen Sie uns die wichtigsten Möglichkeiten der Bindung von grafischen Objekten am Beispiel unseres Falles definieren. Die ersten Screenshots zeigen vielleicht die häufigste Position von horizontalen Diagrammen. Sie sind an den Beginn eines bestimmten Zeitraums gebunden. Hier ist es der Beginn des Tages.

Dies erschöpft natürlich nicht die Liste der zeitlichen Koordinaten-Bindungsoptionen. Eine weitere Möglichkeit ist die Bindung an die linke oder rechte Seite des Terminalfensters. Sie kann z.B. verwendet werden, wenn ein Diagramm einen sehr langen Zeitraum abdeckt und sein Anfang außerhalb des sichtbaren Fensters liegt. In diesem Fall kann die Bindung an die linke Seite des Fensters verwendet werden.

Eine weitere Möglichkeit ist die Bindung des Diagramms an die aktuelle Periode, wobei eine übermäßige Anzahl von Objekten im Arbeitsteil des Diagramms vermieden wird. In diesem Fall kann die rechte Seite des Terminals verwendet werden. Auf jeden Fall bleibt eine der Zeitkoordinaten der grafischen Primitive, aus denen das Diagramm besteht, gleich. Es wird eine weitere Koordinate berechnet (diejenige, die die "horizontale Länge der Spalte" des Diagramms berechnet).

Im Hinblick auf die Preisbindung der grafischen Primitive ist der Fall viel einfacher. Ein bestimmtes Preissegment wird in Intervalle mit einer festen Schrittweite unterteilt. So kann beispielsweise davon ausgegangen werden, dass ein Preissegment gleich hundert Prozent ist, während eine Schrittweite zehn Prozent beträgt. In diesem Fall können wir Diagramme mit einer konstanten Anzahl von "horizontalen Balken" erhalten, was in einigen Fällen zu einer erheblichen Rundung der Ergebnisse führt. Daher werden wir in diesem Artikel eine effizientere Methode anwenden, die etwas später betrachtet wird.
Aus den obigen Ausführungen können wir schließen, dass die Reihe der Preisbindungen in diesem Fall möglicherweise nicht erforderlich ist. Wir können die folgende einfache Gleichung verwenden, um die i-te Preisbindung zu berechnen: 

Die i-te Preisreferenz = der Beginn des Preisintervalls des Diagramms + i * Schrittweite der Aufteilung des Intervalls.

Für die bereits vorliegenden Beispiele mit Tick-Volumina wird das Preisintervall, auf dem die Diagramme aufgebaut werden können, durch das Intervall zwischen Niedrig und Hoch des Untersuchungszeitraums dargestellt.

Das Problem der Diagrammanzeige sollte ebenfalls detailliert behandelt werden. So haben wir Arrays mit Bindungen für Diagramme nach dem Prinzip: "ein Diagramm - ein Satz von Arrays zum Binden" erhalten. Eine weitere offensichtliche Beobachtung: Diagramme enthalten oft Diagramme, die auf ähnlichen grafischen Primitiven der gleichen Farbe und des gleichen "Stils" basieren (z.B. die Diagramme auf dem zweiten und ersten Screenshot). Auf dem dritten Screenshot sind alle Diagramme unterschiedlich und variieren je nach Farbe und "Richtung". Ein Diagramm hat seine Spalten von rechts nach links "gerichtet", während zwei weitere - von links nach rechts. Es wäre logisch, "gleichartige" Diagramme (wie auf den ersten beiden Screenshots) in einem Array zu kombinieren und einen Manager zuzuweisen, der es steuert. Lassen Sie uns das folgende Prinzip nutzen, um die Aufgabe noch mehr zu standardisieren:

  • Jeder Satz von "gleichartigen" Diagrammen sollte einen eigenen Manager haben, auch wenn der Satz aus einem Diagramm besteht. Somit ist das Array von mindestens drei Diagrammen, die von einem Manager verwaltet werden, für die ersten beiden Screenshots zu erstellen, während wir für das Diagramm auf dem dritten Screenshot drei Arrays (jeweils bestehend aus einem Diagramm) und drei Manager (jeweils einer pro Array) erstellen müssen.

Daher haben wir die wichtigsten Entwicklungspunkte skizziert. Allerdings gibt es hier eine grundlegende Besonderheit. Wie Sie sich vielleicht erinnern, zeigen horizontale Diagramme verschiedene Verteilungen der Tick-Volumina, Preise usw. an. Daher können die Methoden und Prinzipien zum Erhalten von Quellarrays zum Binden von grafischen Primitiven sehr unterschiedlich sein, und es ist nicht sinnvoll, eine Bibliotheksdatei für diesen Teil der Aufgabe zu erstellen.

Insbesondere wird bei der Entwicklung eines Trainingsindikators für die tageweise Verteilung der Tick-Volumina ein effizienterer Ansatz verfolgt. Dieser Artikel bietet eine weitere Möglichkeit. Mit anderen Worten, der erste Teil der Aufgabe wird bei jeder Gelegenheit auf unterschiedliche Weise gelöst. Im Gegenteil, der zweite Teil der Aufgabe (Entwicklung eines Arrays von Diagrammen und Verwaltung dieses Arrays mit Hilfe eines Managers oder Entwicklung mehrerer Assoziationen "Manager - Array von Diagrammen") wird in allen Fällen nahezu gleich sein. Dies ermöglicht es, eine Bibliotheksdatei zu erstellen, die in alle Projekte mit horizontalen Diagrammen eingebunden werden kann.


Das Letzte, was wir angeben sollten, ist, woraus genau die Diagramme bestehen sollen. Die Diagramme bestehen entweder aus horizontalen Liniensegmenten oder aus Rechtecken. Dies sind die beiden natürlichsten Optionen.


Kommen wir nun direkt zum Code.

Konstanten und Eingabeparameter

Es ist ziemlich offensichtlich, dass viele der genannten Diagrammparameter mit Hilfe von Enumerationen gespeichert werden sollen.

Position der Diagramme:

enum HD_POSITION
{
        HD_LEFT  = -1,
        HD_RIGHT =  1,
        HD_CNDLE =  2 
};

Es gibt drei Platzierungsoptionen für die Diagramme — Bindung an die linke Seite (HD_LEFT), an die rechte (HD_RIGHT) des Terminals oder mit Bindung an eine Kerze oder Kerzen (HD_CNDLE). Auf allen drei Screenshots am Anfang des Artikels werden die Diagramme mit HD_CNDLE platziert. Bei den ersten beiden wird die Bindung an Kerzen zu Beginn bestimmter Perioden (Beginn eines Tages) durchgeführt, während bei der dritten die Bindung an eine einzige Kerze erfolgt, die sich zu Beginn des aktuellen Tages befindet.

Die Darstellung des Diagramms (grafische Primitive):

enum HD_STYLE 
{
        HD_LINE      = OBJ_HLINE,        
        HD_RECTANGLE = OBJ_RECTANGLE,    
};

Es gibt zwei Darstellungsoptionen — horizontale Liniensegmente (HD_LINE) und Rechtecke (HD_RECTANGLE). Auf den ersten und dritten Screenshots am Anfang des Artikels bestehen die Diagramme aus HD_LINE-Primitiven, während auf dem zweiten aus HD_RECTANGLE bestehen.

Richtung der "horizontalen Balken" der Diagramme:

enum HD_DIRECT 
{
   HD_LEFTRIGHT = -1,
   HD_RIGHTLEFT =  1 
};

Im dritten Screenshot wird das aus roten Segmenten bestehende Diagramm als HD_RIGHTLEFT und zwei weitere als HD_LEFTRIGHT dargestellt.

Der letzte aufgezählte Typ bezieht sich auf die Anzahl der Diagrammebenen. Ich habe bereits erwähnt, dass die beste Methode zur Berechnung der Anzahl der Stufen und nicht die einfache Aufteilung eines Preisintervalls in eine bestimmte Anzahl von Stufen ist. Es ist sehr wichtig zu beachten, dass sich dieser aufgezählte Typ auf den ersten Teil von Erklärung des Problems bezieht, daher wird er nicht in die endgültige Bibliotheksdatei aufgenommen.

Die angewandte Methode ist sehr einfach und führt dazu, dass das Preisniveau auf zehn oder hundert gerundet wird. Dementsprechend wird auch die Schrittweite der Preisniveaus zehn oder hundert betragen. Bei diesem Ansatz variiert die Anzahl der Preisstufen. Für diejenigen, die die maximale Berechnungswährung (bei erhöhtem Ressourcenverbrauch) erhalten wollen, bleibt die HD_MIN-Methode ohne Rundung übrig:

enum HD_ZOOM {
   HD_MIN    = 0,  //1
   HD_MIDDLE = 1,  //10
   HD_BIG    = 2   //100
}; 

Die Methode HD_MIDDLE wird standardmäßig verwendet.

Betrachten wir folgendes Beispiel. Wir werden einen Trainingsindikator verwenden, der die Verteilung der Tick-Volumina als Entwicklungsobjekt anzeigt. Die Arbeit eines ähnlichen Indikators ist exemplarisch in den ersten beiden Screenshots am Anfang des Artikels dargestellt.

Kommen wir zum Eingangsblock:

input HD_STYLE        hdStyle      = HD_LINE;
input int             hdHorSize    = 20;
input color           hdColor      = clrDeepSkyBlue;
input int             hdWidth      = 2;
input ENUM_TIMEFRAMES TargetPeriod = PERIOD_D1;
input ENUM_TIMEFRAMES SourcePeriod = PERIOD_M1;
input HD_ZOOM         hdStep       = HD_MIDDLE;   
input int             MaxHDcount   = 5;
input int             iTimer       = 1;   

Warum gibt es keinen Parameter, der für die Positionierung verantwortlich ist? Die Antwort ist offensichtlich. Für den erforderlichen Indikator - HD_CNDLE - ist nur eine Positionierungsmethode anwendbar. Daher müssen wir das nicht extra begründen.

Die Parameterfunktion HD_STYLE ist offensichtlich und bedarf keiner zusätzlichen Klärung.

  • Der Parameter hdHorSize spielt eine wichtige Rolle. Er definiert die maximale Größe der "horizontalen Balken" des Kerzendiagramms. In diesem Fall darf der längste "horizontale Stab" zwanzig Kerzen nicht überschreiten. Es ist klar, dass je größer dieser Parameter, desto genauer das Diagramm. Wenn der Parameter jedoch zu groß ist, beginnen sich die Diagramme zu überlappen.
  • Die Parameter hdColor und hdWidth regeln das Aussehen der Diagramme (Farbe und Linienbreite entsprechend).
  • TargetPeriod enthält den analysierten Zeitrahmen. In diesem Fall zeigt der Indikator die Verteilung der Tick-Volumina innerhalb eines Tages an.
  • Der Parameter SourcePeriod enthält einen Zeitrahmen, aus dem der Indikator die Quelldaten entnimmt, um die Verteilung aufzubauen. In diesem Fall wird der Zeitrahmen M1 verwendet. Verwenden Sie diesen Parameter mit Vorsicht. Wenn der monatliche Zeitrahmen als der analysierte ausgewählt wird, können Berechnungen sehr lange dauern.
  • Der Parameter hdStep rundet das Preisniveau ab. Ich habe es bereits oben beschrieben.
  • Der Parameter MaxHDcount enthält die maximale Anzahl von Diagrammen in einem Diagramm. Beachten Sie, dass jedes Diagramm aus mehreren grafischen Primitiven besteht. Zu viele Diagramme können den Terminalbetrieb verlangsamen.
  • Der Parameter iTimer enthält die festgelegte die Häufigkeit der Aufrufe des Timers. Beim Auslösen wird die Erstellung neuer Kerzen überprüft und notwendige Aktionen durchgeführt. Das Ergebnis des Aufrufs PeriodSeconds(SourcePeriod) könnte hier platziert worden sein. Aber der Standardwert ist eine Sekunde, was es uns ermöglicht, den genauen Zeitpunkt zu bestimmen, zu dem neue Kerzen genauer erscheinen.

Initialisierung

In diesem Stadium müssen wir das Objekt des Diagrammmanagers erstellen. Da alle Diagramme vom gleichen Typ sind, benötigen wir nur einen Manager. Da die Klasse des Managers selbst noch nicht geschrieben ist, denken Sie daran, dass sie hier, in der Funktion OnInit(), angelegt wird. Auch hier werden zwei Diagramme erstellt (aber nicht gezeichnet):

  1. Diagramm zur Darstellung der Verteilung der Tick-Volumina für die aktuelle Periode. Dieses Diagramm ist regelmäßig neu zu zeichnen.
  2. Das Diagramm zeigt die zeitliche Verteilung der Tick-Volumina. Diese Verteilungen werden nicht neu gezeichnet, so dass das Diagramm sie "vergisst", nachdem sie angezeigt wurden, und die Kontrolle wird dem Terminal übertragen.

Die erwähnte Effizienz des Ansatzes besteht darin, dass der Indikator keine grafischen Primitive der Diagramme betreut, deren Aussehen sich nicht ändert.

Anschließend werden die Variablen für nachfolgende Berechnungen von Preisniveaus mit einem Schritt initialisiert. Es gibt zwei solche Variablen. Sie werden von Digit() und Point() abgeleitet:

   hdDigit = Digits() - (int)hdStep; 
   switch (hdStep)
    {
      case HD_MIN:
         hdPoint =       Point();
         break;
      case HD_MIDDLE:
         hdPoint = 10 *  Point();
         break;     
      case HD_BIG:
         hdPoint = 100 * Point();
         break;      
      default:
         return (INIT_FAILED);
    }

Einige kleinere Aktionen und der Start des Timers finden auch in dieser Funktion statt.


Grundlegende Berechnungen

Die nächste Aufgabe ist in zwei Phasen unterteilt:

  1. Berechnen der erforderlichen Daten und Zeichnen der erforderlichen Anzahl von Diagrammen mit Ausnahme des aktuellen. Diese Diagramme ändern sich nicht mehr, und es genügt, sie einmal anzuzeigen.
  2. Berechnung der notwendigen Daten und Zeichnungsdiagramme für einen Zeitraum, der den aktuellen beinhaltet. Die Berechnungen für diesen Punkt sollten regelmäßig wiederholt werden. Dies kann im OnTimer() erfolgen.

Lassen Sie uns einen Teil der Arbeit in der Funktion OnCalculate() in Form eines Pseudocode darstellen:

int OnCalculate(...)
  {
   if (prev_calculated == 0 || rates_total > prev_calculated + 1) {
   }else {
      if (!bCreateHis) 
       {
         int br = 1;
         while (br < MaxHDcount) {
           {
            if(Calculate for bar "br") 
                 {
                  sdata.bRemovePrev = false;
                  Print("Send data to the new Diagramm");
                 }

           }
         ChartRedraw();
         bCreateHis = true;
      }
   }  
   return(rates_total);
  }  

Hier werden die notwendigen Berechnungen durchgeführt und die erforderliche Anzahl von Diagrammen außer dem aktuellen gezeichnet. Um dies zu erreichen, werden Berechnungen in einer Schleife für jeden Balken des Zeitrahmens TargetPeriod durchgeführt, vom ersten bis MaxHDcount. Wenn die Berechnungen erfolgreich sind, wird dem Manager ein Befehl gegeben, das Diagramm zu zeichnen, das neue Daten an den Manager in der gleichen Schleife übergibt. Am Ende der gesamten Schleife wird das Diagramm neu gezeichnet und ein Flag gesetzt, das anzeigt, dass dieser Teil der Arbeit nicht mehr notwendig ist. Die Diagramme selbst werden nun vom Terminal gesteuert.

Das Erstellen und Zeichnen des Diagramms einschließlich der aktuellen Periode erfolgt in der Funktion OnTimer(). Ich werde den Pseudocode hier nicht zeigen, da das Problem offensichtlich, einfach und klar ist:

  1. Wir warten auf eine neue Kerze im Zeitrahmen von SourcePeriod.
  2. Führen die notwendigen Berechnungen durch.
  3. Senden Daten an das Diagramm der aktuellen Periode, um die neuen Primitiven zu erstellen und zu zeichnen.
Wir werden die Funktion etwas später betrachten, innerhalb derer die Hauptberechnungen für den bestimmten Zeitrahmen TargetPeriod durchgeführt werden.

Der Code anderer Funktionen und Indikatoren sind nicht so interessant und sind beigefügt. Nun ist es an der Zeit, die Klassen zu beschreiben, die für die Erstellung, Zeichnung und Verwaltung von Diagrammen zuständig sind.

Die Managementklasse des Diagramms

Beginnen wir mit dem Manager, der die horizontalen Diagramme regelt. Dies ist eine Klasse, die selbst keine grafischen Primitive enthält, sie betreut aber die Arrays anderer Klassen, die solche Primitive enthalten. Da alle Diagramme in einem einzigen Manager (wie oben erwähnt) vom gleichen Typ sind, sind viele Eigenschaften solcher Diagramme gleich. Daher ist es nicht sinnvoll, den gleichen Satz von Eigenschaften in jedem Diagramm beizubehalten. Stattdessen lohnt es sich, einen einzigen Satz von Eigenschaften in den Manager einzufügen. Nennen wir die Managerklasse "CHDiags" und beginnen wir mit dem Schreiben des Codes:

  1.   Geschlossene Felder der Klasse CHDiags, die unter anderem einen Satz von Eigenschaften enthalten, der für alle Diagramme unter der Kontrolle dieses Managers gleich ist:
private:    
                HD_POSITION m_position;  
                HD_STYLE    m_style;     
                HD_DIRECT   m_dir;       
                int         m_iHorSize;     
                color       m_cColor;    
                int         m_iWidth;     
                int         m_id;
                int         m_imCount;       
                long        m_chart;    
                datetime    m_dtVis; 
   static const string      m_BaseName;  
                CHDiagDraw* m_pHdArray[];

Beschreibungen:

  • m_position, m_style, m_dir — diese drei Parameter beschreiben die Bindung und das Aussehen von Diagrammen. Wir haben sie bereits erklärt.
  • m_iHorSize — maximal mögliche horizontale Größe des Diagramms. Wir haben auch sie bereits beschrieben.
  • m_cColor und  m_iWidth — Diagrammfarbe und Linienbreite.
Die oben beschriebenen Felder sind Eigenschaften, die für alle vom Manager verwalteten Diagramme einheitlich sind.
  • m_id — eindeutige Manager-ID. Da es mehr als eine geben kann, sollte jeder von ihnen eine eindeutige ID haben. Es ist erforderlich, eindeutige Objektnamen zu bilden.
  • m_chart — ID des Diagramms, in dem die Diagramme angezeigt werden. Standardmäßig ist der Wert dieses Feldes Null (das aktuelle Diagramm).
  • m_imCount — maximale Anzahl von Diagrammen in einem Diagramm. Letztendlich wird die Anzahl der Diagramme in einem Diagramm durch dieses und die folgenden Felder bestimmt.
  • m_dtVis — keine Diagramme links von diesem Zeitstempel erstellen.
  • m_BaseName — extrem wichtiger Parameter, der den "base"-Namen definiert. Alle Elemente der Diagramme sowie die Diagramme selbst sollten eindeutige Namen haben, um erfolgreich erstellt zu werden. Solche Namen werden hinter dem "Basisnamen" angegeben.
Alle oben genannten Felder sind mit den Funktionen des GetXXXX()-Formulars verfügbar.
  • m_pHdArray[] — Array mit Zeigern auf Objekte, die einzelne Diagramme enthalten. Dieses Feld ist keine Eigenschaft und es gibt keine GetXXXX() Funktion dafür.

Es gibt keine Funktion SetXXXX() für die Eigenschaften. Alle (außer m_BaseName) werden im Klassenkonstruktor gesetzt. Eine weitere Ausnahme bildet das Feld m_dtVis. Sie wird im Konstruktor durch den Parameter vom Typ bool mit folgender Bedeutung gesetzt:

  • Anzeigen der Diagramme nur auf den Kerzencharts. Dies geschieht, um das Terminal nicht zu belasten, indem Diagramme angezeigt werden, die sich links neben dem linken Terminalrand befinden. Die Voreinstellung ist 'true'.

Nachdem wir den Manager erstellt haben, können wir Objekte der Diagramme erstellen. Dies geschieht mit der Klassenmethode CHDiags:

int CHDiags::AddHDiag(datetime dtCreatedIn)

Die Methode gibt den Index des erzeugten CHDiagDraw-Klassenobjekts im m_pHdArray-Array des Managers zurück, oder -1 im Fehlerfall. Der Startzeitpunkt des Diagramms wird als Parameter an die Methode dtCreatedIn übergeben. Beispielsweise wird hier die Eröffnungszeit der Tageskerze an den analysierten Indikator übergeben. Wenn der Zeitstempel nicht verwendet wird (Kerzen sind an die Grenzen des Terminalfensters gebunden), sollte hier TimeCurrent() übergeben werden. Wenn sich das Diagramm links neben dem Zeitstempel des Feldes m_dtVis befindet, wird das Objekt nicht erstellt. Der folgende Code zeigt, wie die Methode funktioniert:

int CHDiags::AddHDiag(datetime dtCreatedIn) {
   if(dtCreatedIn < m_dtVis ) return (-1);
   int iSize = ArraySize(m_pHdArray);
   if (iSize >= m_imCount) return (-1);
   if (ArrayResize(m_pHdArray,iSize+1) == -1) return (-1);
   m_pHdArray[iSize] = new CHDiagDraw(GetPointer(this) );
   if (m_pHdArray[iSize] == NULL) {
      return (-1);
   }
   return (iSize);
}//AddHDiag()

Wie Sie sehen können, führt die Methode einige Prüfungen durch, vergrößert die Größe des Arrays, um die Diagramme zu speichern, und erstellt das gewünschte Objekt, indem sie einen Zeiger an den Manager selbst für den späteren Zugriff auf die Eigenschaften übergibt.

Der Manager verfügt auch über andere Methoden, mit denen Sie das Diagramm interagieren können, allerdings nicht direkt, sondern ausschließlich über den Diagrammmanager:

   bool        RemoveDiag(const string& dname);
   void        RemoveContext(int index, bool bRemovePrev);
   int         SetData(const HDDATA& hddata, int index); 
  1. Die erste Methode, wie aus dem Namen ersichtlich ist, entfernt das Diagramm vollständig aus dem Manager und aus dem Diagramm, wobei der Name des Diagramms als Parameter verwendet wird. Derzeit ist dies eine reservierte Option.
  2. Die zweite entfernt nur die grafischen Primitiven, aus denen das Diagramm besteht. Das Diagramm wird aus dem Diagramm entfernt, bleibt aber im Manager vorhanden, obwohl es "leer" ist. Der Wert des Flags bRemovePrev wird weiter verdeutlicht.
  3. Die dritte Methode übergibt die Struktur mit Eingabedaten zur Erstellung von grafischen Primitiven und zum Zeichnen des Diagramms. Der Diagrammindex im m_pHdArray-Array des Managers wird als Parameter in der aktuellen und der vorherigen Methode verwendet.

Die letzte Methode, die eine kurze Erwähnung wert ist, ist die Klassenmethode CHDiags:

void        Align();

Das Verfahren wird aufgerufen, wenn Diagramme an die linke oder rechte Seite des Terminalfensters gebunden sind. In diesem Fall wird die Methode in der Funktion OnChartEvent des CHARTEVENT_CHART_CHART_CHANGE-Ereignisses aufgerufen, um Diagramme an ihre vorherige Stelle wieder darzustellen.

Andere Methoden der Managerklasse CHDiags-Diagramm sind Hilfsmethoden und in der angehängten Datei verfügbar.

Klasse zum Zeichnen und Verwalten der grafischen Primitive von Diagrammen

Nennen wir diese Klasse "CHDiagDraw" und leiten wir sie von CObject ab. Im Klassenkonstruktor erhalten wir den Pointer auf den Manager (speichern Sie ihn im Feld m_pProp). Auch hier wird der eindeutige Diagrammname definiert.

Als nächstes sollten wir die Methode Type() implementieren:

int CHDiagDraw::Type() const
  {
   switch (m_pProp.GetHDStyle() ) {
      case HD_RECTANGLE:
         return (OBJ_RECTANGLE);
      case HD_LINE:
         return (OBJ_TREND);
   }
   return (0);
  }

Der angezeigte Diagrammtyp entspricht den Typen der verwendeten grafischen Primitive, was sehr logisch ist.

Die Hauptberechnungen in der Klasse CHDiagDraw werden von der Methode ausgeführt, die von der Methode SetData des Managers aufgerufen wird:

int       SetData(const HDDATA& hddata);

Ziel der Methode ist es, die Diagrammgrößen zu definieren und die notwendige Anzahl von Primitiven an einem bestimmten Punkt des Diagramms zu erzeugen. Dazu wird in der Aufrufstelle eine Verbindung zur Strukturinstanz übergeben:

struct HDDATA
 {
   double   pcur[];
   double   prmax; 
   double   prmin;   
   int      prsize;
   double   vcur[];
   datetime dtLastTime;
   bool     bRemovePrev;
 };

Lassen Sie uns die Strukturfelder näher beschreiben:

  • pcur[] — Array der Diagrammpreisstufen. Für die erstellten grafischen Primitive ist dies die Reihe der Preisbindungen.
  • prmax — maximaler horizontaler Wert, den das Diagramm haben kann. In diesem Fall ist dies der Maximalwert des Tick-Volumens, das auf einem bestimmten Niveau gehandelt wird.
  • prmin — reservierter Parameter.
  • prsize — Anzahl der Diagrammebenen. Mit anderen Worten, dies ist die Anzahl der Primitive, aus denen das Diagramm bestehen wird.
  • vcur[] — Array von Werten, das die "horizontale Größe" der Balken des Diagramms definiert. In diesem Fall enthält das Array Tick-Volumina, die auf den entsprechenden Ebenen des pcur[] Arrays gehandelt werden. Die Größe der Arrays pcur- und vcur sollte übereinstimmen und gleich der prsize sein.
  • dtLastTime — Diagrammposition. Für grafische Primitive ist dies eine Zeitbindung. Dieses Feld hat eine höhere Priorität als das Argument der Methode AddHDiag des Managers.
  • bRemovePrev — wenn 'true', wird das Diagramm komplett neu gezeichnet, während die vorherigen grafischen Primitive gelöscht werden. Wenn auf 'false' gesetzt wurde, stoppt das Diagramm die Verwaltung der vorherigen grafischen Primitive und zeichnet ein neues Diagramm, ohne es zu löschen, als ob es sie "vergisst".

Der Code der Methode SetData wird im Folgenden aufgrund seiner Bedeutung vollständig angegeben:

int CHDiagDraw::SetData(const HDDATA &hddata) 
  {
   RemoveContext(hddata.bRemovePrev);
   if(hddata.prmax == 0.0 || hddata.prsize == 0) return (0);
   double dZoom=NormalizeDouble(hddata.prmax/m_pProp.GetHDHorSize(),Digits());
   if(dZoom==0.0) dZoom=1;
   ArrayResize(m_hItem,hddata.prsize);
   m_hItemCount=hddata.prsize;
   int iTo,t;
   datetime dtTo;

   string n;
   double dl=hddata.pcur[0],dh=0;
   

   GetBorders(hddata);
   for(int i=0; i<hddata.prsize; i++) 
     {
      if (hddata.vcur[i] == 0) continue;
      t=(int)MathCeil(hddata.vcur[i]/dZoom);
      switch(m_pProp.GetHDPosition()) 
        {
         case HD_LEFT:
         case HD_RIGHT:
            iTo=m_iFrom+m_pProp.GetHDPosition()*t;
            dtTo=m_pProp.GetBarTime(iTo);
            break;
         case HD_CNDLE:
            iTo   = m_iFrom + m_pProp.GetHDDirect() * t;
            dtTo  = m_pProp.GetBarTime(iTo);
            break;
         default:
            return (-1);
        }//switch (m_pProp.m_position)
      n=CHDiags::GetUnicObjNameByPart(m_pProp.GetChartID(),m_hname,m_iNameBase);
      m_iNameBase++;
      bool b=false;
      switch(m_pProp.GetHDStyle()) 
        {
         case HD_LINE:
            b=CHDiags::ObjectCreateRay(m_pProp.GetChartID(),n,dtTo,hddata.pcur[i],m_dtFrom,hddata.pcur[i]);
            break;
         case HD_RECTANGLE:
            if(dl!=hddata.pcur[i]) dl=dh;
            dh=(i == hddata.prsize-1) ? hddata.pcur[i] :(hddata.pcur[i]+hddata.pcur[i+1])/2;
            b = ObjectCreate(m_pProp.GetChartID(),n,OBJ_RECTANGLE,0,dtTo,dl,m_dtFrom,dh);
            break;
        }//switch(m_pProp.m_style)
      if(!b) 
        {
         Print("ERROR while creating graphic item: ",n);
         return (-1);
           } else {
         m_hItem[i]=n;
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_COLOR, m_pProp.GetHDColor() );
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_WIDTH, m_pProp.GeHDWidth() );
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_SELECTABLE, false);
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_BACK, true);
        }//if (!ObjectCreateRay(n, dtTo, hddata.pcur[i], m_dtFrom, hddata.pcur[i]) )    
     }// for (int i = 0; i < l; i++)      
   return (hddata.prsize);
  }//int CHDiagDraw::SetData(const HDDATA& hddata)

Das erste, was die Methode macht, ist die Bereinigung und Berechnung der Skalierung. Es enthält auch die maximale Länge der "horizontalen Balken" des Diagramms und die maximale horizontale Größe des Diagramms in Kerzen. Wir können das Verhältnis ermitteln. Es ist leicht zu erkennen, dass dadurch gerundet wird, da die Diagrammgröße "in Kerzen" eine ganze Zahl ist.

Das Array zum Speichern der Namen der Primitive wird als nächstes vorbereitet. Es werden zusätzliche Diagrammbindungsparameter berechnet — Kerzenindex und temporäre Bindung in der Methode GetBorders. Die zweite Zeitbindung des Diagramms wird in der Schleife danach definiert. Somit stehen alle Bindungen für die Erstellung eines grafischen Primitivs zur Verfügung. Wir erhalten eindeutige Namen der Primitive und erstellen sie sequentiell mit den empfangenen Parametern. Die Namen der Primitiven werden im Array gespeichert und ihre Eigenschaften werden korrigiert. Das Diagramm wird erstellt. Das Verfahren gibt die Anzahl der Diagrammebenen zurück.

Vielleicht sieht die Methode zu lang aus. Es wäre möglich, den Code für die Erstellung und das Rendern der Primitive in eine separate geschützte Methode zu verschieben. Allerdings sehen beide Teile der Methode organisch kompatibel aus, der zweite Teil ist eine klare Fortsetzung des ersten. Diese Überlegung sowie die Zurückhaltung, mehrere zusätzliche Aufrufe der neuen Methode mit einer großen Anzahl von Argumenten zu erzeugen, führten dazu, dass die Methode SetData in ihrer aktuellen Form entwickelt wurde.

Andere Funktionen der Klasse CHDiagDraw sind Hilfsfunktionen und werden zur Integration mit dem Manager verwendet.

Benutzer rufen keine Methode der Klasse CHDiagDraw direkt auf. Stattdessen handeln sie ausschließlich über den horizontalen Diagrammmanager.

Der gesamte Code der oben genannten horizontalen Diagramme Managerklasse, Diagrammzeichnungsklasse, Strukturen und Aufzählungen ist in der angehängten Datei HDiagsE.mqh verfügbar.

Nun können wir auf den EA-Code zurückkommen und seinen Inhalt im Detail betrachten, ohne einen Pseudocode zu verwenden.

Zurück zum Indikator

Deklarieren Sie zwei Objekte und Variablen im globalen Kontext:

CHDiags    *pHd;
int         iCurr, iCurr0;
HDDATA      sdata;

Dies sind der Zeiger auf den Diagrammmanager, die Diagrammindizes und die Struktur für die Datenübergabe an das Diagramm.

Der Manager und beide Diagramme werden sofort in OnInit() mit Hilfe der Indikatoreingaben erstellt:

   pHd         = new CHDiags(HD_CNDLE, hdStyle, HD_LEFTRIGHT, hdHorSize, hdColor, hdWidth, 0, MaxHDcount);
   if(pHd      == NULL) return (INIT_FAILED);
   iCurr       = pHd.AddHDiag(TimeCurrent() );
   if(iCurr  == -1) return (INIT_FAILED); 
   iCurr0       = pHd.AddHDiag(TimeCurrent() );
   if(iCurr0  == -1) return (INIT_FAILED);    

Unten ist die Funktion OnCalculate, für die wir oben den Pseudo-Code geschrieben haben:

        {
         int br=1;
         while(br<MaxHDcount) 
           {
            if(PrepareForBar(br++,sdata)) 
              {
               sdata.bRemovePrev = false;
               if(iCurr!=-1) 
                 {
                  Print(br-1," diag level: ",pHd.SetData(sdata,iCurr));
                 }
              }
           }
         ChartRedraw();
         bCreateHis=true;
        }

Nun müssen wir nur noch die Funktion betrachten, die die Struktur des Typs HDDATA mit Daten für einen ausgewählten Balken aus dem Zeitrahmen füllt, für den die Verteilung (TargetPeriod) gebaut ist:

bool PrepareForBar(int bar, HDDATA& hdta) {

   hdta.prmax  = hdta.prmin  = hdta.prsize  = 0;
   int iSCount;
   datetime dtStart, dtEnd;
   dtEnd = (bar == 0)? TimeCurrent() : iTime(Symbol(), TargetPeriod, bar - 1);
   hdta.dtLastTime = dtStart = iTime(Symbol(), TargetPeriod, bar);
   
   hdta.prmax = iHigh(Symbol(), TargetPeriod, bar);
   if(hdta.prmax == 0) return (false);
   hdta.prmax      = (int)MathCeil(NormalizeDouble(hdta.prmax, hdDigit) / hdPoint );
   
   hdta.prmin = iLow(Symbol(), TargetPeriod, bar);
   if(hdta.prmin == 0) return (false);
   hdta.prmin      = (int)MathCeil(NormalizeDouble(hdta.prmin, hdDigit) / hdPoint );

   iSCount = CopyRates(Symbol(), SourcePeriod, dtStart, dtEnd, source);
   if (iSCount < 1) return (false);
   
   hdta.prsize = (int)hdta.prmax - (int)hdta.prmin + 10;
   
   ArrayResize(hdta.pcur,  hdta.prsize);
   ArrayResize(hdta.vcur,  hdta.prsize);
   ArrayInitialize(hdta.pcur, 0);
   ArrayInitialize(hdta.vcur, 0);
   
   double avTick;
   int i, delta;
   hdta.prmax = 0;
   
   for (i = 0; i < hdta.prsize; i++) hdta.pcur[i] = (hdta.prmin + i) * hdPoint;
   int rs = 0;
   for (i = 1; i < iSCount; i++) {
      if (source[i].tick_volume == 0.0) continue;
      if (!MqlRatesRound(source[i], (int)hdta.prmin) ) continue;
      delta = (int)(source[i].high - source[i].low);
      if (delta == 0) delta = 1;
      avTick = (double)(source[i].tick_volume / delta);
      int j;
      for (j = (int)source[i].low; j <= (int)(source[i].low) + delta; j++) {
         if (j >= hdta.prsize) {
            Print("Internal ERROR. Wait for next source period or switch timeframe");
            return false;
         }
         hdta.vcur[j] += avTick;
         if (hdta.vcur[j] > hdta.prmax) hdta.prmax = (int)hdta.vcur[j];
      }//for (int j = (int)source[i].low; j <= (int)(source[i].low) + delta; j++)   
      if (j > rs) rs = j; //real size
   }//for (int i = 1; i < iSCount; i++)
   hdta.prsize = rs + 1;
   return (true);
}  

Ganz am Anfang findet die Methode heraus, für welchen Zeitraum sie die Berechnung durchführen soll. Auch hier werden die Grenzen der Arbeitspreisspanne unter Berücksichtigung einer weiteren Aufteilung in Ziehungsebenen berechnet.

MqlRates des Zeitrahmens, der als Quelldaten für den neu angegebenen Zeitraum bestimmt wurde, werden anschließend ausgelesen. Für jede erhaltene MqlRates-Struktur wird berücksichtigt, dass tick_volume gleichmäßig im Bereich von niedrig bis hoch der Struktur verteilt ist. Da die Grenzen der Preisspanne für das gesamte zukünftige Diagramm bereits bekannt sind, wird die Verteilung der Tick-Volumina im Diagramm positioniert. Dadurch wird das Array der Verteilung der Tick-Volumina innerhalb des gesuchten Zeitraums gebildet.


Die Berechnungen enden mit der Definition der tatsächlichen Größe des Verteilungsarrays der Tick-Volumina und damit der Anzahl der grafischen Primitive im zukünftigen Diagramm.

Somit sind die Datenstrukturfelder des Diagramms mit der Methode SetData(....) ausgefüllt und bereit zur Übergabe.

Im Allgemeinen sieht das Arbeiten mit den beschriebenen Klassen wie folgt aus:

  1. Einbinden von HDiagsE.mqh.
  2. Erstellen eines Managerobjekt pro Gruppe von "gleichartigen" Diagrammen.
  3. Erstellen der Diagramme, indem Sie die Managermethode AddHDiag aufrufen. Die Methode gibt ihren Index in dem dem Manager bekannten Array zurück.
  4. Löschen Sie das Diagramm von irrelevanten Daten, indem Sie die Methode RemoveContext aufrufen. Neue Daten werden an das Diagramm übergeben, indem die Methode SetData aufgerufen und die Struktur vom Typ HDDATA mit Daten an das Diagramm übergeben wird. Die Verantwortung für das korrekte Ausfüllen der Felder dieser Struktur liegt bei der aufrufenden Seite.
  5. Bei Bedarf können Diagramme durch Aufruf der Methode Align des Managers auf der linken oder rechten Seite des Terminalfensters ausgerichtet werden.
  6. Alle Diagramme werden in der CHDiags Managerklasse Destruktor gelöscht.

Der vollständige Indikatorcode befindet sich in der angehängten Datei VolChart.mq5. Die Bibliotheksdatei HDiagsE.mqh ist ebenfalls im Anhang enthalten.

Namen

Fast alle Objekte, die sich auf horizontale Diagramme beziehen, haben Namen. Sie sind wie folgt hierarchisch aufgebaut:

  1. Der Manager verfügt über das private Feld m_BaseName, das den "Basisnamen" definiert. Alle anderen Namen beginnen damit.
  2. Beim Anlegen eines Managerobjekts wird ihm eine eindeutige ID zugewiesen. Der Aufrufcode ist für die Eindeutigkeit dieses Parameters verantwortlich. Das Feld m_BaseName und die ID bilden den Managernamen.
  3. Beim Erstellen eines Diagrammobjekts erhält es auch einen eindeutigen Namen, der auf dem des Managers basiert.
  4. Schließlich erhalten die im Diagrammobjekt erstellten grafischen Primitive ihre eindeutigen Namen basierend auf dem Namen des Diagrammobjekts, das die Primitive enthält.

Auf diese Weise können die benötigten Objekte einfach erfasst und verwaltet werden.

Noch ein weiterer Indikator

Lassen Sie uns den bereits entwickelten Indikator in denjenigen umwandeln, der die Verteilung der Tick-Volumina als Diagramm auf der rechten Seite des Terminalfensters anzeigt:


Dafür ändern wir den Code etwas:

  • Der Initialisierungscode sollte etwas verändert werden
       pHd         = new CHDiags(HD_RIGHT, hdStyle, HD_RIGHTLEFT, hdHorSize, hdColor, hdWidth, 0, MaxHDcount);
       if(pHd      == NULL) return (INIT_FAILED);
       iCurr       = pHd.AddHDiag(TimeCurrent() );
       if(iCurr    == -1) return (INIT_FAILED); 
    
  • Wir entfernen den ganzen Codes aus der Funktion OnCalculate. 
  • Wir ergänzen die Funktion OnChartEvent
    void OnChartEvent(const int id,
                      const long &lparam,
                      const double &dparam,
                      const string &sparam)
      {
          switch (id) {
             case CHARTEVENT_CHART_CHANGE:
                pHd.Align();
                break;
            default:
                break;    
          }//switch (id)
      }  
    
Das ist alles. Der neue Indikator ist fertig und funktioniert. Der vollständige Code ist in der unten angehängten Datei VolChart1.mq5 enthalten.

Schlussfolgerung

Jetzt können wir horizontale Diagramme erstellen, indem wir eine einzelne Datei verwenden. Die Aufgabe des Bauherrn wird die Aufbereitung der Daten für den Bau sein, der sich hauptsächlich außerhalb des Themas des aktuellen Artikels befindet. Der Code der Bibliotheksdatei schlägt dank der reservierten Methoden mögliche Verbesserungen vor. Andere grafische Primitive können zum Zeichnen hinzugefügt werden.

Vergessen Sie nicht, dass die beigefügten Indikatoren nur zur Demonstration und Schulung gedacht sind. Wenden Sie sie nicht im realen Handel an. Beachten Sie insbesondere, dass es in dem als Datenquelle verwendeten Zeitrahmen keine Artefakte geben sollte.

Die im Artikel verwendeten Programme und Dateien

 # Name
Typ
 Beschreibung
1 VolChart.mq5 Indikator
Die Verteilung der Tick-Volumina des Indikators.
2
HDiagsE.mqh Bibliotheksdatei
Bibliotheksdatei mit dem Manager der horizontalen Diagramme und dem horizontalen Diagramm.
3
VolChart1.mq5
Indikator
Die Verteilung der Tick-Volumina, ausgerichtet nach der rechten Seite des Fensters des Terminals.