SQLite: Natives Arbeiten mit SQL-Datenbanken in MQL5

MetaQuotes | 21 Februar, 2020

Inhalt


Moderner, algorithmischer Handel in MetaTrader 5

MQL5 ist eine perfekte Lösung für den algorithmischen Handel, da sie sowohl in Bezug auf die Syntax als auch auf die Berechnungsgeschwindigkeit so nah wie möglich an C++ ist. Die Plattform MetaTrader 5 bietet ihren Benutzern die moderne Fachsprache zur Entwicklung von Handelsrobotern und benutzerdefinierten Indikatoren, die es ihnen ermöglichen, über einfache Handelsaufgaben hinauszugehen und analytische Systeme beliebiger Komplexität zu erstellen.

Zusätzlich zu den asynchronen Handelsfunktionen und Mathematik-Bibliotheken haben die Händler auch Zugang zu den Netzwerkfunktionen, Import von Daten in Python, paralleles Rechnen in OpenCL, native Unterstützung für .NET-Bibliotheken mit "intelligentem" Funktionsimport, Integration mit MS Visual Studio und Datenvisualisierung mit DirectX. Diese unentbehrlichen Werkzeuge im Arsenal des modernen algorithmischen Handels ermöglichen es den Benutzern derzeit, eine Vielzahl von Aufgaben zu lösen, ohne die MetaTrader 5 Handelsplattform zu verlassen.


Funktionen für die Arbeit mit Datenbanken

Die Entwicklung von Handelsstrategien ist mit dem Umgang mit großen Datenmengen verbunden. Ein Handelsalgorithmus in Form eines zuverlässigen und schnellen MQL5-Programms ist nicht mehr ausreichend. Um verlässliche Ergebnisse zu erhalten, müssen die Händler auch eine Vielzahl von Tests und Optimierungen an einer Vielzahl von Handelsinstrumenten durchführen, die Ergebnisse speichern und verarbeiten, eine Analyse durchführen und entscheiden, wohin sie als Nächstes gehen soll.

Jetzt können Sie mit Datenbanken arbeiten, indem Sie die einfache und beliebte SQLite Engine direkt in MQL5 verwenden. Die Testergebnisse auf der Website der Entwickler zeigen eine hohe Geschwindigkeit bei der Ausführung von SQL-Abfragen. Bei den meisten Aufgaben übertraf sie PostgreSQL und MySQL. Wir haben wiederum die Geschwindigkeiten dieser Testausführungen auf MQL5 und LLVM 9.0.0 verglichen und in der Tabelle dargestellt. Die Ausführungsergebnisse werden in Millisekunden angegeben — je weniger, desto besser.

Name
Beschreibung
 LLVM  
MQL5
Test 1
 1000 INSERTs
11572
8488
Test 2
 25000 INSERTs in einer Transaktion
59
60
Test 3
 25000 INSERTs in eine indizierte Tabelle
102
105
Test 4
 100 SELECTs ohne Index
142
150
Test 5
 100 SELECTs mit einem Vergleich einer Zeichenkette
391
390
Test 6
 Erstellen eines Index
43
33
Test 7
 5000 SELECTs mit einem Index
385
307
Test 8
 1000 UPDATEs ohne Index
58
54
Test 9
 25000 UPDATEs mit Index
161
165
Test 10
 25000 Text-UPDATEs mit Index
124
120
Test 11  INSERTs nach einem SELECT
84
84
Test 12
 DELETE ohne Index
25
74
Test 13
 DELETE mit Index
70
72
Test 14  Ein umfangreiches INSERT nach einem umfangreichen DELETE
62
66
Test 15  Ein umfangreiches DELETE gefolgt von vielen kleinen INSERTs
33
33
Test 16  DROP TABLE: beendet
42
40

Sie finden die Testdetails in der angehängten Datei SqLiteTest.zip. Spezifikationen des Computers, auf dem die Messungen durchgeführt wurden — Windows 10 x64, Intel Xeon E5-2690 v3 @ 2.60GHz. 

Die Ergebnisse zeigen, dass Sie sich der maximalen Leistung sicher sein können, wenn Sie mit Datenbanken in MQL5 arbeiten. Diejenigen, die noch nie mit SQL konfrontiert waren, werden sehen, dass die Sprache der strukturierten Abfragen es ihnen erlaubt, viele Aufgaben schnell und elegant zu lösen, ohne dass komplexe Schleifen und Bemusterung erforderlich sind.


Einfache Abfragen

Datenbanken speichern Informationen in Form von Tabellen, während das Empfangen/Modifizieren und Hinzufügen neuer Daten mit Hilfe von Abfragen in der Sprache SQL erfolgt. Schauen wir uns die Erstellung einer einfachen Datenbank und den Erhalt von Daten aus dieser Datenbank an.

//+------------------------------------------------------------------+
//| Script Programm Start Funktion                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string filename="company.sqlite";
//--- Erstellen oder Öffnen einer Datenbank im Verzeichnis Common des Terminals
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
... working with the database


//--- Schließen der Datenbank
   DatabaseClose(db);
  }

Das Erstellen und Schließen einer Datenbank ist ähnlich wie das Arbeiten mit Dateien. Zuerst erstellen wir ein Handle für eine Datenbank, dann prüfen wir sie und schließen sie schließlich.

Als nächstes prüfen wir das Vorhandensein einer Tabelle in der Datenbank. Wenn die Tabelle bereits vorhanden ist, endet der Versuch, die Daten aus dem obigen Beispiel einzufügen, mit einem Fehler.

//--- Wenn die Tabelle COMPANY existiert, wird sie gelöscht
   if(DatabaseTableExists(db, "COMPANY"))
     {
      //--- delete the table
      if(!DatabaseExecute(db, "DROP TABLE COMPANY"))
        {
         Print("Failed to drop table COMPANY with code ", GetLastError());
         DatabaseClose(db);
         return;
        }
     }
//--- Erstellen der Datenbank COMPANY 
   if(!DatabaseExecute(db, "CREATE TABLE COMPANY("
                       "ID INT PRIMARY KEY     NOT NULL,"
                       "NAME           TEXT    NOT NULL,"
                       "AGE            INT     NOT NULL,"
                       "ADDRESS        CHAR(50),"
                       "SALARY         REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Die Tabelle wird mit Hilfe von Abfragen erstellt und gelöscht, und das Ausführungsergebnis sollte jederzeit überprüft werden. Die Tabelle COMPANY enthält nur fünf Felder: Eentry ID, Name, Alter, Adresse und Gehalt. Das Feld entry ID ist ein Schlüssel, d.h. ein eindeutiger Index. Indizes ermöglichen eine zuverlässige Definition jedes Eintrags und können in verschiedenen Tabellen verwendet werden, um sie miteinander zu verknüpfen. Dies ist vergleichbar mit der Art und Weise, wie eine Positions-ID alle mit einer bestimmten Position verbundenen Geschäfte und Aufträge verknüpft.

Nun sollte die Tabelle mit Daten gefüllt werden. Dies geschieht mit Hilfe der INSERT-Abfrage:

//--- enter data to the table 
   if(!DatabaseExecute(db, "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
     {
      Print("DB: ", filename, " insert failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Wie wir sehen, werden der Tabelle COMPANY vier Einträge hinzugefügt. Für jeden Eintrag wird die Reihenfolge der Felder und die in diese Felder einzufügenden Werte festgelegt. Jeder Eintrag wird durch eine separate "INSERT...." Abfrage eingefügt, die zu einer einzigen Abfrage kombiniert wird. Mit anderen Worten, wir könnten jeden Eintrag durch einen separaten Aufruf von DatabaseExecute() in die Tabelle einfügen.

Da nach Abschluss der Skriptoperation die Datenbank in der Datei Firma.sqlite gespeichert wird, würden wir versuchen, beim nächsten Start des Skripts die gleichen Daten in die Tabelle COMPANY mit der gleichen ID zu schreiben. Dies würde zu einem Fehler führen. Deshalb haben wir die Tabelle zuerst gelöscht, um die Arbeit bei jedem Start des Skripts von vorn zu beginnen.

Nun wollen wir alle Einträge aus der Tabelle COMPANY abrufen, bei denen das Feld SALARY > 15000 ist. Dies geschieht mit Hilfe der Funktion DatabasePrepare(), die den Abfragetext kompiliert und das Handle dafür zur späteren Verwendung in DatabaseRead() oder DatabaseReadBind() zurückgibt.

//--- create a query and get a handle for it
   int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

Nachdem die Abfrage erfolgreich erstellt wurde, müssen wir ihre Ausführungsergebnisse erhalten. Wir werden dies mit DatabaseRead() tun, das die Abfrage beim ersten Aufruf ausführt und zum ersten Eintrag in den Ergebnissen springt. Bei jedem weiteren Aufruf liest es einfach den nächsten Eintrag, bis das Ende erreicht ist. In diesem Fall gibt sie 'false' zurück, was bedeutet, dass keine weiteren Einträge mehr vorhanden sind.

//--- print all entries with the salary greater than 15000
   int    id, age;
   string name, address;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseRead(request); i++)
     {
      //--- read the values of each field from the obtained entry
      if(DatabaseColumnInteger(request, 0, id) && DatabaseColumnText(request, 1, name) &&
         DatabaseColumnInteger(request, 2, age) && DatabaseColumnText(request, 3, address) && DatabaseColumnDouble(request, 4, salary))
         Print(i, ":  ", id, " ", name, " ", age, " ", address, " ", salary);
      else
        {
         Print(i, ": DatabaseRead() failed with code ", GetLastError());
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
        }
     }
//--- remove the query after use
   DatabaseFinalize(request);

Das Ergebnis der Ausführung ist wie folgt:

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0

Den vollständigen Beispielcode finden Sie in der Datei DatabaseRead.mq5.

Debuggen von SQL-Abfragen in MetaEditor

Alle Funktionen für die Arbeit mit der Datenbank geben im Falle eines erfolglosen Codes einen Fehlercode zurück. Die Arbeit mit ihnen sollte keine Probleme verursachen, wenn Sie vier einfache Regeln befolgen:

  1. alle Abfrage-Handles sollten nach der Benutzung durch DatabaseFinalize() zerstört werden;
  2. die Datenbank sollte vor der Fertigstellung mit DatabaseClose() geschlossen werden;
  3. die Ergebnisse der Abfrageausführung sollten überprüft werden;
  4. im Falle eines Fehlers wird eine Abfrage zuerst zerstört, während die Datenbank danach geschlossen wird.

Am schwierigsten ist es, den Fehler zu verstehen, wenn die Abfrage nicht erstellt wurde. Der MetaEditor ermöglicht das Öffnen von *.sqlite-Dateien und die Arbeit mit ihnen unter Verwendung von SQL-Abfragen. Sehen wir uns an, wie dies am Beispiel der Datei company.sqlite geschieht:

1. Öffnen Sie die Datei company.sqlite im gemeinsamen Terminal-Ordner.

2. Nach dem Öffnen der Datenbank können wir die Tabelle COMPANY im Navigator sehen. Klicken Sie doppelt darauf.

3. Die Abfrage "SELECT * FROM COMPANY" wird automatisch in der Statusleiste erstellt.

4. Die Abfrage wird automatisch ausgeführt. Sie kann auch durch Drücken von F9 oder durch Klicken auf Ausführen ausgeführt werden.

5. Das Ergebnis der Abfrageausführung sehen.

6. Wenn etwas nicht stimmt, werden die Fehler im Journal des Editors angezeigt.


SQL-Abfragen ermöglichen es, Statistiken über Tabellenfelder zu erhalten, z.B. die Summe oder den Durchschnitt. Lassen Sie uns die Abfragen machen und prüfen, ob sie funktionieren.

Arbeiten mit Abfragen im MetaEditor

Jetzt können wir diese Abfragen im MQL5-Code implementieren:

   Print("Some statistics:");
//--- prepare a new query about the sum of salaries
   request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double total_salary;
      DatabaseColumnDouble(request, 0, total_salary);
      Print("Total salary=", total_salary);
     }
//--- remove the query after use
   DatabaseFinalize(request);
 
//--- prepare a new query about the average salary
   request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      ResetLastError();
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double aver_salary;
      DatabaseColumnDouble(request, 0, aver_salary);
      Print("Average salary=", aver_salary);
     }
//--- remove the query after use
   DatabaseFinalize(request);

Vergleich wir die Ausführungsergebnisse:

Some statistics:
Total salary=125000.0
Average salary=31250.0


Automatisches Einlesen von Abfrageergebnissen in die Struktur mittels DatabaseReadBind()

Die Funktion DatabaseRead() ermöglicht es, alle Einträge der Abfrageergebnisse durchzugehen und vollständige Daten zu jeder Spalte in der resultierenden Tabelle zu erhalten:

Diese Funktionen ermöglichen es, mit beliebigen Abfrageergebnissen in einheitlicher Weise zu arbeiten. Dieser Vorteil wird jedoch durch einen übermäßigen Code erkauft. Wenn die Struktur der Abfrageergebnisse im Voraus bekannt ist, ist es besser, die Funktion DatabaseReadBind() zu verwenden, die es Ihnen ermöglicht, sofort den gesamten Eintrag in die Struktur zu lesen. Wir können das vorherige Beispiel wie folgt wiederholen — zuerst deklarieren wir die Personenstruktur:

struct Person
  {
   int               id;
   string            name;
   int               age;
   string            address;
   double            salary;
  };

Als Nächstes wird jeder Eintrag mit DatabaseReadBind(Anfrage, Person) aus den Abfrageergebnissen gelesen:

//--- display obtained query results
   Person person;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseReadBind(request, person); i++)
      Print(i, ":  ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary);
//--- remove the query after use
   DatabaseFinalize(request);

Dadurch können wir die Werte aller Felder aus dem aktuellen Eintrag sofort erhalten, ohne sie einzeln lesen zu müssen.


Beschleunigung von Transaktionen durch Einhüllen in DatabaseTransactionBegin()/DatabaseTransactionCommit() 

Beim Arbeiten mit einer Tabelle kann es notwendig sein, die Befehle INSERT, UPDATE oder DELETE massenhaft zu verwenden. Die beste Art, dies zu tun, ist die Verwendung von Transaktionen. Bei der Durchführung von Transaktionen wird die Datenbank zunächst gesperrt (DatenbankTransaktionBegin). Dann werden die Massenänderungsbefehle ausgeführt und gespeichert (DatabaseTransactionCommit) oder im Fehlerfall abgebrochen (DatabaseTransactionRollback).

Die Funktionsbeschreibung von DatabasePrepare enthält ein Beispiel für die Verwendung von Transaktionen:

//--- auxiliary variables
   ulong    deal_ticket;         // deal ticket
   long     order_ticket;        // a ticket of an order a deal was executed by
   long     position_ticket;     // ID of a position a deal belongs to
   datetime time;                // deal execution time
   long     type ;               // deal type
   long     entry ;              // deal direction
   string   symbol;              // a symbol a deal was executed for
   double   volume;              // operation volume
   double   price;               // price
   double   profit;              // financial result
   double   swap;                // swap
   double   commission;          // commission
   long     magic;               // Magic number (Expert Advisor ID)
   long     reason;              // deal execution reason or source
//--- go through all deals and add them to the database
   bool failed=false;
   int deals=HistoryDealsTotal();
// --- lock the database before executing transactions
   DatabaseTransactionBegin(database);
   for(int i=0; i<deals; i++)
     {
      deal_ticket=    HistoryDealGetTicket(i);
      order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
      position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
      time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
      type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
      entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
      symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
      volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
      price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
      profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
      swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
      commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
      magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
      reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
      //--- add each deal to the table using the following query
      string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
      if(!DatabaseExecute(database, request_text))
        {
         PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
         PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
         failed=true;
         break;
        }
     }
//--- check for transaction execution errors
   if(failed)
     {
      //--- roll back all transactions and unlock the database
      DatabaseTransactionRollback(database);
      PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
      return(false);
     }
//--- all transactions have been performed successfully — record changes and unlock the database
   DatabaseTransactionCommit(database);

Transaktionen ermöglichen es, Massen von Tabellenoperationen hundertfach zu beschleunigen, wie im Beispiel DatabaseTransactionBegin gezeigt wird:

Ergebnis:
   Deals in the trading history: 2737 
   Transations WITH    DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds
   Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds
   Use of DatabaseTransactionBegin/DatabaseTransactionCommit provided acceleration by 532.8 times


Handhabung der Deals in der Handelshistorie

Die Stärke von SQL-Abfragen liegt darin, dass Sie Quelldaten einfach sortieren, auswählen und ändern können, ohne dass Sie Code schreiben müssen. Fahren wir mit der Analyse des Beispiels aus der Funktionsbeschreibung DatabasePrepare fort, das zeigt, wie man über eine einzelne Abfrage Abschlüsse aus Geschäften erhält. Ein Geschäft enthält Daten zu den Ein-/Ausgangsdaten und Preisen von Positionen sowie Informationen zu Symbolen, Richtungen und Volumen. Wenn wir einen Blick auf die Geschäftsstruktur werfen, können wir sehen, dass Einstiegs-/Ausstiegsgeschäfte durch die gemeinsame Positions-ID verknüpft sind. Wenn wir also ein einfaches Handelssystem auf einem Hedging-Konto haben, können wir zwei Geschäfte leicht zu einem einzigen Handel kombinieren. Dies geschieht mit Hilfe der folgenden Abfrage:

//--- fill in the TRADES table using an SQL query based on DEALS table data
   ulong start=GetMicrosecondCount();
   if(DatabaseTableExists(db, "DEALS"))
     {
      //--- fill in the TRADES table
      if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "   d1.time as time_in,"
                          "   d1.position_id as ticket,"
                          "   d1.type as type,"
                          "   d1.volume as volume,"
                          "   d1.symbol as symbol,"
                          "   d1.price as price_in,"
                          "   d2.time as time_out,"
                          "   d2.price as price_out,"
                          "   d1.commission+d2.commission as commission,"
                          "   d2.swap as swap,"
                          "   d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=0 AND d2.entry=1"))
        {
         Print("DB: fillng the TRADES table failed with code ", GetLastError());
         return;
        }
     }
   ulong transaction_time=GetMicrosecondCount()-start;

Hier wird die bestehende Tabelle DEALS verwendet und erstellen Datensätze aus den Deals mit derselben DEAL_POSITION_ID über INNER JOIN. Das Ergebnis der Beispielarbeit von DatabasePrepare auf dem Handelskonto:

Ergebnis:
   Deals in the trading history: 2741 
   The first 10 deals:
       [ticket] [order_ticket] [position_ticket]              [time] [type] [entry] [symbol] [volume]   [price]   [profit] [swap] [commission] [magic] [reason]
   [0] 34429573              0                 0 2019.09.05 22:39:59      2       0 ""        0.00000   0.00000 2000.00000 0.0000      0.00000       0        0
   [1] 34432127       51447238          51447238 2019.09.06 06:00:03      0       0 "USDCAD"  0.10000   1.32320    0.00000 0.0000     -0.16000     500        3
   [2] 34432128       51447239          51447239 2019.09.06 06:00:03      1       0 "USDCHF"  0.10000   0.98697    0.00000 0.0000     -0.16000     500        3
   [3] 34432450       51447565          51447565 2019.09.06 07:00:00      0       0 "EURUSD"  0.10000   1.10348    0.00000 0.0000     -0.18000     400        3
   [4] 34432456       51447571          51447571 2019.09.06 07:00:00      1       0 "AUDUSD"  0.10000   0.68203    0.00000 0.0000     -0.11000     400        3
   [5] 34432879       51448053          51448053 2019.09.06 08:00:00      1       0 "USDCHF"  0.10000   0.98701    0.00000 0.0000     -0.16000     600        3
   [6] 34432888       51448064          51448064 2019.09.06 08:00:00      0       0 "USDJPY"  0.10000 106.96200    0.00000 0.0000     -0.16000     600        3
   [7] 34435147       51450470          51450470 2019.09.06 10:30:00      1       0 "EURUSD"  0.10000   1.10399    0.00000 0.0000     -0.18000     100        3
   [8] 34435152       51450476          51450476 2019.09.06 10:30:00      0       0 "GBPUSD"  0.10000   1.23038    0.00000 0.0000     -0.20000     100        3
   [9] 34435154       51450479          51450479 2019.09.06 10:30:00      1       0 "EURJPY"  0.10000 118.12000    0.00000 0.0000     -0.18000     200        3
 
   The first 10 trades:
                 [time_in] [ticket] [type] [volume] [symbol] [price_in]          [time_out] [price_out] [commission]   [swap]  [profit]
   [0] 2019.09.06 06:00:03 51447238      0  0.10000 "USDCAD"    1.32320 2019.09.06 18:00:00     1.31761     -0.32000  0.00000 -42.43000
   [1] 2019.09.06 06:00:03 51447239      1  0.10000 "USDCHF"    0.98697 2019.09.06 18:00:00     0.98641     -0.32000  0.00000   5.68000
   [2] 2019.09.06 07:00:00 51447565      0  0.10000 "EURUSD"    1.10348 2019.09.09 03:30:00     1.10217     -0.36000 -1.31000 -13.10000
   [3] 2019.09.06 07:00:00 51447571      1  0.10000 "AUDUSD"    0.68203 2019.09.09 03:30:00     0.68419     -0.22000  0.03000 -21.60000
   [4] 2019.09.06 08:00:00 51448053      1  0.10000 "USDCHF"    0.98701 2019.09.06 18:00:01     0.98640     -0.32000  0.00000   6.18000
   [5] 2019.09.06 08:00:00 51448064      0  0.10000 "USDJPY"  106.96200 2019.09.06 18:00:01   106.77000     -0.32000  0.00000 -17.98000
   [6] 2019.09.06 10:30:00 51450470      1  0.10000 "EURUSD"    1.10399 2019.09.06 14:30:00     1.10242     -0.36000  0.00000  15.70000
   [7] 2019.09.06 10:30:00 51450476      0  0.10000 "GBPUSD"    1.23038 2019.09.06 14:30:00     1.23040     -0.40000  0.00000   0.20000
   [8] 2019.09.06 10:30:00 51450479      1  0.10000 "EURJPY"  118.12000 2019.09.06 14:30:00   117.94100     -0.36000  0.00000  16.73000
   [9] 2019.09.06 10:30:00 51450480      0  0.10000 "GBPJPY"  131.65300 2019.09.06 14:30:01   131.62500     -0.40000  0.00000  -2.62000
   Das Füllen der Tabelle TRADES dauerte 12,51 Millisekunden

Starten Sie dieses Skript auf Ihrem Hedging-Konto und vergleichen Sie die Ergebnisse mit den Positionen in der Historie. Es kann sein, dass Sie bisher nicht genug Wissen oder Zeit hatten, um die Schleifen zu programmieren, um ein solches Ergebnis zu erhalten. Jetzt können Sie dies mit einer einzigen SQL-Abfrage tun. Sie können das Ergebnis der Skriptoperation im MetaEditor ansehen. Öffnen Sie dazu die angehängte Datei trades.sqlite.


Portfolio-Analyse nach Strategien

Die Ergebnisse der oben gezeigten Skriptoperation DatabasePrepare machen deutlich, dass der Handel auf mehrere Währungspaare ausgerichtet ist. Außerdem zeigt die Spalte [magic] die Werte von 100 bis 600. Das bedeutet, dass das Handelskonto von mehreren Strategien verwaltet wird, wobei jede dieser Strategien ihre eigene Magicnummer zur Identifizierung der Geschäfte hat.

Eine SQL-Abfrage erlaubt es uns, den Handel im Kontext von magic Werten zu analysieren:

//--- get trading statistics for Expert Advisors by Magic Number
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT MAGIC,"
                           "   sum(case when entry =1 then 1 else 0 end) as trades,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(swap) as total_swap,"
                           "   sum(commission) as total_commission,"
                           "   sum(profit) as total_profit,"
                           "   sum(profit+swap+commission) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM DEALS "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY MAGIC"
                           "   ) as r");

Ergebnis:

Trade statistics by Magic Number
    [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0]     100      242     2584.80000  -2110.00000          -33.36000    -93.53000      474.80000    347.91000          143            99           1.96198      59.09091       40.90909         18.07552      -21.31313         1.22502
[1]     200      254     3021.92000  -2834.50000          -29.45000    -98.22000      187.42000     59.75000          140           114           0.73787      55.11811       44.88189         21.58514      -24.86404         1.06612
[2]     300      250     2489.08000  -2381.57000          -34.37000    -96.58000      107.51000    -23.44000          134           116           0.43004      53.60000       46.40000         18.57522      -20.53078         1.04514
[3]     400      224     1272.50000  -1283.00000          -24.43000    -64.80000      -10.50000    -99.73000          131            93          -0.04687      58.48214       41.51786          9.71374      -13.79570         0.99182
[4]     500      198     1141.23000  -1051.91000          -27.66000    -63.36000       89.32000     -1.70000          116            82           0.45111      58.58586       41.41414          9.83819      -12.82817         1.08491
[5]     600      214     1317.10000  -1396.03000          -34.12000    -68.48000      -78.93000   -181.53000          116            98          -0.36883      54.20561       45.79439         11.35431      -14.24520         0.94346

4 von 6 Strategien haben sich als profitabel erwiesen. Wir haben für jede Strategie folgende statistische Werte erhalten:

Die Statistiken zur Berechnung von Gewinn und Verlust berücksichtigen nicht die auf der Position aufgelaufenen Swaps und Provisionen. Dadurch können Sie die Nettokosten sehen. Es kann sich herausstellen, dass eine Strategie zwar einen kleinen Gewinn abwirft, aber aufgrund von Swaps und Provisionen im Allgemeinen unrentabel ist.


Analysieren der Deals nach Symbolen

Wir sind in der Lage, den Handel nach Symbolen zu analysieren. Machen Sie dazu die folgende Abfrage:

Machen Sie dazu die folgende Abfrage:
   int request=DatabasePrepare(db, "SELECT r.*,"
                               "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                               "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                               "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                               "   r.gross_profit/r.win_trades as average_profit,"
                               "   r.gross_loss/r.loss_trades as average_loss,"
                               "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                               "FROM "
                               "   ("
                               "   SELECT SYMBOL,"
                               "   sum(case when entry =1 then 1 else 0 end) as trades,"
                               "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                               "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                               "   sum(swap) as total_swap,"
                               "   sum(commission) as total_commission,"
                               "   sum(profit) as total_profit,"
                               "   sum(profit+swap+commission) as net_profit,"
                               "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                               "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                               "   FROM DEALS "
                               "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                               "   GROUP BY SYMBOL"
                               "   ) as r");

Ergebnis:

Trade statistics by Symbol
      [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0] "AUDUSD"      112      503.20000   -568.00000           -8.83000    -24.64000      -64.80000    -98.27000           70            42          -0.57857      62.50000       37.50000          7.18857      -13.52381         0.88592
[1] "EURCHF"      125      607.71000   -956.85000          -11.77000    -45.02000     -349.14000   -405.93000           54            71          -2.79312      43.20000       56.80000         11.25389      -13.47676         0.63512
[2] "EURJPY"      127     1078.49000  -1057.83000          -10.61000    -45.76000       20.66000    -35.71000           64            63           0.16268      50.39370       49.60630         16.85141      -16.79095         1.01953
[3] "EURUSD"      233     1685.60000  -1386.80000          -41.00000    -83.76000      298.80000    174.04000          127           106           1.28240      54.50644       45.49356         13.27244      -13.08302         1.21546
[4] "GBPCHF"      125     1881.37000  -1424.72000          -22.60000    -51.56000      456.65000    382.49000           80            45           3.65320      64.00000       36.00000         23.51712      -31.66044         1.32052
[5] "GBPJPY"      127     1943.43000  -1776.67000          -18.84000    -52.46000      166.76000     95.46000           76            51           1.31307      59.84252       40.15748         25.57145      -34.83667         1.09386
[6] "GBPUSD"      121     1668.50000  -1438.20000           -7.96000    -49.93000      230.30000    172.41000           77            44           1.90331      63.63636       36.36364         21.66883      -32.68636         1.16013
[7] "USDCAD"       99      405.28000   -475.47000           -8.68000    -31.68000      -70.19000   -110.55000           51            48          -0.70899      51.51515       48.48485          7.94667       -9.90563         0.85238
[8] "USDCHF"      206     1588.32000  -1241.83000          -17.98000    -65.92000      346.49000    262.59000          131            75           1.68199      63.59223       36.40777         12.12458      -16.55773         1.27902
[9] "USDJPY"      107      464.73000   -730.64000          -35.12000    -34.24000     -265.91000   -335.27000           50            57          -2.48514      46.72897       53.27103          9.29460      -12.81825         0.63606


Die Statistik zeigt, dass der Nettogewinn bei 5 von 10 Symbolen erzielt werden konnte (net_profit>0), während der Gewinnfaktor bei 6 von 10 Symbolen positiv war (profit factor>1). Dies ist genau dann der Fall, wenn Swaps und Provisionen die Strategie auf EURJPY unrentabel machen.


Analysieren der Deals nach Eröffnungszeitpunkten

Selbst wenn der Handel mit einem einzigen Symbol und einer einzigen Strategie durchgeführt wird, kann die Analyse der Geschäfte nach Markteintrittszeiten immer noch nützlich sein. Dies wird durch die folgende SQL-Abfrage durchgeführt:

//--- get trading statistics by market entry hours
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           "   r.gross_profit/r.win_trades as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_loss,"
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT HOUR_IN,"
                           "   count() as trades,"
                           "   sum(volume) as volume,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(profit) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM TRADES "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY HOUR_IN"
                           "   ) as r");

Ergebnis:

Trade statistics by entry hour
     [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[ 0]         0       50  5.00000      336.51000   -747.47000   -410.96000           21            29          -8.21920      42.00000       58.00000         16.02429      -25.77483         0.45020
[ 1]         1       20  2.00000      102.56000    -57.20000     45.36000           12             8           2.26800      60.00000       40.00000          8.54667       -7.15000         1.79301
[ 2]         2        6  0.60000       38.55000    -14.60000     23.95000            5             1           3.99167      83.33333       16.66667          7.71000      -14.60000         2.64041
[ 3]         3       38  3.80000      173.84000   -200.15000    -26.31000           22            16          -0.69237      57.89474       42.10526          7.90182      -12.50938         0.86855
[ 4]         4       60  6.00000      361.44000   -389.40000    -27.96000           27            33          -0.46600      45.00000       55.00000         13.38667      -11.80000         0.92820
[ 5]         5       32  3.20000      157.43000   -179.89000    -22.46000           20            12          -0.70187      62.50000       37.50000          7.87150      -14.99083         0.87515
[ 6]         6       18  1.80000       95.59000   -162.33000    -66.74000           11             7          -3.70778      61.11111       38.88889          8.69000      -23.19000         0.58886
[ 7]         7       14  1.40000       38.48000   -134.30000    -95.82000            9             5          -6.84429      64.28571       35.71429          4.27556      -26.86000         0.28652
[ 8]         8       42  4.20000      368.48000   -322.30000     46.18000           24            18           1.09952      57.14286       42.85714         15.35333      -17.90556         1.14328
[ 9]         9      118 11.80000     1121.62000   -875.21000    246.41000           72            46           2.08822      61.01695       38.98305         15.57806      -19.02630         1.28154
[10]        10      206 20.60000     2280.59000  -2021.80000    258.79000          115            91           1.25626      55.82524       44.17476         19.83122      -22.21758         1.12800
[11]        11      138 13.80000     1377.02000   -994.18000    382.84000           84            54           2.77420      60.86957       39.13043         16.39310      -18.41074         1.38508
[12]        12      152 15.20000     1247.56000  -1463.80000   -216.24000           84            68          -1.42263      55.26316       44.73684         14.85190      -21.52647         0.85227
[13]        13       64  6.40000      778.27000   -516.22000    262.05000           36            28           4.09453      56.25000       43.75000         21.61861      -18.43643         1.50763
[14]        14       62  6.20000      536.93000   -427.47000    109.46000           38            24           1.76548      61.29032       38.70968         14.12974      -17.81125         1.25606
[15]        15       50  5.00000      699.92000   -413.00000    286.92000           28            22           5.73840      56.00000       44.00000         24.99714      -18.77273         1.69472
[16]        16       88  8.80000      778.55000   -514.00000    264.55000           51            37           3.00625      57.95455       42.04545         15.26569      -13.89189         1.51469
[17]        17       76  7.60000      533.92000  -1019.46000   -485.54000           44            32          -6.38868      57.89474       42.10526         12.13455      -31.85813         0.52373
[18]        18       52  5.20000      237.17000   -246.78000     -9.61000           24            28          -0.18481      46.15385       53.84615          9.88208       -8.81357         0.96106
[19]        19       52  5.20000      407.67000   -150.36000    257.31000           30            22           4.94827      57.69231       42.30769         13.58900       -6.83455         2.71129
[20]        20       18  1.80000       65.92000    -89.09000    -23.17000            9             9          -1.28722      50.00000       50.00000          7.32444       -9.89889         0.73993
[21]        21       10  1.00000       41.86000    -32.38000      9.48000            7             3           0.94800      70.00000       30.00000          5.98000      -10.79333         1.29277
[22]        22       14  1.40000       45.55000    -83.72000    -38.17000            6             8          -2.72643      42.85714       57.14286          7.59167      -10.46500         0.54408
[23]        23        2  0.20000        1.20000     -1.90000     -0.70000            1             1          -0.35000      50.00000       50.00000          1.20000       -1.90000         0.63158

Es ist klar, dass die größte Anzahl von Geschäften in einem Intervall von 9 bis einschließlich 16 Uhr durchgeführt wird. Der Handel während anderer Stunden führt zu weniger Abschlüssen und ist meist unrentabel. Den vollständigen Quellcode mit diesen drei Abfragetypen finden Sie im Beispiel für die Funktion DatabaseExecute().


Bequeme Datenausgabe an das EA-Protokoll in DatabasePrint()

In den vorherigen Beispielen mussten wir jeden Eintrag in die Struktur einlesen und die Einträge einzeln anzeigen, um die Abfrageergebnisse anzuzeigen. Es kann oft unbequem sein, eine Struktur nur zu erstellen, um die Tabelle oder die Abfrageergebniswerte zu sehen. Für solche Fälle wurde die Funktion DatabasePrint() hinzugefügt:

long  DatabasePrint(
   int     database,          // database handle received in DatabaseOpen
   string  table_or_sql,      // a table or an SQL query
   uint    flags              // combination of flags
   );

Es ermöglicht nicht nur den Ausdruck einer bestehenden Tabelle, sondern auch den Ausdruck von Abfrageausführungsergebnissen, die als Tabelle dargestellt werden können. Zeigen Sie z.B. die Werte der Tabelle DEALS mit der folgenden Abfrage an:

   DatabasePrint(db,"SELECT * from DEALS",0);

Ergebnis (die ersten 10 Tabellenzeilen werden angezeigt):

  #|       ID ORDER_ID POSITION_ID       TIME TYPE ENTRY SYMBOL VOLUME   PRICE  PROFIT  SWAP COMMISSION MAGIC REASON
---+----------------------------------------------------------------------------------------------------------------
  1| 34429573        0           0 1567723199    2     0           0.0     0.0  2000.0   0.0        0.0     0      0 
  2| 34432127 51447238    51447238 1567749603    0     0 USDCAD    0.1  1.3232     0.0   0.0      -0.16   500      3 
  3| 34432128 51447239    51447239 1567749603    1     0 USDCHF    0.1 0.98697     0.0   0.0      -0.16   500      3 
  4| 34432450 51447565    51447565 1567753200    0     0 EURUSD    0.1 1.10348     0.0   0.0      -0.18   400      3 
  5| 34432456 51447571    51447571 1567753200    1     0 AUDUSD    0.1 0.68203     0.0   0.0      -0.11   400      3 
  6| 34432879 51448053    51448053 1567756800    1     0 USDCHF    0.1 0.98701     0.0   0.0      -0.16   600      3 
  7| 34432888 51448064    51448064 1567756800    0     0 USDJPY    0.1 106.962     0.0   0.0      -0.16   600      3 
  8| 34435147 51450470    51450470 1567765800    1     0 EURUSD    0.1 1.10399     0.0   0.0      -0.18   100      3 
  9| 34435152 51450476    51450476 1567765800    0     0 GBPUSD    0.1 1.23038     0.0   0.0       -0.2   100      3 
 10| 34435154 51450479    51450479 1567765800    1     0 EURJPY    0.1  118.12     0.0   0.0      -0.18   200      3 


Datenimport/-export

Um den Datenimport/-export zu vereinfachen, wurden die Funktionen DatabaseImport() und DatabaseExport() hinzugefügt. Diese Funktionen ermöglichen die Arbeit mit CSV-Dateien und Daten innerhalb von ZIP-Archiven.

DatabaseImport() importiert Daten in eine angegebene Tabelle. Wenn keine Tabelle mit dem angegebenen Namen existiert, wird sie automatisch erstellt. Namen und Feldtypen in der erstellten Tabelle werden ebenfalls automatisch auf Grundlage der Dateidaten definiert. 

DatabaseExport() ermöglicht das Speichern der Tabelle oder der Abfrageergebnisse in der Datei. Wenn die Abfrageergebnisse exportiert werden, sollte die SQL-Abfrage mit "SELECT" oder "select" beginnen. Mit anderen Worten, die SQL-Abfrage kann den Datenbankstatus nicht ändern, andernfalls schlägt DatabaseExport() mit einer Fehlermeldung fehl.

Siehe die vollständige Beschreibung der Funktionen in der MQL5-Dokumentation.


Speichern von Optimierungsergebnissen in der Datenbank

Die Funktionen für die Arbeit mit Datenbanken können auch für die Behandlung von Optimierungsergebnissen verwendet werden. Verwenden wir den EA "MACD Sample" aus der Standardauslieferung, um die Gewinnung von Testergebnissen mit Hilfe von Frames zu veranschaulichen und die Werte aller Optimierungskriterien anschließend in einer einzigen Datei zu speichern. Dazu erstellen wir die Klasse CDatabaseFrames, in der wir die Methode OnTester() zum Senden von Handelsstatistiken definieren:

//+------------------------------------------------------------------+
//| Tester function — sends trading statistics in a frame            |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTester(const double OnTesterValue)
  {
//--- stats[] array to send data to a frame
   double stats[16];
//--- allocate separate variables for trade statistics to achieve more clarity
   int    trades=(int)TesterStatistics(STAT_TRADES);
   double win_trades_percent=0;
   if(trades>0)
      win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades;
//--- fill in the array with test results
   stats[0]=trades;                                       // number of trades
   stats[1]=win_trades_percent;                           // percentage of profitable trades
   stats[2]=TesterStatistics(STAT_PROFIT);                // net profit
   stats[3]=TesterStatistics(STAT_GROSS_PROFIT);          // gross profit
   stats[4]=TesterStatistics(STAT_GROSS_LOSS);            // gross loss
   stats[5]=TesterStatistics(STAT_SHARPE_RATIO);          // Sharpe Ratio
   stats[6]=TesterStatistics(STAT_PROFIT_FACTOR);         // profit factor
   stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR);       // recovery factor
   stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // trade mathematical expectation
   stats[9]=OnTesterValue;                                // custom optimization criterion
//--- calculate built-in standard optimization criteria
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   double balance_plus_profitfactor=0;
   if(TesterStatistics(STAT_GROSS_LOSS)!=0)
      balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR);
   double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF);
   double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT);
   double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR);
   double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO);
//--- add the values of built-in optimization criteria
   stats[10]=balance;                                     // Balance
   stats[11]=balance_plus_profitfactor;                   // Balance+ProfitFactor
   stats[12]=balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   stats[13]=balance_plus_dd;                             // Balance+EquityDrawdown
   stats[14]=balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   stats[15]=balance_plus_sharpe;                         // Balance+Sharpe
//--- create a data frame and send it to the terminal
   if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats))
      Print("Frame add error: ", GetLastError());
   else
      Print("Frame added, Ok");
  }

Die zweite wichtige Methode der Klasse ist OnTesterDeinit(). Nach der Optimierung liest sie alle erhaltenen Frames aus und speichert die Statistiken in der Datenbank:

//+------------------------------------------------------------------+
//| TesterDeinit function — read data from frames                    |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTesterDeinit(void)
  {
//--- take the EA name and optimization end time
   string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite";
   StringReplace(filename, ":", "."); // ":" character is not allowed in file names
//--- open/create the database in the common terminal folder
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
   else
      Print("DB: ", filename, " opened successful");
//--- create the PASSES table
   if(!DatabaseExecute(db, "CREATE TABLE PASSES("
                       "PASS               INT PRIMARY KEY NOT NULL,"
                       "TRADES             INT,"
                       "WIN_TRADES         INT,"
                       "PROFIT             REAL,"
                       "GROSS_PROFIT       REAL,"
                       "GROSS_LOSS         REAL,"
                       "SHARPE_RATIO       REAL,"
                       "PROFIT_FACTOR      REAL,"
                       "RECOVERY_FACTOR    REAL,"
                       "EXPECTED_PAYOFF    REAL,"
                       "ON_TESTER          REAL,"
                       "BL_BALANCE         REAL,"
                       "BL_PROFITFACTOR    REAL,"
                       "BL_EXPECTEDPAYOFF  REAL,"
                       "BL_DD              REAL,"
                       "BL_RECOVERYFACTOR  REAL,"
                       "BL_SHARPE          REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
//--- variables for reading frames
   string        name;
   ulong         pass;
   long          id;
   double        value;
   double        stats[];
//--- move the frame pointer to the beginning
   FrameFirst();
   FrameFilter("", STATS_FRAME); // select frames with trading statistics for further work
//--- variables to get statistics from the frame
   int trades;
   double win_trades_percent;
   double profit, gross_profit, gross_loss;
   double sharpe_ratio, profit_factor, recovery_factor, expected_payoff;
   double ontester_value;                              // custom optimization criterion
   double balance;                                     // Balance
   double balance_plus_profitfactor;                   // Balance+ProfitFactor
   double balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   double balance_plus_dd;                             // Balance+EquityDrawdown
   double balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   double balance_plus_sharpe;                         // Balance+Sharpe
//--- block the database for the period of bulk transactions
   DatabaseTransactionBegin(db);
//--- go through frames and read data from them
   bool failed=false;
   while(FrameNext(pass, name, id, value, stats))
     {
      Print("Got pass #", pass);
      trades=(int)stats[0];
      win_trades_percent=stats[1];
      profit=stats[2];
      gross_profit=stats[3];
      gross_loss=stats[4];
      sharpe_ratio=stats[5];
      profit_factor=stats[6];
      recovery_factor=stats[7];
      expected_payoff=stats[8];
      stats[9];
      balance=stats[10];
      balance_plus_profitfactor=stats[11];
      balance_plus_expectedpayoff=stats[12];
      balance_plus_dd=stats[13];
      balance_plus_recoveryfactor=stats[14];
      balance_plus_sharpe=stats[15];
      PrintFormat("VALUES (%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%2.f,%.2f,%.2f,%.2f,%.2f)",
                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                  balance_plus_sharpe);
      //--- write data to the table
      string request=StringFormat("INSERT INTO PASSES (PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS,"
                                  "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER,"
                                  "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) "
                                  "VALUES (%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)",
                                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                                  balance_plus_sharpe);

      //--- execute a query to add a pass to the PASSES table
      if(!DatabaseExecute(db, request))
        {
         PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError());
         failed=true;
         break;
        }
     }
//--- if an error occurred during a transaction, inform of that and complete the work
   if(failed)
     {
      Print("Transaction failed, error code=", GetLastError());
      DatabaseTransactionRollback(db);
      DatabaseClose(db);
      return;
     }
   else
     {
      DatabaseTransactionCommit(db);
      Print("Transaction done successful");
     }
//--- Schließen der Datenbank
   if(db!=INVALID_HANDLE)
     {
      Print("Close database with handle=", db);
      DatabaseClose(db);
     }

Binden Sie die Datei DatabaseFrames.mqh in den EA "MACD Sample" ein und deklarieren Sie die Klassenvariable CDatabaseFrames:

#define MACD_MAGIC 1234502
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include "DatabaseFrames.mqh"
...
CDatabaseFrames DB_Frames;

Als Nächstes fügen Sie drei Funktionen am Ende des EA hinzu, die nur während der Optimierung aufgerufen werden sollen:

//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
int OnTesterInit()
  {
   return(DB_Frames.OnTesterInit());
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   DB_Frames.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double ret=0;
   //--- create a custom optimization criterion as the ratio of a net profit to a relative balance drawdown
   if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0)
      ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
   DB_Frames.OnTester(ret);
   return(ret);
  }
//+------------------------------------------------------------------+

Starten Sie die Optimierung und holen Sie sich die Datenbankdatei mit den Handelsstatistiken im gemeinsamen Terminalordner:

CDatabaseFrames::OnTesterInit: optimization launched at 15:53:27
DB: MACD Sample Database 2020.01.20 15.53.sqlite opened successful
Transaction done successful
Close database with handle=65537
Database stored in file 'MACD Sample Database 2020.01.20 15.53.sqlite'


Die neu erstellte Datenbankdatei kann im MetaEditor geöffnet oder in einer anderen MQL5-Anwendung für die weitere Arbeit verwendet werden.

Arbeiten mit der Datenbank im MetaEditor

So können Sie beliebige Daten in der erforderlichen Form für weitere Analysen oder den Austausch mit anderen Händlern aufbereiten. Den Quellcode, die ini-Datei mit den Optimierungsparametern und das Ausführungsergebnis finden Sie im unten angehängten MACD.zip-Archiv.


Optimierung der Abfrageausführung mit Indizes

Optimierung der Abfrageausführung mit Indizes Die beste Eigenschaft von SQL (in allen seinen Implementierungen, nicht nur in SQLite) ist, dass es eine deklarative Sprache ist und keine prozedurale. Wenn Sie in SQL programmieren, sagen Sie dem System, WAS Sie berechnen wollen, nicht WIE es berechnet werden soll.

Die Aufgabe, das "Wie" herauszufinden, wird an das Abfrageplaner-Subsystem innerhalb der SQL-Datenbank-Engine delegiert. Für jede beliebige SQL-Anweisung kann es Hunderte oder Tausende von verschiedenen Algorithmen zur Durchführung der Operation geben. Alle diese Algorithmen erhalten die richtige Antwort, wobei einige schneller als andere laufen.

Der Abfrageplaner versucht, den schnellsten und effizientesten Algorithmus für jede SQL-Anweisung auszuwählen. In den meisten Fällen leistet der Abfrageplaner in SQLite gute Arbeit. Der Abfrageplaner benötigt jedoch Indizes, um sein Bestes zu erreichen. Diese Indizes sollten normalerweise von Programmierern hinzugefügt werden. In diesen Fällen möchten die Programmierer vielleicht zusätzliche Hinweise geben, um dem Abfrageplaner zu helfen, seine Arbeit besser zu machen.

Suchen ohne Indizes

Angenommen, wir haben die Tabelle DEALS, die die angegebenen 14 Felder enthält. Unten sind die ersten 10 Einträge dieser Tabelle aufgeführt.

rowid
ID ORDER_ID POSITION_ID TIME TYPE ENTRY SYMBOL VOLUME PRICE PROFIT SWAP COMMISSION MAGIC REASON
1 34429573 0 0 1567723199 2 0 0 0 2000 0 0 0 0
2 34432127 51447238 51447238 1567749603 0 0 USDCAD 0.1 1.3232 0 0 -0.16 500 3
3 34432128 51447239 51447239 1567749603 1 0 USDCHF 0.1 0.98697 0 0 -0.16 500 3
4 34432450 51447565 51447565 1567753200 0 0 EURUSD 0.1 1.10348 0 0 -0.18 400 3
5 34432456 51447571 51447571 1567753200 1 0 AUDUSD 0.1 0.68203 0 0 -0.11 400 3
6 34432879 51448053 51448053 1567756800 1 0 USDCHF 0.1 0.98701 0 0 -0.16 600 3
7 34432888 51448064 51448064 1567756800 0 0 USDJPY 0.1 106.962 0 0 -0.16 600 3
8 34435147 51450470 51450470 1567765800 1 0 EURUSD 0.1 1.10399 0 0 -0.18 100 3
9 34435152 51450476 51450476 1567765800 0 0 GBPUSD 0.1 1.23038 0 0 -0.2 100 3
10 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0 0 -0.18 200 3

Sie enthält Daten aus dem Abschnitt Deal Properties (außer DEAL_TIME_MSC, DEAL_COMMENT und DEAL_EXTERNAL_ID), die für die Analyse der Handelshistorie erforderlich sind. Abgesehen von den gespeicherten Daten weist jede Tabelle immer den ganzzahligen Schlüssel rowid gefolgt von Eingabefeldern auf. rowid Schlüsselwerte werden automatisch erzeugt und sind innerhalb der Tabelle eindeutig. Sie werden beim Hinzufügen neuer Einträge erhöht. Das Löschen von Einträgen kann zu Nummerierungslücken führen, aber die Tabellenzeilen werden immer in rowid aufsteigender Reihenfolge gespeichert.

Wenn wir Geschäfte finden müssen, die sich auf eine bestimmte Position beziehen, z.B. ID=51447571, sollten wir die folgende Abfrage schreiben:

SELECT * FROM deals WHERE position_id=51447571

In diesem Fall wird ein vollständiger Tabellenscan durchgeführt — alle Zeilen werden angezeigt und die POSITION_ID wird bei jeder Zeile auf Gleichheit mit dem Wert 51447571 geprüft. Zeilen, die diese Bedingung erfüllen, werden in den Ergebnissen der Abfrageausführung angezeigt. Wenn die Tabelle Millionen oder Zehnmillionen von Datensätzen enthält, kann die Suche sehr lange dauern. Wenn wir die Suche nach der Bedingung rowid=5 statt position_id=51447571 durchführen würden, würde sich die Suchzeit um das Tausend- oder sogar Millionenfache (je nach Tabellengröße) verringern.

SELECT * FROM deals WHERE rowid=5

Das Ergebnis der Abfrageausführung wäre dasselbe, da die Zeile mit rowid=5 die position_id=51447571 speichert. Die Beschleunigung wird dadurch erreicht, dass die Werte rowid in aufsteigender Reihenfolge sortiert werden und die binäre Suche zur Ermittlung des Ergebnisses verwendet wird. Leider ist die Suche nach rowid für uns nicht geeignet, da wir an Einträgen mit dem notwendigen position_id-Wert interessiert sind.

Suche nach Index

Um die Ausführung einer Abfrage zeiteffizienter zu gestalten, müssen wir den POSITION_ID-Feldindex mit der folgenden Abfrage hinzufügen:

 CREATE INDEX Idx1 ON deals(position_id)

In diesem Fall wird eine separate Tabelle mit zwei Spalten erzeugt. Die erste Spalte besteht aus aufsteigend sortierten POSITION_ID-Werten, während die zweite Spalte aus rowid besteht.

POSITION_ID rowid
0 1
51447238 2
51447239 3
51447565 4
51447571 5
51448053 6
51448064 7
51450470 8
51450476 9
51450479 10

Die sequenzielle Reihe von rowid könnte bereits gebrochen sein, obwohl sie in unserem Beispiel erhalten geblieben ist, da auch die POSITION_ID beim Öffnen einer Position durch die Zeit erhöht wird.

Da wir nun den POSITION_ID-Feldindex haben, wird unsere Abfrage

SELECT * FROM deals WHERE position_id=51447571

wird anders durchgeführt. Zunächst wird eine binäre Suche im Idx1-Index über die Spalte POSITION_ID durchgeführt und alle Zeileniden, die der Bedingung entsprechen, werden gefunden. Die zweite binäre Suche in der ursprünglichen Tabelle DEALS sucht nach allen Einträgen mit den bekannten Werten rowid. Somit wird nun ein einziger vollständiger Scan der großen Tabelle durch zwei aufeinanderfolgende Suchvorgänge ersetzt — zuerst durch den Index und dann durch die Tabellenzeilennummern. Dadurch kann die Ausführungszeit solcher Abfragen bei einer großen Anzahl von Tabellenzeilen um Tausende oder mehr Male reduziert werden.

Allgemeine Regel: Wenn einige der Tabellenfelder häufig zum Suchen/Vergleichen/Sortieren verwendet werden, wird empfohlen, Indizes für diese Felder zu erstellen.

Die Tabelle DEALS enthält auch die Felder SYMBOL, MAGIC (EA-ID) und ENTRY (Eröffnungsrichtung). Wenn Sie in diesen Feldern Stichproben nehmen müssen, ist es sinnvoll, die entsprechenden Indizes zu erstellen. Zum Beispiel:

CREATE INDEX Idx2 ON deals(symbol)
CREATE INDEX Idx3 ON deals(magic)
CREATE INDEX Idx4 ON deals(entry)

Denken Sie daran, dass die Erstellung von Indizes zusätzlichen Speicherplatz erfordert und jedes Hinzufügen/Löschen eines Eintrags eine Neuindizierung nach sich zieht. Sie können auch Multi-Indizes erstellen, die auf mehreren Feldern basieren. Wenn wir zum Beispiel alle Geschäfte auswählen wollen, die von dem EA mit MAGIC=500 auf USDCAD durchgeführt wurden, können wir die folgende Abfrage erstellen:

SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'

In diesem Fall können Sie einen Multi-Index für MAGIC- und SYMBOL-Felder erstellen

CREATE INDEX Idx5 ON deals(magic, symbol)

wodurch die folgende Indextabelle erstellt wird (die ersten 10 Zeilen sind schematisch dargestellt)

MAGIC SYMBOL rowid
100 EURUSD 4
100 EURUSD 10
100 EURUSD 20
100 GBPUSD 5
100 GBPUSD 11
200 EURJPY 6
200 EURJPY 12
200 EURJPY 22
200 GBPJPY 7
200 GBPJPY 13

Im neu erstellten Multi-Index werden die Einträge zunächst blockweise nach MAGIC und dann — nach SYMBOL-Feld sortiert. Bei UND-Abfragen erfolgt daher die Suche im Index zunächst über die Spalte MAGIC. Danach wird der Wert der SYMBOL-Spalte überprüft. Wenn beide Bedingungen erfüllt sind, wird rowid zu der Ergebnismenge hinzugefügt, die bei der ursprünglichen Tabellensuche verwendet werden soll. Im Allgemeinen ist ein solcher Multi-Index nicht mehr geeignet für Abfragen, bei denen zuerst SYMBOL geprüft wird.

SELECT * FROM deals WHERE  symbol='USDCAD' AND magic=500 

Obwohl der Abfrageplaner weiß, wie er in solchen Fällen richtig zu handeln hat und die Suche in der richtigen Reihenfolge durchführt, wäre es dennoch unklug zu hoffen, dass er Ihre Fehler im Tabellen- und Abfragedesign immer automatisch behebt.

OR (Oder)-Abfragen

Multi-Indizes sind nur für UND-Abfragen geeignet. Nehmen wir zum Beispiel an, dass wir alle Geschäfte finden wollen, die von der EA mit MAGIC=100 oder auf EURUSD durchgeführt wurden:

SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'

In diesem Fall werden zwei separate Suchen implementiert. Alle gefundenen Rowids werden dann für die endgültige Suche nach Zeilennummern in der Quelltabelle zu einer gemeinsamen Auswahl zusammengefasst.

SELECT * FROM deals WHERE magic=100 
SELECT * FROM deals WHERE symbol='EURUSD'

Aber selbst in diesem Fall ist es notwendig, dass beide Felder der ODER-Abfrage Indizes haben, da sonst die Suche die vollständige Tabellenprüfung verursacht.

Sortieren

Um die Sortierung zu beschleunigen, wird auch ein Index nach den Feldern empfohlen, die zur Anordnung der Abfrageergebnisse verwendet werden. Nehmen wir beispielsweise an, dass wir alle Geschäfte auf EURUSD nach Geschäftszeit sortiert auswählen müssen:

SELECT * FROM deals symbol='EURUSD' ORDER BY time

In diesem Fall sollten Sie die Erstellung eines Index nach dem Feld TIME in Betracht ziehen. Die Notwendigkeit von Indizes hängt von der Tabellengröße ab. Wenn die Tabelle nur wenige Einträge hat, kann die Indizierung kaum Zeit sparen.

Hier haben wir nur die Grundlagen der Abfrageoptimierung untersucht. Zum besseren Verständnis empfehlen wir Ihnen, das Thema ab dem Abschnitt Abfrageplanung auf der Website der SQLite-Entwickler zu studieren.


Integration der Datenbank-Behandlung in MetaEditor

Die MetaTrader 5 Plattform wird ständig weiterentwickelt. Wir haben der MQL5-Sprache die native Unterstützung für SQL-Abfragen hinzugefügt und die neuen Funktionen zur Handhabung von Datenbanken in MetaEditor integriert, einschließlich der Erstellung einer Datenbank, des Einfügens und Löschens von Daten und der Durchführung von Massentransaktionen. Die Erstellung einer Datenbank ist Standard und ist Teil des MQL5-Assistenten. Geben Sie einfach Datei- und Tabellennamen an und fügen Sie alle erforderlichen Felder zur Angabe des Typs hinzu.

Erstellen einer Datenbank im MQL-Assistenten

Als Nächstes können Sie die Tabelle mit Daten füllen, eine Suche und Auswahl durchführen, SQL-Abfragen einleiten, usw. So können Sie mit Datenbanken nicht nur aus MQL5-Programmen, sondern auch manuell arbeiten. Dazu sind keine Browser von Drittanbietern erforderlich.

Die Einführung von SQLite in MetaTrader eröffnet den Händlern neue Möglichkeiten im Umgang mit großen Datenmengen sowohl programmatisch als auch manuell. Wir haben unser Bestes getan, um sicherzustellen, dass diese Funktionen am bequemsten zu benutzen sind und in Bezug auf die Geschwindigkeit mit anderen Lösungen gleichwertig sind. Studieren Sie die Sprache der SQL-Abfragen und wenden Sie sie in Ihrer Arbeit an.