English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
preview
SQLite Gestion native des bases de données SQL dans MQL5

SQLite Gestion native des bases de données SQL dans MQL5

MetaTrader 5Tester | 10 octobre 2022, 14:09
689 0
MetaQuotes
MetaQuotes

Sommaire


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.0

L'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 :

  1. tous les handles de requêtes doivent être détruits après avoir été utilisés par DatabaseFinalize()
  2. la base de données doit être fermée avec DatabaseClose() avant la fin du programme
  3. les résultats de l'exécution de la requête doivent être vérifiés
  4. 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 :

Travailler avec des requêtes SQL dans MetaEditor

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 :

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.

Travailler avec une base de données dans MetaEditor

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.

Création d'une base de données dans l'Assistant MQL

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.

Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/7463

Fichiers joints |
SqLiteTest.zip (2708.45 KB)
trades.sqlite (340 KB)
MACD.zip (8.27 KB)
DatabaseRead.mq5 (10.11 KB)
DatabasePrepare.mq5 (35.02 KB)
DatabaseExecute.mq5 (64.83 KB)
Comment créer des graphiques 3D avec DirectX dans MetaTrader 5 Comment créer des graphiques 3D avec DirectX dans MetaTrader 5
Les graphiques 3D constituent un excellent moyen d'analyser de grosses quantités de données, car ils permettent de visualiser des choses cachées. Ces tâches peuvent être faites directement dans MQL5, et les fonctions DireсtX permettent de créer des objets tridimensionnels. Il est ainsi possible de créer des programmes de n’importe quelle complexité, même des jeux en 3D pour MetaTrader 5. Vous pouvez commencer à apprendre les graphiques 3D en dessinant des formes tridimensionnelles simples.
Visualisez le ! Bibliothèque graphique MQL5 similaire à 'plot' du langage R Visualisez le ! Bibliothèque graphique MQL5 similaire à 'plot' du langage R
Lors de l'étude de la logique de trading, la représentation visuelle sous forme de graphiques est d’une grande importance. Un certain nombre de langages de programmation populaires dans la communauté scientifique (tels que R et Python) contiennent une fonction spéciale "plot" utilisée pour la visualisation. Elle permet de dessiner des lignes, des distributions de points et des histogrammes pour visualiser les modèles. En MQL5, vous pouvez faire de même en utilisant la classe CGraphics.
Développer un Expert Advisor à partir de zéro (partie 10) : Accéder aux indicateurs personnalisés Développer un Expert Advisor à partir de zéro (partie 10) : Accéder aux indicateurs personnalisés
Comment accéder aux indicateurs personnalisés directement depuis un Expert Advisor ? Un EA de trading ne peut être vraiment utile que s'il peut utiliser des indicateurs personnalisés ; sinon, ce n'est qu'un ensemble de lignes de codes et d'instructions.
Apprenez à concevoir un système de trading basé sur Le SAR Parabolique Apprenez à concevoir un système de trading basé sur Le SAR Parabolique
Dans cet article, nous poursuivons notre série sur la conception d'un système de trading utilisant des indicateurs les plus populaires. Dans cet article, nous allons découvrir en détail l'indicateur SAR Parabolique, puis comment nous pouvons concevoir un système de trading à utiliser dans MetaTrader 5 en utilisant quelques stratégies simples.