English Русский
preview
Desenvolvendo um EA multimoeda (Parte 29): Aprimoramento do pipeline

Desenvolvendo um EA multimoeda (Parte 29): Aprimoramento do pipeline

MetaTrader 5Testador |
259 0
Yuriy Bykov
Yuriy Bykov

Introdução

Nas três partes anteriores, nós nos afastamos um pouco da linha principal de desenvolvimento para nos dedicarmos ao aprimoramento de ferramentas auxiliares que, de uma forma ou de outra, serão úteis mais adiante. 

Por exemplo, na Parte 28, ampliamos os mecanismos de gerenciamento de capital do nosso EA multimoeda com o desenvolvimento e a integração de um novo módulo de software: o gerenciador de fechamento. Esse módulo permitiu monitorar o lucro ou prejuízo total em relação a um saldo-base atualizado dinamicamente e reiniciar todas as estratégias de negociação quando os valores definidos fossem atingidos. Para etapas futuras, planejamos ampliar sua funcionalidade com recursos de trailing do lucro total e movimentação conjunta das posições abertas para o ponto de equilíbrio.

Na Parte 27, criamos outro componente: uma janela de diálogo que ocupa toda a área do gráfico do terminal e permite exibir textos com várias linhas, configurações flexíveis de fonte e suporte a rolagem. Essa ferramenta tornou a visualização das informações mais prática e clara. O componente desenvolvido foi integrado à biblioteca Adwizard como recurso para exibir diferentes informações sobre o funcionamento dos EAs.

De modo geral, consolidamos a abordagem adotada anteriormente, baseada na separação clara de todo o código entre a biblioteca, mantida no repositório Adwizard, e as partes específicas de cada projeto, mantidas nos repositórios SimpleCandles e SymbolsInformer.

Agora, vamos retomar os resultados obtidos na Parte 25. Nela, alcançamos um marco importante no desenvolvimento do nosso EA multimoeda: concluímos, em linhas gerais, o desenvolvimento de um sistema universal de otimização automática e integramos com sucesso a nova estratégia de negociação SimpleCandles. Todo o processo, desde a criação do projeto de otimização no banco de dados e a execução da otimização em várias etapas em um cluster de agentes até a geração de um EA final pronto para uso, foi totalmente automatizado. 

No entanto, desde a publicação do artigo, encontramos algumas dificuldades e limitações práticas no uso desse sistema. Isso não é surpreendente, pois se tratava apenas da primeira iteração de seu desenvolvimento. Portanto, analisaremos algumas melhorias que permitirão utilizar a otimização automática de forma mais eficiente.


Traçamos o caminho

Vamos relembrar a essência do pipeline proposto para a criação do EA final. Na primeira etapa, queremos executar, no testador de estratégias do MetaTrader 5, várias otimizações de uma mesma estratégia de negociação para diferentes símbolos, timeframes e outros parâmetros. Entre os resultados obtidos, selecionaremos, para cada símbolo, um número considerável de combinações de parâmetros com bons resultados, caso existam. Vamos chamá-las de instâncias individuais da estratégia de negociação.

Na segunda etapa, realizaremos uma otimização para identificar os melhores grupos formados por um pequeno número de instâncias individuais das estratégias de negociação. Ou seja, entre milhares de instâncias, manteremos para cada símbolo apenas um grupo de 8 a 16 instâncias que tenha apresentado os melhores resultados em operação conjunta. Na terceira etapa, reuniremos esses melhores grupos para carregá-los e utilizá-los no EA final.

Após um longo período de desenvolvimento, todas essas ações foram automatizadas tanto quanto possível. Atualmente, precisamos definir manualmente os parâmetros para a criação do projeto de otimização, isto é, de um cenário geral que orientará a otimização automática. Depois que ela for concluída, ainda será necessário realizar algumas operações para executar o EA final em uma conta de negociação. No entanto, o tempo gasto nas operações manuais, que pode ser de apenas alguns minutos, é insignificante diante das horas, dias ou até semanas em que o pipeline de otimização automática pode funcionar sem intervenção por horas, dias ou até semanas. Foi com uma ferramenta desse tipo que começamos a trabalhar.

O primeiro inconveniente surgiu quando concluímos os testes de integração de uma nova estratégia e decidimos passar para outra. Descobrimos que, apesar da separação prevista entre a biblioteca e as partes específicas dos projetos, ainda permaneciam algumas dependências entre elas. Vamos verificar onde isso ocorre e tentar eliminá-las.

Outra dificuldade estava relacionada à duração de determinadas tarefas de otimização executadas pelo pipeline do projeto. Essa duração podia ser bastante elevada e dependia diretamente da extensão do intervalo temporal utilizado em todas as otimizações. Para realizar uma otimização em um intervalo de cinco anos, por exemplo, seria necessário muito mais tempo do que para um período de três meses. No entanto, a prática mostrou que, durante a otimização genética, boas combinações de parâmetros das estratégias podem surgir muito antes do término previsto. Portanto, podemos reduzir o tempo total de execução do pipeline interrompendo as otimizações após um período previamente definido. Vamos adicionar ao sistema a possibilidade de especificar um limite de tempo para a execução de cada tarefa de otimização.

Por fim, tornaremos o acompanhamento da otimização automática um pouco mais prático, exibindo informações mais detalhadas sobre a tarefa atual, em vez de mostrar apenas o seu número.

Para facilitar a compreensão, reproduziremos passo a passo todo o processo de criação do EA final, fazendo pausas para aplicar os ajustes desejados.


Criação do banco de dados

Primeiro, é necessário clonar dois repositórios para a pasta MQL5/Shared Projects: o repositório da biblioteca Adwizard e o repositório do projeto. O procedimento para clonar no computador os repositórios armazenados no MQL5 Algo Forge foi explicado em detalhes neste artigo.

Como repositório do projeto, também é possível criar um repositório próprio usando o SimpleCandles como modelo. Explicamos como fazer isso nos artigos dedicados à migração para uma nova estratégia. Essa opção será necessária caso você queira implementar uma estratégia de negociação própria. Nós continuaremos usando a estratégia SimpleCandles, criada anteriormente, e o repositório de projeto com o mesmo nome. Esse repositório contém o projeto usado para criar o EA multimoeda final com essa estratégia de negociação.

Quando os dois repositórios estiverem na pasta de trabalho do terminal, em MQL5/Shared Projects, compilaremos o arquivo do EA para criação do projeto de otimização automática SimpleCandles/Optimization/CreateProject.mq5 e arrastaremos a versão compilada, exibida no Navegador do terminal, para qualquer gráfico. Na janela de definição dos parâmetros de entrada, acessaremos a guia Inputs e veremos o seguinte:

O parâmetro que define o arquivo do banco de dados de otimização contém o nome usado nos artigos anteriores. Como planejamos fazer algumas alterações que também afetarão a estrutura do banco de dados de otimização, não podemos continuar usando o banco de dados antigo. Será necessário criar um novo.

Isso é muito simples: basta informar outro nome para o banco de dados nesse parâmetro de entrada. Caso não exista um arquivo com esse nome, o EA para criação do projeto criará automaticamente um banco de dados vazio com o nome especificado e a estrutura de tabelas necessária. Para não precisarmos alterar novamente o nome definido por padrão nas próximas execuções desse EA, vamos substituí-lo pelo novo nome diretamente no arquivo-fonte:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "::: Database"
sinput string fileName_  = "article.17607.db.sqlite"; // - Optimization database file

sinput group "::: Project parameters - Basic"
sinput string  projectName_ = "SimpleCandles";        // - Name
sinput string  projectVersion_ = "1.00";              // - Version
sinput string  symbols_ = "GBPUSD,EURUSD,EURGBP";     // - Symbols
sinput string  timeframes_ = "H1,M30";                // - Timeframes
...

Também é mais prático alterar os valores dos demais parâmetros no código-fonte desse EA do que na janela de entrada de parâmetros durante sua execução. Esta é uma etapa importante, na qual precisamos definir o cenário geral do pipeline de otimização automática, registrá-lo nos parâmetros de entrada do EA de criação do projeto e, em seguida, executar esse EA uma única vez. Na prática, nós o executamos várias vezes, pois é muito difícil definir todos os parâmetros logo na primeira tentativa. Por isso, avançamos de forma iterativa: registramos os valores dos parâmetros no código-fonte, criamos o projeto, iniciamos a otimização automática e, ao perceber que algo precisa ser alterado, interrompemos o processo e voltamos ao início.

O que pode dar errado? Por exemplo, podemos escolher um intervalo temporal muito amplo para os testes e, com base em uma estimativa preliminar, concluir que todo o processo levará várias semanas. Nesse caso, se não quisermos esperar tanto, podemos reduzir a duração do intervalo de teste ou, por exemplo, reduzir o número de símbolos usados nas diferentes execuções de otimização. Para isso, alteramos os parâmetros de entrada e criamos o projeto novamente. Também podemos excluir antes o arquivo do banco de dados, para que o projeto recém-criado seja o único no novo banco, em vez de ser adicionado a um banco de dados já existente.

De modo geral, alterar os valores padrão desses parâmetros na parte específica do projeto é perfeitamente aceitável e justificável, pois cada projeto exige uma configuração própria, definida na parte correspondente do projeto.

Ao final desta etapa, é criado na pasta comum de dados do terminal um arquivo de banco de dados com o nome especificado. O banco de dados contém o cenário do projeto de otimização automática na tabela projects. Esse cenário é composto por um conjunto de etapas, registradas na tabela stages. As etapas são compostas por trabalhos, registrados na tabela jobs, e cada trabalho pode conter uma ou mais tarefas de otimização, armazenadas na tabela tasks. Essas relações são indicadas pelas setas na figura:

Todas as tarefas do projeto estão com o status Queued, ou seja, foram colocadas na fila de execução. Agora podemos passar para a próxima etapa: a execução do pipeline de otimização automática, durante a qual todas as tarefas criadas serão transformadas em execuções de otimização separadas dos EAs de cada etapa no testador de estratégias do MetaTrader 5. 


Execução do pipeline de otimização

Para isso, compilaremos o arquivo do EA responsável pela otimização automática SimpleCandles/Optimization/Optimization.mq5 e arrastaremos a versão compilada, exibida no Navegador do terminal, para qualquer gráfico. Na guia Inputs da janela de definição dos parâmetros de entrada, veremos o seguinte:

Aqui, novamente, vemos que os valores padrão dos parâmetros ainda contêm o nome do arquivo de banco de dados anterior. É claro que podemos substituí-lo manualmente pelo novo nome, article.17607.db.sqlite, mas a prática mostrou que isso é pouco prático. Durante o ajuste do pipeline, provavelmente precisaremos realizar várias execuções de teste. Em cada uma delas, teríamos de alterar manualmente o nome do arquivo de banco de dados para o nome atual.

No entanto, se tentarmos proceder como descrito anteriormente, alterando o nome diretamente no código, descobriremos que essa mudança precisa ser feita na biblioteca, e não na parte específica do projeto. Na parte do projeto, o arquivo do EA de otimização SimpleCandles/Optimization/Optimization.mq5 apenas inclui o arquivo da biblioteca para que, durante a compilação, seja gerado um arquivo executável na pasta do projeto:

#include "../Include/Adwizard/Experts/Optimization.mqh"

Como esse arquivo não contém mais nada, o valor utilizado por padrão está definido no arquivo de biblioteca incluído. Vamos corrigir isso da seguinte forma: antes de incluir o arquivo da biblioteca, declararemos constantes com os valores padrão necessários no arquivo do projeto e, ao mesmo tempo, ajustaremos o caminho do arquivo incluído, removendo o nome da pasta Include:

// Constants with default parameters for the project:
// - File with the main database
#define OPT_FILEMNAME "article.17607.db.sqlite"

// - Path to the Python interpreter
#define OPT_PYTHONPATH "C:\\Python\\Python312\\python.exe"

#include "../../Adwizard/Experts/Optimization.mqh"

Na biblioteca, no arquivo Adwizard/Experts/Optimization.mqh, trataremos o caso em que essas constantes não estejam declaradas. Nesse caso, elas serão declaradas com valores vazios. Em seguida, usaremos seus valores como padrão nos parâmetros de entrada:

// Create constants for default parameters,
// if they are not defined in the project part
#ifndef OPT_FILEMNAME
#define OPT_FILEMNAME ""
#endif

#ifndef OPT_PYTHONPATH
#define OPT_PYTHONPATH ""
#endif

sinput string fileName_    = OPT_FILEMNAME;  // - File with the main database
sinput string pythonPath_  = OPT_PYTHONPATH; // - Path to the Python interpreter

Agora podemos definir esses parâmetros no código da parte específica do projeto sem alterar a biblioteca. Assim, a mesma biblioteca poderá ser utilizada em diferentes projetos.


Limite de tempo de execução das tarefas

Como já mencionado, outra dificuldade estava relacionada à duração potencialmente elevada de determinadas tarefas de otimização executadas pelo pipeline do projeto. Para adicionar um limite ao tempo de execução de cada tarefa de otimização, precisaremos fazer alterações em vários pontos.

Em primeiro lugar, precisamos adicionar ao arquivo do EA de criação do projeto parâmetros de entrada que permitam especificar o tempo máximo desejado para a execução das tarefas. Essas alterações dizem respeito à parte específica do projeto. Em segundo lugar, precisamos adicionar à tabela tasks do banco de dados um campo para armazenar esse valor. Em terceiro lugar, o código do EA de otimização deve passar a aceitar e utilizar o valor do tempo máximo de execução. Essas alterações serão feitas na biblioteca.

Vamos adicionar ao arquivo SimpleCandles/Optimization/CreateProject.mq5 dois parâmetros para definir o tempo máximo de execução das tarefas de otimização na primeira e na segunda etapa. 

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "::: Database"
sinput string fileName_  = "article.17607.db.sqlite"; // - Optimization database file

sinput group "::: Project parameters - Basic"
sinput string  projectName_ = "SimpleCandles";        // - Name
sinput string  projectVersion_ = "1.00";              // - Version
sinput string  symbols_ = "GBPUSD,EURUSD,EURGBP";     // - Symbols
sinput string  timeframes_ = "H1,M30";                // - Timeframes

sinput group "::: Project parameters - Optimization interval"
sinput datetime fromDate_ = D'2023-09-01';            // - Start date
sinput datetime toDate_ = D'2024-01-01';              // - End date

sinput group "::: Project parameters - Account"
sinput string   mainSymbol_ = "GBPUSD";               // - Main symbol
sinput int      deposit_ = 10000;                     // - Initial deposit

sinput group "::: Stage 1. Search"
sinput string   stage1ExpertName_ = "Stage1.ex5";     // - Stage EA
sinput string   stage1Criterions_ = "6,6,6";          // - Optimization criteria for tasks
sinput long     stage1MaxDuration_ = 20;              // - Max duration of tasks (с)

sinput group "::: Stage 2. Grouping"
sinput string   stage2ExpertName_ = "Stage2.ex5";     // - Stage EA
sinput string   stage2Criterion_  = "6";              // - Optimization criterion for tasks
sinput long     stage2MaxDuration_ = 20;              // - Max duration of tasks (с)
//sinput bool     stage2UseClusters_= false;          // - Use clustering?
sinput double   stage2MinCustomOntester_ = 500;       // - Min norm. profit
sinput uint     stage2MinTrades_  = 20;               // - Min number of trades
sinput double   stage2MinSharpeRatio_ = 0.7;          // - Min Sharpe coeff.
sinput uint     stage2Count_      = 8;                // - Number of strategies in the group (1 - 16)

sinput group "::: Stage 3. Final"
sinput string   stage3ExpertName_ = "Stage3.ex5";     // - Stage EA
sinput ulong    stage3Magic_      = 27183;            // - Magic
sinput bool     stage3Tester_     = true;             // - For the tester?

Não podemos definir um tempo máximo para a terceira etapa, pois nela não é realizada uma otimização, mas uma única execução do testador de estratégias. Somente após sua conclusão são calculados os volumes normalizados das posições a serem abertas e gerada a string de inicialização do EA final. Se interrompermos essa execução antes do término, não conseguiremos obter seus resultados. Já na primeira e na segunda etapa, a interrupção antecipada da otimização apenas reduzirá o número total de execuções concluídas. Se o número de execuções concluídas ainda for suficiente, isso não representará um problema.

Para adicionar o novo campo à estrutura da tabela tasks, basta complementar a consulta SQL correspondente no arquivo de esquema do banco de dados de otimização db.opt.schema.sql:

-- Table: tasks
DROP TABLE IF EXISTS tasks;

CREATE TABLE tasks (
    id_task                INTEGER  PRIMARY KEY AUTOINCREMENT,
    id_job                 INTEGER  NOT NULL
                                    REFERENCES jobs (id_job) ON DELETE CASCADE
                                                             ON UPDATE CASCADE,
    optimization_criterion INTEGER  DEFAULT (7) 
                                    NOT NULL,
    start_date             DATETIME,
    finish_date            DATETIME,
    max_duration           INTEGER  NOT NULL
                                    DEFAULT 0,
    status                 TEXT     NOT NULL
                                    DEFAULT Queued
                                    CHECK (status IN ('Queued', 'Process', 'Done') ) 
);

Agora, vamos passar às alterações na biblioteca. As principais alterações serão feitas em dois arquivos. Primeiro, no arquivo da classe de tarefa de otimização Adwizard/Optimization/OptimizerTask.mqh, adicionaremos à estrutura usada para ler os parâmetros da tarefa no banco de dados um campo adicional para a duração máxima:

//+------------------------------------------------------------------+
//| Optimization task class                                          |
//+------------------------------------------------------------------+
class COptimizerTask {
protected:
   // ...

public:
   // Data structure for reading a single row of a query result 
   struct params {
      string         expert;
      int            optimization;
      string         from_date;
      string         to_date;
      int            forward_mode;
      string         forward_date;
      double         deposit;
      string         symbol;
      string         period;
      string         tester_inputs;
      ulong          id_task;
      int            optimization_criterion;
      long           max_duration;
   } m_params;

   // ...
};

Também adicionaremos esse campo à consulta que obtém do banco de dados as informações da tarefa:

//+------------------------------------------------------------------+
//| Get the next optimization task from the queue                    |
//+------------------------------------------------------------------+
void COptimizerTask::Load(ulong p_id) {
// Save task ID
   m_id = p_id;

// Request to get optimization task from queue by ID
   string query = StringFormat(
                     "SELECT s.expert,"
                     "       s.optimization,"
                     "       s.from_date,"
                     "       s.to_date,"
                     "       s.forward_mode,"
                     "       s.forward_date,"
                     "       s.deposit,"
                     "       j.symbol,"
                     "       j.period,"
                     "       j.tester_inputs,"
                     "       t.id_task,"
                     "       t.optimization_criterion,"
                     "       t.max_duration"
                     "  FROM tasks t"
                     "       JOIN"
                     "       jobs j ON t.id_job = j.id_job"
                     "       JOIN"
                     "       stages s ON j.id_stage = s.id_stage"
                     " WHERE t.id_task=%I64u;", m_id);

// Open the database
   if(DB::Connect(m_fileName)) {
      // Execute the request
      int request = DatabasePrepare(DB::Id(), query);

      // ...
   }
}

No método que verifica se a tarefa foi concluída, adicionaremos um bloco de código que verificará se a duração máxima da tarefa é diferente de zero. Nesse caso, ele obterá do banco de dados o tempo transcorrido desde o início da execução e o comparará com o limite especificado. Se a duração atual já tiver ultrapassado o valor máximo, será chamado o método de interrupção forçada da tarefa:

//+------------------------------------------------------------------+
//| Task completed?                                                  |
//+------------------------------------------------------------------+
bool COptimizerTask::IsDone() {
// If there is no current task, then everything is done
   if(m_id == 0) {
      return true;
   }

// Result
   bool res = false;

// If this is the EA optimization task
   if(m_type == TASK_TYPE_EX5) {
      // Check if the strategy tester has finished its work
      res |= MTTESTER::IsReady();

      // If the tester is running and the maximum duration is specified, then
      if(!res && m_params.max_duration > 0) {
         // Request to get the elapsed execution time of the current task
         string query = StringFormat("SELECT unixepoch(datetime()) - unixepoch(start_date) AS duration"
                                     "  FROM tasks"
                                     " WHERE id_task=%I64u;", m_id);
         
         // Get the execution time in seconds
         DB::Connect(m_fileName);
         long duration = StringToInteger(DB::GetValue(query));
         DB::Close();

         // If the execution time is greater than the maximum allowed, 
         if(duration > m_params.max_duration) {
            // Stop the task
            Stop();
         }
      }

      // If this is a task to run a Python program, then
   } else if(m_type == TASK_TYPE_PY) {
      // ...
      }
   } else {
      res = true;
   }

   return res;
}

No arquivo Adwizard/Optimization/OptimizationProject.mqh, adicionaremos aos métodos de criação das tarefas um parâmetro para informar a duração máxima:

//+------------------------------------------------------------------+
//| Optimization project class                                       |
//+------------------------------------------------------------------+
class COptimizationProject {
public:
   // ...
   
   // Add new tasks to the database for the specified optimization criteria
   COptimizationProject* AddTasks(string p_criterions, long p_maxDuration = 0);
   COptimizationProject* AddTasks(string &p_criterions[], long p_maxDuration = 0);

   // ...
};

//+------------------------------------------------------------------+
//| Add new tasks to the database for the specified                  |
//| optimization criteria in one string                              |
//+------------------------------------------------------------------+
COptimizationProject* COptimizationProject::AddTasks(string p_criterions, long p_maxDuration) {
// Array for optimization criteria
   string criterions[];
   StringReplace(p_criterions, ";", ",");
   StringSplit(p_criterions, ',', criterions);

   return AddTasks(criterions, p_maxDuration);
}

//+------------------------------------------------------------------+
//| Add new tasks to the database for the specified                  |
//| optimization criteria in the array                               |
//+------------------------------------------------------------------+
COptimizationProject* COptimizationProject::AddTasks(string &p_criterions[], long p_maxDuration) {
// For each job of the current stage
   FOREACH_AS(m_stage.jobs, m_job) {
      // For each optimization criterion
      FOREACH(p_criterions) {
         // Create a new task object for the job
         m_task = new COptimizationTask(0, m_job, (int) p_criterions[i], p_maxDuration);

         // Insert it into the optimization database
         m_task.Insert();

         // Add it to the array of all tasks
         APPEND(m_tasks, m_task);

         // Add it to the current job's tasks array
         APPEND(m_job.tasks, m_task);
      }
   }

   return &this;
}

No arquivo Adwizard/Optimization/OptimizationTask.mqh, resta adicionar um novo campo à classe COptimizationTask e inicializá-lo tanto no construtor quanto no método que insere os dados na tabela tasks do banco de dados:

//+------------------------------------------------------------------+
//| Optimization task class                                          |
//+------------------------------------------------------------------+
class COptimizationTask {
public:
   ulong             id_task;       // task ID
   ulong             id_job;        // job ID
   int               optimization;  // Optimization criterion
   long              maxDuration;   // Max duration
   string            status;        // Task status

   COptimizationJob* job;           // The job for the task will be launched for

   // Constructor
                     COptimizationTask(ulong p_taskId = 0, COptimizationJob* p_job = NULL,
                     int p_optimization = 6,
                     long p_maxDuration = 0,
                     string p_status = "Done");

   // Create a task in the database
   void              Insert();
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
COptimizationTask::COptimizationTask(ulong p_taskId = 0,
                                     COptimizationJob* p_job = NULL,
                                     int p_optimization = 6,
                                     long p_maxDuration = 0,
                                     string p_status = "Done") :
   id_task(p_taskId),
   job(p_job),
   id_job(!!p_job ? p_job.id_job : 0),
   optimization(p_optimization),
   maxDuration(p_maxDuration),
   status(p_status) {}

//+------------------------------------------------------------------+
//| Create a task in the database                                    |
//+------------------------------------------------------------------+
void COptimizationTask::Insert() {
   string query = StringFormat("INSERT INTO tasks "
                               " VALUES (NULL,%I64u,%d,NULL,NULL,%I64d,'%s');",
                               id_job, optimization, maxDuration, status);

   id_task = DB::Insert(query);
   PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_task);
}
//+------------------------------------------------------------------+

Agora podemos compilar os dois EAs: CreateProject.ex5, que cria no banco de dados os projetos de otimização com o tempo máximo das tarefas nas duas primeiras etapas, e Optimization.ex5, que inicia a execução do pipeline.


Exibição de informações durante a otimização

Como nossa prioridade anterior era garantir o funcionamento correto do EA do pipeline de otimização automática, a exibição de informações sobre o andamento desse processo ficou em segundo plano. Utilizamos a função padrão Comment(), que permite exibir texto em fonte pequena no canto superior esquerdo do gráfico em que o EA está sendo executado. A única informação exibida era o identificador da tarefa de otimização atual. Agora que o funcionamento principal do EA de otimização já está razoavelmente estabilizado, podemos dedicar alguma atenção a aspectos menos importantes. Além disso, já dispomos de um componente pronto para exibir textos no gráfico do EA de forma mais flexível: a classe CConsoleDialog. Vale lembrar que um objeto dessa classe permite criar uma janela de diálogo que ocupa todo o gráfico, com botões para minimizar e fechar, e exibe um texto multilinha com rolagem e ajuste de escala. Vamos utilizá-lo.

Para implementar essa funcionalidade, faremos as seguintes alterações. No arquivo de biblioteca incluído pelo EA de otimização, Adwizard/Experts/Optimization.mqh, criaremos um ponteiro global para um objeto dessa classe. Na função de inicialização, instanciaremos o objeto de diálogo e chamaremos o método que inicia sua execução:

CConsoleDialog *dialog;                      // Dialog for displaying text with data

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// If the database file is not specified, then exit
   if(fileName_ == "") {
      PrintFormat(__FUNCTION__" | ERROR: Set const OPT_FILEMNAME with filename of DB in project", 0);
      return INIT_FAILED;
   }

// Create an optimizer
   optimizer = new COptimizer(fileName_, pythonPath_);

// Create and launch a dialog to display information
   dialog = new CConsoleDialog();
   dialog.Create(__FILE__);
   dialog.Run();

// Create the timer and start its handler
   EventSetTimer(2);
   OnTimer();

   return(INIT_SUCCEEDED);
}

No manipulador do temporizador, atualizaremos o objeto de diálogo com o novo texto gerado pelo objeto otimizador:

//+------------------------------------------------------------------+
//| Expert timer function                                            |
//+------------------------------------------------------------------+
void OnTimer() {
   
// Start the optimizer handling
   optimizer.Process();

   dialog.Text(optimizer.Text());
}

Também adicionaremos a função de tratamento de eventos do gráfico OnChartEvent(), que encaminhará os eventos ao objeto de diálogo para permitir a interação com o usuário:

//+------------------------------------------------------------------+
//| Event handling                                                   |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // event ID
                  const long & lparam,  // event parameter of the long type
                  const double & dparam, // event parameter of the double type
                  const string & sparam) { // event parameter of the string type

   if(!!dialog && !IsStopped()) {
      dialog.ChartEvent(id, lparam, dparam, sparam);
   }
}

Por fim, não devemos esquecer de excluir o objeto de diálogo criado quando o EA for encerrado e redesenhar o gráfico:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   PrintFormat(__FUNCTION__" | Reason: %d", reason);
   EventKillTimer();

// Remove the optimizer
   if(!!optimizer) {
      delete optimizer;
   }   
   
// Remove the dialog
   if(!!dialog) {
      dialog.Destroy();
      delete dialog;
      ChartRedraw();
   }
}

Agora precisamos montar o texto que será exibido na tela durante a otimização. Faremos isso no método Text() do objeto otimizador, no arquivo Adwizard/Optimization/Optimizer.mqh:

//+------------------------------------------------------------------+
//| Information about the current optimization state                 |
//+------------------------------------------------------------------+
string COptimizer::Text(void) {
   string text = "";

   // Get the number of projects with different statuses
   DB::Connect(m_fileName);
   int process_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Process'");
   int queued_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Queued'");
   int done_projects_count = (int) DB::GetValue("SELECT count(status) FROM projects WHERE status = 'Done'");
   int total_projects_count = process_projects_count + queued_projects_count + done_projects_count;
   DB::Close();

   // Add this to the message text
   text += StringFormat("DB: %s | %d Projects (Process: %d, Queued: %d, Done: %d)\n",
                        m_fileName,
                        total_projects_count,
                        process_projects_count,
                        queued_projects_count,
                        done_projects_count
                       );

   // If there is an active project 
   if(process_projects_count > 0) {
      // Add information about the current task to the message text
      text += m_task.Text();

      // And the total number of tasks in the queue
      if(m_totalTasks)
         text += StringFormat(
                    "Total tasks in queue: %d\n",
                    m_totalTasks);
   }

   return text;
}

As informações sobre a tarefa atual serão obtidas por meio do método Text() da classe COptimizerTask. No arquivo Adwizard/Optimization/OptimizerTask.mqh, faremos esse método obter informações sobre o projeto, a etapa e o trabalho aos quais pertence a tarefa de otimização atual. Também calcularemos o tempo transcorrido desde o início da tarefa e o tempo restante até sua conclusão, caso tenha sido definido um tempo máximo de execução:

//+------------------------------------------------------------------+
//| Current task info                                                |
//+------------------------------------------------------------------+
string COptimizerTask::Text() {
   string text = "";

   // If there is an active task
   if(m_params.id_task) {
      DB::Connect(m_fileName);

      // Add information about the project
      text += StringFormat("═════════════════════════════════════════════════════════════════════════\n"
                           "PROJECT: %s v. %s\n%s\n\n",
                           DB::GetValue("SELECT name FROM projects WHERE status = 'Process' LIMIT 1"),
                           DB::GetValue("SELECT version FROM projects WHERE status = 'Process'  LIMIT 1"),
                           DB::GetValue("SELECT description FROM projects WHERE status = 'Process'  LIMIT 1")
                          );

      // Request to get all information about the task
      string query = "SELECT s.name, s.expert, s.from_date, s.to_date, "
                     "  j.symbol, j.period, t.optimization_criterion, t.start_date, "
                     "  time(max_duration, 'unixepoch') AS max_duration,"
                     "  time(unixepoch('now') - unixepoch(t.start_date), 'unixepoch') AS elapsed_time,"
                     "  time(MAX(0, max_duration - (unixepoch('now') - unixepoch(t.start_date))), 'unixepoch') AS remaining_time"
                     "  FROM stages s"
                     "       JOIN"
                     "       projects p ON s.id_project = p.id_project AND"
                     "                     p.status = 'Process' AND"
                     "                     s.expert IS NOT NULL"
                     "     JOIN jobs j ON j.id_stage = s.id_stage"
                     "     JOIN tasks t ON t.id_job = j.id_job AND t.status = 'Process';";

      // Execute the request
      int request = DatabasePrepare(DB::Id(), query);

      struct Row {
         string stage_name;
         string expert_name;
         string from_date;
         string to_date;
         string symbol;
         string timeframe;
         int optimization_criterion;
         string start_date;
         string max_duration;
         string elapsed_time;
         string remainig_time;
      } row;

      // If there is no error
      if(request != INVALID_HANDLE) {

         // Read data from the first line of the result and add it to the text
         if(DatabaseReadBind(request, row)) {
            text += StringFormat("TASK #%I64u:\n"
                                 " %10.10s │ %14.14s │ %-23s │ %6.6s │ %-3.3s │ %15.15s │ %-10.10s │ %-10.10s\n"
                                 "────────────┼────────────────┼─────────────────────────┼────────┼─────┼─────────────────┼────────────┼─────────────\n"
                                 " %10.10s │ %14.14s │ %s - %s │ %6.6s │ %-3.3s │ %15.15s │ %-10.10s │ %-10.10s \n\n"
                                 "═════════════════════════════════════════════════════════════════════════\n",
                                 m_id,
                                 "Stage",
                                 "Expert",
                                 "Testing period",
                                 "Symbol",
                                 "TF",
                                 "Criterion",
                                 "Max Durat.",
                                 "Remaining",
                                 row.stage_name,
                                 row.expert_name,
                                 row.from_date,
                                 row.to_date,
                                 row.symbol,
                                 row.timeframe,
                                 s_criterionNames[row.optimization_criterion],
                                 m_params.max_duration ? row.max_duration : "Unlimited",
                                 row.remainig_time
                                );
         }
      }
      DatabaseFinalize(request);

      DB::Close();
   }

   return text;
}

Os dados obtidos sobre a tarefa serão organizados em uma tabela de texto. Com isso, concluímos as alterações necessárias para implementar essa funcionalidade e podemos passar aos testes.


Testes

Vamos executar o EA de otimização SimpleCandles/Optimization/Optimization.ex5 e veremos algo semelhante ao seguinte:

Para esta captura de tela, definimos o tempo máximo permitido para a execução de uma tarefa de otimização como 3 minutos, ou 180 segundos. Realizaremos as otimizações em dois símbolos e dois timeframes, usando um intervalo temporal de quatro meses.

Na primeira etapa, executaremos três vezes a otimização para cada combinação de símbolo e timeframe. Portanto, nessa etapa, será necessário executar 2 símbolos × 3 timeframes × 3 otimizações = 18 tarefas. 

Na segunda etapa, executaremos uma otimização para selecionar um bom grupo para cada combinação de símbolo e timeframe. Assim, nessa etapa, será necessário executar 2 símbolos × 3 timeframes = 6 tarefas.

Na terceira etapa, é executada uma única tarefa final. Portanto, o número total de tarefas é 18 + 6 + 1 = 25. Para todas elas, exceto a última, definimos um tempo máximo de execução. Se estimarmos também em 3 minutos o tempo de execução da última tarefa, poderemos calcular aproximadamente quanto durará todo o pipeline de otimização automática: 25 × 3 = 75 minutos = 1 hora e 15 minutos. Naturalmente, isso é muito menos do que os dias ou semanas que o pipeline poderia levar. Mas vamos experimentar uma configuração ainda mais extrema.

Vamos limitar o tempo máximo de execução a apenas 20 segundos. Nesse caso, a duração total da otimização automática será ainda menor, ficando em torno de 25 × 20 segundos = 500 segundos = 8 minutos e 20 segundos.

Após esse breve período, todas as tarefas de otimização estarão concluídas. No terminal MetaTrader 5 em que executamos o processo, podemos ver os resultados da tarefa da terceira e última etapa, na qual foi realizada uma única execução do EA que reúne 2 símbolos × 3 timeframes × 8 instâncias por grupo = 48 instâncias individuais da estratégia de negociação SimpleCandles:

Esses resultados ainda não estão normalizados. Isso significa que, ao longo desses quatro meses, o drawdown foi inferior a US$ 500, enquanto o valor máximo esperado era de US$ 1.000, ou 10%. Portanto, podemos aumentar aproximadamente duas vezes o tamanho das posições abertas sem ultrapassar o limite de 10% de drawdown nesse intervalo. Essa operação já foi realizada ao final da terceira etapa, e as informações sobre o tamanho das posições, já com essa normalização aplicada, foram gravadas em um banco de dados separado do EA final.

Além disso, na terceira etapa não foi utilizado nem o risk manager de fechamento nem o gerenciador de fechamento.

De acordo com a convenção adotada anteriormente, o nome do arquivo de banco de dados do EA final é gerado segundo um algoritmo bem definido, com base no nome do projeto, no número mágico especificado nos parâmetros do projeto e no sufixo ".test". Em nosso caso específico, após a conclusão da otimização automática, foi criado na pasta comum de dados do terminal o arquivo SimpleCandles-27183.test.db.sqlite.

Vamos examinar o conteúdo do banco de dados do EA final. Ele contém quatro tabelas, mas, por enquanto, interessam-nos apenas as duas últimas:

Na tabela strategy_groups, há um registro correspondente ao grupo de estratégias criado, com o identificador id_group=1. Caso executemos novamente o pipeline de otimização automática para esse mesmo projeto, novas linhas com outros identificadores serão adicionadas a essa tabela. Precisaremos informar o identificador do grupo de estratégias nos parâmetros do EA final. Na tabela strategies, foram adicionadas as instâncias individuais das estratégias de negociação pertencentes a determinado grupo. As demais tabelas serão utilizadas pelo EA final somente durante sua operação em uma conta de negociação.

Vamos compilar o arquivo do EA final SimpleCandles/SimpleCandles.mq5 e testá-lo usando o identificador existente do grupo de estratégias, sem atualização automática, com o risk manager e o gerenciador de fechamento desativados e com o número mágico 27183:

Obtemos os seguintes resultados:

Agora, o drawdown já se aproxima do valor máximo esperado definido nas configurações do EA final, enquanto o lucro aumentou na mesma proporção, acompanhando a duplicação aproximada do tamanho das posições.



Conclusão

O trabalho realizado neste artigo permitiu melhorar significativamente e tornar mais prático o uso do nosso pipeline de otimização automática. Não adicionamos novas funcionalidades à lógica de negociação, mas nos concentramos na solução de problemas práticos encontrados durante a operação do sistema.

Continuamos avançando na separação entre a biblioteca e a parte específica do projeto. Agora, para criar um novo projeto baseado em outra estratégia de negociação, basta descrever seus parâmetros na parte do projeto, sem alterar o núcleo comum. Esse processo ainda não foi concluído e terá continuidade no futuro.

A implementação do mecanismo que limita o tempo de execução das tarefas de otimização nos proporcionou uma ferramenta eficaz para controlar a duração de todo o pipeline. Agora podemos equilibrar de forma flexível a profundidade da otimização e o tempo necessário, interrompendo as otimizações das duas primeiras etapas quando atingirem o limite definido. Isso é essencial para testes rápidos e para o desenvolvimento iterativo.

A integração do componente CConsoleDialog para exibir informações detalhadas sobre o andamento da otimização transformou o simples acompanhamento dos identificadores das tarefas em um monitoramento prático e claro. Agora podemos ver em tempo real em qual etapa o pipeline está operando e com qual símbolo e timeframe, quanto tempo resta para a conclusão da tarefa atual e qual é o andamento geral.

Assim, todo o ciclo, desde a criação do banco de dados do projeto e a execução do pipeline até a obtenção de um EA final pronto para testes e com parâmetros normalizados, foi demonstrado na prática. A execução "extrema", com limites de tempo muito curtos, mostrou claramente a viabilidade da abordagem: mesmo com limites rigorosos de duração para cada tarefa, o sistema consegue concluir todo o ciclo de otimização e gerar um resultado funcional. Isso abre novas possibilidades para a prototipagem rápida e a validação de novas estratégias de negociação.

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


Aviso importante

Todos os resultados apresentados neste artigo e em todos os artigos anteriores deste ciclo baseiam-se exclusivamente em dados de testes em histórico e não constituem garantia de qualquer tipo de rentabilidade futura. O trabalho realizado no âmbito deste projeto tem caráter de pesquisa. Todos os resultados publicados podem ser utilizados livremente por qualquer interessado, por sua própria conta e risco.


Conteúdo do arquivo compactado
#
 Nome
Versão  Descrição  Últimas alterações
  SimpleCandles   Pasta de trabalho do projeto
(dentro de MQL5/Shared Projects)
 
SimpleCandles.mq5
1.03
EA final para execução paralela de múltiplos grupos de estratégias-modelo. Os parâmetros são obtidos da biblioteca interna de grupos.
Parte 29
  Optimization
  Pasta dos EAs de otimização do projeto  
2    CreateProject.mq5 1.05 EA-script para criação de projeto com etapas, trabalhos e tarefas de otimização.
Parte 29
3    Optimization.mq5 1.03
EA para otimização automática de projetos
Parte 29
4    Stage1.mq5 1.02
EA de otimização de uma única instância de estratégia de negociação (Etapa 1)
Parte 25
5    Stage2.mq5 1.01
EA de otimização de grupo de instâncias de estratégias de negociação (Etapa 2)
Parte 25
6    Stage3.mq5 1.01
EA que grava o grupo normalizado de estratégias formado no banco de dados do Expert Advisor com o nome especificado.  Parte 25
  Strategies   Pasta das estratégias do projeto
Parte 25
7    SimpleCandlesStrategy.mqh
1.03
Classe da estratégia de negociação SimpleCandles
Parte 29
  Adwizard   Pasta da biblioteca Adwizard
(dentro de MQL5/Shared Projects)
 
  Base
  Classes base das quais derivam as demais classes do projeto    
8    Advisor.mqh 1.04 Classe base do EA Parte 10
9    Factorable.mqh
1.06
Classe base de objetos criados a partir de uma string
Parte 28
10    FactorableCreator.mqh
1.00 Classe de criadores que associam nomes a construtores estáticos de classes derivadas de CFactorable Parte 24
11    Interface.mqh 1.01
Classe base para visualização de diversos objetos
Parte 4
12    Receiver.mqh
1.04  Classe base para conversão de volumes abertos em posições de mercado
Parte 12
13    Strategy.mqh
1.04
Classe base para estratégia de negociação
Parte 10
  Database
  Arquivos para acesso a todos os tipos de bancos de dados utilizados pelos EAs do projeto
 
14    Database.mqh 1.13 Classe para trabalhar com banco de dados Parte 29
15    db.adv.schema.sql 1.00
Esquema do banco de dados do EA final Parte 22
16    db.cut.schema.sql
1.00 Esquema do banco de dados reduzido de otimização
Parte 22
17    db.opt.schema.sql
1.06  Esquema do banco de dados de otimização
Parte 29
18    Storage.mqh   1.01
Classe para trabalhar com armazenamento do tipo Key-Value para o EA final no banco de dados do expert
Parte 23
  Experts
  Arquivos com os componentes comuns aos diferentes tipos de EA utilizados
 
19    Expert.mqh  1.24 Arquivo de biblioteca para o EA final. Os parâmetros de grupos podem ser obtidos do banco de dados do expert
Parte 28
20    Optimization.mqh  1.06 Arquivo de biblioteca para o EA que gerencia a execução de tarefas de otimização
Parte 29
21    Stage1.mqh
1.19 Arquivo de biblioteca para o EA de otimização de uma instância individual de estratégia de negociação (Etapa 1)
Parte 23
22    Stage2.mqh 1.04 Arquivo de biblioteca para o EA de otimização de um grupo de instâncias de estratégias de negociação (Etapa 2)   Parte 23
23    Stage3.mqh
1.04 Arquivo de biblioteca para o EA que grava o grupo de estratégias normalizadas formado no banco de dados do Expert com o nome especificado. Parte 23
  Optimization
  Classes responsáveis pelo funcionamento da otimização automática
 
24    OptimizationJob.mqh 1.00 Classe que representa um trabalho de uma etapa do projeto de otimização
Parte 25
25    OptimizationProject.mqh 1.00 Classe para o projeto de otimização Parte 25
26    OptimizationStage.mqh 1.00 Classe para a etapa do projeto de otimização Parte 25
27    OptimizationTask.mqh 1.01 Classe para a tarefa de otimização (para criação) Parte 29
28    Optimizer.mqh
1.04  Classe para o gerenciador da otimização automática de projetos
Parte 29
29    OptimizerTask.mqh
1.06
Classe para a tarefa de otimização (para o pipeline)
Parte 29
  Strategies    Exemplos de estratégias de negociação usadas para demonstrar o funcionamento do projeto
 
24    HistoryStrategy.mqh 
1.00 Classe da estratégia de negociação para reprodução do histórico de operações
Parte 16
25    SimpleVolumesStrategy.mqh
1.11
Classe da estratégia de negociação com uso de volumes de ticks
Parte 22
  Utils
  Utilitários auxiliares e macros para reduzir o código

26    ConsoleDialog.mqh 1.01 Classe para exibição de informações textuais no gráfico Parte 28
26    ExpertHistory.mqh 1.00 Classe para exportar o histórico de operações para um arquivo Parte 16
27    Macros.mqh 1.07 Macros úteis para operações com arrays Parte 26
28     MTTester.mqh 
Arquivo para integração com o testador de estratégias da biblioteca MultiTester
Parte 28
29    NewBarEvent.mqh 1.00  Classe para detecção de nova barra para um símbolo específico  Parte 8
30    SymbolsMonitor.mqh  1.01 Classe para obtenção de informações sobre instrumentos de negociação (símbolos) Parte 28
  Virtual
  Classes para criação de diversos objetos, unificados pelo uso do sistema de ordens e posições virtuais de trading

31    Money.mqh 1.01  Classe base para gerenciamento de capital
Parte 12
32    TesterHandler.mqh  1.07 Classe para tratamento de eventos de otimização  Parte 23
33    VirtualAdvisor.mqh  1.12  Classe do expert que trabalha com posições (ordens) virtuais Parte 28
34    VirtualChartOrder.mqh  1.02  Classe da posição virtual gráfica Parte 28
35    VirtualCloseManager.mqh 1.00 Classe do gerenciador de fechamento Parte 28
36    VirtualHistoryAdvisor.mqh 1.00  Classe do expert para reprodução do histórico de operações  Parte 16
37    VirtualInterface.mqh  1.00  Classe da interface gráfica do EA  Parte 4
38    VirtualOrder.mqh 1.09  Classe de ordens e posições virtuais  Parte 22
39    VirtualReceiver.mqh 1.04 Classe que converte volumes abertos em posições de mercado (receptor)  Parte 23
40    VirtualRiskManager.mqh  1.06 Classe de gerenciamento de risco (risk manager)  Parte 28
41    VirtualStrategy.mqh 1.09  Classe da estratégia de negociação com posições virtuais  Parte 23
42    VirtualStrategyGroup.mqh  1.04  Classe de um grupo de estratégias de negociação ou de grupos de estratégias Parte 28
43    VirtualSymbolReceiver.mqh  1.00 Classe do receptor simbólico  Parte 3
  Common/Files   Pasta comum de dados dos terminais MetaTrader 5  
44 article.17607.db.sqlite Banco de dados de otimização
Parte 29
45 SimpleCandles-27183.test.db.sqlite

Banco de dados do EA final
Parte 29

O código-fonte também está disponível nos repositórios públicos SimpleCandles e Adwizard

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

Arquivos anexados |
MQL5.zip (2157.63 KB)
Redes neurais em trading: Dos transformers aos neurônios spiking (Componentes principais) Redes neurais em trading: Dos transformers aos neurônios spiking (Componentes principais)
Apresentamos ao leitor uma implementação das abordagens do framework SpikingBrain com base em atenção linear recorrente com gates, analisada em detalhes neste artigo. Os algoritmos de propagação para frente, propagação dos gradientes e atualização dos pesos proporcionam um processamento eficiente de séries temporais financeiras e permitem colocar em prática as principais ideias do framework.
Previsão da distribuição condicional com uma MLP Previsão da distribuição condicional com uma MLP
Neste artigo, analisaremos um modelo de regressão baseado em MLP que prevê não apenas a esperança matemática condicional, mas também a variância condicional. Em outras palavras, treinaremos nossa rede para prever toda a distribuição dos preços futuros com base em um vetor de atributos de entrada. Para isso, porém, precisaremos implementar nossa própria função de perda.
Negociando opções sem opções (Parte 3): Estratégias complexas com opções Negociando opções sem opções (Parte 3): Estratégias complexas com opções
São analisadas estratégias com opções para mercado lateral (não direcionais) e para mercado em tendência (direcionais), bem como sua implementação em MQL5. O EA desenvolvido no artigo anterior é aprimorado com a exibição dos níveis de opções. Agora é hora de analisar o funcionamento e implementar as estratégias que os traders de opções utilizam na prática.
Redes neurais em trading: dos Transformers aos neurônios de disparo (SpikingBrain) Redes neurais em trading: dos Transformers aos neurônios de disparo (SpikingBrain)
O framework SpikingBrain apresenta uma abordagem singular para o processamento de dados: seus neurônios reagem apenas a eventos relevantes, filtrando o ruído com eficiência. Sua arquitetura orientada a eventos reduz os custos computacionais sem perder informações essenciais sobre os movimentos do mercado. Os limiares adaptativos e a possibilidade de utilizar módulos pré-treinados conferem flexibilidade e escalabilidade ao modelo.