English 日本語
preview
Verständnis von Programmierparadigmen (Teil 2): Ein objektorientierter Ansatz für die Entwicklung eines Price Action Expert Advisors

Verständnis von Programmierparadigmen (Teil 2): Ein objektorientierter Ansatz für die Entwicklung eines Price Action Expert Advisors

MetaTrader 5Beispiele | 22 Mai 2024, 15:55
73 0
Kelvin Muturi Muigua
Kelvin Muturi Muigua

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ü.

MetaEditor Wizard


Schritt 2: Wählen Sie die Option Neue Klasse und klicken Sie auf Weiter.

MQL-Assistent neue Klassendatei


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.

Details der neuen Klassendatei des MQL-Assistenten


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.

PhoneClass Kompilierfehler

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. 

  1. 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.
  2. 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:

  1. MakePhoneCall(int phoneNumber);
  2. ReceivePhoneCall();
  3. SendSms(int phoneNumber, string message);
  4. ReceiveSms();
  5. SendInternetData();
  6. ReceiveInternetData();
  7. 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.

PhoneObjectEA-Experten-Protokoll


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.
So erzeugen Sie eine leere Klassendatei für die neue SmartPhoneClass.mqhzu erstellen, folgen Sie den Schritten, die wir zum Erstellen der PhoneClass mit dem MQL-Assistenten in MetaEditor verwendet haben. Fügen Sie dann folgenden Code in den Körper von SmartPhoneClass.mqh ein:

#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:

  1. Verkapselung: Bündelung von Daten und Methoden in einer einzigen Einheit (Klasse), um interne Details zu verbergen.
  2. Abstraktion: Vereinfachung komplexer Systeme durch Konzentration auf wesentliche Eigenschaften und Verhaltensweisen.
  3. Vererbung: Erlaubt einer Klasse, Eigenschaften und Verhaltensweisen von einer anderen Klasse zu erben, um Code wiederverwenden zu können.
  4. Polymorphismus: Ermöglicht die flexible Behandlung von Objekten unterschiedlicher Klassen als Objekte einer gemeinsamen Basisklasse.
  5. Klassen und Objekte: Klassen sind Blaupausen, und Objekte sind Instanzen; sie organisieren den Code auf modulare Weise.
  6. 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.
Überblick über die Preisaktions-EMA-Strategie


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

OOP_PriceActionEMA Tester-Einstellungen

OOP_PriceActionEMA Tester Eingaben


Mit einem gut optimierten EA-Setup erzielt unsere unkomplizierte Preisaktionsstrategie einen Jahresgewinn von 41 %, wenn mit einem Startkapital von 10.000 $ auf das EURJPY-Paar gehandelt wird, ein Leverage-Konto im Verhältnis 1:100 verwendet wird und ein niedriger Equity Drawdown von nur 5 % beibehalten wird. Diese Strategie zeigt Potenzial und kann durch die Integration zusätzlicher technischer Indikatoren oder die Optimierung für bessere Ergebnisse weiter verbessert werden, insbesondere wenn sie auf mehrere Symbole gleichzeitig angewendet wird.


OOP_PriceActionEMA Backtest-Grafik

OOP_PriceActionEMA Backtest-Ergebnisse

OOP_PriceActionEMA Backtest-Ergebnisse



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

Wie man einen einfachen Multi-Currency Expert Advisor mit MQL5 erstellt (Teil 7): Signale von ZigZag und dem Awesome Oszillator Wie man einen einfachen Multi-Currency Expert Advisor mit MQL5 erstellt (Teil 7): Signale von ZigZag und dem Awesome Oszillator
Der Multi-Currency Expert Advisor in diesem Artikel ist ein Expert Advisor für den automatisierten Handel, der den ZigZag-Indikator und den Awesome Oscillator als Signale verwendet.
Modified Grid-Hedge EA in MQL5 (Part III): Optimizing Simple Hedge Strategy (I) Modified Grid-Hedge EA in MQL5 (Part III): Optimizing Simple Hedge Strategy (I)
In this third part, we revisit the Simple Hedge and Simple Grid Expert Advisors (EAs) developed earlier. Our focus shifts to refining the Simple Hedge EA through mathematical analysis and a brute force approach, aiming for optimal strategy usage. This article delves deep into the mathematical optimization of the strategy, setting the stage for future exploration of coding-based optimization in later installments.
Saisonale Filterung und Zeitabschnitt für Deep Learning ONNX Modelle mit Python für EA Saisonale Filterung und Zeitabschnitt für Deep Learning ONNX Modelle mit Python für EA
Können wir bei der Erstellung von Modellen für Deep Learning mit Python von der Saisonalität profitieren? Hilft das Filtern von Daten für die ONNX-Modelle, um bessere Ergebnisse zu erzielen? Welchen Zeitabschnitt sollten wir verwenden? Wir werden all dies in diesem Artikel behandeln.
Deep Learning GRU model with Python to ONNX  with EA, and GRU vs LSTM models Deep Learning GRU model with Python to ONNX with EA, and GRU vs LSTM models
We will guide you through the entire process of DL with python to make a GRU ONNX model, culminating in the creation of an Expert Advisor (EA) designed for trading, and subsequently comparing GRU model with LSTN model.