English 日本語
preview
Vereinfachen von Datenbanken in MQL5 (Teil 2): Verwendung von Metaprogrammierung zur Erstellung von Entitäten

Vereinfachen von Datenbanken in MQL5 (Teil 2): Verwendung von Metaprogrammierung zur Erstellung von Entitäten

MetaTrader 5Beispiele |
143 0
joaopedrodev
joaopedrodev

Einführung

Im vorigen Artikel haben wir die ersten Schritte unternommen, um zu verstehen, wie MQL5 mit Datenbanken umgeht: Wir haben Tabellen erstellt, Datensätze eingefügt, aktualisiert und gelöscht und sogar Transaktionsfunktionen, Datenimport und -export erkundet. All dies wurde direkt durchgeführt, indem rohes SQL geschrieben und die von der Sprache angebotenen nativen Funktionen aufgerufen wurden. Dieser Schritt war von entscheidender Bedeutung, da er das Fundament legte, auf dem jede Abstraktionsschicht aufgebaut wird. Nun stellt sich aber unweigerlich die Frage, ob wir jedes Mal SQL schreiben wollen, wenn wir Daten in unseren Robotern und Indikatoren verarbeiten müssen.

Wenn wir ein robusteres System in Betracht ziehen, lautet die Antwort nein. Die ausschließliche Verwendung von SQL macht den Code langatmig, repetitiv und fehleranfällig, insbesondere wenn die Anwendung wächst und beginnt, mehrere Tabellen, Beziehungen und Validierungen zu verarbeiten. An dieser Stelle kommt ein ORM (Object-Relational Mapping) ins Spiel: eine Möglichkeit, die Kluft zwischen der objektorientierten Welt, in der wir programmieren, und der relationalen Welt, in der die Daten leben, zu überbrücken. Der erste Schritt in diese Richtung besteht darin, eine Möglichkeit zu schaffen, Datenbanktabellen direkt als Klassen in MQL5 darzustellen.

In diesem zweiten Artikel lernen wir, wie man eine oft unterschätzte, aber extrem leistungsfähige Sprachfunktion verwendet: #define. Es wird uns ermöglichen, die Erstellung von Strukturen zu automatisieren und zu standardisieren, wodurch Doppelarbeit vermieden und künftige Erweiterungen erleichtert werden. Damit erstellen wir unsere ersten Entitäten (Klassen, die Tabellen darstellen) und auch einen Mechanismus zur Beschreibung von Spaltenmetadaten, wie z. B. Datentyp, Primärschlüssel, automatische Inkrementierung, erforderliche Felder und Standardwerte.

Dieser Ansatz bildet die Grundlage für alles, was folgt: Repositories, Query Builders und automatische Tabellenerstellung. Mit anderen Worten: Wir beginnen, das MQL5 ORM zu gestalten, das wir uns von Anfang an vorgestellt haben.


Was #define ist und wie es in MQL5 funktioniert

In Sprachen der C-Familie (und MQL5 gehört zu dieser Gruppe) ist #define ein Vorverarbeitungswerkzeug, d. h. etwas, das der Compiler noch vor der Kompilierung des endgültigen Codes interpretiert. Es erzeugt keine Funktionen oder Variablen, sondern ersetzt den Text auf intelligente Weise, fast wie ein „Shortcut"-System oder Makro.

In der Praxis bedeutet dies, dass wir ein Muster einmal schreiben und es an mehreren Stellen im Code wiederverwenden können, wodurch Doppelarbeit und Fehler vermieden werden. Darüber hinaus können wir #define in ein Werkzeug für eine Metaprogrammierung umwandeln, das in der Lage ist, aus einfachen Definitionen komplexe Strukturen wie unsere Datenbankentitäten zu erzeugen.

Beginnen wir mit der Untersuchung der verschiedenen Verwendungszwecke, von den einfachsten bis zu den fortgeschrittensten.

1. Das einfachste #define: Aliase und direkte Ersetzungen

Der häufigste Anwendungsfall ist die Verwendung von #define zur Erstellung eines Textkürzels.

#define PI 3.14159
#define AUTHOR "João Pedro"

//--- Use
int OnInit()
  {
   double area = PI * 2 * 2;
   Print("Code written by", AUTHOR);
   return(INIT_SUCCEEDED);
  }

Wenn der Compiler hier auf PI stößt, ersetzt er es durch den Wert 3,14159. Es gibt keine Typ- oder Kontextprüfung; es ist eine reine Textersetzung. Dies ist nützlich, aber immer noch trivial.

2. Parameter in Makros

Mit #define können wir auch Makros erstellen, die Parameter erhalten.

#define SQUARE(x) (x * x)

int OnInit()
  {
   Print("Square of 5: ", SQUARE(5));
   Print("Square of 10: ", SQUARE(10));
  }

Beim Kompilieren wird SQUARE(5) wörtlich durch (5 * 5) ersetzt. Dies gibt Ihnen eine Vorstellung davon, wie wir wiederkehrende Muster in wiederverwendbare Formen kapseln können.

3. Der #-Operator: Umwandlung von Argumenten in Zeichenketten

Eine wenig erforschte Funktion in MQL5 ist der #-Operator, der das an das Makro übergebene Argument in eine literale Zeichenkette verwandelt.

#define META(name) Print("Variable name: ", #name)

int OnInit()
  {
   int value = 42;
   META(value);
  }

Beachten Sie, dass nicht der Inhalt der Variablen, sondern ihr Name ausgegeben wird. Dieser Trick ist äußerst nützlich für die Erstellung von Protokollen oder die Erzeugung von Metadaten aus den Code-Bezeichnern selbst.

4. Der ##-Operator: Verkettung von Bezeichnern

Eine weitere fortschrittliche Funktion ist der ##-Operator, der Token (Codestücke) verkettet.

#define META(name) Print("Concatenated: ", name##Id)

int OnInit()
  {
   int userId = 7;
   META(user);
  }

Der Compiler fügt user und Id buchstäblich zusammen und bildet userId. Diese Technik ermöglicht die dynamische Generierung von Variablen-, Methoden- oder Konstantennamen, was in MQL5 sonst unmöglich wäre.

5. Makros als Parameter für andere Makros

Bislang haben wir #define als einfache Substitution oder mit Parametern gesehen. Aber hier wird es interessant, wenn wir feststellen, dass Makros auch andere Makros als Argumente erhalten können. Dies eröffnet eine Art „Wiederholungsmaschine", mit der wir komplexe Codeblöcke aus einer einzigen Definition erzeugen können.

Siehe dieses Beispiel:

// Step 1 - Macro that describes a set of operations
#define MATH_OPERATIONS(OP) \
  OP(Add, +)                \
  OP(Sub, -)                \
  OP(Mul, *)                \
  OP(Div, /)

// Step 2 - Macro that generates functions from the list above
#define GENERATE_FUNCTION(name, symbol) \
  double name(double a, double b) { return a symbol b; }

// Step 3 - Expansion: Creates multiple functions at once
MATH_OPERATIONS(GENERATE_FUNCTION)

// Step 4 - Use
int OnInit()
  {
   Print("2 + 3 = ", Add(2,3));
   Print("10 - 7 = ", Sub(10,7));
   Print("6 * 4 = ", Mul(6,4));
   Print("20 / 5 = ", Div(20,5));
   return INIT_SUCCEEDED;
  }

Was passiert in diesem Beispiel? Lassen Sie uns Schritt für Schritt vorgehen:

  • Schritt 1: Hier erstellen wir ein Makro, das für sich genommen nichts Nützliches erzeugt. Sie enthält einfach vier Elemente (Add, Sub, Mul, Div), die jeweils mit dem Symbol einer mathematischen Operation verbunden sind. Der Clou ist, dass jede Zeile ein OP-Makro aufruft, von dem wir noch nicht wissen, wie es implementiert werden wird. Das bedeutet, dass MATH_OPERATIONS als Vorlage funktioniert, aber immer noch ein „Werkzeug" benötigt, das ihm sagt, was es mit jedem Element in der Liste tun soll.

  • Schritt 2: Dieses Makro tut bereits etwas Konkretes: Mit einem Namen und einem Symbol erstellt es eine Funktion, die die Operation anwendet. Wenn wir zum Beispiel (Add, +) übergeben, wird die folgende Funktion erstellt:

    double Add(double a, double b) { return a + b; }

    Und so weiter für den Rest (Sub, Mul, und Div).

  • Schritt 3: Jetzt kommt der Clou: Wir übergeben GENERATE_FUNCTION als Parameter an die Operationsliste. Dies veranlasst den Compiler, jeden OP-Aufruf innerhalb von MATH_OPERATIONS zu erweitern und OP durch GENERATE_FUNCTION zu ersetzen. Das Endergebnis ist gleichwertig mit dem manuellen Schreiben:

    double Add(double a, double b) { return a + b; }
    double Sub(double a, double b) { return a - b; }
    double Mul(double a, double b) { return a * b; }
    double Div(double a, double b) { return a / b; }
  • Schritt 4: Hier verwenden wir direkt die Funktionen, die der Compiler aus der Kombination von Makros erstellt. Für den Programmierer sieht es so aus, als hätte es schon immer Funktionen namens Add, Sub, Mul und Div gegeben, aber in Wirklichkeit wurden sie automatisch eingebaut.

Zusammengefasst:

  • Ein Makro kann Elemente auflisten ( MATH_OPERATIONS ).
  • Ein weiteres Makro definiert, wie jedes Element in Code umgewandelt werden soll (GENERATE_FUNCTION).
  • Wenn wir beide kombinieren, erzeugt der Compiler automatisch eine Reihe von Funktionen (oder Methoden, Eigenschaften, Klassen usw.).

Diese Technik der Übergabe von Makros als Parameter an andere Makros ist ein unglaubliches Werkzeug, das wir verwenden werden, um Wiederholungen zu vermeiden, Strukturen zu standardisieren und erweiterbare Blöcke zu erstellen. Später werden wir die gleiche Logik auf Datenbanktabellen und -spalten anwenden, aber hier wird deutlich, dass die Idee nicht auf Datenbanken beschränkt ist: Wir können die Funktion in jeder Situation verwenden, in der sich Muster wiederholen.


Erstellen einer Klasse, die eine Tabelle darstellt (Entity)

Vertiefen wir das Konzept einer Entität, die im Grunde eine Klasse ist, die eine Datenbanktabelle widerspiegelt. Mit anderen Worten: Wenn wir in der Datenbank eine Tabelle mit dem Namen „Account" haben, die Spalten wie „id", „number", „balance" und „owner" enthält, dann haben wir im Code eine Klasse „Account" mit Eigenschaften, die jede dieser Spalten darstellen. So können wir Daten als Objekte manipulieren, ohne uns ständig mit SQL beschäftigen zu müssen.

Nehmen wir an, wir haben die Tabelle „Account“ in der Datenbank. Um es in MQL5 darzustellen, würden wir eine Klasse wie diese erstellen:

class Account
  {
public:
   ulong             id;        // unique identifier
   double            number;    // account number
   double            balance;   // available balance
   string            owner;     // account owner

                     Account(void);
                    ~Account(void);

   //--- Converts the data into a string
   string            ToString();
  };
Account::Account(void)
  {
  }
Account::~Account(void)
  {
  }
string Account::ToString(void)
  {
   return("Account[id="+ (string)id+ ", number="+ (string)number+ ", balance="+ (string)balance+ ", owner="+ (string)owner+ "]");
  }

Dieser Code funktioniert, aber er hat zwei klassische Probleme:

  1. Wiederholung: Für jede Tabelle müssen wir alle Eigenschaften und Konstruktoren manuell umschreiben. Wenn das System 20 Tabellen hat, gibt es 20 nahezu identische Klassen, bei denen sich nur die Felder unterscheiden.
  2. Schlechte Skalierbarkeit: Wenn sich ein Tabellenfeld ändert (z. B. die Umbenennung von „number" in „account_number"), müssen wir es sowohl in der Datenbank als auch in der Klasse manuell ändern, was zu Inkonsistenzen führen kann.

An dieser Stelle kommt die Metaprogrammierung mit #define ins Spiel. Bei der Verwendung von Makros zur automatischen Generierung der Entität ist die Idee sehr ähnlich zu der, die wir im vorherigen Abschnitt gesehen haben: Wir erstellen eine Liste von Spalten als Makro und generieren daraus automatisch die entsprechende Klasse. Lassen Sie uns Schritt für Schritt vorgehen:

Schritt 1 – Definieren der Spalten mit einem Makro
#define ACCOUNT_COLUMNS(COLUMN) \
  COLUMN(ulong,  id,      0)   \
  COLUMN(double, number,  0.0) \
  COLUMN(double, balance, 0.0) \
  COLUMN(string, owner,   "")

Hier definiert ACCOUNT_COLUMNS alle Spalten der Tabelle Account. Beachten Sie, dass jede Spalte durch Typ, Name und Standardwert beschrieben wird.

Das Geheimnis liegt in dem Parameter COLUMN, der später in einem anderen Makro übergeben wird, um zu entscheiden, was mit den einzelnen Elementen geschehen soll.

Schritt 2 – Erstellen des Makros, das die Attribute der Klasse erzeugt
#define ENTITY_FIELD(type, name, default_value) type name;

#define ENTITY_DEFAULT(type, name, default_value) name = default_value;

#define ENTITY_TO_STRING(type, name, default_value) _s += #name+"="+(string)name+", ";

Diese Makros sind die Bausteine unserer Entität. Jede von ihnen nimmt das in der Spaltenliste definierte Tripel (Typ, Name, Standardwert) und erzeugt ein bestimmtes Stück Code:

  • ENTITY_FIELD → erstellt das Klassenattribut.
    • Unter Berücksichtigung dieser Parameter: ENTITY_FIELD(ulong, id, 0) erzeugt ulong id;
    • Mit anderen Worten: Es wird nur die Variable mit ihrem Typ deklariert.
  • ENTITY_DEFAULT → initialisiert das Attribut mit einem Standardwert innerhalb des Konstruktors.
    • Unter Berücksichtigung dieser Parameter: ENTITY_DEFAULT(double, balance, 0.0) erzeugt balance = 0.0;
    • Dadurch wird sichergestellt, dass jedes Objekt in der Klasse mit konsistenten Werten beginnt.
  • ENTITY_TO_STRING → erzeugt eine Zeichenkette zur Anzeige der Attributwerte.
    • Unter Berücksichtigung dieser Parameter: ENTITY_TO_STRING(string, owner, „") wird der Name und der Wert _s += „owner="+(string)owner+", „; mit der Zeichenkette „_s" verkettet.
    • Auf diese Weise können wir eine generische ToString-Methode erstellen, die alle Felder der Klasse ausgibt, ohne dass jedes Attribut manuell geschrieben werden muss.
Schritt 3 – Erstellen des Hauptmakros für die Entität
#define ENTITY(name, COLUMN) \
class name \
  { \
public: \
                     COLUMN(ENTITY_FIELD) \
                     name(void){COLUMN(ENTITY_DEFAULT)}; \
                    ~name(void){}; \
   string            ToString(void) \
     { \
      string _s = ""; \
      COLUMN(ENTITY_TO_STRING) \
      _s = StringSubstr(_s,0,StringLen(_s)-2); \
      return(#name+ "["+_s+"]"); \
     } \
  };

Dies ist das Makro, das die gesamte Klasse erzeugt. Lassen Sie uns Zeile für Zeile aufschlüsseln:

  1. class name {...};

    • Erzeugt eine Klasse mit dem angegebenen Namen.
    • Beispiel: ENTITY(Account, ACCOUNT_COLUMNS) → class Account {...};
  2. COLUMN(ENTITY_FIELD)

    • Wendet für jede Spalte der Liste das Makro ENTITY_FIELD an.
    • Ergebnis: Alle Attribute der Klasse sind deklariert.
  3. Konstruktor name(void){COLUMN(ENTITY_DEFAULT)};

    • Der Konstruktor ruft COLUMN(ENTITY_DEFAULT) auf, d.h. er initialisiert alle Attribute mit ihren Standardwerten.
  4. Destruktor ~name(void){};

    • Hier machen wir nur den leeren Destruktor explizit.
  5. Die Methode ToString()

    • Sie setzt eine Zeichenkette _s durch Verkettung von name=wert aus jedem Feld zusammen.
    • Sie verwendet COLUMN(ENTITY_TO_STRING), um diese Logik auf alle Spalten anzuwenden.
    • Sie entfernt das nachgestellte Komma mit StringSubstr.
    • Sie druckt so etwas wie:

    Account[id=1, number=12345, balance=500.0, owner=João]

Schritt 4 – Erstellen der Entität
ENTITY(Account, ACCOUNT_COLUMNS)

Diese einzige Zeile erzeugt automatisch die Klasse, die der Klasse entspricht, die wir zu Beginn manuell geschrieben haben. Danach können wir die Klasse Account ganz normal verwenden:

Der Unterschied zwischen der manuellen Version und der #define-Version ist im Hinblick auf Skalierbarkeit und Wartungsfreundlichkeit enorm. Um eine neue Entität zu erstellen, müssen Sie lediglich:

  1. Seine Spalten in einem TABLE_COLUMNS-Makro definieren und
  2. DEFINE_ENTITY(Table, TABLE_COLUMNS) aufrufen.

Dies vermeidet Wiederholungen, erleichtert die Aktualisierung (wenn Sie den Namen einer Spalte ändern, können Sie ihn im Makro ändern) und eröffnet uns Raum für die Erweiterung dieses Konzepts, z. B. durch Hinzufügen von automatischen Konstruktoren, Serialisierungsmethoden, SQL-Integration und vieles mehr.


Spaltenmetadaten: Verkapselung der Eigenschaften

Bislang haben wir Entitäten mit Attributen erstellt und es sogar geschafft, ihre Werte mithilfe von Makros automatisch zu drucken. Aber es gibt ein Problem: Diese Attribute sind immer noch "stumm".

Sie wissen, wie sie Daten speichern können, aber sie wissen nicht, wie sie sich selbst beschreiben können. Wir können zum Beispiel nicht nach einem Attribut fragen: "Handelt es sich um einen Primärschlüssel (PK)?", „Können Nullwerte akzeptiert werden?", „Handelt es sich um ein Feld mit automatischer Inkrementierung?", oder „Was ist der tatsächliche Typ in der Datenbank (INTEGER, TEXT, REAL, usw.)?"

Diese Informationen werden als Metadaten oder Daten über die Daten bezeichnet. Wenn unser ORM in der Lage sein soll, automatisch SQL zu generieren (z. B. Tabellen zu erstellen oder Strukturen zu validieren), benötigen wir eine zusätzliche Schicht, die diese Eigenschaften speichert und es der Entität ermöglicht, sich selbst zu beschreiben.

Die Idee ist, eine Metadatenklasse namens IColumnMetadata zu erstellen. Sie ist für die Speicherung aller Informationen über eine Spalte zuständig:

  • Feldname (m_name)
  • Logischer Typ in MQL5 (m_type)
  • Datenbanktyp (m_db_type)
  • ob sie Null sein kann (m_nullable)
  • ob es sich um Autoinkrement handelt (m_auto_increment)
  • ob es ein Primärschlüssel ist (m_primary_key)
  • ob sie eindeutig ist (m_unique)

Auf diese Weise kann jede Spalte der Entität mit einer vollständigen Beschreibung versehen werden, die in Zukunft verwendet wird, um automatisch CREATE TABLE-Anweisungen zu generieren, zu überprüfen, ob die Datenbank mit der Entität übereinstimmt, und Werte korrekt zwischen MQL5 und SQL abzubilden.

Wir erstellen einen neuen Ordner mit dem Namen TickORM innerhalb von includes, und darin einen Ordner mit dem Namen metadata, und eine neue Datei mit dem Namen IColumnMetadata.mqh, am Ende ist dies das Verzeichnis: <MQL5/Includes/TickORM/metadata/ColumnMetadata.mqh>. Und wir erstellen die folgende Klasse:

//+------------------------------------------------------------------+
//| class abstract : IColumnMetadata                                 |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : IColumnMetadata                                    |
//| Heritage    : No heritage                                        |
//| Description : Stores all the information in a column.            |
//|                                                                  |
//+------------------------------------------------------------------+
class IColumnMetadata
  {
private:
   
   //--- Props
   string            m_name;
   string            m_type;
   string            m_db_type;
   bool              m_nullable;
   bool              m_auto_increment;
   bool              m_primary_key;
   bool              m_unique;

public:
                     IColumnMetadata(string name, string type,string db_type,bool nullable,bool auto_increment,bool primary_key,bool unique);
                     IColumnMetadata(void);
                    ~IColumnMetadata(void);

   //--- Get Props
   string            Name(void);
   string            Type(void);
   string            DbType(void);
   bool              Nullable(void);
   bool              AutoIncrement(void);
   bool              PrimaryKey(void);
   bool              Unique(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
IColumnMetadata::IColumnMetadata(string name,string type,string db_type,bool nullable,bool auto_increment,bool primary_key,bool unique)
  {
   m_name = name;
   m_type = type;
   m_db_type = db_type;
   m_nullable = nullable;
   m_auto_increment = auto_increment;
   m_primary_key = primary_key;
   m_unique = unique;
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
IColumnMetadata::IColumnMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
IColumnMetadata::~IColumnMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Get name                                                         |
//+------------------------------------------------------------------+
string IColumnMetadata::Name(void)
  {
   return m_name;
  }
//+------------------------------------------------------------------+
//| Get type                                                         |
//+------------------------------------------------------------------+
string IColumnMetadata::Type(void)
  {
   return m_type;
  };
//+------------------------------------------------------------------+
//| Get database type                                                |
//+------------------------------------------------------------------+
string IColumnMetadata::DbType(void)
  {
   return m_db_type;
  };
//+------------------------------------------------------------------+
//| Get is nullable                                                  |
//+------------------------------------------------------------------+
bool IColumnMetadata::Nullable(void)
  {
   return m_nullable;
  };
//+------------------------------------------------------------------+
//| Get is auto increment                                            |
//+------------------------------------------------------------------+
bool IColumnMetadata::AutoIncrement(void)
  {
   return m_auto_increment;
  };
//+------------------------------------------------------------------+
//| Get is primary key                                               |
//+------------------------------------------------------------------+
bool IColumnMetadata::PrimaryKey(void)
  {
   return m_primary_key;
  };
//+------------------------------------------------------------------+
//| Get is unique                                                    |
//+------------------------------------------------------------------+
bool IColumnMetadata::Unique(void)
  {
   return m_unique;
  };
//+------------------------------------------------------------------+

Beachten Sie, dass die Klasse keine Intelligenz oder komplexe Logik besitzt, sondern lediglich die Daten der Tabellenspalten speichert. Diese Abstraktionsebene mag auf den ersten Blick bürokratisch erscheinen, aber sie ist genau das, was uns den nächsten Schritt ermöglicht:

  • Eine Entität kann ihre Metadaten offenlegen.
  • Der ORM kann diese Metadaten scannen und automatisch SQL generieren, um die entsprechende Tabelle in der Datenbank zu erstellen.

Ohne diese Schicht müssten wir jedes Mal, wenn wir eine Tabelle erstellen wollten, die gesamte Anweisung CREATE TABLE... manuell schreiben, was genau die Art von Wiederholung ist, die wir vermeiden wollen.

Stellen Sie sich vor, Sie haben eine Tabelle „Trades" mit drei Spalten:

  1. id: Ganzzahl, Primärschlüssel, auto_increment.
  2. symbol: erforderliche Zeichenkette, darf nicht leer sein.
  3. volume: Dezimalzahl, ebenfalls erforderlich.

Mit unserer Metadatenklasse können wir sie wie folgt beschreiben:

IColumnMetadata id("id", "int", "INTEGER", false, true, true, true);
IColumnMetadata symbol("symbol", "string", "TEXT", false, false, false, false);
IColumnMetadata volume("volume", "double", "REAL", false, false, false, false);

Jetzt ist es nicht nur ein einfaches Attribut: Es ist ein Objekt, das sich selbst kennt, das seine Regeln und Einschränkungen zu erklären weiß.

Jetzt ist es nicht mehr nur ein einfaches Attribut: Es ist ein Objekt, das sich selbst kennt und seine Regeln und Einschränkungen erklären kann.


Erstellung der Klasse ITableMetadata: die vollständige Beschreibung der Entität

Wenn wir bisher auf Spaltenebene gearbeitet haben, müssen wir jetzt einen Schritt weiter gehen und auf der Ebene der gesamten Tabelle denken. Jede Tabelle (oder Entität) besteht nicht nur aus einer Reihe von Attributen: Sie hat auch einen eigenen Namen, einen Primärschlüssel und eine Sammlung von Spaltenmetadaten.

Mit anderen Worten, wir brauchen eine Struktur, die alle IColumnMetadaten einer Entität speichern kann. Wenn IColumnMetadata ein Feld beschreibt, beschreiben die ITableMetadata, die wir erstellen werden, die gesamte Entität. Sie muss auch andere Fragen beantworten, wie zum Beispiel: "Wie lautet der Tabellenname?", „Was ist der Primärschlüssel?", „Wie viele Spalten hat die Tabelle?" und „Welche Eigenschaften hat jede Spalte?"

Darüber hinaus muss es erweiterbar sein, d. h. jede Entität wird ihre eigene Metadatenversion haben, aber sie können alle dieselbe „Basisschnittstelle" verwenden.

Erstellen wir eine neue Datei namens TableMetadata.mqh im Verzeichnis <MQL5/Include/TickORM/metadata/TableMetadata.mqh>. Wir haben bereits ColumnMetadata.mqh importiert.

//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include "PropertyMetadata.mqh"
//+------------------------------------------------------------------+
//| class abstract : IEntityMetadata                                 |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : IEntityMetadata                                    |
//| Heritage    : No heritage                                        |
//| Description : Stores all the information in a table.             |
//|                                                                  |
//+------------------------------------------------------------------+
class ITableMetadata
  {
protected:
   IColumnMetadata  *m_properties[];

public:
                     ITableMetadata(void);
                    ~ITableMetadata(void);

   //--- Add new column
   void              AddColumn(IColumnMetadata *column);
   IColumnMetadata   *Column(int index);
   int               ColumnSize(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
ITableMetadata::ITableMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
ITableMetadata::~ITableMetadata(void)
  {
   int size = ArraySize(m_columns);
   for(int i=0;i<size;i++)
     {
      delete m_columns[i];
     }
  }
//+------------------------------------------------------------------+
//| Add new column                                                   |
//+------------------------------------------------------------------+
void ITableMetadata::AddColumn(IColumnMetadata *column)
  {
   int size = ArraySize(m_columns);
   ArrayResize(m_columns,size+1);
   m_columns[size] = column;
  }
//+------------------------------------------------------------------+
//| Get property metadata                                            |
//+------------------------------------------------------------------+
IColumnMetadata *ITableMetadata::Column(int index)
  {
   return(m_columns[index]);
  }
//+------------------------------------------------------------------+
//| Get size columns                                                 |
//+------------------------------------------------------------------+
int ITableMetadata::ColumnSize(void)
  {
   return(ArraySize(m_columns));
  }
//+------------------------------------------------------------------+

Beachten Sie, dass bereits ein Array des IColumnMetadata-Objekts hinzugefügt wurde, wobei jede Position im Array eine Tabellenspalte darstellt. Auch wurden einige grundlegende Methoden zur Manipulation des Arrays hinzugefügt.

Schließlich haben wir zwei virtuelle Methoden hinzugefügt. Das bedeutet, dass jede untergeordnete Klasse TableName() und PrimaryKey() außer Kraft setzen kann, um sich selbst zu beschreiben, da nur untergeordnete Klassen den Tabellennamen kennen. Um dies zu vermeiden, haben wir eine Basisimplementierung erstellt, die NULL zurückgibt.

class ITableMetadata
  {
public:
   //--- Virtual methods (will be implemented in the child class)
   virtual string    TableName(void);
   virtual string    PrimaryKey(void);
  };
//+------------------------------------------------------------------+
//| Get table name                                                   |
//+------------------------------------------------------------------+
string ITableMetadata::TableName(void)
  {
   return(NULL);
  }
//+------------------------------------------------------------------+
//| Get is primary key                                               |
//+------------------------------------------------------------------+
string ITableMetadata::PrimaryKey(void)
  {
   return(NULL);
  }
//+------------------------------------------------------------------+


Alles miteinander verbinden

Bisher haben wir zwei grundlegende Schritte durchlaufen: Zuerst haben wir manuell eine Entitätsklasse erstellt; dann haben wir gesehen, wie man Makro-Metaprogrammierung verwendet, um diese Klassen automatisch zu generieren. Aber es fehlt noch ein wichtiger Teil: Wir müssen die Definition von Entitäten und ihren Spalten an einer einzigen Stelle zentralisieren. Dieser Ort muss in der Lage sein, native MQL5-Typen in SQL-Typen zu übersetzen und gleichzeitig alle Tabellen-Metadaten in einer organisierten Weise zu erfassen.

Dies ist genau die Aufgabe der Datei TickORM.mqh, die sich unter <MQL5/TickORM/TickORM.mqh> befindet. Wir können es als eine Brücke betrachten, die den in MQL5 geschriebenen Code mit dem relationalen Datenbankmodell verbindet. Die Logik ist einfach: Für jede Entität erstellen wir eine von ITableMetadata abgeleitete Klasse, die automatisch alle Tabellenspalten aufzeichnet.

Dadurch kann der ORM nicht nur die Tabellenstruktur verstehen, sondern auch, wie Spalten erstellt, validiert und manipuliert werden, ohne dass die Entwickler dieselben Konvertierungen für jede neue Entität manuell durchführen müssen.

Um die Nützlichkeit dieser Methode besser zu verstehen, gehen wir Schritt für Schritt vor: Erstellen Sie manuell eine Metadatenklasse für die Tabelle „Konto", die vier grundlegende Spalten hat. Wir werden hier noch keine Makros verwenden, um genau zu klären, welche Arbeit wir automatisieren wollen:

class AccountMetadata : public ITableMetadata
  {
public:
                     AccountMetadata(void);
                    ~AccountMetadata(void);
   
   string            TableName();
   string            PrimaryKey(void);
  };
AccountMetadata::AccountMetadata(void)
  {
   this.AddColumn(new IColumnMetadata("id","ulong","INTEGER",false,true,true,true));
   this.AddColumn(new IColumnMetadata("number","ulong","REAL",false,false,false,false));
   this.AddColumn(new IColumnMetadata("balance","ulong","REAL",false,false,false,false));
   this.AddColumn(new IColumnMetadata("owner","ulong","TEXT",false,false,false,false));
  }
AccountMetadata::~AccountMetadata(void)
  {
  }
string AccountMetadata::TableName(void)
  {
   return("Account");
  }
string AccountMetadata::PrimaryKey(void)
  {
   int size = ArraySize(m_columns);
   for(int i=0;i<size;i++)
     {
      if(m_columns[i].PrimaryKey())
        {
         return(m_columns[i].Name());
        }
     }
   return(NULL);
  }

Diese Implementierung ist ausreichend, um die Metadaten der Tabelle abzufragen. So können wir sie nutzen:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   AccountMetadata metadata;
   int size_cols = metadata.ColumnSize();
   Print("table name: ",metadata.TableName());
   Print("ptimary key: ",metadata.PrimaryKey());
   Print("size columns: ",size_cols);
   for(int i=0;i<size_cols;i++)
     {
      IColumnMetadata *column = metadata.Column(i);
      Print("===");
      Print("Column name: "+column.Name());
      Print("Type: "+column.Type());
      Print("DbType: "+column.DbType());
      Print("Nullable: "+column.Nullable());
      Print("PrimaryKey: "+column.PrimaryKey());
      Print("Unique: "+column.Unique());
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Die Konsolenausgabe zeigt alle erfassten Informationen deutlich an:

table name: Account
ptimary key: id
size columns: 4
===
Column name: id
Type: ulong
DbType: INTEGER
Nullable: false
PrimaryKey: true
Unique: true
===
Column name: number
Type: ulong
DbType: REAL
Nullable: false
PrimaryKey: false
Unique: false
===
Column name: balance
Type: ulong
DbType: REAL
Nullable: false
PrimaryKey: false
Unique: false
===
Column name: owner
Type: ulong
DbType: TEXT
Nullable: false
PrimaryKey: false
Unique: false

Beachten Sie, dass alle wesentlichen Daten für die Tabelle und ihre Spalten bereits zur Laufzeit zur Verfügung stehen. Was jetzt noch fehlt, ist die Automatisierung der Erstellung dieser Metadatenklasse mittels #define, damit wir nicht für jede neue Entität diese gesamte Struktur manuell wiederholen müssen. Dies wird der nächste Schritt sein.


Automatisieren der Erstellung der Metadatenklasse

Wenn wir beginnen, MQL5-Entitäten mit der Datenbank zu verbinden, ergibt sich sofort eine Herausforderung: Die Datentypen sprechen nicht dieselbe Sprache. Im MQL5-Code verwenden wir int, double, string und andere. In der Datenbank sind die Typen unterschiedlich: INTEGER, REAL, TEXT, usw.

Ohne eine klare Übersetzungsregel müsste jede Spalte manuell zugeordnet werden, was mühsam und fehleranfällig wäre. Um diese Nacharbeit zu vermeiden, haben wir ein kleines „Umwandlungswörterbuch" mit #define erstellt:

#define MQL5_TO_SQL_int        "INTEGER"
#define MQL5_TO_SQL_double     "REAL"
#define MQL5_TO_SQL_float      "REAL"
#define MQL5_TO_SQL_long       "INTEGER"
#define MQL5_TO_SQL_ulong      "INTEGER"
#define MQL5_TO_SQL_datetime   "INTEGER"
#define MQL5_TO_SQL_string     "TEXT"
#define MQL5_TO_SQL_bool       "INTEGER" 
#define DB_TYPE_FROM_MQL5(type) MQL5_TO_SQL_##type

Es funktioniert ganz einfach: Wenn wir eine Spalte in der Entität als int deklarieren, wandelt das Makro DB_TYPE_FROM_MQL5 sie automatisch in „INTEGER" um. Dadurch wird sichergestellt, dass jeder MQL5-Typ immer seinen entsprechenden Typ in der Datenbank hat, ohne dass man sich diese Zuordnung merken oder manuell wiederholen muss.

Nun, da die Typen geklärt sind, brauchen wir eine Möglichkeit, die Metadaten für jede Tabelle zu organisieren. Zu diesem Zweck erstellen wir dynamisch eine Klasse mit dem Namen name##Metadata (z. B. AccountMetadata ). Diese Klasse erbt von ITableMetadata und hat zwei Hauptfunktionen:

  • Tabellenname(): gibt den Entitätsnamen zurück (der als Tabellenname verwendet wird).
  • PrimaryKey(): identifiziert automatisch, welche Spalte als Primärschlüssel markiert wurde.
#define ENTITY_META_DATA(name, COLUMNS) \
class name##Metadata : public ITableMetadata \
  { \
public: \
                     name##Metadata(void) \
     { \
      COLUMNS(ENTITY_META_DATA_COLUMNS); \
     } \
                    ~name##Metadata(void){}; \
   string            TableName() { return(#name); }; \
   string            PrimaryKey(void) \
     { \
      int size = ArraySize(m_columns); \
      for(int i=0;i<size;i++) \
        { \
         if(m_columns[i].PrimaryKey()) \
           { \
            return(m_columns[i].Name()); \
           } \
        } \
      return(NULL); \
     } \
  };

Um schließlich eine vollständige Entität (Klasse + Metadaten) zu erstellen, verwenden wir zwei Makros: Das erste definiert die Spalten, das zweite erzeugt automatisch die Entität und ihre Metadatenklasse:

#define ACCOUNT_COLUMNS(COLUMN) \
  COLUMN(ulong,  id,      false, 0, true,  true,  false) \
  COLUMN(double, number,  false, 0, false, false, false) \
  COLUMN(double, balance, false, 0, false, false, false) \
  COLUMN(string, owner,   false,"", false, false, false)

ENTITY(Account, COLUMNS)
ENTITY_META_DATA(Account, COLUMNS)

Hier deklarieren wir in wenigen Zeilen die gesamte Struktur der Tabelle Konto:

  • id ist der Primärschlüssel (primary = true) und auto-increment (auto_inc = true),
  • number und balance sind Pflichtangaben,
  • owner ist Pflichttext.

Mit anderen Worten: Mit einem einzigen Definitionspunkt konnten wir die Klasse Account und ihre Metadatenklasse AccountMetadata erstellen, die für die Verwendung durch den ORM bereit waren.


Schlussfolgerung und nächste Schritte

Wir haben das Ende einer weiteren Phase der Entwicklung unseres ORM erreicht. Mit diesem Artikel haben wir einen wichtigen Weg beschritten:

  • Wir haben damit begonnen, besser zu verstehen, wie #define in MQL5 funktioniert, und zwar nicht nur für einfache Konstanten, sondern als Metaprogrammierwerkzeug.
  • Wir sind zur Erstellung von Entitäten (den Klassen, die unsere Tabellen darstellen) übergegangen und haben gesehen, wie man ihre Definition mithilfe von Makros vereinfachen kann.
  • Wir haben diese Entitäten mit Spaltenmetadaten angereichert, die Attribute wie Typ, Primärschlüssel, Autoinkrement, Eindeutigkeit und Nullbarkeit beschreiben.
  • Schließlich haben wir alles in der Datei TickORM.mqh zentralisiert, MQL5-Typen mit SQL verbunden und die Erzeugung von Metadatenklassen automatisiert.

Diese Grundlage ist von entscheidender Bedeutung: Jetzt haben wir nicht nur Entitäten, sondern auch die vollständige Beschreibung ihrer Eigenschaften, und dies wird der Motor sein, der es dem ORM ermöglicht, die Datenbank intelligent und automatisch zu manipulieren.

Im nächsten Artikel werden wir einen weiteren entscheidenden Schritt tun: Wir werden die Repository-Schicht erstellen. Diese Schicht ist für die Bearbeitung der Daten zuständig, ohne dass SQL manuell geschrieben werden muss. Stattdessen machen wir Aufrufe wie accountRepository.Save(account) oder ordersRepository.FindById(1), und der ORM kümmert sich um den Rest.

Mit anderen Worten: Wenn wir bisher gelernt haben, die Struktur der Tabellen zu beschreiben, werden wir im nächsten Artikel lernen, wie wir die Daten auf saubere, organisierte und sichere Weise bearbeiten können.

Dateiname Beschreibung
Include/TickORM/metadata/ColumnMetadata.mq5
Schnittstelle, die die Daten der Spalte darstellt
Include/TickORM/metadata/TableMetadata.mqh Schnittstelle, die die Tabellendaten darstellt
Include/TickORM/TickORM.mqh
Hauptdatei

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/19594

Beigefügte Dateien |
TickORM.zip (3.02 KB)
Wie man ein zyklusbasiertes Handelssystem aufbaut und optimiert (Detrended Price Oscillator – DPO) Wie man ein zyklusbasiertes Handelssystem aufbaut und optimiert (Detrended Price Oscillator – DPO)
Dieser Artikel erklärt, wie man ein Handelssystem mit dem Detrended Price Oscillator (DPO) in MQL5 entwickelt und optimiert. Er umreißt die Kernlogik des Indikators und zeigt, wie er kurzfristige Zyklen erkennt, indem er langfristige Trends herausfiltert. Anhand einer Reihe von Schritt-für-Schritt-Beispielen und einfachen Strategien lernen die Leser, wie man den Code erstellt, Ein- und Ausstiegssignale definiert und Backtests durchführt. Schließlich werden praktische Optimierungsmethoden vorgestellt, um die Leistung zu verbessern und das System an die sich ändernden Marktbedingungen anzupassen.
Pipelines in MQL5 Pipelines in MQL5
In diesem Beitrag befassen wir uns mit einem wichtigen Schritt der Datenaufbereitung für das maschinelle Lernen, der zunehmend an Bedeutung gewinnt. Pipelines für die Datenvorverarbeitung. Dabei handelt es sich im Wesentlichen um eine rationalisierte Abfolge von Datenumwandlungsschritten, mit denen Rohdaten aufbereitet werden, bevor sie in ein Modell eingespeist werden. So uninteressant dies für den Laien auch erscheinen mag, diese „Datenstandardisierung“ spart nicht nur Trainingszeit und Ausführungskosten, sondern trägt auch zu einer besseren Generalisierung bei. In diesem Artikel konzentrieren wir uns auf einige SCIKIT-LEARN Vorverarbeitungsfunktionen, und während wir den MQL5-Assistenten nicht ausnutzen, werden wir in späteren Artikeln darauf zurückkommen.
Automatisieren von Handelsstrategien in MQL5 (Teil 33): Erstellung des Preisaktions-Systems des harmonischen Musters Shark Automatisieren von Handelsstrategien in MQL5 (Teil 33): Erstellung des Preisaktions-Systems des harmonischen Musters Shark
In diesem Artikel entwickeln wir das System des Shark-Musters in MQL5, das steigende und fallende harmonische Shark-Muster unter Verwendung von Umkehrpunkten und Fibonacci-Ratios identifiziert und Handelsgeschäfte mit anpassbaren Einstiegs-, Stop-Loss- und Take-Profit-Levels basierend auf vom Nutzer ausgewählten Optionen ausführt. Wir verbessern den Einblick des Händlers mit visuellem Feedback durch Chart-Objekte wie Dreiecke, Trendlinien und Kennzeichnungen, um die X-A-B-C-D-Musterstruktur klar darzustellen.
Die Grenzen des maschinellen Lernens überwinden (Teil 4): Überwindung des irreduziblen Fehlers durch mehrere Prognosehorizonte Die Grenzen des maschinellen Lernens überwinden (Teil 4): Überwindung des irreduziblen Fehlers durch mehrere Prognosehorizonte
Maschinelles Lernen wird oft durch die Brille der Statistik oder der linearen Algebra betrachtet, aber dieser Artikel betont eine geometrische Perspektive der Modellvorhersagen. Sie zeigt, dass sich die Modelle dem Ziel nicht wirklich annähern, sondern es auf ein neues Koordinatensystem abbilden, was zu einer inhärenten Fehlausrichtung führt, die irreduzible Fehler zur Folge hat. In dem Artikel wird vorgeschlagen, dass mehrstufige Vorhersagen, bei denen die Prognosen des Modells über verschiedene Zeithorizonte hinweg verglichen werden, einen effektiveren Ansatz darstellen als direkte Vergleiche mit dem Ziel. Durch die Anwendung dieser Methode auf ein Handelsmodell zeigt der Artikel erhebliche Verbesserungen der Rentabilität und Genauigkeit, ohne das zugrunde liegende Modell zu verändern.