Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite
Inhaltsverzeichnis
- Einleitung: Vereinfachung der Arbeit mit der Datenbank
- Schrittweiser Aufbau eines ORM-Frameworks in MQL5
- Praktische Umsetzung
- Vollständige Muster
- Schlussfolgerung
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
- Eliminiert SQL aus der Geschäftslogik
- Bietet starke Typisierung
- Ermöglicht Code-First-Entwicklung
- Verbessert Sicherheit und Wartungsfreundlichkeit
BaseModel → ORMField → DatabaseORM → SQLite API ↑ MacroModelSchlü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:
| Methoden | Beschreibung |
|---|---|
| DefineFields | In 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. |
| AddField | Fügt der Tabelle ein Spaltenfeld hinzu. |
| GetCreateTableQuery | Hier wird die Methode zur Tabellenerstellung erstellt. |
| GetInsertQuery | Der Befehl zum Einfügen einer Zeile in die Datenbank ist eine virtuelle Methode, die in der abgeleiteten Klasse überschrieben werden kann. |
| GetUpdateQuery | Der Befehl zum Aktualisieren einer Zeile in der Datenbank ist eine virtuelle Methode, die in der abgeleiteten Klasse überschrieben werden kann. |
| LoadFromStatement | In 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. |
| GetTableName | Gibt den Tabellennamen zurück. |
| GetTableCount | Gibt die Anzahl der ausgewählten Felder zurück. |
| GetPrimaryKeyName | Gibt den Namen des Feldes zurück, das als Primärschlüssel ausgewählt wurde. |
| OnGet | Da der Wert über die Schnittstelle an die Variable übergeben wird, handelt es sich um eine abstrakte Schnittstelle, die von der Unterklasse geerbt werden sollte. |
| OnSet | Durch 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. |
| PullFromModel | Ruft den Wert der Variablen automatisch ab. |
| PushToModel | Setzt 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(); ... ... ... };
| Methoden | Beschreibung |
|---|---|
| Connect | Startet die Verbindung zur Datenbank. |
| Disconnect | Trennt die Verbindung zur Datenbank. |
| CreateTable | Erstellt einer Tabelle nach Bedarf. |
| Insert | Fügt Daten in eine Tabelle ein. |
| Update | Aktualisiert Daten in einer Tabelle. |
| Delete | Löscht Daten aus einer Tabelle. |
| Select | Sucht das erste Element, das durch die WHERE-Bedingung gefunden werden soll. |
| SelectAll | Durchsucht 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:

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:

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:

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:

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:

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.
| Dateiname | Beschreibung |
|---|---|
| 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.mqh | Datei mit den gekapselten MQL5-SQL-Basisfunktionen |
| MacroModel.mqh | Datei mit den Makrodefinitionen zur sofortigen Erstellung der nutzerdefinierten Basisklasse |
| MyDBClassModel v1.0.mqh | Datei 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.mqh | Datei mit den Datenbank-/SQL-Fehlerdefinitionen |
| ORMField.mqh | Datei 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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.