English Русский Español 日本語 Português
preview
Entwicklung eines Replay System (Teil 26): Expert Advisor Projekt — die Klasse C_Terminal

Entwicklung eines Replay System (Teil 26): Expert Advisor Projekt — die Klasse C_Terminal

MetaTrader 5Tester | 11 März 2024, 17:24
120 0
Daniel Jose
Daniel Jose

Einführung

Im vorherigen Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 25): Vorbereitung auf die nächste Phase“, wir haben das Replay oder Wiedergabe-/Simulationssystem für die grundlegende Nutzung vorbereitet. Wir brauchen jedoch mehr als nur die Analyse vergangener Bewegungen oder möglicher Aktionen. Wir brauchen nämlich ein Instrument, mit dem wir gezielte Untersuchungen durchführen können, als ob wir auf einem echten Markt arbeiten würden. Zu diesem Zweck müssen wir einen Expert Advisor erstellen, um eine tiefer gehende Recherche durchzuführen. Darüber hinaus beabsichtigen wir, einen universellen Expert Advisor zu entwickeln, der für verschiedene Märkte (Aktien und Devisen) anwendbar ist und auch mit unserem Replay/Simulationssystem kompatibel ist.

In Anbetracht des Umfangs des Projekts wird die Aufgabe sehr ernst sein. Die Komplexität der Entwicklung ist jedoch nicht so hoch, wie es scheint, da wir den Großteil des Prozesses bereits in früheren Artikeln behandelt haben. Unter ihnen sind: „Einen Trading Expert Advisor von Grund auf entwickeln (Teil 31): Auf dem Weg in die Zukunft (IV)“ und „Erstellen eines EA, der automatisch funktioniert (Teil 15): Automatisierung (VII)“. In diesen Artikeln habe ich ausführlich beschrieben, wie man einen vollautomatischen Expert Advisor erstellt. Trotz dieser Materialien haben wir es hier mit einer einzigartigen und noch schwierigeren Herausforderung zu tun: Die MetaTrader 5-Plattform soll eine Verbindung zu einem Handelsserver simulieren und so eine realistische Simulation des offenen Marktes fördern. Die Aufgabe ist zweifelsohne sehr komplex.

Trotzdem sollten wir uns von der anfänglichen Komplexität nicht einschüchtern lassen. Irgendwo müssen wir anfangen, sonst enden wir damit, dass wir über die Schwierigkeit einer Aufgabe nachgrübeln, ohne überhaupt zu versuchen, sie zu bewältigen. Genau darum geht es beim Programmieren: Hindernisse durch Lernen, Testen und umfassende Forschung zu überwinden. Bevor wir beginnen, möchte ich sagen, dass es mir sehr viel Spaß macht, zu skizzieren und zu erklären, wie die Dinge eigentlich entstehen. Ich glaube, dass viele aus dieser Artikelserie gelernt haben, indem sie über die üblichen Grundlagen hinausgegangen sind und getestet haben, dass wir viel mehr erreichen können, als manche für möglich halten.


Expert Advisor-Implementierungskonzepte

Sie haben vielleicht schon bemerkt, dass ich ein großer Fan der objektorientierten Programmierung (OOP) bin. Dies ist auf die umfangreichen Möglichkeiten zurückzuführen, die OOP bietet. Außerdem bietet es eine Möglichkeit, von Anfang an robusten, sicheren und zuverlässigen Code zu erstellen. Zunächst müssen wir uns eine erste Vorstellung davon machen, was wir benötigen, indem wir die Struktur des Projekts organisieren. Da ich sowohl als Anwender als auch als Programmierer Erfahrung habe, wurde mir klar, dass ein Expert Advisor nur dann wirklich effektiv sein kann, wenn er die Ressourcen nutzt, die uns immer zur Verfügung stehen: Tastatur und Maus. Da die MetaTrader 5-Plattform auf Charts basiert, ist die Verwendung der Maus zur Interaktion mit grafischen Elementen unerlässlich. Aber auch die Tastatur spielt eine wichtige Rolle bei der Unterstützung in verschiedenen Bereichen. Die Diskussion geht jedoch über die Verwendung von Maus und Tastatur hinaus, die in der Automatisierungsserie behandelt wird. In einigen Fällen kann eine vollständige Automatisierung auch ohne diese Werkzeuge erreicht werden, aber wenn man sich für deren Einsatz entscheidet, ist es wichtig, die Art des auszuführenden Vorgangs zu berücksichtigen. Daher sind nicht alle Expert Advisors für alle Arten von Vermögenswerten gut geeignet.

Der Grund dafür ist, dass einige der Vermögenswerte eine Preisbewegung von 0,01 aufweisen. Andere haben vielleicht 0,5, wieder andere 5. Im Falle von Forex weichen diese Werte erheblich von den genannten Beispielen ab. Aufgrund dieser Vielfalt an Werten entscheiden sich einige Programmierer dafür, EAs speziell für bestimmte Vermögenswerte zu entwickeln. Der Grund liegt auf der Hand: Der Handelsserver akzeptiert keine willkürlichen Werte; wir müssen uns an die vom Server festgelegten Regeln halten. Dasselbe Prinzip gilt für das Wiedergabe-/Simulationssystem. Wir können nicht zulassen, dass der EA Aufträge mit zufälligen Werten ausführt.

Die Einführung dieser Beschränkung ist nicht nur notwendig, sondern extrem notwendig. Es macht keinen Sinn, funktionierende Replays/Simulationen für das Training zu haben, wenn sich das System beim Handel auf einem echten Konto völlig anders verhält. Es ist daher wichtig, dass das System eine gewisse Standardisierung beibehält und sich so weit wie möglich an die Realität eines echten Kontos anpasst. Daher ist es notwendig, einen EA zu entwickeln, der so arbeitet, als würde er direkt mit dem Handelsserver interagieren, unabhängig von den Umständen.


Beginnen wir mit der ersten Klasse: der Klasse C_Terminal

Zwar ist es oft möglich, den gesamten Code ohne spezielle Anleitung zu schreiben, doch ist dieser Ansatz für Projekte, die sehr groß und komplex werden können, nicht zu empfehlen. Wir wissen noch nicht genau, wie sich das Projekt entwickeln wird, aber es ist wichtig, dass wir uns zu Beginn immer auf die besten Programmierpraktiken konzentrieren. Ohne eine angemessene Projektplanung entsteht sonst eine riesige Menge an unordentlichem Code. Deshalb ist es wichtig, von Anfang an in großen Dimensionen zu denken, auch wenn sich das Projekt als nicht so grandios oder komplex erweist. Die Anwendung von Best Practices macht uns auch bei kleinen Projekten organisierter und lehrt uns, einer stabilen Methodik zu folgen. Beginnen wir mit der Entwicklung der ersten Klasse. Zu diesem Zweck erstellen wir eine neue Header-Datei mit dem Namen C_Terminal.mqh. Es hat sich bewährt, der Datei denselben Namen wie der Klasse zu geben, damit sie bei der Arbeit mit ihr leichter zu finden ist. Der Code beginnt wie folgt:

class C_Terminal
{

       protected:
   private  :
   public   :
};

In dem Artikel „Erstellen eines EA, der automatisch arbeitet (Teil 05): Manuelle Auslöser (II)“, haben wir einige grundlegende Konzepte über Klassen und reservierte Wörter betrachtet. Es lohnt sich, einen Blick darauf zu werfen, wenn Sie mit der Serie zur Erstellung eines automatisierten EA nicht vertraut sind, da sie viele der Elemente enthält, die wir hier verwenden werden. Dieser Kodex ist jedoch veraltet und völlig überholt. Hier werden wir einen Code sehen, der eine neue Form der Interaktion bietet, weil wir bestimmte Probleme angehen und das System noch robuster, zuverlässiger und effizienter machen müssen. Das erste, was im Klassencode erscheint, nachdem Sie mit der Codierung begonnen haben, ist die Struktur, die im Folgenden ausführlich beschrieben wird:

class C_Terminal
{
   protected:
//+------------------------------------------------------------------+
      struct st_Terminal
      {
         long    ID;
         string  szSymbol;
         int     Width,
                 Height,
                 nDigits;
         double  PointPerTick,
                 ValuePerPoint,
                 VolumeMinimal,
                 AdjustToTrade;
      };
//+------------------------------------------------------------------+

Es ist zu beachten, dass diese Struktur innerhalb des privaten Teils des Codes deklariert wird, was für unsere Arbeit entscheidend ist. Es ist interessant, dass wir hier keine Variablen deklarieren. Tatsächlich muss jede globale Variable innerhalb einer Klasse in einem privaten Teil des Codes deklariert werden, was den höchsten Grad an Sicherheit und Informationskapselung bietet. Wir werden im Laufe der Umsetzung auf dieses Thema zurückkommen, um es besser zu verstehen. Als beste Programmierpraxis sollten Sie niemals zulassen, dass auf interne Klassenvariablen von anderen Teilen des Codes aus zugegriffen wird, die nicht zu der Klasse gehören.

Schauen wir uns nun an, wie Klassenvariablen deklariert werden:

   private :
      st_Terminal m_Infos;

Im Moment haben wir nur eine globale und private Variable in der Klasse C_Terminal, in der wir die entsprechenden Daten speichern werden. Wir werden uns später ansehen, wie man auf diese Daten von außerhalb der Klasse zugreifen kann. An dieser Stelle ist es sehr wichtig, daran zu denken, dass keine Informationen nach außen dringen oder ohne Erlaubnis in die Klasse gelangen können. Die Einhaltung dieses Konzepts ist entscheidend. Viele neue Programmierer erlauben es, dass Code außerhalb der Klasse die Werte interner Variablen ändert, was ein Fehler ist, auch wenn der Compiler dies nicht als Fehler anzeigt. Diese Praxis gefährdet die Kapselung und macht den Code deutlich unsicherer und weniger handhabbar, da das Ändern eines Wertes ohne Kenntnis der Klasse zu Fehlern und Abstürzen führen kann, die schwer zu erkennen und zu beheben sind.

Danach müssen Sie eine neue Header-Datei erstellen, um die Struktur zu erhalten. Diese Datei mit dem Namen Macros.mqh enthält zunächst nur eine Zeile.

#define macroGetDate(A) (A - (A % 86400))

Diese Zeile dient zur Hervorhebung der Datumsangaben. Die Wahl eines Makros anstelle einer Funktion mag ungewöhnlich erscheinen, aber in vielen Situationen ist die Verwendung von Makros sinnvoller. Dies liegt daran, dass das Makro als Inline-Funktion in den Code eingefügt wird, sodass es so schnell wie möglich ausgeführt werden kann. Die Verwendung von Makros ist auch dadurch gerechtfertigt, dass die Wahrscheinlichkeit erheblicher Programmierfehler verringert wird, insbesondere wenn die eine oder andere Umstrukturierung im Code mehrmals wiederholt werden muss.

Hinweis: In diesem System werde ich an einigen Stellen versuchen, eine höhere Programmiersprache zu verwenden, um das Lesen und Verstehen zu erleichtern, insbesondere für diejenigen, die mit dem Programmieren beginnen. Die Verwendung einer Hochsprache bedeutet nicht, dass der Code langsamer ist, sondern eher, dass er leichter zu lesen ist. Ich werde Ihnen zeigen, wie wir dies auf unsere Codes anwenden können.

Wann immer es möglich ist, sollten Sie versuchen, den Code in einer Hochsprache zu schreiben, da dies die Fehlersuche und die Verbesserung erheblich erleichtert. Denken Sie auch daran, dass der Code nicht nur für die Maschine geschrieben wird, da auch andere Programmierer ihn verstehen müssen.

Nachdem wir die Header-Datei Macros.mqh erstellt haben, in der alle globalen Makros definiert sind, fügen wir diese Datei in die Header-Datei C_Terminal.mqh ein. Sie ist wie folgt enthalten:

#include "Macros.mqh"

Beachten Sie, dass der Name der Header-Datei in doppelte Anführungszeichen gesetzt wird. Warum wird es so angegeben und nicht zwischen dem Kleiner- und dem Größer-Zeichen(< >)? Gibt es dafür einen besonderen Grund? Ja, die gibt es. Durch die Verwendung von Anführungszeichen teilen wir dem Compiler mit, dass der Pfad zur Header-Datei in dem Verzeichnis beginnen soll, in dem sich die aktuelle Header-Datei befindet, in diesem Fall C_Terminal.mqh. Da kein spezifischer Pfad angegeben ist, sucht der Compiler die Datei Macros.mqh im selben Verzeichnis wie die Datei C_Terminal.mqh. Wenn also die Verzeichnisstruktur des Projekts geändert wurde, wir aber die Datei Macros.mqh im gleichen Verzeichnis wie die Datei C_Terminal.mqh speichern, brauchen wir dem Compiler den neuen Pfad nicht mitzuteilen.

Mit einem Namen, der in ein Kleiner-als- und ein Größer-als-Zeichen(< >) eingeschlossen ist, weisen wir den Compiler an, die Datei in einem vordefinierten Verzeichnis auf dem Build-System zu suchen. Bei MQL5 ist dieses Verzeichnis INCLUDE. Daher muss jeder Pfad zur Datei Macros.mqh von diesem Verzeichnis INCLUDE aus angegeben werden, das sich im Ordner MQL5 befindet. Wenn sich die Verzeichnisstruktur des Projekts ändert, müssen alle Pfade neu definiert werden, damit der Compiler die Header-Dateien finden kann. Auch wenn dies wie eine Kleinigkeit erscheinen mag, kann die Wahl einer bestimmten Methode einen großen Unterschied machen.

Nachdem wir nun diesen Unterschied verstanden haben, wollen wir uns den ersten Code der Klasse C_Terminal ansehen. Dieser Code ist für die Klasse privat und kann daher nicht von außen eingesehen werden:

void CurrentSymbol(void)
   {
      MqlDateTime mdt1;
      string sz0, sz1;
      datetime dt = macroGetDate(TimeCurrent(mdt1));
      enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
                
      sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
      for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
      switch (eTS)
      {
         case DOL:
         case WDO: sz1 = "FGHJKMNQUVXZ"; break;
         case IND:
         case WIN: sz1 = "GJMQVZ";       break;
         default : return;
      }
      for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
      if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;
   }

Der vorgestellte Code mag komplex und verwirrend erscheinen, aber er hat eine ganz bestimmte Funktion: Er soll einen Asset-Namen generieren, damit er im Cross-Order-System verwendet werden kann. Um zu verstehen, wie dies zu bewerkstelligen ist, sollten wir den Prozess genau analysieren. Derzeit liegt der Schwerpunkt auf der Schaffung von Namen für Index-Futures und Dollar-Futures-Transaktionen gemäß der B3 (Brasilianische Börse). Wenn man die Logik hinter der Erstellung dieser Namen versteht, kann der Code so angepasst werden, dass er die Namen aller zukünftigen Kontrakte generiert, sodass diese Kontrakte über ein Cross-Order-System operieren können, wie bereits in dem Artikel „Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 11): System von Kreuzaufträgen“. Ziel ist es jedoch, diese Funktionalität so zu erweitern, dass sich der EA an unterschiedliche Bedingungen, Szenarien, Vermögenswerte oder Märkte anpassen kann. Dies setzt voraus, dass die EA in der Lage ist zu bestimmen, um welche Art von Vermögenswert es sich handelt, was dazu führen kann, dass mehr verschiedene Arten von Vermögenswerten in den Kodex aufgenommen werden müssen. Um es besser zu erklären, sollten wir es in kleinere Teile zerlegen.

MqlDateTime mdt1;
string sz0, sz1;
datetime dt = macroGetDate(TimeCurrent(mdt1));

Diese drei Zeilen sind die Variablen, die im Code verwendet werden sollen. Das Hauptproblem liegt hier möglicherweise in der Art und Weise, wie diese Variablen initialisiert werden. Die Funktion TimeCurrent wird verwendet, um zwei verschiedene Variablen in einem Schritt zu initialisieren. Die erste Variable mdt1 ist eine Struktur vom Typ MqlDateTime, die detaillierte Datums- und Zeitinformationen in der Variablen mdt1 speichert, während TimeCurrent ebenfalls den in mdt1 gespeicherten Wert zurückgibt. Die zweite Variable dt verwendet ein Makro, um einen Datumswert zu extrahieren und zu speichern, sodass zwei Variablen in einer Zeile Code vollständig initialisiert werden können.

enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;

Diese Zeile mag ungewöhnlich erscheinen, weil wir hier gleichzeitig eine Enumeration erstellen, eine Variable deklarieren und ihr einen Anfangswert zuweisen. Das Verständnis dieser Zeile ist der Schlüssel zum Verständnis des anderen Teils der Funktion. Bitte beachten Sie die folgenden Punkte: In MQL5 kann eine Enumeration nicht ohne einen Namen erstellt werden, daher geben wir den Namen ganz am Anfang an. Innerhalb der Enumeration gibt es Elemente, die standardmäßig mit einem Nullwert beginnen. Dies kann geändert werden, aber wir werden uns später damit befassen. Denken Sie daran: Standardmäßig beginnt die Enumeration bei Null. Der Wert von WIN ist also Null, IND ist Eins, WDO ist Zwei usw. Aus einem Grund, der später erläutert wird, muss das letzte Element OTHER sein, unabhängig von der Anzahl der Elemente, die wir aufnehmen wollen. Nachdem wir eine Enumeration definiert haben, deklarieren wir eine Variable, die die Daten dieser Enumeration verwenden wird, beginnend mit dem Wert dieser Variable als Wert des letzten Elements, d. h. OTHER.

Wichtiger Hinweis: Sehen Sie sich die Deklaration der Enumeration an. Kommt Ihnen das nicht bekannt vor? Bitte beachten Sie, dass die Namen auch in Großbuchstaben angegeben werden, was sehr wichtig ist. Was passiert, ist: Wenn wir zusätzliche Anlagen zur Verwendung in einem zukünftigen Vertrag hinzufügen wollen, müssen wir dies tun, indem wir die ersten drei Zeichen des Vertragsnamens vor dem Element OTHER hinzufügen, damit die Funktion den Namen des aktuellen Vertrags korrekt generieren kann. Wenn wir zum Beispiel einen Bullen-Kontrakt hinzufügen wollen, müssen wir den BGI-Wert in die Enumeration einfügen, und das ist der erste Schritt. Es gibt noch einen weiteren Schritt, den wir später besprechen werden. Ein anderes Beispiel: Wenn wir einen Mais-Futures-Kontrakt hinzufügen wollen, fügen wir den CCM-Wert usw. immer vor OTHER hinzu. Andernfalls wird die Enumeration nicht wie erwartet funktionieren.

Betrachten Sie nun das folgende Stück Code. Zusammen mit der oben beschriebenen Enumeration werden wir den ersten Zyklus der Arbeit abschließen.

   sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
   for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
   switch (eTS)
   {
      case DOL:
      case WDO: sz1 = "FGHJKMNQUVXZ"; break;
      case IND:
      case WIN: sz1 = "GJMQVZ";       break;
      default : return;
   }

Die erste Aktion besteht darin, den Namen des Assets in einer privaten globalen Klassenvariablen zu speichern. Um den Prozess zu vereinfachen, verwenden wir die Funktion StringSubstr, um die ersten drei Buchstaben des Asset-Namens zu erfassen, in dem der Klassencode ausgeführt wird, und speichern sie in der Variablen sz0. Dies ist die einfachste Phase. Lassen Sie uns nun etwas sehr Ungewöhnliches, aber Mögliches tun: eine Enumeration verwenden, um zu bestimmen, welche Benennungsregel auf den Vertrag angewendet werden soll. Zu diesem Zweck verwenden wir die for-Schleife. Der Ausdruck, der in dieser Schleife verwendet wird, mag recht seltsam anmuten, aber wir durchlaufen die Enumeration auf der Suche nach dem ursprünglich in der Enumeration definierten Vertragsnamen, wie ich oben erklärt habe. Da die Standardenumeration bei Null beginnt, wird auch unsere lokale Schleifenvariable bei Null beginnen. Unabhängig davon, welches Element an erster Stelle steht, beginnt die Schleife dort und wird so lange fortgesetzt, bis das Element OTHER gefunden wird oder bis die Variable eTS von OTHER verschieden ist. Bei jeder Iteration erhöhen wir die Position innerhalb der Enumeration. Jetzt kommt der interessante Teil: In MQL5 verwenden wir die Funktion EnumToString, um den Enumerationswert bei jeder Iteration der Schleife in einen String umzuwandeln und ihn mit dem Wert in der Variablen sz0 zu vergleichen. Wenn diese Werte übereinstimmen, wird die Position in der eTS-Variablen gespeichert, wodurch sie sich von OTHER unterscheidet. Diese Funktion ist sehr interessant und zeigt, dass die Enumeration in MQL5 nicht so behandelt werden sollte wie in anderen Programmiersprachen. Hier können Sie sich eine Enumeration als ein Array von Zeichenketten vorstellen, das viel mehr Funktionalität und Praktikabilität bietet als in anderen Sprachen.

Nachdem der gewünschte Wert in der Variablen eTS definiert wurde, muss im nächsten Schritt eine spezifische Benennungsregel für jeden Vertrag festgelegt werden, was die entsprechende Initialisierung der Variablen sz1 erfordert. Die Auswahl des nächsten Buchstabens in sz1 hängt von der Suche nach dem spezifischen Vertrag ab, den wir der Benennungsregel hinzufügen wollen, und folgt der hier vorgestellten Methodik.

Wenn die Anlage nicht in der Enumeration enthalten ist und die entsprechende Regel nicht gefunden wurde, wird die Funktion beendet. Dies gilt insbesondere dann, wenn wir ein Asset im Wiederholungs-/Simulationsmodus verwenden, da diese Art von Asset von Natur aus personalisiert und speziell ist. In diesen Fällen endet die Funktion hier.

Jetzt werden wir eine weitere Schleife untersuchen, das ist die Phase, in der „alles noch komplizierter wird“. Die Komplexität dieser Schleife kann viele Programmierer verwirren und macht es schwierig, ihre Funktionalität zu verstehen. Daher ist es wichtig, der folgenden Erklärung noch mehr Aufmerksamkeit zu schenken. Hier ist der Code für diese Schleife:

for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
	if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;

Obwohl der Code auf den ersten Blick verwirrend und komplex erscheinen mag, ist er eigentlich ganz einfach. Der Code wurde gekürzt, um ihn effizienter zu machen. Um die Dinge zu vereinfachen und unnötige Komplexität zu vermeiden, verwenden wir den Befehl IF, obwohl dies nicht unbedingt notwendig ist. Theoretisch kann der gesamte Befehl in die FOR-Schleife aufgenommen werden, aber das würde die Erklärung ein wenig verkomplizieren. Deshalb verwenden wir in dieser Schleife IF, um die Übereinstimmung zwischen dem Namen des generierten Kontrakts und dem Namen auf dem Handelsserver zu überprüfen, um zu bestimmen, welcher der zukünftigen Kontrakte der relevanteste ist. Um diesen Prozess zu verstehen, ist es wichtig, die Namenskonvention zu kennen, die zur Erstellung des Vertragsnamens verwendet wird. Betrachten wir als Beispiel, was mit einem an der brasilianischen Börse gehandelten Mini-Dollar-Futures-Kontrakt geschieht, der einer bestimmten Nomenklaturregel folgt:

  • Die ersten 3 Zeichen des Vertragsnamens lauten WDO. Unabhängig vom Verfallsdatum oder davon, ob es sich um einen historischen Vertrag handelt oder nicht.
  • Als Nächstes haben wir ein Symbol, das den Monat des Verfalls angibt.
  • Danach folgt ein zweistelliger Wert, der das Jahr des Ablaufs angibt.

Wir konstruieren also mathematisch den Namen des Vertrags, und genau das tut diese Schleife. Mithilfe einfacher mathematischer Regeln und einer Schleife erstellen wir einen Vertragsnamen und überprüfen seine Gültigkeit. Daher ist es wichtig, den Erklärungen zu folgen, um zu verstehen, wie es gemacht wird.

Zunächst initialisieren wir drei lokale Variablen in der Schleife, die als die benötigten Buchungseinheiten fungieren werden. Die Schleife führt ihre erste Iteration aus, die, was besonders interessant ist, nicht innerhalb des Schleifenkörpers, sondern im if-Befehl stattfindet. Allerdings kann derselbe Code wie im if-Befehl zwischen den Semikolons (;) in der for-Schleife platziert werden, und die Schleife wird identisch funktionieren. Wir wollen verstehen, was bei dieser Interaktion geschieht. Zunächst wird der Name des Vertrags erstellt, wobei bestimmte Regeln für seine Bildung gelten. Mit der Funktion StringFormat erhalten wir den gewünschten Namen, der als Symbolname gespeichert wird, auf den wir später zugreifen können. Wenn wir den Kontraktnamen bereits kennen, fordern wir vom Handelsserver eine der Eigenschaften des Vermögenswerts an - die Verfallszeit des Kontraktes unter Verwendung der Enumeration SYMBOL_EXPIRATION_TIME. Die Funktion SymbolInfoInteger gibt einen Wert zurück, während wir nur an dem Datum interessiert sind. Um genau diesen Wert zu extrahieren, verwenden wir ein Makro, das es uns ermöglicht, das Verfallsdatum mit dem aktuellen Datum zu vergleichen. Wenn der Rückgabewert ein zukünftiges Datum ist, wird die Schleife beendet, da wir den letzten Vertrag in der Variablen definiert haben. In der Anfangsphase ist dies jedoch unwahrscheinlich, da das Jahr 2000 beginnt, also bereits in der Vergangenheit liegt, sodass eine neue Iteration erforderlich sein wird. Bevor wir den gesamten beschriebenen Prozess wiederholen, müssen wir die Position erhöhen, um einen neuen Vertragsnamen zu erstellen. Hier ist Vorsicht geboten, da diese Erhöhung zuerst im Verfallscode vorgenommen werden muss. Nur wenn keiner der Verfallscodes in diesem Jahr zufriedenstellend ist, erhöhen wir das Jahr. Diese Aktion wird in drei Stufen durchgeführt. Im Code verwenden wir zwei ternäre Operatoren, um dieses Inkrement durchzuführen.

Bevor die Schleife erneut durchlaufen wird und noch bevor die ternären Operatoren ausgeführt werden, erhöhen wir den Wert, der das Symbol für den Verfallsmonat angibt. Nach dieser Inkrementierung wird mit Hilfe des ersten ternären Operators geprüft, ob der Wert innerhalb akzeptabler Grenzen liegt. Der Index zeigt also immer einen der für den Verfallsmonat gültigen Werte an. Der nächste Schritt ist die Überprüfung des Verfallsmonats mit dem zweiten ternären Operator. Ist der Index des Verfallsmonats gleich Null, wurden alle Monate geprüft. Dann erhöhen wir das aktuelle Jahr für einen neuen Versuch, einen gültigen Vertrag zu finden, und diese Prüfung wird im if-Befehl erneut durchgeführt. Der Vorgang selbst wird so lange wiederholt, bis ein gültiger Vertrag gefunden wird, was zeigt, wie das System den Namen des aktuellen Vertrags nachschlägt. Dies ist keine Zauberei, sondern eine Kombination aus Mathematik und Programmierung.

Ich hoffe, die Erläuterungen haben Ihnen geholfen zu verstehen, wie der Code für dieses Verfahren funktioniert. Trotz der Komplexität und der Länge des Textes war es mein Ziel, ihn auf eine zugängliche Art und Weise zu erklären, sodass Sie das gleiche Konzept anwenden können, um Funktionen für andere zukünftige Verträge zu implementieren, die es ihnen ermöglichen, die Geschichte zu verarbeiten. Dies ist wichtig, denn unabhängig davon, ob Verträge existieren oder nicht, wird der Code immer den richtigen Vertrag verwenden.

Analysieren wir nun den folgenden Code, der auf unseren Klassenkonstruktor verweist:

C_Terminal()
{
   m_Infos.ID = ChartID();
   CurrentSymbol();
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
   m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
   m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
   m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
   m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
   m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
   m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
}

Dieser Code stellt sicher, dass die Werte in der globalen Variablenstruktur korrekt initialisiert werden. Achten Sie auf die Teile, die das Verhalten der MetaTrader 5 Plattform verändern. Das geschieht folgendermaßen. Wir bitten die MetaTrader 5 Plattform, keine Beschreibungen von Objekten auf dem Chart zu generieren, auf die dieser Code angewendet wird. In einer weiteren Zeile geben wir an, dass die MetaTrader 5-Plattform jedes Mal, wenn ein Objekt aus dem Chart entfernt wird, ein Ereignis erzeugen soll, das anzeigt, welches Objekt entfernt wurde, und in dieser Zeile geben wir die Entfernung der Zeitskala an. Das ist alles, was wir in diesem Stadium brauchen. In weiteren Zeilen werden Informationen über den Vermögenswert gesammelt.

Der nächste Code ist der Destruktor der Klasse:

~C_Terminal()
{
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
}

In diesem Destruktorcode setzen wir die Bedingungen zurück, die vor der Ausführung des Klassenkonstruktorcodes bestanden, und versetzen das Chart in seinen ursprünglichen Zustand zurück. Dies ist zwar nicht mehr genau der ursprüngliche Zustand, aber die Zeitskala wird wieder auf dem Chart sichtbar sein. Lassen Sie uns nun den Fall lösen, dass das Verhalten des Charts durch die Klasse geändert werden kann. Wir werden eine kleine Struktur erstellen und den Konstruktor- und Destruktor-Code so ändern, dass das Chart tatsächlich in den Zustand zurückkehrt, in dem es sich befand, bevor es von der Klasse geändert wurde. Dies geschieht wie folgt:


   private :
      st_Terminal m_Infos;
      struct mem
      {
         long    Show_Descr,
                 Show_Date;
      }m_Mem;
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+          
      C_Terminal()
      {
         m_Infos.ID = ChartID();
         CurrentSymbol();
         m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR);
         m_Mem.Show_Date  = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
         m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
         m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);                                
	 m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
         m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
         m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
         m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
      }
//+------------------------------------------------------------------+
      ~C_Terminal()
      {
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
      }

Diese globale Variable stellt die Struktur dar, in der die Daten gespeichert werden sollen. Auf diese Weise kann die Klasse wissen, wie das Chart aussah, bevor es durch den Code verändert wurde. Wir erfassen hier die Daten vor der Änderung und versetzen das Chart ab diesem Zeitpunkt in seinen ursprünglichen Zustand zurück. Beachten Sie, wie eine einfache Codeänderung das System für uns angenehmer und bequemer machen kann. Es ist erwähnenswert, dass eine globale Variable Daten für die gesamte Lebensdauer der Klasse speichert. Um dies zu verstehen, sollten wir uns eine Klasse jedoch nicht nur als einen Code-Satz vorstellen - es ist sehr wichtig, sich eine Klasse wie ein Objekt oder eine spezielle Variable vorzustellen. Wenn es erstellt wird, wird der Konstruktorcode ausgeführt, und wenn es gelöscht oder nicht mehr benötigt wird, wird der Destruktorcode aufgerufen. Dies geschieht automatisch. Wenn Sie noch nicht ganz verstehen, wie das funktioniert, machen Sie sich keine Sorgen, das Konzept wird Ihnen später noch klarer werden. Für den Moment müssen Sie Folgendes verstehen: Eine Klasse ist nicht einfach nur ein Haufen Code, sondern etwas Besonderes und sollte auch als solches behandelt werden.

Bevor wir dieses Thema abschließen, wollen wir noch einen kurzen Blick auf zwei andere Funktionen werfen. Wir werden sie im nächsten Artikel im Detail betrachten, aber jetzt sehen wir uns erst einmal einen Teil ihres Codes an. Hier ist der Code:

//+------------------------------------------------------------------+
inline const st_Terminal GetInfoTerminal(void) const 
{

   return m_Infos;
}
//+------------------------------------------------------------------+
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{

   switch (id)
   {
      case CHARTEVENT_CHART_CHANGE:
         m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
         m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         break;
   }
}
//+------------------------------------------------------------------+

Diese beiden Funktionen sind in jeder Hinsicht besonders. Ich werde sie nur kurz erläutern, da sie besser erklärt werden, wenn wir sie verwenden. Diese Funktion ermöglicht jedem Code außerhalb des Klassenkörpers den Zugriff auf die Daten einer globalen Variable, die in der Klasse enthalten ist. Dieser Aspekt wird in dem von uns zu entwickelnden Code ausführlich behandelt werden. Auf diese Weise stellen wir sicher, dass keine Gefahr besteht, die Werte einer Variablen zu ändern, ohne dass die Klasse davon weiß, da wir den Compiler nutzen, um diese Art von Problemen zu vermeiden. Allerdings gibt es hier ein Problem, das wir in Zukunft lösen werden. Diese Funktion wird bereits verwendet, um Klassendaten zu aktualisieren, wenn sich das Chart ändert. Diese Werte werden oft in anderen Teilen des Codes verwendet, wenn wir etwas im Chart zeichnen. Auch dies werden wir in der Zukunft im Detail sehen.


Schlussfolgerung

Aus dem Material, das in diesem Artikel behandelt wurde, haben wir bereits unsere Basisklasse C_Terminal gebildet. Es gibt jedoch noch eine Funktion, die wir diskutieren müssen. Dies werden wir im nächsten Artikel tun, in dem wir die Klasse C_Mouse erstellen werden. Was wir hier behandelt haben, ermöglicht es uns, die Klasse zu nutzen, um etwas Nützliches zu schaffen. Ich füge hier keinen entsprechenden Code bei, da unsere Arbeit gerade erst beginnt. Jeder Code, der jetzt vorgelegt wird, wird keinen praktischen Nutzen haben. Im nächsten Artikel werden wir etwas wirklich Nützliches erstellen, damit Sie mit Ihrem Chart beginnen können. Wir werden Indikatoren und andere Hilfsmittel entwickeln, die den Betrieb von Demo- und Realkonten und sogar des Replay-/Simulationssystems unterstützen. Wir sehen uns im nächsten Artikel!

Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/11328

Von der Saisonalität des Devisenmarktes profitieren Von der Saisonalität des Devisenmarktes profitieren
Wir sind alle mit dem Konzept der Saisonalität vertraut, z. B. sind wir alle an steigende Preise für frisches Gemüse im Winter oder an steigende Kraftstoffpreise bei strengem Frost gewöhnt, aber nur wenige Menschen wissen, dass es auf dem Devisenmarkt ähnliche Muster gibt.
Entwicklung eines Replay Systems — Marktsimulation (Teil 25): Vorbereitungen für die nächste Phase Entwicklung eines Replay Systems — Marktsimulation (Teil 25): Vorbereitungen für die nächste Phase
In diesem Artikel schließen wir die erste Phase der Entwicklung unseres Replay- und Simulationssystems ab. Liebe Leserin, lieber Leser, damit bestätige ich, dass das System ein fortgeschrittenes Niveau erreicht hat und den Weg für die Einführung neuer Funktionen ebnet. Ziel ist es, das System noch weiter zu bereichern und es zu einem leistungsfähigen Instrument für die Forschung und Entwicklung von Marktanalysen zu machen.
Entwicklung eines Replay System (Teil 27): Expert Advisor Projekt — Die Klasse C_Mouse (II) Entwicklung eines Replay System (Teil 27): Expert Advisor Projekt — Die Klasse C_Mouse (II)
In diesem Artikel werden wir die Klasse C_Mouse implementieren. Es bietet die Möglichkeit, auf höchstem Niveau zu programmieren. Wenn man über High-Level- oder Low-Level-Programmiersprachen spricht, geht es jedoch nicht darum, obszöne Wörter oder Jargon in den Code aufzunehmen. Es ist genau andersherum. Wenn wir von High-Level- oder Low-Level-Programmierung sprechen, meinen wir, wie leicht oder schwer der Code für andere Programmierer zu verstehen ist.
Quantisierung beim maschinellen Lernen (Teil 1): Theorie, Beispielcode, Analyse der Implementierung in CatBoost Quantisierung beim maschinellen Lernen (Teil 1): Theorie, Beispielcode, Analyse der Implementierung in CatBoost
Der Artikel befasst sich mit der theoretischen Anwendung der Quantisierung bei der Konstruktion von Baummodellen und stellt die in CatBoost implementierten Quantisierungsmethoden vor. Es werden keine komplexen mathematischen Gleichungen verwendet.