
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:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) { // Запоминаем строку инициализации m_params = p_params; ... // Читаем масштабирующий множитель m_scale = ReadDouble(p_params); // Исправляем его при необходимости if(m_scale <= 0.0) { m_scale = 1.0; } if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) { // Если мы наполнили массив групп, а массив стратегий пустой, то PrintFormat(__FUNCTION__" | Scale = %.2f, total groups = %d", m_scale, ArraySize(m_groups)); // Масштабируем все группы Scale(m_scale / ArraySize(m_groups)); } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) { // Если мы наполнили массив стратегий, а массив групп пустой, то PrintFormat(__FUNCTION__" | Scale = %.2f, total strategies = %d", m_scale, ArraySize(m_strategies)); // Масштабируем все стратегии Scale(m_scale / ArraySize(m_strategies)); } else { // Иначе сообщаем об ошибке в строке инициализации 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):
//+------------------------------------------------------------------+ //| Результат тестирования | //+------------------------------------------------------------------+ double OnTester(void) { // Обрабатываем завершение прохода в объекте эксперта double res = expert.Tester(); // Если имя группы не пустое, то сохраняем проход в библиотеку 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 "::: Сохранение в библиотеку" input string groupName_ = "SimpleVolumes_v.1.20_2023.01.01"; // - Название версии (если пустое - не сохранять) input string advFileName_ = "SimpleVolumes-27183.test.db.sqlite"; // - Название базы данных эксперта
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:
//+------------------------------------------------------------------+ //| Экспорт текущей группы стратегий в заданную базу данных эксперта | //+------------------------------------------------------------------+ 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:
// Импорт sql-файлов создания структуры БД разных типов #resource "db.opt.schema.sql" as string dbOptSchema #resource "db.cut.schema.sql" as string dbCutSchema #resource "db.adv.schema.sql" as string dbAdvSchema // Тип базы данных enum ENUM_DB_TYPE { DB_TYPE_OPT, // БД оптимизации DB_TYPE_CUT, // БД для подбора групп (урезанная БД оптимизации) DB_TYPE_ADV, // БД эксперта (итогового советника) };
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:
//+------------------------------------------------------------------+ //| Создание пустой БД | //+------------------------------------------------------------------+ void CDatabase::Create(string p_schema) { bool res = Execute(p_schema); if(res) { PrintFormat(__FUNCTION__" | Database successfully created from %s", "db.*.schema.sql"); } } //+------------------------------------------------------------------+ //| Проверка подключения к базе данных с заданным именем | //+------------------------------------------------------------------+ bool CDatabase::Connect(string p_fileName, ENUM_DB_TYPE p_dbType = DB_TYPE_OPT) { // Если база данных открыта, то закроем её Close(); // Если задано имя файла, то запомним его s_fileName = p_fileName; // Установим флаг общей папки для БД оптимизации и эксперта s_common = (p_dbType != DB_TYPE_CUT ? DATABASE_OPEN_COMMON : 0); // Открываем базу данных // Пробуем открыть существующий файл БД s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common); // Если файл БД не найден, то пытаемся создать его при открытии if(!IsOpen()) { s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common); // Сообщаем об ошибке при неудаче 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.
//+------------------------------------------------------------------+ //| Экспорт массива стратегий в заданную базу данных эксперта | //| как новой группы стратегий | //+------------------------------------------------------------------+ void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) { // Подключаемся к нужной базе данных эксперта if(DB::Connect(p_advFileName, DB_TYPE_ADV)) { string fromDate = ""; // Дата начала интервала оптимизации string toDate = ""; // Дата конца интервала оптимизации // Создаём запись для новой группы стратегий 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); // Для каждой стратегии FOREACH(p_strategies, { CVirtualStrategy *strategy = p_strategies[i]; // Формируем строку инициализации в виде группы из одной стратегии с нормирующим множителем string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)", ~strategy, strategy.Scale()); // Сохраняем её в базе данных эксперта с указанием нового идентификатора группы string query = StringFormat("INSERT INTO strategies " "VALUES (NULL, %I64u, '%s', '%s')", groupId, strategy.Hash(~strategy), params); DB::Execute(query); }); // Закрываем базу данных 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 "::: Использовать группу стратегий" input ENUM_GROUPS_LIBRARY groupId_ = -1; // - Группа из старой библиотеки ИЛИ: input int newGroupId_ = 0; // - ID группы из новой библиотеки (0 - последняя)
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() { // ... // Строка инициализации с наборами параметров стратегий string strategiesParams = NULL; // Если выбранный индекс группы стратегий из библиотеки является допустимым, то if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) { // Берём строку инициализации из библиотеки для выбранной группы strategiesParams = CGroupsLibrary::s_params[groupId_]; } else { // Берём строку инициализации из новой библиотеки для выбранной группы // (из базы данных эксперта) strategiesParams = CVirtualAdvisor::Import( CVirtualAdvisor::FileName(__NAME__, magic_), newGroupId_ ); } // Если группа стратегий из библиотеки не задана, то прерываем работу if(strategiesParams == NULL) { return INIT_FAILED; } // ... // Успешная инициализация return(INIT_SUCCEEDED); }
As alterações seguintes serão feitas no arquivo VirtualAdvisor.mqh. Vamos adicionar os dois métodos mencionados acima:
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: // ... public: // ... // Имя файла с базой данных эксперта static string FileName(string p_name, ulong p_magic = 1); // Получение строки инициализации группы стратегий // из базы данных эксперта с заданным идентификатором 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.
//+------------------------------------------------------------------+ //| Имя файла с базой данных эксперта | //+------------------------------------------------------------------+ 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.
//+------------------------------------------------------------------+ //| Получение строки инициализации группы стратегий | //| из базы данных эксперта с заданным идентификатором | //+------------------------------------------------------------------+ string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) { string params[]; // Массив для строк инициализации стратегий // Запрос на получение стратегий заданной группы либо последней группы 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)")); // Открываем базу данных эксперта if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // Выполняем запрос int request = DatabasePrepare(DB::Id(), query); // Если нет ошибки if(request != INVALID_HANDLE) { // Структура данных для чтения одной строки результата запроса struct Row { int groupId; string params; } row; // Читаем данные из первой строки результата while(DatabaseReadBind(request, row)) { // Запоминаем идентификатор группы стратегий // в статическом свойстве класса эксперта s_groupId = row.groupId; // Добавляем очередную строку инициализации стратегии в массив APPEND(params, row.params); } } else { // Сообщаем об ошибке при необходимости PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); } // Закрываем базу данных эксперта DB::Close(); } // Строка инициализации группы стратегий string groupParams = NULL; // Общее количество стратегий в группе int totalStrategies = ArraySize(params); // Если стратегии есть, то if(totalStrategies > 0) { // Соединяем их строки инициализации через запятую JOIN(params, groupParams, ","); // Создаём строку инициализации группы стратегий groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)", groupParams, totalStrategies); } // Возвращаем строку инициализации группы стратегий 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(...)
//+------------------------------------------------------------------+ //| Класс для работы с базой данных эксперта в виде | //| хранилища Key-Value для свойств и виртуальных позиций | //+------------------------------------------------------------------+ class CStorage { protected: static bool s_res; // Результат всех операций чтения/записи базы данных public: // Подключение к базе данных эксперта static bool Connect(string p_fileName); // Закрытие подключения к базе данных static void Close(); // Сохранение виртуального ордера/позиции static void Set(int i, CVirtualOrder* order); // Сохранение одного значения произвольного простого типа template<typename T> static void Set(string key, const T &value); // Сохранение массива значений произвольного простого типа template<typename T> static void Set(string key, const T &values[]); // Получение значения в виде строки по заданному ключу static string Get(string key); // Получение массива виртуальных ордеров/позиций по заданному хешу стратегии static bool Get(string key, CVirtualOrder* &orders[]); // Получение значения по заданному ключу в переменную произвольного простого типа template<typename T> static bool Get(string key, T &value); // Получение массива значений простого типа по заданному ключу в переменную template<typename T> static bool CStorage::Get(string key, T &values[]); // Результат операций 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:
//+------------------------------------------------------------------+ //| Подключение к базе данных эксперта | //+------------------------------------------------------------------+ bool CStorage::Connect(string p_fileName) { // Подключаемся к базе данных эксперта if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // Устанавливаем, что пока ошибок нет s_res = true; // Начинаем транзакцию DatabaseTransactionBegin(DB::Id()); return true; } return false; } //+------------------------------------------------------------------+ //| Закрытие подключения к базе данных | //+------------------------------------------------------------------+ void CStorage::Close() { // Если ошибок нет, то if(s_res) { // Подтверждаем транзакцию DatabaseTransactionCommit(DB::Id()); } else { // Иначе транзакцию отменяем DatabaseTransactionRollback(DB::Id()); } // Закрываем соединение с базой данных 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:
//+------------------------------------------------------------------+ //| Сохранение одного значения произвольного простого типа | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &value) { // Экранируем символы одинарных кавычек (пока не можно не использовать) // StringReplace(key, "'", "\\'"); // StringReplace(value, "'", "\\'"); // Запрос на сохранение значения string query = StringFormat("REPLACE INTO storage(key, value) VALUES('%s', '%s');", key, (string) value); // Выполняем запрос s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Сообщаем об ошибке при необходимости 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:
//+------------------------------------------------------------------+ //| Сохранение массива значений произвольного простого типа | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &values[]) { string value = ""; // Соединяем все значения из массива в одну строку через запятую JOIN(values, value, ","); // Сохраняем строку с заданным ключом 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.
//+------------------------------------------------------------------+ //| Получение значения в виде строки по заданному ключу | //+------------------------------------------------------------------+ string CStorage::Get(string key) { string value = NULL; // Возвращаемое значение // Запрос на получение значения string query = StringFormat("SELECT value FROM storage WHERE key='%s'", key); // Выполняем запрос int request = DatabasePrepare(DB::Id(), query); // Если нет ошибки if(request != INVALID_HANDLE) { // Читаем данные из первой строки результата DatabaseRead(request); if(!DatabaseColumnText(request, 0, value)) { // Сообщаем об ошибке при необходимости PrintFormat(__FUNCTION__" | ERROR: Reading row in DB [adv] for request \n%s\n" "failed with code %d", query, GetLastError()); } } else { // Сообщаем об ошибке при необходимости PrintFormat(__FUNCTION__" | ERROR: Request in DB [adv] \n%s\nfailed with code %d", query, GetLastError()); } return value; } //+------------------------------------------------------------------+ //| Получение значения по заданному ключу в переменную | //| произвольного простого типа | //+------------------------------------------------------------------+ template<typename T> bool CStorage::Get(string key, T &value) { // Получаем значение в виде строки string res = Get(key); // Если значение получено if(res != NULL) { // Приводим его к типу Т и присваиваем целевой переменной 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.
//+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Save() { // Сохраняем состояние, если: if(true // появились более поздние изменения && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime // и сейчас не оптимизация && !MQLInfoInteger(MQL_OPTIMIZATION) // и сейчас не тестирование либо сейчас визуальное тестирование && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // Если подключение к базе данных эксперта установлено if(CStorage::Connect(m_fileName)) { // Сохраняем время последних изменений CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime); CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId); // Сохраняем все стратегии FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save()); // Сохраняем риск-менеджер m_riskManager.Save(); // Обновляем время последнего сохранения m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_fileName); // Закрываем соединение CStorage::Close(); // Возвращаем результат 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.
//+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Load() { bool res = true; ulong groupId = 0; // Загружаем состояние, если: if(true // файл существует && FileIsExist(m_fileName, FILE_COMMON) // и сейчас не оптимизация && !MQLInfoInteger(MQL_OPTIMIZATION) // и сейчас не тестирование либо сейчас визуальное тестирование && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // Если подключение к базе данных эксперта установлено if(CStorage::Connect(m_fileName)) { // Загружаем время последних изменений res &= CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime); // Загружаем идентификатор сохранённой группы стратегий res &= CStorage::Get("CVirtualAdvisor::s_groupId", groupId); // Если время последних изменений находится в будущем, то игнорируем загрузку 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) { // Действия при запуске эксперта с новой группой стратегий. // Пока тут ничего не происходит } // Загружаем все стратегии FOREACH(m_strategies, { res &= ((CVirtualStrategy*) m_strategies[i]).Load(); if(!res) break; }); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_fileName); } // Загружаем риск-менеджер res &= m_riskManager.Load(); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_fileName); } // Закрываем соединение 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:
//+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ void CVirtualStrategy::Save() { // Сохраняем виртуальные позиции (ордера) стратегии FOREACH(m_orders, CStorage::Set(i, m_orders[i])); } //+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ bool CVirtualStrategy::Load() { bool res = true; // Загружаем виртуальные позиции (ордера) стратегии 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:
//+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Save() { double avrVolume = ArrayAverage(m_volumes); // Сформируем общую часть ключа с типом и хешем стратегии string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Сохраняем средний тиковый объём CStorage::Set(key + ".avrVolume", avrVolume); // Сохраняем массив тиковых объёмов CStorage::Set(key + ".m_volumes", m_volumes); // Вызываем метод базового класса (для сохранения виртуальных позиций) CVirtualStrategy::Save(); } //+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::Load() { bool res = true; double avrVolume = 0; // Сформируем общую часть ключа с типом и хешем стратегии string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Загружаем массив тиковых объёмов res &= CStorage::Get(key + ".avrVolume", avrVolume); // Загружаем массив тиковых объёмов res &= CStorage::Get(key + ".m_volumes", m_volumes); // Вызываем метод базового класса (для загрузки виртуальных позиций) 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:
// Структура для чтения/записи из БД // основных свойств виртуального ордера/позиции 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:
//+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ 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]; // Оповещаем получатель и стратегию, что позиция (ордер) открыта if(IsOpen()) { m_receiver.OnOpen(&this); m_strategy.OnOpen(&this); } else { m_receiver.OnClose(&this); m_strategy.OnClose(&this); } } //+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Сохранение виртуального ордера/позиции | //+------------------------------------------------------------------+ void CStorage::Set(int i, CVirtualOrder* order) { VirtualOrderStruct o; // Структура для информации о виртуальной позиции order.Save(o); // Наполняем её // Экранируем кавычки в комментарии StringReplace(o.comment, "'", "\\'"); // Запрос на сохранение 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); // Выполняем запрос s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Сообщаем об ошибке при необходимости 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:
//+------------------------------------------------------------------+ //| Получение массива виртуальных ордеров/позиций | //| по заданному хешу стратегии | //+------------------------------------------------------------------+ bool CStorage::Get(string key, CVirtualOrder* &orders[]) { // Запрос на получение данных о виртуальных позициях string query = StringFormat("SELECT * FROM storage_orders " " WHERE strategy_hash = '%s' " " ORDER BY strategy_index ASC;", key); // Выполняем запрос int request = DatabasePrepare(DB::Id(), query); // Если нет ошибки if(request != INVALID_HANDLE) { // Структура для информации о виртуальной позиции VirtualOrderStruct row; // Читаем построчно данные из результата запроса while(DatabaseReadBind(request, row)) { orders[row.strategyIndex].Load(row); } } else { // Запоминаем ошибку и сообщаем об ней при необходимости 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.





- 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