Desenvolvendo um EA multimoeda (Parte 29): Aprimoramento do pipeline
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.
| # | Nome | Versão | Descrição | Últimas alterações |
|---|---|---|---|---|
| SimpleCandles | Pasta de trabalho do projeto (dentro de MQL5/Shared Projects) | |||
| 1 | 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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Redes neurais em trading: Dos transformers aos neurônios spiking (Componentes principais)
Previsão da distribuição condicional com uma MLP
Negociando opções sem opções (Parte 3): Estratégias complexas com opções
Redes neurais em trading: dos Transformers aos neurônios de disparo (SpikingBrain)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso