SQLite: Gestione nativa dei database SQL in MQL5

MetaQuotes | 25 ottobre, 2022

Contenuto


Il trading algoritmico moderno in MetaTrader 5

MQL5 è una soluzione perfetta per il trading algoritmico, poiché è il più vicino possibile al C++ sia in termini di sintassi che di velocità di calcolo. La piattaforma MetaTrader 5 offre ai suoi utenti il moderno linguaggio specializzato per lo sviluppo di robot di trading e indicatori personalizzati, consentendo loro di andare oltre i semplici compiti di trading e di creare sistemi analitici di qualsiasi complessità.

Oltre alle funzioni di trading asincrono e alle librerie matematiche, i trader hanno accesso anche alle funzioni di rete, importazione di dati in Python, calcolo parallelo nativo in OpenCL,supporto per le librerie .NET con importazione "intelligente" delle funzioni, integrazione con MS Visual Studio e alla visualizzazione dei dati tramite DirectX. Questi indispensabili strumenti nell'arsenale del moderno trading algoritmico attualmente consentono agli utenti di risolvere una serie di compiti senza lasciare la piattaforma di trading MetaTrader 5.


Funzioni per lavorare con i database

Lo sviluppo delle strategie di trading è associato alla gestione di grandi quantità di dati. Un algoritmo di trading sotto forma di un programma MQL5 affidabile e veloce non è più sufficiente. Per ottenere risultati affidabili, i trader devono anche eseguire un gran numero di test e ottimizzazioni su una varietà di strumenti di trading, salvare e gestire i risultati, condurre un'analisi e decidere come procedere.

Ora hai la possibilità di lavorare con i database utilizzando un semplice e popolare motore SQLite direttamente in MQL5. I risultati dei test sul sito web degli sviluppatori mostrano un'elevata velocità di esecuzione delle query SQL. Nella maggior parte dei compiti, ha superato PostgreSQL e MySQL. A loro volta, abbiamo confrontato le velocità delle esecuzioni di questi test su MQL5 e LLVM 9.0.0 e le abbiamo riportate nella tabella. I risultati dell'esecuzione sono espressi in millisecondi: meno sono, meglio è.

Nome
Descrizione
 LLVM  
MQL5
Test 1
 1000 INSERIMENTI
11572
8488
Test 2
 25000 INSERIMENTI in una transazione
59
60
Test 3
 25000 INSERIMENTI in una tabella indicizzata
102
105
Test 4
 100 SELEZIONI senza indice
142
150
Test 5
 100 SELEZIONI su un confronto di stringhe
391
390
Test 6
 Creazione di un indice
43
33
Test 7
 5000 SELEZIONI con un indice
385
307
Test 8
 1000 AGGIORNAMENTI senza indice
58
54
Test 9
 25000 AGGIORNAMENTI con un indice
161
165
Test 10
 25000 AGGIORNAMENTI di testo con un indice
124
120
Test 11  INSERIMENTO da una SELEZIONE
84
84
Test 12
 ELIMINARE senza indice
25
74
Test 13
 ELIMINARE con un indice
70
72
Test 14  Un grande INSERIMENTO dopo una grande ELIMINAZIONE
62
66
Test 15  Una grande CANCELLAZIONE seguita da tanti piccoli INSERIMENTI
33
33
Test 16  DROP TABLE: finita
42
40

I dettagli del test sono disponibili nel file SqLiteTest.zip allegato. Specifiche del computer su cui sono state effettuate le misurazioni - Windows 10 x64, Intel Xeon E5-2690 v3 @ 2,60GHz. 

I risultati dimostrano che potete essere certi di ottenere le massime prestazioni quando lavorate con i database in MQL5. Chi non ha mai incontrato SQL prima d'ora vedrà che il linguaggio delle query strutturate permette di risolvere molti compiti in modo rapido ed elegante senza la necessità di complessi cicli e campionamenti.


Interrogazione semplice

I database memorizzano le informazioni sotto forma di tabelle, mentre la ricezione/modifica e l'aggiunta di nuovi dati avviene tramite query nel linguaggio SQL. Vediamo la creazione di un semplice database e l'ottenimento di dati da esso.

//+------------------------------------------------------------------+
//| 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);
  }

Creare e chiudere un database è simile a lavorare con i file. Prima si crea un handle per un database, poi lo si controlla e infine lo si chiude.

Successivamente, si verifica la presenza di una tabella nel database. Se la tabella esiste già, il tentativo di inserire i dati dell'esempio sopra si conclude con un errore.

//--- 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 tabella viene creata e cancellata tramite query e il risultato dell'esecuzione deve essere sempre controllato. La tabella COMPANY presenta solo cinque campi: entry ID, name, age, address e salary. Il campo ID è una chiave, cioè un indice univoco. Gli indici consentono di definire in modo affidabile ogni voce e possono essere utilizzati in diverse tabelle per collegarle tra loro. Questo è simile al modo in cui l'ID della posizione collega tutte le operazioni e gli ordini relativi a una particolare posizione.

Ora la tabella dovrebbe essere riempita con dati. Questo viene fatto utilizzando la query 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;
     }

Come si può vedere, vengono aggiunte quattro voci alla tabella COMPANY. Per ogni voce viene specificata la sequenza dei campi e i valori da inserire in questi campi. Ogni voce viene inserita da una query "INSERT...." separata, combinata in un'unica query. In altre parole, potremmo inserire ogni voce nella tabella con una chiamata separata a DatabaseExecute().

Poiché, al termine dell'operazione di script, il database viene salvato nel file company.sqlite, si cercherà di scrivere gli stessi dati nella tabella COMPANY con lo stesso ID durante il successivo lancio dello script. In questo caso si verificherebbe un errore. Per questo motivo dobbiamo eliminare prima la tabella, in modo da ricominciare il lavoro da zero ogni volta che si lancia lo script.

Ora prendiamo tutte le voci della tabella COMPANY in cui il campo SALARY è > 15000. Questo viene fatto utilizzando la funzione DatabasePrepare() che compila il testo della query e restituisce l'handle per il successivo utilizzo in DatabaseRead() o 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;
     }

Dopo che la query è stata creata con successo, dobbiamo ottenere i risultati della sua esecuzione. Lo faremo utilizzando DatabaseRead(), che esegue la query durante la prima chiamata e si sposta alla prima voce dei risultati. A ogni chiamata successiva, legge semplicemente la voce successiva, fino a quando raggiunge la fine. In questo caso, restituisce 'false', che significa "nessuna voce".

//--- 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);

Il risultato dell'esecuzione è il seguente:

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

Il codice di esempio completo si trova nel file DatabaseRead.mq5.

Debug delle query SQL in MetaEditor

Tutte le funzioni per lavorare con il database restituiscono il codice di errore in caso di codice non riuscito. Lavorare con loro non dovrebbe causare alcun problema se si seguono quattro semplici regole:

  1. tutti gli handle delle query devono essere distrutti dopo l'uso da parte di DatabaseFinalize();
  2. il database deve essere chiuso con DatabaseClose() prima del completamento;
  3. i risultati dell'esecuzione della query devono essere controllati;
  4. in caso di errore, prima viene distrutta una query, mentre il database viene chiuso successivamente.

La cosa più difficile è capire quale sia l'errore se la query non è stata creata. MetaEditor permette di aprire file *.sqlite e di lavorare con essi utilizzando query SQL. Vediamo come fare utilizzando il file company.sqlite come esempio:

1. Aprire il file company.sqlite nella cartella common del terminale.

2. Dopo aver aperto il database, si può vedere la tabella COMPANY nel Navigatore. Fare doppio click su di esso.

3. La query "SELECT * FROM COMPANY" viene creata automaticamente nella barra di stato.

4. La query viene eseguita automaticamente. Può essere eseguita anche premendo F9 o facendo clic su Esegui.

5. Vedere il risultato dell'esecuzione della query.

6. Se qualcosa non va, gli errori vengono visualizzati nel Diario.


Le query SQL consentono di ottenere statistiche sui campi della tabella, ad esempio la somma e la media. Creiamo le query e verifichiamo se funzionano.

Lavorare con le query in MetaEditor

Ora possiamo implementare queste query nel codice 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);

Confrontiamo i risultati dell'esecuzione:

Some statistics:
Total salary=125000.0
Average salary=31250.0


Lettura automatica dei risultati delle query nella struttura tramite DatabaseReadBind()

La funzione DatabaseRead() consente di scorrere tutte le voci dei risultati della query e di ottenere i dati completi di ogni colonna della tabella risultante:

Queste funzioni consentono di lavorare con qualsiasi risultato delle query in modo unificato. Tuttavia, questo vantaggio è controbilanciato da un codice eccessivo. Se la struttura dei risultati della query è nota in anticipo, è meglio usare la funzione DatabaseReadBind() che consente di leggere immediatamente l'intera voce nella struttura. Possiamo rifare l'esempio precedente nel modo seguente: per prima cosa, dichiarare la struttura Person:

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

Successivamente, ogni voce viene letta dai risultati della query utilizzando 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);

Ciò consente di ottenere subito i valori di tutti i campi della voce corrente, senza doverli leggere separatamente.


Accelerare le transazioni sistemandole in DatabaseTransactionBegin()/DatabaseTransactionCommit() 

Quando si lavora con una tabella, può essere necessario utilizzare in massa i comandi INSERT, UPDATE o DELETE. Il modo migliore per farlo è utilizzare le transazioni. Quando si effettuano transazioni, il database viene prima bloccato (DatabaseTransactionBegin). I comandi di modifica di massa vengono quindi eseguiti e salvati (DatabaseTransactionCommit) o annullati in caso di errore (DatabaseTransactionRollback).

La descrizione della funzione DatabasePrepare presenta un esempio di utilizzo delle transazioni:

//--- 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);

Le transazioni consentono di accelerare centinaia di volte le operazioni di massa sulle tabelle, come mostrato nell’esempio 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


Gestione delle transazioni nello storico di trading

La potenza delle query SQL risiede nel fatto che è possibile ordinare, selezionare e modificare facilmente i dati di origine senza scrivere codice. Continuiamo ad analizzare l'esempio della descrizione della funzione DatabasePrepare, che mostra come ottenere i trades dalle transazioni con una sola query. Un trade presenta i dati relativi alle date e ai prezzi di entrata/uscita della posizione, nonché le informazioni su simbolo, direzione e volume. Se diamo un'occhiata alla struttura delle transazioni possiamo vedere che le operazioni di entrata e di uscita sono collegate dall'ID della posizione in comune. Quindi, se abbiamo un semplice sistema di trading su un conto hedging, possiamo facilmente combinare due operazioni in un singolo trade. Questo viene fatto utilizzando le seguenti query:

//--- 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;

In questo caso si utilizza la tabella DEALS esistente. Le voci vengono create a partire dalle transazioni con lo stesso DEAL_POSITION_ID utilizzando la combinazione interna tramite INNER JOIN. Il risultato dell'operazione di esempio da DatabasePrepare su un conto di 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

Lanciate questo script sul vostro conto hedging e confrontate i risultati con le posizioni nello storico. In precedenza, potresti non aver avuto abbastanza conoscenze o tempo per codificare i cicli per ottenere un tale risultato. Ora è possibile farlo con un'unica query SQL. È possibile visualizzare il risultato dell'operazione di script nel MetaEditor. A tal fine, aprire il file allegato trades.sqlite.


Analisi del portafoglio per strategie

I risultati dell'operazione di script DatabasePrepare sopra indicata rendono chiaro che il trading è condotto su più coppie di valute. Inoltre, la colonna [magic] mostra i valori da 100 a 600. Ciò significa che il conto di trading è gestito da diverse strategie, ognuna delle quali ha un proprio numero magico per identificare le proprie operazioni.

Una query SQL ci permette di analizzare il trading nel contesto dei valori magic:

//--- 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");

Risultato:

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 strategie su 6 si sono rivelate redditizie. Abbiamo ricevuto valori statistici per ogni strategia:

Le statistiche per il calcolo del profitto e della perdita non considerano gli swap e le commissioni maturate sulla posizione. In questo modo è possibile vedere i costi netti. Può accadere che una strategia produca un piccolo profitto ma sia generalmente poco redditizia a causa degli swap e delle commissioni.


Analisi delle transazioni per simboli

Siamo in grado di analizzare il trading per simboli. A tal fine, eseguire la seguente query:

//--- 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");

Risultato:

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

Le statistiche mostrano che il profitto netto è stato ottenuto su 5 simboli su 10 (profitto_netto>0), mentre il fattore di profitto è stato positivo su 6 simboli su 10 (fattore_di_profitto>1). Questo è esattamente il caso in cui gli swap e le commissioni rendono la strategia non redditizia su EURJPY.


Analisi delle transazioni in base alle ore di ingresso

Anche se il trading viene effettuato su un singolo simbolo e viene applicata una singola strategia, l'analisi delle operazioni in base all'orario di ingresso nel mercato può essere ancora utile. Questo viene eseguito con la seguente query SQL:

//--- 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");

Risultato:

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

È evidente che il maggior numero di trade viene effettuato nell'intervallo compreso tra le ore 9 e le 16. Il trading durante le altre ore offre un minor numero di trade e per lo più non profittevoli. Trovate il codice sorgente completo con questi tre tipi di query nell'esempio della funzione DatabaseExecute().


Pratico output dei dati nel log dell’EA in DatabasePrint()

Negli esempi precedenti, abbiamo dovuto leggere tutte le voci della struttura e visualizzarle una per una per vedere i risultati della query. Spesso può essere scomodo creare una struttura solo per vedere i valori della tabella o dei risultati della query. La funzione DatabasePrint() è stata aggiunta per questi casi:

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

Permette di stampare non solo una tabella esistente, ma anche i risultati dell'esecuzione di una query che può essere rappresentata come una tabella. Ad esempio, visualizzare i valori della tabella DEALS utilizzando la seguente query:

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

Risultato (vengono visualizzate le prime 10 righe della tabella):

  #|       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 


Importazione/esportazione dei dati

Per semplificare l'importazione/esportazione dei dati, sono state aggiunte le funzioni DatabaseImport() e DatabaseExport(). Queste funzioni permettono di lavorare con i file CSV e con i dati contenuti negli archivi ZIP.

DatabaseImport() importa i dati in una tabella specificata. Se non esiste una tabella con il nome specificato, viene creata automaticamente. Anche i nomi e i tipi di campo della tabella creata vengono definiti automaticamente in base ai dati del file. 

DatabaseExport() consente di salvare i risultati della tabella o della query nel file. Se i risultati della query vengono esportati, la query SQL deve iniziare con "SELECT" o "select". In altre parole, la query SQL non può alterare lo stato del database, altrimenti DatabaseExport() fallisce con un errore.

Vedere la descrizione completa delle funzioni nella Documentazione MQL5.


Salvataggio dei risultati dell'ottimizzazione nel database

Le funzioni per lavorare con i database possono essere utilizzate anche per gestire i risultati dell'ottimizzazione. Utilizziamo l'EA di esempio MACD della fornitura standard per illustrare i risultati del test ottenuti utilizzando i frame e salvando poi i valori di tutti i criteri di ottimizzazione in un unico file. A tal fine, si crea la classe CDatabaseFrames, in cui si definisce il metodo OnTester() per l'invio delle statistiche di 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");
  }

Il secondo importante metodo della classe è OnTesterDeinit(). Dopo l'ottimizzazione, legge tutti i frames ottenuti e salva le statistiche nel database:

//+------------------------------------------------------------------+
//| 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);
     }

Nel MACD Sample EA, includere il file DatabaseFrames.mqh e dichiarare la variabile della 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;

Quindi, aggiungere tre funzioni alla fine dell'EA, da richiamare solo durante l'ottimizzazione:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Avviare l'ottimizzazione per ottenere il file del database con le statistiche di trading nella cartella del terminale common:

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'

Il file di database appena creato può essere aperto in MetaEditor o utilizzato in un'altra applicazione MQL5 per ulteriori lavori.

Lavorare con il database in MetaEditor

In questo modo è possibile preparare qualsiasi dato nella forma necessaria per un'ulteriore analisi o per lo scambio con altri trader. Trovate il codice sorgente, il file ini con i parametri di ottimizzazione e il risultato dell'esecuzione nell'archivio MACD.zip allegato di seguito.


Ottimizzare l'esecuzione delle query utilizzando gli indici

La caratteristica migliore di SQL (in tutte le sue implementazioni, non solo in SQLite) è che si tratta di un linguaggio dichiarativo, non procedurale. Quando si programma in SQL, si dice al sistema COSA si vuole calcolare, non COME calcolarlo. Il compito di capire il "come" è delegato al sottosistema di pianificazione delle query all'interno del motore del database SQL.

Per una qualsiasi istruzione SQL data, potrebbero esistere centinaia o migliaia di algoritmi diversi per eseguire l'operazione. Tutti questi algoritmi otterranno la risposta corretta, anche se alcuni saranno più veloci di altri. Il query planner prova a scegliere l'algoritmo più veloce ed efficiente per ogni istruzione SQL.

Nella maggior parte dei casi, il query planner di SQLite fa un buon lavoro. Tuttavia, il query planner ha bisogno di indici per fare del suo meglio. Questi indici devono normalmente essere aggiunti dai programmatori. A volte, il query planner effettuerà una scelta di algoritmo non ottimale. In questi casi, i programmatori possono fornire ulteriori suggerimenti per aiutare il query planner a fare un lavoro migliore.

Ricerca senza indici

Supponiamo di avere una tabella DEALS contenente i 14 campi specificati. Di seguito sono riportate le prime 10 voci di questa tabella.

rowid
ID ORDER_ID POSITION_ID TEMPO TIPO ENTRY SIMBOLO VOLUME PREZZO PROFITTO SWAP COMMISSIONE MAGIC MOTIVO
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.20 100 3
10 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0 0 -0.18 200 3

Presenta i dati della sezione Deal Properties (ad eccezione di DEAL_TIME_MSC, DEAL_COMMENT e DEAL_EXTERNAL_ID) necessari per analizzare la cronologia delle transazioni. Oltre ai dati memorizzati, ogni tabella presenta sempre la chiave intera rowid, seguita dai campi di inserimento. rowid I valori delle chiavi vengono creati automaticamente e sono unici all'interno della tabella. Vengono aumentati quando si aggiungono nuove voci. L'eliminazione di voci può causare dei buchi nella numerazione ma le righe della tabella sono sempre memorizzate in ordine crescente di rowid.

Se dobbiamo trovare i deals relativi a una determinata posizione, ad esempio ID=51447571, dobbiamo scrivere la seguente query:

SELECT * FROM deals WHERE position_id=51447571

In questo caso, viene eseguita una scansione completa della tabella — tutte le righe vengono consultate e il POSITION_ID viene controllato per verificare l'uguaglianza con il valore 51447571 ad ogni riga. Le righe che soddisfano questa condizione vengono visualizzate nei risultati dell'esecuzione della query. Se la tabella contiene milioni o decine di milioni di record, la ricerca può richiedere molto tempo. Se si effettuasse la ricerca in base alla condizione rowid=5 anziché position_id=51447571, il tempo di ricerca si ridurrebbe di migliaia o addirittura milioni di volte (a seconda delle dimensioni della tabella).

SELECT * FROM deals WHERE rowid=5

Il risultato dell'esecuzione della query sarebbe lo stesso poiché la riga con rowid=5 memorizza position_id=51447571. L'accelerazione è ottenuta grazie al fatto che i valori rowid sono ordinati in ordine crescente e la ricerca binaria viene utilizzata per ottenere il risultato. Purtroppo, la ricerca per rowidnon è adatta a noi, poiché siamo interessati alle voci che hanno il valore position_id necessario.

Ricerca per indice

Per rendere più efficiente l'esecuzione di una query, è necessario aggiungere l'indice del campo POSITION_ID utilizzando la seguente query:

 CREATE INDEX Idx1 ON deals(position_id)

In questo caso, viene generata una tabella separata con due colonne. La prima colonna è costituita dai valori POSITION_ID ordinati in ordine crescente, mentre la seconda colonna è costituita da rowid.

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

La sequenza rowid potrebbe già essere violata, anche se nel nostro esempio viene preservata, poiché POSITION_ID viene incrementato anche quando si apre una posizione tramite tempo.

Ora che disponiamo dell'indice del campo POSITION_ID, la nostra query

SELECT * FROM deals WHERE position_id=51447571

viene eseguita in modo diverso. In primo luogo, viene eseguita una ricerca binaria nell'indice Idx1 in base alla colonna POSITION_ID e vengono trovati tutti i rowid che soddisfano la condizione. La seconda ricerca binaria nella tabella DEALS originale cerca tutte le voci in base ai valori rowid conosciuti. Pertanto, una singola scansione completa della tabella di grandi dimensioni è ora sostituita da due ricerche consecutive — la prima per indice e poi per numero di riga della tabella. Questo permette di ridurre il tempo di esecuzione di tali query di migliaia o più volte nel caso di un numero elevato di righe nella tabella.

Regola generale: Se alcuni campi della tabella vengono utilizzati spesso per la ricerca/comparazione/ordinamento, è raccomandato creare indici per questi campi.

La tabella DEALS contiene anche i campi SYMBOL, MAGIC (ID EA) e ENTRY (direzione di entrata). Se è necessario prelevare campioni in questi campi, allora è ragionevole creare gli indici appropriati. Per esempio:

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

Tieni presente che la creazione di indici richiede memoria aggiuntiva e che ogni aggiunta/cancellazione di voci comporta una nuova indicizzazione. È inoltre possibile creare indici multipli basati su più campi. Ad esempio, se vogliamo selezionare tutte le operazioni eseguite dall'EA che ha MAGIC= 500 su USDCAD, possiamo creare la seguente query:

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

In questo caso, è possibile creare un indice multiplo per campi MAGIC e SYMBOL

CREATE INDEX Idx5 ON deals(magic, symbol)

e viene creata la seguente tabella di indici (le prime 10 righe sono mostrate in modo schematico)

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

Nell'indice multiplo appena creato, le voci vengono prima ordinate in blocchi per MAGIC e poi – per campo SYMBOL. Pertanto, in caso di query AND la ricerca nell'indice viene eseguita prima dalla colonna MAGIC. Il valore della colonna SYMBOL viene controllato successivamente. Se entrambe le condizioni vengono soddisfatte, il rowid viene aggiunto all'insieme dei risultati da utilizzare nella ricerca della tabella originale. In generale, un indice multiplo di questo tipo non è più adatto per le interrogazioni in cui SYMBOL viene controllato per primo

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

Sebbene il query planner capisca come agire correttamente ed esegua la ricerca nell'ordine giusto in questi casi, non sarebbe comunque saggio sperare che corregga automaticamente sempre gli errori di progettazione di tabelle e query.

Interrogazioni OR

Gli indici multipli sono adatti solo per le interrogazioni AND. Ad esempio, supponiamo di voler trovare tutte le operazioni eseguite dall'EA con MAGIC=100 o su EURUSD:

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

In questo caso, vengono implementate due ricerche separate. Tutti i rowid trovati vengono quindi combinati in una selezione comune per la ricerca finale in base ai numeri di riga nella tabella di origine.

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

Ma anche in questo caso, è necessario che entrambi i campi della query OR abbiano degli indici, altrimenti la ricerca causerà la scansione completa della tabella.

Ordinamento

Per velocizzare l'ordinamento, si consiglia anche di avere un indice per i campi utilizzati per organizzare i risultati delle query. Per esempio, supponiamo di dover selezionare tutte le operazioni su EURUSD ordinate in base all'ora dell'operazione:

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

In questo caso, si dovrebbe considerare la possibilità di creare un indice per il campo TIME. La necessità di indici dipende dalle dimensioni della tabella. Se la tabella ha poche voci, l'indicizzazione difficilmente può far risparmiare tempo.

Qui abbiamo esaminato solo le basi dell'ottimizzazione delle query. Per una migliore comprensione, si consiglia di studiare l'argomento a partire dalla sezione Query Planning sul sito web degli sviluppatori di SQLite.


Integrazione della gestione dei database in MetaEditor

La piattaforma MetaTrader 5 è in costante sviluppo. Abbiamo aggiunto il supporto nativo per le query SQL al linguaggio MQL5 e integrato le nuove funzionalità per la gestione dei database in MetaEditor, tra cui la creazione di un database, l'inserimento e l'eliminazione di dati e l'esecuzione di transazioni in blocco. La creazione di un database è standard e coinvolge la procedura guidata MQL5. È sufficiente specificare i nomi dei file e delle tabelle e aggiungere tutti i campi necessari che indicano il tipo.

Creazione di un database in MQL Wizard

Successivamente, è possibile riempire la tabella con i dati, eseguire una ricerca e una selezione, introdurre query SQL, ecc. In questo modo, è possibile lavorare con i database non solo dai programmi MQL5, ma anche manualmente. Non sono necessari browser di terze parti.

L'introduzione di SQLite in MetaTrader apre nuove opportunità per i trader in termini di gestione di grandi quantità di dati sia in modo programmatico che manuale. Abbiamo fatto del nostro meglio per assicurarci che queste funzioni siano comode da usare e che siano alla pari con le altre soluzioni in termini di velocità. Studia e applica il linguaggio delle query SQL nel tuo lavoro.