English Русский 中文 Español 日本語 Português
preview
MQL5 beherrschen, vom Anfänger zum Profi (Teil III): Komplexe Datentypen und Include-Dateien

MQL5 beherrschen, vom Anfänger zum Profi (Teil III): Komplexe Datentypen und Include-Dateien

MetaTrader 5Beispiele | 31 Januar 2025, 11:49
153 0
Oleh Fedorov
Oleh Fedorov

Einführung

Dieser Artikel ist eine Fortsetzung der Serie für Anfänger. Hier gehe ich davon aus, dass der Leser bereits mit dem Material aus den beiden vorangegangenen Artikeln vertraut ist.

Der erste Artikel ist eine Einführung. Es setzt voraus, dass der Leser keine Vorkenntnisse in der Programmierung hat, und stellt die erforderlichen Werkzeuge für Programmierer vor, beschreibt die wichtigsten Programmtypen und führt in einige grundlegende Konzepte ein, insbesondere in das Konzept einer „Funktion“.

Der zweite Artikel beschreibt den Umgang mit Daten. Es werden die Begriffe „Literal“, „Variable“, „Datentyp“, „Operator“ usw. eingeführt und die wichtigsten Datenänderungsoperatoren untersucht: arithmetisch, logisch, bitweise und andere

In diesem Artikel werde ich beschreiben, wie ein Programmierer komplexe Datentypen erstellen kann:

  • Strukturen
  • Union
  • Klassen (auf Anfängerniveau)
  • Typen, bei denen ein Variablenname als Funktion verwendet werden kann. Dies ermöglicht es unter anderem, Funktionen als Parameter an andere Funktionen zu übergeben.

Der Artikel beschreibt auch, wie man externe Textdateien mit der Präprozessoranweisung #include einbindet, um sicherzustellen, dass unser Programm modular und flexibel ist. Ich möchte Sie daran erinnern, dass Daten auf verschiedene Arten organisiert werden können, aber der Compiler muss immer wissen, wie viel Speicher unser Programm benötigt, und deshalb müssen die Daten vor ihrer Verwendung durch die Angabe ihres Typs beschrieben werden.

Einfache Datentypen wie double, enum, string und andere wurden im zweiten Artikel beschrieben. Dabei wurden sowohl Variablen (Daten, die sich während des Betriebs ändern) als auch Konstanten eingehend untersucht. Beim Programmieren ergeben sich jedoch oft Situationen, in denen es bequemer ist, komplexere Typen aus einfachen Daten zu erstellen. Über genau diese „Konstruktionen“ werden wir im ersten Teil dieses Artikels sprechen.

Je modularer ein Programm aufgebaut ist, desto einfacher ist es, es zu entwickeln und zu pflegen. Dies ist besonders wichtig, wenn man in einem Team arbeitet. Außerdem ist es für „Solo-Entwickler“ viel einfacher, Fehler nicht in einem langen Stück Code, sondern in seinen kleinen Fragmenten zu suchen. Vor allem, wenn Sie nach längerer Zeit zum Code zurückkehren, um Ihrem Programm einige Funktionen hinzuzufügen oder einige logische Fehler zu beheben, die nicht sofort auffielen.

Wenn Sie geeignete Datenstrukturen bereitstellen, praktische Funktionen trennen, anstatt lange Listen von Bedingungen und Schleifen zu verwenden, und verschiedene logisch zusammenhängende Codeblöcke auf verschiedene Dateien verteilen, wird es viel einfacher, Änderungen vorzunehmen.


Strukturen

Eine Struktur beschreibt einen komplexen Satz von Daten, die bequem in einer einzigen Variablen gespeichert werden können. So sollten beispielsweise Informationen über den Zeitpunkt der Ausführung eines Innertagesgeschäfts Stunden, Minuten und Sekunden enthalten.

Natürlich können Sie für jede Komponente drei Variablen erstellen und auf jede von ihnen nach Bedarf zugreifen. Da diese Variablen jedoch Teil einer einzigen Beschreibung sind und meist zusammen verwendet werden, ist es zweckmäßig, einen eigenen Typ für solche Daten zu beschreiben. Die Struktur kann auch zusätzliche Daten anderer Art enthalten, z. B. eine Zeitzone oder alles andere, was der Programmierer benötigt.

Im einfachsten Fall wird die Struktur wie folgt beschrieben:

struct IntradayTime {
  int hours;
  int minutes;
  int seconds;
  string timeCodeString;
};  // note the semicolon after the curly brace

Beispiel 1. Eine Beispielstruktur für die Beschreibung der Handelszeit.

Dieser Code erzeugt einen neuen Datentyp IntradayTime. Zwischen den geschweiften Klammern dieser Erklärung sehen Sie alle Variablen, die wir kombinieren wollen. Daher enthalten alle Variablen vom Typ IntradayTime Stunden, Minuten und Sekunden.

Auf jeden Teil der Struktur innerhalb jeder Variablen kann über einen Punkt „.“ zugegriffen werden.

IntradayTime dealEnterTime;

dealEnterTime.hours = 8;
dealEnterTime.minutes = 15;
dealEnterTime.timeCodeString = "GMT+2";

Beispiel 2. Verwendung von Strukturtyp-Variablen.

Wenn wir eine Struktur beschreiben, können ihre „internen“ Variablen (oft als Felder bezeichnet) jeden gültigen Datentyp haben, einschließlich der Verwendung anderer Strukturen. Zum Beispiel:

// Nested structure
struct TradeParameters
{
   double stopLoss;
   double takeProfit;
   int magicNumber;
};

// Main structure
struct TradeSignal
{
   string          symbol;    // Symbol name
   ENUM_ORDER_TYPE orderType; // Order type (BUY or SELL)
   double          volume;    // Order volume
   TradeParameters params;    // Nested structure as parameter type
};

// Using the structure
void OnStart()
{

// Variable description for the structure
   TradeSignal signal;

// Initializing structure fields
   signal.symbol = Symbol();
   signal.orderType = ORDER_TYPE_BUY;
   signal.volume = 0.1;

   signal.params.stopLoss = 20;
   signal.params.takeProfit = 40;
   signal.params.magicNumber = 12345;

// Using data in an expression
   Print("Symbol: ",  signal.symbol);
   Print("Order type: ",  signal.orderType);
   Print("Volume: ",  signal.volume);
   Print("Stop Loss: ",  signal.params.stopLoss);
   Print("Take Profit: ",  signal.params.takeProfit);
   Print("Magic Number: ",  signal.params.magicNumber);
}

Beispiel 3. Verwendung einer Struktur zur Beschreibung des Typs von Feldern einer anderen Struktur.


Wenn Sie als Initialwerte für eine Struktur keine Ausdrücke, sondern Konstanten verwenden, können Sie eine Kurzschreibweise für die Initialisierung verwenden. Hier sollten Sie geschweifte Klammern verwenden. Der Initialisierungsblock aus dem vorangegangenen Beispiel könnte zum Beispiel wie folgt umgeschrieben werden:

TradeSignal signal = 
  {
    "EURUSD", 
    ORDER_TYPE_BUY, 
    0.1, 
 
     {20.0,  40.0,  12345}
  };

Beispiel 4. Initialisierung einer Struktur mit Hilfe von Konstanten.


Die Reihenfolge der Konstanten muss mit der Reihenfolge der Felder in der Beschreibung übereinstimmen. Sie können auch nur einen Teil der Struktur initialisieren, indem Sie Werte für die Initialisierung der Felder auflisten. In diesem Fall werden alle anderen Felder mit Nullen initialisiert.

MQL5 bietet eine Reihe von vordefinierten Strukturen, wie MqlDateTime, MqlTradeRequest, MqlTick und andere. In der Regel ist ihre Verwendung nicht komplizierter als in diesem Abschnitt beschrieben. Die Liste der Felder dieser und vieler anderer Strukturen ist in der Sprachreferenz detailliert beschrieben.

Darüber hinaus ist diese Liste für beliebige Strukturen (und andere komplexe Typen) im MetaEditor sichtbar, wenn Sie eine Variable des gewünschten Typs erstellen, dann ihren Namen eingeben und den Punkt „.“ auf der Tastatur drücken.

Liste der Felder einer Struktur

Abbildung 1. Liste der Felder einer Struktur in MetaEditor.

Alle Felder der Struktur sind standardmäßig für alle Funktionen unseres Programms verfügbar.


Über MQL5-Strukturen: Ein paar Worte für diejenigen, die wissen, wie man mit externen DLLs arbeitet

Warnung. Dieser Abschnitt kann für Anfänger schwierig sein. Wenn Sie den Artikel zum ersten Mal lesen, können Sie ihn überspringen und direkt zu den Unions übergehen, um später zu diesem Abschnitt zurückzukehren.

Standardmäßig befinden sich die Daten in MQL5-Strukturen in gepackter Form, d.h. direkt hintereinander. Wenn Sie also möchten, dass die Struktur eine bestimmte Anzahl von Bytes belegt, müssen Sie eventuell zusätzliche Elemente hinzufügen.

In diesem Fall ist es besser, zuerst die größten Daten und dann die kleineren Daten zu platzieren. Auf diese Weise können Sie viele Probleme vermeiden. MQL5-Strukturen bieten jedoch auch die Möglichkeit, Daten mithilfe eines speziellen Operators pack „auszurichten“:

struct pack(sizeof(long)) MyStruct1
     {
      // structure members will be aligned on an 8-byte boundary
     };

// or

struct MyStruct2 pack(sizeof(long))
     {
      // structure members will be aligned on an 8-byte boundary
     };

Beispiel 5. Angleichung der Struktur.

Innerhalb der Klammern von pack können Sie nur die Zahlen 1, 2, 4, 8, 16 verwenden.

Mit dem speziellen Befehl offsetof können Sie den Offset in Bytes für jedes Feld der Struktur relativ zum Anfang ermitteln. Wenn wir zum Beispiel die Struktur TradeParameters aus Beispiel 3 nehmen, können Sie den folgenden Code verwenden, um den Offset des Feldes stopLoss zu erhalten:

Print (offsetof(TradeParameters, stopLoss)); // Result: 0

Beispiel 6. Verwendung des Operators offsetof.

Die Strukturen, die KEINE Zeichenketten, dynamischen Arrays, klassenbasierten Objekte und Zeiger enthalten, werden als simple bezeichnet. Variablen von einfachen Strukturen sowie Arrays, die aus solchen Elementen bestehen, können frei an Funktionen übergeben werden, die aus externen DLL-Bibliotheken importiert wurden.

Es ist auch möglich, einfache Strukturen mit dem Zuweisungsoperator ineinander zu kopieren, allerdings nur in zwei Fällen:

  • entweder sind die Variablen vom gleichen Typ;
  • oder die Variablentypen sind durch eine direkte Vererbungslinie miteinander verbunden.

    Das heißt, wenn wir die Strukturen „Pflanzen“ und „Bäume“ definiert haben, kann jede Variable von „Pflanzen“ in jede Variable kopiert werden, die auf der Grundlage von „Bäumen“ erstellt wurde, und umgekehrt. Wenn wir jedoch auch „Büsche“ haben, können Sie nur Element für Element von „Büschen“ zu „Bäumen“ (oder umgekehrt) kopieren.

In allen anderen Fällen müssen auch Strukturen mit denselben Feldern Element für Element kopiert werden.

Die gleichen Regeln gelten auch für die Typisierung: Sie können „Busch“ nicht direkt in „Baum“ umwandeln, auch wenn sie dieselben Felder haben, aber Sie können „Pflanze“ in „Busch“ umwandeln.

Wenn Sie den Typ „Busch“ wirklich in einen „Baum“ umwandeln müssen, können Sie Unions verwenden. Sie sollten jedoch die Beschränkungen für Unions beachten, die im entsprechenden Abschnitt dieses Artikels beschrieben sind. Kurz gesagt, alle numerischen Felder können leicht konvertiert werden.

//---
enum ENUM_LEAVES
  {
   rounded,
   oblong,
   pinnate
  };

//---
struct Tree
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
struct Bush
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
union Plant
  {
   Bush bush;
   Tree tree;
  };

//---
void OnStart()
  {
   Tree tree = {1, rounded};
   Bush bush;
   Plant plant;

// bush = tree; // Error!
// bush = (Bush) tree; // Error!
   plant.tree = tree;
   bush = plant.bush; // No problem...

   Print(EnumToString(bush.leaves));
  }
//+------------------------------------------------------------------+

Beispiel 7. Konvertierung von Strukturen mit Hilfe von Unions.

Das war's für den Moment. Die vollständige Beschreibung aller Möglichkeiten von Strukturen enthält mehr Details und Nuancen als in diesem Artikel beschrieben. Vielleicht möchten Sie MQL5-Strukturen mit anderen Sprachen vergleichen oder weitere Details erfahren. Auch in diesem Fall überprüfen Sie bitte die Sprachreferenz.

Aber für Anfänger ist das Material, das über Strukturen geschrieben wurde, meiner Meinung nach völlig ausreichend, sodass ich zum nächsten Abschnitt übergehe.


Unions

Bei manchen Aufgaben müssen Sie die Daten in einer Speicherzelle als Variablen unterschiedlichen Typs interpretieren. Am häufigsten treten solche Probleme bei der Konvertierung von Strukturtypen auf. Ähnliche Anforderungen können auch bei der Verschlüsselung entstehen.

Die Beschreibung solcher Daten unterscheidet sich kaum von der Beschreibung einfacher Strukturen:

// Creating a type
union AnyNumber {
  long   integerSigned;  // Any valid data types (see further)
  ulong  integerUnsigned;
  double doubleValue;
};

// Using
AnyNumber myVariable;

myVariable.integerSigned = -345;

Print(myVariable.integerUnsigned);
Print(myVariable.doubleValue);

Beispiel 8. Verwendung von Unions.

Um Fehler bei Unions zu vermeiden, empfiehlt es sich, Daten zu verwenden, deren Typ denselben Speicherplatz belegt (obwohl dies bei einigen Konvertierungen unnötig oder sogar schädlich sein kann).

Die folgenden Datentypen können keine Union sein:

  • Dynamische Arrays
  • Zeichenketten (string)
  • Zeiger auf Objekte und Funktionen
  • Objekte der Klasse
  • Strukturobjekte, die Konstruktoren oder Destruktoren haben
  • Objekte von Strukturen, die Elemente aus den Punkten 1-5 haben

Es gelten keine weiteren Einschränkungen.

Bitte denken Sie daran: Wenn Ihre Struktur ein String-Feld verwendet, wird der Compiler einen Fehler ausgeben. Berücksichtigen Sie dies immer.


Grundkenntnisse in objektorientierter Programmierung

Objektorientierte Programmierung ist ein Programmierparadigma, das für viele Programmiersprachen grundlegend ist. Bei diesem Ansatz wird alles, was im Programm passiert, in einzelne Blöcke aufgeteilt. Jeder dieser Blöcke beschreibt eine bestimmte „Entität“: eine Datei, eine Zeile, ein Fenster, eine Liste von Preisen, usw.

Der Zweck jedes Blocks besteht darin, Daten und die zu ihrer Verarbeitung erforderlichen Aktionen an einem Ort zu sammeln. Wenn die Blöcke richtig konstruiert sind, bietet diese Struktur viele Vorteile:

  • Ermöglicht die mehrfache Wiederverwendung von Code
  • Erleichtert IDE-Operationen, indem es die Möglichkeit bietet, schnell die Namen von Variablen und Funktionen zu ersetzen, die sich auf bestimmte Objekte beziehen
  • Erleichtert das Auffinden von Fehlern und verringert die Wahrscheinlichkeit, dass neue Fehler hinzukommen.
  • Erleichtert parallele Operationen für verschiedene Personen (oder sogar Teams), die an verschiedenen Teilen des Codes arbeiten
  • Erleichtert die Änderung Ihres Codes, auch wenn viel Zeit vergangen ist
  • All dies führt letztlich zu einer schnelleren Programmentwicklung, einer höheren Zuverlässigkeit und einer einfacheren Kodierung.

Ein solches Layout ist im Allgemeinen natürlich, da es den Prinzipien des täglichen Lebens folgt. Wir klassifizieren immer alle Arten von Objekten: „Dieses Ding gehört zur Klasse der Tiere, jenes zu den Pflanzen, das andere ist ein Möbelstück“, und so weiter. Die Möbel wiederum können schrankartig oder gepolstert sein. Und so weiter.

Alle diese Klassifikationen verwenden bestimmte Merkmale von Objekten und deren Beschreibungen. Pflanzen haben zum Beispiel einen Stamm und Wurzeln, und Tiere haben bewegliche Gliedmaßen, mit denen sie sich fortbewegen können. Jede Klasse hat also einige charakteristische Eigenschaften. Bei der Programmierung verhält es sich genauso.

Wenn Sie sich zum Ziel gesetzt haben, eine Bibliothek für die Arbeit mit Linien zu erstellen, müssen Sie genau wissen, was jede Linie kann und was ihre Eigenschaften sind. Jede Linie hat zum Beispiel einen Anfangs- und einen Endpunkt, eine Stärke und eine Farbe.

Dies sind die Eigenschaften, Attribute oder Felder der Klasse ür Linien. Für Aktionen können Sie die Verben „draw“, „move“, „copy with a certain offset“, „rotate by a certain angle“ (Zeichnen, Verschieben, Kopieren mit einem bestimmten Versatz, Drehen um einen bestimmten Winkel) und andere verwenden.

Wenn ein Linienobjekt alle diese Aktionen unabhängig voneinander ausführen kann, sprechen Programmierer von den Methoden dieses Objekts.

Eigenschaften und Methoden zusammen werden als die Mitglieder (Elemente) der Klasse bezeichnet.

Um eine Zeile mit diesem Ansatz zu erstellen, müssen Sie also zunächst eine Klasse (Beschreibung) für diese Zeile - und alle anderen Zeilen im Programm - erstellen und dann den Compiler informieren: „Diese Variablen sind Linien, und diese Funktion verwendet sie.“

Klasse ist ein Variablentyp, der eine Beschreibung der Eigenschaften und Methoden von Objekten enthält, die zu dieser Klasse gehören.

Was die Art der Beschreibung betrifft, so ist eine Klasse einer Struktur sehr ähnlich. Der Hauptunterschied besteht darin, dass alle Mitglieder einer Klasse standardmäßig nur innerhalb dieser Klasse zugänglich sind. In einer Struktur sind alle ihre Mitglieder für alle Funktionen unseres Programms zugänglich. Im Folgenden finden Sie das allgemeine Schema für die Erstellung der gewünschten Klasse:

// class (variable type) description
class TestClass { // Create a type

private:          // Describe private variables and functions 
                  //   They will only be accessible to functions within the class 
  
// Description of data (class "properties" or "fields")
  double m_privateField; 

// Description of functions (class "methods")
  bool  Private_Method(){return false;} 

public:           // Description of public variables and functions 
                  //   They will be available to all functions that use objects of this class    

// Description of data (class "properties", "fields", or "members")   
  int m_publicField; 

// Description of functions (class "methods")   
  void Public_Method(void)
    {
     Print("Value of `testElement` is ",  testElement );   
    }
 }; 


Beispiel 9. Beschreibung der Klassenstruktur

Die Schlüsselwörter public: und private: definieren die Sichtbarkeitsbereiche der Klassenmitglieder.

Alles, was unter dem Wort public: steht, wird „außerhalb“ der Klasse verfügbar sein, d.h. für andere Funktionen unseres Programms, auch für solche, die nicht zu dieser Klasse gehören.

Alles oberhalb dieses Abschnitts (und unterhalb des Wortes private:) wird „versteckt“, und der Zugriff auf diese Elemente ist nur für die Funktionen derselben Klasse möglich.

Eine Klasse kann eine beliebige Anzahl von public: und private: Abschnitten enthalten.

Trotz des Vorschlags im Kasten ist es jedoch besser, nur einen Block pro Bereich zu verwenden (einen privaten: und einen öffentlichen:), damit alle Daten oder Funktionen mit denselben Zugriffsebenen nahe beieinander liegen. Einige erfahrene Entwickler ziehen es immer noch vor, vier Abschnitte zu erstellen - zwei (private und public) für Funktionen und zwei für Variablen. Jetzt ist es an Ihnen, sich zu entscheiden.

Grundsätzlich kann das Wort private: weggelassen werden, da alle Mitglieder der Klasse, die nicht als public: deklariert sind, standardmäßig privat sind (im Gegensatz zu Strukturen). Es ist jedoch nicht empfehlenswert, dies zu tun, da ein solcher Code beschwerlicher zu lesen ist.

Es ist wichtig, daran zu denken, dass im Allgemeinen mindestens eine Funktion in der beschriebenen Klasse „öffentlich“ sein muss, da die Klasse sonst in den meisten Fällen nutzlos ist. Es gibt Ausnahmen, aber die sind selten.

Es gilt als gute Programmierpraxis, nur Funktionen (nicht Variablen) in den Abschnitt public: zu stellen, um die Daten zu schützen. Dadurch können Klassenvariablen nur durch Methoden dieser Klasse geändert werden. Dieser Ansatz erhöht die Zuverlässigkeit des Programmcodes.

Um eine beschriebene Klasse zu verwenden, werden Variablen des gewünschten Typs an der gewünschten Stelle des Programms angelegt. Variablen werden auf die übliche Weise erstellt. Der Zugriff auf die Methoden und Eigenschaften jeder dieser Variablen erfolgt in der Regel über das Punktsymbol, wie bei Strukturen:
// Description of the variable of the required type
TestClass myTestClassVariable;

// Using the capabilities of this variable
myTestClassVariable.testElement = 5;
myTestClassVariable.PrintTestElement();

Beispiel 10. Verwendung einer Klasse.

Um zu veranschaulichen, wie öffentliche und private Eigenschaften funktionieren, fügen Sie den Code aus Beispiel 11 in die OnStart-Funktionsdefinition Ihres Skripts ein und kompilieren Sie die Datei. Die Kompilierung sollte erfolgreich sein.

Versuchen Sie dann, die Zeile „myVariable.a = 5;“ auszukommentieren und den Code erneut zu kompilieren. In diesem Fall erhalten Sie einen Kompilierungsfehler, der anzeigt, dass Sie versuchen, auf private Mitglieder einer Klasse zuzugreifen. Diese Funktion des Compilers hilft, einige der subtilen Fehler zu vermeiden, die Programmierern bei der Arbeit mit anderen Ansätzen unterlaufen können.

class PrivateAndPublic 
  {
private:
    int a;
public:
    int b;
  };

PrivateAndPublic myVariable;

// myVariable.a = 5; // Compiler error! 
myVariable.b = 10;   // Success

Beispiel 11. Verwendung öffentlicher und privater Eigenschaften einer Klasse.

Wenn wir alle Klassen selbst schreiben müssten, wäre dieser Ansatz nicht anders als alle anderen, und es hätte wenig Sinn.

Glücklicherweise sind viele Standardklassen bereits im Verzeichnis MQL5\Include verfügbar. Außerdem finden Sie in der Code Base eine Reihe von nützlichen Bibliotheken. In vielen Fällen müssen wir nur die entsprechende Datei einfügen (wie unten beschrieben), um von den Entwicklungen anderer kluger Köpfe zu profitieren. Dies ist eine große Hilfe für Programmierer.

Der OOP sind riesige Bücher gewidmet, und sie verdient definitiv einen eigenen Artikel. Der Zweck dieses Artikels besteht jedoch lediglich darin, Anfängern eine Vorstellung von der Verwendung komplexer Datentypen in den Programmen zu vermitteln. Da Sie nun wissen, wie man eine Basisklasse definiert und wie man die Klassen anderer Leute verwendet, gehe ich zum nächsten Abschnitt über.


Funktionaler Datentyp (Typedef-Operator)

Warnung. Dieser Abschnitt kann für Anfänger schwierig sein, daher können Sie ihn überspringen, wenn Sie den Artikel zum ersten Mal lesen.

Das Verständnis des Materials in diesem Abschnitt wird sich nicht auf das Verständnis des restlichen Materials auswirken - oder vielleicht sogar auf den Rest Ihrer Programmierreise. Für die meisten Probleme gibt es mehrere Lösungen, und funktionale Typen können leicht vermieden werden.

Die Möglichkeit, bestimmte Funktionen einer Variablen zuzuweisen (und sie daher in einigen Fällen als Argumente für andere Funktionen zu verwenden), kann jedoch praktisch sein, und ich denke, es lohnt sich, über diese Möglichkeit Bescheid zu wissen, zumindest um den Code eines anderen lesen zu können.

Manchmal ist es sinnvoll, Variablen eines „funktionalen“ Typs zu erstellen, um sie z. B. als Argument an eine andere Funktion zu übergeben.

In einer Handelssituation zum Beispiel sind Kauf- und Verkaufsaufträge sehr ähnlich und unterscheiden sich nur in einem Parameter. Der Ankaufspreis ist jedoch immer Ask (Briefkurs), und der Verkaufspreis ist immer Bid (Geldkurs).

Oft schreiben Programmierer ihre eigenen Kauf- und Verkaufsfunktionen, die alle Nuancen eines bestimmten Auftrags berücksichtigen. Dann schreiben sie eine Funktion wie Trade, die diese beiden Möglichkeiten kombiniert und sowohl beim „Aufwärts-“ als auch beim „Abwärtshandel“ gleich aussieht. Das ist praktisch, denn Trade selbst ersetzt die Aufrufe der geschriebenen Buy- oder Sell-Funktionen je nach der berechneten Richtung der Kursbewegung, und der Programmierer kann sich auf etwas anderes konzentrieren.

Sie können sich viele Fälle vorstellen, in denen Sie sagen möchten: „Roboter, mach es selbst!“ und Sie lassen die Funktion entscheiden, welche der Optionen in einer bestimmten Situation aufgerufen werden soll. Sollte bei der Berechnung des Take-Profits die Anzahl der Punkte zum Kurs addiert oder davon abgezogen werden? Bei der Berechnung des Stop-Loss? Sollte bei der Erteilung eines Auftrags an einem Extremwert nach Höchst- oder Mindestwerten gesucht werden? Und so weiter.

In solchen Fällen wird manchmal der unten beschriebene Ansatz verwendet.

Wie üblich müssen Sie zunächst den Typ der benötigten Variablen beschreiben. In diesem Fall wird dieser Typ anhand der folgenden Vorlage beschrieben:

typedef function_result_type (*Function_type_name)(input_parameter1_type,input_parameter1_type ...); 

Beispiel 12. Vorlage für die Beschreibung eines Funktionstyps.

wobei:

  • function_result_type ist der Typ des Rückgabewerts (jeder gültige Typ, wie int, double und andere).
  • Function_type_name ist der Name des Typs, den wir bei der Erstellung von Variablen verwenden werden.
  • input_parameter1_type ist der Typ des ersten Parameters. Die Parameterliste folgt den Regeln für normale Funktionslisten.

Beachten Sie das Sternchen (*) vor der Typenbezeichnung. Sie ist wichtig, und ohne sie wird nichts funktionieren.

Das bedeutet, dass die Variable dieses Typs kein Ergebnis oder eine Zahl enthält, sondern die Funktion selbst, die über einen vollständigen Satz von Fähigkeiten verfügt, und dass diese Variable daher die Fähigkeiten anderer Variablen und Funktionen in sich vereint.

Eine solche Konstruktion, die bei der Beschreibung eines Datentyps das Objekt selbst (eine Funktion, ein Objekt einer bestimmten Klasse usw.) und nicht eine Kopie der Daten des Objekts oder seines Operationsergebnisses verwendet, wird als Zeiger bezeichnet.

Wir werden in zukünftigen Artikeln mehr über Zeiger sprechen. Schauen wir uns ein Arbeitsbeispiel für die Verwendung des typedef-Operators an.

Nehmen wir an, wir haben die Funktionen Diff und Add, die wir einer Variablen zuweisen wollen. Beide Funktionen geben ganzzahlige Werte zurück und benötigen zwei ganzzahlige Parameter. Ihre Umsetzung ist einfach:

//---
int Add (int a,int b)
  {
    return (a+b);
  }

//---
int Diff (int a,int b)
  {
    return (a-b);
  }

Beispiel 13. Summations- und Differenzfunktionen für die funktionale Typenprüfung.

Beschreiben wir den Typ TFunc für Variablen, die jede dieser Funktionen speichern können:
typedef int (*TFunc) (int,  int);

Beispiel 14. Typendeklaration für Variablen, die Add- und Diff-Funktionen speichern können.


Lassen Sie uns nun prüfen, wie diese Beschreibung funktioniert:

void OnStart()
  {
    TFunc operate;       //As usual, we declare a variable of the described type
 
    operate = Add;       // Write a value to a variable (in this case, assign a function)
    Print(operate(3, 5)); // Use the variable as a normal function
                         // Function output: 8

    operate=Diff;
    Print(operate(3, 5)); // Function output: -2
  }

Beispiel 15. Verwendung einer Variablen eines funktionalen Typs.

Ich möchte anmerken, dass der Operator typedef nur mit nutzerdefinierten Funktionen funktioniert

Sie können Standardfunktionen wie MathMin oder ähnliche Funktionen nicht direkt verwenden, aber Sie können einen „Wrapper“, einen „Umschlag“, für sie erstellen. Zum Beispiel:

//---
double MyMin(double a, double b){
   return (MathMin(a,b));
}

//---
double MyMax(double a, double b){
   return (MathMax(a,b));
}

//---
typedef double (*TCompare) (double,  double);

//---
void OnStart()
  {
    TCompare extremumOfTwo;

    compare= MyMin;
    Print(extremumOfTwo(5, 7));// 5

    compare= MyMax;
    Print(extremumOfTwo(5, 7));// 7
  }

Beispiel 16. Verwendung von Wrappern für die Arbeit mit Standardfunktionen.


Einbinden externer Dateien (#include Präprozessor-Direktive)

Jedes Programm kann in mehrere Module unterteilt werden.

Wenn Sie mit großen Projekten arbeiten, ist eine Aufteilung unabdingbar. Die Modularität des Programms löst mehrere Probleme auf einmal.

  • Erstens ist der modulare Code einfacher zu navigieren.
  • Zweitens, wenn Sie in einem Team arbeiten, kann jedes Modul von verschiedenen Personen entwickelt werden, was den Prozess erheblich beschleunigt.
  • Und drittens können die erstellten Module wiederverwendet werden.

Das offensichtlichste „Modul“ ist eine Funktion. Darüber hinaus können Sie Module für alle Konstanten, einige komplexe Datentypbeschreibungen, Kombinationen mehrerer verwandter Funktionen (z. B. Funktionen zur Änderung des Aussehens von Objekten oder mathematische Funktionen) usw. erstellen.

In großen Projekten ist es sehr praktisch, solche Codeblöcke in separaten Dateien zu speichern und diese Dateien dann in das aktuelle Programm einzubinden.

Um zusätzliche Textdateien in ein Programm einzubinden, verwenden wir die Präprozessoranweisung #include:

#include <SomeFile.mqh>     // Angle brackets specify search relative to MQL5\Include directory 
#include "AnyOtherPath.mqh" // Quotes specify search relative to current file

Beispiel 17. Zwei Formen von #include.

Wenn der Compiler irgendwo in Ihrem Code auf die Anweisung #include stößt, versucht er, den Inhalt der angegebenen Datei anstelle dieser Anweisung einzufügen, allerdings nur einmal pro Programm. Wenn die Datei bereits verwendet wurde, kann sie nicht ein zweites Mal aufgenommen werden.

Sie können diese Aussage mit dem im nächsten Abschnitt beschriebenen Skript testen.

Meistens erhalten die eingeschlossenen Dateien die Erweiterung *.mqh, da dies praktisch ist, aber im Allgemeinen kann die Erweiterung beliebig sein.


Skript zum Testen der Funktion der #include-Direktive

Um zu testen, wie sich der Compiler verhält, wenn er auf diese Präprozessoranweisung stößt, müssen wir zwei Dateien erstellen.

Zunächst erstellen wir eine Datei mit dem Namen „1.mqh“ im Verzeichnis „Scripts“(MQL5\Scripts). Der Inhalt dieser Datei wird sehr einfach sein:

Print("This is include with number "+i);

Beispiel 18. Die einfachste Include-Datei kann nur einen Befehl enthalten.

Ich hoffe, es ist klar, was dieser Code bewirkt. Unter der Annahme, dass irgendwo eine Variable i deklariert wurde, erstellt dieser Code eine Nachricht für den Nutzer, indem er den Wert einer Variablen an die Nachricht anhängt, und gibt diese Nachricht dann in das Protokoll aus.

Die Variable i ist eine Markierung, die angibt, an welcher Stelle des Skripts die Anweisung aufgerufen wurde. In diese Datei sollte nichts anderes geschrieben werden. Nun erstellen wir im gleichen Verzeichnis (in dem sich die Datei „1.mqh“ befindet) ein Skript mit folgendem Code:

//+------------------------------------------------------------------+ 
//| Script program start function                                    | 
//+------------------------------------------------------------------+ 
void OnStart() 
  { 
    //---   
    int i=1; 
#include "1.mqh"   
    i=2; 
#include "1.mqh" 
  } 
//+------------------------------------------------------------------+

// Script output:
// 
//   This is include with number 1
//
// The second attempt to use the same file will be ignored

//+------------------------------------------------------------------+ 

Beispiel 19. Prüfung der wiederholten Aufnahme einer Datei.

In diesem Code haben wir versucht, die Datei „1.mqh“ zweimal zu verwenden, um zwei Auslösemeldungen zu erhalten.

Wenn wir dieses Skript im Terminal ausführen, werden wir sehen, dass die erste Nachricht wie erwartet funktioniert und die Zahl 1 in der Nachricht angezeigt wird, aber die zweite Nachricht erscheint nicht.

Warum wird diese Einschränkung vorgenommen? Warum kann die Datei nicht mehrfach verwendet werden?

Dies ist ein wichtiger Grundsatz, da Include-Dateien oft Deklarationen von Variablen und Funktionen enthalten. Sie wissen bereits, dass es in einem Programm auf globaler Ebene (außerhalb aller Funktionen) nur eine Variable mit einem bestimmten Namen geben sollte.

Wenn zum Beispiel die Variable int a; deklariert wird, kann dieselbe Variable auf dieser Ebene nicht ein zweites Mal deklariert werden. Sie können nur das verwenden, was bereits vorhanden ist. Bei den Funktionen ist die Situation etwas schwieriger, aber die Idee ist dieselbe: Jede Funktion muss innerhalb unseres Programms einzigartig sein. Stellen Sie sich nun vor, dass das Programm zwei unabhängige Module verwendet, aber jedes von ihnen dieselbe Standardklasse enthält, die sich in der Datei <Arrays\List.mqh> befindet (Abbildung 2).

Verwendung der gleichen Klasse durch zwei Module

Abbildung 2. Verwendung der gleichen Klasse durch zwei Module.

Ohne diese Einschränkung würde der Compiler eine Fehlermeldung ausgeben, da es verboten ist, die gleiche Klasse zweimal zu deklarieren. Aber in diesem Fall ist eine solche Konstruktion durchaus praktikabel, da nach der Beschreibung des Feldes FieldOf_Module1 die CList-Beschreibung bereits in den Compiler-Listen enthalten ist und daher einfach diese Beschreibung für Modul 2 verwendet.

Wenn Sie dieses Prinzip verstehen, können Sie sogar „mehrschichtige“ Verschachtelungen erstellen, zum Beispiel, wenn einige Klassenelemente „zirkulär“ voneinander abhängen, wie in Abbildung 3.

Sie können sogar eine Variable der gleichen Klasse innerhalb einer Klasse beschreiben.

Alle diese Konstruktionen sind akzeptabel, da #include genau einmal für eine Datei funktioniert.

Zirkuläre Abhängigkeit: jede Klasse enthält Elemente, die von einer anderen abhängen

Abbildung 3. Zirkuläre Abhängigkeit: Jede Klasse enthält Elemente, die von einer anderen abhängen.

Zum Abschluss dieses Abschnitts möchte ich Sie noch einmal daran erinnern, dass sich die Dateien der MetaTrader 5 Standardbibliotheken, die Sie in Ihren Code einbinden können, im Verzeichnis MQL5\Include befinden. Um dieses Verzeichnis im Explorer zu öffnen, wählen Sie im MetaTrader-Terminal das Menü „Datei“ -> „Datenverzeichnis öffnen“ (Abbildung 4).

Datenverzeichnis öffnen

Abbildung 4. So öffnen Sie das Datenverzeichnis.

Wenn Sie die Dateien aus diesem Verzeichnis in MetaEditor öffnen möchten, suchen Sie den Ordner Include im Navigationsbereich. Sie können Ihre eigenen Include-Dateien entweder im gleichen Verzeichnis anlegen (vorzugsweise in separaten Ordnern) oder Sie können das Verzeichnis Ihres Programms und dessen Unterverzeichnisse verwenden (siehe Kommentare in Beispiel 17). In der Regel werden #include-Anweisungen am Anfang der Datei verwendet, bevor alle anderen Aktionen beginnen. Diese Regel ist jedoch nicht strikt, und alles hängt von den spezifischen Aufgaben ab.


Schlussfolgerung

Lassen Sie mich noch einmal kurz die Themen nennen, die in diesem Artikel behandelt wurden.

  1. Wir haben die Präprozessoranweisung #include behandelt, mit der wir zusätzliche Textdateien in unser Programm einbinden können, in der Regel einige Bibliotheken.
  2. Wir haben komplexe Datentypen besprochen: Strukturen, Assoziationen und Objekte (Variablen, die auf Klassen basieren) sowie funktionale Datentypen.

Ich hoffe, dass die in diesem Artikel beschriebenen Datentypen für Sie jetzt nur noch in der Struktur „komplex“ sind, nicht aber in der Anwendung.

Im Gegensatz zu einfachen Typen, die in die Sprache eingebaut sind, müssen „komplexe“ Typen zunächst deklariert werden, und erst dann können Variablen erstellt werden. Die Arbeit mit solchen Daten unterscheidet sich jedoch im Wesentlichen nicht von der Arbeit mit „einfachen“ Typen: Sie erstellen Variablen und rufen Komponenten (Mitglieder) dieser Variablen auf (falls vorhanden) oder verwenden den Variablennamen als Funktionsnamen, wenn Sie einen funktionalen Typ erstellt haben.

Die Initialisierung von Variablen, die mit Hilfe von Strukturen erstellt wurden, kann mit geschweiften Klammern erfolgen.

Ich hoffe, Sie verstehen jetzt, dass die Möglichkeit, eigene komplexe Typen zu erstellen und das Programm in Module aufzuteilen, die in externen Dateien gespeichert sind, die Programmentwicklung flexibel und bequem macht.

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

Neuronale Netze leicht gemacht (Teil 97): Modelle mit MSFformer trainieren Neuronale Netze leicht gemacht (Teil 97): Modelle mit MSFformer trainieren
Bei der Erforschung verschiedener Modellarchitekturen wird dem Prozess des Modelltrainings oft nicht genügend Aufmerksamkeit geschenkt. In diesem Artikel möchte ich diese Lücke schließen.
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 14): Adaptive Volumenänderung im Risikomanager Entwicklung eines Expertenberaters für mehrere Währungen (Teil 14): Adaptive Volumenänderung im Risikomanager
Der zuvor entwickelte Risikomanager enthielt nur grundlegende Funktionen. Versuchen wir, mögliche Wege zu seiner Entwicklung zu betrachten, die es uns ermöglichen, die Handelsergebnisse zu verbessern, ohne die Logik der Handelsstrategien zu beeinträchtigen.
Algorithmus für künstliche elektrische Felder (AEFA) Algorithmus für künstliche elektrische Felder (AEFA)
In diesem Artikel wird ein Algorithmus für ein künstliches elektrisches Feld (AEFA) vorgestellt, der durch das Coulombsche Gesetz der elektrostatischen Kraft inspiriert ist. Der Algorithmus simuliert elektrische Phänomene, um komplexe Optimierungsprobleme mit Hilfe geladener Teilchen und ihrer Wechselwirkungen zu lösen. AEFA weist im Zusammenhang mit anderen Algorithmen, die sich auf Naturgesetze beziehen, einzigartige Eigenschaften auf.
Neuronales Netz in der Praxis: Pseudoinverse (II) Neuronales Netz in der Praxis: Pseudoinverse (II)
Da es sich bei diesen Artikeln um Lehrmaterial handelt und sie nicht dazu gedacht sind, die Implementierung bestimmter Funktionen zu zeigen, werden wir in diesem Artikel ein wenig anders vorgehen. Anstatt zu zeigen, wie man die Faktorisierung anwendet, um die Inverse einer Matrix zu erhalten, werden wir uns auf die Faktorisierung der Pseudoinverse konzentrieren. Der Grund dafür ist, dass es keinen Sinn macht, zu zeigen, wie man den allgemeinen Koeffizienten erhält, wenn man es auf eine spezielle Weise tun kann. Noch besser: Der Leser kann ein tieferes Verständnis dafür entwickeln, warum die Dinge so geschehen, wie sie geschehen. Lassen Sie uns nun herausfinden, warum die Hardware die Software im Laufe der Zeit ersetzt.