SQLite: Natives Arbeiten mit SQL-Datenbanken in MQL5
Inhalt
- Moderner, algorithmischer Handel in MetaTrader 5
- Funktionen für die Arbeit mit Datenbanken
- Einfache Abfragen
- Debuggen von SQL-Abfragen in MetaEditor
- Automatisches Einlesen von Abfrageergebnissen in die Struktur mittels DatabaseReadBind()
- Beschleunigung von Transaktionen durch Einhüllen in DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Handhabung der Deals in der Handelshistorie
- Portfolio-Analyse nach Strategien
- Analysieren der Deals nach Symbolen
- Analysieren der Deals nach Eröffnungszeitpunkten
- Bequeme Datenausgabe an das EA-Protokoll in DatabasePrint()
- Datenimport/-export
- Speichern von Optimierungsergebnissen in der Datenbank
- Optimierung der Abfrageausführung mit Indizes
- Integration der Datenbank-Behandlung in MetaEditor
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.0Den 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:
- alle Abfrage-Handles sollten nach der Benutzung durch DatabaseFinalize() zerstört werden;
- die Datenbank sollte vor der Fertigstellung mit DatabaseClose() geschlossen werden;
- die Ergebnisse der Abfrageausführung sollten überprüft werden;
- 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.
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:
- DatabaseColumnName — Name,
- DatabaseColumnType — Datentyp,
- DatabaseColumnSize — Datengröße in Bytes,
- DatabaseColumnText — als Text gelesen,
- DatabaseColumnInteger — liefert den Wert vom Typ int,
- DatabaseColumnLong — liefert den Wert des langen Typs,
- DatabaseColumnDouble — liefert den
doppelten Typwert,
- DatenbankColumnBlob — Datenarray abrufen.
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:
- trades — Anzahl der Trades nach Strategie,
- gross_profit — Gesamtgewinn nach Strategie (die Summe aller positiven Gewinn-Werte),
- gross_loss — Gesamtverlust nach Strategie (die Summe aller negativen Gewinn-Werte),
- total_commission — Summe aller Provisionen nach Strategiegeschäften,
- total_swap — Summe aller Swaps nach Strategiegeschäften,
- total_profit — Summe von Brutto_Gewinn und Brutto_Verlust,
- net_profit — Summe (gross_profit + gross_loss + total_commission + total_swap),
- win_trades — Anzahl der Trades, bei denen profit>0,
- loss_trades — Anzahl der trades, bei denen profit<0,
- expected_payoff — Expected Payoff für den Handel ohne Swaps und Provisionen = net_profit/trades,
- win_percent — Prozentsatz der win_trades,
- loss_percent — Prozentsatz der loss_trades,
- average_profit — durchschnittlicher Gewinn = gross_profit / win_trades,
- average_loss — durchschnittlicher Verlust = gross_loss / loss_trades,
- profit_factor — Gewinnfaktor = gross_profit/gross_loss.
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.
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.
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.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/7463
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.