English
preview
Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite

Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite

MetaTrader 5Beispiele |
16 0
Hans Alexander Nolawon Djurberg
Hans Alexander Nolawon Djurberg


Inhaltsverzeichnis



Einleitung: Vereinfachung der Arbeit mit der Datenbank

In der Welt des algorithmischen Handels ist eine solide Datenverwaltung von entscheidender Bedeutung. MQL5 bietet eine Low-Level-Datenbank-API für SQLite, die zwar leistungsstark ist, aber eine manuelle SQL-Bearbeitung erfordert. Im Gegensatz zu modernen Sprachen wie C#, die ORM-Lösungen wie ADO.NET oder Entity Framework anbieten, fehlt MQL5 derzeit ein wiederverwendbares und produktionsreifes ORM-Framework. Dieser Artikel stellt das umfassende, leichtgewichtige, portable und professionelle ORM-Framework SQLite ORM (Object-Relational Mapping) für MQL5 vor, das SQL-Abfragefähigkeiten in MetaTrader 5 bringt, die speziell für SQLite entwickelt wurden.

Im Gegensatz zu herkömmlichen SQL-Ansätzen bietet dieses System eine flüssige Schnittstelle, die Datenbankoperationen intuitiv und wartbar macht. Auf den ersten Blick mag dies mühsam oder schwierig erscheinen, aber durch die Entwicklung eines Datenbankverwaltungssystems wird die Arbeit erleichtert, und wir werden die langen und mühsamen Befehle los. 

Nun, in dieser Hinsicht werden wir schrittweise vorgehen, um dieses Datenbank-Management-System im Stil des ORM (Object Relational Mapping) zu implementieren, die es uns einfach macht, mit der Datenbank zu arbeiten. Theoretisch müssen Sie nicht jedes Mal die grundlegenden MQL5-SQL-Funktionen wie DatabaseOpen/DatabaseClose/DatabasePrepare/DatabaseFinalize/... überall im Code kopieren und einfügen. Daher benötigen wir in diesem Fall ein Framework, das alle nützlichen Methoden sammelt, um die Arbeit auf einfache Weise zu erledigen und die Daten zwischen der Datenbank und den gewünschten Variablen reibungslos zu delegieren. Im praktischen Teil implementieren wir dann unser eigenes Modell, das die Datendelegation zwischen unseren gewünschten Variablen und der Datenbankdatei über ein ORM-Klassenobjekt widerspiegelt. Zum Schluss testen wir die implementierten Klassen in jedem Abschnitt, um die Ergebnisse zu überprüfen, die wir vom Framework erwarten. Am Ende des Artikels finden Sie das Framework-Modul und die Testdateien, um es in jedes Projekt zu integrieren, das ein Entwickler benötigt. Zum Zeitpunkt der Erstellung dieses Artikels gibt es kein ähnliches ORM-Framework im Bereich der MQL5-Artikel. Es ist das erste seiner Art für MQL5. Um dieses Rahmenwerk umzusetzen, werden wir einen Schritt nach dem anderen machen.


Warum ORM in MQL5?

Typischer Datenbankcode in MQL5:

  • Ist SQL-lastig 
  • Hat eine schwache Fehlerbehandlung
  • Ist nicht wiederverwendbar
  • Skaliert nicht für große EAs

Das Ziel dieses Rahmens:

  • Eliminiert SQL aus der Geschäftslogik
  • Bietet starke Typisierung
  • Ermöglicht Code-First-Entwicklung
  • Verbessert Sicherheit und Wartungsfreundlichkeit

Überblick über die Architektur:
BaseModel → ORMField → DatabaseORM → SQLite API
  ↑ MacroModel

Schlüsselkomponenten:

  • ORMField: Metadaten und -bindung der Daten
  • BaseModel: Basisklasse der Entität
  • DatabaseORM: SQL-Generierung und -Ausführung (ADO.NET-ähnliche Schnittstelle)
  • MacroModel: Code-First-Syntax


Schrittweiser Aufbau eines ORM-Frameworks in MQL5

1. Typ der Klasse für das Wörterbuch: Spiegelung der Schlüsselzuordnung

Im ersten Schritt benötigen wir eine Klasse, die Objekte mit dem Typ des Wörterbuchs speichert, auf das wir bei Bedarf über den Namen der Variablen, die wir dem Objekt geben, zugreifen können. Um diese Klasse zu implementieren, müssen wir sie von der in MQL5 integrierten Klasse CArrayOb ableiten und die einfache Wörterbuchstruktur implementieren.

template<typename T>
class CDictObj : public CArrayObj
  {

public :
                     CDictObj(void) {};
                    ~CDictObj(void) {Clear();};
   T*                operator[](const int index) const { return(At(index)); }
   T*                operator[](const string param) const
     {
      for(int index=0; index<Total(); index++)
        {
         T* obj = At(index);
         if(obj.Name() == param)
            return obj;
        }

      return NULL;
     }

  };

Sie muss als generisch implementiert werden, damit wir eine beliebige Klasse an sie binden können, sodass wir sie von der Methode dieser Klasse empfangen können.

2. Speichern der Feldeigenschaften: Definition einer Klasse zum Speichern von Feldinformationen für jede Spalte

Wir benötigen eine weitere Klasse, die die Felder, die wir der Datenbanktabelle hinzufügen wollen, als Column-Mapping speichert, d. h., sie speichert die Informationen dieses Feldes für uns in Form der Eingabeinformationen des SQL-Feldformats, zu denen gehören auch: Name / Typ (ist Text | Integer | Real) / Ist Primärschlüssel / Ist Autoinkrement / Ist nicht Null / Standardwert. In dieser Klasse werden die wichtigen Merkmale der einzelnen Felder der Spalte gespeichert. Diese Felder werden in der Klasse des Wörterbuchs, die wir kürzlich implementiert haben, mit ihren Feldnamen gespeichert, sodass sie verwendet werden können, um Daten aus diesen Feldern in die Datenbank zu übertragen.

// Field Definition
//+------------------------------------------------------------------+
class CORMField : public CObject
  {
   ...
   ...
   ...

   string            Name()    const { return m_name; }
   ENUM_FIELD_TYPE   FieldType() const { return m_fieldType; }
   uint              Flags()   const { return m_flags; }

   // -------- Attributes --------
   void              PrimaryKey(bool v=true)    { m_isPrimaryKey=v; }
   void              AutoIncrement(bool v=true) { m_isAutoIncrement=v; }
   void              Nullable(bool v=true)      { m_isNullable=v; }


   template<typename T>
   void              SetValue(T v)
     {
      if(m_value!=string(v))
         m_value=string(v);
     }

   string            GetValue() const
     {
      return m_value;
     }

   bool              IsPrimaryKey()    const { return m_isPrimaryKey; }
   bool              IsAutoIncrement() const { return m_isAutoIncrement; }
   bool              IsNullable()      const { return m_isNullable; }

   ...
   ...
   ...

  };
//+------------------------------------------------------------------+

Diese Klasse bietet zwei wichtige Methoden, um den Wert des gewünschten Feldes zu erhalten oder zu setzen. Sie wird verwendet, um den Wert des gewünschten Feldes abzurufen, zu setzen oder zu aktualisieren.

3. Erstellen einer Basismodellklasse für die Kommunikation zwischen Feldern und Datenbank: Steuerung von Daten und SQL-Modellen

Das CBaseModel bildet die Grundlage für alle Datenbankentitäten mit automatischer Feldverwaltung. Es ist eine starke Typisierung, automatische Zuordnung, wiederverwendbare Logik für das Kind geerbte Klasse.

Die Klasse CBaseModel speichert die Struktur einer Tabelle mit den Feldern der einzelnen Spalten und verwaltet die Rohdaten mit der SQL-Anweisung. Es gibt nämlich eine Beziehung zwischen der Datenbank und den Nutzerdaten, die mit der Struktur der SQL-Anweisung arbeitet, sodass die SQL-Anweisung nicht jedes Mal kopiert werden muss, wenn sie benötigt wird. Dieses Modell führt die SQL-Anweisung für uns aus und liefert uns die gewünschten Aufgaben.

// Base Model Class
class CBaseModel : public CObject
  {
   ...
   ...
   ...
  
   virtual void      DefineFields();
   void              AddField();
   string            GetCreateTableQuery();
   virtual string    GetInsertQuery();
   virtual string    GetUpdateQuery();
   virtual void      LoadFromStatement();
   string            GetTableName();
   int               GetTableCount();
   string            GetPrimaryKeyName();
   
      // --- Reflection-like API ---
   string            Get(string field);
   void              Set(string field,string value);
   // --------- Binding Interface (IMPORTANT) ---------
   virtual string    OnGet(string field) { return ""; };
   virtual void      OnSet(string field,string value) {};
   void              PullFromModel();
   void              PushToModel();

   ...
   ...
   ...
};

Hier sind einige der wichtigsten Methoden, die es bietet:

MethodenBeschreibung
DefineFieldsIn der abgeleiteten Klasse können wir diese Methode verwenden, um automatisch mehrere Felder in dieser Methode hinzuzufügen, sodass es nicht notwendig ist, das Feld an anderer Stelle im Code hinzuzufügen.
AddFieldFügt der Tabelle ein Spaltenfeld hinzu.
GetCreateTableQueryHier wird die Methode zur Tabellenerstellung erstellt.
GetInsertQueryDer Befehl zum Einfügen einer Zeile in die Datenbank ist eine virtuelle Methode, die in der abgeleiteten Klasse überschrieben werden kann.
GetUpdateQueryDer Befehl zum Aktualisieren einer Zeile in der Datenbank ist eine virtuelle Methode, die in der abgeleiteten Klasse überschrieben werden kann.
LoadFromStatementIn diesem Abschnitt erfolgt die Zuordnung der Daten aus der Datenbank zu der gewünschten Variablen. Sie ist virtuell und kann in einer abgeleiteten Klasse überschrieben werden.
GetTableNameGibt den Tabellennamen zurück.
GetTableCountGibt die Anzahl der ausgewählten Felder zurück.
GetPrimaryKeyNameGibt den Namen des Feldes zurück, das als Primärschlüssel ausgewählt wurde.
OnGetDa der Wert über die Schnittstelle an die Variable übergeben wird, handelt es sich um eine abstrakte Schnittstelle, die von der Unterklasse geerbt werden sollte.
OnSetDurch die Zuweisung eines Werts an die Schnittstelle, die mit der Variablen verknüpft ist, wird diese abstrakt, sodass sie in der Unterklasse vererbt werden sollte.
PullFromModelRuft den Wert der Variablen automatisch ab.
PushToModelSetzt automatisch den Wert der Variablen.

Diese Klasse bildet den Vorgang der Datenübertragung in die Datenbank ab. Wir müssen lediglich die gewünschten Felder hinzufügen, um diese Felder mit ihren Werten ohne weitere Anweisungen in die Datenbank zu übertragen. Wenn wir jedoch von dieser Klasse erben möchten, gibt es mehrere wichtige Methoden, die wir durch die Implementierung unseres gewünschten Modells umsetzen. Durch die Ableitung von dieser Modellklasse können wir unser eigenes Getter/Setter-Modell definieren. Indem wir drei wichtige Methoden überschreiben, stellen wir unsere eigene variable Verbindung mit der Datenbank her. In den Beispielen werden Sie sehen, wie wir die Verbindung zwischen unserer Variablen und der Datenbank herstellen, indem wir sie einfach von dieser Modellklasse ableiten.

4. SQLite-API: Eine Klasse verbindet sich direkt mit SQLite-Funktionen

Es gibt ein weiteres Modul, das direkt mit den SQLite-Funktionen arbeitet, um die SQL-Eingabeaufforderungen an die Datenbank nach der Verbindung zu reflektieren. Die Klasse CDatabase ist ein einfaches Modul, das SQLite-Funktionen kapselt und einfach einige nützliche Befehle ausführt, die wir benötigen, wie z. B. die Funktionen zum Öffnen | Schließen | Lesen | Löschen.

// Database sqlite API
class CDatabase
  {
    ...
    ...
    ...

   // ---------- Open / Close ----------
   bool              Open(string file, uint flags);
   void              Close();
   bool              IsOpen();
   int               Prepare(string sql);
   void              Finalize(int stmt);
   bool              Execute(string sql);
   bool              TransactionBegin();
   bool              TransactionCommit();
   bool              TransactionRollback(string sql);
   bool              Read(int stmt);

    ...
    ...
    ...
  };

Dieses Klassenobjekt enthält mehrere nützliche Funktionen, um sich direkt mit der SQLite-Datenbank zu verbinden, ohne die grundlegenden MQL5-Datenbankfunktionen an anderer Stelle im Code zu duplizieren. Dann initialisieren wir einfach ein Objekt dieser Klasse und verbinden uns jederzeit direkt mit unserer Datenbankdatei über dieses Objekt.

5. Kernarchitektur: Database-ORM-Foundation (Erstellen eines ORM-Klassenmodells)

Die Klasse CDatabaseORM dient als Grundlage und stellt die Verbindungsverwaltung, die Transaktionsverarbeitung und die wichtigsten CRUD-Operationen (Erstellen/Lesen/Aktualisieren/Löschen) bereit. Diese Klasse bietet die grundlegendste und gleichzeitig wichtigste Datenbankarchitektur, d. h., sie nimmt ein Objekt aus dem Datenbankmodell, das die Felder der Datenbank speichert, als Eingabe und führt auf der Grundlage der korrekten SQL-Abfrage, die sie bereitstellt, die angeforderten Aufgaben aus. Diese Klasse führt nämlich datenbankbezogene Aufgaben aus, einschließlich CREATE/INSERT/UPDATE/DELETE:

// ORM core model
class CDatabaseORM
{
    ...
    ...
    ...

    bool Connect();
    void Disconnect();
    bool CreateTable();
    bool Insert();
    bool Update();
    bool Delete();
    bool Select();
    bool SelectAll();

    ...
    ...
    ...
};
MethodenBeschreibung
ConnectStartet die Verbindung zur Datenbank.
DisconnectTrennt die Verbindung zur Datenbank.
CreateTableErstellt einer Tabelle nach Bedarf.
InsertFügt Daten in eine Tabelle ein.
UpdateAktualisiert Daten in einer Tabelle.
DeleteLöscht Daten aus einer Tabelle.
SelectSucht das erste Element, das durch die WHERE-Bedingung gefunden werden soll.
SelectAllDurchsucht alle Artikel, die wir mit der WHERE-Bedingung finden möchten.

Wesentliche Merkmale:

  • Automatisches Verbindungsmanagement: Nahtlose Verarbeitung von Datenbankverbindungen.
  • Transaktionsunterstützung: Vollständige Übereinstimmung mit dem Verbinden/Trennen.
  • SQL-Eingabeaufforderungen anwenden: Volle Unterstützung von Create/Insert/Update/Delete/Select.
  • Pooling von Verbindungen: Effizientes Ressourcenmanagement.
  • Fehlerbehandlung: Umfassende Fehlerberichte und Wiederherstellung.

Diese Klasse übernimmt die Arbeit der Verbindung zur Datenbank für uns. Hier brauchen wir etwas, das die Arbeit der Datenübertragung für uns übernimmt, d. h. eine Verbindung zur Klasse CDatabaseORM von dem Objekt aus, das Daten in sich selbst gesammelt hat, um Daten in der Datenbank zu aktualisieren oder einzufügen. Dieses Objekt empfängt Daten von der CBaseModel-Klasse (oder einem Modell, das von dieser CBaseModel-Klasse abgeleitet wurde) als Eingabe, und wir übertragen unseren gewünschten Befehl mit diesem Objekt, und CDatabaseORM führt diese Befehle aus.

Kommen wir nun zu praktischen Beispielen für jeden Teil der Klasse, die wir bisher implementiert haben.


Praktische Umsetzung

1. Definieren eines nutzerdefinierten Handelsberichtsmodells

Lassen Sie uns ein umfassendes Modell für die Speicherung von Backtest-Ergebnissen erstellen:

class CTradeReportModel : public CBaseModel
  {
private:
   long             m_id;
   string           m_strategy_name;
   datetime         m_report_date;
   double           m_total_net_profit;
   double           m_profit_factor;
   double           m_max_drawdown_relative;
   int              m_total_trades;
   string           m_parameters;

public:
                     CTradeReportModel(string table_name="trade_report") : CBaseModel(table_name)
     {
      m_id = 0;
      m_total_net_profit = 0;
      m_profit_factor = 0;
      m_max_drawdown_relative = 0;
      m_total_trades = 0;
     }

                     CTradeReportModel(const CTradeReportModel& model) : CBaseModel(model)
     {
     }



   void              DefineFields() override
     {
      AddField("id", FIELD_TYPE_INT, true, true, true);
      AddField("strategy_name", FIELD_TYPE_STRING, false, false, true);
      AddField("report_date", FIELD_TYPE_DATETIME, false, false, true);
      AddField("total_net_profit", FIELD_TYPE_DOUBLE);
      AddField("profit_factor", FIELD_TYPE_DOUBLE);
      AddField("max_drawdown_relative", FIELD_TYPE_DOUBLE);
      AddField("total_trades", FIELD_TYPE_INT);
      AddField("parameters", FIELD_TYPE_STRING);
     }

   string            GetInsertQuery() override
     {
      string query = StringFormat("INSERT INTO %s (strategy_name, report_date, total_net_profit, profit_factor, " +
                                  "max_drawdown_relative, total_trades, parameters) " +
                                  "VALUES ('%s','%s', %d, %.2f, %.2f, %d, '%s')",
                                  GetTableName(),
                                  m_strategy_name, TimeToString(m_report_date), m_total_trades, m_profit_factor, m_max_drawdown_relative,
                                  m_total_trades, m_parameters);

      return query;
     }

   void              LoadFromStatement(int statement) override
     {
      m_id = DatabaseColumnInt(statement, 0);
      m_strategy_name = DatabaseColumnText(statement, 1);
      m_report_date = (datetime)DatabaseColumnInt(statement, 2);
      m_total_net_profit = DatabaseColumnDouble(statement, 3);
      m_profit_factor = DatabaseColumnDouble(statement, 4);
      m_max_drawdown_relative = DatabaseColumnDouble(statement, 5);
      m_total_trades = DatabaseColumnInt(statement, 6);
      m_parameters = DatabaseColumnText(statement, 7);
     }

   // Setters
   void              SetStrategyName(string name) { m_strategy_name = name; m_fields["strategy_name"].SetValue(name); }
   void              SetTotalNetProfit(double profit) { m_total_net_profit = profit; m_fields["total_net_profit"].SetValue(profit);}
   void              SetProfitFactor(double factor) { m_profit_factor = factor; m_fields["profit_factor"].SetValue(factor);}
   void              SetMaxDrawdown(double max_dd) { m_max_drawdown_relative = max_dd; m_fields["max_drawdown_relative"].SetValue(max_dd);}
   void              SetReportDate(datetime date) { m_report_date = date; m_fields["report_date"].SetValue(date);}
   void              SetTotalTrades(int total) { m_total_trades = total; m_fields["total_trades"].SetValue(total);}
   void              SetParameters(string param) { m_parameters = param; m_fields["parameters"].SetValue(param);}

   // Getters
   long            GetId() const { return m_id; }
   string          GetStrategyName() const { return m_strategy_name; }
   double          GetTotalNetProfit() const { return m_total_net_profit; }
   double          GetProfitFactor() const { return m_profit_factor; }
   double          GetMaxDrawdown() const { return m_max_drawdown_relative; }
   datetime        GetReportDate() const { return m_report_date; }
   int             GetTotalTrades() const { return m_total_trades; }
   string          GetParameters() const { return m_parameters; }

  };

Dies ist eine nutzerdefinierte untergeordnete Klasse, die von CBaseModel abgeleitet worden ist, und wir bieten unsere eigenen Getter/Setter-Methoden. In der Methode „LoadFromStatement“ binden wir unsere Variablen tatsächlich an die Datenbank in dieser Methode. Wenn wir Werte aus der Datenbank laden möchten, erfolgt dies über diese Methode, und wir rufen den Getter auf, um den Wert der gewünschten Variablen abzurufen.

2. Verwendung der Schnittstelle für die Reflexionsbindung

In einem anderen Beispiel können wir unsere Variablen binden, um die Werte von/auf die Datenbank zu erhalten/zu setzen, indem wir die Hauptmethoden von ihnen als OnGet/OnSet wie unten überschreiben:

//  binding model
class TradeReportModel : public CBaseModel
  {
public:
   int               Id;
   string            Symbol;
   double            Lots;
   datetime          OpenTime;

                     TradeReportModel()
     {
      Table("TradeReport");

      AddField(new CORMField("Id",FIELD_TYPE_INT,PRIMARY_KEY|AUTO_INCREMENT));
      AddField(new CORMField("Symbol",FIELD_TYPE_STRING,REQUIRED));
      AddField(new CORMField("Lots",FIELD_TYPE_DOUBLE));
      AddField(new CORMField("OpenTime",FIELD_TYPE_DATETIME));
     }

   // ---------- Binding ----------
   string            OnGet(string field) override
     {
      if(field=="Id")
         return (string)Id;
      if(field=="Symbol")
         return Symbol;
      if(field=="Lots")
         return DoubleToString(Lots);
      if(field=="OpenTime")
         return (string)OpenTime;
      return "";
     }

   void              OnSet(string field,string value) override
     {
      if(field=="Id")
         Id=(int)value;
      if(field=="Symbol")
         Symbol=value;
      if(field=="Lots")
         Lots=StringToDouble(value);
      if(field=="OpenTime")
         OpenTime=(datetime)value;
     }
  };

Bei dieser Art der Bindung haben wir die überschriebenen Funktionen verwendet, die die gebundenen Variablen widerspiegeln, um zwischen unseren Daten und Feldern zu delegieren.

3. Die Macht des Makros nutzen: Makromodelle für Code-First

Es gibt eine weitere Möglichkeit, unsere eigene nutzerdefinierte Klasse in kürzester Zeit zu implementieren, indem wir die Macht der Makros nutzen. Nun, eine nutzerdefinierte Klasse mit mehreren Variablen und entsprechenden Getter/Setter-Methoden sowie die Bindung unserer Variablen an die Datenbank:

//--------DB class start
//-- class start
DB_CLASS_BEGIN(CMyDBClassModel)

    //-- defining the getter/setter members 
    DB_DEFINE_FIELD_INT(id)
    DB_DEFINE_FIELD_STRING(strategy_name)
    DB_DEFINE_FIELD_DATETIME(report_date)
    DB_DEFINE_FIELD_DOUBLE(total_net_profit)
    DB_DEFINE_FIELD_DOUBLE(profit_factor)
    DB_DEFINE_FIELD_DOUBLE(max_drawdown_relative)
    DB_DEFINE_FIELD_INT(total_trades)
    DB_DEFINE_FIELD_STRING(parameters)

    
    //-- adding the members
    DB_ADD_BEGIN
    
        DB_ADD_FIELD_INT(id,PRIMARY_KEY | AUTO_INCREMENT | REQUIRED)
        DB_ADD_FIELD_STRING(strategy_name,REQUIRED)
        DB_ADD_FIELD_DATETIME(report_date,REQUIRED)
        DB_ADD_FIELD_DOUBLE(total_net_profit,FIELD_NONE)
        DB_ADD_FIELD_DOUBLE(profit_factor,FIELD_NONE)
        DB_ADD_FIELD_DOUBLE(max_drawdown_relative,FIELD_NONE)
        DB_ADD_FIELD_INT(total_trades,FIELD_NONE)
        DB_ADD_FIELD_STRING(parameters,FIELD_NONE)
    
    DB_ADD_END
    
    
    //-- binding the members
    DB_BIND_BEGIN
    
        DB_BIND_FIELD_INT(id,0)
        DB_BIND_FIELD_STRING(strategy_name,1)
        DB_BIND_FIELD_DATETIME(report_date,2)
        DB_BIND_FIELD_DOUBLE(total_net_profit,3)
        DB_BIND_FIELD_DOUBLE(profit_factor,4)
        DB_BIND_FIELD_DOUBLE(max_drawdown_relative,5)
        DB_BIND_FIELD_INT(total_trades,6)
        DB_BIND_FIELD_STRING(parameters,7)
    
    DB_BIND_END

//-- class end
DB_CLASS_END

Hier werden die Felder der Typen Integer/Double/String/Datetime/Boolean definiert und gebunden, und die nutzerdefinierte Klasse „CMyDBClassModel“ wird erstellt. Durch die Definition eines Objekts dieser Klasse haben wir unser eigenes nutzerdefiniertes Modell, das von CBaseModel geerbt wird, sodass wir die Struktur der Felder einer Tabelle auf unser Datenbankobjekt CDatabaseORM übertragen können, sodass der gewünschte Befehl auf dieses Objekt angewendet werden kann. Im nächsten Abschnitt gehen wir zum Testbeispiel und verwenden die von uns implementierten Klassen, um zu sehen, wie einfach es ist, mit der Datenbank zu arbeiten.


Vollständige Muster

Beispiel für die einfache Verwendung der Klassen und die anschließende Umwandlung und den Aufruf des Modells in eine ORM-Objektklasse durch das Befüllen der Datenbank mit Backtest-Ergebnissen. Hier ist ein umfassendes Beispiel, das eine Datenbank erstellt und diese mit den Beispieldaten des Backtests befüllt. (Datei TestDatabase.mq5):

1. Durch Verwendung des CBaseModel-Objekts:

bool              InitializeDatabase1()
     {
      Print("=== Backtest Data Generator by Native Base ===");

      Print("Initializing database...");

      if(!m_database.Connect())
        {
         Print("Failed to connect to database");
         return false;
        }

      // Create tables
      report_model.AddField("id", FIELD_TYPE_INT, true, true, true);
      report_model.AddField("strategy_name", FIELD_TYPE_STRING, false, false, true);
      report_model.AddField("report_date", FIELD_TYPE_DATETIME, false, false, true);
      report_model.AddField("total_net_profit", FIELD_TYPE_DOUBLE);
      report_model.AddField("profit_factor", FIELD_TYPE_DOUBLE);
      report_model.AddField("max_drawdown_relative", FIELD_TYPE_DOUBLE);
      report_model.AddField("total_trades", FIELD_TYPE_INT);
      report_model.AddField("parameters", FIELD_TYPE_STRING);

      if(!m_database.CreateTable(report_model))
        {
         Print("Failed to create table");
         return false;
        }

      Print("Database initialized successfully");
      return true;
     }

Nachdem wir eine Verbindung zur Datenbank hergestellt haben, initialisieren wir unser gewünschtes Modell mithilfe der Methode AddField über das Objekt der Klasse CBaseModel. Und dann erstellen wir eine Tabelle, indem wir dasselbe Modellobjekt der Datenbank präsentieren. Und dann legen wir die Werte fest, die in die Datenbank übertragen werden sollen:

// Sample strategy configurations
      string strategies[] =
        {
         "MA_Crossover_10_20", "RSI_Strategy_14_30_70",
         "Bollinger_Bands_20_2", "MACD_Strategy_12_26_9",
         "Stochastic_14_3_3", "Parabolic_SAR_002_02",
         "Ichimoku_Cloud", "ADX_Strategy_14",
         "Price_Action_Breakout", "Volume_Weighted_MA"
        };

      string symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "XAUUSD"};
      string timeframes[] = {"H1", "H4", "D1", "W1"};

      int totalRecords = 0;

      for(int i = 0; i < ArraySize(strategies); i++)
        {
         for(int j = 0; j < ArraySize(symbols); j++)
           {
            for(int k = 0; k < ArraySize(timeframes); k++)
              {
               // Generate realistic backtest results
               report_model["strategy_name"].SetValue(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]);
               report_model["report_date"].SetValue(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month
               report_model["total_net_profit"].SetValue(GenerateRealisticProfit());
               report_model["profit_factor"].SetValue(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0
               report_model["max_drawdown_relative"].SetValue(5.0 + (MathRand() % 250) / 10.0); // 5% to 30%
               report_model["total_trades"].SetValue(50 + (MathRand() % 450)); // 50 to 500 trades

               string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s",
                                                symbols[j], timeframes[k], strategies[i]);
               report_model["parameters"].SetValue(parameters);

               if(m_database.Insert(report_model))
                 {
                  totalRecords++;
                 }

               // Add some variation
               Sleep(10);
              }
           }
        }

Hier haben wir Beispielwerte erstellt und diese im Wörterbuchmodus an das Modell übergeben; anschließend haben wir die Werte mithilfe desselben Modells an die Datenbank weitergeleitet, um die INSERT-Anweisung in der Datenbank über die Insert-Methode des Datenbankmodellobjekts auszuführen. In der folgenden Abbildung sehen Sie diese in der Datenbank gespeicherten Daten, wenn wir die Datenbankdatei mit MetaEditor öffnen.

Ausgabe:

Mit dem Beispielcode erstellte Datenbank

Dies ist die Berichtsmeldung über die Initialisierung der Datenbank mit dem Beispielcode. Es gibt drei Code-Beispiele für die Erstellung von Datenbanken nach jedem Modell, die jeweils in den unten stehenden Berichtsmeldungen ausgedruckt werden.

Ausgabe:

Bericht über die Initialisierung der Datenbank anhand eines Code-Beispiels melden

Anschließend können wir mit diesem Code leicht eine der benötigten Zeilen auswählen:

...
...
...
// Select example
   if(db.Select(report_model))
     {
      PrintFormat("Found record : %s", report_model["strategy_name"].GetValue());
     }

// SelectAll example
   CArrayObj found_array;

   int items = db.SelectAll(found_array,report_model,"profit_factor > 1.0");
   Print("items : ", items);
   if(items>0)
     {
      for(int i=0; i<found_array.Total(); i++)
        {
         CBaseModel* _temp = found_array.At(i);
         if(_temp!=NULL)
            PrintFormat("Found record : %s", _temp["strategy_name"].GetValue());
        }

     }
...
...
...

Wie Sie sehen können, haben wir, um den ersten Wert aus der Datenbank abzurufen, die Methode Select aufgerufen, die den SQL-Befehl SELECT aus dem CDataBaseORM-Objekt ohne WHERE-Bedingung bereitstellt. Im nächsten Schritt wollten wir, dass mehrere Werte für uns gefunden und zurückgegeben werden. Daher haben wir die SelectAll-Methode mit der WHERE-Bedingung „profit_factor > 1.0“ aufgerufen, und wie Sie sehen können, wurde dieser Befehl korrekt ausgeführt und hat die gewünschten Werte zurückgegeben.

Ausgabe:

Ausführungsabfrage SelectAll

2. Durch die Verwendung des nutzerdefinierten CTradeReportModel-Objekts

Hier geht es um die Verwendung der nutzerdefinierten Klasse CTradeReportModel, die wir implementiert haben:

...
...
...
bool              InitializeDatabase2()
     {

      Print("=== Backtest Data Generator by Inherited Base ===");

      Print("Initializing database...");

      if(!m_database.Connect())
        {
         Print("Failed to connect to database");
         return false;
        }

      // Create tables
      trade_model.DefineFields();
      if(!m_database.CreateTable(trade_model))
        {
         Print("Failed to create table");
         return false;
        }

      Print("Database initialized successfully");
      return true;
     }
...
...
...

Nachdem wir eine Verbindung zu unserer gewünschten Datenbank hergestellt haben, rufen wir die Methode DefineFields auf, die, wie im vorherigen Beispiel, automatisch die Methode AddFields aufruft, die wir in dieser Methode angegeben haben. Und dann erstellen wir die Tabelle mit dem Datenbank-ORM-Objekt und einem Objekt des nutzerdefinierten Modells.

...
...
...
               // Generate realistic backtest results
               trade_model.SetStrategyName(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]);
               trade_model.SetReportDate(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month
               trade_model.SetTotalNetProfit(GenerateRealisticProfit());
               trade_model.SetProfitFactor(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0
               trade_model.SetMaxDrawdown(5.0 + (MathRand() % 250) / 10.0); // 5% to 30%
               trade_model.SetTotalTrades(50 + (MathRand() % 450)); // 50 to 500 trades

               string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s",
                                                symbols[j], timeframes[k], strategies[i]);
               trade_model.SetParameters(parameters);

               if(m_database.Insert(trade_model))
...
...
...

Hier verhalten wir uns wie im vorherigen Beispiel, jedoch mit einem kleinen Unterschied: Wir können nicht nur den Status des Wörterbuchs unseres Modells verwenden, sondern auch die Getter/Setter-Status der von uns implementierten nutzerdefinierten Methoden. Anschließend können wir, wie im vorherigen Beispiel, die gewünschten Werte mit dem WHERE-Befehl wie folgt aus der Datenbank abrufen:

...
...
...
// Select example
   CTradeReportModel found_by_select;
   if(db.Select(found_by_select))
     {
      PrintFormat("Found record : %s", found_by_select.GetStrategyName());
     }

// SelectAll example
   CTradeReportModel found_by_selectAll;
   CArrayObj found_array;
   if(db.SelectAll(found_array,found_by_selectAll,"profit_factor > 1.0")>0)
     {
      for(int i=0; i<found_array.Total(); i++)
        {
         CTradeReportModel* _temp = found_array.At(i);
         PrintFormat("Found record : %s", _temp.GetStrategyName());
        }

     }
 ...
 ...
 ...

Hier gibt die Klasse CArrayObj ein Objekt unserer nutzerdefinierten Modellklasse zurück, nämlich die Klasse CTradeReportModel, und wir rufen die von uns definierte Getter-Methode auf, um die Werte zu erhalten. Wie Sie sehen können, werden auch hier die Werte korrekt zurückgegeben, indem die von der SelectAll-Methode zurückgegebenen Objekte gedruckt werden.

Ausgabe:

SelectAll-Ausführungsabfrage für abgeleitete Basis

3. Durch die Nutzung der Macht des Makroobjekts

In diesem Teil können wir ganz einfach die gleichen Dinge tun wie in dem Beispiel, das wir gerade gegeben haben, außer dass wir die nutzerdefinierte Klasse verwenden, die wir mit dem Makro erstellt haben:

...
...
...
bool              InitializeDatabase3()
     {

      Print("=== Backtest Data Generator by Macro Base ===");

      Print("Initializing database...");

      if(!m_database.Connect())
        {
         Print("Failed to connect to database");
         return false;
        }

      // Create tables
      macro_model.DefineFields();
      if(!m_database.CreateTable(macro_model))
        {
         Print("Failed to create table");
         return false;
        }

      Print("Database initialized successfully");
      return true;
     }
...
...
...

Hier gehen wir genauso vor wie kürzlich bei der nutzerdefinierten Makroklasse und übertragen die Beispielwerte in die Datenbank:

...
...
...
             // Generate realistic backtest results
               macro_model.Set_strategy_name(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]);
               macro_model.Set_report_date(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month
               macro_model.Set_total_net_profit(GenerateRealisticProfit());
               macro_model.Set_profit_factor(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0
               macro_model.Set_max_drawdown_relative(5.0 + (MathRand() % 250) / 10.0); // 5% to 30%
               macro_model.Set_total_trades(50 + (MathRand() % 450)); // 50 to 500 trades

               string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s",
                                                symbols[j], timeframes[k], strategies[i]);
               macro_model.Set_parameters(parameters);

               if(m_database.Insert(macro_model))
...
...
...

Wie Sie sehen können, werden die automatischen Getter-/Setter-Methoden durch die Makrodefinition implementiert. Und dann rufen wir, wie im vorherigen Beispiel, die Methode SelectAll der Datenbank auf, um die gewünschten Werte anhand der WHERE-Bedingung abzurufen:

...
...
...
// Select example
   CMyDBClassModel found_by_select;
   if(db.Select(found_by_select))
     {
      PrintFormat("Found record : %s", found_by_select.Get_strategy_name());
     }

// SelectAll example
   CMyDBClassModel found_by_selectAll;
   CArrayObj found_array;
   if(db.SelectAll(found_array,found_by_selectAll,"profit_factor > 1.0")>0)
     {
      for(int i=0; i<found_array.Total(); i++)
        {
         CMyDBClassModel* _temp = found_array.At(i);
         PrintFormat("Found record : %s", _temp.Get_strategy_name());
        }

     }
...
...
...

Hier wird ein Objekt der Klasse CMyDBClassModel in der Modellklasse zurückgegeben, und wir drucken den Wert mithilfe der Getter-Methode aus, die von der SelectAll-Methode gefunden wurde.

Ausgabe:

SelectAll-Ausführungsabfrage für Makrobasis

4. Schnittstelle für die Reflexionsbindung

In einem anderen Beispiel zur Bindung unserer Variablen durch Reflexion, die wir vor Kurzem als überschriebene OnGet-/OnSet-Methoden implementiert haben, gehen wir wie folgt vor:

...
...
...

   // creating fields
   CORMField *id=new CORMField("id",FIELD_TYPE_INT);
   id.PrimaryKey(true);
   id.AutoIncrement(true);

   CORMField *name=new CORMField("name",FIELD_TYPE_STRING);
   name.SetValue("Alice");

   // adding fields to model
   model.AddField(id);
   model.AddField(name);
   
   // spending the model fields to ORM database
   orm.CreateTable(model);
   orm.Insert(model);

...
...
...

Zuerst haben wir die Spaltenfelder erstellt und sie dann dem Modell hinzugefügt, das aus der Klasse CBaseModel erstellt wurde. Anschließend werden sie an die ORM-Datenbank ausgegeben, um in die gewünschte Datenbank gefüllt zu werden. Auf eine andere Weise haben wir das von unserer CBaseModel-Entität geerbte Modell verwendet, das ebenfalls die Reflection-API ausführt:

...
...
...

   // creating ORM DB
   CDatabaseORM db;
   db.Connect("trade.db", db_flags);

   // definging the inherited model
   TradeReportModel report;
   report.Symbol   = "EURUSD";
   report.Lots     = 0.10;
   report.OpenTime = TimeCurrent();

   // spending the custom model to ORM DB
   db.CreateTable(report);
   db.Insert(report);

   // Retrieving data from DB by custom model 
   TradeReportModel loaded;
   if(db.Select(loaded,"Symbol='EURUSD'"))
     {
      Print("Loaded: ",loaded.Symbol," ",loaded.Lots);
     }

   // Updating data
   loaded.Lots = 0.20;
   db.Update(loaded);

   // Soft delete
   db.Delete(loaded);

...
...
...

Mit diesen vier einfachen Methoden können wir leicht eine Verbindung zur Datenbank herstellen, ohne lange und mühsame SQL-Befehle.

Vorzüge, Anwendungsfälle und wesentliche Vorteile:

  • Typensicherheit: Überprüfung von Feldnamen und -typen zur Kompilierungszeit
  • Leistung: Effiziente SQL-Generierung und -Ausführung
  • Flexibilität: Unterstützung für komplexe Abfragen, Transaktionen



Schlussfolgerung

Dieses leistungsstarke ORM-Framework-System vereinfacht die Datenbankentwicklung und bringt moderne Datenbankmanagement-Fähigkeiten des C# ORM-Konzepts in MQL5 ein, die eine robuste Grundlage für die Speicherung und Analyse von Handelsstrategiedaten bieten. Ganz gleich, ob Sie ein umfassendes Backtest-System aufbauen, die Performance des Live-Handels verfolgen oder Strategieforschung betreibe: Dieses ORM bietet Ihnen die Werkzeuge, die Sie für ein effektives Datenmanagement in MetaTrader 5 benötigen. Wir haben gelernt, wie man Daten einfach mit der Datenbank verbindet, sendet und abruft. Der vollständige Quellcode ist in den beiliegenden Header-Dateien verfügbar. Er ist für professionelle und umfangreiche Projekte geeignet und kann in Ihre MQL5-Projekte integriert werden. In der folgenden Tabelle sind alle Quellcodedateien beschrieben, die dem Artikel beigefügt sind.

DateinameBeschreibung
BaseModel v1.0.mqh
BaseModel v2.0.mqh
Datei mit den Basisklassenmodellen für die Kommunikation zwischen Daten und Datenbank 
DatabaseORM v1.0.mqh
DatabaseORM v2.0.mqh
Datei, die das ORM-Framework enthält, das das Basismodellobjekt abruft, um SQL-Abfragebefehle auszuführen
Database.mqhDatei mit den gekapselten MQL5-SQL-Basisfunktionen
MacroModel.mqhDatei mit den Makrodefinitionen zur sofortigen Erstellung der nutzerdefinierten Basisklasse
MyDBClassModel v1.0.mqhDatei mit dem Beispielcode einer nutzerdefinierten Basisklasse, die durch Makrodefinition erstellt wurde
TradeReportModel v1.0.mqh
TradeReportModel v2.0.mqh
Datei mit dem Beispielcode einer von CBaseModel abgeleiteten Klasse
ORMError.mqhDatei mit den Datenbank-/SQL-Fehlerdefinitionen
ORMField.mqhDatei mit der Feldsammlung der Datenbankspalte
TestDatabase.mq5
TestDatabase v2.mq5
Datei mit Beispielen für die Verwendung der einzelnen Modelle wie CBase Model | Inherited Model | Macro Model | Reflection Binding Model

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

Beigefügte Dateien |
DatabaseORM.zip (19.65 KB)
Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates
Dieser Artikel beschreibt die Verwendung von CSV-Dateien für das Backtesting von Aktualisierungen der Portfoliogewichte in einer auf der Rückkehr zum Mittelwert basierenden Strategie, die statistische Arbitrage durch kointegrierte Aktien nutzt. Sie reicht von der Einspeisung der Ergebnisse der Rolling Windows Eigenvektor Comparison (RWEC) in die Datenbank bis zum Vergleich der Backtest-Berichte. In der Zwischenzeit werden in dem Artikel die Rolle der einzelnen RWEC-Parameter und ihre Auswirkung auf das Gesamtergebnis des Backtests detailliert beschrieben und gezeigt, wie der Vergleich des relativen Drawdowns uns helfen kann, diese Parameter weiter zu verbessern.
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement
In diesem Artikel verbessern wir den Indikator Smart WaveTrend Crossover in MQL5 durch die Integration von Canvas-basiertem Zeichnen für Überlagerung mit Nebelverläufen, Signalkästchen, die Ausbrüche erkennen, und anpassbaren Kauf-/Verkaufsblasen oder Dreiecken für visuelle Warnungen. Wir integrieren Funktionen für das Risikomanagement mit dynamischen Take-Profit- und Stop-Loss-Niveaus, die über Kerzenmultiplikatoren oder Prozentsätze berechnet und in Form von Linien und einer Tabelle angezeigt werden, sowie Optionen für Trendfilterung und Box-Erweiterungen.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren
In diesem Artikel entwickeln wir einen nutzerdefinierten Indikator in MQL5 namens Smart WaveTrend Crossover, der zwei WaveTrend-Oszillatoren verwendet – einen für die Erzeugung der Signale über das Kreuzen und einen anderen für die Trendfilterung – mit anpassbaren Parametern für Kanal-, Durchschnitts- und gleitende Durchschnittslängen. Der Indikator stellt farbige Kerzen auf der Grundlage der Trendrichtung dar, zeigt Kauf- und Verkaufspfeilsignale bei Überkreuzungen an und enthält Optionen zur Aktivierung der Trendbestätigung und zur Anpassung visueller Elemente wie Farben und Offsets.