SQLite: trabalho nativo com bancos de dados SQL em MQL5

20 fevereiro 2020, 13:41
MetaQuotes
0
1 287

Sumário


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

O 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:

  1. após serem usados todos os identificadores de consultas devem ser destruídos com a ajuda de DatabaseFinalize();
  2. antes de concluir o trabalho o banco de dados deve ser fechado com a ajuda de DatabaseClose();
  3. os resultados da consulta devem ser verificados;
  4. 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.

Trabalhando com consultas no MetaEditor

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:

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.

Trabalhando com um banco de dados no MetaEditor

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.

Criando um banco de dados no Assistente MQL

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 Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7463

Arquivos anexados |
SqLiteTest.zip (2708.45 KB)
trades.sqlite (340 KB)
MACD.zip (8.27 KB)
DatabaseRead.mq5 (10.11 KB)
DatabasePrepare.mq5 (35.02 KB)
DatabaseExecute.mq5 (64.83 KB)
Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIV): classe básica de negociação, correção automática de parâmetros errados Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIV): classe básica de negociação, correção automática de parâmetros errados

No artigo, analisaremos um manipulador de parâmetros errôneos de uma ordem de negociação, finalizaremos a classe básica de negociação e também corrigiremos o funcionamento da classe de eventos de negociação - agora todos os eventos de negociação serão detectados corretamente nos programas.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIII): classe básica de negociação, controle de parâmetros válidos Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIII): classe básica de negociação, controle de parâmetros válidos

Neste artigo, continuaremos a acompanhar o desenvolvimento da classe de negociação, criaremos um controle que encontre valores incorretos nos parâmetros da ordem de negociação e sonorizaremos eventos de negociação.

Otimização Walk Forward Contínua (Parte 2): Mecanismo para a criação de um relatório de otimização para qualquer robô Otimização Walk Forward Contínua (Parte 2): Mecanismo para a criação de um relatório de otimização para qualquer robô

O primeiro artigo da série Otimização Walk Forward descreveu a criação de uma DLL a ser usada em nosso otimizador automático. Essa continuação é inteiramente dedicada à linguagem MQL5.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXV): processamento de erros retornados pelo servidor de negociação Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXV): processamento de erros retornados pelo servidor de negociação

Depois de enviarmos uma ordem de negociação para o servidor, não devemos assumir que o trabalho está concluído, uma vez que é necessário verificar quer os códigos de erro quer a ausência de erros. No artigo, veremos o processamento de erros retornados pelo servidor de negociação e prepararemos a base para a criação de ordens de negociação pendentes.