English Русский 中文 Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 17): Preparação adicional para o trading real

Desenvolvendo um EA multimoeda (Parte 17): Preparação adicional para o trading real

MetaTrader 5Testador | 2 dezembro 2024, 16:37
178 0
Yuriy Bykov
Yuriy Bykov

Introdução

Em um dos artigos anteriores, já havíamos voltado nossa atenção para as melhorias necessárias no EA para operar em contas reais. Até então, nossos esforços estavam concentrados principalmente em obter resultados aceitáveis do EA no testador de estratégias. Contudo, o trabalho para avançar para a operação real não foi concluído. Ainda há muito a ser feito.

Além de restaurar o funcionamento do EA após reiniciar o terminal, a possibilidade de usar nomes ligeiramente diferentes para os instrumentos de trading e a finalização automática das operações ao atingir indicadores definidos, enfrentamos outro desafio: para formar a string de inicialização, utilizamos informações obtidas diretamente do banco de dados, onde são armazenados todos os resultados das otimizações das instâncias de estratégias de trading e seus grupos.

Para iniciar o EA, é necessário que o arquivo do banco de dados esteja na pasta compartilhada dos terminais. O tamanho do banco de dados já ultrapassa vários gigabytes e continuará crescendo. Portanto, incorporar o banco de dados como parte integrante do EA não é racional, pois para a inicialização é necessária apenas uma pequena parte das informações armazenadas. Assim, é necessário implementar um mecanismo para extrair e utilizar essas informações no EA.


Traçando o caminho 

Relembrando, analisamos e implementamos a automação de duas etapas de teste. Na primeira, os parâmetros de uma instância individual da estratégia de trading são otimizados (discutido na Parte 11). O modelo de estratégia de trading analisado usa apenas um instrumento de trading (símbolo) e um timeframe. Consequentemente, executamos a estratégia sequencialmente no otimizador, alterando os símbolos e os timeframes. Para cada combinação de símbolo e faixa de tempo, a otimização é realizada sequencialmente com diferentes critérios. Todos os resultados das execuções de otimização foram gravados na tabela passes de nosso banco de dados.

Na segunda etapa, otimizamos a seleção de grupos de conjuntos de parâmetros obtidos na primeira etapa, que apresentam os melhores resultados quando trabalham juntos (discutido nas Partes 6 e 13). Assim como na primeira etapa, incluímos nos grupos conjuntos de parâmetros que utilizavam a mesma combinação de símbolo e timeframe. As informações sobre os resultados de todos os grupos testados durante a otimização também foram armazenadas em nosso banco de dados.

Na terceira etapa, não utilizamos o otimizador padrão do testador de estratégias, portanto, sua automação não foi abordada. A terceira etapa consistiu em selecionar o melhor grupo encontrado na segunda etapa para cada combinação de símbolo e timeframe disponível. Optamos por otimizar três símbolos (EURGBP, EURUSD e GBPUSD) e três timeframes (H1, M30 e M15). Assim, o resultado da terceira etapa seria nove grupos selecionados. Contudo, para simplificar e acelerar os cálculos no testador, nos restringimos, nos últimos artigos, apenas aos três melhores grupos (com três símbolos diferentes e timeframe H1).

O resultado da terceira etapa foi um conjunto de identificadores de linhas a partir da tabela passes, que passávamos como parâmetro de entrada para o nosso EA final, SimpleVolumesExpert.mq5:

input string     passes_ = "734469,"
                           "736121,"
                           "776928";    // - Comma-separated pass IDs

Antes de iniciar o teste do EA, poderíamos modificar este parâmetro, se desejássemos. Assim, seria possível executar o EA final com qualquer subconjunto de grupos desejado dentre os disponíveis na tabela passes do banco de dados. Mais precisamente, não qualquer subconjunto, mas apenas aqueles cuja representação como string, separada por vírgulas, não excedesse 247 caracteres. Esse é um limite imposto pela linguagem MQL5 para valores de parâmetros de entrada do tipo string. Segundo a documentação, o comprimento máximo do valor de um parâmetro string pode variar entre 191 e 253 caracteres, dependendo do comprimento do nome do parâmetro.

Portanto, se quisermos incluir mais de aproximadamente 40 grupos, não será possível fazer isso dessa forma. Será necessário, por exemplo, transformar a variável passes_ de um parâmetro de entrada string para uma variável string comum, removendo a palavra input do código. Nesse caso, poderíamos definir o conjunto necessário de grupos apenas no código-fonte. Contudo, no momento, não há necessidade de usar conjuntos tão grandes. Além disso, como demonstrado na Parte 5, é mais vantajoso não formar um único grupo com um grande número de instâncias individuais de estratégias de trading ou grupos de estratégias de trading. É mais eficiente dividir a quantidade inicial de instâncias individuais em vários subgrupos e formar um número reduzido de novos grupos a partir deles. Esses novos grupos podem ser, então, reunidos em um único grupo final ou submetidos a um novo processo de agrupamento em subgrupos. Assim, em cada nível de união, será necessário incluir um número relativamente pequeno de estratégias ou grupos em um grupo.

Quando o EA tem acesso ao banco de dados com os resultados de todas as execuções de otimização, basta passar uma lista de identificadores das execuções desejadas como parâmetro de entrada. O EA obtém do banco de dados as strings de inicialização das estratégias de trading que participaram dessas execuções. Com as strings de inicialização obtidas, ele constrói a string de inicialização do objeto "expert", que inclui todas as estratégias de trading listadas. Esse "expert" realiza operações utilizando todas as instâncias de estratégias de trading que o compõem.

Na ausência de acesso ao banco de dados, o EA ainda precisa de uma forma de gerar a string de inicialização do objeto "expert", contendo o conjunto necessário de instâncias individuais ou grupos de estratégias de trading. Uma possibilidade é salvar a string em um arquivo e fornecer ao EA o nome do arquivo de onde ela será carregada. Outra possibilidade é inserir o conteúdo da string de inicialização diretamente no código-fonte do EA, utilizando um arquivo de biblioteca adicional .mqh. É possível, ainda, combinar esses dois métodos: salvar a string de inicialização em um arquivo e, em seguida, importá-lo utilizando as ferramentas de importação de arquivos no editor MetaEditor (Edit → Insert → File).

No entanto, se quisermos permitir que um único EA trabalhe com diferentes grupos selecionados, escolhendo o desejado como parâmetro de entrada, esse método rapidamente mostrará sua limitada escalabilidade. Será necessário realizar muitas tarefas manuais e repetitivas. Por isso, reformularemos o objetivo: queremos criar uma biblioteca de boas strings de inicialização, permitindo escolher uma delas para cada execução do EA. Essa biblioteca deve ser integrada ao EA, eliminando a necessidade de carregar um arquivo adicional junto com ele.

Com base nisso, o trabalho pode ser dividido nas seguintes etapas:

  • Seleção e salvamento: nesta etapa, devemos desenvolver uma ferramenta que permita selecionar grupos e salvar suas strings de inicialização para uso futuro. Também seria útil incluir informações adicionais sobre os grupos escolhidos (nome, descrição breve, composição aproximada, data de criação, etc.).

  • Criação da biblioteca: a partir dos grupos selecionados na etapa anterior, será feita a escolha final daqueles que serão usados na biblioteca para uma versão específica do EA. Será gerado um arquivo incluível contendo todas as informações necessárias.

  • Desenvolvimento do EA final: ao modificar o EA da etapa anterior, transformaremos o código em um novo EA final que usará a biblioteca criada. Esse EA não precisará mais acessar o banco de dados de otimização, pois todas as informações sobre os grupos de estratégias de trading estarão embutidas.

Vamos começar a implementação.


Revisando o trabalho anterior

Esses passos refletem a estrutura da implementação da Etapa 8, apresentada na Parte 9. Relembramos que, naquele artigo, listamos um conjunto de etapas que poderiam resultar em um EA com bom desempenho em trading. A Etapa 8 sugeria que as melhores combinações de grupos de estratégias, símbolos, timeframes e outros parâmetros fossem reunidas em um EA final. Entretanto, ainda não detalhamos como selecionar os melhores grupos.

Por um lado, a resposta pode ser simples: selecionar os melhores resultados com base em algum critério, como lucro total, índice de Sharpe ou retorno médio anual normalizado. Por outro lado, a resposta pode ser mais complexa. Por exemplo, será que resultados melhores podem ser alcançados nos testes ao selecionar grupos com base em um critério mais abrangente? Ou será que alguns grupos "melhores" não devem ser incluídos no EA final, pois sua inclusão pode prejudicar os resultados globais? Esse tópico provavelmente exigirá uma análise detalhada.

Outra questão que também exigirá estudo separado é a divisão ideal de grupos em subgrupos com a normalização apropriada. Na parte 5, abordamos esse tema antes mesmo de implementar qualquer automação dos estágios de teste. Naquela ocasião, selecionamos manualmente nove instâncias individuais de estratégias de trading, divididas em três por instrumento de trading (símbolo).

Descobrimos que os resultados nos testes foram ligeiramente melhores quando criamos inicialmente três grupos normalizados com três estratégias cada, para cada símbolo, e posteriormente os unimos em um único grupo normalizado final, em comparação à união direta das nove instâncias individuais em um único grupo normalizado final. No entanto, não podemos afirmar com certeza que essa abordagem é a mais eficiente. Será que esse método seria mais vantajoso para outras estratégias de trading do que a simples união em um único grupo? Essa questão, portanto, abre espaço para investigações adicionais.

Felizmente, essas duas questões podem ser postergadas para um momento mais oportuno. Para explorá-las, no entanto, precisaríamos de ferramentas auxiliares que ainda não foram implementadas. Sem elas, o trabalho seria muito menos eficiente e demandaria muito mais tempo.


Seleção e salvamento de grupos

À primeira vista, parece que já temos tudo o que é necessário. Usamos o EA existente SimpleVolumesExpert.mq5  e indicamos, no parâmetro de entrada passes_, os identificadores das execuções separados por vírgulas. Iniciamos uma única execução no testador e gravamos a string de inicialização necessária no banco de dados. No entanto, falta uma informação adicional. Descobrimos que as informações sobre a execução não são salvas no banco de dados.

Isso ocorre porque temos uma rotina de exportação para o banco de dados apenas dos resultados das execuções de otimização, mas não dos resultados de execuções individuais. Lembremos que a exportação é feita dentro do método CTesterHandler::ProcessFrames(), que é chamado no nível superior pelo manipulador OnTesterPass():

//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Open the database
   DB::Connect();

// Variables for reading data from frames
   ...

// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);
      
      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                           s_idTask, pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Add it to the SQL query array
      APPEND(queries, query);
   }

// Execute all requests
   DB::ExecuteTransaction(queries);

// Close the database
   DB::Close();
}

Ao iniciar uma execução individual, esse manipulador não é chamado, pois a modelagem de eventos não prevê essa execução para esse tipo de evento. Ele é invocado apenas quando o EA está configurado para operar no modo de coleta de quadros de dados (frames). O EA é iniciado nesse modo automaticamente no início da otimização, mas isso não acontece nas execuções individuais. Como resultado, a implementação atual não salva no banco de dados informações sobre as execuções individuais.

Uma solução seria manter as coisas como estão e criar um EA que precisaria ser executado em modo de otimização com base em algum parâmetro irrelevante. O objetivo seria obter os resultados da primeira execução e interromper a otimização em seguida. Dessa forma, os resultados da execução seriam salvos no banco de dados. Contudo, essa abordagem parece inadequada, por isso seguiremos por outro caminho.

Durante a execução individual no EA, ao final dessa execução, o manipulador OnTester() será sempre chamado. Assim, o código de salvamento dos resultados de uma execução individual deve ser inserido diretamente nesse manipulador ou em algum dos métodos por ele chamados. O local mais apropriado para essa inserção parece ser o método CTesterHandler::Tester(). Entretanto, é importante lembrar que esse método também será chamado ao término de uma execução de otimização. Atualmente, esse método já contém o código responsável por gerar e enviar os resultados da execução de otimização por meio do mecanismo de quadros de dados.

Ao executar uma passagem individual, os dados do frame ainda são gerados, mas o próprio frame não pode ser utilizado, mesmo que tenha sido criado. Se tentarmos usar a função FrameNext() após criar um frame com a função FrameAdd() no EA em execução individual, a função FrameNext() não irá ler o frame criado. Ela se comportará como se nenhum frame tivesse sido gerado.

Portanto, faremos o seguinte: no manipulador CTesterHandler::Tester(), verificaremos se a execução atual é individual ou se faz parte de uma otimização. Com base nesse resultado, salvaremos os dados diretamente no banco de dados (no caso de uma execução individual) ou criaremos um frame de dados para enviar ao EA principal (no caso de uma otimização). Para isso, adicionaremos um novo método para salvar os resultados de uma execução individual e outro método auxiliar para criar a consulta SQL necessária para inserir os dados na tabela passes. O último método é importante porque essa ação, que antes era realizada em um único ponto do código, agora será executada em dois lugares diferentes. Assim, faz sentido isolá-la em um método específico.

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   
    ...

   static void       ProcessFrame(string values);  // Handle single pass data

   // Generate SQL query to insert pass results
   static string     GetInsertQuery(string values, string inputs, ulong pass = 0);
public:
   ...
};

A implementação do método GetInsertQuery() já existe. Precisamos apenas transferir para ele o bloco de código atualmente localizado no método ProcessFrames() e chamá-lo onde for necessário dentro deste método: ProcessFrames():

//+------------------------------------------------------------------+
//| Generate SQL query to insert pass results                        |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}


//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
   ...

// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);

      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = GetInsertQuery(values, inputs, pass);

      // Add it to the SQL query array
      APPEND(queries, query);
   }

   ...
}

Para salvar os dados de uma execução individual, usaremos o novo método ProcessFrame(), que aceita como parâmetro uma string contendo parte da consulta SQL com os dados da execução a serem inseridos na tabela passes. Dentro do método, conectamos ao banco de dados, formamos a consulta SQL final e a executamos:

//+------------------------------------------------------------------+
//| Handle single pass data                                          |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Open the database
   DB::Connect();

// Form an SQL query from the received data
   string query = GetInsertQuery(values, "", 0);

// Execute the request
   DB::Execute(query);

// Close the database
   DB::Close();
}

Com os métodos adicionados, o manipulador do evento de finalização da execução pode ser modificado da seguinte forma:

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {

    ... 

// Generate a string with pass data
   data = StringFormat("%s,'%s'", data, params);

// If this is a pass within the optimization,
   if(MQLInfoInteger(MQL_OPTIMIZATION)) {
      // Open a file to write a frame data
      int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

      // Write a description of the EA parameters
      FileWriteString(f, data);

      // Close the file
      FileClose(f);

      // Create a frame with data from the recorded file and send it to the main terminal
      if(!FrameAdd("", 0, 0, s_fileName)) {
         PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
      }
   } else {
      // Otherwise, it is a single pass, call the method to add its results to the database
      CTesterHandler::ProcessFrame(data);
   }
}

Salvamos as alterações no arquivo TesterHandler.mqh na pasta atual.

Agora, após cada execução, as informações sobre seus resultados são salvas em nosso banco de dados. Para a tarefa em questão, não nos preocupamos tanto com os diversos indicadores estatísticos da execução. O mais importante é a string de inicialização da normed group de estratégias utilizada na execução. Esse foi, afinal, o objetivo principal de todo o processo de salvamento.

Contudo, a presença das strings de inicialização em uma das colunas da tabela passes ainda não é suficiente para um uso posterior confortável. Queremos anexar informações adicionais a elas. No entanto, não é adequado expandir o conjunto de colunas da tabela passes para isso, já que a maioria das linhas dessa tabela armazenará resultados de execuções de otimização, para as quais informações extras não são necessárias. 

Por essa razão, criaremos uma nova tabela para armazenar os resultados selecionados. Este passo já pode ser considerado parte da etapa de criação da biblioteca.


Formação da biblioteca

Não sobrecarregaremos a nova tabela com campos redundantes contendo informações que podem ser obtidas de outras tabelas do banco de dados. Por exemplo, se um registro na nova tabela estiver vinculado a um registro na tabela passes por meio de uma chave estrangeira, a data de criação já estará disponível lá. Além disso, pelo identificador do passe, podemos construir uma cadeia de relações para determinar a qual projeto o passe pertence e, consequentemente, qual grupo de estratégias foi utilizado no passe.

Com base nisso, criaremos a tabela strategy_groups com o seguinte conjunto de campos:

  • id_pass. Identificador do passe na tabela passes (chave estrangeira)
  • name. Nome do grupo de estratégias, que será usado para formar enumerações para o parâmetro de entrada de seleção do grupo de estratégias.

O código SQL para criar a tabela necessária pode ser o seguinte:

-- Table: strategy_groups
DROP TABLE IF EXISTS strategy_groups;

CREATE TABLE strategy_groups (
    id_pass INTEGER REFERENCES passes (id_pass) ON DELETE CASCADE
                                                ON UPDATE CASCADE
                    PRIMARY KEY,
    name    TEXT
);

Para a maioria das ações subsequentes, criaremos uma classe auxiliar chamada CGroupsLibrary. Sua função será inserir e recuperar informações sobre grupos de estratégias do banco de dados, bem como formar o arquivo .mqh com a biblioteca de bons grupos que será utilizada pelo EA final. Porém, voltaremos a essa classe mais tarde; por ora, vamos criar o EA que usaremos para formar a biblioteca.

O EA existente, SimpleVolumesExpert.mq5, embora realize quase tudo o que é necessário, ainda precisa de algumas melhorias. E planejamos usá-lo como uma versão final do consultor final. Assim, salvaremos uma nova versão com o nome SimpleVolumesStage3.mq5 e incluiremos as modificações necessárias no novo arquivo. Precisamos de duas funcionalidades adicionais: a possibilidade de especificar o nome do grupo formado pelos passes selecionados atualmente (no parâmetro passes_) e o salvamento da string de inicialização deste grupo na nova tabela strategy_groups.

A primeira funcionalidade é implementada de forma muito simples. Adicionaremos um novo parâmetro de entrada ao EA, cujo valor será usado como o nome do grupo. Se esse parâmetro estiver vazio, o salvamento na biblioteca não ocorrerá.

input group "::: Saving to library"
input string groupName_  = "";         // - Group name (if empty - no saving)

Já para a segunda funcionalidade, será necessário um pouco mais de esforço. O problema é que, para inserir dados na tabela strategy_groups, precisamos saber o identificador atribuído ao registro do passe atual ao inseri-lo na tabela passes. Como esse valor é atribuído automaticamente pelo banco de dados (no comando de inserção, passamos simplesmente NULL em vez do valor do identificador), ele não está armazenado em uma variável no código. Portanto, no momento, não podemos usá-lo em outro local onde seja necessário. Precisamos encontrar uma forma de determiná-lo.

Isso pode ser feito de várias maneiras. Por exemplo, sabendo que os identificadores atribuídos às novas linhas formam uma sequência crescente, podemos simplesmente selecionar o valor máximo do identificador no momento da inserção. Essa abordagem é válida se soubermos com certeza que novas linhas não estão sendo adicionadas à tabela passes simultaneamente. No entanto, se outra otimização dos estágios um ou dois estiver em andamento no momento, seus resultados também podem ser adicionados ao mesmo banco de dados. Nesse caso, não podemos garantir que o último identificador corresponda ao passe que utilizamos para formar a biblioteca. Em suma, essa abordagem é possível, mas exige que aceitemos certas limitações.

Uma maneira muito mais confiável, livre de possíveis erros mencionados anteriormente, é a seguinte abordagem. Podemos modificar ligeiramente a consulta SQL para inserção de dados, transformando-a em uma consulta que retorna o identificador gerado para a nova linha da tabela como resultado. Para isso, basta adicionar o operador RETURNING rowid ao final da consulta. Faremos essa modificação no método GetInsertQuery(), que cria a consulta SQL para inserir uma nova linha na tabela passes. Embora o campo identificador na tabela passes tenha o nome id_pass, podemos referenciá-lo como rowid, pois ele possui o tipo correspondente (INTEGER PRIMARY KEY AUTOINCREMENT) e substitui a coluna rowid que o SQLite gera automaticamente em suas tabelas.

//+------------------------------------------------------------------+
//| Generate SQL query to insert pass results                        |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s') RETURNING rowid;",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}

Também será necessário modificar o código em MQL5 que envia essa consulta. Atualmente, utilizamos o método DB::Execute(query) para isso, assumindo que a consulta query passada não retorna nenhum dado.

Portanto, adicionaremos à classe CDatabase um novo método chamado Insert(), que executará a consulta de inserção recebida e retornará o único valor lido como resultado. Dentro deste método, em vez da função DatabaseExecute(), utilizaremos a função DatabasePrepare(), que permite acessar os resultados da consulta posteriormente:

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   ...
public:
   ...
   // Execute a query to the database for insertion with return of the new entry ID
   static ulong      Insert(string query);
};

...

//+------------------------------------------------------------------+
//| Execute a query to the database for insertion returning the      |
//| new entry ID                                                     |
//+------------------------------------------------------------------+
ulong CDatabase::Insert(string query) {
   ulong res = 0;
   
// Execute the request
   int request = DatabasePrepare(s_db, query);

// If there is no error
   if(request != INVALID_HANDLE) {
      // Data structure for reading a single string of a query result 
      struct Row {
         int         rowid;
      } row;

      // Read data from the first result string
      if(DatabaseReadBind(request, row)) {
         res = row.rowid;
      } else {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d",
                     query, GetLastError());
      }
   } else {
      // Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d",
                  query, GetLastError());
   }
   return res;
}
//+------------------------------------------------------------------+

Optamos por não complicar este método com verificações adicionais para garantir que a consulta passada seja, de fato, uma consulta INSERT, que inclua o comando para retornar o identificador, ou que o valor retornado não seja composto. Qualquer desvio dessas condições resultará em erros na execução do código. No entanto, como esse método será utilizado apenas em uma parte do projeto por enquanto, garantiremos que a consulta passada será sempre correta.

Salvaremos as alterações feitas no arquivo Database.mqh na pasta atual.

A próxima questão que surgiu durante a implementação foi como passar o valor do identificador para o nível superior do código, pois processá-lo no ponto onde ele é obtido resultaria na necessidade de adicionar funcionalidades estranhas e parâmetros extras aos métodos existentes. Por isso, decidimos proceder da seguinte maneira: adicionamos à classe CTesterHandler uma propriedade estática chamada s_idPass. O identificador do passe atual é gravado nesta propriedade. Que pode ser acessada de qualquer ponto do programa:

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   ...
public:
   ...
   static ulong      s_idPass;
};

...
ulong CTesterHandler::s_idPass = 0;

...

//+------------------------------------------------------------------+
//| Handle single pass data                                          |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Open the database
   DB::Connect();

// Form an SQL query from the received data
   string query = GetInsertQuery(values, "", 0);

// Execute the request
   s_idPass = DB::Insert(query);

// Close the database
   DB::Close();
}

Salvaremos as alterações feitas no arquivo TesterHandler.mqh na pasta atual.

Agora, é hora de retornar à classe auxiliar mencionada anteriormente, CGroupsLibrary. Nesta classe, foi necessário declarar dois métodos públicos, um método privado e um array estático:

//+------------------------------------------------------------------+
//| Class for working with a library of selected strategy groups     |
//+------------------------------------------------------------------+
class CGroupsLibrary {
private:
   // Exporting group names and initialization strings extracted from the database as MQL5 code
   static void       ExportParams(string &p_names[], string &p_params[]);

public:
   // Add the pass name and ID to the database
   static void       Add(ulong p_idPass, string p_name);

   // Export passes to mqh file
   static void       Export(string p_idPasses);

   // Array to fill with initialization strings from mqh file
   static string     s_params[];
};

No EA que utilizaremos para a formação da biblioteca, será empregado apenas o método Add(). A ele serão passados o identificador do passe e o nome do grupo a ser salvo na biblioteca. O código do método é bastante simples: com base nos dados de entrada, cria-se uma consulta SQL para inserir um novo registro na tabela strategy_groups e essa consulta é executada.

//+------------------------------------------------------------------+
//| Add the pass name and ID to the database                         |
//+------------------------------------------------------------------+
void CGroupsLibrary::Add(ulong p_idPass, string p_name) {
   string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, '%s')",
                               p_idPass, p_name);

// Open the database
   if(DB::Connect()) {
      // Execute the request
      DB::Execute(query);

      // Close the database
      DB::Close();
   }
}

Agora, para finalizar o desenvolvimento da ferramenta de formação da biblioteca, falta apenas adicionar a chamada do método Add() após a conclusão de cada passe do testador no EA SimpleVolumesStage3.mq5:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   // Handle the completion of the pass in the EA object
   double res = expert.Tester();

   // If the group name is not empty, save the pass to the library
   if(groupName_ != "") {
      CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_);
   }
   return res;
}

Salvamos as alterações nos arquivos SimpleVolumesStage3.mq5 e GroupsLibrary.mqh na pasta atual. Com a adição de implementações simples para os demais métodos da classe CGroupsLibrary, o EA SimpleVolumesStage3.mq5 já pode ser utilizado. 


Preenchendo a biblioteca

Vamos formar uma biblioteca utilizando os nove identificadores de passes selecionados anteriormente. Para isso, executaremos o EA SimpleVolumesStage3.ex5 no testador, especificando diferentes combinações dos nove identificadores no parâmetro de entrada passes_. No parâmetro de entrada groupName_, atribuiremos nomes claros que reflitam a composição do grupo atual de instâncias individuais de estratégias de trading, que serão unidas em um único grupo.

Após algumas execuções, analisaremos os resultados armazenados na tabela strategy_groups, adicionando alguns indicadores dos passes realizados com diferentes grupos para maior clareza. Isso pode ser feito, por exemplo, utilizando a seguinte consulta SQL:

SELECT sg.id_pass,
       sg.name,
       p.custom_ontester,
       p.sharpe_ratio,
       p.profit,
       p.profit_factor,
       p.equity_dd_relative
  FROM strategy_groups sg
       JOIN
       passes p ON sg.id_pass = p.id_pass;

Como resultado, obtemos a seguinte tabela:

Fig. 1. Composição da biblioteca de grupos

Na coluna name, visualizamos os nomes dos grupos, que indicam os instrumentos de trading (símbolos), os timeframes e a quantidade de instâncias de estratégias de trading incluídas no grupo. Por exemplo, a presença de 'EUR-GBP-USD' significa que o grupo contém estratégias que operam com os três símbolos: EURGBP, EURUSD e GBPUSD. Se o nome do grupo começar com 'Only EURGBP', ele inclui apenas estratégias para o símbolo EURGBP. Os timeframes também são interpretados de maneira similar. No final do nome do grupo, é especificada a quantidade de instâncias de estratégias de trading. Por exemplo, '3x16 items' indica que o grupo contém três subgrupos normalizados, cada um com 16 estratégias.

Na coluna custom_ontester, é possível visualizar a rentabilidade anual média normalizada de cada grupo. Vale observar que a variação dos valores deste parâmetro foi maior do que o esperado, o que sugere a necessidade de investigar as causas. Por exemplo, os resultados dos grupos que utilizam apenas GBPUSD foram significativamente melhores do que os grupos com múltiplos símbolos. O melhor resultado, armazenado por último na linha 20, é o seguinte: Esse grupo incluiu subgrupos que apresentaram os melhores resultados para cada símbolo e para um ou mais intervalos de tempo.


Exportando a biblioteca

O próximo passo é transferir a biblioteca de grupos do banco de dados para um arquivo .mqh, que poderá ser incorporado ao EA final. Para isso, implementaremos os métodos da classe CGroupsLibrary responsáveis pela exportação, além de um EA auxiliar que será utilizado para executá-los.

No método Export(), extrairemos do banco de dados os nomes dos grupos da biblioteca e suas strings de inicialização e os adicionaremos aos arrays correspondentes. Os arrays formados serão então passados ao método seguinte, ExportParams():

//+------------------------------------------------------------------+
//| Exporting passes to mqh file                                     |
//+------------------------------------------------------------------+
void CGroupsLibrary::Export(string p_idPasses) {
// Array of group names
   string names[];

// Array of group initialization strings
   string params[];

// If the connection to the main database is established,
   if(DB::Connect()) {
      // Form a request to receive passes with the specified IDs
      string query = "SELECT sg.id_pass,"
                     "       sg.name,"
                     "       p.params"
                     "  FROM strategy_groups sg"
                     "       JOIN"
                     "       passes p ON sg.id_pass = p.id_pass";

      query = StringFormat("%s "
                           "WHERE p.id_pass IN (%s);",
                           query, p_idPasses);

      // Prepare and execute the request
      int request = DatabasePrepare(DB::Id(), query);

      // If the request is successful
      if(request != INVALID_HANDLE) {
         // Structure for reading results
         struct Row {
            ulong          idPass;
            string         name;
            string         params;
         } row;

         // For all query results, add the name and initialization string to the arrays
         while(DatabaseReadBind(request, row)) {
            APPEND(names, row.name);
            APPEND(params, row.params);
         }
      }

      DB::Close();

      // Export to mqh file
      ExportParams(names, params);
   }
}

No método ExportParams(), geramos uma string de código em MQL5 que criará uma enumeração (enum) com o nome ENUM_GROUPS_LIBRARY e a preencherá com elementos. Cada elemento terá um comentário associado, que conterá o nome do grupo. Em seguida, no código, será declarado um array estático de strings, CGroupsLibrary::s_params[], que será preenchido com as strings de inicialização dos grupos da biblioteca. Antes de serem inseridas no array, as strings de inicialização serão processadas: todos os caracteres de quebra de linha serão substituídos por espaços e serão adicionadas barras invertidas antes de aspas duplas. Isso é necessário para encapsular as strings de inicialização dentro de aspas duplas no código gerado.

Depois que o código estiver completamente gerado e armazenado na variável data, criamos um arquivo chamado ExportedGroupsLibrary.mqh e salvamos nele o código gerado.

//+------------------------------------------------------------------+
//| Export group names extracted from the database and               |
//| initialization strings in the form of MQL5 code                  |
//+------------------------------------------------------------------+
void CGroupsLibrary::ExportParams(string &p_names[], string &p_params[]) {
   // ENUM_GROUPS_LIBRARY enumeration header
   string data = "enum ENUM_GROUPS_LIBRARY {\n";

   // Fill the enumeration with group names
   FOREACH(p_names, { data += StringFormat("   GL_PARAMS_%d, // %s\n", i, p_names[i]); });

   // Close the enumeration
   data += "};\n\n";

   // Group initialization string array header and its opening bracket
   data += "string CGroupsLibrary::s_params[] = {";

   // Fill the array by replacing invalid characters in the initialization strings
   string param;
   FOREACH(p_names, {
      param = p_params[i];
      StringReplace(param, "\r", "");
      StringReplace(param, "\n", " ");
      StringReplace(param, "\"", "\\\"");
      data += StringFormat("\"%s\",\n", param);
   });

   // Close the array
   data += "};\n";

// Open the file to write data
   int f = FileOpen("ExportedGroupsLibrary.mqh", FILE_WRITE | FILE_TXT | FILE_ANSI);

// Write the generated code
   FileWriteString(f, data);

// Close the file
   FileClose(f);
}

Segue-se então uma parte muito importante:

// Connecting the exported mqh file.
// It will initialize the CGroupsLibrary::s_params[] static variable
// and ENUM_GROUPS_LIBRARY enumeration
#include "ExportedGroupsLibrary.mqh"

O arquivo gerado pelo processo de exportação será incluído diretamente no arquivo GroupsLibrary.mqh. Dessa forma, o EA final precisará apenas incluir esse arquivo para acessar a biblioteca exportada. Contudo, esse método tem um pequeno inconveniente: para compilar o EA responsável pela exportação da biblioteca, o arquivo ExportedGroupsLibrary.mqh, que aparece apenas após a exportação, já deve existir. No entanto, apenas a existência desse arquivo é importante, não o seu conteúdo. Assim, basta criar um arquivo vazio com esse nome na pasta atual para que a compilação ocorra sem erros.

Para executar o método de exportação, será necessário um script ou EA que o faça. Esse EA pode ser estruturado da seguinte forma:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Exporting from library"
input string     passes_ = "802150,802151,802152,802153,802154,"
                           "802155,802156,802157,802158,802159,"
                           "802160,802161,802162,802164,802165,"
                           "802166,802167,802168,802169,802173";    // - Comma-separated IDs of the saved passes


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Call the group library export method
   CGroupsLibrary::Export(passes_);

// Successful initialization
   return(INIT_SUCCEEDED);
}

void OnTick() {
   ExpertRemove();
}

Alterando o parâmetro passes_, podemos determinar a composição e a ordem das exportações dos grupos da biblioteca do banco de dados. Após executar este EA uma vez no gráfico, o arquivo ExportedGroupsLibrary.mqh aparecerá na pasta de dados do terminal. Este arquivo deverá ser transferido para a pasta atual do código do projeto.


Criação do EA Final

Finalmente, chegamos à fase final. Restam apenas algumas pequenas alterações no EA SimpleVolumesExpert.mq5. Primeiro, será necessário incluir o arquivo GroupsLibrary.mqh:

#include "GroupsLibrary.mqh"

Em seguida, substituímos o parâmetro de entrada passes_ por um novo parâmetro, que permitirá selecionar um grupo da biblioteca:

input group "::: Selection for the group"
input ENUM_GROUPS_LIBRARY       groupId_     = -1;    // - Group from the library

Na função OnInit(), em vez de obter as strings de inicialização do banco de dados usando os identificadores dos passes (como era feito anteriormente), agora simplesmente selecionaremos a string de inicialização do array CGroupsLibrary::s_params[] cujo índice corresponde ao valor do novo parâmetro de entrada groupId_:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Initialization string with strategy parameter sets
   string strategiesParams = NULL;

// If the selected strategy group index from the library is valid, then
   if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) {
      // Take the initialization string from the library for the selected group
      strategiesParams = CGroupsLibrary::s_params[groupId_];
   }

// If the strategy group from the library is not specified, then we interrupt the operation
   if(strategiesParams == NULL) {
      return INIT_FAILED;
   }

   ...

// Successful initialization
   return(INIT_SUCCEEDED);
}

Salvamos as alterações realizadas no arquivo SimpleVolumesExpert.mq5 na pasta atual.

Como adicionamos comentários com os nomes aos elementos da enumeração ENUM_GROUPS_LIBRARY, agora, na interface de seleção de parâmetros do EA, podemos visualizar nomes mais descritivos em vez de apenas uma sequência de números:


Fig. 2. Seleção do grupo nos parâmetros do EA pelo nome

Executaremos o EA com o último grupo da lista e analisaremos o resultado:

Fig. 3. Resultados do teste do EA final com o grupo mais atraente da biblioteca

Pode-se observar que, com base na rentabilidade anual normalizada, os resultados são próximos aos armazenados no banco de dados. Pequenas diferenças ocorrem principalmente porque o EA final utilizou um grupo já normalizado (o que pode ser confirmado ao observar o valor da máxima retração relativa, que representa aproximadamente 10% do depósito utilizado). Durante a geração da string de inicialização do grupo no EA SimpleVolumesStage3.ex5 no momento do passe, o grupo ainda não estava normalizado, resultando em uma retração de cerca de 5,4%. 


Considerações finais

Bem, obtivemos um EA final que pode operar de forma independente do banco de dados gerado durante o processo de otimização. É possível que retornemos a esse tópico no futuro, já que a prática pode revelar ajustes necessários, e o método proposto neste artigo pode ser superado por abordagens mais convenientes. Contudo, alcançar o objetivo estabelecido já representa um avanço.

Durante o desenvolvimento do código para este artigo, surgiram novas questões que demandam estudo adicional. Por exemplo, descobrimos que os resultados dos testes deste EA são sensíveis não apenas ao servidor de cotações, mas também ao símbolo definido como principal nas configurações do testador de estratégias. Pode ser necessário ajustar a automação dos processos de otimização nos estágios um e dois, mas isso será tratado em outra ocasião.

Por fim, uma advertência que esteve implícita ao longo de todas as partes anteriores. Não afirmamos em nenhum momento que seguir o caminho proposto garantirá lucros. Pelo contrário, em algumas etapas, obtivemos resultados insatisfatórios nos testes. Além disso, apesar dos esforços para preparar o EA para operações reais, dificilmente poderemos afirmar que fizemos tudo o que era possível para garantir seu funcionamento correto em contas reais. Isso representa um ideal a ser perseguido, mas sua realização sempre parece estar longe. Contudo, isso não nos impede de seguir avançando.

Todos os resultados apresentados neste artigo e nos anteriores baseiam-se exclusivamente em dados de testes históricos e não garantem nenhum lucro futuro. Este projeto tem caráter exploratório. Todos os resultados publicados podem ser utilizados por qualquer interessado, sob sua própria responsabilidade e risco.

Obrigado pela atenção e até a próxima!


Conteúdo do arquivo

#
 Nome
Versão  Descrição   Últimas alterações
 MQL5/Experts/Article.15360
1 Advisor.mqh 1.04 Classe básica do EA (Expert Advisor) Parte 10
2 Database.mqh 1.04 Classe para trabalho com banco de dados Parte 17
3 ExpertHistory.mqh 1.00 Classe para exportação do histórico de negociações para arquivo Parte 16
4 ExportedGroupsLibrary.mqh
Arquivo gerado contendo nomes de grupos de estratégias e um array de suas strings de inicialização Parte 17
5 Factorable.mqh 1.01 Classe base de objetos criados a partir de uma string Parte 10
6 GroupsLibrary.mqh 1.00 Classe para trabalho com a biblioteca de grupos de estratégias selecionados Parte 17
7 HistoryReceiverExpert.mq5 1.00 EA (Expert Advisor) para reprodução de histórico de negociações com gerenciamento de risco Parte 16  
8 HistoryStrategy.mqh  1.00 Classe de estratégia de negociação para reprodução de histórico de negociações  Parte 16
9 Interface.mqh 1.00 Classe base de visualização de vários objetos Parte 4
10 LibraryExport.mq5 1.00 EA que salva strings de inicialização de passes selecionados da biblioteca no arquivo ExportedGroupsLibrary.mqh Parte 17
11 Macros.mqh 1.02 Macros úteis para operações com arrays Parte 16  
12 Money.mqh 1.01  Classe base de gerenciamento de capital Parte 12
13 NewBarEvent.mqh 1.00  Classe para definição de um novo candle (barra) para um símbolo específico  Parte 8
14 Receiver.mqh 1.04  Classe base para transferência de volumes abertos para posições de mercado  Parte 12
15 SimpleHistoryReceiverExpert.mq5 1.00 EA simplificado para reprodução de histórico de negociações   Parte 16
16 SimpleVolumesExpert.mq5 1.20 EA para trabalho paralelo de vários grupos de estratégias modeladas. Os parâmetros serão extraídos da biblioteca embutida de grupos. Parte 17
17 SimpleVolumesStage3.mq5 1.00 EA que salva um grupo de estratégias normalizado gerado na biblioteca de grupos com um nome especificado. Parte 17
18 SimpleVolumesStrategy.mqh 1.09  Classe de estratégia de negociação usando volumes tick Parte 15
19 Strategy.mqh 1.04  Classe base de estratégia de negociação Parte 10
20 TesterHandler.mqh  1.03 Classe para manipulação de eventos de otimização  Parte 17 
21 VirtualAdvisor.mqh  1.06  Classe de EA para operar com posições virtuais (ordens) Parte 15
22 VirtualChartOrder.mqh  1.00  Classe de posição virtual gráfica Parte 4  
23 VirtualFactory.mqh 1.04  Classe de fábrica de objetos  Parte 16
24 VirtualHistoryAdvisor.mqh 1.00  Classe de EA para reprodução de histórico de negociações  Parte 16
25 VirtualInterface.mqh  1.00  Classe de interface gráfica do EA  Parte 4  
26 VirtualOrder.mqh 1.04  Classe de ordens e posições virtuais  Parte 8
27 VirtualReceiver.mqh 1.03  Classe para transferência de volumes abertos para posições de mercado (receptor)  Parte 12
28 VirtualRiskManager.mqh  1.02  Classe de gerenciamento de risco (gerenciador de risco)  Parte 15
29 VirtualStrategy.mqh 1.05  Classe de estratégia de negociação com posições virtuais  Parte 15
30 VirtualStrategyGroup.mqh  1.00  Classe de grupo de estratégias de negociação ou grupos de estratégias de negociação Parte 11 
31 VirtualSymbolReceiver.mqh  1.00 Classe de receptor de símbolo  Parte 3



Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15360

Arquivos anexados |
MQL5.zip (72.95 KB)
Usando o Algoritmo de Aprendizado de Máquina PatchTST para Prever a Ação do Preço nas Próximas 24 Horas Usando o Algoritmo de Aprendizado de Máquina PatchTST para Prever a Ação do Preço nas Próximas 24 Horas
Neste artigo, aplicamos um algoritmo relativamente complexo de rede neural chamado PatchTST, lançado em 2023, para prever a ação do preço nas próximas 24 horas. Usaremos o repositório oficial, faremos algumas modificações, treinaremos um modelo para EURUSD e o aplicaremos para fazer previsões futuras, tanto em Python quanto em MQL5.
Análise de Sentimento e Deep Learning para Trading com EA e Backtesting com Python Análise de Sentimento e Deep Learning para Trading com EA e Backtesting com Python
Neste artigo, vamos introduzir a Análise de Sentimento e Modelos ONNX com Python para serem usados em um EA. Um script executa um modelo ONNX treinado do TensorFlow para previsões de deep learning, enquanto outro busca manchetes de notícias e quantifica o sentimento usando IA.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 26): Médias Móveis e o Exponente de Hurst Técnicas do MQL5 Wizard que você deve conhecer (Parte 26): Médias Móveis e o Exponente de Hurst
O Exponente de Hurst é uma medida de quanto uma série temporal se autocorrela ao longo do tempo. Entende-se que ele captura as propriedades de longo prazo de uma série temporal e, portanto, tem um peso significativo na análise de séries temporais, mesmo fora do contexto econômico/financeiro. No entanto, focamos em seu potencial benefício para os traders ao analisar como essa métrica poderia ser combinada com médias móveis para construir um sinal potencialmente robusto.
Teoria do caos no trading (Parte 2): Continuamos a imersão Teoria do caos no trading (Parte 2): Continuamos a imersão
Continuamos a imersão na teoria do caos nos mercados financeiros e analisamos sua aplicabilidade à análise de moedas e outros ativos.