Desenvolvendo um EA multimoeda (Parte 22): Início da transição para substituição dinâmica de configurações
Introdução
Nas duas partes anteriores do nosso ciclo de artigos, fizemos uma preparação importante para os experimentos com a otimização automática de EAs. A atenção principal foi voltada para a criação de um pipeline de otimização, que por enquanto é composto por três etapas:
- Otimização de instâncias individuais de estratégias para combinações específicas de símbolos e timeframes.
- Formação de grupos com as melhores instâncias individuais obtidas na primeira etapa.
- Geração da string de inicialização do EA final, unindo os grupos formados, e seu salvamento na biblioteca.
Para permitir a automação da criação do próprio pipeline, foi desenvolvido um EA-script especializado. Ele permite preencher o banco de dados com projetos de otimização, criando para eles etapas, trabalhos e tarefas com base em parâmetros e modelos definidos. Essa abordagem permite a execução posterior das tarefas de otimização em uma ordem determinada, avançando de uma etapa para outra.
Também buscamos formas de aumentar a performance, utilizando profiling e otimização de código. O foco principal foi no trabalho com objetos responsáveis por obter informações sobre os instrumentos financeiros (símbolos). Isso possibilitou uma redução significativa na quantidade de chamadas a métodos para obter dados sobre preços e especificações dos símbolos.
Como resultado, obtivemos dados automaticamente que podem ser usados para novos experimentos e análises. Isso abre caminho para testar hipóteses sobre como a frequência e a ordem da reotimização podem afetar a eficiência da negociação. Mas esse caminho ainda precisa ser percorrido.
Nesta nova artigo, vamos nos aprofundar na implementação de um novo mecanismo de carregamento de parâmetros dos EAs finais, que deve permitir substituir parcial ou totalmente a composição e os parâmetros das instâncias individuais das estratégias de negociação, tanto em uma execução única no testador de estratégias quanto durante a operação do EA final na conta de negociação.
Traçando o caminho
Vamos tentar descrever com mais detalhes, em palavras, o que queremos alcançar. No cenário ideal, o funcionamento do sistema deve se parecer com isto:
- Um projeto é gerado com a data atual como data de término do período de otimização.
- O projeto é iniciado no pipeline. Sua execução leva algum tempo, variando de alguns dias a algumas semanas.
- Os resultados são carregados no EA final. Se o EA final ainda não estiver operando, ele é iniciado na conta real. Se ele já estiver rodando na conta, então seus parâmetros são substituídos pelos novos, obtidos após a conclusão da execução do pipeline pelo último projeto.
- Retornamos ao item 1.
Vamos analisar cada um desses pontos. Para implementar o primeiro item, já temos o EA-script de geração de projeto da parte anterior, no qual podemos, usando parâmetros, escolher a data de término da otimização. Mas ele ainda é iniciado apenas manualmente. Isso pode ser corrigido com a adição de uma etapa extra no pipeline de execução do projeto, que gera um novo projeto após o término de todas as etapas do projeto atual. Assim, manualmente, só precisamos iniciá-lo uma única vez.
Para o segundo item, basta termos um terminal com o EA Optimization.ex5 instalado, com os parâmetros configurados apontando para o banco de dados desejado. Assim que surgirem novas tarefas de projeto ainda não executadas no banco, elas serão iniciadas automaticamente, conforme a ordem de fila. A última etapa, anterior à criação de um novo projeto, deve de alguma forma transferir os resultados da otimização do projeto para o EA final.
O terceiro item é o mais complexo. Já fizemos uma versão da transferência de parâmetros para o EA final, mas ela ainda exige operações manuais: é necessário iniciar um EA separado que exporta a biblioteca de parâmetros para um arquivo, depois copiar esse arquivo para a pasta do projeto, e então recompilar o EA final. Embora agora possamos automatizar essas etapas via código, a estrutura como um todo começa a parecer exageradamente complicada. Seria desejável ter algo mais simples e confiável.
Outro problema do método atual de transferência de parâmetros para o EA final é a impossibilidade de substituição parcial dos parâmetros. Apenas uma substituição completa é possível, o que leva ao fechamento de todas as posições abertas, se existirem, e o início de uma nova operação do zero. E essa limitação é fundamentalmente impossível de resolver dentro do método existente.
Lembrando que por "parâmetros" nos referimos aos parâmetros de um grande número de instâncias de estratégias individuais que operam em paralelo dentro de um único EA final. Se os parâmetros antigos forem substituídos de uma vez pelos novos (mesmo que, em sua maioria, coincidam com os antigos), a implementação atual provavelmente não conseguirá carregar corretamente as informações das posições virtuais que estavam abertas anteriormente. Isso só será possível se houver uma correspondência exata na quantidade e na ordem dos parâmetros das instâncias individuais, tal como estavam na string de inicialização do EA final.
Para permitir a substituição parcial de parâmetros, é necessário de alguma forma organizar a existência simultânea tanto dos parâmetros antigos quanto dos novos. Nesse caso, podemos desenvolver um algoritmo de transição gradual, mantendo parte das instâncias individuais sem alterações. Suas posições virtuais devem continuar ativas. As posições das instâncias que não estiverem entre os novos parâmetros devem ser encerradas corretamente. E as novas instâncias, recém-adicionadas, devem iniciar a operação do zero.
Parece que estamos diante de mudanças mais profundas do que gostaríamos. Mas fazer o quê, se não há outra forma clara de alcançar o resultado desejado, é melhor aceitar logo a necessidade de mudanças. Se continuarmos seguindo por um caminho que não é o mais apropriado, quanto mais avançarmos, mais difícil será mudar de direção.
Então, chegou a hora de passar para o lado sombrio do armazenamento de todas as informações sobre o funcionamento do EA em um banco de dados. E isso implica em um banco de dados separado, pois os bancos de dados utilizados na otimização se tornam extremamente pesados (chegando a vários gigabytes por projeto). Não faz sentido mantê-los acessíveis ao EA final, já que apenas uma fração ínfima das informações neles contidas será realmente necessária durante a operação.
Além disso, seria desejável que conseguíssemos reestruturar a ordem de execução das etapas da otimização automática. Pro esse conceito já foi mencionado na parte 20, sob o nome de agrupamento por símbolo e timeframe. Mas, naquela ocasião, optamos por não seguir esse caminho, já que sem a possibilidade de substituição parcial de parâmetros, esse tipo de ordenamento não fazia sentido. Agora, se tudo der certo, ele pode se tornar a opção mais vantajosa. Mas antes de tudo, vamos tentar realizar a transição para o uso de um banco de dados separado para o EA final, com suporte à substituição dinâmica de parâmetros das instâncias individuais das estratégias de negociação.
Transformação da string de inicialização
A tarefa proposta é bastante extensa, então vamos avançar com pequenos passos. Comecemos com o fato de que, no banco de dados do EA, precisaremos armazenar informações sobre as instâncias individuais das estratégias de negociação. Atualmente, essa informação está representada na string de inicialização do EA. O EA pode obtê-la de duas formas: ou a partir do banco de dados de otimização, ou a partir dos dados embutidos no código do próprio EA (constantes de texto), retirados da biblioteca de parâmetros durante a etapa de compilação. A primeira forma é usada nos EAs de otimização (SimpleVolumesStage2.mq5 e SimpleVolumesStage3.mq5), enquanto a segunda forma é utilizada no EA final (SimpleVolumesExpert.mq5).
Queremos adicionar uma terceira forma: a string de inicialização deve ser dividida em partes, cada uma correspondente a uma instância individual de uma estratégia de negociação, e essas partes devem ser salvas no banco de dados do EA. Depois, o EA poderá ler essas partes do seu banco de dados e montar a partir desses pedaços a string de inicialização completa. Ela será usada para criar o objeto do expert, que executará todo o trabalho posterior.
Para entender como podemos dividir a string de inicialização, vamos observar um exemplo típico da string apresentado no artigo anterior. Ela é bastante longa (~200 linhas), por isso mostraremos apenas a parte mínima necessária para dar uma ideia de sua estrutura.
class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.00,1.30,80,3200.00,930.00,12000,3) ],8.428150), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.20,140,2200.00,1220.00,19000,3) ],12.357884), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.20,0.10,0,1800.00,780.00,8000,3) ],4.756016), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,0.30,0.10,150,4400.00,1000.00,1000,3) ],4.459508), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.50,1.10,200,2800.00,1030.00,32000,3) ],5.021593), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.70,100,200.00,1640.00,32000,3) ],18.155410), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.40,160,8400.00,1080.00,44000,3) ],4.313320), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.00,110,3600.00,1030.00,53000,3) ],4.490144), ],4.615527), class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.80,240,4800.00,1620.00,57000,3) ],6.805962), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.80,40,400.00,930.00,53000,3) ],11.825922), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,212,1.30,1.50,160,600.00,1000.00,28000,3) ],16.866251), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.30,1.50,30,3000.00,1280.00,28000,3) ],5.824790), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.30,0.10,10,2000.00,780.00,1000,3) ],3.476085), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,16000.00,700.00,11000,3) ],4.522636), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.40,1.80,80,2200.00,360.00,25000,3) ],8.206812), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,19200.00,700.00,44000,3) ],2.698618), ],5.362505), class CVirtualStrategyGroup([ ... ],5.149065), ... class CVirtualStrategyGroup([ ... ],2.718278), ],2.072066)
Essa string de inicialização é composta por grupos aninhados de estratégias de negociação de primeiro, segundo e terceiro nível. As instâncias individuais das estratégias de negociação estão inseridas apenas nos grupos de terceiro nível. Cada instância possui seus parâmetros. Cada grupo possui um multiplicador de escala, presente no primeiro, segundo e terceiro nível. O uso desses multiplicadores foi explicado na parte 5. Eles servem para normalizar o rebaixamento máximo alcançado no período de teste até o valor de 10%. Além disso, o valor do multiplicador da escala de um grupo que contém vários subgrupos ou várias instâncias de estratégias é, primeiro, dividido pelo número de elementos nesse grupo e, depois, esse novo multiplicador é aplicado a todos os elementos internos. Veja como isso aparece no código do arquivo VirtualStrategyGroup.mqh:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) { // Save the initialization string m_params = p_params; ... // Read the scaling factor m_scale = ReadDouble(p_params); // Correct it if necessary if(m_scale <= 0.0) { m_scale = 1.0; } if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) { // If we filled the array of groups, and the array of strategies is empty, then PrintFormat(__FUNCTION__" | Scale = %.2f, total groups = %d", m_scale, ArraySize(m_groups)); // Scale all groups Scale(m_scale / ArraySize(m_groups)); } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) { // If we filled the array of strategies, and the array of groups is empty, then PrintFormat(__FUNCTION__" | Scale = %.2f, total strategies = %d", m_scale, ArraySize(m_strategies)); // Scale all strategies Scale(m_scale / ArraySize(m_strategies)); } else { // Otherwise, report an error in the initialization string SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params)); } }
Dessa forma, a string de inicialização possui uma estrutura hierárquica, onde os níveis superiores são ocupados por grupos de estratégias, e o nível mais inferior contém as próprias estratégias. Embora um grupo de estratégias possa conter várias estratégias, no desenvolvimento do projeto percebemos que é mais conveniente, no nível inferior, usar não várias estratégias em um único grupo, mas encapsular cada instância de estratégia em seu próprio grupo individual. Daí vem o terceiro nível. Os dois primeiros níveis são resultado da primeira etapa de agrupamento após a otimização inicial, e depois da segunda etapa de agrupamento realizada no pipeline.
Nós, claro, poderíamos criar no banco de dados uma estrutura de tabelas que preservasse a hierarquia existente entre estratégias e grupos, mas será que isso é realmente necessário? Na prática, não. A estrutura hierárquica é útil no pipeline de otimização. Quando se trata do funcionamento do EA final na conta de negociação, o que importa é apenas a lista de instâncias individuais das estratégias de negociação, com os multiplicadores de escala devidamente recalculados. E para armazenar essa lista no banco de dados, basta uma tabela simples. Por isso, vamos adicionar um método que preencha essa lista a partir da string de inicialização, e outro método que faça o caminho inverso, isto é, a partir da lista de instâncias individuais das estratégias, com seus respectivos multiplicadores, gerar a string de inicialização para o EA final.
Exportação da lista de estratégias
Vamos começar com o método para obter a lista de estratégias do expert. Esse método deve ser um método da classe do expert, pois nela temos todas as informações que queremos transformar no formato desejado para armazenamento. O que queremos salvar para cada instância individual da estratégia de negociação? Antes de tudo, seus parâmetros de inicialização e o multiplicador de escala.
Quando escrevemos o parágrafo anterior, ainda não havia nem mesmo um rascunho de código que realizasse essa tarefa. Parecia que a liberdade de escolha da implementação simplesmente impedia a definição de qualquer decisão concreta. Surgia uma infinidade de dúvidas sobre qual seria a melhor abordagem, pensando no uso futuro. Mas a ausência de uma visão clara sobre o que seria necessário ou não no futuro dificultava até mesmo as decisões mais simples. Por exemplo, seria necessário incluir o número da versão no nome do arquivo do banco de dados usado pelo EA? E o número mágico? Esse nome deveria ser um parâmetro configurável no EA final, ou deveria ser gerado por algum algoritmo com base no nome da estratégia e no número mágico? Ou talvez de outra forma?
Enfim, para casos assim, existe apenas uma maneira de escapar desse círculo vicioso de perguntas infinitas. É preciso fazer alguma escolha, mesmo que não seja a melhor. E a partir dela, tomar a próxima decisão, e assim por diante. Caso contrário, nada sai do papel. Agora o código já está escrito, e podemos olhar para trás com tranquilidade e revisitar as etapas que tivemos que percorrer durante o desenvolvimento. Sim, nem toda decisão entrou no código final, nem toda escolha permaneceu sem alterações, mas todas elas ajudaram a chegar ao ponto atual, que vamos tentar descrever a seguir.
Então, exportação da lista de estratégias. Para começar, vamos definir de onde ela será chamada. Que seja a partir do EA da terceira etapa, o mesmo que já fazia a exportação do grupo de estratégias para o EA final. Mas, como já foi mencionado acima, para usar essa informação no EA final, era necessário realizar manipulações adicionais. Ao final da terceira etapa, obtínhamos apenas os identificadores dos passes com nomes atribuídos na tabela strategy_groups do banco de dados de otimização. Veja como era seu conteúdo após a otimização feita durante o trabalho da parte 21:

Cada um desses quatro passes contém uma string de inicialização salva, referente ao grupo de instâncias individuais de estratégias de negociação, selecionadas durante a otimização no intervalo de teste com a mesma data inicial (2018.01.01) e datas finais ligeiramente diferentes, indicadas no nome do grupo.
Vamos substituir, no arquivo SimpleVolumesStage3.mq5, a chamada da função que fazia a exportação nesse formato por uma chamada de outra função (ainda inexistente):
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { // Handle the completion of the pass in the EA object double res = expert.Tester(); // If the group name is not empty, save the pass to the library if(groupName_ != "") { // CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_, fileName_); expert.Export(groupName_, advFileName_); } return res; }
Vamos adicionar um novo método Export() à classe do expert CVirtualAdvisor. Como parâmetros, ele receberá o nome do novo grupo e o nome do arquivo do banco de dados do expert para o qual a exportação deverá ser feita. Vale destacar que esse já é um novo banco de dados, e não o banco de dados de otimização usado anteriormente. Para definir esse argumento, adicionaremos um parâmetro de entrada ao EA da terceira etapa:
input group "::: Saving to library" input string groupName_ = "SimpleVolumes_v.1.20_2023.01.01"; // - Version name (if empty - not saving) input string advFileName_ = "SimpleVolumes-27183.test.db.sqlite"; // - EA database name
No nível da classe do expert, ainda não trabalhamos com o banco de dados diretamente. Todos os métodos que formam as consultas SQL foram extraídos para uma classe estática separada chamada CTesterHandler. Portanto, não vamos quebrar esse esquema, e redirecionaremos os argumentos recebidos para o novo método CTesterHandler::Export(), adicionando também o array de estratégias do expert:
//+------------------------------------------------------------------+ //| Export the current strategy group to the specified EA database | //+------------------------------------------------------------------+ void CVirtualAdvisor::Export(string p_groupName, string p_advFileName) { CTesterHandler::Export(m_strategies, p_groupName, p_advFileName); }
Para implementar esse método, precisaremos definir a estrutura das tabelas no banco de dados do expert, e a presença desse novo banco de dados exigirá a capacidade de conectar-se a diferentes bancos de dados.
Acesso a diferentes bancos de dados
Após muita indecisão, optamos pela seguinte abordagem. Vamos modificar a classe existente CDatabase para que possamos indicar não apenas o nome do arquivo do banco de dados, mas também seu tipo. Com a introdução do novo tipo de banco de dados, precisaremos trabalhar com três tipos diferentes:
- Banco de dados de otimização. Utilizado para estruturar os projetos de otimização automática e armazenar informações sobre os passes do testador de estratégias realizados no pipeline de otimização automática.
- Banco de dados para seleção de grupos (versão reduzida do BD de otimização). Utilizado para envio aos agentes de teste remotos da parte necessária do banco de dados de otimização, na segunda etapa do pipeline de otimização automática.
- Banco de dados do expert (EA final). Banco de dados que será usado pelo EA final que opera na conta de negociação, para armazenar todas as informações necessárias sobre sua operação, incluindo a composição do grupo de instâncias individuais de estratégias de negociação utilizado.
Vamos criar três arquivos para armazenar o código SQL de criação dos bancos de dados de cada tipo, conectá-los como recursos ao arquivo Database.mqh e criar uma enumeração para os três tipos de banco de dados:
// Import SQL files for creating database structures of different types #resource "db.opt.schema.sql" as string dbOptSchema #resource "db.cut.schema.sql" as string dbCutSchema #resource "db.adv.schema.sql" as string dbAdvSchema // Database type enum ENUM_DB_TYPE { DB_TYPE_OPT, // Optimization database DB_TYPE_CUT, // Database for group selection (stripped down optimization database) DB_TYPE_ADV, // EA (final EA) database };
Como agora teremos disponíveis os scripts de criação de qualquer um desses três tipos de bancos de dados (claro, quando os preenchermos com o conteúdo correspondente), podemos modificar a lógica de funcionamento do método de conexão com o banco de dados Connect(). Se for detectado que não existe um banco com o nome fornecido, em vez de exibir uma mensagem de erro, vamos criá-lo a partir do script e conectar imediatamente ao banco recém-criado.
Mas para saber qual tipo de banco de dados é necessário, adicionaremos ao método de conexão um parâmetro de entrada, por meio do qual será possível informar o tipo desejado. Para evitar muitas modificações no código já existente, vamos definir como valor padrão desse parâmetro o tipo de banco de dados de otimização, já que até agora sempre nos conectamos a ele:
//+------------------------------------------------------------------+ //| Create an empty DB | //+------------------------------------------------------------------+ void CDatabase::Create(string p_schema) { bool res = Execute(p_schema); if(res) { PrintFormat(__FUNCTION__" | Database successfully created from %s", "db.*.schema.sql"); } } //+------------------------------------------------------------------+ //| Check connection to the database with the given name | //+------------------------------------------------------------------+ bool CDatabase::Connect(string p_fileName, ENUM_DB_TYPE p_dbType = DB_TYPE_OPT) { // If the database is open, close it Close(); // If a file name is specified, save it s_fileName = p_fileName; // Set the shared folder flag for the optimization and EA databases s_common = (p_dbType != DB_TYPE_CUT ? DATABASE_OPEN_COMMON : 0); // Open the database // Try to open an existing DB file s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common); // If the DB file is not found, try to create it when opening if(!IsOpen()) { s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common); // Report an error in case of failure if(!IsOpen()) { PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d", s_fileName, GetLastError()); return false; } if(p_dbType == DB_TYPE_OPT) { Create(dbOptSchema); } else if(p_dbType == DB_TYPE_CUT) { Create(dbCutSchema); } else { Create(dbAdvSchema); } } return true; }
Note que foi decidido armazenar os bancos de dados de otimização e do expert na pasta comum do terminal, e o banco de dados para seleção de grupos, na pasta de trabalho do terminal. Do contrário, não será possível organizar seu envio automático aos agentes de teste.
Banco de dados do expert
Para armazenar informações sobre os grupos de estratégias formados no banco de dados do expert, foi decidido utilizar duas tabelas: strategy_groups e strategies com a seguinte estrutura:

CREATE TABLE strategies ( id_strategy INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_group INTEGER REFERENCES strategy_groups (id_group) ON DELETE CASCADE ON UPDATE CASCADE, hash TEXT NOT NULL, params TEXT NOT NULL ); CREATE TABLE strategy_groups ( id_group INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, from_date TEXT, to_date TEXT, create_date TEXT );
Como se pode ver, cada registro na tabela de estratégias faz referência a um registro na tabela de grupos de estratégias. Por isso, podemos armazenar neste banco de dados diversas configurações diferentes de grupos de estratégias ao mesmo tempo.
O campo hash na tabela strategies armazenará o valor hash dos parâmetros da instância individual da estratégia de negociação. Com isso, será possível verificar se uma instância de uma determinada grupo é idêntica a uma instância de outro grupo.
O campo params na tabela strategies armazenará a string de inicialização da instância individual da estratégia de negociação. A partir dela, será possível montar a string de inicialização completa do grupo de estratégias para criar o objeto do expert (classe CVirtualAdvisor) no EA final.
Os campos from_date e to_date da tabela strategy_groups irão futuramente armazenar as datas de início e término do intervalo de otimização usado para gerar aquele grupo. Por enquanto, ficarão apenas vazios.
Exportação novamente das estratégias
Agora estamos prontos para implementar o método de exportação do grupo de estratégias para o banco de dados do expert no arquivo TesterHandler.mqh. Para isso, é necessário conectar-se ao banco de dados apropriado, criar um registro para o novo grupo de estratégias na tabela strategy_groups, formar para cada estratégia do grupo uma string de inicialização com seu multiplicador de normalização atual (encapsulando com "class CVirtualStrategyGroup([strategy], scale)") e salvá-las na tabela strategies.
//+------------------------------------------------------------------+ //| Export an array of strategies to the specified EA database | //| as a new group of strategies | //+------------------------------------------------------------------+ void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) { // Connect to the required EA database if(DB::Connect(p_advFileName, DB_TYPE_ADV)) { string fromDate = ""; // Start date of the optimization interval string toDate = ""; // End date of the optimization interval // Create an entry for a new strategy group string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL) RETURNING rowid;", p_groupName, fromDate, toDate); ulong groupId = DB::Insert(query); PrintFormat(__FUNCTION__" | Export %d strategies into new group [%s] with ID=%I64u", ArraySize(p_strategies), p_groupName, groupId); // For each strategy FOREACH(p_strategies, { CVirtualStrategy *strategy = p_strategies[i]; // Form an initialization string as a group of one strategy with a normalizing factor string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)", ~strategy, strategy.Scale()); // Save it in the EA database with the new group ID specified string query = StringFormat("INSERT INTO strategies " "VALUES (NULL, %I64u, '%s', '%s')", groupId, strategy.Hash(~strategy), params); DB::Execute(query); }); // Close the database DB::Close(); } }
Para calcular o valor hash dos parâmetros das estratégias, transferimos o método já existente da classe do expert para a classe-pai CFactorable. Portanto, agora ele está acessível a todos os descendentes dessa classe, inclusive às classes de estratégias de negociação.
Agora, se executarmos novamente as terceiras etapas dos projetos de otimização, veremos que apareceram registros com instâncias individuais das estratégias de negociação na tabela strategies:

E na tabela strategy_group apareceram registros sobre os grupos finais de cada projeto:

Com isso, resolvemos a exportação, agora vamos para a operação inversa, que é a importação desses grupos no EA final.
Importação de estratégias
Por enquanto, não vamos abandonar completamente o método anterior de exportação de grupos. Vamos permitir o uso paralelo tanto do método novo quanto do antigo. Se o novo método se mostrar eficaz, aí sim podemos considerar eliminar o antigo.
Vamos pegar nosso EA final SimpleVolumesExpert.mq5 e adicionar um novo parâmetro de entrada chamado newGroupId_, através do qual será possível indicar o identificador da nova biblioteca de grupos de estratégias:
input group "::: Use a strategy group" input ENUM_GROUPS_LIBRARY groupId_ = -1; // - Group from the old library OR: input int newGroupId_ = 0; // - ID of the group from the new library (0 - last)
Vamos adicionar uma constante para o nome do EA final:
#define __NAME__ "SimpleVolumes"
Na função de inicialização do EA final, primeiro verificaremos se algum grupo da biblioteca antiga foi selecionado no parâmetro groupId_. Se não, então iremos buscar a string de inicialização na nova biblioteca. Para isso, adicionamos dois novos métodos estáticos à classe do expert CVirtualAdvisor: FileName() e Import(). Eles podem ser chamados antes da criação do objeto expert.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // ... // Initialization string with strategy parameter sets string strategiesParams = NULL; // If the selected strategy group index from the library is valid, then if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) { // Take the initialization string from the library for the selected group strategiesParams = CGroupsLibrary::s_params[groupId_]; } else { // Take the initialization string from the new library for the selected group // (from the EA database) strategiesParams = CVirtualAdvisor::Import( CVirtualAdvisor::FileName(__NAME__, magic_), newGroupId_ ); } // If the strategy group from the library is not specified, then we interrupt the operation if(strategiesParams == NULL) { return INIT_FAILED; } // ... // Successful initialization return(INIT_SUCCEEDED); }
As alterações seguintes serão feitas no arquivo VirtualAdvisor.mqh. Vamos adicionar os dois métodos mencionados acima:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: // ... public: // ... // Name of the file with the EA database static string FileName(string p_name, ulong p_magic = 1); // Get the strategy group initialization string // from the EA database with the given ID static string Import(string p_fileName, int p_groupId = 0); };
No método FileName(), definimos a regra para formação do nome do arquivo do banco de dados do expert. Esse nome incluirá o nome do EA final e seu número mágico, para que EAs com números mágicos diferentes utilizem sempre bancos de dados distintos. Também será adicionado automaticamente o sufixo ".test", no caso de execução do EA no testador de estratégias. Isso foi feito para que o EA rodando no testador não sobrescreva acidentalmente as informações do banco de dados de um EA que já esteja operando na conta de negociação.
//+------------------------------------------------------------------+ //| Name of the file with the EA database | //+------------------------------------------------------------------+ string CVirtualAdvisor::FileName(string p_name, ulong p_magic = 1) { return StringFormat("%s-%d%s.db.sqlite", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); }
No método Import(), obtemos do banco de dados do expert a lista de strings de inicialização das instâncias individuais das estratégias de negociação pertencentes ao grupo especificado. Se o identificador do grupo desejado for igual a zero, então será carregada a lista de estratégias do grupo que foi criado por último.
A partir da lista obtida, formamos a string de inicialização do grupo de estratégias, unindo as strings de inicialização das estratégias por vírgula e inserindo o resultado no local adequado da string de inicialização do grupo. O multiplicador de escala do grupo na string de inicialização é definido como igual ao número de estratégias. Isso é necessário para que, ao criar o expert com essa string de inicialização do grupo, os multiplicadores de escala de todas as estratégias fiquem iguais aos que foram salvos no banco de dados do expert. Afinal, durante a criação, os multiplicadores de todas as estratégias do grupo são automaticamente divididos pelo número de estratégias no grupo. Neste caso, isso nos atrapalha, e para contornar esse obstáculo, aumentamos intencionalmente o multiplicador do grupo pelo mesmo fator que ele será reduzido depois.
//+------------------------------------------------------------------+ //| Get the strategy group initialization string | //| from the EA database with the given ID | //+------------------------------------------------------------------+ string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) { string params[]; // Array for strategy initialization strings // Request to get strategies of a given group or the last group string query = StringFormat("SELECT id_group, params " " FROM strategies" " WHERE id_group = %s;", (p_groupId > 0 ? (string) p_groupId : "(SELECT MAX(id_group) FROM strategy_groups)")); // Open the EA database if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Data structure for reading a single string of a query result struct Row { int groupId; string params; } row; // Read data from the first result string while(DatabaseReadBind(request, row)) { // Remember the strategy group ID // in the static property of the EA class s_groupId = row.groupId; // Add another strategy initialization string to the array APPEND(params, row.params); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); } // Close the EA database DB::Close(); } // Strategy group initialization string string groupParams = NULL; // Total number of strategies in the group int totalStrategies = ArraySize(params); // If there are strategies, then if(totalStrategies > 0) { // Concatenate their initialization strings with commas JOIN(params, groupParams, ","); // Create a strategy group initialization string groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)", groupParams, totalStrategies); } // Return the strategy group initialization string return groupParams; }
Esse método não é exatamente "puro", pois além de retornar a string de inicialização do grupo, ele também define o valor da propriedade estática CVirtualAdvisor::s_groupId como o identificador do grupo de estratégias que está sendo carregado. Esse modo de registrar qual grupo foi carregado da biblioteca pareceu suficientemente simples e confiável, ainda que não seja o mais elegante.
Transferência de dados do EA final
Já que organizamos um banco de dados separado para armazenar os parâmetros de criação das instâncias individuais de estratégias de negociação usadas pelo EA final, não vamos parar pela metade e vamos também transferir o armazenamento do restante das informações sobre o funcionamento do EA final na conta de negociação para esse mesmo banco de dados. Antes, essas informações eram salvas em um arquivo separado por meio do método CVirtualAdvisor::Save() e podiam ser carregadas a partir dele, quando necessário, pelo método CVirtualAdvisor::Load().
As informações salvas no arquivo incluíam:
- Parâmetros gerais do EA: o horário do último salvamento e... por enquanto, apenas isso. Mas no futuro essa lista pode ser expandida.
- Dados de cada estratégia: a lista de posições virtuais e quaisquer dados que a estratégia possa precisar armazenar. As estratégias usadas atualmente não exigem o armazenamento de dados adicionais, mas esse requisito pode surgir com outros tipos de estratégias.
- Dados do gerenciador de risco: estado atual, últimos níveis de saldo e patrimônio, multiplicadores dos tamanhos das posições, etc.
A limitação do método escolhido anteriormente está no fato de que o arquivo com os dados só podia ser lido e interpretado como um todo. Se quisermos, por exemplo, aumentar a quantidade de estratégias na string de inicialização e reiniciar o EA final, ele não conseguirá ler o arquivo com os dados salvos sem erros. Durante a leitura, o EA final esperará que o arquivo também contenha informações para as estratégias adicionadas. Mas essas informações não estarão lá. Como resultado, o método de carregamento tentará interpretar os próximos dados do arquivo (que, na verdade, são referentes ao gerenciador de risco) como se fossem dados das estratégias adicionais. E é claro que isso não vai terminar bem.
Para resolver esse problema, precisamos abandonar o armazenamento sequencial rígido de todas as informações do funcionamento do EA final, e o uso de um banco de dados vem a calhar nesse contexto. Vamos organizar nele um armazenamento simples de dados arbitrários no formato chave-valor (Key-Value).
Armazenamento Key-Value
Embora acima tenhamos falado em armazenar dados arbitrários, não precisamos definir a tarefa de forma tão ampla. Observando o que está atualmente sendo salvo no arquivo de dados do EA final, podemos nos limitar a garantir o salvamento de números individuais (inteiros e reais) e de objetos de posições virtuais. Lembrando também que cada estratégia possui um array de posições virtuais de tamanho fixo. Esse tamanho é definido nos parâmetros de inicialização da estratégia. Ou seja, os objetos de posições virtuais sempre fazem parte de algum array. E, pensando no futuro, já vamos incluir a possibilidade de salvar não apenas números individuais, mas também arrays de números de diferentes tipos.
Com base no exposto, vamos criar uma nova classe estática que conterá os seguintes métodos:
- Conexão com o banco de dados necessário: Connect()/Close()
- Definição de valores de diferentes tipos: Set(...)
- Leitura de valores de diferentes tipos: Get(...)
//+------------------------------------------------------------------+ //| Class for working with the EA database in the form of | //| Key-Value storage for properties and virtual positions | //+------------------------------------------------------------------+ class CStorage { protected: static bool s_res; // Result of all database read/write operations public: // Connect to the EA database static bool Connect(string p_fileName); // Close connection to the database static void Close(); // Save a virtual order/position static void Set(int i, CVirtualOrder* order); // Store a single value of an arbitrary simple type template<typename T> static void Set(string key, const T &value); // Store an array of values of an arbitrary simple type template<typename T> static void Set(string key, const T &values[]); // Get the value as a string for the given key static string Get(string key); // Get an array of virtual orders/positions for a given strategy hash static bool Get(string key, CVirtualOrder* &orders[]); // Get the value for a given key into a variable of an arbitrary simple type template<typename T> static bool Get(string key, T &value); // Get an array of values of a simple type by a given key into a variable template<typename T> static bool CStorage::Get(string key, T &values[]); // Result of operations static bool Res() { return s_res; } };
Adicionamos à classe uma propriedade estática s_res e um método para leitura de seu valor. Ela armazenará a indicação de qualquer erro ocorrido durante operações de leitura/gravação no banco de dados.
Como essa classe será usada exclusivamente para salvar e carregar o estado do EA final, a conexão com o banco de dados também será feita apenas nesses momentos. Até o fechamento da conexão, nenhuma outra operação lógica com o banco será executada. Por isso, no método de conexão com o banco de dados, uma transação será aberta imediatamente, dentro da qual ocorrerão todas as operações necessárias. E no método de fechamento da conexão, essa transação será ou confirmada, ou cancelada:
//+------------------------------------------------------------------+ //| Connect to the EA database | //+------------------------------------------------------------------+ bool CStorage::Connect(string p_fileName) { // Connect to the EA database if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // No errors yet s_res = true; // Start a transaction DatabaseTransactionBegin(DB::Id()); return true; } return false; } //+------------------------------------------------------------------+ //| Close the database connection | //+------------------------------------------------------------------+ void CStorage::Close() { // If there are no errors, if(s_res) { // Confirm the transaction DatabaseTransactionCommit(DB::Id()); } else { // Otherwise, cancel the transaction DatabaseTransactionRollback(DB::Id()); } // Close connection to the database DB::Close(); }
Vamos adicionar à estrutura do banco de dados do EA final mais duas tabelas com o seguinte conjunto de colunas:

A primeira tabela (storage) será usada para armazenar valores numéricos individuais e arrays de valores numéricos. Embora também seja possível armazenar strings nela. A segunda tabela (storage_orders) será utilizada para armazenar informações dos elementos dos arrays de posições virtuais de diferentes instâncias de estratégias de negociação. Por isso, os primeiros campos da tabela são strategy_hash e strategy_index, que armazenam o valor hash dos parâmetros da estratégia (único para cada estratégia) e o índice da posição virtual dentro do array de posições virtuais da estratégia.
Todos os valores numéricos individuais são salvos por meio da chamada ao método template Set(), que recebe como parâmetros uma string com o nome da chave e uma variável de qualquer tipo primitivo T. Isso pode ser, por exemplo, int, ulong ou double. Na hora de montar a query SQL para o salvamento, o valor da variável é convertido para o tipo string e armazenado no banco de dados como uma string:
//+------------------------------------------------------------------+ //| Store a single value of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &value) { // Escape single quotes (can't avoid using them yet) // StringReplace(key, "'", "\\'"); // StringReplace(value, "'", "\\'"); // Request to save the value string query = StringFormat("REPLACE INTO storage(key, value) VALUES('%s', '%s');", key, (string) value); // Execute the request s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } }
Quando queremos salvar, sob uma mesma chave, um array de valores de tipo primitivo, primeiro criamos uma única string com todos os valores do array, separados por vírgula. Esse processo ocorre em outro método template, também chamado Set(), mas cujo segundo parâmetro é uma referência para um array de valores de tipo primitivo:
//+------------------------------------------------------------------+ //| Store an array of values of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &values[]) { string value = ""; // Concatenate all values from the array into one string separated by commas JOIN(values, value, ","); // Save a string with a specified key Set(key, value); }
Para realizar as operações inversas (leitura a partir do banco de dados) adicionamos o método Get(), que retorna, para uma determinada chave, a string salva no banco com esse identificador. Para obter o valor no tipo primitivo desejado, criamos um método template com o mesmo nome, mas que recebe como segundo argumento uma referência para uma variável de tipo primitivo. Nesse método, primeiro buscamos o valor do banco em formato string e, se a busca for bem-sucedida, convertemos a string para o tipo adequado e armazenamos na variável fornecida.
//+------------------------------------------------------------------+ //| Get the value as a string for the given key | //+------------------------------------------------------------------+ string CStorage::Get(string key) { string value = NULL; // Return value // Request to get the value string query = StringFormat("SELECT value FROM storage WHERE key='%s'", key); // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Read data from the first result string DatabaseRead(request); if(!DatabaseColumnText(request, 0, value)) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Reading row in DB [adv] for request \n%s\n" "failed with code %d", query, GetLastError()); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Request in DB [adv] \n%s\nfailed with code %d", query, GetLastError()); } return value; } //+------------------------------------------------------------------+ //| Get the value for a given key into a variable | //| of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> bool CStorage::Get(string key, T &value) { // Get the value as a string string res = Get(key); // If the value is received if(res != NULL) { // Cast it to type T and assign it to the target variable value = (T) res; return true; } return false; }
Vamos utilizar os métodos adicionados para realizar o salvamento e carregamento do estado do EA final.
Salvamento e carregamento do EA
No método de salvamento do estado do EA CVirtualAdvisor::Save(), basta nos conectarmos ao banco de dados do expert e salvar todas as informações necessárias, chamando diretamente os métodos da classe CStorage ou, de forma indireta, por meio das chamadas aos métodos Save()/Load() dos objetos que precisam ser armazenados.
Por enquanto, estamos salvando diretamente apenas dois valores: o horário da última modificação na estrutura das posições virtuais e o identificador do grupo de estratégias. Em seguida, para todas as estratégias, dentro de um laço, é chamado o método Save(). E no final, é chamado o método de salvamento do gerenciador de risco. Também precisaremos fazer alterações nesses métodos mencionados, para que eles também realizem o salvamento no banco de dados do expert.
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Save() { // Save status if: if(true // later changes appeared && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // If the connection to the EA database is established if(CStorage::Connect(m_fileName)) { // Save the last modification time CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime); CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId); // Save all strategies FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save()); // Save the risk manager m_riskManager.Save(); // Update the last save time m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_fileName); // Close the connection CStorage::Close(); // Return the result return CStorage::Res(); } else { PrintFormat(__FUNCTION__" | ERROR: Can't open database [%s], LastError=%d", m_fileName, GetLastError()); return false; } } return true; }
No método de carregamento CVirtualAdvisor::Load(), as operações são feitas de forma inversa: lemos do banco de dados o horário da última modificação e o identificador do grupo de estratégias, e em seguida, cada estratégia e o gerenciador de risco carregam suas respectivas informações. Se for detectado que o horário da última modificação está no futuro, então nada mais será carregado. Essa situação pode ocorrer durante uma segunda execução visual no testador de estratégias. A execução anterior salvou as informações no final do teste, e ao iniciar uma nova execução, o EA usará o mesmo banco de dados da primeira. Por isso, é necessário apenas ignorar os dados salvos anteriormente e iniciar o processo a partir do zero.
No momento da chamada do método de carregamento, o objeto do expert já foi criado com o grupo de estratégias cujo identificador é fornecido pelos parâmetros de entrada do EA. Esse identificador foi armazenado dentro do método CVirtualAdvisor::Import() na propriedade estática CVirtualAdvisor::s_groupId. Por isso, ao carregar o identificador do grupo de estratégias a partir do banco de dados do expert, temos a possibilidade de compará-lo com o valor já existente. Se forem diferentes, significa que o EA final foi reiniciado com um novo grupo de estratégias e, possivelmente, vai exigir alguma ação adicional. Mas, por enquanto, ainda não está claro que tipo de ação será necessariamente exigida nesse caso. Então, por ora, deixaremos apenas um comentário no código como lembrete para o futuro.
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Load() { bool res = true; ulong groupId = 0; // Load status if: if(true // file exists && FileIsExist(m_fileName, FILE_COMMON) // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // If the connection to the EA database is established if(CStorage::Connect(m_fileName)) { // Download the last modification time res &= CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime); // Download the saved strategy group ID res &= CStorage::Get("CVirtualAdvisor::s_groupId", groupId); // If the last modification time is in the future, then ignore the download if(m_lastSaveTime > TimeCurrent()) { PrintFormat(__FUNCTION__" | IGNORE LAST SAVE at %s in the future", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); m_lastSaveTime = 0; return true; } PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); if(groupId != CVirtualAdvisor::s_groupId) { // Actions when launching an EA with a new group of strategies. // Nothing is happening here yet } // Load all strategies FOREACH(m_strategies, { res &= ((CVirtualStrategy*) m_strategies[i]).Load(); if(!res) break; }); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_fileName); } // Download the risk manager res &= m_riskManager.Load(); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_fileName); } // Close the connection CStorage::Close(); return res; } } return true; }
Agora vamos descer um nível e observar a implementação dos métodos de salvamento e carregamento das estratégias.
Salvamento e carregamento de estratégia
Na classe CVirtualStrategy, implementaremos nesses métodos apenas o que for comum a todas as estratégias que utilizam posições virtuais. Em cada uma delas existe um array de objetos de posições virtuais que precisam ser salvos e carregados. A implementação detalhada ficará em um nível ainda mais baixo, e aqui vamos apenas chamar os métodos específicos da classe CStorage:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CVirtualStrategy::Save() { // Save virtual positions (orders) of the strategy FOREACH(m_orders, CStorage::Set(i, m_orders[i])); } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualStrategy::Load() { bool res = true; // Download virtual positions (orders) of the strategy res = CStorage::Get(this.Hash(), m_orders); return res; }
Para os descendentes da classe CVirtualStrategy (entre os quais está a CSimpleVolumesStrategy), além do salvamento do array de posições virtuais, pode ser necessário armazenar outras informações. Nossa estratégia modelo é muito simples e não requer o salvamento de nada além da lista de posições virtuais. Mas vamos imaginar que, por algum motivo, quiséssemos salvar um array de volumes em ticks e o valor médio dos volumes em ticks. Como os métodos de salvamento e carregamento são declarados como virtuais, podemos sobrescrevê-los nas classes-filhas, acrescentando o tratamento dos dados desejados e chamando os métodos da classe-base para salvar e carregar as posições virtuais:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Save() { double avrVolume = ArrayAverage(m_volumes); // Let's form the common part of the key with the type and hash of the strategy string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Save the average tick volume CStorage::Set(key + ".avrVolume", avrVolume); // Save the array of tick volumes CStorage::Set(key + ".m_volumes", m_volumes); // Call the base class method (to save virtual positions) CVirtualStrategy::Save(); } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::Load() { bool res = true; double avrVolume = 0; // Let's form the common part of the key with the type and hash of the strategy string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Load the tick volume array res &= CStorage::Get(key + ".avrVolume", avrVolume); // Load the tick volume array res &= CStorage::Get(key + ".m_volumes", m_volumes); // Call the base class method (to load virtual positions) res &= CVirtualStrategy::Load(); return res; }
Osta apenas implementar o salvamento e o carregamento das posições virtuais.
Salvamento/carregamento de posições virtuais
Anteriormente, na classe das posições virtuais, os métodos Save() e Load() realizavam diretamente o salvamento das informações necessárias do objeto de posição virtual no arquivo de dados. Agora vamos alterar um pouco esse esquema. Vamos adicionar uma estrutura simples chamada CVirtualOrderStruct, que terá campos para todos os dados relevantes da posição virtual:
// Structure for reading/writing // basic properties of a virtual order/position from the database struct VirtualOrderStruct { string strategyHash; int strategyIndex; ulong ticket; string symbol; double lot; ENUM_ORDER_TYPE type; datetime openTime; double openPrice; double stopLoss; double takeProfit; datetime closeTime; double closePrice; datetime expiration; string comment; double point; };
Ao contrário dos objetos de posições virtuais, que têm controle rigoroso de todas as instâncias criadas e são processados automaticamente no módulo que recebe os volumes de negociação, essas estruturas podem ser criadas livremente, quantas forem necessárias. Vamos usá-las para transferir informações entre os objetos de posições virtuais e os métodos de salvamento/carregamento no banco de dados do expert, implementados na classe CStorage. Assim, os métodos de salvamento e carregamento dentro da própria classe de posições virtuais apenas preencherão a estrutura recebida ou extrairão os valores dela para atualizar seus próprios campos:
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ void CVirtualOrder::Load(const VirtualOrderStruct &o) { m_ticket = o.ticket; m_symbol = o.symbol; m_lot = o.lot; m_type = o.type; m_openPrice = o.openPrice; m_stopLoss = o.stopLoss; m_takeProfit = o.takeProfit; m_openTime = o.openTime; m_closePrice = o.closePrice; m_closeTime = o.closeTime; m_expiration = o.expiration; m_comment = o.comment; m_point = o.point; PrintFormat(__FUNCTION__" | %s", ~this); s_ticket = MathMax(s_ticket, m_ticket); m_symbolInfo = m_symbols[m_symbol]; // Notify the recipient and the strategy that the position (order) is open if(IsOpen()) { m_receiver.OnOpen(&this); m_strategy.OnOpen(&this); } else { m_receiver.OnClose(&this); m_strategy.OnClose(&this); } } //+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CVirtualOrder::Save(VirtualOrderStruct &o) { o.ticket = m_ticket; o.symbol = m_symbol; o.lot = m_lot; o.type = m_type; o.openPrice = m_openPrice; o.stopLoss = m_stopLoss; o.takeProfit = m_takeProfit; o.openTime = m_openTime; o.closePrice = m_closePrice; o.closeTime = m_closeTime; o.expiration = m_expiration; o.comment = m_comment; o.point = m_point; }
E finalmente, vamos utilizar a tabela storage_orders criada no banco de dados do expert para armazenar ali as propriedades de cada posição virtual. A manipulação dessa tabela será feita pelo método CStorage::Set(), ao qual devem ser passados o índice da posição virtual e o próprio objeto de posição virtual:
//+------------------------------------------------------------------+ //| Save a virtual order/position | //+------------------------------------------------------------------+ void CStorage::Set(int i, CVirtualOrder* order) { VirtualOrderStruct o; // Structure for virtual position data order.Save(o); // Fill it // Escape quotes in the comment StringReplace(o.comment, "'", "\\'"); // Request to save string query = StringFormat("REPLACE INTO storage_orders VALUES(" "'%s',%d,%I64u," "'%s',%.2f,%d,%I64d,%f,%f,%f,%I64d,%f,%I64d,'%s',%f);", order.Strategy().Hash(), i, o.ticket, o.symbol, o.lot, o.type, o.openTime, o.openPrice, o.stopLoss, o.takeProfit, o.closeTime, o.closePrice, o.expiration, o.comment, o.point); // Execute the request s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } }
O método CStorage::Get(), cujo segundo argumento é um array de objetos de posições virtuais, irá carregar da tabela storage_orders as informações das posições virtuais da estratégia com o valor hash fornecido no primeiro argumento:
//+------------------------------------------------------------------+ //| Get an array of virtual orders/positions | //| by the given strategy hash | //+------------------------------------------------------------------+ bool CStorage::Get(string key, CVirtualOrder* &orders[]) { // Request to obtain data on virtual positions string query = StringFormat("SELECT * FROM storage_orders " " WHERE strategy_hash = '%s' " " ORDER BY strategy_index ASC;", key); // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Structure for virtual position information VirtualOrderStruct row; // Read the data from the query result string by string while(DatabaseReadBind(request, row)) { orders[row.strategyIndex].Load(row); } } else { // Save the error and report it if necessary s_res = false; PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } return s_res; }
Com isso, a parte principal das modificações relacionadas à migração do armazenamento das informações do EA final para um banco de dados separado está concluída.
Pequeno teste
Apesar do grande volume de alterações realizadas, ainda não chegamos ao ponto em que podemos testar de fato a substituição dinâmica das configurações do EA final enquanto ele está em operação. Mas já podemos verificar se não danificamos o mecanismo de inicialização do EA final.
Para isso, realizamos a exportação do array de strings de inicialização a partir do banco de dados de otimização usando o método antigo e o novo. Agora, as informações sobre os quatro grupos de estratégias estão presentes tanto no arquivo ExportedGroupsLibrary.mqh, quanto no banco de dados do expert, que tem o nome SimpleVolumes-27183.test.db.sqlite. Vamos compilar o arquivo de código do EA final SimpleVolumesExpert.mq5.
Se definirmos os valores dos parâmetros de entrada da seguinte forma,

então será utilizada a carga da string de inicialização escolhida a partir do array interno do EA final. Esse array foi preenchido durante a compilação a partir dos dados localizados no arquivo ExportedGroupsLibrary.mqh (método antigo).
Se os valores dos parâmetros forem definidos da seguinte maneira,

então a string de inicialização será formada com base nas informações obtidas do banco de dados do expert (método novo).
Vamos rodar o EA final com o método antigo de inicialização em um intervalo curto, por exemplo, no último mês. Obtemos os seguintes resultados:


Resultados do EA final com o método antigo de carregamento de estratégias
Agora rodamos o EA final com o novo método de inicialização no mesmo intervalo de tempo. Os resultados foram os seguintes:


Resultados do EA final com o novo método de carregamento de estratégias
Como se pode ver, os resultados obtidos com o uso do método antigo e do novo coincidem completamente.
Considerações finais
Hora de fazer um balanço. A tarefa que assumimos acabou sendo um pouco mais complexa do que parecia no início. Embora ainda não tenhamos alcançado todos os resultados esperados, obtivemos uma solução plenamente funcional, adequada para testes posteriores e desenvolvimento. Agora podemos iniciar projetos de otimização com exportação de novos grupos de estratégias de negociação diretamente para o banco de dados utilizado por um EA final em execução em conta real. Mas a confiabilidade desse mecanismo ainda precisa ser verificada.
Começaremos essa verificação, como de costume, simulando o comportamento desejado no EA executado no testador de estratégias. Se os resultados forem satisfatórios por lá, passaremos para seu uso nos EAs finais operando fora do testador. Mas isso é assunto para a próxima vez.
Obrigado pela atenção, até a próxima!
Aviso importante:
Todos os resultados apresentados neste artigo e em todos os artigos anteriores da série baseiam-se exclusivamente em dados de testes históricos e não constituem garantia de qualquer lucro futuro. O trabalho dentro deste projeto tem caráter de pesquisa. Todos os resultados publicados podem ser utilizados por qualquer pessoa, por sua conta e risco.
Conteúdo do arquivo compactado
| # | Nome | Versão | Descrição | Últimas alterações |
|---|---|---|---|---|
| MQL5/Experts/Article.16452 | ||||
| 1 | Advisor.mqh | 1.04 | Classe base do expert | Parte 10 |
| 2 | ClusteringStage1.py | 1.01 | Programa de clusterização dos resultados da primeira etapa de otimização | Parte 20 |
| 3 | CreateProject.mq5 | 1.00 | EA-script de criação de projeto com etapas, trabalhos e tarefas de otimização | Parte 21 |
| 4 | Database.mqh | 1.10 | Classe para trabalhar com o banco de dados | Parte 22 |
| 5 | db.adv.schema.sql | 1.00 | Esquema do banco de dados do EA final | Parte 22 |
| 6 | db.cut.schema.sql | 1.00 | Esquema do banco de dados de otimização reduzido | Parte 22 |
| 7 | db.opt.schema.sql | 1.05 | Esquema do banco de dados de otimização | Parte 22 |
| 8 | ExpertHistory.mqh | 1.00 | Classe para exportar o histórico de ordens para arquivo | Parte 16 |
| 9 | ExportedGroupsLibrary.mqh | — | Arquivo gerado com a lista de nomes de grupos de estratégias e array com suas strings de inicialização | Parte 22 |
| 10 | Factorable.mqh | 1.03 | Classe base para objetos criados a partir de strings | Parte 22 |
| 11 | GroupsLibrary.mqh | 1.01 | Classe para trabalhar com a biblioteca de grupos de estratégias selecionados | Parte 18 |
| 12 | HistoryReceiverExpert.mq5 | 1.00 | EA de reprodução de histórico de ordens com gerenciador de risco | Parte 16 |
| 13 | HistoryStrategy.mqh | 1.00 | Classe da estratégia de reprodução de histórico de ordens | Parte 16 |
| 14 | Interface.mqh | 1.00 | Classe base para visualização de diversos objetos | Parte 4 |
| 15 | LibraryExport.mq5 | 1.01 | EA que salva strings de inicialização dos passes selecionados da biblioteca no arquivo ExportedGroupsLibrary.mqh | Parte 18 |
| 16 | Macros.mqh | 1.05 | Macros úteis para operações com arrays | Parte 22 |
| 17 | Money.mqh | 1.01 | Classe base de gerenciamento de capital | Parte 12 |
| 18 | NewBarEvent.mqh | 1.00 | Classe para detecção de novo candle para símbolo específico | Parte 8 |
| 19 | Optimization.mq5 | 1.04 | EA que gerencia a execução das tarefas de otimização | Parte 22 |
| 20 | Optimizer.mqh | 1.03 | Classe para o gerenciador de otimização automática de projetos | Parte 22 |
| 21 | OptimizerTask.mqh | 1.03 | Classe para tarefa de otimização | Parte 22 |
| 22 | Receiver.mqh | 1.04 | Classe base para conversão de volumes abertos em ordens a mercado | Parte 12 |
| 23 | SimpleHistoryReceiverExpert.mq5 | 1.00 | EA simplificado para reprodução de histórico de ordens | Parte 16 |
| 24 | SimpleVolumesExpert.mq5 | 1.21 | EA final para operação paralela de múltiplos grupos de estratégias modelo. Parâmetros serão obtidos da biblioteca embutida de grupos. | Parte 22 |
| 25 | SimpleVolumesStage1.mq5 | 1.18 | EA de otimização de instância individual de estratégia de negociação (Etapa 1) | Parte 19 |
| 26 | SimpleVolumesStage2.mq5 | 1.02 | EA de otimização de grupo de instâncias de estratégias de negociação (Etapa 2) | Parte 19 |
| 27 | SimpleVolumesStage3.mq5 | 1.03 | EA que salva o grupo de estratégias normalizado em biblioteca com nome definido. | Parte 22 |
| 28 | SimpleVolumesStrategy.mqh | 1.11 | Classe de estratégia de negociação com uso de volumes em ticks | Parte 22 |
| 29 | Storage.mqh | 1.00 | Classe para armazenamento Key-Value para o EA final | Parte 22 |
| 30 | Strategy.mqh | 1.04 | Classe base da estratégia de negociação | Parte 10 |
| 31 | SymbolsMonitor.mqh | 1.00 | Classe para obtenção de informações sobre instrumentos de negociação (símbolos) | Parte 21 |
| 32 | TesterHandler.mqh | 1.06 | Classe para tratamento de eventos de otimização | Parte 22 |
| 33 | VirtualAdvisor.mqh | 1.09 | Classe do expert que trabalha com posições virtuais (ordens) | Parte 22 |
| 34 | VirtualChartOrder.mqh | 1.01 | Classe para posição virtual gráfica | Parte 18 |
| 35 | VirtualFactory.mqh | 1.04 | Classe fábrica de objetos | Parte 16 |
| 36 | VirtualHistoryAdvisor.mqh | 1.00 | Classe do expert de reprodução de histórico de ordens | Parte 16 |
| 37 | VirtualInterface.mqh | 1.00 | Classe de 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.03 | Classe de conversão de volumes abertos em ordens a mercado (receptor) | Parte 12 |
| 40 | VirtualRiskManager.mqh | 1.02 | Classe de gerenciamento de risco (gerenciador de risco) | Parte 15 |
| 41 | VirtualStrategy.mqh | 1.08 | Classe de estratégia de negociação com posições virtuais | Parte 22 |
| 42 | VirtualStrategyGroup.mqh | 1.00 | Classe de grupo de estratégias de negociação ou grupo de grupos de estratégias | Parte 11 |
| 43 | VirtualSymbolReceiver.mqh | 1.00 | Classe de receptor por símbolo | Parte 3 |
| MQL5/Common/Files | Pasta comum dos terminais | |||
| 44 | SimpleVolumes-27183.test.db.sqlite | — | Banco de dados do expert com quatro grupos de estratégias adicionados | |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16452
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: Modelos bidimensionais do espaço de conexões (Chimera)
Algoritmo de busca circular — Circle Search Algorithm (CSA)
Do básico ao intermediário: Eventos em Objetos (III)
Fibonacci no Forex (Parte I): Testando relações entre preço e tempo
- 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