English 日本語
preview
Meistern der Log-Einträge (Teil 6): Speichern von Protokollen in der Datenbank

Meistern der Log-Einträge (Teil 6): Speichern von Protokollen in der Datenbank

MetaTrader 5Beispiele |
60 0
joaopedrodev
joaopedrodev

Einführung

Stellen Sie sich einen belebten Marktplatz für digitalen Handel und Finanzmagie vor, auf dem jede Bewegung verfolgt, aufgezeichnet und akribisch auf Erfolg analysiert wird. Wie wäre es, wenn Sie nicht nur auf eine Chronik aller Entscheidungen und Fehler Ihrer Expert Advisors (EAs) zugreifen könnten, sondern auch über ein leistungsstarkes Tool verfügen würden, um diese Bots in Echtzeit zu optimieren und zu verfeinern? Willkommen zu Teil 1 von Meistern der Log-Einträge (Teil 1): Grundlegende Konzepte und erste Schritte in MQL5, wo wir mit der Erstellung einer ausgefeilten, auf die MQL5-Entwicklung zugeschnittenen Logging-Bibliothek begonnen haben.

Hier haben wir die Beschränkungen der Standard-Schnittstelle für die Logs von MetaTrader 5 geknackt, um eine robuste, anpassbare und dynamische Protokollierungslösung zu entwickeln, die die MQL5-Landschaft bereichert. Unsere Reise begann mit der Integration wichtiger Anforderungen: eine zuverlässige Singleton-Struktur für konsistente Kodierung, fortschrittliche Datenbankprotokolle für umfassende Prüfpfade, vielseitige Ausgabeflexibilität, Klassifizierung von Protokollstufen und anpassbare Formate für unterschiedliche Projektanforderungen.

Erfahren Sie jetzt, wie Sie Rohdaten in verwertbare Erkenntnisse umwandeln können, um die Leistung Ihrer EAs besser zu verstehen, zu kontrollieren und zu steigern als je zuvor.

In diesem Artikel werden wir alles von grundlegenden Konzepten bis hin zur praktischen Implementierung eines Log-Handlers untersuchen, der Daten direkt aus einer Datenbank schreibt und abfragt.


Was sind Datenbanken?

Protokolle sind der Puls eines Systems und erfassen alles, was hinter den Kulissen passiert. Aber sie effizient zu lagern ist eine andere Sache. Bisher haben wir die Protokolle in Textdateien gespeichert, eine einfache und funktionelle Lösung für viele Fälle. Das Problem entsteht, wenn das Datenvolumen wächst: Die Suche nach Informationen in Tausenden von Zeilen wird zu einem Leistungs- und Verwaltungsalptraum.

Hier kommen die Datenbanken ins Spiel. Sie bieten eine strukturierte und optimierte Möglichkeit, Informationen zu speichern, abzufragen und zu organisieren. Anstatt Dateien manuell zu durchsuchen, können wir schnelle Abfragen durchführen und genau das finden, was wir brauchen. Aber was ist eigentlich eine Datenbank und warum ist sie so wichtig?


Die Struktur einer Datenbank

Eine Datenbank ist ein intelligentes Speichersystem, in dem die Daten logisch organisiert sind, um die Suche und Bearbeitung zu erleichtern. Stellen Sie sich eine gut katalogisierte Datei vor, in der jede Information ihren festen Platz hat. Im Zusammenhang mit Protokollen können wir die Datensätze, anstatt sie in verstreuten Dateien zu speichern, strukturiert speichern und schnell nach Datum, Fehlertyp oder anderen relevanten Kriterien filtern.

Zum besseren Verständnis sollten wir die Struktur einer Datenbank in ihre drei grundlegenden Bestandteile zerlegen: Tabellen, Spalten und Zeilen.

  • Tabellen: Die Grundlage der Datenbank. Eine Tabelle funktioniert wie eine Tabellenkalkulation, in der wir zusammengehörige Daten gruppieren. Im Falle von Protokollen könnten wir eine Tabelle mit dem Namen „logs“ haben, die ausschließlich für die Speicherung dieser Datensätze bestimmt ist.

    Jede Tabelle ist für eine bestimmte Art von Daten vorgesehen, um die Organisation und den effizienten Zugriff auf Informationen zu gewährleisten.

Spalten: die Datenfelder, innerhalb einer Tabelle haben wir Spalten, die die verschiedenen Kategorien der gespeicherten Informationen darstellen. Jede Spalte ist gleichbedeutend mit einem Datenfeld und definiert einen bestimmten Informationstyp. In einer Protokolltabelle können wir zum Beispiel Spalten haben wie:

  • id → Eindeutiger Log-ID
  • timestamp → Datum und Uhrzeit der Aufzeichnung
  • level → Protokollstufe (DEBUG, INFO, ERROR...)
  • Meldung → Logmeldung
  • Quelle → Herkunft des Protokolls (welches System oder Modul den Datensatz erzeugt hat)

Jede Spalte hat eine klar definierte Aufgabe. Der Zeitstempel zum Beispiel speichert Datum und Uhrzeit, während die „message“ den Text enthält. Diese Struktur vermeidet Redundanzen und verbessert die Suchleistung.

  • Zeilen: die gespeicherten Datensätze; wenn die Spalten definieren, welche Informationen gespeichert werden, stellen die Zeilen die einzelnen Datensätze innerhalb der Tabelle dar. Jede Zeile enthält einen vollständigen Satz von Werten, die die entsprechenden Spalten ausfüllen. Siehe ein praktisches Beispiel für eine Protokolltabelle:

    ID der Zeitstempel Stufe Nachricht Herkunft
    1
     2025-02-12 10:15 DEBUG RSI-Indikatorwert berechnet: 72.56
    Indikatoren
    2  2025-02-12 10:16 INFO Kaufauftrag erfolgreich gesendet
    Auftragsverwaltung
    3  2025-02-12 10:17 ALERT Stop Loss auf Break-even-Niveau angepasst
    Risikomanagement
    4  2025-02-12 10:18 ERROR Verkaufsauftrag konnte nicht gesendet werden
    Auftragsverwaltung
    5  2025-02-12 10:19 FATAL EA konnte nicht initialisiert werden: Ungültige Einstellungen
    Initialisierung

    Jede Zeile ist ein einzelner Datensatz, der ein bestimmtes Ereignis beschreibt.

Nachdem wir nun die Struktur der Datenbank verstanden haben, können wir untersuchen, wie sie in der Praxis innerhalb von MQL5 angewendet werden kann, um Protokolle effizient zu speichern und abzufragen. Schauen wir uns das in Aktion an.


Datenbanken in MQL5

MQL5 ermöglicht es uns, Daten strukturiert zu speichern und abzurufen, aber die Datenbankunterstützung weist einige Besonderheiten auf, die wir verstehen müssen, bevor wir zur Implementierung übergehen.

Im Gegensatz zu Sprachen, die auf Web- oder Unternehmensanwendungen ausgerichtet sind, bietet MQL5 keine native Unterstützung für robuste, relationale Datenbanken wie MySQL oder PostgreSQL. Aber das bedeutet nicht, dass wir in den Textdateien feststecken! Wir können diese Einschränkung auf zwei Arten umgehen:

Verwendung von SQLite, einer leichtgewichtigen, dateibasierten Datenbank, die in MQL5 nativ unterstützt wird (siehe Datenbankfunktionen in MQL5), oder Herstellung externer Verbindungen über APIs, die die Integration mit leistungsfähigeren Datenbanken ermöglichen. Für unseren Zweck, Protokolle effizient zu speichern und abzufragen, ist SQLite die ideale Wahl. Es ist einfach, schnell und erfordert keinen dedizierten Server, was es perfekt für unsere Bedürfnisse macht. Bevor wir zur Implementierung übergehen, sollten wir die Merkmale einer Datenbank auf der Grundlage einer .sqlite-Datei besser verstehen.

  • Vorteile
    • Kein Server erforderlich: SQLite ist eine „embedded“ Datenbank, d.h. sie erfordert keine Installation oder Konfiguration eines Servers.
    • Sofort einsatzbereit: Wir erstellen einfach die .sqlite-Datei und beginnen mit dem Speichern von Daten.
    • Schnelles Lesen: Da die Daten in einer einzigen Datei gespeichert werden, kann SQLite beim Lesen kleiner bis mittlerer Daten extrem schnell sein.
    • Geringe Latenzzeit: Bei einfachen Abfragen kann sie schneller sein als herkömmliche relationale Datenbanken.
    • Hohe Kompatibilität: Kompatibel mit verschiedenen Programmiersprachen
  • Nachteile
    • Risiko der Dateibeschädigung: Wenn die Datei beschädigt ist, kann die Wiederherstellung der Daten schwierig sein.
    • Manuelle Sicherung: Die Sicherung muss durch Kopieren der .sqlite-Datei erfolgen, da SQLite keine native Unterstützung für die automatische Replikation bietet.
    • Ist nicht gut skalierbar: Bei großen Datenmengen und gleichzeitigen Zugriffen ist SQLite nicht die beste Wahl. Da unser Ziel jedoch darin besteht, die Protokolle lokal zu speichern, ist dies kein Problem.

Nachdem wir nun die Möglichkeiten und Grenzen von Datenbanken in MQL5 kennen, können wir uns mit den grundlegenden Operationen befassen, die wir zum effektiven Speichern und Abrufen von Protokollen implementieren müssen.


Die Grundlagen der Datenbankoperationen, die wir benötigen

Bevor wir unseren Handler implementieren, müssen wir die grundlegenden Operationen verstehen, die wir in der Datenbank verwenden werden. Zu diesen Vorgängen gehören das Erstellen von Tabellen, das Einfügen neuer Datensätze, das Abrufen von Daten und gegebenenfalls das Löschen oder Aktualisieren von Protokollen, falls erforderlich.

In einem Protokollierungskontext müssen wir in der Regel Informationen wie Datum und Uhrzeit, Protokollstufe, Meldung und möglicherweise den Namen der Datei oder Komponente, die den Eintrag erzeugt hat, speichern. Zu diesem Zweck müssen wir unsere Tabelle so strukturieren, dass schnelle und effiziente Abfragen möglich sind.

Erstellen wir zunächst einen einfachen Test-Expert Advisor (EA) namens DatabaseTest.mq5 im Ordner Experts/Logify. Mit der erstellten Datei erhalten wir ein ähnliches Bild wie dieses:

//+------------------------------------------------------------------+
//|                                                 DatabaseTest.mq5 |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/de/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/de/users/joaopedrodev"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Erstellen und Verbinden mit der Datenbank

Der erste Schritt besteht darin, eine Verbindung zur Datenbank zu erstellen und herzustellen. Zu diesem Zweck verwenden wir die Funktion DatabaseOpen(), die zwei Parameter benötigt:

  • Dateiname : Name der Datenbankdatei, bezogen auf den Ordner „MQL5\\\\Files“.
  • Flags : Kombination von Flags aus der Enumeration ENUM_DATABASE_OPEN_FLAGS. Diese Flags bestimmen, wie der Zugriff auf die Datenbank erfolgt. Die verfügbaren Flags sind:
    • DATABASE_OPEN_READONLY - Nur-Lese-Zugriff.
    • DATABASE_OPEN_READWRITE - Erlaubt Lesen und Schreiben.
    • DATABASE_OPEN_CREATE - Erzeugt die Datenbank auf der Festplatte, wenn sie nicht existiert.
    • DATABASE_OPEN_MEMORY - Erzeugt eine temporäre Datenbank im Speicher.
    • DATABASE_OPEN_COMMON - Die Datei wird in dem für alle Terminals gemeinsamen Ordner gespeichert.

Für unser Beispiel werden wir DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE verwenden. Auf diese Weise stellen wir sicher, dass die Datenbank automatisch erstellt wird, wenn sie noch nicht existiert, und vermeiden manuelle Überprüfungen.

Die Funktion DatabaseOpen() gibt ein Datenbank-Handle zurück, das wir in einer Variablen speichern, um es bei zukünftigen Operationen zu verwenden. Darüber hinaus ist es wichtig, die Verbindung am Ende der Nutzung zu schließen, was wir mit der Funktion DatabaseClose() tun.

Unser Code sieht nun wie folgt aus:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Opening a database connection
   int dbHandle = DatabaseOpen(path,DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Database error (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   Print("Open database file");
   
   //--- Closing database after use
   DatabaseClose(handle_db);
   Print("Closed database file");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Nachdem wir es nun geschafft haben, die Datenbank zu öffnen und zu schließen, ist es nun an der Zeit, die gespeicherten Daten zu strukturieren. Beginnen wir mit der Erstellung unserer ersten Tabelle: Logs.


Erstellen einer Tabelle

Bevor wir jedoch damit beginnen, Tabellen ohne Kriterien zu erstellen, müssen wir prüfen, ob sie bereits existieren. Zu diesem Zweck verwenden wir die Funktion DatabaseTableExists(). Wenn die Tabelle noch nicht in der Datenbank vorhanden ist, wird sie mit einem einfachen SQL-Befehl erstellt. SQL (Structured Query Language) ist die Sprache, die zur Interaktion mit Datenbanken verwendet wird und mit der Sie Daten einfügen, abfragen, ändern oder löschen können. Stellen Sie sich SQL als eine Art „Restaurant-Menü“ für Datenbanken vor: Sie geben eine Bestellung auf (SQL-Abfrage) und erhalten genau das, was Sie wollten - vorausgesetzt natürlich, die Bestellung wurde gut formuliert!

Nun werden wir uns dies in der Praxis ansehen und unsere Protokolltabelle strukturieren und sicherstellen, dass sie immer korrekt erstellt wird.

Für unsere Zwecke brauchen wir nur einige wenige SQL-Befehle zu kennen, von denen der erste zum Erstellen einer Tabelle verwendet wird:

CREATE TABLE {table_name} ({column_name} {type_data}, …);
  • {tabelle_name}: Name der zu erstellenden Tabelle.
  • {column_name} {type_data}: Definition der Spalten, wobei {type_data} den Datentyp angibt (Text, Zahl, Datum usw.).

Jetzt werden wir die Funktion DatabaseExecute() verwenden, um den Befehl zur Erstellung der Tabelle auszuführen. Die Tabellenstruktur basiert auf der MqlLogifyModel-Struktur und enthält die folgenden Felder:

  • id: Eindeutiger Bezeichner der Zeile.
  • formated: Formatierte Nachricht.
  • levelname: Name der Protokollstufe.
  • msg: Ursprüngliche Nachricht.
  • args: Argumente der Nachricht.
  • timestamp: Datum und Uhrzeit im numerischen Format.
  • date_time: Formatiertes Datum und Uhrzeit.
  • level: Schweregrad des Protokolls.
  • origin: Log-Quelle.
  • filename: Name der Quelldatei.
  • function: Die Funktion, in der das Protokoll erstellt wurde.
  • line: Codezeile, in der das Protokoll erstellt wurde.

Unser Code sieht nun wie folgt aus:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Damit ist der Schritt der Erstellung der Datenbank und der Tabelle „logs“ abgeschlossen. Nach der Erstellung der Tabelle sollte die Datenbankdatei im Ordner „Dateien“ des Datei-Explorers erscheinen:

Wenn Sie auf die Datei klicken, die vom Metaeditor unterstützt wird, sollte sich ein ähnliches Fenster wie das folgende öffnen:

Hier haben wir eine Schnittstelle, über die wir die Datenbankdaten einsehen und verschiedene SQL-Befehle ausführen können, wie rot hervorgehoben. Wir werden diese Funktion häufig verwenden, um die Daten im Editor anzuzeigen.


Wie man Daten in die Datenbank einfügt

In SQL wird der Befehl zum Einfügen von Daten in eine Tabelle verwendet:

INSERT INTO {table_name} ({column}, ...) VALUES ({value}, ...)

Im Rahmen von MQL5 kann diese Aussage mit Hilfe spezifischer Funktionen vereinfacht werden, die den Prozess intuitiver und weniger fehleranfällig machen. Die wichtigsten Funktionen, die Sie verwenden werden, sind:

  • DatabasePrepare() - Diese Funktion erstellt einen Bezeichner für die SQL-Abfrage und bereitet sie für die spätere Ausführung vor. Sie dient als erster Schritt für die Interpretation der Anfrage durch die Datenbank.
  • DatabaseBind() - Mit dieser Funktion verknüpfen Sie reale Werte mit den Abfrageparametern. Im SQL-Befehl werden die Werte durch Platzhalter dargestellt (z. B. ?1 , ?2 usw.), die zum Zeitpunkt der Ausführung durch die angegebenen Daten ersetzt werden.
  • DatabaseRead() - Verantwortlich für die Ausführung der vorbereiteten Abfrage. Bei Befehlen, die keine Datensätze zurückgeben (z. B. INSERT), sorgt diese Funktion für die Ausführung des Befehls und den Übergang zum nächsten Datensatz, falls erforderlich.
  • DatabaseFinalize() - Nach der Verwendung ist es wichtig, die mit der Abfrage verbundenen Ressourcen freizugeben. Diese Funktion schließt die zuvor vorbereitete Abfrage ab, um Speicherlecks zu vermeiden.

Bei der Erstellung der Abfrage für die Dateneinfügung können wir Platzhalter verwenden, um anzugeben, wo die Werte später verknüpft werden sollen. Das folgende Beispiel fügt einen neuen Datensatz in die Protokolltabelle ein und folgt dabei den Spalten, die wir zuvor erstellt haben:

INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);

Beachten Sie, dass alle Felder der Tabelle aufgeführt sind, mit Ausnahme des Feldes id, das automatisch von der Datenbank generiert wird. Außerdem werden die einzufügenden Werte durch ?1 , ?2 usw. angegeben, wobei jeder Platzhalter einem Index entspricht, der später verwendet wird, um den tatsächlichen Wert über die Funktion DatabaseBind() zuzuordnen.

//--- Prepare SQL statement for inserting a log entry
string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
             "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
int sqlRequest = DatabasePrepare(dbHandle, sql);
if(sqlRequest == INVALID_HANDLE)
  {
   Print("[ERROR] Failed to prepare SQL statement for log insertion");
  }

//--- Bind values to the SQL statement
DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
DatabaseBind(sqlRequest, 10, 100);                                           // Line number
Nachdem alle Werte gebunden sind, wird die Funktion DatabaseRead() verwendet, um die vorbereitete Abfrage auszuführen. Ist die Ausführung erfolgreich, wird eine Bestätigungsmeldung gedruckt, andernfalls wird ein Fehler gemeldet:
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return(INIT_FAILED);
     }
   Print("[INFO] Database connection opened successfully");
   
   //--- Create the 'logs' table if it does not exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      DatabaseExecute(dbHandle,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID
         "formated TEXT,"     // Formatted log message
         "levelname TEXT,"    // Log level (INFO, ERROR, etc.)
         "msg TEXT,"          // Main log message
         "args TEXT,"         // Additional details
         "timestamp BIGINT,"  // Log event timestamp (Unix time)
         "date_time DATETIME,"// Human-readable date and time
         "level BIGINT,"      // Log level as an integer
         "origin TEXT,"       // Module or component name
         "filename TEXT,"     // Source file name
         "function TEXT,"     // Function where the log was recorded
         "line BIGINT);");    // Source code line number
      Print("[INFO] 'logs' table created successfully");
     }
   
   //--- Prepare SQL statement for inserting a log entry
   string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) "
                "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
   int sqlRequest = DatabasePrepare(dbHandle, sql);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement for log insertion");
     }
   
   //--- Bind values to the SQL statement
   DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message
   DatabaseBind(sqlRequest, 1, "INFO");                                        // Log level name
   DatabaseBind(sqlRequest, 2, "Buy order sent successfully");                 // Main log message
   DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1");                  // Additional details
   DatabaseBind(sqlRequest, 4, 1739471040);                                     // Unix timestamp
   DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00");                          // Readable date and time
   DatabaseBind(sqlRequest, 6, 1);                                              // Log level as integer
   DatabaseBind(sqlRequest, 7, "Order Management");                             // Module or component name
   DatabaseBind(sqlRequest, 8, "File.mq5");                                     // Source file name
   DatabaseBind(sqlRequest, 9, "OnInit");                                       // Function name
   DatabaseBind(sqlRequest, 10, 100);                                           // Line number
   
   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL insertion request failed");
     }
   else
     {
      Print("[INFO] Log entry inserted successfully");
     }
   
   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
Wenn Sie diesen Expert Advisor ausführen, werden in der Konsole folgende Meldungen angezeigt:
[INFO] Database file opened successfully
[INFO] Table 'logs' created successfully
[INFO] Log entry inserted successfully
[INFO] Database file closed successfully

Wenn Sie die Datenbank im Editor öffnen, können Sie außerdem die Protokolltabelle mit allen eingegebenen Daten sehen, wie in der folgenden Abbildung dargestellt:



Wie man Daten aus der Datenbank liest

Das Lesen von Daten aus einer Datenbank ist ein Vorgang, der dem Einfügen von Datensätzen sehr ähnlich ist, jedoch mit dem Ziel, bereits gespeicherte Informationen abzurufen. In MQL5 besteht der grundlegende Ablauf beim Lesen von Daten aus:

  1. Bereiten wir die SQL-Abfrage vor: Mit der Funktion DatabasePrepare() erstellen wir einen Bezeichner für die Abfrage, die ausgeführt werden soll.
  2. Ausführen der Abfrage: Mit dem vorbereiteten Bezeichner führt die Funktion DatabaseRead() die Abfrage aus und positioniert den Cursor auf den ersten Datensatz des Ergebnisses.
  3. Extrahieren wir die Daten: Aus dem aktuellen Datensatz erhalten wir mit Hilfe spezifischer Funktionen die Werte der einzelnen Spalten, je nach erwartetem Datentyp. Diese Funktionen umfassen:

Mit diesen Schritten haben Sie einen einfachen und effektiven Ablauf, um Informationen abzurufen und sie nach Bedarf für Ihre Anwendung zu verwenden.

Nehmen wir zum Beispiel an, wir möchten alle Datensätze aus der Tabelle der Logs abrufen. Die SQL-Abfrage für diesen Vorgang ist recht einfach:

SELECT * FROM logs

Diese Abfrage wählt alle Spalten aus allen Datensätzen der Tabelle aus. In MQL5 verwenden wir die Funktion DatabasePrepare(), um den Abfragebezeichner zu erstellen, so wie wir es beim Einfügen von Daten getan haben.

Am Ende sieht der Code wie folgt aus:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Open the database connection
   int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")");
      return INIT_FAILED;
     }
   Print("[INFO] Database connection opened successfully.");

   //--- Create the 'logs' table if it doesn't exist
   if(!DatabaseTableExists(dbHandle, "logs"))
     {
      string createTableSQL =
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"    // Auto-incrementing unique ID
         "formated TEXT,"                           // Formatted log message
         "levelname TEXT,"                          // Log level name (INFO, ERROR, etc.)
         "msg TEXT,"                                // Main log message
         "args TEXT,"                               // Additional arguments/details
         "timestamp BIGINT,"                        // Timestamp of the log event
         "date_time DATETIME,"                      // Human-readable date and time
         "level BIGINT,"                            // Log level as an integer
         "origin TEXT,"                             // Module or component name
         "filename TEXT,"                           // Source file name
         "function TEXT,"                           // Function where the log was recorded
         "line BIGINT);";                           // Line number in the source code

      DatabaseExecute(dbHandle, createTableSQL);
      Print("[INFO] 'logs' table created successfully.");
     }

   //--- Prepare SQL statement to retrieve log entries
   string sqlQuery = "SELECT * FROM logs";
   int sqlRequest = DatabasePrepare(dbHandle, sqlQuery);
   if(sqlRequest == INVALID_HANDLE)
     {
      Print("[ERROR] Failed to prepare SQL statement.");
     }

   //--- Execute the SQL statement
   if(!DatabaseRead(sqlRequest))
     {
      Print("[ERROR] SQL query execution failed.");
     }
   else
     {
      Print("[INFO] SQL query executed successfully.");

      //--- Bind SQL query results to the log data model
      MqlLogifyModel logData;
      DatabaseColumnText(sqlRequest, 1, logData.formated);
      DatabaseColumnText(sqlRequest, 2, logData.levelname);
      DatabaseColumnText(sqlRequest, 3, logData.msg);
      DatabaseColumnText(sqlRequest, 4, logData.args);
      DatabaseColumnLong(sqlRequest, 5, logData.timestamp);

      string dateTimeStr;
      DatabaseColumnText(sqlRequest, 6, dateTimeStr);
      logData.date_time = StringToTime(dateTimeStr);

      DatabaseColumnInteger(sqlRequest, 7, logData.level);
      DatabaseColumnText(sqlRequest, 8, logData.origin);
      DatabaseColumnText(sqlRequest, 9, logData.filename);
      DatabaseColumnText(sqlRequest, 10, logData.function);
      DatabaseColumnLong(sqlRequest, 11, logData.line);

      Print("[INFO] Data retrieved: Formatted = ", logData.formated, " | Level = ", logData.level, " | Origin = ", logData.origin);
     }

   //--- Close the database connection
   DatabaseClose(dbHandle);
   Print("[INFO] Database connection closed successfully.");

   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+

Okay, wenn wir den Code ausführen, erhalten wir folgendes Ergebnis

[INFO] Database file opened successfully
[INFO] SQL request successfully
[INFO] Data read! | Formated: 06:24:00 [INFO] Buy order sent successfully | Level: 1 | Origin: Order Management
[INFO] Database file closed successfully

Mit diesen grundlegenden Operationen im Hinterkopf sind wir bereit, unseren Datenbank-Handler zu konfigurieren. Bereiten wir die Umgebung vor, die für die Integration der Datenbank mit unserer Protokollierungsbibliothek erforderlich ist.


Konfigurieren des Datenbank-Handlers

Um eine Datenbank zum Speichern von Protokollen zu verwenden, müssen wir unseren Handler richtig konfigurieren. Dazu müssen die Attribute der Konfigurationsstruktur definiert werden, ähnlich wie beim Datei-Handler. Wir erstellen eine Konfigurationsstruktur mit dem Namen „MqlLogifyHandleDatabaseConfig“, kopieren diese Struktur und nehmen einige Änderungen vor:

struct MqlLogifyHandleDatabaseConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   ENUM_LOG_FILE_EXTENSION file_extension;   // File extension type
   ENUM_LOG_ROTATION_MODE rotation_mode;     // Rotation mode
   int messages_per_flush;                   // Messages before flushing
   uint codepage;                            // Encoding (e.g., UTF-8, ANSI)
   ulong max_file_size_mb;                   // Max file size in MB for rotation
   int max_file_count;                       // Max number of files before deletion
   
   //--- Default constructor
   MqlLogifyHandleDatabaseConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension
      rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation
      messages_per_flush = 100;              // Default flush threshold
      codepage = CP_UTF8;                    // Default UTF-8 encoding
      max_file_size_mb = 5;                  // Default max file size in MB
      max_file_count = 10;                   // Default max file count
     }
  };

Ich habe die Attribute wie Rotation, Dateityp, maximale Anzahl von Dateien, Kodierungsmodus und andere, die entfernt werden, rot markiert, da sie für den Datenbankkontext nicht sinnvoll sind. Mit den definierten Attributen passen wir die Methode ValidityConfig() an, sodass der Code am Ende wie folgt aussieht:

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleDatabaseConfig                            |
//+------------------------------------------------------------------+
struct MqlLogifyHandleDatabaseConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   int messages_per_flush;                   // Messages before flushing
   
   //--- Default constructor
   MqlLogifyHandleDatabaseConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      messages_per_flush = 100;              // Default flush threshold
     }
   
   //--- Destructor
   ~MqlLogifyHandleDatabaseConfig(void)
     {
     }

   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the base filename is not empty
      if(base_filename == "")
        {
         base_filename = "expert";
         error_message = "The base filename cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
   
      //--- No errors found
      return(is_valid);
     }
  };

Nachdem die Konfiguration fertig ist, können wir endlich mit der Implementierung des Handlers beginnen.


Implementierung des Datenbank-Handlers

Nachdem wir nun unsere Konfiguration strukturiert haben, können wir zum praktischen Teil übergehen: der Implementierung des Datenbank-Handlers. Ich werde jeden Teil der Implementierung detailliert beschreiben, die getroffenen Entscheidungen erläutern und sicherstellen, dass der Handler flexibel für zukünftige Verbesserungen ist.

Wir beginnen mit der Definition der Klasse CLogifyHandlerDatabase, die CLogifyHandler erweitert. Diese Klasse muss die Konfiguration des Handlers, ein Zeitsteuerungsprogramm (CIntervalWatcher) und einen Cache für Protokollmeldungen speichern. Dieser Zwischenspeicher dient dazu, ein übermäßiges Schreiben in die Datenbank zu vermeiden, indem die Nachrichten vor dem Schreiben zwischengespeichert werden.

class CLogifyHandlerDatabase : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleDatabaseConfig m_config;
   
   //--- Update utilities
   CIntervalWatcher  m_interval_watcher;
   
   //--- Cache data
   MqlLogifyModel    m_cache[];
   int               m_index_cache;
   
public:
                     CLogifyHandlerDatabase(void);
                    ~CLogifyHandlerDatabase(void);
   
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleDatabaseConfig &config);
   MqlLogifyHandleDatabaseConfig GetConfig(void);
   
   virtual void      Emit(MqlLogifyModel &data);         // Processes a log message and sends it to the specified destination
   virtual void      Flush(void);                        // Clears or completes any pending operations
   virtual void      Close(void);                        // Closes the handler and releases any resources
  };

Der Konstruktor initialisiert die Attribute und stellt sicher, dass der Name des Handlers „database“ lautet, legt ein Intervall für den m_interval_watcher fest und leert den Cache. Im Destruktor rufen wir Close() auf, um sicherzustellen, dass alle anstehenden Protokolle geschrieben werden, bevor das Objekt abgeschlossen wird.

Eine weitere wichtige Methode ist SetConfig() , die es Ihnen ermöglicht, den Handler zu konfigurieren, die Konfiguration zu speichern und sie zu validieren, um sicherzustellen, dass keine Fehler vorliegen. Die Methode GetConfig() gibt einfach die aktuelle Konfiguration zurück.

CLogifyHandlerDatabase::CLogifyHandlerDatabase(void)
  {
   m_name = "database";
   m_interval_watcher.SetInterval(PERIOD_D1);
   ArrayFree(m_cache);
   m_index_cache = 0;
  }
CLogifyHandlerDatabase::~CLogifyHandlerDatabase(void)
  {
   this.Close();
  }
void CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &config)
  {
   m_config = config;
   
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(void)
  {
   return(m_config);
  }

Kommen wir nun zum Kernstück des Datenbank-Handlers, der direkten Speicherung der Protokollsätze. Zu diesem Zweck werden wir die drei grundlegenden Methoden eines jeden Handlers implementieren:

  • Emit(MqlLogifyModel &data): Verarbeitet eine Logmeldung und sendet sie an den Cache.
  • Flush(): Beendet oder löscht alle Vorgänge, indem es Informationen zum richtigen Ziel (Datei, Konsole, Datenbank usw.) hinzufügt.
  • Close(): Schließt das Handle und gibt alle zugehörigen Ressourcen frei.

Beginnend mit der Methode Emit(), die für das Hinzufügen der Daten zum Cache verantwortlich ist, und wenn die festgelegte Grenze erreicht ist, ruft sie Flush() auf.

//+------------------------------------------------------------------+
//| Processes a log message and sends it to the specified destination|
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Resize cache if necessary
      int size = ArraySize(m_cache);
      if(size != m_config.messages_per_flush)
        {
         ArrayResize(m_cache, m_config.messages_per_flush);
         size = m_config.messages_per_flush;
        }
      
      //--- Add log to cache
      m_cache[m_index_cache++] = data;
      
      //--- Flush if cache limit is reached or update condition is met
      if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect())
        {
         //--- Save cache
         Flush();
         
         //--- Reset cache
         m_index_cache = 0;
         for(int i=0;i<size;i++)
           {
            m_cache[i].Reset();
           }
        }
     }
  }
//+------------------------------------------------------------------+

Fahren wir mit der Methode Flush() fort, lesen die Daten aus dem Cache und fügen sie der Datenbank hinzu, wobei der gleichen Struktur folgen, der zu Beginn des Artikels in der Sitzung „Wie man Daten in die Datenbank einfügt“ unter Verwendung der Funktion DatabasePrepare() erläutert habe.

//+------------------------------------------------------------------+
//| Clears or completes any pending operations                       |
//+------------------------------------------------------------------+
void CLogifyHandlerDatabase::Flush(void)
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
      return;
     }
   
   if(!DatabaseTableExists(handle_db,"logs"))
     {
      DatabaseExecute(handle_db,
         "CREATE TABLE logs ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT,"
         "formated TEXT,"
         "levelname TEXT,"
         "msg TEXT,"
         "args TEXT,"
         "timestamp BIGINT,"
         "date_time DATETIME,"
         "level BIGINT,"
         "origin TEXT,"
         "filename TEXT,"
         "function TEXT,"
         "line BIGINT);");
     }
   
   //--- 
   string sql="INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; // parâmetro de consulta
   int request = DatabasePrepare(handle_db,sql);
   if(request == INVALID_HANDLE)
     {
      Print("Erro");
     }
   
   //--- Loop through all cached messages
   int size = ArraySize(m_cache);
   for(int i=0;i<size;i++)
     {
      if(m_cache[i].timestamp > 0)
        {
         DatabaseBind(request,0,m_cache[i].formated);
         DatabaseBind(request,1,m_cache[i].levelname);
         DatabaseBind(request,2,m_cache[i].msg);
         DatabaseBind(request,3,m_cache[i].args);
         DatabaseBind(request,4,m_cache[i].timestamp);
         DatabaseBind(request,5,TimeToString(m_cache[i].date_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS));
         DatabaseBind(request,6,(int)m_cache[i].level);
         DatabaseBind(request,7,m_cache[i].origin);
         DatabaseBind(request,8,m_cache[i].filename);
         DatabaseBind(request,9,m_cache[i].function);
         DatabaseBind(request,10,m_cache[i].line);
         DatabaseRead(request);
         DatabaseReset(request);
        }
     }
   
   //--- 
   DatabaseFinalize(request);
   
   //--- Close database
   DatabaseClose(handle_db);
  }
//+------------------------------------------------------------------+

Abschließend sorgt Close() dafür, dass alle anstehenden Protokolle geschrieben werden, bevor das Programm beendet wird.

void CLogifyHandlerDatabase::Close(void)
  {
   Flush();
  }

Damit haben wir einen robusten Handler implementiert, der sicherstellt, dass die Protokolle effizient und ohne Datenverlust gespeichert werden. Im nächsten Schritt müssen wir nun, da unser Datenbank-Handler zur Aufzeichnung von Protokollen bereit ist, effiziente Methoden zur Abfrage dieser Datensätze erstellen. Die Idee ist, eine generische Basismethode namens Query() zu haben, die einen SQL-Befehl im String-Format erhält und die Daten in einem Array vom Typ MqlLogifyModel zurückgibt. Daraus können wir spezifische Methoden entwickeln, um wiederkehrende Abfragen zu erleichtern. Unsere Methode Query() ist für das Öffnen der Datenbank, das Ausführen der Abfrage und das Speichern der Ergebnisse in der Protokollstruktur zuständig (siehe Implementierung unten):

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- Get the full path of the file
   string path = m_config.directory+"\\"+m_config.base_filename+".sqlite";
   
   //--- Open database
   ResetLastError();
   int handle_db = DatabaseOpen(path,DATABASE_OPEN_READWRITE);
   if(handle_db == INVALID_HANDLE)
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
      return(false);
     }
   
   //--- Prepare the SQL query
   int request = DatabasePrepare(handle_db,query);
   if(request == INVALID_HANDLE)
     {
      Print("Erro query");
      return(false);
     }
   
   //--- Clears array before inserting new data
   ArrayFree(data);
   
   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      DatabaseColumnLong(request,5,data[size].timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      DatabaseColumnInteger(request,7,data[size].level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      DatabaseColumnLong(request,11,data[size].line);
     }
   
   //--- Ends the query and closes the database
   DatabaseFinalize(handle_db);
   DatabaseClose(handle_db);
   return(true);
  }
//+------------------------------------------------------------------+

Diese Methode gibt uns die volle Flexibilität, jede beliebige SQL-Abfrage in der Protokolldatenbank durchzuführen. Um die Verwendung zu erleichtern, werden wir jedoch Hilfsmethoden erstellen, die allgemeine Abfragen kapseln.

Damit die Entwickler nicht jedes Mal SQL schreiben müssen, wenn sie Protokolle abfragen wollen, habe ich Methoden entwickelt, die bereits die am häufigsten verwendeten SQL-Befehle enthalten. Sie dienen als Abkürzungen zum Durchsuchen von Protokollen, indem sie nach Schweregrad, Datum, Quelle, Meldung, Argumenten, Dateinamen und Funktionsnamen gefiltert werden. Nachfolgend finden Sie die SQL-Befehle, die jedem dieser Filter entsprechen:

SELECT * FROM 'logs' WHERE level=1;
SELECT * FROM 'logs' WHERE timestamp BETWEEN '{start_time}' AND '{stop_time}';
SELECT * FROM 'logs' WHERE origin LIKE '%{origin}%';
SELECT * FROM 'logs' WHERE msg LIKE '%{msg}%';
SELECT * FROM 'logs' WHERE args LIKE '%{args}%';
SELECT * FROM 'logs' WHERE filename LIKE '%{filename}%';
SELECT * FROM 'logs' WHERE function LIKE '%{function}%';

Jetzt implementieren wir die spezifischen Methoden, die diese Befehle verwenden:

class CLogifyHandlerDatabase : public CLogifyHandler
  {
public:
   //--- Query methods
   bool              Query(string query, MqlLogifyModel &data[]);
   bool              QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]);
   bool              QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[]);
   bool              QueryByOrigin(string origin, MqlLogifyModel &data[]);
   bool              QueryByMsg(string msg, MqlLogifyModel &data[]);
   bool              QueryByArgs(string args, MqlLogifyModel &data[]);
   bool              QueryByFile(string file, MqlLogifyModel &data[]);
   bool              QueryByFunction(string function, MqlLogifyModel &data[]);
  };
//+------------------------------------------------------------------+
//| Get logs filtering by level                                      |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE level="+IntegerToString(level)+";",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by start end stop time                        |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE timestamp BETWEEN '"+IntegerToString((ulong)start_time)+"' AND '"+IntegerToString((ulong)stop_time)+"';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by origin                                     |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByOrigin(string origin, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE origin LIKE '%"+origin+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by message                                    |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByMsg(string msg, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE msg LIKE '%"+msg+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by args                                       |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByArgs(string args, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE args LIKE '%"+args+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by file name                                  |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFile(string file, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE filename LIKE '%"+file+"%';",data));
  }
//+------------------------------------------------------------------+
//| Get logs filtering by function name                              |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::QueryByFunction(string function, MqlLogifyModel &data[])
  {
   return(this.Query("SELECT * FROM 'logs' WHERE function LIKE '%"+function+"%';",data));
  }
//+------------------------------------------------------------------+

Wir verfügen nun über eine Reihe effizienter und flexibler Methoden für den direkten Zugriff auf Protokolle in der Datenbank. Mit der Methode Query() können Sie jeden beliebigen SQL-Befehl ausführen und sogar einen komplexeren SQL-Befehl mit mehreren Filtern je nach Ihren spezifischen Anforderungen übergeben, während die Hilfsmethoden häufige Abfragen kapseln, was die Verwendung intuitiver macht und Fehler reduziert.

Nun, da unser Handler implementiert ist, ist es an der Zeit zu testen, ob alles richtig funktioniert. Lassen Sie uns die Ergebnisse visualisieren und sicherstellen, dass die Protokolle wie erwartet gespeichert und abgerufen werden.


Visualisierung des Ergebnisses

Nach der Implementierung des Handlers ist der nächste Schritt die Überprüfung, ob er wie erwartet funktioniert. Wir müssen das Einfügen von Protokollen testen, prüfen, ob die Datensätze korrekt in der Datenbank gespeichert sind, und sicherstellen, dass die Abfragen schnell und genau sind.

In den Tests verwende ich dieselbe Datei LogifyTest.mq5 und füge lediglich einige Protokollmeldungen am Anfang hinzu. Wir werden auch einige Operationen ohne eine komplexe Strategie hinzufügen, einfach eine Position eröffnen, wenn es keine offene Position gibt, und Take-Profit und Stop-Loss für die Position definieren, um den Ausstieg zu machen.

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
#include <Trade/Trade.mqh>
CLogify logify;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleDatabaseConfig m_config;
   m_config.directory = "db";
   m_config.base_filename = "logs";
   m_config.messages_per_flush = 5;
   
   //--- Handler Database
   CLogifyHandlerDatabase *handler_database = new CLogifyHandlerDatabase();
   handler_database.SetConfig(m_config);
   handler_database.SetLevel(LOG_LEVEL_DEBUG);
   handler_database.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Add handler in base class
   logify.AddHandler(handler_database);
   
   //--- Using logs
   logify.Info("Expert starting successfully", "Boot", "",__FILE__,__FUNCTION__,__LINE__);
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- No positions
   if(PositionsTotal() == 0)
     {
      double price_entry = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double volume = 1;
      if(trade.Buy(volume,_Symbol,price_entry,price_entry - 100 * _Point, price_entry + 100 * _Point,"Buy at market"))
        {
         logify.Debug("Transaction data | Price: "+DoubleToString(price_entry,_Digits)+" | Symbol: "+_Symbol+" | Volume: "+DoubleToString(volume,2), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Info("Purchase order sent successfully", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
      else
        {
         logify.Debug("Error code: "+IntegerToString(trade.ResultRetcode(),_Digits)+" | Description: "+trade.ResultRetcodeDescription(), "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
         logify.Error("Failed to send purchase order", "CTrade", "",__FILE__,__FUNCTION__,__LINE__);
        }
     }
  }
//+------------------------------------------------------------------+

Beim Testen des Strategietesters für 1 Tag auf EURUSD reichte es aus, um 909 Log-Einträge zu erzeugen. Wie von uns konfiguriert, wurden sie in der .sqlite-Datei gespeichert. Um darauf zuzugreifen, rufen Sie einfach den Terminal-Ordner auf oder drücken Sie „Strg/Cmd + Shift + D“ und der Dateibrowser erscheint. Folgen Sie dem Pfad „MQL5/Files/db/logs.sqlite“. Wenn wir die Datei in der Hand haben, können wir sie direkt im Meta-Editor öffnen, wie wir es zuvor getan haben:


Damit ist ein weiterer Schritt nach vorn in unserer Protokollbibliothek getan. Unsere Protokolle können nun effizient in einer Datenbank gespeichert und abgerufen werden, was eine bessere Skalierbarkeit und Organisation ermöglicht.


Schlussfolgerung

In diesem Artikel haben wir uns mit der Integration von Datenbanken in unsere Protokollbibliothek befasst, von den grundlegenden Konzepten bis hin zur praktischen Implementierung eines speziellen Handlers. Wir haben zunächst die Bedeutung von Datenbanken als skalierbare und strukturierte Alternative für die Speicherung von Protokollen erörtert und ihre Vorteile gegenüber herkömmlichen Textdateien hervorgehoben. Anschließend haben wir die Besonderheiten der Verwendung von Datenbanken im Zusammenhang mit MQL5 untersucht, wobei wir uns mit ihren Einschränkungen und den verfügbaren Lösungen zur Überwindung dieser Einschränkungen befasst haben.

Schließlich analysierten wir die Ergebnisse unserer Implementierung und stellten sicher, dass die Protokolle korrekt gespeichert wurden und schnell und effizient abgerufen werden konnten. Darüber hinaus haben wir Möglichkeiten erörtert, diese Protokolle einzusehen, entweder durch direkte Abfragen der Datenbank oder durch spezielle Tools zur Überwachung der Protokolle. Dieser Validierungsprozess war unerlässlich, um sicherzustellen, dass die implementierte Lösung in realen Szenarien funktionsfähig und effektiv ist.

Damit haben wir eine weitere Etappe in der Entwicklung unserer Protokollbibliothek abgeschlossen. Die Einführung von Datenbanken zur Speicherung von Protokollen hat erhebliche Vorteile mit sich gebracht und die Protokollverwaltung übersichtlicher, zugänglicher und skalierbarer gemacht. Dieser Ansatz ermöglicht es uns, große Datenmengen effizienter zu verarbeiten, und erleichtert die Analyse und Überwachung der vom System erfassten Informationen.

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

Beigefügte Dateien |
LogifyePart6p.zip (22.67 KB)
Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (V): Die Klasse AnalyticsPanel Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (V): Die Klasse AnalyticsPanel
In dieser Diskussion wird untersucht, wie man Echtzeit-Marktdaten und Handelskontoinformationen abruft, verschiedene Berechnungen durchführt und die Ergebnisse in einem nutzerdefinierten Panel anzeigt. Um dies zu erreichen, werden wir die Entwicklung der Klasse AnalyticsPanel vertiefen, die all diese Funktionen, einschließlich der Panel-Erstellung, kapselt. Dieser Aufwand ist Teil unserer kontinuierlichen Erweiterung des New Admin Panel EA, mit der wir fortschrittliche Funktionalitäten unter Verwendung modularer Designprinzipien und Best Practices für die Codeorganisation einführen.
Larry Connors‘ Strategien RSI2 Mean-Reversion im Day-Trading Larry Connors‘ Strategien RSI2 Mean-Reversion im Day-Trading
Larry Connors ist ein renommierter Händler und Autor, der vor allem für seine Arbeit im Bereich des quantitativen Handels und für Strategien wie den 2-Perioden-RSI (RSI2) bekannt ist, der dabei hilft, kurzfristig überkaufte und überverkaufte Marktbedingungen zu erkennen. In diesem Artikel werden wir zunächst die Motivation für unsere Forschung erläutern, dann drei von Connors' berühmtesten Strategien in MQL5 nachbilden und sie auf den Intraday-Handel mit dem S&P 500 Index CFD anwenden.
Vom Neuling zum Experten: Support and Resistance Strength Indicator (SRSI) Vom Neuling zum Experten: Support and Resistance Strength Indicator (SRSI)
In diesem Artikel erfahren Sie, wie Sie die MQL5-Programmierung nutzen können, um Marktniveaus zu bestimmen und zwischen schwächeren und stärkeren Kursniveaus zu unterscheiden. Wir werden einen funktionierenden Support and Resistance Strength Indicator (SRSI) entwickeln.
Automatisieren von Handelsstrategien in MQL5 (Teil 13): Aufbau eines Kopf-Schulter-Handelsalgorithmus Automatisieren von Handelsstrategien in MQL5 (Teil 13): Aufbau eines Kopf-Schulter-Handelsalgorithmus
In diesem Artikel automatisieren wir das Muster aus Kopf und Schultern in MQL5. Wir analysieren seine Architektur, implementieren einen EA, um ihn zu erkennen und zu handeln, und führen einen Backtest der Ergebnisse durch. Der Prozess offenbart einen praktischen Handelsalgorithmus, der noch verfeinert werden kann.