
SQLite: trabalho nativo com bancos de dados SQL em MQL5
Sumário
- Negociação algorítmica moderna no MetaTrader 5
- Funções para trabalhar com bancos de dados
- Exemplo de consulta simples
- Depuração de consultas SQL no MetaEditor
- Leitura automática de resultados de consulta na estrutura usando DatabaseReadBind()
- Aceleração de transações por embrulhamento em DatabaseTransactionBegin()/DatabaseTransactionCommit()
- Trabalho com transações do histórico de negociação
- Análise de portfólio por estratégias
- Análise de transações por símbolos
- Análise de transações por horas de entrada
- Conveniente exibição de dados no log do EA em DatabasePrint()
- Importação e exportação de dados
- Salvando resultados de otimização em um banco de dados
- Otimizando a execução de consultas com a ajuda de índices
- Integração com bancos de dados no MetaEditor
Negociação algorítmica moderna no MetaTrader 5
A MQL5 é uma solução ideal para negociação algorítmica, pois é o mais próximo possível da linguagem C ++, tanto na sintaxe quanto na velocidade computacional. A plataforma MetaTrader 5 fornece aos traders uma linguagem especializada e moderna para escrever robôs de negociação e indicadores personalizados, o que permite ir além de tarefas simples de negociação, aqui você pode criar sistemas analíticos de qualquer nível de complexidade.
Além das funções de negociação assíncronas e bibliotecas matemáticas, os traders dispõem de funções de rede, importação de dados para Python, computação paralela em OpenCL, suporte nativo para bibliotecas .NET com recursos de importação inteligentes integração com o MS Visual Studio e visualização de dados usando DirectX. Hoje, essas são as ferramentas necessárias no arsenal da negociação algorítmica moderna que permitem resolver uma variedade de problemas sem ir além da plataforma de negociação MetaTrader 5.Funções para trabalhar com bancos de dados
O desenvolvimento de estratégias de negociação está associado ao processamento de grandes quantidades de dados. Hoje, não basta escrever um algoritmo de negociação na forma de um programa MQL5 confiável e rápido. Para obter resultados confiáveis, o trader também precisa realizar um grande número de testes e otimizações com base em diferentes instrumentos, salvar os resultados, processá-los, analisar e decidir qual o caminho a seguir.
Agora, em MQL5, você pode trabalhar com bancos de dados no SQLite, um mecanismo simples e popular. Os resultados dos testes no site dos desenvolvedores mostram alta velocidade de execução de consultas SQL. Assim, no que diz respeito à maioria das tarefas, ela ultrapassou o PostgreSQL e MySQL. Por sua vez, comparamos a velocidade desses testes em MQL5 e LLVM 9.0.0 e preenchemos uma tabela com esse dados. Os resultados da execução são fornecidos em milissegundos - quanto menos, melhor.
Nome |
Descrição |
LLVM |
MQL5 |
---|---|---|---|
Test 1 |
1000 INSERTs |
11572 |
8488 |
Test 2 |
25000 INSERTs in a transaction |
59 |
60 |
Test 3 |
25000 INSERTs into an indexed table |
102 |
105 |
Test 4 |
100 SELECTs without an index |
142 |
150 |
Test 5 |
100 SELECTs on a string comparison |
391 |
390 |
Test 6 |
Creating an index |
43 |
33 |
Test 7 |
5000 SELECTs with an index |
385 |
307 |
Test 8 |
1000 UPDATEs without an index |
58 |
54 |
Test 9 |
25000 UPDATEs with an index |
161 |
165 |
Test 10 |
25000 text UPDATEs with an index |
124 |
120 |
Test 11 | INSERTs from a SELECT |
84 |
84 |
Test 12 |
DELETE without an index |
25 |
74 |
Test 13 |
DELETE with an index |
70 |
72 |
Test 14 | A big INSERT after a big DELETE |
62 |
66 |
Test 15 | A big DELETE followed by many small INSERTs |
33 |
33 |
Test 16 | DROP TABLE: finished |
42 |
40 |
Você pode encontrar mais detalhes sobre os testes nos códigos fonte no arquivo anexado SqLiteTest.zip. Especificações do computador em que foram tomadas as medidas — Windows 10 x64, Intel Xeon E5-2690 v3 @ 2.60GHz.
Os resultados mostram que você pode ter certeza de um desempenho máximo ao trabalhar com bancos de dados em MQL5. Aqueles que nunca encontraram o SQL antes serão agradavelmente surpreendidos, uma vez que muitas tarefas na linguagem de consultas estruturadas são resolvidas de maneira rápida e elegante, sem a necessidade de criar ciclos e amostragens complexas.
Exemplo de consulta simples
Os bancos de dados armazenam informações na forma de tabelas, a aquisição/modificação/adição de novos dados é realizada usando consultas
em linguagem SQL.
Mostramos um exemplo de como criar um banco de dados simples e obter informações dele.
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string filename="company.sqlite"; //--- criamos ou abrimos um banco de dados na pasta compartilhada do terminal 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; } ... trabalho com banco de dados //--- fechamos o banco de dados DatabaseClose(db); }
Criar e fechar um banco de dados é semelhante a trabalhar com arquivos, isto é, também criamos um identificador para o banco de dados,
verificamos e fechamos no final.
Em seguida, verificamos a presença de tabela no banco de dados, assim, se a tabela já existir, quando você tentar inserir os mesmos dados,
como no nosso exemplo, ocorrerá um erro.
//--- se a tabela COMPANY existir, vamos exclui-la if(DatabaseTableExists(db, "COMPANY")) { //--- excluímos a tabela if(!DatabaseExecute(db, "DROP TABLE COMPANY")) { Print("Failed to drop table COMPANY with code ", GetLastError()); DatabaseClose(db); return; } } //--- criamos a tabela COMPANY if(!DatabaseExecute(db, "CREATE TABLE COMPANY(" "ID INT PRIMARY KEY NOT NULL," "NAME TEXT NOT NULL," "AGE INT NOT NULL," "ADDRESS CHAR(50)," "SALARY REAL );")) { Print("DB: ", filename, " create table failed with code ", GetLastError()); DatabaseClose(db); return; }
A exclusão e a criação de uma tabela são feitas com a ajuda de consultas, enquanto você deve sempre verificar o resultado da execução. Na tabela COMPANY, temos apenas 5 campos: ID do registro, nome, idade, endereço e salário. Além disso, o campo ID é uma chave, ou seja, um índice exclusivo. Os índices permitem identificar exclusivamente cada registro e podem ser usados em tabelas diferentes para vinculá-los. É semelhante à maneira como o ID da posição vincula todas as transações e ordens relacionados a uma posição específica.
Agora você precisa preencher a tabela com dados, isso é feito usando a consulta "INSERT":
//--- insere dados na tabela 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; }
Vemos que à tabela COMPANY são adicionados 4 registros, para cada registro, são indicados a ordem dos campos e os valores que serão gravados
nesses campos. Cada registro é inserido por uma consulta separada "INSERT....", e seu conjunto é combinado em uma consulta. Isto quer dizer
que poderíamos inserir cada registro na tabela usando uma chamada separada DatabaseExecute().
Como o banco de dados é salvo em um arquivo company.sqlite após o script concluir seu trabalho, na próxima inicialização do script tentaríamos gravar os mesmos dados na tabela COMPANY com o mesmo ID. O que causaria um erro, e é por isso que primeiro excluímos a tabela para que cada vez que o script seja executado, ele comece do zero.
Agora, obtemos todos os registros da tabela COMPANY, onde o campo SALARY>15000. Isso é feito usando a função DatabasePrepare(),
que compila o texto da consulta e retorna um identificador para uso posterior em DatabaseRead()
ou DatabaseReadBind().
//--- criamos uma consulta obtemos seu identificador 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; }
Depois que a solicitação é criada com sucesso, precisamos obter sua resultados de execução. Fazemos isso usando DatabaseRead(), que, na primeira chamada, executará a consulta e irá para o primeiro registro nos resultados. A cada chamada, ela simplesmente lerá o próximo registro até chegar ao fim. Nesse caso, ela retornará false, o que significa "não há mais registros".
//--- imprimimos todos os registros com salário superior a 15.000 int id, age; string name, address; double salary; Print("Persons with salary > 15000:"); for(int i=0; DatabaseRead(request); i++) { //--- no registro recebido lemos os valores de cada campo 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; } } //--- excluímos a consulta após ser usada DatabaseFinalize(request);
O resultado da execução será:
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.0O exemplo de código completo está no arquivo DatabaseRead.mq5.
Depuração de consultas SQL no MetaEditor
Todas as funções para trabalhar com o banco de dados retornam um código de erro no caso de uma chamada sem êxito. Trabalhar com eles não causa problemas se você seguir simples 4 regras:
- após serem usados todos os identificadores de consultas devem ser destruídos com a ajuda de DatabaseFinalize();
- antes de concluir o trabalho o banco de dados deve ser fechado com a ajuda de DatabaseClose();
- os resultados da consulta devem ser verificados;
- em caso de erro, você deve primeiro destruir a consulta e depois fechar o banco de dados.
O mais difícil é entender qual é o erro se não foi criada a consulta. O MetaEditor permite abrir arquivos *.sqlite e trabalhar com eles usando
consultas SQL. Mostramos como isso é feito usando o arquivo criado company.sqlite como exemplo:
1. Abrimos na pasta compartilhada dos terminais o arquivo company.sqlite.
2. Após abrir o banco de dados, vemos no Navegador a tabela COMPANY e clicamos duas vezes nela.
3. Na barra de status é criada automaticamente a consulta "SELECT * FROM COMPANY".
4. A consulta é executada quer automaticamente quer pressionando a tecla F9 ou o botão Executar.
5. Vemos o resultado da consulta.
6. Se algo der errado, os erros serão mostrados no Diário do Editor.
Usando consultas SQL, você pode obter estatísticas dos campos da tabela, por exemplo, a soma e a média. Colocamos consultas, verificamos que elas funcionem.
Agora podemos transferir essas consultas para o código MQL5:
Print("Some statistics:"); //--- preparamos uma nova consulta sobre a soma de salários 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); } //--- excluímos a consulta após ser usada DatabaseFinalize(request); //--- preparamos uma nova consulta de salário médio 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); } //--- excluímos a consulta após ser usada DatabaseFinalize(request);
E comparamos os resultados da execução:
Some statistics: Total salary=125000.0 Average salary=31250.0
Leitura automática de resultados de consulta na estrutura usando DatabaseReadBind()
A função DatabaseRead() permite percorrer todos os registros a partir do resultado da consulta e obter informações completas sobre cada coluna na tabela resultante:
- DatabaseColumnName — nome,
- DatabaseColumnType — tipo de dados,
- DatabaseColumnSize — tamanho dos dados em bytes,
- DatabaseColumnText — lê como texto,
- DatabaseColumnInteger — obtém valores do tipo int,
- DatabaseColumnLong — obtém valores do tipo long,
- DatabaseColumnDouble — obtém
valores do tipo double,
- DatabaseColumnBlob — obtém matrizes de dados.
Estas funções permitem trabalhar universalmente com os resultados de qualquer consulta, mas à custa de código redundante. Se a estrutura dos
resultados da consulta é conhecida antecipadamente, é melhor usar a função DatabaseReadBind(),
que permite ler imediatamente todo o registro na estrutura. Podemos refazer o exemplo anterior assim, primeiro declaramos a estrutura
Person:
struct Person { int id; string name; int age; string address; double salary; };
Em seguida, subtraímos cada registro dos resultados da consulta usando DatabaseReadBind(request, person):
//--- imprimimos os resultados da consulta recebidos 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); //--- excluímos a consulta após ser usada DatabaseFinalize(request);
Assim, do registro atual obtemos imediatamente os valores de todos os campos e não precisamos lê-los separadamente.
Embrulhamos a aceleração de transações com a ajuda de
DatabaseTransactionBegin()/DatabaseTransactionCommit()
Se, ao trabalhar com uma tabela, for necessário executar comandos INSERT, UPDATE ou DELETE massivos, será melhor fazer isso com a ajuda de transações. Quando uma transação é executada, primeiro é bloqueado o banco de dados (DatabaseTransactionBegin), são executados os comandos de alteração massivos e, em seguida, são salvas estas alterações (DatabaseTransactionCommit) ou são canceladas em caso de erro (DatabaseTransactionRollback)
Na descrição da função DatabasePrepare é exemplificado o uso de transações:
//--- variáveis auxiliares ulong deal_ticket; // ticket da transação long order_ticket; // ticket da ordem com a qual foi concluída a transação long position_ticket; // ID da posição à qual pertence a transação datetime time; // hora em que concluída a transação long type ; // tipo de transação long entry ; // direção da transação string symbol; // símbolo da transação double volume; // volume da operação double price; // preço double profit; // resultado financeiro double swap; // swap double commission; // comissão long magic; // Magic number (ID do EA) long reason; // motivo ou origem da transação //--- percorremos todas as transações e as inserimos no banco de dados bool failed=false; int deals=HistoryDealsTotal(); //--- bloqueamos o banco de dados antes de executar transações 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); //--- inserimos na tabela cada transação através de uma consulta 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; } } //--- verifica se há erros ao executar as transações if(failed) { //--- revertemos todas as transações e desbloqueamos o banco de dados DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError()); return(false); } //--- todas as transações foram bem-sucedidas, por isso, fixamos as alterações e desbloqueamos o banco de dados DatabaseTransactionCommit(database);
Com o uso de transações, você pode acelerar operações maciças para alterar tabelas centenas de vezes, como mostra o exemplo DatabaseTransactionBegin:
Resultado: O histórico de negociação inclui 2737 transações Transations WITH DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds Использование DatabaseTransactionBegin/DatabaseTransactionCommit дало ускорение в 532.8 раз
Trabalho com transações do histórico de negociação
O poder das consultas SQL está em que você pode facilmente classificar, selecionar e modificar os dados de origem sem escrever código. Vamos continuar analisando o exemplo da descrição da função DatabasePrepare, uma vez que mostra como a partir de transações com uma consulta obter trades. O trade contém informações sobre as datas de entrada e saída da posição, preços de entrada e saída, símbolo, direção e volume. Se você olhar para a estrutura do trade, é possível ver que as transações de entrada e saída estão vinculadas por um identificador de posição comum. Assim, se tivermos um sistema de negociação simples em uma conta com registro de posições de cobertura para uma posição, podemos facilmente combinar duas transações em um trade. Isso é feito usando esta consulta:
//--- preenchemos a tabela TRADES através da consulta SQL com base nos dados de DEALS ulong start=GetMicrosecondCount(); if(DatabaseTableExists(db, "DEALS")) { //--- preenchemos a tabela TRADES 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;
Aqui é tomada a tabela existente DEALS e com a ajuda da junção interna através de INNER JOIN são criados os registros das transações com o mesmo
DEAL_POSITION_ID. O resultado do exemplo a partir de DatabasePrepare
na conta de negociação:
Resultado: O histórico de negociação inclui 2741 transações Primeiras 10 transações: [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 Primeiros 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 O preenchimento da tabela TRADES tardou 12,51 milissegundos
Execute esse script na sua conta de negociação com registro de cobertura e compare os resultados com as posições no histórico. Se antes você não
tinha conhecimento ou tempo suficiente para codificar loops para obter esse resultado, agora pode fazê-lo com uma consulta SQL. O
resultado do script pode ser visualizado no MetaEditor, para isso, abra o arquivo trades.sqlite anexado.
Análise de portfólio por estratégias
No script acima, com base nos resultados do script a partir de DatabasePrepare fica claro que a negociação é realizada usando vários pares de moedas. Mas também, no mesmo lugar, vemos valores de 100 a 600 na coluna [magic]. Isso significa que na conta de negociação estão sendo realizadas várias estratégias para operar, cada uma com seu próprio número mágico para identificar suas transações.
Com a ajuda da consulta SQL, podemos fazer a análise da negociação no contexto dos valores do magic:
//--- conseguimos estatísticas de negociação detalhadas no contexto dos EAs de acordo como o 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");
Resultado:
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
Vemos que 4 das 6 estratégias mostram lucro. E para cada uma das estratégias, temos indicadores estatísticos:
- trades — número de trades por estratégia,
- gross_profit — lucro total da estratégia (soma de todos os valores positivos profit),
- gross_loss — perda total da estratégia (soma de todos os valores negativos profit),
- total_commission — soma de todas as comissões dos trades da estratégia,
- total_swap — soma de todos os swaps dos trades da estratégia,
- total_profit — soma de gross_profit e gross_loss,
- net_profit — soma (gross_profit + gross_loss + total_commission + total_swap),
- win_trades — número de trades, onde profit>0,
- loss_trades— número de trades, onde profit<0,
- expected_payoff — expectância de trade excluindo swaps e comissões = net_profit/trades,
- win_percent — porcentagem de trades vencedores,
- loss_percent — porcentagem de trades perdedores,
- average_profit — vitória média = gross_profit/win_trades,
- average_loss — perda média = gross_loss /loss_trades,
- profit_factor — Fator de lucro = gross_profit/gross_loss.
Estas estatísticas para calcular lucro e perdas não levam em consideração swaps e comissões acumuladas na posição. Isso permite que você veja
esses custos na forma pura. Pode acontecer que a própria estratégia dê um pequeno lucro, mas, devido a swaps e comissões, acaba sendo inútil.
Análise de transações por símbolos
Podemos realizar análises da negociação no contexto de símbolos. Para fazer isso, fazemos esta consulta:
//--- obtemos estatísticas de negociação por símbolos 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");
Resultado:
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
As estatísticas nos dizem que apenas 5 em cada 10 símbolos obtiveram lucro líquido (net_profit>0), embora 6 em 10 do fator de lucro tenha
sido positivo (profit_factor>1). É exatamente esse o caso quando swaps e comissões fazem a estratégia perder no EURJPY.
Análise de transações por horas de entrada
Mesmo que a negociação seja realizada por uma estratégia em um símbolo, pode ser útil a análise segundo as horas de entrada no mercado. Isso é feito com a seguinte consulta SQL:
//--- obtemos estatísticas de negociação no contexto da hora de entrada no mercado 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");
Resultado:
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
Vê-se claramente que o maior número de negociações é realizado no intervalo de 9 a 16 horas, inclusive. Negociar durante outras horas oferece
menos trades e é basicamente inútil. O código fonte completo com esses 3 tipos de consultas pode ser encontrado no exemplo da função DatabaseExecute().
Saída de dados conveniente para o log do Expert Advisor em DatabasePrint()
Nos exemplos anteriores, para gerar os resultados da consulta, tivemos que ler cada registro na estrutura e imprimir os registros um de cada
vez. Criar uma estrutura apenas para observar os valores de uma tabela ou resultado da consulta nem sempre é conveniente. Portanto,
especialmente para tais fins, é adicionada a função DatabasePrint():
long DatabasePrint( int database, // identificador de banco de dados recebido em DatabaseOpen string table_or_sql, // tabela ou consulta SQL uint flags // combinação de sinalizadores );
Com ele, você pode imprimir facilmente não apenas uma tabela existente, mas também os resultados da execução da consulta, que podem ser apresentados na forma de uma tabela. Por exemplo, exibimos os valores da tabela DEALS usando a consulta:
DatabasePrint(db,"SELECT * from DEALS",0);
Resultado (são mostradas as primeiras 10 linhas da tabela):
#| 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
Importação e exportação de dados
Também para facilitar a importação e exportação de dados foram adicionadas as funções DatabaseImport() e DatabaseExport(). Essas funções permitem trabalhar com arquivos CSV e dados gravados em um arquivo ZIP.
DatabaseImport() importa dados para a tabela especificada; se a tabela com o nome especificado não existir, ela será criada automaticamente. Os nomes e o tipo de campos na tabela criada serão reconhecidos automaticamente com base nos dados contidos no arquivo.
DatabaseExport() permite salvar em um arquivo uma tabela ou os resultados de uma consulta. Se os resultados da consulta forem exportados, a consulta SQL
deverá começar com "SELECT" ou "select". Em outras palavras, a consulta SQL não pode alterar o estado do banco de dados, caso contrário
DatabaseExport() falhará.
Veja a descrição completa das funções na Documentação MQL5.
Salvando resultados de otimização em um banco de dados
As funções para trabalhar com bancos de dados também podem ser usadas para processar resultados de otimização. Exemplificaremos por meio do Expert Advisor "Amostra MACD" da entrega padrão como obter resultados de teste usando quadros e, em seguida, salvaremos os valores de todos os critérios de otimização em um arquivo. Para fazer isso, criamos a classe CDatabaseFrames, na qual definimos o método OnTester() para enviar estatísticas de negociação:
//+------------------------------------------------------------------+ //| Tester function - envia no quadro estatísticas de negociação | //+------------------------------------------------------------------+ void CDatabaseFrames::OnTester(const double OnTesterValue) { //--- matriz stats[] para enviar dados para o quadro double stats[16]; //--- para maior clareza colocamos em variáveis separadas as estatísticas sobre trades int trades=(int)TesterStatistics(STAT_TRADES); double win_trades_percent=0; if(trades>0) win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades; //--- preenchemos a matriz com os resultados do teste stats[0]=trades; // número de trades stats[1]=win_trades_percent; // porcentagem de trades lucrativos stats[2]=TesterStatistics(STAT_PROFIT); // lucro líquido stats[3]=TesterStatistics(STAT_GROSS_PROFIT); // lucro total stats[4]=TesterStatistics(STAT_GROSS_LOSS); // perda total stats[5]=TesterStatistics(STAT_SHARPE_RATIO); // índice de Sharpe stats[6]=TesterStatistics(STAT_PROFIT_FACTOR); // fator de lucro stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR); // fator de recuperação stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF); // expectância de trade stats[9]=OnTesterValue; // critério personalizado de otimização //--- calculamos os critérios de otimização padrão embutidos 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); //--- adicionamos os valores dos critérios de otimização embutidos 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 //--- criamos um quadro com dados e o enviamos para o terminal if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats)) Print("Frame add error: ", GetLastError()); else Print("Frame added, Ok"); }
O segundo método importante dessa classe é OnTesterDeinit(), no final da otimização, ele lê todos os quadros recebidos e salva as estatísticas no banco de dados:
//+------------------------------------------------------------------+ //| TesterDeinit function - lê dados dos quadro | //+------------------------------------------------------------------+ void CDatabaseFrames::OnTesterDeinit(void) { //--- tomamos o nome do EA e o hora final da otimização string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite"; StringReplace(filename, ":", "."); // o caractere ":" não é permitido nos nomes de arquivo //--- abrimos/criamos o banco de dados na pasta do terminal compartilhado 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"); //--- criamos uma tabela PASSES 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; } //--- variáveis para leitura de quadros string name; ulong pass; long id; double value; double stats[]; //--- movemos o ponteiro do quadro para o início FrameFirst(); FrameFilter("", STATS_FRAME); // selecionamos para trabalhar os quadros contendo estatísticas de negociação //--- variáveis para obter estatísticas do quadro int trades; double win_trades_percent; double profit, gross_profit, gross_loss; double sharpe_ratio, profit_factor, recovery_factor, expected_payoff; double ontester_value; // critério de otimização personalizado 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 //--- bloqueamos o banco de dados durante transações em massa DatabaseTransactionBegin(db); //--- percorremos os quadros e lemos os dados a partir deles 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); //--- gravamos os dados em uma tabela 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); //--- realizamos a consulta para adicionar passagens à tabela PASSES if(!DatabaseExecute(db, request)) { PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError()); failed=true; break; } } //--- se ocorrer um erro ao executar a transação, informamos e concluímos o trabalho if(failed) { Print("Transaction failed, error code=", GetLastError()); DatabaseTransactionRollback(db); DatabaseClose(db); return; } else { DatabaseTransactionCommit(db); Print("Transaction done successful"); } //--- fechamos o banco de dados if(db!=INVALID_HANDLE) { Print("Close database with handle=", db); DatabaseClose(db); }
Далее в советнике "MACD Sample" подключаем файл DatabaseFrames.mqh и объявляем переменную класса 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;
Em seguida, adicionamos ao final do EA três funções que serão chamadas apenas durante a otimização:
//+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ int OnTesterInit() { return(DB_Frames.OnTesterInit()); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { DB_Frames.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { double ret=0; //--- создадим пользовательский критерий оптимизации как отношение чистой прибыли к относительной просадке по балансу if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0) ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT); DB_Frames.OnTester(ret); return(ret); } //+------------------------------------------------------------------+
Iniciamos a otimização e obtemos na pasta compartilhada de terminais um arquivo de banco de dados com estatísticas de negociação:
CDatabaseFrames::OnTesterInit: otimização iniciada em 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'
O arquivo de banco de dados criado pode ser aberto no MetaEditor ou usado em outro programa MQL5 para trabalhar mais para frente.
Assim, você pode preparar todos os dados da forma correta para análises adicionais ou intercâmbio com outros traders. O código fonte, o arquivo
ini com os parâmetros de otimização e o resultado da execução podem ser encontrados no arquivo MACD.zip anexo.
Otimizando a execução de consultas com a ajuda de índices
A beleza de usar SQL (em todas as suas implementações, não apenas SQLite) é que é uma linguagem declarativa, não uma linguagem processual.
Ao programar em SQL, você diz ao sistema O QUE deseja calcular, não COMO calculá-lo. A tarefa de "como fazer isso" é delegada ao subsistema do
planejador de consultas no núcleo do banco de dados SQL.
Para executar uma consulta SQL específica, podem ser usados centenas e
milhares de algoritmos de operação. Todos esses algoritmos darão a resposta correta, embora alguns funcionem mais rápido que outros. O
planejador de consultas no SQLite tenta selecionar o algoritmo mais rápido e eficiente para cada instrução SQL.
Normalmente, o planejador de consultas no SQLite faz um bom trabalho ao escolher o algoritmo correto. No entanto, para um melhor desempenho, o planejador de consultas precisa de índices. E estes índices geralmente devem ser adicionados pelos programadores. Mas, às vezes, o planejador de consultas escolhe um algoritmo não ideal. Em tais casos, o programador pode fornecer dicas adicionais para ajudar o planejador de consultas a fazer seu trabalho melhor.
Pesquisa sem índices
Suponha que tenhamos uma tabela DEALS com transações que contenham os 14 campos especificados. Aqui estão as 10 primeiras entradas nesta tabela.
rowid |
ID | ORDER_ID | POSITION_ID | TIME | TYPE | ENTRY | SYMBOL | VOLUME | PRICE | PROFIT | SWAP | COMMISSION | MAGIC | REASON |
1 | 34429573 | 0 | 0 | 1567723199 | 2 | 0 | 0 | 0 | 2000 | 0 | 0 | 0 | 0 | |
2 | 34432127 | 51447238 | 51447238 | 1567749603 | 0 | 0 | USDCAD | 0.1 | 1.3232 | 0 | 0 | -0.16 | 500 | 3 |
3 | 34432128 | 51447239 | 51447239 | 1567749603 | 1 | 0 | USDCHF | 0.1 | 0.98697 | 0 | 0 | -0.16 | 500 | 3 |
4 | 34432450 | 51447565 | 51447565 | 1567753200 | 0 | 0 | EURUSD | 0.1 | 1.10348 | 0 | 0 | -0.18 | 400 | 3 |
5 | 34432456 | 51447571 | 51447571 | 1567753200 | 1 | 0 | AUDUSD | 0.1 | 0.68203 | 0 | 0 | -0.11 | 400 | 3 |
6 | 34432879 | 51448053 | 51448053 | 1567756800 | 1 | 0 | USDCHF | 0.1 | 0.98701 | 0 | 0 | -0.16 | 600 | 3 |
7 | 34432888 | 51448064 | 51448064 | 1567756800 | 0 | 0 | USDJPY | 0.1 | 106.962 | 0 | 0 | -0.16 | 600 | 3 |
8 | 34435147 | 51450470 | 51450470 | 1567765800 | 1 | 0 | EURUSD | 0.1 | 1.10399 | 0 | 0 | -0.18 | 100 | 3 |
9 | 34435152 | 51450476 | 51450476 | 1567765800 | 0 | 0 | GBPUSD | 0.1 | 1.23038 | 0 | 0 | -0.2 | 100 | 3 |
10 | 34435154 | 51450479 | 51450479 | 1567765800 | 1 | 0 | EURJPY | 0.1 | 118.12 | 0 | 0 | -0.18 | 200 | 3 |
Ela contém informações da seção Propriedades
das transações (exceto DEAL_TIME_MSC, DEAL_COMMENT e DEAL_EXTERNAL_ID), necessárias para a análise do histórico de negociação.
Além das informações mais armazenadas, cada tabela sempre possui uma chave inteira rowid seguida pelos campos de entrada. Valores
da chave rowid são criados automaticamente, são exclusivos na tabela e aumentam quando são adicionadas novas entradas. Ao excluir
registros, podem ocorrer lacunas na numeração, mas as linhas da tabela são sempre armazenadas em ordem crescente rowid .
Se precisarmos encontrar transações relacionados a uma posição específica, por exemplo, ID=51447571, escreveremos uma consulta assim:
SELECT * FROM deals WHERE position_id=51447571
Nesse caso, será executada uma verificação completa de tabela - todas as linhas serão varridas e em cada linha será verificado se o campo POSITION_ID é igual ao valor 51447571. Linhas que atendem a essa condição serão emitidas nos resultados da consulta. Se a tabela contiver milhões ou dezenas de milhões de registros, a pesquisa poderá levar um tempo substancial. Se fizessemos uma pesquisa não pela condição position_id=51447571, mas, sim, pela condição rowid=5, o tempo de pesquisa seria reduzido em milhares ou até milhões de vezes (dependendo do tamanho da tabela).
SELECT * FROM deals WHERE rowid=5
O resultado da consulta seria o mesmo, pois a linha contendo rowid=5 armazena position_id = 51447571. A aceleração é alcançada graças a que
os valores rowid são classificados em ordem crescente e a que para obter o resultado é usada uma pesquisa binária (ou binária). Mas,
infelizmente, a pesquisa por valor rowid não nos convém, porque estamos interessados em registros com o valor position_id
desejado.
Pesquisa por índice
Para que a solicitação seja executada também eficientemente em termos de tempo, você precisa adicionar o índice no campo POSITION_ID usando a consulta:
CREATE INDEX Idx1 ON deals(position_id)
Neste caso, será criada uma tabela separada a partir de duas colunas, a primeira consistindo em valores POSITION_ID, classificados em ordem crescente, e a segunda, em rowid.
POSITION_ID | rowid |
0 | 1 |
51447238 | 2 |
51447239 | 3 |
51447565 | 4 |
51447571 | 5 |
51448053 | 6 |
51448064 | 7 |
51450470 | 8 |
51450476 | 9 |
51450479 | 10 |
Neste caso, a ordem rowid já pode ser violada, embora em nosso exemplo seja mantida. Porque ao abrir uma posição segundo o
tempo, POSITION_ID também aumenta.
Agora que temos um índice do campo POSITION_ID, nossa consulta
SELECT * FROM deals WHERE position_id=51447571
será realizada de maneira diferente. Primeiro, será realizada uma pesquisa binária no índice Idx1 segundo a coluna POSITION_ID e serão
encontradas todas as condições correspondentes rowid. E, em seguida, uma segunda pesquisa binária na tabela DEALS original
selecionará todos os registros por valores rowid conhecidos. Assim, agora, em vez de uma varredura completa de tabela grande,
ocorrem duas pesquisas consecutivas, primeiro pelo índice e depois pelos números das linhas da tabela. Isso permite reduzir o tempo de
execução dessas consultas em milhares ou mais vezes com um grande número de linhas na tabela.
Regra geral: se alguns campos da tabela são frequentemente usados para pesquisa/comparação/classificação, é
recomendável criar índices nesses campos.
Nesta tabela DEALS, também existem campos como SYMBOL, MAGIC (identificador de EA) e ENTRY (direção de entrada). Se você precisar fazer seleções nesses campos, é aconselhável com base neles criar os respectivos índices. Por exemplo:
CREATE INDEX Idx2 ON deals(symbol) CREATE INDEX Idx3 ON deals(magic) CREATE INDEX Idx4 ON deals(entry)
Durante isso, deve-se ter em mente que a criação de índices requer memória adicional e que a cada adição/exclusão de registros ocorre reindexação. Você também pode criar vários índices com base em vários campos. Por exemplo, se precisarmos selecionar todas as transações que foram concluídas pelo EA com MAGIC= 500 no símbolo USDCAD, podemos criar tal consulta:
SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'
Neste caso, você pode criar um índice múltiplo de acordo com os campos MAGIC e SYMBOL
CREATE INDEX Idx5 ON deals(magic, symbol)
e, então, será criada uma tabela de índice desse tipo (as 10 primeiras linhas são mostradas esquematicamente)
MAGIC | SYMBOL | rowid |
100 | EURUSD | 4 |
100 | EURUSD | 10 |
100 | EURUSD | 20 |
100 | GBPUSD | 5 |
100 | GBPUSD | 11 |
200 | EURJPY | 6 |
200 | EURJPY | 12 |
200 | EURJPY | 22 |
200 | GBPJPY | 7 |
200 | GBPJPY | 13 |
No multi-índice criado, os registros são classificados primeiro em blocos pelo MAGIC, depois pelo campo SYMBOL. Por isso, com as consultas AND, a pesquisa no índice passará primeiro pela primeira coluna MAGIC e, em seguida, será verificado o valor da segunda coluna SYMBOL. Se forem atendidas ambas as condições, rowid é adicionado ao conjunto de resultados, que já será pesquisado na tabela de origem. De um modo geral, esse multi-índice não é mais adequado para consultas em que SYMBOL é verificado primeiro e depois MAGIC
SELECT * FROM deals WHERE symbol='USDCAD' AND magic=500
Embora o planejador de consultas, nesses casos, entenda como agir corretamente e realize a pesquisa na ordem correta. Ainda assim, vale
lembrar, pois você não pode confiar no fato de que os erros de design de tabelas e consultas serão corrigidos automaticamente pelo
agendador.
Consultas OR
Os multi-índices são adequados apenas para consultas AND. Por exemplo, queremos encontrar todas as transações que foram feitas por um EA
com MAGIC=100 ou pelo símbolo EURUSD:
SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'
Neste caso, serão feitas duas pesquisas separadas e o rowid encontrado será combinado em uma amostragem comum para a pesquisa final por números
de linhas na tabela de origem.
SELECT * FROM deals WHERE magic=100 SELECT * FROM deals WHERE symbol='EURUSD'
Mas, mesmo nesse caso, é necessário que ambos os campos da consulta OR tenham índices, pois, caso contrário, a pesquisa causará uma
verificação completa da tabela.
Classificação
Para acelerar a classificação, também é recomendável ter um índice dos campos pelos quais são ordenados os resultados da consulta. Por
exemplo, você precisa selecionar todas as transações segundo o EURUSD, classificadas pela hora de realização da transação:
SELECT * FROM deals symbol='EURUSD' ORDER BY time
Neste caso, você deve considerar a criação de um índice de acordo com o campo TIME. A necessidade de usar índices depende do tamanho das tabelas;
se o número de registros na tabela for pequeno, a indexação não proporcionará um ganho notável no tempo.
Aqui examinamos apenas os princípios básicos da otimização de consultas. Para um melhor entendimento, recomendamos iniciar o estudo deste
tópico na seção Query
Planning no site do desenvolvedor SQLite.
Integração com bancos de dados no MetaEditor
O desenvolvimento da plataforma MetaTrader 5 continua. Adicionamos suporte nativo para consultas SQL na linguagem MQL5 e incorporamos
no MetaEditor novas funcionalidades para trabalhar com bancos de dados - criação de banco de dados, inserção e exclusão de dados e
realização de transações em massa. A criação de um banco de dados é feita por padrão usando o Assistente MQL5. Basta especificar o nome do
arquivo, o nome da tabela e adicionar todos os campos necessários, indicando o tipo.
Em seguida, você pode preencher a tabela com dados, fazer uma pesquisa e amostragem, inserir consultas SQL, etc. Assim, você pode trabalhar com bancos de dados não apenas a partir dos programas MQL5, mas também manualmente, para fazer isso, você não precisa recorrer a navegadores de terceiros.
A introdução do SQLite no MetaTrader abre novas oportunidades para os traders processarem grandes quantidades de dados de forma programática e manual. Além disso, tentamos garantir que essas funções fossem mais convenientes de usar e não tivessem velocidade inferior a outras soluções. Aprenda e aplique a linguagem de consulta SQL no seu trabalho.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7463





- Aplicativos de negociação gratuitos
- VPS Forex grátis por 24 horas
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso