English Русский 中文 Español 日本語 Português
preview
Ein manuelles Chart- und Handelswerkzeug (Teil III). Optimierungen und neue Werkzeuge

Ein manuelles Chart- und Handelswerkzeug (Teil III). Optimierungen und neue Werkzeuge

MetaTrader 5Beispiele | 18 Januar 2022, 12:24
715 0
Oleh Fedorov
Oleh Fedorov

Einführung

In den vorherigen Artikeln (1, 2) habe ich die Shortcuts-Bibliothek beschrieben und ein Beispiel für die Verwendung der Bibliothek als Expert Advisor gezeigt. In gewisser Weise ist die Bibliothek mit einem lebenden Organismus vergleichbar. Sie wird geboren, der Öffentlichkeit vorgestellt und trifft auf die Umgebung, in der sie "leben" wird. Diese Umgebung verändert sich jedoch und hat ihre eigenen Gesetze. Eines der wichtigsten Gesetze ist "Verbessern". Man muss sich also immer weiter entwickeln und verbessern. Dieser Artikel zeigt einige der Ergebnisse dieses Verbesserungsprozesses.

Also, unsere Bibliothek besteht aus fünf Dateien.

Die Hauptdatei ist Shortcuts.mqh. In dieser Datei ist die Logik zur Verarbeitung von Tastatureingaben gespeichert. Außerdem ist dies die einzige Datei, die direkt in einen Expert Advisor oder Indikator eingebunden wird. Dementsprechend schließt sie auch die übrigen Dateien ein und initialisiert sie.

Die Datei GlobalVariables.mqh enthält alle globalen Variablen. Dies sind vor allem die Einstellungen, mit denen Sie Linienfarbe, Liniendicke, Liniendehnungskoeffizienten und mehr festlegen können.

Die Datei Mouse.mqh enthält die Beschreibung der Klasse, die Mausbewegungen verarbeitet. Sie speichert die aktuellen Cursor-Koordinaten, sowohl in Pixeln als auch in "Preis-Zeit"-Koordinaten, und die aktuelle Balkennummer.

Utilites.mqh enthält Hilfsfunktionen. Sie berechnen Balken-Extremwerte, Linienschnittpunkte und andere nützliche Parameter, die zwar nicht direkt mit dem Zeichnen zu tun haben, aber alle seine Aspekte bestimmen.

Die Datei Graphics.mqh ist für das Zeichnen auf der Grundlage der Daten aus anderen Dateien zuständig. Hauptsächlich rufen die Funktionen aus dieser Datei die Datei Shortcuts.mqh auf.

Ich gruppiere die Funktionen nicht immer streng. Einige Berechnungen können innerhalb der Zeichenfunktionen durchgeführt werden. Bis jetzt ist es für mich bequem, diese Struktur zu entwickeln und zu pflegen. Vielleicht werde ich eines Tages die Gesamtanordnung verbessern.

Diese Implementierung zeigt, wie der Bibliothekscode in einem Indikator verwendet werden kann.


Optimierung der Bibliotheksleistung

Die Änderungen hier sind minimal.

Warum habe ich mich in den ersten Bibliotheksversionen für einen Expert Advisor und nicht für einen Indikator entschieden? Das ist einfach. Jeder Expert Advisor läuft in seinem eigenen Ausführungs-Thread. Im Idealfall beeinflussen sie sich nicht gegenseitig, sodass das Terminal nicht zu sehr verlangsamt wird, wenn wir Tastaturkürzel für mehrere Charts verwenden müssen.

Der Zweck eines Expert Advisors ist jedoch der Handel, während dieses Programm keine Handelsoperationen durchführt. Wenn ein Indikator an einen Chart angehängt ist, ist es außerdem viel einfacher, einen anderen Expert Advisor auf diesem auszuführen. Daher entschied ich, alles in einem Indikator zu implementieren. Hier stellt sich die Frage der Leistungsgeschwindigkeit. Dies ist besonders wichtig, wenn der Nutzer viele Fenster geöffnet hat. Wenn der Nutzer z.B. 40 Tabs geöffnet hat (es können auch mehr sein), dann wird es schwierig, die Tastenanschläge zu verarbeiten, wenn alle Charts gleichzeitig die Tastenanschläge verarbeiten.

Und dann habe ich verstanden: Warum müssen wir alle Charts bearbeiten? Alle Überprüfungen sollten nur im aktiven Fenster durchgeführt werden.

Der Code ist sehr einfach.

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
 //...

  if(ChartGetInteger(0,CHART_BRING_TO_TOP)==false)
   {
    return;
   }
 
 //...

Zu Beginn der Funktion muss geprüft werden, ob das Chart aktiv ist, d. h. ob es sich im Vordergrund befindet. Wenn dies nicht der Fall ist, sollte nichts getan werden.

Ich habe die tatsächliche Leistung oder den Geschwindigkeitszuwachs nicht in Prozent gemessen. Aber den Bewertungen von Nutzern zufolge, die die Bibliothek heruntergeladen haben und die wirklich viele Tabs verwenden, ist die Anwendung auch in Form eines Indikators reaktionsschneller geworden. Das ist genau das, was wir brauchen.

Vergessen wir aber nicht den Zweck der Anwendung.

Erstens ist sie für die Ausführung episodischer Funktionen konzipiert (sie wird also nicht bei jedem Tick aufgerufen) und verbraucht daher nur für eine sehr begrenzte Zeit Ressourcen.

Zweitens sind die Grafiken selbst eine Quelle für Geschwindigkeitsprobleme, wenn Ihr Computer nicht leistungsstark genug ist. Je mehr Objekte auf dem Chart sind, desto schwieriger ist es, sie zu verarbeiten. Da es sich jedoch um eine Grafikbibliothek handelt, müssen Sie die Kosten für das Zeichnen in Kauf nehmen und sorgfältig kontrollieren, was gezeichnet wird.

Drittens: Die ressourcenintensivste Funktion des Programms ist die Extremum-Suchfunktion. Aber ich weiß nicht, wie man sie schneller implementieren kann. Daher betrachte ich die derzeitige Implementierung als optimal für den Moment. Wie auch immer, diese Funktion wird nicht so oft aufgerufen, nur beim Zeichnen von Linien und einigen anderen nützlichen Formen, und daher kann ihre nicht-optimale Leistung vorerst ignoriert werden.

Alle anderen Funktionen werden viel seltener aufgerufen, und sie arbeiten schnell genug, sodass es keinen Grund gibt, sie zu diskutieren.


Code-Refactoring: Verwaltung der Konnektivität

Die in den vorangegangenen Artikeln vorgestellte Codeversion geht davon aus, dass die Anwendung monolithisch ist und ihre Teile nicht getrennt verwendet werden. Aus diesem Grund verwendete der Code die globalen Einstellungen direkt, während einige Dienstprogramme von der Mausklasse abhingen.

Dadurch konnte ich den Code zwar schneller schreiben, aber unter dem Gesichtspunkt der Wiederverwendung war dies ungünstig. Wenn ich die vorhandene Dienstprogrammdatei mit einem neuen Projekt verbinden möchte, das weder die Maus noch Grafiken verwendet, muss ich immer noch die Datei mit den globalen Einstellungen und die Mausklasse verbinden.

Das ist falsch und unpraktisch.

Deshalb habe ich beschlossen, den Code leicht zu ändern. Alle globalen Variablen werden weiterhin verwendet. Wir können sie nicht entfernen, da sie zu den Einstellungen gehören.

Ich habe private Felder mit Kopien dieser Variablen zu den Hauptklassen hinzugefügt. Um diese Werte zu speichern, ist es notwendig, spezielle "öffentliche" Funktionen hinzuzufügen. Sie werden auch benötigt, um die Werte zu lesen.

Sie sieht wie folgt aus:

private:
  /* Fields */
  //---
  static int          m_TrendLengthCoefficient;

public:
  /* Methods */
  //---
  static int          TrendLengthCoefficient(void) {return m_TrendLengthCoefficient;}
  //---
  static void         TrendLengthCoefficient(int _coefficient) {m_TrendLengthCoefficient=_coefficient;}

Der Prozess scheint langwierig und mühsam zu sein, wenn man die Anzahl der vorhandenen Einstellungen bedenkt.

Aber der Vorteil ist groß. Erstens wird die Klasse unabhängig von externen Dateien. Wenn jemand die Klasse verwenden möchte, kann er nur die erforderlichen Variablen verwenden und ihre Werte nach Bedarf einstellen.

Zweitens können solche Variablen zur Laufzeit geändert werden. Ein Beispiel: Jemand möchte eine Funktion schreiben, die von einem einzigen Punkt aus einen Fächer von Linien aufbaut. Jede Linie ist doppelt so lang wie die vorherige, und sie laufen in unterschiedlichen Winkeln auseinander. Wie kann man das machen? Mit der aktuellen Implementierung der Klasse CUtilites: Vor jeder Zeichnung wird der für das Beispiel beschriebene Parameter, TrendLengthCoefficient, gesetzt, wobei die Anfangspunkte an denselben Koordinaten platziert werden, während die Endpunkte an einem Kreis mit beliebigem Radius platziert werden sollten.

Drittens können die Daten innerhalb der Klasse auf beliebige Weise gruppiert werden. Sie können Strukturen oder sogar ganze Klassen erstellen, die z. B. Daten zu den Rechtecken, Daten zu den Diagonalen und Daten zu den Ebenen getrennt speichern. Aus der Sicht des Endbenutzers hat sich die Schnittstelle (die Art des Zugriffs auf die Daten) nicht geändert.

Viertens: Die Daten müssen nicht unbedingt im RAM gespeichert werden. Manchmal können Variablen in den globalen Variablen des Terminals gespeichert werden, oder allgemein in Dateien oder sogar in einer Datenbank. Einige Parameter können im laufenden Betrieb auf der Grundlage anderer Parameter berechnet werden. Mit dieser "richtigen" Organisation des Datenzugriffs, wie im obigen Beispiel gezeigt, kann der Endbenutzer den Code wiederverwenden, ohne sich um die direkte Implementierung von Datenstrukturen kümmern zu müssen. Es ist auf jeden Fall sehr bequem, trotz des zusätzlichen Aufwands, der durch das Schreiben von übermäßigem Code, den Aufruf unnötiger Funktionen und das Initialisieren der erforderlichen Variablen bei jeder Erstellung einer Instanz entsteht.

Daher habe ich alles, was ich in der aktuellen Bibliotheksversion schreiben konnte, an den neuen Stil angepasst, sodass die Utilities-Datei nun "wie sie ist" in jedem Projekt verwendet werden kann.

Die Mausklasse enthielt ursprünglich alle zugehörigen Funktionen, sodass es hier nichts zu ändern gab. Die Zeichenklasse ist ohne die Utilities nutzlos. Nichtsdestotrotz habe ich alle externen Einstellungen in ihr für die neue Form der Felder geändert.

Hier ist also, was wir haben. Die Maus und die Hilfsprogramme sind völlig unabhängige Klassen, die einzeln oder in Kombinationen verwendet werden können. Die Zeichenklasse verwendet beide, ist aber unabhängig von anderen externen Dateien mit Ausnahme des Dispatchers, der die Klasse initialisieren soll. Die Klasse, die die Tastenkombinationen enthält, ist die Verwaltungsklasse, d. h. der Dispatcher, der den gesamten Code wie gewünscht funktionieren lässt. Dadurch ist die Verbindung der Klassen untereinander viel schwächer geworden, was zu den oben genannten Vorteilen führt.


Das Werkzeug "Fadenkreuz"

In der vorherigen Version der Bibliothek wurde beim Zeichnen einer Trendlinie ein Fadenkreuz am Ende der Linie gezeichnet, um die Zeit- und Preisebenen zu beschlagen. Um es zu erstellen, habe ich zwei einfache Linien verwendet, eine vertikale und eine horizontale. Um das Fadenkreuz jedoch an einem beliebigen Punkt des Charts anzuzeigen, musste ich zwei Tasten drücken, H und I. Manchmal ist das praktisch, aber manchmal möchte man weniger Aufwand. Deshalb habe ich das Fadenkreuz hinzugefügt.

Das Werkzeug funktioniert auf die übliche Weise. Bewegen Sie den Cursor an die gewünschte Stelle und drücken Sie X - damit wird das Fadenkreuz erzeugt. Unten ist der Funktionscode.


/* Graphics.mqh */

//+------------------------------------------------------------------+
//| Draws a crosshair at specified coordinates. If the coordinates   |
//|   are not set, the mouse pointer coordinates are used.           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _time - crosshair time                                |
//|   double _price - price level                                    |
//+------------------------------------------------------------------+
void CGraphics::DrawCross(datetime _time=-1,double _price=-1)
 {
  datetime time;
  double price;
//---
  if(_time==-1)
   {
    time=CMouse::Time();
   }
  else
   {
    time=_time;
   }

  if(_price==-1)
   {
    price=CMouse::Price();
   }
  else
   {
    price=NormalizeDouble(_price,Digits());
   }
  DrawSimple(OBJ_HLINE,time,price);
  DrawSimple(OBJ_VLINE,time,price);
  
 }

Für diejenigen, die mit dem Code der vorherigen Version vertraut sind, gibt es hier nichts Besonderes. Zunächst werden die Koordinaten gesetzt. Wenn diese Koordinaten über die Parameter übergeben werden, dann werden genau diese Werte verwendet. Sind die Standardparameter gesetzt, so werden die Koordinaten des Mauszeigers verwendet.

Weiterhin wird die Linie mit der Funktion gezeichnet, die im zweiten Artikel beschrieben wurde.

Damit diese Funktion genau so funktioniert, wie beschrieben, sollte sie aus der Datei Shortcuts.mqh bei einem bestimmten Ereignis augerufen werden - dem Drücken der Taste X.

/* GlobalVariables.mqh */
  
  // ...
  
  input string   Cross_Key="X";                       // Crosshair where the mouse was clicked
  
  // ...
  /* Shortcuts.mqh */
  
  void CShortcuts::OnChartEvent( /* ... */ )
    switch(id)
     {
       case CHARTEVENT_KEYDOWN:
       
       // ... 
       
       //--- Draw a crosshair
       if(CUtilites::GetCurrentOperationChar(Cross_Key) == lparam)
        {
         m_graphics.DrawCross();
        }
     }

Trendlinie durch willkürliche Extrema

Die Möglichkeit, eine Trendlinie durch Extrema mit einer bestimmten Anzahl von Balken auf der linken und rechten Seite zu erstellen, ist praktisch. Manchmal möchte man aber auch eine Linie durch beliebige Extrema zeichnen. Dies kann mit dem Befehl Q erfolgen.

Das folgende Bild zeigt ein Beispiel für die Funktionsweise dieser Funktion.

Ein Beispiel für das Zeichnen einer Trendlinie durch beliebige Extrema

Da meine App für ein Bildschirmfoto einige spezielle Funktionen hat, musste ich jedes Mal auf den Chart klicken, bevor ich ihn zeichnen konnte. Unter realen Bedingungen sollten man den Chart einfach aktivieren, und dann kann man so viele Linien zeichnen, wie man benötigt.

Die Linie wird in zwei Schritten gezeichnet. Im ersten Schritt sollten Sie Q drücken. Dadurch wird der Modus zum Zeichnen einer beliebigen Linie aktiviert und der erste Punkt markiert - damit ist klar, dass der Befehl ausgeführt wurde.

Wenn Sie diesen extremen Punkt (wo sich die Markierung befindet) nicht verwenden wollen, können Sie Q noch einmal drücken — dies wechselt den Modus und bricht das Zeichnen ab. (Eines Tages werde ich dieses Verhalten wahrscheinlich ändern, indem ich die Esc-Taste auf "Abbrechen" setze, obwohl ich persönlich mit der jetzigen Funktionsweise zufrieden bin).

Wenn der erste Punkt korrekt ist, wird der nächste Punkt durch einen Klick in der Nähe des zweiten Extremums ausgewählt. Wenn es funktioniert, wird die Markierung nicht mehr benötigt. Er wird gelöscht und die Trendlinie wird gezeichnet.

Die Parameter der "willkürlichen" Linie hängen nicht vom Modus T ab, sodass man z.B. einstellen kann, dass T eine fette Linie mit einer Breite von 4 px zeichnet, die viermal länger ist als der Abstand zwischen den Extrema, während Q eine dünne Linie zeichnet, die doppelt so lang ist wie der Abstand.

Wie üblich ist der Code auf mehrere Dateien aufgeteilt.

Beginnen wir am Ende, bei der Verarbeitung des Ereignisses CHARTEVENT_KEYDOWN:

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
   //...
   
   switch(id)
   {
   
   //...
   
     case CHARTEVENT_KEYDOWN:
      if(CUtilites::GetCurrentOperationChar(Free_Line_Key) == lparam)
       {
        m_graphics.ToggleFreeLineMode();
        if(m_graphics.IsFreeLineMode()){
          m_graphics.DrawFreeLine(CMouse::Bar(),CMouse::Above());
        }
       } 
    
    //...

Wenn das Programm feststellt, dass die Taste Q gedrückt ist (der Buchstabe ist in der externen Variablen Free_Line_Key gespeichert), dann schaltet es den Zeichenmodus um. Stellt sich nach der Modusumschaltung heraus, dass der Modus eingeschaltet ist, wird ein Befehl zum Ausführen der Linienzeichnungsfunktion generiert.

Der Klick wird in der Ereignisbehandlung verarbeitet

/* Shortcuts.mqh */

        //...
        
    case CHARTEVENT_CLICK:
        ChartClick_Handler();
      break;
      
      //...
      
}

//+------------------------------------------------------------------+
//| Processing a click on a free chart field                         |
//+------------------------------------------------------------------+
void CShortcuts::ChartClick_Handler()
 {
  
//---
  if(m_graphics.IsFreeLineMode()){
    m_graphics.DrawFreeLine(
      CMouse::Bar(),CMouse::Above()
    );
  }
  
 }

Noch einmal, bitte beachten Sie, dass beim Drücken der Taste der Zeichenmodus sofort umgeschaltet wird, noch bevor das Zeichnen beginnt (der Name meiner Funktion beginnt mit Toggle). Dieser Zustand bleibt erhalten, bis er mit den Tasten wieder umgeschaltet wird oder die Linie gezeichnet ist. Wenn Sie klicken, prüft das Programm zunächst, ob es etwas zu zeichnen gibt. Wenn ja, zeichnet es und schaltet in den neutralen Modus.

Die Funktion ChartClick_Handler ist separat implementiert, da ich plane, weitere Modi hinzuzufügen, die Klicks auf das Chart erfordern. Zum Beispiel kann der Modus zum Löschen komplexer Objekte wie Fadenkreuze oder vertikale Ebenen, der im vorherigen Artikel beschrieben wurde, manchmal einen Klick auf das Chart erfordern, um das Menü abzubrechen. Bisher sieht es so aus, dass eine separate Implementierung von Klickfunktionen die weitere Entwicklung vereinfachen wird. Aber all diese Funktionen werden später implementiert.

Betrachten wir nun weiter, wie das Zeichnen funktioniert.

/* Graphics.mqh */


//+------------------------------------------------------------------+
//|  Draws a line by arbitrary specified extrema. In the current     |
//|    implementation, the first extremum is set by a hot key        |
//|    (Q by default), the second is set by clicking near the        |
//|    required top                                                  |
//+------------------------------------------------------------------+
//|  Parameters:                                                     |
//|    int _bar - bar to start search at                             |
//|    bool _isUp - top or bottom?                                   |
//|    int _fractalSizeRight - number of bars to the right of extr   |
//|    int _fractalSizeLeft -  number of bars to the left of extremum|
//+------------------------------------------------------------------+
void CGraphics::DrawFreeLine(
  int _bar,
  bool _isUp,
  int _fractalSizeRight=1,
  int _fractalSizeLeft=1
)
 {
//--- Variables
  double    selectedPrice,countedPrice,trendPrice1,trendPrice2;
  datetime  selectedTime,countedTime,trendTime1,trendTime2;
  int       selectedBar,countedBar;
  int       bar1,bar2;

  string trendName="",trendDescription="p2;";
  int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* 2;

//--- Search for a bar that meets the extremum criteria
  selectedBar = CUtilites::GetNearesExtremumSearchAround(
    _bar,
    _isUp,
    _fractalSizeLeft,
    _fractalSizeRight
  );

//--- Building the starting marker
  if(0==m_Clicks_Count)
   {
    m_Clicks_Count=1;
    if(_isUp)
     {
      m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //---
    m_First_Point_Time=CUtilites::DeepPointSearch(
                         m_First_Point_Time,
                         _isUp,
                         ENUM_TIMEFRAMES(Period())
                       );
    //---
    DrawFirstPointMarker(_isUp);
   
   }
//--- Processing a click on the chart
  else
   {
    ObjectDelete(0,m_First_Point_Marker_Name);
    if(_isUp)
     {
      countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //--- Move a point in time on smaller timeframes
    countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period()));

    //--- The line is always drawn from left to right. 
    //--- If it is not convenient, you can comment this part
    //---   up to the next comment
    if(countedTime<m_First_Point_Time)
     {
      trendTime1=countedTime;
      trendPrice1=countedPrice;
      trendTime2=m_First_Point_Time;
      trendPrice2=m_First_Point_Price;
     }
    else
     {
      trendTime2=countedTime;
      trendPrice2=countedPrice;
      trendTime1=m_First_Point_Time;
      trendPrice1=m_First_Point_Price;
     }
    //--- Set the description for future correction
    trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());

    //selectedPrice=CUtilites::EquationDirect(
    //                trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime
    //              );
    trendName=CUtilites::GetCurrentObjectName(allPrefixes[0],OBJ_TREND);
    
    TrendCreate(
      0,                    // Chart ID
      trendName,            // Line name
      0,                    // Subwindow number
      trendTime1,           // time of the first point
      trendPrice1,          // price of the first point
      trendTime2,           // time of the second point
      trendPrice2,          // price of the second point
      CUtilites::GetTimeFrameColor(
        CUtilites::GetAllLowerTimeframes()
      ),                    // line color
      Trend_Line_Style,     // line style
      Trend_Line_Width,     // line width
      false,                // background object
      true,                 // is the line selected
      true                  // ray to the right
    );
    
    bar1=iBarShift(NULL,0,trendTime1);
    bar2=iBarShift(NULL,0,trendTime2);
    selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );
    selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);
    ObjectSetInteger(0,trendName,OBJPROP_RAY,IsRay());
    ObjectSetInteger(0,trendName,OBJPROP_RAY_RIGHT,IsRay());
    ObjectMove(0,trendName,1,selectedTime,selectedPrice);
    //---
    m_Clicks_Count=0;
    ToggleFreeLineMode();
   }

  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);
  ChartRedraw();
 }

Die Funktion ist recht umfangreich, daher werde ich sie wahrscheinlich später in mehrere kleinere Funktionen aufteilen. Ich hoffe, dass die Hervorhebung und die Kommentare zum Verständnis der Funktionsweise beitragen werden.

In dieser Implementierung prüft die Funktion beide Signale: das Ereignis, das anzeigt, dass das Zeichnen des ersten Punktes begonnen hat, und die Benachrichtigung, dass der zweite Punkt gefunden wurde und sein Zeichnen begonnen hat. Zur Unterscheidung dieser Ereignisse wurde die Variable m_Clicks_Count eingeführt. Anhand des Buchstabens "m_" am Anfang wird deutlich, dass die Variable für diese Klasse global ist und ihre Lebensdauer gleich der Lebensdauer der Objektinstanz ist.

Wenn es sich um den ersten Funktionsaufruf handelt (d.h. eine Taste wurde gedrückt), ist es notwendig, den ersten Punkt zu finden und den Marker zu zeichnen.

Wenn es sich um den zweiten Aufruf handelt, ist es notwendig, den Marker zu löschen, den zweiten Punkt zu finden und eine Linie zu zeichnen. Dies waren die wichtigsten fünf Blöcke, während alle anderen für ihre Implementierung benötigt werden.

In der aktuellen Implementierung wird der Preis in der Zukunft anhand der Geraden selbst bestimmt. Im Allgemeinen ist das keine gute Idee, da das Terminal im Moment des Zeichnens zuerst einen Strahl zeichnen muss, dann das Ende der Linie an den gewünschten Punkt verschiebt und entscheidet, ob der Strahl gezeichnet werden soll (abhängig von externen Einstellungen). Normalerweise mache ich Vorberechnungen mit der berühmten Funktion von Igor Kim (Kim IV), die auch in der Bibliothek enthalten ist. Der pink Teil des Codes hat einen kommentierten Aufruf dieser Funktion. In diesem Fall, wenn die Punkte nach der Zeit berechnet werden, kann es jedoch zu einem Fehler kommen, der mit dem Wochenende zusammenhängt, was ich gerne vermeiden möchte. Natürlich ließe sich der Fehler leicht vermeiden, indem man die Linie nach dem Balkenindex berechnet und dann die Zahlen in echte Daten umrechnet. Allerdings scheint mir die derzeitige Implementierung klarer zu sein.

In dem in rosa hervorgehobenen Code wurden die grundlegenden Extrema bereits gefunden. Jetzt müssen wir nur noch die Linie ziehen. Zunächst zeichnen wir eine Linie zwischen zwei Basis-Extremstellen - hier sollten wir die Eigenschaft "Strahl" aktivieren, damit die Linie in die Zukunft gezeichnet wird (die Funktion TrendCreate ganz am Anfang dieses Blocks).

Berechnung der benötigten Zeit in der Zukunft anhand der Einstellungen:

selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );

Holen Sie sich dann den benötigten Preis mit der Standardfunktion.

selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);

Danach müssen wir nur noch den zweiten Punkt der Linie an die gewünschten Koordinaten verschieben und die Eigenschaft "real ray" setzen (standardmäßig wird diese Eigenschaft mit der Taste R für "Ray" (Strahl) umgeschaltet).

Sobald die Linie gezeichnet ist, sollte der Klick-Wartezustand ausgeschaltet werden - dies geschieht in den folgenden Zeilen: 

    m_Clicks_Count=0;
    ToggleFreeLineMode();

Der Code in anderen Blöcken dieser Funktion ist ein wenig komplizierter. Hier habe ich ein paar nützliche Funktionen hinzugefügt, um die Nutzbarkeit von geraden Linien zu verbessern.

Die erste Funktion bezieht sich auf den Effekt der Linienverschiebung bei kleineren Zeitrahmen. Wenn Sie Linien auf die übliche Art und Weise zeichnen, wird beim Wechsel zwischen den Zeitrahmen etwas Ähnliches erscheinen:

D1-Linie endet H4-Linie endet

Der linke Rand der Linie, der genau mit einem Extremum bei D1 zusammenfällt, ist bei H4 nach links verschoben und fällt somit nicht mit dem Extremum zusammen. Dies ist ein offensichtlicher Effekt, da das Extremum eines Tages nicht auf den Tagesanfang fallen muss. Wenn Sie beim manuellen Zeichnen eine höhere Genauigkeit benötigen, können Sie die Linie ungefähr zeichnen und dann zu niedrigeren Zeitrahmen wechseln, um die Kanten anzupassen.

Diese Lösung bietet sich an, wenn Sie nur einen oder zwei Charts haben. Was aber, wenn Sie 20 davon haben? Oder sogar 100? Das kann lästig sein.

Da das Programm über eine automatische Zeichenfunktion verfügt, können wir diese Aufgabe dem Programm anvertrauen, wenn wir jedes Objekt zeichnen.

Auf der Grundlage dieser Überlegungen habe ich die Funktion DeepPointSearch entwickelt.


Die Funktion DeepPointSearch

In der Funktion zum Zeichnen beliebiger Linien wird die Funktion DeepPointSearch zweimal aufgerufen, einmal für jeden Punkt. Die Funktion ist in der Utilities-Datei verfügbar. Ihr Code ist wie folgt:

//+------------------------------------------------------------------+
//| Search for a given point on lower timeframes                     |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _neededTime - start time on a higher timeframe        |
//|   bool _isUp - search by highs or by lows                        |
//|   ENUM_TIMEFRAMES _higher_TF - the highest period                |
//+------------------------------------------------------------------+
//| Return value:                                                    |
//|   More accurate date (on the lowest possible timeframe)          |
//+------------------------------------------------------------------+
datetime CUtilites::DeepPointSearch(
  datetime _neededTime,
  bool _isUp,
  ENUM_TIMEFRAMES _higher_TF=PERIOD_CURRENT
)
 {
//---
  //--- As a result it gets the most accurate time available
  datetime deepTime=0;
  //--- current timeframe
  ENUM_TIMEFRAMES currentTF;
  //--- The number of the highest timeframe in the list of all available periods
  int highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); 
  //--- The higher period in seconds
  int highTFSeconds = PeriodSeconds(_higher_TF);
  //--- Current interval in seconds
  int currentTFSeconds;
  //--- Counter
  int i;
  //--- Bar number on a higher timeframe
  int highBar=iBarShift(NULL,_higher_TF,_neededTime);
  //--- Bar number on the current timeframe
  int currentBar;
  //--- The total number of bars on the current timeframe
  int tfBarsCount;
  //--- How many bars of a lower TF fit into one bar of a higher TF
  int lowerBarsInHigherPeriod;
  //--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

//--- Loop sequentially through all timeframes
  for(i=0; i<highTFIndex; i++)
   {
    //--- Get a timeframe by a number in the list
    currentTF=GetTimeFrameByIndex(i);
//--- Check if this timeframe has the required time.
    tfBarsCount=iBars(NULL,currentTF);
    if(tfBarsCount>terminalMaxBars-1)
     {
      tfBarsCount=terminalMaxBars-1;
     }
    deepTime=iTime(NULL,currentTF,tfBarsCount-1);
//--- If it has, find it.
    if(deepTime>0 && deepTime<_neededTime)
     {
      currentTFSeconds=PeriodSeconds(currentTF);
      
      //--- Search for the required bar only within the higher TF candlestick
      lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds;
      currentBar = iBarShift(NULL,currentTF,_neededTime);
      
      if(_isUp)
       {
        currentBar = iHighest(
                       NULL,currentTF,MODE_HIGH,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );

       }
      else
       {
        currentBar = iLowest(
                       NULL,currentTF,MODE_LOW,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );
       }
      deepTime=iTime(NULL,currentTF,currentBar);
      //--- Once the required time is found, stop the search
      break;
     }
   }
//--- If reached the end of the loop
  if(i==highTFIndex)
   {
    //--- then the required time is only available on the higher timeframe.
    deepTime=_neededTime;
   }
//---
  return (deepTime);
 }

Für mich bestand die Hauptschwierigkeit darin, zu verstehen, wie das Hauptsuchfragment funktionieren sollte. Als Erstes muss natürlich festgestellt werden, ob der gewünschte Zeitpunkt in der Historie vorhanden ist. Wie Sie vielleicht wissen, enthalten niedrigere Zeitrahmen oft einige der Informationen nicht, die in höheren Zeitrahmen verfügbar sind. Die Standardfunktion iBars berechnet die Anzahl der Balken in der Historie. Dies ist jedoch nicht ausreichend, da das Terminal nur eine begrenzte Anzahl von Balken anzeigen kann. Gleich zu Beginn ermitteln wir mit folgendem Code, wie viele Balken das Terminal anzeigen kann.

//--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

Wenn die Historie zu viele Balken enthält, begrenzen Sie sie auf die angezeigten Balken.

Anschließend wird mit der Funktion iTime die Zeit des letzten Balkens in der Historie bestimmt. Ist diese Zeit größer als die gewünschte, dann ist es sinnlos, weiter zu suchen, da das höchste verfügbare Datum das jüngste ist, also wechseln Sie einfach zum nächsten, höheren TF. Liegt die letzte verfügbare Kerze im Terminal vor dem gesuchten Zeitpunkt, dann haben wir höchstwahrscheinlich die tiefste Stelle gefunden, an der dieser Punkt noch sinnvoll ist.

Die Routine beginnt nach allen Überprüfungen. Der gesuchte Punkt ist der extremste innerhalb des Bereichs der höheren TF-Kerze. Wir müssen nur bestimmen, wie viele Kerzen analysiert werden sollen. Danach helfen Standardfunktionen bei der Bestimmung des extremsten Extremums, auf dessen Grundlage wir die Zeit berechnen und die Arbeit beenden können.

In der aktuellen Implementierung der Bibliothek gilt diese Funktion nur für die Zeilen, die mit der Taste T und Q aufgerufen werden. In der nächsten Version wird diese Funktion jedoch für alle Instrumente verfügbar sein. Darüber hinaus plane ich, sie für jedes Instrument separat anpassbar zu machen.


Zeitkorrektur

Die zweite Besonderheit dieser Implementierung ist die Korrektur der Linien nach der Zeit. Die folgende Animation erklärt das Problem.

Achten Sie auf die Kontraktion des letzten Rechtecks. Das Ende der Linie, das mehr als einen Tag vom mittleren Rechteck entfernt war, ist nun sehr nahe an dieses herangekommen. Dementsprechend haben sich auch die oben genannten Punkte verschoben (man beachte das Verhalten der geraden Linie nahe der Spitze). Wenn sich die Linie zusammenzieht, entstehen neue Ausbrüche, die die Handelsstrategie beeinflussen können.

Dies mag für den Devisenmarkt nicht so entscheidend sein, wo die Ausbrüche etwa einmal pro Woche auftreten können. Aber auf dem Aktienmarkt können solche Zeitlücken je nach Börse jeden Tag auftreten, und oft geschieht dies innerhalb eines Tages.

Hier kommt die Automatisierung ins Spiel!

Damit das Teil wie gewünscht funktioniert, sollten wir irgendwie die "richtigen" Koordinaten speichern und sie dann nach Bedarf anpassen.

Ich habe die Beschreibung einer geraden Linie gewählt, um die Koordinaten zu speichern, da die meisten Händler bei der Erstellung automatischer Objekte keine Beschreibungen verwenden. Optional können wir Dateien mit einer Liste von Linien oder eine globale Terminalvariable verwenden, wenn es zu viele Linien gibt.

/* Graphics.mqh */

void CGraphics::DrawFreeLine(//...)
 {

//...
  string trendDescription="p2;";

//...
  trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());
  
//...
  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);

Als Nächstes wenden Sie die zuvor beschriebenen Aktionen auf die Koordinaten der "physischen" Linie an. Ich denke, der folgende Code ist ziemlich klar.

/* Utilites.mqh */

//+------------------------------------------------------------------+
//|  Adjusts the position of line end in the future in case of price |
//|   gaps                                                           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   string _line_name - the name of the line to be corrected       |
//+------------------------------------------------------------------+
void CUtilites::CorrectTrendFutureEnd(string _line_name)
 {
//---
  if(ObjectFind(0,_line_name)<0)
   {
    PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Object does not exist");
    //--- If there is no object to search, there is nothing more to do.
    return;
   }
  //--- Get a description
  string line_text=ObjectGetString(0,_line_name,OBJPROP_TEXT);
  
  string point_components[]; // array for point description fragments
  string name_components[];  // array containing line name fragments
  string helpful_name="Helpful line"; // the name of the auxiliary line
  string vertical_name=""; // the name of the corresponding vertical from the crosshair
  
  //--- Get the point time and price in string form
  int point_components_count=StringSplit(line_text,StringGetCharacter(";",0),point_components);
  
  datetime time_of_base_point; // time of the basic point
  datetime time_first_point,time_second_point; // the time of the first and the second point
  datetime time_far_ideal; // estimated time in the future
  double price_of_base_point; // the price of the basic point
  double price_first_point,price_second_point; // the prices of the first and the second point
  int i; // counter

//--- Check if the line is needed
  if(line_text=="" || point_components_count<3 || point_components[0]!="p2")
   {
    PrintDebugMessage(__FUNCTION__+" Error: the line cannot be used");
    return;
   }
//--- Get the coordinates of the "basic" point from the line description
  time_of_base_point=StringToTime(point_components[1]);
  price_of_base_point=StringToDouble(point_components[2]);
  if(time_of_base_point==0 || price_of_base_point==0)
   {
    PrintDebugMessage(__FUNCTION__+" Error: Unusable description");
    return;
   }
//--- Get the real coordinates of the line
  time_first_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,0);
  time_second_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,1);
  price_first_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,0);
  price_second_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,1);

//--- Create an auxiliary line (from the starting point to the base one)
  MakeHelpfulLine(
    time_first_point,
    price_first_point,
    time_of_base_point,
    price_of_base_point
  );

//--- Calculate the correct time for the current situation
  time_far_ideal=ObjectGetTimeByValue(0,helpful_name,price_second_point);
//---
  if(time_second_point != time_far_ideal)
   {
    //--- move the free end of the trend line
    ObjectMove(0,_line_name,1,time_far_ideal,price_second_point);
    //--- and the corresponding vertical
    StringSplit(_line_name,StringGetCharacter("_",0),name_components);
    for(i=0; i<ObjectsTotal(0,-1,OBJ_VLINE); i++)
     {
      vertical_name = ObjectName(0,i,-1,OBJ_VLINE);
      if(name_components[0]==StringSubstr(vertical_name,0,StringFind(vertical_name,"_",0)))
       {
        if((datetime)ObjectGetInteger(0,vertical_name,OBJPROP_TIME,0)==time_second_point)
         {
          ObjectMove(0,vertical_name,0,time_far_ideal,price_second_point);
          break;
         }
       }
     }
   }
  // Delete the auxiliary line
  RemoveHelpfulLine();
 }

Dieser Code sollte in bestimmten Abständen aufgerufen werden. Ich habe ihn auf den Beginn einer jeden neuen Stunde eingestellt.

/* Shortcuts.mq5 */

int OnCalculate(/*...*/)
 {
   //...
   if(CUtilites::IsNewBar(First_Start_True,PERIOD_H1))
   {
    for(i=0; i<all_lines_count; i++)
     {
      line_name=ObjectName(0,i,-1,OBJ_TREND);
      CUtilites::CorrectTrendFutureEnd(line_name);
      ChartRedraw();
     }
   }
   //...
 }


Die Tasten, die in der aktuellen Bibliothek implementiert sind

Aktion
 Schlüssel Bedeutung
 Wechsel des Zeitrahmen nach oben in den Haupt-Zeitrahmen (aus dem Panel der Zeitrahmen)  U  Up (aufwärts)
 Wechsel des Zeitrahmens nach unten  D  Down (abwärts)
 Wechsel der Chart Z-Ebenen (Chart ganz nach oben über allen Objekten oder nicht)  Z  Z order
 Zeichnen einer schrägen Trendlinie basierend auf zwei unidirektionalen Extrempunkten, die der Maus am nächsten liegen  T  Trendlinie
 Wechsel des Modus für Strahl (ray) für neue Linien
 R  Ray (Strahl)
 Zeichnen einer einfachen senkrechten Linie
 I(i) [Nur visuelle Vertikale]
 Zeichnen einer einfachen horizontalen Linie
 H  Horizontal
 Zeichnen von Andrews' Pitchfork
 P  Pitchfork
 Zeichnen eines Fibonacci-Fächers (VFun)
 F  Fun
 Zeichnen einer kurzen horizontalen Linie
 S  Short (kurz)
 Zeichnen einer längeren horizontalen Linie
 L  Long (lang)
 Zeichnen einer vertikalen Linie mit Level-Markierungen
 V  Vertical
 Zeichnen des Fadenkreuzes
 X  [Nur visuelles Kreuz]
 Zeichne eine Linie durch beliebige Extrempunkte
 Q  [Keine Übereinstimmung... "L" und "T" sind schon belegt]
 Zeichnen einer Gruppe von Rechtecken
 B  Box


Schlussfolgerung

Ich hoffe, dass Ihnen dieses Material helfen wird. Wenn Sie Kommentare oder Verbesserungsvorschläge haben, teilen Sie diese bitte in den Kommentaren zu diesem Artikel mit.

Außerdem plane ich, die Möglichkeit zu implementieren, Linien nicht nur an strengen Extrempunkten, sondern auch an Tangenten zu zeichnen.

Außerdem würde ich gerne etwas für Kanäle implementieren. Im Moment denke ich nur an die Arbeit mit äquidistanten Kanälen. Wenn jedoch jemand einen Kommentar hinterlässt oder mir eine PM schreibt, in der er vorschlägt, etwas anderes nach den Prinzipien der Bibliothek zu zeichnen, werde ich solche Vorschläge berücksichtigen.

Als weitere Verbesserung werde ich später das Speichern von Einstellungen in einer Datei (anstelle von oder zusammen mit den Indikatoreinstellungen über eine Eingangsvariable) und die Hinzufügung einer grafischen Schnittstelle implementieren, um die Änderung der Einstellungen im Fluss zu ermöglichen.


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/9914

Beigefügte Dateien |
Websockets für MetaTrader 5 — Unter Verwendung der Windows API Websockets für MetaTrader 5 — Unter Verwendung der Windows API
In diesem Artikel werden wir die WinHttp.dll verwenden, um einen Websocket-Client für MetaTrader 5-Programme zu erstellen. Der Client wird letztendlich als Klasse implementiert und auch gegen die Binary.com Websocket API getestet.
MQL5 Kochbuch – Der Wirtschaftskalender MQL5 Kochbuch – Der Wirtschaftskalender
Der Artikel hebt die Programmierfunktionen des Wirtschaftskalenders hervor und befasst sich mit der Erstellung einer Klasse für einen vereinfachten Zugriff auf die Kalendereigenschaften und den Empfang der Ereigniswerte. Als praktisches Beispiel dient die Entwicklung eines Indikators, der die nicht-kommerziellen Nettopositionen der CFTC verwendet.
Grafiken in der Bibliothek DoEasy (Teil 89): Programmieren von grafischen Standardobjekten, grundlegende Funktionsweise Grafiken in der Bibliothek DoEasy (Teil 89): Programmieren von grafischen Standardobjekten, grundlegende Funktionsweise
Derzeit ist die Bibliothek in der Lage, Standard-Grafikobjekte auf dem Client-Terminal-Chart zu kontrollieren, einschließlich ihrer Entfernung und Änderung einiger ihrer Parameter. Derzeit fehlt die Möglichkeit, grafische Standardobjekte aus nutzerdefinierten Programmen zu erstellen.
Ein Versuch, einen EA-Konstruktor zu entwickeln Ein Versuch, einen EA-Konstruktor zu entwickeln
In diesem Artikel biete ich eine Reihe von Handelsfunktionen in Form eines fertigen EA an. Diese Methode ermöglicht es, durch einfaches Hinzufügen von Indikatoren und Ändern von Eingaben mehrere Handelsstrategien zu erstellen.