Verständnis von Programmierparadigmen (Teil 2): Ein objektorientierter Ansatz für die Entwicklung eines Price Action Expert Advisors
Einführung
Im ersten Artikel habe ich Programmierparadigmen vorgestellt und mich darauf konzentriert, wie man prozedurale Programmierung in MQL5 implementiert. Ich habe mich auch mit funktionaler Programmierung beschäftigt. Nachdem wir ein tieferes Verständnis für die Funktionsweise der prozeduralen Programmierung erlangt hatten, erstellten wir einen einfachen Price Action Expert Advisor, der den Indikator des exponentiellen gleitenden Durchschnitts (EMA) und Kursdaten der Kerzen verwendet.
In diesem Artikel wird ein tieferer Einblick in das Paradigma der objektorientierten Programmierung gegeben. Wir werden dieses Wissen dann anwenden, um den prozeduralen Code des zuvor entwickelten Expertenberaters aus dem ersten Artikel in objektorientierten Code umzuwandeln. Dieser Prozess wird unser Verständnis für die Hauptunterschiede zwischen diesen beiden Programmierparadigmen vertiefen.
Denken Sie beim Lesen daran, dass es nicht in erster Linie darum geht, eine Preisaktionsstrategie vorzustellen. Stattdessen möchte ich veranschaulichen und Ihnen helfen, ein tieferes Verständnis dafür zu erlangen, wie verschiedene Programmierparadigmen funktionieren und wie wir sie in MQL5 implementieren können. Der einfache Price Action Expert Advisor, den wir entwickeln, ist das sekundäre Ziel und dient als Leitfaden, um zu zeigen, wie wir dies in einem realen Beispiel anwenden können.
Verständnis der objektorientierten Programmierung
Objektorientierte Programmierung (auch OOP genannt) ist ein Programmierstil, bei dem der Code um die Idee von Objekten herum organisiert wird. Sie betrachtet Gegenstände hauptsächlich als Modelle für tatsächliche Dinge oder Konzepte.
Wenn sie sich in die objektorientierte Programmierung einarbeiten, haben Anfänger oft spezielle Fragen. Ich beginne mit der Beantwortung dieser Fragen, da dies dazu beitragen wird, Ihr Verständnis für dieses Programmierparadigma zu festigen.
Was ist eine Klasse in der objektorientierten Programmierung?
Eine Klasse ist eine Blaupause für die Erstellung von Objekten. Es hat eine Reihe von Eigenschaften (Attribute), die die Merkmale des Objekts beschreiben, und Funktionen (Methoden), die die verschiedenen erforderlichen Aufgaben ausführen.
Um das objektorientierte Paradigma besser zu erklären, möchte ich ein Telefon als Beispiel verwenden.
Stellen Sie sich vor, Sie gründen ein neues Unternehmen, das Telefone herstellt, und Sie sitzen in einer Besprechung mit dem Leiter der Produktdesignabteilung. Ihr Ziel ist es, eine Blaupause für das ideale Telefon zu erstellen, das Ihr Unternehmen produzieren wird. In dieser Sitzung besprechen Sie die wesentlichen Merkmale und Funktionen, die jedes Telefon haben sollte.
Sie beginnen mit der Erstellung einer Blaupause, der den Ausgangspunkt für jedes Telefon bildet, das Ihr Unternehmen produziert. In der objektorientierten Programmierung wird dieser Blaupause als Klasse bezeichnet.
Der Produktdesigner schlägt vor, dass man für die Erstellung der Blaupause zunächst eine Liste mit verschiedenen Aufgaben erstellen sollte, die ein Telefon erfüllen kann. Sie erstellen die folgende Aufgabenliste:
- Telefonanrufe tätigen und entgegennehmen.
- Senden und empfangen von Kurzmitteilungen (SMS).
- Senden und empfangen von Daten über das Internet.
- Fotos und Videos aufnehmen.
In der objektorientierten Programmierung werden die in der obigen Blaupause beschriebenen Aufgaben als Methoden bezeichnet. Methoden sind dasselbe wie gewöhnliche Funktionen, aber wenn sie in einer Klasse erstellt werden, werden sie als Methoden oder Mitgliedsfunktionen bezeichnet.
Sie entscheiden dann, dass jedes Telefon Eigenschaften und Merkmale haben muss, die es besser beschreiben. Sie machen ein fünfminütiges Brainstorming und kommen auf die folgende Liste:
- Modellnummer.
- Farbe.
- Art der Eingabe.
- Bildschirmtyp.
- Bildschirmgröße.
In der objektorientierten Programmierung werden die in der Blaupause (Klasse) beschriebenen Eigenschaften und Merkmale als Klassenattribute oder Mitgliedsvariablen bezeichnet. Attribute werden in einer Klasse als Variablen deklariert.
Was ist ein Objekt in der objektorientierten Programmierung?
Ein Objekt ist eine Implementierung einer Klasse. Einfacher ausgedrückt: Eine Klasse ist der Plan oder die Blaupause auf dem Papier, und das Objekt ist die tatsächliche Umsetzung des Plans oder der Blaupause im wirklichen Leben.
Sie und Ihr Produktdesigner haben die Blaupause des Telefons auf dem Papier fertiggestellt, um mit dem Beispiel der Telefonfirma fortzufahren. Sie beschließen, zwei verschiedene Telefontypen für unterschiedliche Verbrauchermärkte zu produzieren. Das erste Modell wird eine Low-End-Version sein, die nur Anrufe tätigen oder entgegennehmen und Textnachrichten senden oder empfangen kann. Das zweite Modell wird eine High-End-Version (Smartphone) mit allen Funktionen des ersten Low-End-Modells, einer High-End-Kamera, einem großen Akku und einem hochauflösenden Touchscreen sein.
Sie gehen aufgeregt in die Konstruktionsabteilung, übergeben dem Chefingenieur die Blaupause des Telefons (Klasse) und geben ihm Anweisungen, um Ihre Blaupause in die Tat umzusetzen. Er beginnt sofort mit der Arbeit an der Blaupause. Die Ingenieure benötigen etwa eine Woche, um die Entwicklung der Telefone abzuschließen. Wenn sie fertig sind, übergeben sie Ihnen die fertigen Produkte, damit Sie sie testen können.
Ein Telefon, das Sie jetzt in den Händen halten, sind die von der Klasse (Blaupause) abgeleiteten Objekte. Das Low-End-Telefonmodell implementiert nur einige Klassenmethoden, während das High-End-Telefonmodell (Smartphone) alle Klassenmethoden implementiert.
Lassen Sie mich dieses Telefonbeispiel mit etwas Code demonstrieren. Führen Sie die folgenden Schritte aus, um eine Klassendatei in Ihrer MetaEditor-IDE zu erstellen.
Schritt 1: Öffnen Sie die MetaEditor IDE und starten Sie den MQL-Assistenten über die Schaltfläche New im Menü.
Schritt 2: Wählen Sie die Option Neue Klasse und klicken Sie auf Weiter.
Schritt 3: Wählen Sie im Fenster Klasse erstellen das Eingabefeld Klassenname:, geben Sie PhoneClass als Klassenname ein, und geben Sie in das Eingabefeld Include-Datei:Experten\OOP_Artikel\PhoneClass.mqh ein, um die Klassendatei im selben Ordner wie den Quellcode unserer Expert Advisors zu speichern. Lassen Sie das Eingabefeld Basisklasse leer. Klicken Sie auf Fertig stellen, um eine neue MQL5-Klassendatei zu erzeugen.
Wir haben jetzt eine leere MQL5-Klassendatei. Ich habe einige Kommentare hinzugefügt, die uns helfen, die verschiedenen Teile der Klasse aufzuschlüsseln. Das Kodieren einer neuen MQL5-Klasse ist jetzt ein unkomplizierter Prozess und wird vom MQL-Assistenten in der MetaEditor-IDE automatisch für uns erledigt. Studieren Sie die nachstehende Syntax, da sie den Ausgangspunkt für eine richtig strukturierte MQL5-Klassendatei enthält.
class PhoneClass //class name { private: //access modifier public: //access modifier PhoneClass(); //constructor method declaration ~PhoneClass(); //destructor method declaration }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ PhoneClass::PhoneClass() //constructor method definition { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ PhoneClass::~PhoneClass() //destructor method definition { } //+------------------------------------------------------------------+
Achten Sie nicht auf den Code #properties, da er für das vorliegende Thema nicht relevant ist. Die wichtige Syntax beginnt in der Zeile, in der wir die öffnende Klassensyntax haben: class PhoneClass { direkt unter der Zeile #property version "1.00".
//+------------------------------------------------------------------+ //| PhoneClass.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--------------------------------------- //Ignore the code segment above //---------------------------------------
Lassen Sie uns die verschiedenen Teile der Klassendatei besprechen, die wir gerade erstellt haben.
Nach der öffnenden, geschweiften Klammer der Klasse finden Sie zwei Zugriffsmodifikatoren: private, und public. Ich werde ausführlich erklären, worum es sich dabei handelt, wenn ich das Thema Vererbung behandle.
Unterhalb des privaten Zugriffsmodifikators fügen wir die Klassenattribute(Telefoneigenschaften und -merkmale) aus unserem Telefon-Blaupause hinzu. Wir werden diese Eigenschaften als globale Variablen hinzufügen.
private: //access modifier //class attributes int modelNumber; string phoneColor; string inputType; string screenType; int screenSize;
In der nächsten Zeile unter dem Zugriffsmodifikator public: sind die Deklarationen der Methoden des Konstruktors und des Destruktors. Diese beiden Methoden ähneln den Standardfunktionen OnInit() und OnDeInit() von Expert Advisor. Der Klassenkonstruktor führt ähnliche Operationen aus wie OnInit(), während der Klassendestruktor Aufgaben ausführt, die OnDeinit() ähneln.
public: //access modifier PhoneClass(); //constructor method declaration ~PhoneClass(); //destructor method declaration
Was sind Konstruktoren und Destruktoren in der objektorientierten Programmierung?
Konstrukteure
Ein Konstruktor ist eine spezielle Methode innerhalb einer Klasse, die automatisch aufgerufen und ausgeführt wird, wenn ein Objekt dieser Klasse erstellt wird. Sein Hauptzweck besteht darin, die Attribute des Objekts zu initialisieren und alle Einrichtungsaktionen durchzuführen, die erforderlich sind, damit sich das Objekt in einem gültigen und aktualisierten Zustand befindet.
Hauptmerkmale von Konstrukteuren:
- Gleicher Name wie der der Klasse: Die Konstruktormethode hat den gleichen Namen wie die Klasse. Diese Namenskonvention hilft der Programmiersprache, den Konstruktor zu identifizieren und mit der Klasse zu verknüpfen. In MQL5 sind alle Konstruktoren standardmäßig vom Typ void, das heißt, sie geben keinen Wert zurück.
- Initialisierung: Konstruktoren sind für die Initialisierung der Attribute eines Objekts mit Standard- oder vorgegebenen Werten verantwortlich. Dadurch wird sichergestellt, dass das Objekt mit einem genau definierten Zustand beginnt. Wie das funktioniert, werde ich weiter unten in unserer PhoneClass demonstrieren.
- Automatische Ausführung: Der Konstruktor wird automatisch aufgerufen oder ausgeführt, wenn ein Objekt erstellt wird. Dies geschieht in dem Moment, in dem das Objekt instanziiert wird.
- Optionale Parameter: Konstruktoren können Parameter übernehmen, was eine Anpassung während der Objekterstellung ermöglicht. Diese Parameter liefern Werte, die der Konstruktor verwendet, um den Anfangszustand des Objekts festzulegen.
Sie können mehrere Konstruktoren in einer Klasse haben, aber sie müssen durch die Argumente oder Parameter, die sie besitzen, unterscheidbar sein. Abhängig von ihren Parametern werden die Konstruktoren in folgende Kategorien eingeteilt:
- Standard-Konstruktor: Dies ist ein Konstruktor ohne Parameter.
- Parametrischer Konstrukteur: Dies ist ein Konstruktor, der Parameter hat. Wenn einer der Parameter in diesem Konstruktor auf ein Objekt der gleichen Klasse verweist, wird er automatisch zu einem Kopierkonstruktor.
- Kopierkonstruktor: Dies ist ein Konstruktor, der einen oder mehrere Parameter hat, die auf ein Objekt der gleichen Klasse verweisen.
Wir brauchen eine Möglichkeit, die Klassenattribute (Variablen) mit spezifischen Telefondetails jedes Mal zu initialisieren oder zu speichern, wenn wir ein neues PhoneClass-Objekt erstellen. Wir werden diese Aufgabe mithilfe eines parametrischen Konstruktors erfüllen. Ändern wir den derzeitigen Standardkonstruktor und wandeln ihn in einen parametrischen Konstruktor mit fünf Parametern um. Kommentieren Sie die Deklaration der Standardkonstruktorfunktion aus, bevor Sie den neuen parametrischen Konstruktor darunter deklarieren und definieren.
//PhoneClass(); //constructor declaration and definition PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; }
Speichern und kompilieren Sie die Klassendatei. Nach dem Kompilieren der Klassendatei werden Sie feststellen, dass in Zeile 40 in Spalte 13 ein Fehler aufgetreten ist. Fehler: PhoneClass - bereits definierte Mitgliedsfunktion mit verschiedenen Parametern.
Bitte beachten Sie, dass die Codenummerierung für Ihre Klassendatei unterschiedlich sein kann, je nachdem, wie Sie Ihren Code eingerückt oder gestaltet haben. Die korrekte Zeilennummer finden Sie in Ihrem MetaEditor-Compiler-Fehlerprotokoll im unteren Bereich des Fensters, wie unten angegeben.
Wir haben unseren neuen parametrischen Konstruktor in einem Codeblock deklariert und definiert, und weiter unten in unserem Code in Zeile 40 finden Sie ein weiteres Codesegment, das ebenfalls den Konstruktor definiert. Sie müssen die Zeilen 40 bis 42 auskommentieren. Wenn Sie die Klassendatei kompilieren, wird sie erfolgreich und ohne Fehler oder Warnungen kompiliert. (Beachten Sie, dass sich dieses Codesegment in verschiedenen Codezeilen Ihrer Klassendatei befinden kann!)
/*PhoneClass::PhoneClass() //constructor method definition
{
}*/
Destruktor
Ein Destruktor ist eine spezielle Methode innerhalb einer Klasse, die automatisch aufgerufen und ausgeführt wird, wenn ein Objekt beendet wird. Sein Hauptzweck ist die „Müllabfuhr“ und das Aufräumen von Ressourcen, die das Objekt zugewiesen hat, wenn die Lebensdauer des Objekts abgelaufen ist. Dies hilft, Speicherlecks und andere ressourcenbezogene Probleme zu vermeiden. Eine Klasse kann nur einen Destruktor haben.
Hauptmerkmale von Destruktoren:
- Gleicher Name wie der der Klasse: Der Destruktor hat den gleichen Namen wie die Klasse, jedoch mit einer vorangestellten Tilde (~). Diese Namenskonvention hilft der Programmiersprache, den Destruktor zu identifizieren und mit der Klasse zu verknüpfen.
- Freigeben von Resourcen: Aufräumen von Ressourcen, die das Objekt zugewiesen hat, wie Speicher, Strings, dynamische Arrays, automatische Objekte oder Netzwerkverbindungen.
- Automatische Ausführung: Der Destruktor wird automatisch aufgerufen oder ausgeführt, wenn ein Objekt beendet wird.
- Keine Parameter: Alle Destruktoren haben keine Parameter und sind standardmäßig vom Typ void, was bedeutet, dass sie keinen Wert zurückgeben.
Fügen wir etwas Code hinzu, der jedes Mal, wenn der Destruktor ausgeführt wird, einen Text ausgibt. Gehen Sie zum Code-Definitionssegment der Destruktor-Methode und fügen Sie den folgenden Code hinzu:
PhoneClass::~PhoneClass() //destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); }
Sie werden feststellen, dass wir bei der Deklaration oder Definition der Konstruktor- und Destruktor-Methoden der Klasse ihnen keinen Rückgabetyp, d.h. (void), zuweisen. Die Angabe eines Rückgabetyps ist nicht notwendig, da es eine einfache Regel ist, dass alle Konstruktoren und Destruktoren in MQL5 vom Typ void sind und der Compiler dies automatisch für uns erledigt.
Damit Sie verstehen, wie Konstruktoren und Destruktoren funktionieren, hier ein kurzes Beispiel: Stellen Sie sich einen Campingausflug vor, bei dem Sie und Ihr Freund sich bestimmte Rollen zuweisen. Ihr Freund, der für den Aufbau des Zeltes und die Organisation bei der Ankunft verantwortlich ist, fungiert als „Konstrukteur“. In der Zwischenzeit spielen Sie, der sich um das Packen und Aufräumen am Ende der Reise kümmert, die Rolle des Zerstörers. In der objektorientierten Programmierung initialisieren Konstruktoren die Objekte, während Destruktoren die Ressourcen aufräumen, wenn die Lebensdauer des Objekts endet.
Als Nächstes fügen wir die Methoden der Klasse hinzu (Aufgaben, die das Telefon wie in der Blaupause beschrieben ausführen soll). Fügen Sie diese Methode direkt unter der Deklaration der Destruktor-Methode ein: ~PhoneClass();.
//class methods bool MakePhoneCall(int phoneNumber); void ReceivePhoneCall(); bool SendSms(int phoneNumber, string message); void ReceiveSms(); bool SendInternetData(); void ReceiveInternetData(); void UseCamera(); void virtual PrintPhoneSpecs();
Was sind virtuelle Methoden in MQL5?
In MQL5 sind virtuelle Methoden spezielle Funktionen innerhalb einer Klasse, die von Methoden mit demselben Namen in abgeleiteten Klassen überschrieben werden können. Wenn eine Methode in der Basisklasse als „virtuell“ gekennzeichnet ist, können abgeleitete Klassen eine andere Implementierung dieser Methode anbieten. Dieser Mechanismus ist wesentlich für den Polymorphismus, d. h., dass Objekte verschiedener Klassen als Objekte einer gemeinsamen Basisklasse behandelt werden können. Sie ermöglicht Flexibilität und Erweiterbarkeit in der objektorientierten Programmierung, indem sie es ermöglicht, spezifisches Verhalten in Unterklassen zu definieren, während eine gemeinsame Schnittstelle in der Basisklasse beibehalten wird.
Wie man die Methode PrintPhoneSpecs() überschreibt, werde ich weiter unten im Artikel zeigen, wenn wir die objektorientierte Vererbung behandeln.
So codieren Sie die Methodendefinition für die Methode PrintPhoneSpecs(). Platzieren Sie diesen Code unterhalb der Definition der Destruktor-Methode am Ende der Klassendatei.
void PhoneClass::PrintPhoneSpecs() //method definition { Print("___________________________________________________________"); PrintFormat("Model: %i Phone Specs", modelNumber); Print("---------------------"); PrintFormat ( "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n", modelNumber, phoneColor, inputType, screenType, screenSize ); }
Es gibt zwei Möglichkeiten, eine Klassenmethode zu definieren.
- Innerhalb des Klassenkörpers: Sie können eine Methode in einem Schritt innerhalb des Klassenkörpers deklarieren und definieren, wie wir es zuvor mit dem parametrischen Konstruktor getan haben. Die Syntax für die Deklaration und Definition einer Methode innerhalb des Klassenkörpers ist identisch mit der normalen Funktionssyntax.
- Außerhalb des Klassenkörpers: Die zweite Möglichkeit besteht darin, die Methode zunächst innerhalb des Klassenkörpers zu deklarieren und sie dann außerhalb des Klassenkörpers zu definieren, wie wir es mit den Methoden des Destruktors und PrintPhoneSpecs() getan haben. Um eine MQL5-Methode außerhalb des Klassenkörpers zu definieren, müssen Sie zuerst den Rückgabetyp der Methode angeben, gefolgt vom Klassennamen, dem Operator für die Bereichsauflösung (::), dem Methodennamen und der in Klammern eingeschlossenen Parameterliste. Anschließend wird der Methodenkörper in geschweifte Klammern {} eingeschlossen. Diese Trennung von Deklaration und Definition ist die bevorzugte Option, da sie eine klare Organisation der Klassenstruktur und der damit verbundenen Methoden ermöglicht.
Was bedeutet der Bereichsauflösungsoperator (::) in der objektorientierten Programmierung?
Der Operator :: ist als Operator zur Auflösung des Bereichs bekannt und wird in C++ und MQL5 verwendet, um den Kontext anzugeben, zu dem eine Funktion oder Methode gehört. Sie definiert oder referenziert Funktionen oder Methoden, die Mitglieder einer Klasse sind, um uns dabei zu helfen, festzulegen, dass sie Mitglieder dieser speziellen Klasse sind.
Ich möchte dies anhand der Definition der Methode PrintPhoneSpecs() näher erläutern:
void PhoneClass::PrintPhoneSpecs() //method definition { //method body }
Aus der obigen Definition der Methode PrintPhoneSpecs() ist ersichtlich, dass der Klassenname vor dem Bereichsoperator „::“ steht. Dies zeigt an, dass diese Funktion zur Klasse PhoneClass gehört. Auf diese Weise verknüpfen Sie die Methode mit der Klasse, mit der sie verbunden ist. Der :: Operator ist für die Definition und Referenzierung von Methoden innerhalb einer Klasse unerlässlich. Er hilft bei der Angabe des Bereichs oder Kontexts, zu dem die Funktion oder Methode gehört.
Unsere Klasse enthält auch die folgenden deklarierten Methoden, die ebenfalls definiert werden müssen:
- MakePhoneCall(int phoneNumber);
- ReceivePhoneCall();
- SendSms(int phoneNumber, string message);
- ReceiveSms();
- SendInternetData();
- ReceiveInternetData();
- UseCamera();
Platzieren Sie deren Methodendefinitionscode über dem Codesegment der Definition von PrintPhoneSpecs(). So sollte Ihre Klassendatei aussehen, wenn Sie die obigen Methodendefinitionen hinzugefügt haben:
class PhoneClass //class name { private: //access modifier //class attributes int modelNumber; string phoneColor; string inputType; string screenType; int screenSize; public: //access modifier //PhoneClass(); //constructor declaration and definition PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; } ~PhoneClass(); //destructor method declaration //class methods bool MakePhoneCall(int phoneNumber); void ReceivePhoneCall(); bool SendSms(int phoneNumber, string message); void ReceiveSms(); bool SendInternetData(); void ReceiveInternetData(); void UseCamera(); void virtual PrintPhoneSpecs(); }; /*PhoneClass::PhoneClass() //constructor method definition { }*/ PhoneClass::~PhoneClass() //destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); } bool PhoneClass::MakePhoneCall(int phoneNumber) //method definition { bool callMade = true; Print("Making phone call..."); return(callMade); } void PhoneClass::ReceivePhoneCall(void) { //method definition Print("Receiving phone call..."); } bool PhoneClass::SendSms(int phoneNumber, string message) //method definition { bool smsSent = true; Print("Sending SMS..."); return(smsSent); } void PhoneClass::ReceiveSms(void) { //method definition Print("Receiving SMS..."); } bool PhoneClass::SendInternetData(void) //method definition { bool dataSent = true; Print("Sending internet data..."); return(dataSent); } void PhoneClass::ReceiveInternetData(void) { //method definition Print("Receiving internet data..."); } void PhoneClass::UseCamera(void) { //method definition Print("Using camera..."); } void PhoneClass::PrintPhoneSpecs() //method definition { Print("___________________________________________________________"); PrintFormat("Model: %i Phone Specs", modelNumber); Print("---------------------"); PrintFormat ( "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n", modelNumber, phoneColor, inputType, screenType, screenSize ); }
Den vollständigen Code von PhoneClass.mqh finden Sie am Ende des Artikels.
Nun ist es an der Zeit, ein PhoneClass-Objekt zu erstellen. Dies ist vergleichbar mit der Art und Weise, wie die Telefoningenieure die Blaupause unseres Telefons in ein physisches Produkt verwandelt haben, das in der Lage war, verschiedene Aufgaben zu erfüllen (z. B. Anrufe zu tätigen und entgegenzunehmen), wie wir es weiter oben im Beispiel der Telefongesellschaft beschrieben haben.
Bitte beachten Sie, dass Klassendateien mit der Erweiterung .mqh gespeichert werden und als Include-Dateien bezeichnet werden. Erstellen Sie nun einen neuen ExpertAdvisor in demselben Ordner, in dem sich unsere PhoneClass.mqh befindet. Wir haben die Datei PhoneClass.mqh im folgenden Dateipfad gespeichert: „Experts\OOP_Article\“. Verwenden Sie den MetaEditor MQL Wizard, um einen neuen Expert Advisor (Template) zu generieren und speichern Sie ihn im folgenden Verzeichnispfad „Experts\OOP_Article\“. Nennen Sie den neuen EA „PhoneObject.mq5“.
Platzieren Sie diesen Code unterhalb des Codesegments des EAs #property version "1.00"
// Include the PhoneClass file so that the PhoneClass code is available in this EA #include "PhoneClass.mqh" int OnInit() { //--- // Create instaces or objects of the PhoneClass with specific parameters // as specified in the 'PhoneClass' consturctor PhoneClass myPhoneObject1(101, "Black", "Keyboard", "Non-touch LCD", 4); PhoneClass myPhoneObject2(102, "SkyBlue", "Touchscreen", "Touch AMOLED", 6); // Invoke or call the PrintPhoneSpecs method to print the specifications myPhoneObject1.PrintPhoneSpecs(); myPhoneObject2.PrintPhoneSpecs(); //--- return(INIT_SUCCEEDED); } void OnDeinit(const int reason){} void OnTick(){}
Sie finden den vollständige Code von PhoneObject.mq5 am Ende des Artikels.
Sehen wir uns an, was der EA-Code „PhoneObject.mq5“ macht:
Er bindet die Klassendatei mit einer Include-Anweisung ein:
Zunächst binden wir unseren PhoneClass-Code mit #include "PhoneClass.mqh" in den EA ein, damit er in unserem neu erstellten EA (PhoneObject.mq5) verwendet werden kann. Wir haben diese Klasse in einem globalen Umfang aufgenommen, sodass sie in allen Abschnitten des EA verfügbar ist.
Erzeugt das PhoneClass-Objekt:
Innerhalb der Funktion OnInit() des EAs haben wir zwei Instanzen oder Objekte der PhoneClass erstellt. Um diese Objekte zu erstellen, beginnen wir mit dem Klassennamen, gefolgt von beschreibenden Namen für die Telefoninstanzen (myPhoneObject1 und myPhoneObject2). Anschließend wurden die Werte für die Spezifikationen des Telefons, wie Modellnummer, Farbe, Eingabetyp, Bildschirmtyp und Bildschirmgröße, wie in den Parametern des PhoneClass-Konstruktors angegeben, in Klammern gesetzt.
Aktiviert oder ruft eine Klassenmethode auf:
Die Zeilen: myPhoneObject1.PrintPhoneSpecs() and myPhoneObject2.PrintPhoneSpecs() rufen PrintPhoneSpecs() des PhoneClass-Objekts auf, um die Telefonspezifikationen auszudrucken.
Ausgänge der Telefon-Spezifikationen:
Laden Sie den EA auf einen Symbolchart im MT5-Handelsterminal, um das PhoneObjectEA auszuführen, gehen Sie zum Toolbox-Fenster und wählen Sie die Registerkarte Experten, um die gedruckten Telefonspezifikationen zu überprüfen.
Die gedruckten Daten zeigen auch eine Textnachricht des Destruktor von „PhoneClass“ („~PhoneClass()“). Wir können sehen, dass jedes Telefonobjekt einen eigenen, unabhängigen Destruktor erstellt und diesen bei Beendigung des Objekts aufruft.
Was bedeutet Vererbung in der objektorientierten Programmierung?
Vererbung ist ein Konzept, bei dem eine neue Klasse, die so genannte abgeleitete Klasse, Unterklasse oder Kindklasse, Attribute und Verhaltensweisen (Eigenschaften und Methoden) von einer bestehenden Klasse, der so genannten Elternklasse oder Basisklasse, erben kann. Dadurch kann die abgeleitete Klasse die Funktionalitäten der übergeordneten Klasse wiederverwenden und erweitern.
Einfach ausgedrückt ist das Erbe wie ein Stammbaum. Stellen Sie sich eine „Basisklasse“ als „Elternteil“ oder „Mutter“ vor. Diese Klasse verfügt über spezifische Merkmale (Eigenschaften und Methoden). Stellen Sie sich eine „Unterklasse“ als „Kind“ oder „Tochter“ vor. Die Unterklasse erbt automatisch alle Eigenschaften (Eigenschaften und Methoden) der Basisklasse, ähnlich wie ein Kind Eigenschaften von seinem Elternteil erbt.
Wenn zum Beispiel die Mutter braune Augen hat, hat auch die Tochter braune Augen, ohne dass dies ausdrücklich gesagt wird. In der Programmierung erbt eine Unterklasse Methoden und Attribute von der Basisklasse, wodurch eine Hierarchie zur Organisation und Wiederverwendung von Code entsteht.
Diese „Familienstruktur“ hilft bei der Organisation und Wiederverwendung von Code. Das Kind (die Unterklasse) erhält alles, was das Elternteil (die Basisklasse) hat, und kann sogar seine eigenen einzigartigen Merkmale hinzufügen. Programmierer verwenden unterschiedliche Begriffe für diese „Familienmitglieder“:
- Basisklasse: Elternklasse, Superklasse, Wurzelklasse, Basisklasse, Masterklasse
- Unterklasse: Unterklasse, abgeleitete Klasse, Nachfolgeklasse, Erbenklasse
Im Folgenden wird gezeigt, wie wir die Vererbung im Kontext des von uns bereitgestellten PhoneClass-Codes implementieren können:
- Die PhoneClass dient als Basisklasse (Blaupause), die die Grundbausteine der Telefonfunktionalität definiert.
- Wir werden eine weitere Klasse erstellen, um das High-End-Telefonmodell (Smartphone) zu implementieren, das wir zuvor im Beispiel der Telefongesellschaft besprochen hatten.
- Wir werden diese neue Klasse SmartPhoneClass nennen. Es wird alle Eigenschaften und Methoden von PhoneClass erben und gleichzeitig neue, für Smartphones spezifische Funktionen einführen und die vorhandene Methode PrintPhoneSpecs() von PhoneClass überschreiben, um das Verhalten von Smartphones zu implementieren.
#include "PhoneClass.mqh" // Include the PhoneClass file class SmartPhoneClass : public PhoneClass { private: string operatingSystem; int numberOfCameras; public: SmartPhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen, string os, int totalCameras) : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen) { operatingSystem = os; numberOfCameras = totalCameras; } void UseFacialRecognition() { Print("Using facial recognition feature..."); } // Override methods from the base class if needed void PrintPhoneSpecs() override { Print("-----------------------------------------------------------"); Print("Smartphone Specifications (including base phone specs):"); Print("-----------------------------------------------------------"); PrintFormat("Operating System: %s \nNumber of Cameras: %i", operatingSystem, numberOfCameras); PhoneClass::PrintPhoneSpecs(); // Call the base class method Print("-----------------------------------------------------------"); } };
Hier ist der direkte Link zur vollständigen SmartPhoneClass.mqh Code.
Im obigen Beispiel erbt SmartPhoneClass von PhoneClass. Es werden neue Eigenschaften (operatingSystem und numberOfCameras) und eine neue Methode (UseFacialRecognition) eingeführt. Der Konstruktor von SmartPhoneClass ruft auch den Konstruktor der Basisklasse (PhoneClass) mit „PhoneClass(...)“ auf. Sie werden auch feststellen, dass wir die Methode PrintPhoneSpecs() aus der Basisklasse überschrieben haben. Wir haben den Override-Spezifizierer in die Definition der Methode PrintPhoneSpecs() in der SmartPhoneClass eingefügt, um den Compiler wissen zu lassen, dass wir absichtlich eine Methode der Basisklasse überschreiben.
Auf diese Weise können Sie Instanzen der SmartPhoneClass erstellen, die alle Funktionen eines normalen Telefons (PhoneClass) und neue, für Smartphones spezifische Zusatzfunktionen enthalten.
Zugriffsmodifikatoren
Zugriffsmodifikatoren spielen eine entscheidende Rolle bei der Vererbung in der objektorientierten Programmierung, indem sie festlegen, wie Mitglieder (Attribute und Methoden) einer Basisklasse vererbt werden und wie auf sie in abgeleiteten Klassen zugegriffen wird.
- Public: Durch „public“, den öffentlichen Zugriff, können die Eigenschaften und Methoden einer Klasse von außerhalb der Klasse zugänglich gemacht werden. Sie können jedes öffentliche Element aus jedem Teil des Programms frei verwenden oder ändern. Dies ist die offenste Stufe des Zugangs.
- Private: Der „private“ Zugriff schränkt die Sichtbarkeit von Eigenschaften und Methoden auf den Zugriff oder die Änderung innerhalb der Klasse selbst ein. Mitglieder, die als „privat2“ deklariert sind, sind von außerhalb der Klasse nicht direkt zugänglich. Dies hilft dabei, Implementierungsdetails zu verbergen und die Datenintegrität zu gewährleisten. Dies wird als Verkapselung bezeichnet.
- Protected: Der geschützte Zugang ist ein Mittelding zwischen public und private. Als protected deklarierte Mitglieder sind innerhalb der Klasse und ihrer Unterklassen (abgeleitete Klassen) zugänglich. Dies ermöglicht ein gewisses Maß an kontrollierter gemeinsamer Nutzung durch verwandte Klassen, während der Zugang von außen weiterhin eingeschränkt wird.
Um das SmartPhoneClass-Objekt zu erstellen, erstellen Sie einen neuen EA, speichern Sie ihn als SmartPhoneObject.mq5 und fügen Sie den folgenden Code ein:
// Include the PhoneClass file so that it's code is available in this EA #include "SmartPhoneClass.mqh" int OnInit() { //--- // Create instaces or objects of the PhoneClass with specific parameters // as specified in the 'PhoneClass' consturctor (base/mother class) PhoneClass myPhoneObject1(103, "Grey", "Touchscreen", "Touch LCD", 8); // as specified in the 'SmartPhoneClass' consturctor SmartPhoneClass mySmartPhoneObject1(104, "White", "Touchscreen", "Touch AMOLED", 6, "Android", 3); // Invoke or call the PrintPhoneSpecs method to print the specifications myPhoneObject1.PrintPhoneSpecs(); // base class method mySmartPhoneObject1.PrintPhoneSpecs(); // overriden method by the derived class (SmartPhoneClass) //--- return(INIT_SUCCEEDED); } void OnDeinit(const int reason){} void OnTick(){}
Sie finden den vollständige Code von SmartPhoneObject.mq5 angehängt am Ende des Artikels.
Speichern und kompilieren Sie den Quellcode von SmartPhoneObject.mq5, bevor Sie ihn im MT5-Handelsterminal auf einen Symbolchart laden, um das Objekt SmartPhoneClass auszuführen. Gehen Sie zum Fenster Toolbox und wählen Sie die Registerkarte Experten, um die gedruckte EA-Ausgabe zu überprüfen.
Haupteigenschaften der objektorientierten Programmierung
Die sechs Haupteigenschaften von OOP sind:
- Verkapselung: Bündelung von Daten und Methoden in einer einzigen Einheit (Klasse), um interne Details zu verbergen.
- Abstraktion: Vereinfachung komplexer Systeme durch Konzentration auf wesentliche Eigenschaften und Verhaltensweisen.
- Vererbung: Erlaubt einer Klasse, Eigenschaften und Verhaltensweisen von einer anderen Klasse zu erben, um Code wiederverwenden zu können.
- Polymorphismus: Ermöglicht die flexible Behandlung von Objekten unterschiedlicher Klassen als Objekte einer gemeinsamen Basisklasse.
- Klassen und Objekte: Klassen sind Blaupausen, und Objekte sind Instanzen; sie organisieren den Code auf modulare Weise.
- Weitergabe von Nachrichten: Objekte kommunizieren durch das Senden von Nachrichten und fördern so die Interaktion.
EIP (Kapselung, Vererbung, Polymorphismus): Dies sind die Grundprinzipien der objektorientierten Programmierung für die Codeorganisation und Flexibilität. Wenn Sie diese Kerneigenschaften verstehen und anwenden, können Sie einen besser organisierten, wartbaren und wiederverwendbaren Code schreiben.
MQL5-Klassen-Namenskonvention
Die übliche Namenskonvention für Klassennamen in MQL5 ist, ihnen ein „C“ voranzustellen. Es ist jedoch nicht zwingend erforderlich, diese Konvention zu befolgen. Das „C“ steht für „class“ (Klasse) und ist eine gängige Praxis, um deutlich zu machen, dass ein bestimmter Bezeichner eine Klasse darstellt.
Sie können zum Beispiel Klassennamen wie CExpert, CIndicator oder CStrategy in MQL5-Code sehen. Diese Namenskonvention hilft, Klassen von anderen Typen von Bezeichnern wie Funktionen oder Variablen zu unterscheiden.
Obwohl die Verwendung von „C“ als Präfix eine Konvention ist und aus Gründen der Übersichtlichkeit allgemein empfohlen wird, setzt MQL5 keine strengen Regeln für die Benennung von Klassen durch. Technisch gesehen können Sie Ihre Klassen auch ohne das Präfix „C“ benennen, aber es ist eine gute Praxis, etablierten Konventionen zu folgen, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.
Der objektorientierte Ansatz zur Entwicklung eines Price Action EA
Da Sie nun alles über das objektorientierte Programmierparadigma wissen, ist es an der Zeit, ein praktisches Beispiel in Angriff zu nehmen und unseren zuvor entwickelten preisaktionsbasierten Expertenberater von prozeduralem Code in objektorientierten Code umzuwandeln. Ich habe den prozeduralen Code des Price Action Expert Advisors Procedural_PriceActionEMA.mq5 am Ende dieses Artikels beigefügt.
Im Folgenden finden Sie einen kurzen Überblick über die Besonderheiten der Strategie „Price Action“. Eine ausführlichere Erklärung der Handelsstrategie finden Sie im ersten Artikel.
Die Price Action EMA Strategie
Die Strategie ist sehr einfach und verwendet nur exponentielle. gleitende Durchschnitte (EMA) und Kursdaten der Kerzen, um Handelsentscheidungen zu treffen. Sie sollten den Strategietester verwenden, um die beste EMA-Einstellung und den besten Zeitrahmen zu finden. Ich bevorzuge den Handel auf dem 1-Stunden-Zeitrahmen oder höheren Zeitrahmen, um bessere Ergebnisse zu erzielen.
Eröffnungsbedingungen:
- KAUFEN: Eröffnen Sie eine Kaufposition, wenn die zuletzt geschlossene Kerze eine Kaufkerze ist (open < close) und ihr Tiefst- und Höchstkurs über der Linie des Exponential Moving Average (EMA) liegt.
- VERKAUFEN: Eröffnen Sie eine Verkaufsposition, wenn die zuletzt geschlossene Kerze eine Verkaufskerze ist (open > close) und ihr Tiefst- und Höchstkurs unter der Linie des Exponential Moving Average (EMA) liegt.
- Eröffnen Sie weiterhin neue Kauf- oder Verkaufspositionen, wenn sich eine neue Kerze bildet und eine der oben genannten Bedingungen erfüllt ist.
Ausstiegsregeln:
- Schließen Sie automatisch alle offenen Positionen, wenn der vom Nutzer angegebene prozentuale Gewinn oder Verlust für das Konto erreicht ist.
- Alternativ können Sie auch vordefinierte traditionelle Stop-Loss- oder Take-Profit-Aufträge verwenden, um Positionen zu verwalten und zu schließen.
Da das Ziel dieses Artikels darin besteht, zu zeigen, wie man die obige Strategie mit Hilfe objektorientierter Prinzipien zu einem mql5 EA entwickeln kann, machen wir weiter und schreiben den Code.
Erstellen einer neuen CEmaExpertAdvisor-Klasse
Verwenden Sie den MQL5-Assistenten, um eine leere Klassendatei mit „EmaExpertAdvisor“ als Klassendateinamen zu erstellen, und speichern Sie sie unter dem folgenden Dateipfad: „Experts\OOP_Article\PriceActionEMA\!. Erstellen Sie innerhalb der neu erstellten Klassendatei EmaExpertAdvisor.mqh eine neue Klasse mit dem Namen CEmaExpertAdvisor. Wir werden die KlasseCEmaExpertAdvisor verwenden, um das Verhalten des EA zu kapseln, und die Mitgliedsvariablen, um seinen Zustand darzustellen.
Fügen Sie den folgenden Code für Klasseneigenschaften/Mitgliedervariablen und Methoden in die KlasseCEmaExpertAdvisor ein:
//+------------------------------------------------------------------+ // Include the trade class from the standard library //--- #include <Trade\Trade.mqh> class CEmaExpertAdvisor { public: CTrade myTrade; public: // Constructor CEmaExpertAdvisor( long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, int _emaPeriod, int _emaShift, bool _enableTrading, bool _enableAlerts, double _accountPercentageProfitTarget, double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl ); // Destructor ~CEmaExpertAdvisor(); };
Vor dem Klassenschlüsselwort habe ich die Klasse Trade aus der MQL5-Standardbibliothek eingefügt, damit wir verschiedene Handelsoperationen effizient und mit weniger Code verwalten können. Das bedeutet, dass wir die Methoden ManageProfitAndLoss() und BuySellPosition(...) neu schreiben müssen, um dieses neue effiziente Upgrade zu berücksichtigen.
#include <Trade\Trade.mqh>
Später in der Zeile können Sie sehen, dass ich die Klasse CTrade instanziiert und ein gebrauchsfertiges Objekt mit dem Namen myTrade erstellt habe, das wir zum Öffnen und Schließen neuer Positionen verwenden werden.
//Create an instance/object of the included CTrade class
CTrade myTrade;
Alle vom Nutzer eingegebenen globalen Variablen des prozeduralen Codes werden zu privaten globalen Variablen der Klasse CEmaExpertAdvisor. Die Nutzereingabevariablen des EAs müssen initialisiert werden, sobald die Klasse instanziiert wird, und wir werden dies erreichen, indem wir sie als Parameter an den Konstruktor übergeben. Dies wird dazu beitragen, den Initialisierungsprozess innerhalb der Klasse zu kapseln.
private: // Private member variables/attributes (formerly procedural global variables) //------------------------ // User input varibles long magicNumber; ENUM_TIMEFRAMES tradingTimeframe; int emaPeriod; int emaShift; bool enableTrading; bool enableAlerts; double accountPercentageProfitTarget; double accountPercentageLossTarget; int maxPositions; int TP; int SL;
Die übrigen globalen Variablen aus dem prozeduralen Code werden als öffentlich deklariert und als globale Variablen in der Klasse definiert.
public: //--- EA global variables // Moving average variables double movingAverage[]; int emaHandle; bool buyOk, sellOk; string movingAverageTrend; // Strings for the chart comments string commentString, accountCurrency, tradingStatus, accountStatus; // Capital management variables double startingCapital, accountPercentageProfit; // Orders and positions variables int totalOpenBuyPositions, totalOpenSellPositions; double buyPositionsProfit, sellPositionsProfit, buyPositionsVol, sellPositionsVol; datetime closedCandleTime;//used to detect new candle formations
Fügen Sie unter der Deklaration der Destruktor-Methode direkt über der Syntax der schließenden geschweiften Klammer der Klasse alle Funktionen des prozeduralen Codes als Deklarationen von Klassenmethoden hinzu.
// Class method declarations (formerly procedural standalone functions) int GetInit(); void GetDeinit(); void GetEma(); void GetPositionsData(); bool TradingIsAllowed(); void TradeNow(); void ManageProfitAndLoss(); void PrintOnChart(); bool BuySellPosition(int positionType, string positionComment); bool PositionFound(string symbol, int positionType, string positionComment);
Wir werden den C++-Kodierungsstil verwenden und alle Klassenmethoden unterhalb des Klassenkörpers wie folgt definieren:
//+------------------------------------------------------------------+ //| METHODS DEFINITIONS | //+------------------------------------------------------------------+ CEmaExpertAdvisor::CEmaExpertAdvisor(long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, int _emaPeriod, int _emaShift, bool _enableTrading, bool _enableAlerts, double _accountPercentageProfitTarget, double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl) { magicNumber = _magicNumber; tradingTimeframe = _tradingTimeframe; emaPeriod = _emaPeriod; emaShift = _emaShift; enableTrading = _enableTrading; enableAlerts = _enableAlerts; accountPercentageProfitTarget = _accountPercentageProfitTarget; accountPercentageLossTarget = _accountPercentageLossTarget; maxPositions = _maxPositions; TP = _tp; SL = _sl; } //+------------------------------------------------------------------+ CEmaExpertAdvisor::~CEmaExpertAdvisor() {} //+------------------------------------------------------------------+ int CEmaExpertAdvisor::GetInit() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetDeinit() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetEma() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetPositionsData() { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::TradingIsAllowed() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::TradeNow() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::ManageProfitAndLoss() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::PrintOnChart() { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment) { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::PositionFound(string symbol, int positionType, string positionComment) { //method body.... }
Alle Methodendefinitionen sind identisch mit der Syntax im prozeduralen Code, mit Ausnahme der Methoden ManageProfitAndLoss() und BuySellPosition(...), die aktualisiert wurden, um das neu erstellte myTrade-Objekt der Klasse CTrade zu verwenden, das wir zuvor in den Klassencode importiert haben.
Hier ist die neue und aktualisierte Methode ManageProfitAndLoss():
void CEmaExpertAdvisor::ManageProfitAndLoss() { //if the account percentage profit or loss target is hit, delete all positions double lossLevel = -accountPercentageLossTarget; if( (accountPercentageProfit >= accountPercentageProfitTarget || accountPercentageProfit <= lossLevel) || ((totalOpenBuyPositions >= maxPositions || totalOpenSellPositions >= maxPositions) && accountPercentageProfit > 0) ) { //delete all open positions if(PositionsTotal() > 0) { //variables for storing position properties values ulong positionTicket; long positionMagic, positionType; string positionSymbol; int totalPositions = PositionsTotal(); //scan all the open positions for(int x = totalPositions - 1; x >= 0; x--) { positionTicket = PositionGetTicket(x);//gain access to other position properties by selecting the ticket positionMagic = PositionGetInteger(POSITION_MAGIC); positionSymbol = PositionGetString(POSITION_SYMBOL); positionType = PositionGetInteger(POSITION_TYPE); int positionDigits= (int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); double positionVolume = PositionGetDouble(POSITION_VOLUME); ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(positionMagic == magicNumber && positionSymbol == _Symbol) //close the position { //print the position details Print("*********************************************************************"); PrintFormat( "#%I64u %s %s %.2f %s [%I64d]", positionTicket, positionSymbol, EnumToString(positionType), positionVolume, DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic ); //print the position close details PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); //send the tradeRequest if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3)) //success, position has been closed { if(enableAlerts) { Alert( _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION (#" + IntegerToString(positionTicket) + "). Check the EA journal for more details." ); } PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); myTrade.PrintResult(); } else //trade tradeRequest failed { //print the information about the operation if(enableAlerts) { Alert( _Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION (#" + IntegerToString(positionTicket) + "). Check the EA journal for more details." ); } PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("OrderSend error %d", GetLastError());//print the error code } } } } } }
Hier ist die neue und aktualisierte BuySellPosition(...) Methode:
bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment) { double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / 10000), 2); double tpPrice = 0.0, slPrice = 0.0, symbolPrice; if(positionType == POSITION_TYPE_BUY) { if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200) { volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), 2); } if(volumeLot < 0.01) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } volumeLot = NormalizeDouble(volumeLot, 2); symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(TP > 0) { tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits); } if(SL > 0) { slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits); } //if(myTrade.Buy(volumeLot, NULL, 0.0, 0.0, 0.0, positionComment)) //successfully openend position if(myTrade.Buy(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position { if(enableAlerts) { Alert(_Symbol, " Successfully openend BUY POSITION!"); } myTrade.PrintResult(); return(true); } else { if(enableAlerts) { Alert(_Symbol, " ERROR opening a BUY POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK)); } PrintFormat("ERROR: Opening a BUY POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code return(false); } } if(positionType == POSITION_TYPE_SELL) { if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200) { volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), 2); } if(volumeLot < 0.01) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } volumeLot = NormalizeDouble(volumeLot, 2); symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(TP > 0) { tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits); } if(SL > 0) { slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits); } if(myTrade.Sell(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position { if(enableAlerts) { Alert(_Symbol, " Successfully openend SELL POSITION!"); } myTrade.PrintResult(); return(true); } else { if(enableAlerts) { Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK)); } PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code return(false); } } return(false); }
Denken Sie daran, alle Methoden/Member-Funktionen mit der Logik des ursprünglichen prozeduralen Codes zu implementieren. Sie finden den vollständigen CEmaExpertAdvisor-Code in der Include-Datei EmaExpertAdvisor.mqh, die am Ende dieses Artikels angehängt ist.
Erstellen des neuen EAs OOP_PriceActionEMA
Nach der Fertigstellung der Blaupause der Handelsstrategie (der Klasse CEmaExpertAdvisor) ist es nun an der Zeit, sie in die Tat umzusetzen. Wir werden unsere Blaupause zum Leben erwecken, indem wir ein reales Objekt schaffen, das in der Lage ist, Handelsgeschäfte auszuführen.
Erzeugen Sie einen neuen Expert Advisor mit dem Namen „OOP_PriceActionEMA.mq5“ und speichern Sie ihn unter dem angegebenen Dateipfad: „Experten\OOP_Artikel\PreisAktionEMA“. Dieser EA wird für die Ausführung unserer Handelsstrategie zuständig sein.
Beginnen Sie mit dem Einbinden der Include-Datei „EmaExpertAdvisor.mqh“, in der die Klasse CEmaExpertAdvisor enthalten ist.
// Include the CEmaExpertAdvisor file so that it's code is available in this EA #include "EmaExpertAdvisor.mqh"
Als Nächstes deklarieren und definieren wir die Nutzereingabevariablen als globale Variablen. Dies sind die Nutzereingabevariablen für die Konfiguration des EA. Sie ähneln den Parametern, die früher in der prozeduralen Version globale Variablen waren.
//--User input variables input long magicNumber = 101;//Magic Number (Set 0 [Zero] to disable input group "" input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;//Trading Timeframe input int emaPeriod = 15;//Moving Average Period input int emaShift = 0;//Moving Average Shift input group "" input bool enableTrading = true;//Enable Trading input bool enableAlerts = false;//Enable Alerts input group "" input double accountPercentageProfitTarget = 6.0;//Account Percentage (%) Profit Target input double accountPercentageLossTarget = 10.0;//Account Percentage (%) Loss Target input group "" input int maxPositions = 3;//Max Positions (Max open positions in one direction) input int TP = 5000;//TP (Take Profit Points/Pips [Zero (0) to diasable]) input int SL = 500;//SL (Stop Loss Points/Pips [Zero (0) to diasable])
Erstellen Sie anschließend eine Instanz von CEmaExpertAdvisor. Diese Zeile erzeugt eine Instanz (ea) der Klasse CEmaExpertAdvisor mit Hilfe des Konstruktors und initialisiert sie mit den Werten der Nutzereingabevariablen.
//Create an instance/object of the included CEmaExpertAdvisor class //with the user inputed data as the specified constructor parameters CEmaExpertAdvisor ea( magicNumber, tradingTimeframe, emaPeriod, emaShift, enableTrading, enableAlerts, accountPercentageProfitTarget, accountPercentageLossTarget, maxPositions, TP, SL );
In der Funktion OnInit rufen wir die Methode GetInit der ea-Instanz auf. Diese Methode ist Teil der Klasse CEmaExpertAdvisor und ist für die Initialisierung des EA zuständig. Wenn die Initialisierung fehlschlägt, wird INIT_FAILED zurückgegeben, andernfalls INIT_SUCCEEDED.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- if(ea.GetInit() <= 0) { return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); }
In der Funktion OnDeinit rufen wir die Methode GetDeinit der ea-Instanz auf. Diese Methode ist Teil der Klasse CEmaExpertAdvisor und ist für die Deinitialisierung des EA zuständig.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- ea.GetDeinit(); }
In der Funktion OnTick rufen wir verschiedene Methoden der ea-Instanz auf, wie GetEma, GetPositionsData, TradingIsAllowed, TradeNow, ManageProfitAndLoss und PrintOnChart. Diese Methoden kapseln verschiedene Aspekte des EA-Verhaltens und machen den Code modularer und übersichtlicher.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- ea.GetEma(); ea.GetPositionsData(); if(ea.TradingIsAllowed()) { ea.TradeNow(); ea.ManageProfitAndLoss(); } ea.PrintOnChart(); }
Ich habe den vollständigen EA-Quellcode am Ende dieses Artikels in der Datei OOP_PriceActionEMA.mq5 Datei angehängt.
Testen unseres EA im Strategy Tester
Es ist obligatorisch zu bestätigen, dass unser EA wie geplant funktioniert. Dazu können Sie die Strategie entweder auf ein aktives Symboldiagramm laden und in einem Demokonto handeln oder den Strategietester für eine gründliche Bewertung nutzen. Obwohl Sie die Strategie auf einem Demokonto testen können, verwenden wir zunächst den Strategietester, um ihre Leistung zu beurteilen.
Hier sind die Einstellungen, die wir im Strategietester anwenden werden:
-
Broker: MT5 Metaquotes-Demokonto (wird automatisch bei der MT5-Installation erstellt)
-
Symbol: EURJPY
-
Testzeitraum (Datum): 1 Jahr 2 Monate (Januar 2023 bis März 2024)
-
Modellierung: Jeder Tick anhand realer Ticks
-
Ersteinlage: $10.000 USD
-
Hebel: 1:100
Schlussfolgerung
Wir sind am Ende unserer Erkundung des objektorientierten Programmierparadigmas angelangt, einem leistungsstarken Werkzeug zur Erstellung von Software. Wir haben uns mit der Komplexität dieses leistungsstarken Paradigmas auseinandergesetzt, das Code in modulare, wiederverwendbare Strukturen umwandelt. Der Wechsel von der prozeduralen zur objektorientierten Programmierung bringt eine neue Ebene der Organisation, Kapselung und Abstraktion mit sich, die Entwicklern einen robusten Rahmen für die Verwaltung komplexer Projekte bietet.
In diesem Artikel haben Sie auch gelernt, wie man prozeduralen MQL5-Code in objektorientierten Code umwandelt, indem man objektorientierte Prinzipien anwendet und die Bedeutung von Klassen, Objekten und Vererbung betont. Durch die Kapselung von Daten und Funktionen in Klassen verbessern wir die Modularität und Wartbarkeit des Codes.
Wenn Sie Ihre eigenen MQL5-Projekte in Angriff nehmen, denken Sie daran, dass die Stärke der objektorientierten Programmierung in ihrer Fähigkeit liegt, reale Entitäten und Beziehungen zu modellieren, was zu einem Code führt, der die Komplexität der Systeme widerspiegelt, die er darstellt. Ich habe alle Quellcodedateien für die verschiedenen Klassen und EAs, die wir erstellt haben, am Ende des Artikels beigefügt.
Vielen Dank, dass Sie mich bei diesem tiefen Einblick in die verschiedenen Programmierparadigmen begleitet haben. Mögen die Grundsätze und Praktiken, die wir entdeckt haben, Ihre Codierungsbemühungen bereichern. Bleiben Sie dran für weitere Einblicke und praktische Beispiele in unserem ständigen Bestreben, einfache und praktische Handelssysteme mit der beliebten und leistungsstarken Sprache MQL5 zu entwickeln.
Vielen Dank, dass Sie sich die Zeit genommen haben, diesen Artikel zu lesen. Ich wünsche Ihnen viel Erfolg bei der Entwicklung von MQL5 und bei Ihren Handelsbestrebungen.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/14161
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.