SQLite Gestion native des bases de données SQL dans MQL5
MetaQuotes | 10 octobre, 2022
Sommaire
- Trading algorithmique moderne dans MetaTrader 5
- Fonctions pour travailler avec les bases de données
- Requête simple
- Déboguer des requêtes SQL dans MetaEditor
- Lecture automatique des résultats de la requête à l'aide de la fonction DatabaseReadBind()
- Accélérer les transactions en les enveloppant dans une transaction avec DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Manipulation de l'historique des transactions
- Analyse de portefeuille par stratégies
- Analyse de transactions par symboles
- Analyse de transactions par heures d'entrée
- Sortie des données vers le journal de l'EA avec DatabasePrint()
- Import/export des données
- Sauvegarde des résultats d'optimisation dans une base de données
- Optimisation de l'exécution de requêtes à l'aide d'index
- Intégration de la gestion des bases de données dans MetaEditor
Trading algorithmique moderne dans MetaTrader 5
MQL5 est une solution parfaite pour le trading algorithmique car il est aussi proche que possible du C++ en termes de syntaxe et de vitesse de calcul. Le site MetaTrader 5 offre à ses utilisateurs un langage spécialisé moderne pour le développement de robots de trading et d'indicateurs personnalisés, leur permettant d'aller au-delà des simples tâches de trading et de créer des systèmes analytiques de toute complexité.
En plus des fonctions de trading asynchrone et des bibliothèques mathématiques, les traders ont également accès aux fonctions de réseaux, à l'import de données vers Python, au calcul parallèle dans OpenCL, au support des bibliothèques .NET avec des fonctions "intelligentes" d’import, à l’intégration avec MS Visual Studio et à la visualisation des données avec DirectX. Ces outils indispensables dans l'arsenal du trading algorithmique moderne permettent actuellement aux utilisateurs de résoudre une variété de tâches sans quitter la plateforme de trading MetaTrader 5.Fonctions pour travailler avec les bases de données
Le développement de stratégies de trading est associé à la manipulation de grandes quantités de données. Un algorithme de trading sous la forme d'un programme MQL5 fiable et rapide ne suffit plus. Pour obtenir des résultats fiables, les traders doivent également effectuer un grand nombre de tests et d’optimisations sur une variété d'instruments de trading, sauvegarder et traiter les résultats, effectuer une analyse et décider de la marche à suivre.
Vous êtes désormais en mesure de travailler avec des bases de données en utilisant le moteur simple et populaire SQLite directement dans MQL5. Les résultats des tests sur le site web des développeurs montrent une grande vitesse d'exécution des requêtes SQL. Dans la plupart des tâches, il surpasse PostgreSQL et MySQL. A notre tour, nous avons comparé les vitesses d'exécution de ces tests sur MQL5 et LLVM 9.0.0. Les résultats sont comparés dans le tableau ci-dessous. Les résultats de l'exécution sont donnés en millisecondes - plus la valeur est petite, plus le résultat est meilleur.
Nom | Description | LLVM | MQL5 |
---|---|---|---|
Test 1 | 1000 INSERT | 11572 | 8488 |
Test 2 | 25000 INSERT dans une transaction | 59 | 60 |
Test 3 | 25000 INSERT dans une table indexée | 102 | 105 |
Test 4 | 100 SELECT sans index | 142 | 150 |
Test 5 | 100 SELECT sur une comparaison de chaînes de caractères | 391 | 390 |
Test 6 | Création d'un index | 43 | 33 |
Test 7 | 5000 SELECT avec un index | 385 | 307 |
Test 8 | 1000 UPDATE sans index | 58 | 54 |
Test 9 | 25000 UPDATE avec un index | 161 | 165 |
Test 10 | 25000 UPDATE de texte avec un index | 124 | 120 |
Test 11 | INSERT à partir d'un SELECT | 84 | 84 |
Test 12 | DELETE sans index | 25 | 74 |
Test 13 | DELETE avec un index | 70 | 72 |
Test 14 | Un gros INSERT après un gros DELETE | 62 | 66 |
Test 15 | Un gros DELETE suivi de nombreux petits INSERT | 33 | 33 |
Test 16 | DROP TABLE | 42 | 40 |
Vous pourrez trouver les détails du test dans le fichier joint SqLiteTest.zip. Spécifications de l'ordinateur sur lequel les mesures ont été effectuées : Windows 10 x64, Intel Xeon E5-2690 v3 @ 2.60GHz.
Les résultats montrent que vous êtes sûrs d'obtenir des performances maximales lorsque vous travaillez avec des bases de données dans MQL5. Ceux qui n'ont jamais travaillé avec du SQL auparavant verront que le langage des requêtes structurées leur permet de résoudre de nombreuses tâches rapidement et élégamment sans avoir besoin de boucles et d'échantillonnages complexes.
Requête simple
Les bases de données stockent des informations sous forme de tables. La consultation, la modification et l'insertion de nouvelles données se font à l'aide de requêtes en langage SQL. Examinons la création d'une base de données simple et la récupération de ses données.
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string filename="company.sqlite"; //--- create or open 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; } ... working with the database //--- close the database DatabaseClose(db); }
La création et la fermeture d'une base de données sont similaires à l’utilisation des fichiers. Tout d'abord, nous créons un handle pour une base de données, nous le vérifions et, enfin, nous le fermons.
Nous vérifions ensuite la présence d'une table dans la base de données. Si la table existe déjà, la tentative d'insertion des données de l'exemple ci-dessus se terminera par une erreur.
//--- if the COMPANY table exists, delete it 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; } } //--- create the COMPANY table 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; }
La table est créée et supprimée à l'aide de requêtes. Le résultat de l'exécution doit être vérifié à tout moment. La table SOCIÉTÉ ne comporte que cinq champs : identifiant, nom, âge, adresse et salaire. Le champ ID est une clé, c'est-à-dire que c’est un index unique, qui identifie un enregistrement de façon unique. Les index permettent une définition fiable de chaque entrée et peuvent être utilisés dans différents tableaux pour les relier entre eux. De la même manière qu'un identifiant de position relie toutes les transactions et les ordres liés à une position particulière.
La table doit maintenant être remplie avec des données. Ceci est fait en utilisant une requête INSERT :
//--- 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; }
Comme nous pouvons le voir, 4 entrées sont ajoutées à la table COMPANY. La séquence des champs et les valeurs à insérer dans ces champs sont spécifiées pour chaque entrée. Chaque entrée est insérée par une requête "INSERT...." séparée, et toutes les requêtes sont combinées en une seule requête. En d'autres termes, nous pourrions insérer chaque entrée dans la table par un appel distinct à DatabaseExecute().
A la fin de l'opération du script, la base de données est enregistrée dans le fichier company.sqlite. Si nous essayions ensuite d'écrire les mêmes données dans la table COMPANY avec le même ID lors du prochain lancement du script, une erreur serait générée. C'est pourquoi nous avons d'abord supprimé la table afin de recommencer le travail à zéro à chaque fois que le script est lancé.
Récupérons maintenant toutes les entrées de la table COMPANY où le champ SALARY > 15000. Ceci est fait en utilisant la fonction DatabasePrepare() qui compile le texte de la requête et renvoie le handle de celle-ci pour une utilisation ultérieure dans DatabaseRead() ou DatabaseReadBind().
//--- 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; }
Après avoir créé la requête, nous devons obtenir les résultats de son exécution. Pour ce faire, nous utiliserons DatabaseRead(), qui exécute la requête lors du premier appel et passe à la première entrée dans les résultats. À chaque appel suivant, il lit simplement la prochaine entrée, jusqu'à la fin. Dans ce cas, il renvoie "false", ce qui signifie qu’il n’y a "plus d'entrées".
//--- 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);
Voici le résultat de l'exécution :
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.0L'exemple de code complet se trouve dans le fichier DatabaseRead.mq5.
Déboguer des requêtes SQL dans MetaEditor
Toutes les fonctions permettant de travailler avec la base de données renvoient un code d'erreur en cas d'échec. Travailler avec ces fonctions ne devrait pas poser de problème si vous suivez 4 règles simples :
- tous les handles de requêtes doivent être détruits après avoir été utilisés par DatabaseFinalize()
- la base de données doit être fermée avec DatabaseClose() avant la fin du programme
- les résultats de l'exécution de la requête doivent être vérifiés
- en cas d'erreur, une requête est détruite en premier, et la base de données est fermée ensuite
Le plus difficile est de comprendre quelle est l'erreur dans le cas où la requête n'a pas été créée. MetaEditor permet d'ouvrir des fichiers *.sqlite et de travailler avec eux en utilisant des requêtes SQL. Voyons comment faire en utilisant le fichier company.sqlite comme exemple :
1. Ouvrez le fichier company.sqlite qui se trouve dans le dossier commun du terminal.
2. Après avoir ouvert la base de données, la table COMPANY est visible dans le Navigateur. Faites un double clic dessus.
3. La requête "SELECT * FROM COMPANY" est automatiquement créée dans la barre de commande.
4. La requête est exécutée automatiquement. Elle peut également être exécutée en appuyant sur F9 ou en cliquant sur Exécuter.
5. Consultez le résultat de l'exécution de la requête.
6. Si quelque chose ne va pas, les erreurs sont affichées dans le Journal de l'éditeur.
Les requêtes SQL permettent d'obtenir des statistiques sur les champs de la table, par exemple, la somme et la moyenne. Exécutons les requêtes et vérifions si elles fonctionnent :
Nous pouvons maintenant implémenter ces requêtes SQL dans le code MQL5 :
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);
Comparons les résultats de l'exécution :
Some statistics: Total salary=125000.0 Average salary=31250.0
Lecture automatique des résultats de la requête SQL dans une structure en utilisant DatabaseReadBind()
La fonction DatabaseRead() permet de parcourir toutes les entrées des résultats de la requête SQL et d'obtenir les données complètes de chaque colonne dans le tableau résultant :
- DatabaseColumnName - nom de la colonne
- DatabaseColumnType - type de données
- DatabaseColumnSize- taille des données en octets
- DatabaseColumnText - retourne la valeur de la colonne sous forme de texte
- DatabaseColumnInteger - retourne la valeur de type int
- DatabaseColumnLong - retourne la valeur de type long
- DatabaseColumnDouble - retourne la valeur de type double
- DatabaseColumnBlob - retourne le tableau de données (BLOB)
Ces fonctions permettent de travailler avec les résultats de n'importe quelle requête SQL d'une manière unifiée. Mais cet avantage est contrebalancé par un code excessif. Si la structure des résultats de la requête est connue à l'avance, il est préférable d'utiliser la fonction DatabaseReadBind() qui vous permet de lire immédiatement l'entrée entière dans la structure. Nous pouvons refaire l'exemple précédent de la manière suivante. Tout d'abord, déclarez la structure Person :
struct Person { int id; string name; int age; string address; double salary; };
Chaque entrée est ensuite lue à partir des résultats de la requête SQL en utilisant DatabaseReadBind(request, person) :
//--- 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);
Cela nous permet d'obtenir immédiatement les valeurs de tous les champs de l'enregistrement actuel, sans avoir à les lire séparément.
Accélérer les transactions en les enveloppant dans DatabaseTransactionBegin() / DatabaseTransactionCommit()
Lorsque vous travaillez avec une table, il peut être nécessaire d'utiliser les commandes INSERT, UPDATE ou DELETE en masse. La meilleure façon de le faire est d'utiliser des transactions. Lors de l'exécution d’une transaction, la base de données est d'abord bloquée(DatabaseTransactionBegin). Les commandes de modification en bloc sont ensuite exécutées et enregistrées (DatabaseTransactionCommit) ou annulées en cas d'erreur (DatabaseTransactionRollback).
La description de la fonction DatabasePrepare présente un exemple d'utilisation des transactions :
//--- 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);
Les transactions permettent d'accélérer des centaines de fois les opérations de table en masse, comme le montre l'exemple DatabaseTransactionBegin :
Result: 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
Manipulation de l'historique des transactions
La puissance des requêtes SQL réside dans le fait que vous pouvez facilement trier, sélectionner et modifier les données sources sans écrire de code supplémentaire. Poursuivons l'analyse de l'exemple issu de la description de la fonction DatabasePrepare qui montre comment obtenir des positions à partir de transactions via une seule requête. Une transaction comporte des données sur les dates et les prix d'entrée et de sortie de la position, ainsi que des informations sur le symbole, la direction et le volume. Si nous jetons un coup d'œil à la structure des transactions, nous pouvons voir que les transactions d'entrée/sortie sont liées par l'ID commun de la position. Ainsi, si nous disposons d'un système de trading simple sur un compte de couverture, nous pouvons facilement combiner deux transactions en une seule. Pour ce faire, il faut utiliser la requête suivante :
//--- 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;
La table DEALS existante est utilisée ici. Les entrées sont créées à partir des transactions ayant le même DEAL_POSITION_ID en utilisant une union interne via INNER JOIN. Voici le résultat de l’utilisation de DatabasePrepare sur un compte de trading :
Result: 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 Filling the TRADES table took 12.51 milliseconds
Lancez ce script sur votre compte de couverture et comparez les résultats avec les positions de l'historique. Auparavant, vous n'aviez peut-être pas assez de connaissances ou de temps pour coder les boucles permettant d'obtenir un tel résultat. Mais vous pouvez maintenant le faire avec une seule requête SQL. Vous pouvez visualiser le résultat de l'opération du script dans MetaEditor. Pour ce faire, ouvrez le fichier trades.sqlite ci-joint.
Analyse du portefeuille par stratégies
Les résultats de l'opération du script DatabasePrepare présentés ci-dessus montrent clairement que les transactions sont effectuées sur plusieurs paires de devises. En outre, la colonne [magique] indique des valeurs de 100 à 600. Cela signifie que le compte de trading est géré par plusieurs stratégies, chacune d'entre elles ayant son propre numéro magique pour identifier ses transactions.
Une requête SQL nous permet d'analyser les transactions dans le contexte des valeurs magiques :
//--- 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");
Résultats
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 stratégies sur 6 se sont avérées rentables. Nous avons calculé les valeurs statistiques pour chaque stratégie :
- trades - nombre de trades par stratégie,
- gross_profit - profit total brut par stratégie (la somme de toutes les valeurs positives de profit ),
- gross_loss - perte totale brute par stratégie (la somme de toutes les valeurs de profit négatif),
- total_commission - somme de toutes les commissions pour les transactions de la stratégie,
- total_swap - somme de tous les swaps par trades de la stratégie,
- total_profit - somme du profit brutet de la perte brute,
- net_profit - somme(profit brut + perte brute + commission totales + swap total),
- win_trades - nombre de transactions où le bénéfice est supérieur à 0,
- loss_trades - nombre de trades où le profit est inférieur à 0,
- expected_payoff - gain attendu pour la transaction, hors swaps et commissions = profit net / nombre de trades,
- win_percent - pourcentage de transactions gagnantes,
- loss_percent - pourcentage de transactions perdantes,
- average_profit - gain moyen = bénéfice brut / nombre de transactions gagnantes,
- average_loss — perte moyenne = perte brute / nombre de transactions perdantes,
- profit_factor — facteur de profit = profit brut/perte brute.
Les statistiques pour le calcul des pertes et des profits ne tiennent pas compte des swaps et des commissions accumulées sur la position. Cela vous permet de voir les coûts nets. Il est possible qu'une stratégie génère un petit bénéfice, mais au final elle est non rentable en raison des swaps et des commissions.
Analyse de transactions par symboles
Nous sommes également en mesure d'analyser les transactions par symboles. Pour cela, effectuez la requête suivante :
//--- get trading statistics per symbols 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");
Résultats
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
Les statistiques montrent qu’un bénéfice net a été perçu pour 5 symboles sur 10 (net_profit > 0), et que le facteur de profit était positif pour 6 symboles sur 10 (facteur de profit > 1). C'est exactement le cas lorsque les swaps et les commissions rendent la stratégie non rentable.
Analyse de transactions par heures d'entrée
Même si le trading est effectué sur un seul symbole et qu'une seule stratégie est appliquée, l'analyse des transactions par heures d'entrée sur le marché peut toujours être utile. Ceci est fait avec la requête SQL suivante :
//--- 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");
Résultats
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
Il est clair que la majorité des transactions est réalisée dans l'intervalle de 9 à 16 heures inclus. Le trading pendant les autres heures donne de moins bons résultats. Retrouvez le code source complet avec ces 3 types de requêtes dans l'exemple de la fonction DatabaseExecute().
Sortie des données vers le journal de l'EA avec DatabasePrint()
Dans les exemples précédents, nous devions lire chaque entrée dans la structure et afficher les entrées une par une pour voir les résultats de la requête. Il peut souvent être peu pratique de créer une structure uniquement pour voir les valeurs de la table ou du résultat de la requête. La fonction DatabasePrint() a été ajoutée pour ces cas :
long DatabasePrint( int database, // database handle received in DatabaseOpen string table_or_sql, // a table or an SQL query uint flags // combination of flags );
Elle permet d'afficher non seulement une table existante, mais aussi les résultats de l'exécution d'une requête. Par exemple, affichez les valeurs de la table DEALS en utilisant la requête suivante :
DatabasePrint(db,"SELECT * from DEALS",0);
Résultats (les 10 premières lignes du tableau sont affichées) :
#| 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
Import/export des données
Pour simplifier l'import/export des données, les fonctions DatabaseImport() et DatabaseExport() ont été ajoutées. Ces fonctions permettent de travailler avec des fichiers CSV et des données contenues dans des archives ZIP.
DatabaseImport() importe des données dans la table spécifiée. Si aucune table n'existe avec le nom spécifié, elle est créée automatiquement. Les noms et les types de champs dans la table créée sont également définis automatiquement sur la base des données du fichier.
DatabaseExport() permet d'enregistrer les résultats de la table ou de la requête dans un fichier. Si les résultats de la requête sont exportés, la requête SQL doit commencer par "SELECT" ou "select". En d'autres termes, la requête SQL ne peut pas modifier l'état de la base de données, sinon DatabaseExport() échouera avec une erreur.
Vous pouvez consulter la description complète des fonctions dans la documentation MQL5.
Sauvegarde des résultats d'optimisation dans une base de données
Les fonctions permettant de travailler avec des bases de données peuvent également être utilisées pour traiter les résultats d'une optimisation. Utilisons l'EA MACD Sample disponible en standard pour illustrer la récupération des résultats de test et l'enregistrement des valeurs de tous les critères d'optimisation dans un fichier unique. Pour cela, créez la classe CDatabaseFrames, dans laquelle nous définissons la méthode OnTester() pour l'envoi des statistiques de trading :
//+------------------------------------------------------------------+ //| 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"); }
La 2ème méthode importante de la classe est OnTesterDeinit(). Après l'optimisation, elle lit toutes les trames obtenues et enregistre les statistiques dans la base de données :
//+------------------------------------------------------------------+ //| 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"); } //--- close the database if(db!=INVALID_HANDLE) { Print("Close database with handle=", db); DatabaseClose(db); }
Dans l’EA MACD Sample, incluez le fichier DatabaseFrames.mqh et déclarez la variable de classe 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;
Ajoutez ensuite 3 fonctions à la fin de l'EA qui ne seront appelées que pendant l'optimisation :
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Lancez l'optimisation et récupérez le fichier de base de données avec les statistiques de trading dans le dossier commun du terminal :
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'
Le fichier de base de données nouvellement créé peut être ouvert dans MetaEditor, ou utilisé dans une autre application MQL5 pour un travail ultérieur.
Vous pouvez ainsi préparer n'importe quelle donnée sous la forme nécessaire pour une analyse ultérieure ou un échange avec d'autres traders. Vous trouverez le code source, le fichier .ini avec les paramètres d'optimisation et le résultat de l'exécution dans l'archive MACD.zip jointe ci-dessous.
Optimisation de l'exécution de requêtes à l'aide d'index
La meilleure caractéristique de SQL (dans toutes ses implémentations, pas seulement SQLite) est qu'il s'agit d'un langage déclaratif et non d'un langage procédural. Lorsque vous programmez en SQL, vous indiquez au système CE QUE vous voulez calculer, et non COMMENT le calculer. La tâche de trouver le "comment" est déléguée au sous-système de planification des requêtes au sein du moteur de base de données SQL.
Pour une instruction SQL donnée, il peut y avoir des centaines ou des milliers d’algorithmes différents pour effectuer l'opération. Tous ces algorithmes obtiendront la bonne réponse, mais certains fonctionneront plus vite que d'autres. Le planificateur de requêtes essaie de choisir l'algorithme le plus rapide et le plus efficace pour chaque instruction SQL.
Le planificateur de requêtes de SQLite fait en général un bon travail. Il a cependant besoin d'index pour être plus efficace. Ces index doivent normalement être ajoutés par les programmeurs. Parfois, le planificateur de requêtes pourra choisir un algorithme sous-optimal. Dans ces cas, les programmeurs peuvent fournir des indications supplémentaires pour aider le planificateur de requêtes à faire un meilleur travail.
Recherche sans index
Supposons que nous ayons la table DEALS et qu’elle contient les 14 champs spécifiés. Vous trouverez ci-dessous les 10 premières entrées de cette table.
Numéro de ligne | 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 |
Elle présente les données de la section Propriétés des transactions (sauf DEAL_TIME_MSC, DEAL_COMMENT et DEAL_EXTERNAL_ID) nécessaires à l'analyse de l'historique des transactions. En dehors des données stockées, chaque table comporte toujours la clé entière rowid suivie des champs de l’enregistrement. Les valeurs de la clé rowid sont créées automatiquement et sont uniques dans la table. Elles sont incrémentées lors de l'ajout de nouvelles entrées. La suppression d'entrées peut entraîner des écarts de numérotation mais les lignes de la table sont toujours stockées dans l'ordre croissant des rowid.
Si nous devons trouver des transactions liées à une certaine position, par exemple l’ID=51447571, nous devons écrire la requête suivante :
SELECT * FROM deals WHERE position_id=51447571
Dans ce cas, un balayage complet de la table est effectué - toutes les lignes sont affichées et l'égalité entre POSITION_ID et la valeur de 51447571 est vérifiée pour chaque ligne. Les lignes qui remplissent cette condition sont affichées dans les résultats de l'exécution de la requête. Si la table contient des millions ou des dizaines de millions d'enregistrements, la recherche peut prendre beaucoup de temps. Si nous effectuons la recherche en fonction de la condition rowid=5 plutôt que position_id=51447571, le temps de recherche serait réduit de milliers, voire de millions de fois (selon la taille de la table).
SELECT * FROM deals WHERE rowid=5
Le résultat de l'exécution de la requête serait de plus le même puisque la ligne avec rowid=5 stocke position_id=51447571. L'accélération est obtenue grâce au fait que les valeurs rowidsont triées par ordre croissant et que la recherche binaire est utilisée pour obtenir le résultat. Malheureusement, la recherche par rowid ne nous convient pas puisque nous sommes intéressés par les entrées ayant la valeur position_id donnée.
Recherche avec un index
Pour rendre l'exécution d'une requête plus efficace en termes de temps, nous devons ajouter un index sur le champ POSITION_ID en utilisant la requête suivante :
CREATE INDEX Idx1 ON deals(position_id)
Dans ce cas, une table séparée à deux colonnes est générée. La première colonne est constituée des valeurs POSITION_ID triées par ordre croissant, et la deuxième colonne se compose de rowid.
POSITION_ID | Numéro de ligne |
0 | 1 |
51447238 | 2 |
51447239 | 3 |
51447565 | 4 |
51447571 | 5 |
51448053 | 6 |
51448064 | 7 |
51450470 | 8 |
51450476 | 9 |
51450479 | 10 |
La séquence rowid peut déjà être violée, bien qu'elle soit préservée dans notre exemple, puisque POSITION_ID est également augmenté lors de l'ouverture d'une position dans le temps.
Maintenant que nous avons un index sur le champ POSITION_ID, notre requête
SELECT * FROM deals WHERE position_id=51447571
s'exécute différemment. Tout d'abord, une recherche binaire dans l'index Idx1 est effectuée sur la colonne POSITION_ID et tous les rowid correspondant à la condition sont trouvés. La deuxième recherche binaire dans la table DEALS originale recherche toutes les entrées par les valeurs rowid connues. Ainsi, un seul balayage complet de la grande table est maintenant remplacé par deux recherches consécutives - d'abord, par index et ensuite par numéros de ligne de la table. Cela permet de réduire le temps d'exécution de ces requêtes de plusieurs milliers de fois, voire plus, en cas de grand nombre de lignes dans la table.
Règle générale: Si certains champs de la table sont souvent utilisés pour la recherche/la comparaison/le tri, il est recommandé de créer des index sur ces champs.
La table DEALS comporte également les champs SYMBOL, MAGIC (EA ID) et ENTRY (direction d'entrée). Si vous devez prélever des échantillons dans ces champs, il est alors raisonnable de créer des index correspondants. Par exemple :
CREATE INDEX Idx2 ON deals(symbol) CREATE INDEX Idx3 ON deals(magic) CREATE INDEX Idx4 ON deals(entry)
Gardez à l'esprit que la création d'index nécessite de la mémoire supplémentaire, et que chaque ajout/suppression d'entrée entraîne une réindexation. Vous pouvez également créer des index multiples basés sur plusieurs champs. Par exemple, si nous voulons sélectionner toutes les transactions effectuées par l'EA ayant MAGIC= 500 sur USDCAD, nous pouvons créer la requête suivante :
SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'
Dans ce cas, vous pouvez créer un index multiple sur les champs MAGIC et SYMBOL.
CREATE INDEX Idx5 ON deals(magic, symbol)
Et la table d'index suivante est créée (les 10 premières lignes sont représentées schématiquement) :
MAGIC | SYMBOL | Numéro de ligne |
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 |
Dans l’index multiple nouvellement créé, les entrées sont d'abord triées en blocs par le champ MAGIC et ensuite par le champ SYMBOL. Par conséquent, dans le cas de requêtes utilisant AND, la recherche dans l'index est d'abord effectuée par la colonne MAGIC. La valeur de la colonne SYMBOL est ensuite vérifiée. Si les deux conditions sont remplies, rowid est ajouté au jeu de résultats à utiliser dans la recherche dans la table originale. D'une manière générale, un tel index multiple n'est plus adapté aux requêtes où le champ SYMBOL est vérifié en premier.
SELECT * FROM deals WHERE symbol='USDCAD' AND magic=500
Bien que le planificateur de requêtes comprenne comment agir correctement et effectue la recherche dans le bon ordre dans de tels cas, il serait tout de même imprudent d'espérer qu’il corrigera automatiquement vos erreurs de conception de tables et de requêtes.
Requêtes OR
Les index multiples ne conviennent que pour les requêtes AND. Supposons par exemple que nous voulons trouver toutes les transactions effectuées par l'EA ayant MAGIC=100 ou sur l’EURUSD :
SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'
Dans ce cas, 2 recherches distinctes sont effectuées. Tous les numéros de ligne trouvés sont ensuite combinés en une sélection commune pour la recherche finale par numéros de ligne dans la table source.
SELECT * FROM deals WHERE magic=100 SELECT * FROM deals WHERE symbol='EURUSD'
Mais même dans ce cas, il est nécessaire que les deux champs de la requête OR aient des index, sinon la recherche entraînera le balayage complet de la table.
Tri
Pour accélérer le tri, il est également recommandé de disposer d'un index sur les champs utilisés pour classer les résultats des requêtes. Supposons par exemple que nous ayons besoin de sélectionner toutes les transactions sur l’EURUSD, triées par heure de transaction :
SELECT * FROM deals symbol='EURUSD' ORDER BY time
Vous devez dans ce cas envisager de créer un index sur le champ TIME. Le besoin d'index dépend de la taille de la table. Si la table a peu d'entrées, alors l'indexation peut difficilement faire gagner du temps.
Nous n'avons examiné ici que les bases de l'optimisation des requêtes SQL. Pour une meilleure compréhension, nous vous recommandons d'étudier le sujet à partir de la section Query Planning du site des développeurs de SQLite.
Intégration de la gestion des bases de données dans MetaEditor
La plateforme MetaTrader 5 est en développement constant. Nous avons ajouté le support natif des requêtes SQL au langage MQL5, et intégré les nouvelles fonctionnalités de gestion des bases de données dans MetaEditor, y compris la création d'une base de données, l'insertion et la suppression de données et l'exécution de transactions en masse. La création d'une base de données est standard et fait appel à l'Assistant MQL5. Il suffit de spécifier les noms du fichier et de tables, et d'ajouter tous les champs nécessaires avec leurs types.
Vous pouvez ensuite remplir la table avec des données, effectuer une recherche et une sélection, introduire des requêtes SQL, etc. Ainsi, vous pouvez travailler avec des bases de données non seulement à partir de programmes MQL5, mais aussi manuellement. Aucun outil supplémentaire n'est nécessaire pour cela.
L'introduction de SQLite dans MetaTrader ouvre de nouvelles possibilités aux traders en termes de traitement de grandes quantités de données, à la fois programmatiquement et manuellement. Nous avons fait de notre mieux pour que ces fonctions soient les plus pratiques à utiliser et qu'elles soient sur un pied d'égalité avec les autres solutions en termes de vitesse. Étudiez et appliquez le langage des requêtes SQL dans votre travail.