
SQLite: Gestione nativa dei database SQL in MQL5
Contenuto
- Il trading algoritmico moderno in MetaTrader 5
- Funzioni per lavorare con i database
- Interrogazione semplice
- Debug delle query SQL in MetaEditor
- Lettura automatica dei risultati delle query nella struttura, utilizzando il metodo DatabaseReadBind()
- Accelerare le transazioni sistemandole in DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Gestione delle transazioni nello storico di trading
- Analisi del portafoglio per strategie
- Analisi delle transazioni per simboli
- Analisi delle transazioni in base alle ore di ingresso
- Pratico output dei dati nel log dell’EA in DatabasePrint()
- Importazione/esportazione dei dati
- Salvataggio dei risultati dell'ottimizzazione nel database
- Ottimizzare l'esecuzione delle query utilizzando gli indici
- Integrazione della gestione dei database in MetaEditor
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.0Il 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:
- tutti gli handle delle query devono essere distrutti dopo l'uso da parte di DatabaseFinalize();
- il database deve essere chiuso con DatabaseClose() prima del completamento;
- i risultati dell'esecuzione della query devono essere controllati;
- 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.
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:
- DatabaseColumnName — nome,
- DatabaseColumnType — tipo di dato,
- DatabaseColumnSize — dimensione dei dati in byte,
- DatabaseColumnText — legge come testo,
- DatabaseColumnInteger — ottiene un valore di tipo int,
- DatabaseColumnLong — ottiene un valore di tipo long,
- DatabaseColumnDouble — ottiene un valore di tipo double,
- DatabaseColumnBlob — ottiene i dati di un array.
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:
- trades - numero dei trades per strategia,
- gross_profit - profitto totale per strategia (la somma di tutti i valori positivi del profitto ),
- gross_loss - perdita totale per strategia (la somma di tutti i valori negativi dei profitti ),
- total_commission - somma di tutte le commissioni per le operazioni della strategia,
- total_swap - somma di tutti gli swap delle operazioni della strategia,
- total_profit - somma di gross_profit e gross_loss,
- net_profit - somma (gross_profit + gross_loss + total_commission + total_swap),
- win_trades - numero di trades con profitto > 0,
- loss_trades - numero di trades in cui il profitto è inferiore a 0,
- expected_payoff - payoff previsto per l'operazione, esclusi swap e commissioni = net_profit/trades,
- win_percent - percentuale di operazioni vincenti,
- loss_percent - percentuale di operazioni perdenti,
- average_profit — media vincite = gross_profit/win_trades,
- average_loss — media perdite = gross_loss /loss_trades,
- profit_factor — Fattore di profitto = gross_profit/gross_loss.
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.
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.
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.
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/7463





- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso