Desenvolvendo um EA multimoeda (Parte 10): Criação de objetos a partir de uma string
Introdução
No artigo anterior, esboçamos um plano geral de desenvolvimento do EA, que inclui várias etapas. Cada etapa gera uma certa quantidade de informações que devem ser usadas nas etapas seguintes. Decidimos salvar essas informações em um banco de dados e criamos nele uma tabela onde podemos armazenar os resultados de passagens individuais do testador de estratégias para diferentes EAs.
Para que seja possível utilizar essas informações nas próximas etapas, precisamos de um método para criar os objetos necessários (estratégias de trading, seus grupos e experts) a partir das informações armazenadas no banco de dados. Não é possível salvar objetos diretamente no banco de dados. A melhor solução que podemos propor é transformar todas as propriedades dos objetos em uma string, salvá-la no banco de dados e, em seguida, ler essa string do banco e criar o objeto necessário a partir dela.
O processo de criação de um objeto a partir de uma string pode ser feito de várias maneiras. Por exemplo, podemos criar um objeto da classe desejada com parâmetros padrão e, em seguida, usar um método ou função especial para analisar a string lida do banco de dados e atribuir os valores correspondentes às propriedades do objeto. Ou podemos criar um construtor adicional para o objeto, que receberia apenas uma string como parâmetro de entrada. Essa string seria dividida em partes dentro do construtor, e os valores correspondentes seriam atribuídos às propriedades do objeto. Para entender qual opção é mais adequada, primeiro vamos examinar como as informações sobre os objetos são armazenadas no banco de dados.
Armazenamento de informações sobre objetos
Vamos abrir a tabela no banco de dados que preenchemos no artigo anterior e examinar as últimas colunas. As colunas params e inputs armazenam o resultado da conversão em string do objeto da estratégia de trading da classe CSimpleVolumesStrategy e os parâmetros de entrada de uma passagem de otimização.

Fig. 1. Fragmento da tabela passes com informações sobre a estratégia utilizada e os parâmetros de teste
Embora estejam relacionados, existem diferenças entre eles: na coluna inputs, os nomes dos parâmetros de entrada estão presentes (embora não coincidam exatamente com os nomes das propriedades do objeto da estratégia), mas faltam alguns parâmetros, como símbolo e período. Portanto, para recriar o objeto, será mais conveniente usar o formato de registro da coluna params.
Vamos lembrar de onde veio a implementação da conversão do objeto da estratégia em string. Na quarta parte da série de artigos, implementamos o salvamento do estado do EA em um arquivo, para permitir a recuperação de seu funcionamento após a reinicialização. Para evitar que o EA utilize acidentalmente um arquivo com dados de outro EA semelhante, adicionamos ao arquivo a gravação de informações sobre os parâmetros de todas as instâncias de estratégias utilizadas neste EA.
Ou seja, inicialmente a tarefa era fazer com que instâncias de estratégias de trading com parâmetros diferentes gerassem strings diferentes. Portanto, não nos preocupamos muito se conseguiríamos criar posteriormente um novo objeto de estratégia de trading a partir dessas strings. Na nona parte, utilizamos o mecanismo de conversão em string já existente sem modificação adicional, pois o foco estava na depuração do processo de adição dessas informações no banco de dados.
Iniciando a implementação
Agora é o momento de pensar em como recriar objetos a partir dessas strings. Temos uma string com o seguinte formato:
class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3)
Ao passá-la para o construtor do objeto da classe CSimpleVolumesStrategy, ele deve fazer o seguinte:
- remover a parte até o primeiro parêntese de abertura;
- dividir a parte restante até o parêntese de fechamento usando a vírgula como delimitador;
- atribuir cada parte resultante às propriedades correspondentes do objeto, realizando a conversão para números quando necessário.
Observando essas etapas, notamos que a primeira pode ser feita em um nível superior. De fato, se inicialmente obtivermos o nome da classe a partir da string, poderemos determinar a qual classe o objeto pertence. Assim, seria mais conveniente passar ao construtor apenas a parte da string contida dentro dos parênteses.
Além disso, a necessidade de criar objetos a partir de strings não se limita a essa única classe. Em primeiro lugar, podemos ter mais de uma estratégia de trading. Em segundo lugar, precisaremos criar objetos da classe CVirtualStrategyGroup, ou seja, grupos de várias instâncias de estratégias de trading com diferentes parâmetros. Isso será útil na fase de agrupamento de várias estratégias selecionadas anteriormente. Por fim, por que não possibilitar também a criação do próprio objeto EA (classe CVirtualAdvisor) a partir de uma string? Isso nos permitirá programar um EA universal capaz de carregar de um arquivo uma descrição em texto de todos os grupos de estratégias que devem ser utilizados. Alterando a descrição nesse arquivo, será possível atualizar completamente o conjunto de estratégias no EA sem recompilá-lo.
Se imaginarmos como seria uma string de inicialização de objetos da classe CVirtualStrategyGroup, poderia ser algo assim:
class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),
class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,27,0.70,0.90,60,10000.00,550.00,10000,3),
class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,37,0.70,0.90,80,10000.00,150.00,10000,3)
], 0.33)
O primeiro parâmetro do construtor dos objetos da classe CVirtualStrategyGroup é um array de objetos de estratégias de trading ou de grupos de estratégias de trading. Portanto, precisamos aprender a analisar a parte da string que representa um array de descrições de objetos do mesmo tipo. Como pode ser visto, usamos a notação padrão, aplicada em JSON ou Python, para representar uma lista (array) de elementos: os registros dos elementos são separados por vírgulas, e, no geral, estão dentro de colchetes.
Também precisaremos aprender a identificar na string partes que não estão apenas entre vírgulas, mas que representam a descrição de outro objeto aninhado de uma classe. Por acaso, para converter o objeto das estratégias de trading em string, usamos a função typename(), que retorna o nome da classe do objeto como uma string com a palavra class precedendo-a. Agora, poderemos usar essa palavra ao analisar a string, como um sinal de que a seguir virá a descrição de um objeto de alguma classe, e não apenas um valor simples como um número ou string.
Assim, chegamos à conclusão da necessidade de implementar o padrão de projeto "Factory", onde um objeto especializado será responsável pela criação de objetos de várias classes sob demanda. Os objetos que a fábrica poderá gerar geralmente precisam compartilhar um ancestral comum na hierarquia de classes. Portanto, começaremos criando uma nova classe base, da qual todos os futuros objetos que podem ser criados a partir de uma string de inicialização herdarão.
Novo classe base
Até o momento, nossos classes base, envolvidos na hierarquia de herança, eram os seguintes:
- СAdvisor. Classe usada para criar EAs, da qual herda a classe CVirtualAdvisor.
- CStrategy. Classe usada para criar estratégias de trading, da qual, por fim, herda CSimpleVolumesStrategy.
- CVirtualStrategyGroup. Classe usada para grupos de estratégias de trading. Ela não tem herdeiros, nem se espera que tenha.
- E mais algum?
Parece que não. Não existem outras classes base com herdeiros que precisem da funcionalidade de inicialização via string. Assim, essas três classes precisam de um ancestral comum, que reunirá todos os métodos auxiliares necessários para permitir a inicialização via string.
O nome para o novo ancestral não foi escolhido de maneira ideal até o momento. Queríamos enfatizar que os herdeiros dessa classe poderiam ser criados por uma "fábrica" (Factory), ou seja, seriam "factoryable". Curiosamente, o tradutor online destaca essa palavra como errada, mas a tradução soa como esperado: "adequado para fabricação". No decorrer da escrita, a letra "y" acabou desaparecendo, e o nome ficou apenas como CFactorable.
Inicialmente, essa classe se parecia com a seguinte:
//+------------------------------------------------------------------+ //| Base class of objects created from a string | //+------------------------------------------------------------------+ class CFactorable { protected: virtual void Init(string p_params) = 0; public: virtual string operator~() = 0; static string Read(string &p_params); };
Ou seja, os herdeiros dessa classe deveriam implementar o método Init(), que seria responsável por realizar todo o trabalho necessário de transformar a string de inicialização nos valores das propriedades do objeto, e o operador til, que lidaria com a conversão inversa das propriedades em uma string de inicialização. Também foi declarado que existiria um método estático Read(), que deveria ser capaz de ler uma parte dos dados da string de inicialização. Por "parte dos dados", entendemos uma substring que contém ou uma string de inicialização de outro objeto, ou um array de outras partes de dados, ou um número, ou uma constante de string.
Embora essa implementação tenha sido levada a um estado funcional, foi decidido fazer alterações significativas nela.
Em primeiro lugar, o método Init() surgiu porque queríamos manter tanto os construtores antigos do objeto quanto o novo construtor (que aceita uma string de inicialização). Para evitar duplicação de código, implementamos isso uma vez no método Init() e o chamamos de vários construtores possíveis. Mas, no final, descobrimos que não havia necessidade de diferentes construtores. Podemos muito bem nos contentar com um único novo construtor. Portanto, o código do método Init() foi movido para o novo construtor, e o próprio método foi removido.
Em segundo lugar, a implementação inicial não continha nenhum meio de controle da correção das strings de inicialização e nem mensagens sobre erros detectados. Sim, esperamos gerar strings de inicialização automaticamente, o que praticamente elimina a ocorrência de tais erros, mas, se por acaso cometeremos um erro com as strings de inicialização geradas, seria bom ser informado disso de forma oportuna e poder encontrar o local exato do erro. Para esses fins, adicionamos uma nova propriedade lógica m_isValid, que indica se todo o código do construtor do objeto foi executado com sucesso ou se algumas partes da string de inicialização contêm erros. Essa propriedade é privada, e para obter e definir seu valor, foram adicionados os métodos correspondentes: IsValid() e SetInvalid(). No entanto, inicialmente, essa propriedade é sempre igual a true, e o método SetInvalid() pode apenas definir seu valor como false.
Em terceiro lugar, o método Read(), ao adicionar verificações e tratamento de erros, se tornou muito volumoso e foi dividido em vários métodos separados, especializados na leitura de diferentes tipos de dados da string de inicialização. Também foram adicionados alguns métodos privados, que desempenham um papel de suporte para os métodos de leitura de dados. Vale a pena mencionar que os métodos de leitura de dados modificam a string de inicialização que lhes é passada. Quando a próxima parte dos dados é lida com sucesso, ela é retornada como resultado do método, e a string de inicialização passada perde a parte lida.
Em quarto lugar, o método de conversão do objeto de volta para uma string de inicialização pode ser praticamente idêntico para objetos de diferentes classes, se armazenarmos a string de inicialização original com os parâmetros do objeto criado. Portanto, foi adicionada ao classe base a propriedade m_params para salvar a string de inicialização no construtor do objeto.
Com as adições feitas, a declaração da classe CFactorable passou a ser assim:
//+------------------------------------------------------------------+ //| Base class of objects created from a string | //+------------------------------------------------------------------+ class CFactorable { private: bool m_isValid; // Is the object valid? // Clear empty characters from left and right in the initialization string static void Trim(string &p_params); // Find a matching closing bracket in the initialization string static int FindCloseBracket(string &p_params, char closeBraket = ')'); // Clear the initialization string with a check for the current object validity bool CheckTrimParams(string &p_params); protected: string m_params; // Current object initialization string // Set the current object to the invalid state void SetInvalid(string function = NULL, string message = NULL); public: CFactorable() : m_isValid(true) {} // Constructor bool IsValid(); // Is the object valid? // Convert object to string virtual string operator~() = 0; // Does the initialization string start with the object definition? static bool IsObject(string &p_params, const string className = ""); // Does the initialization string start with defining an object of the desired class? static bool IsObjectOf(string &p_params, const string className); // Read the object class name from the initialization string static string ReadClassName(string &p_params, bool p_removeClassName = true); // Read an object from the initialization string string ReadObject(string &p_params); // Read an array from the initialization string as a string string ReadArrayString(string &p_params); // Read a string from the initialization string string ReadString(string &p_params); // Read a number from the initialization string as a string string ReadNumber(string &p_params); // Read a real number from the initialization string double ReadDouble(string &p_params); // Read an integer from the initialization string long ReadLong(string &p_params); };
Não vamos nos aprofundar muito na implementação dos métodos dessa classe neste artigo. Vale apenas mencionar que o trabalho de todos os métodos de leitura envolve a execução de um conjunto de ações bastante semelhante. Primeiro, verificamos se a string de inicialização não está vazia e se o objeto está em um estado correto. O objeto pode ter passado para um estado incorreto, por exemplo, como resultado de uma operação anterior de leitura de uma parte dos dados da string de inicialização que falhou. Portanto, essa verificação ajuda a evitar ações desnecessárias em um objeto que já está, comprovadamente, com defeito.
Em seguida, são verificadas certas condições que indicam que os dados do tipo necessário (objeto, array, string ou número) seguem na string de inicialização. Se for esse o caso, encontra-se o local onde essa parte dos dados termina na string de inicialização. O que está à esquerda desse local é usado para obter o valor retornado, e o que está à direita substitui a string de inicialização.
Se em algum estágio das verificações obtivermos um resultado negativo, chamamos o método para definir o estado do objeto atual como incorreto, passando a ele informações sobre o local e a natureza do erro ocorrido.
Vamos salvar o código dessa classe no arquivo Factorable.mqh na pasta atual.
Fábrica de objetos
Como nas strings de inicialização dos objetos o nome da classe é obrigatório, podemos criar uma função pública ou um método estático que atuará como uma "fábrica" de objetos. Vamos passar para ele a string de inicialização e, como resultado, obter um ponteiro para o objeto criado da classe especificada.
Claro, para objetos cujos nomes de classe em um determinado ponto do programa podem ter apenas um valor, a existência de tal fábrica é desnecessária. Podemos criar o objeto de forma padrão usando o operador new, passando ao construtor a string de inicialização com os parâmetros do objeto criado. Mas, se precisarmos criar objetos cujos nomes de classe podem variar (por exemplo, diferentes estratégias de trading), o operador new não nos ajudará, pois precisamos primeiro entender qual classe de objeto deve ser criada. Essa tarefa será delegada à fábrica, mais precisamente, ao seu único método estático Create().
//+------------------------------------------------------------------+ //| Object factory class | //+------------------------------------------------------------------+ class CVirtualFactory { public: // Create an object from the initialization string static CFactorable* Create(string p_params) { // Read the object class name string className = CFactorable::ReadClassName(p_params); // Pointer to the object being created CFactorable* object = NULL; // Call the corresponding constructor depending on the class name if(className == "CVirtualAdvisor") { object = new CVirtualAdvisor(p_params); } else if(className == "CVirtualStrategyGroup") { object = new CVirtualStrategyGroup(p_params); } else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); } // If the object is not created or is created in the invalid state, report an error if(!object) { PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\nclass %s(%s)", className, p_params); } else if(!object.IsValid()) { PrintFormat(__FUNCTION__ " | ERROR: Created object is invalid for:\nclass %s(%s)", className, p_params); delete object; // Remove the invalid object object = NULL; } return object; } };
Vamos salvar esse código no arquivo VirtualFactory.mqh na pasta atual.
Para facilitar o uso da fábrica no futuro, vamos criar dois macros úteis. O primeiro criará um objeto a partir da string de inicialização, substituindo a si mesmo pela chamada do método CVirtualFactory::Create():
// Create an object in the factory from a string #define NEW(Params) CVirtualFactory::Create(Params)
O segundo macro será executado apenas a partir do construtor de outro objeto, que obrigatoriamente será herdeiro da classe CFactorable. Ou seja, somente quando, ao criar um objeto (principal), precisarmos criar outros objetos (aninhados) dentro de seu construtor a partir de uma string de inicialização. Passaremos três parâmetros a esse macro: o nome da classe do objeto criado (Class), o nome da variável que armazenará o ponteiro para o objeto criado (Object) e a string de inicialização (Params).
No início, o macro declarará uma variável ponteiro com o nome e classe especificados, e a inicializará com o valor NULL. Em seguida, verificará se o objeto principal está em um estado correto. Se sim, chamamos o método de criação de objeto na fábrica através do macro NEW(). Tentamos converter o ponteiro criado para a classe desejada. O uso do operador de conversão de tipos dinâmico dynamic_cast<>() permite evitar erros de tempo de execução, caso a fábrica tenha criado um objeto de uma classe diferente da classe Class exigida no momento. Nesse caso, o ponteiro Object permanecerá NULL, e o programa continuará sua execução normalmente. E então, a verificação da validade do ponteiro é feita. Se ele estiver vazio ou incorreto, o objeto principal é colocado em um estado incorreto, é relatado um erro e a execução do construtor do objeto principal é interrompida.
Aqui está como esse macro se parece:
// Creating a child object in the factory from a string with verification. // Called only from the current object constructor. // If the object is not created, the current object becomes invalid // and exit from the constructor is performed #define CREATE(Class, Object, Params) \ Class *Object = NULL; \ if (IsValid()) { \ Object = dynamic_cast<C*> (NEW(Params)); \ if(!Object) { \ SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s", \ #Class, __LINE__, Params)); \ return; \ } \ } \
Vamos adicionar esses macros no início do arquivo Factorable.mqh.
Modificação das classes base anteriores
Adicionaremos a classe CFactorable como base em todas as classes base anteriores: СAdvisor, СStrategy, СVirtualStrategyGroup. Nos dois primeiros casos, não serão necessárias mais alterações:
//+------------------------------------------------------------------+ //| EA base class | //+------------------------------------------------------------------+ class CAdvisor : public CFactorable { protected: CStrategy *m_strategies[]; // Array of trading strategies virtual void Add(CStrategy *strategy); // Method for adding a strategy public: ~CAdvisor(); // Destructor virtual void Tick(); // OnTick event handler virtual double Tester() { return 0; } }; //+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy : public CFactorable { public: virtual void Tick() = 0; // Handle OnTick events };
//+------------------------------------------------------------------+ //| Class of trading strategies group(s) | //+------------------------------------------------------------------+ class CVirtualStrategyGroup : public CFactorable { protected: double m_scale; // Scaling factor void Scale(double p_scale); // Scaling the normalized balance public: CVirtualStrategyGroup(string p_params); // Constructor virtual string operator~() override; // Convert object to string CVirtualStrategy *m_strategies[]; // Array of strategies CVirtualStrategyGroup *m_groups[]; // Array of strategy groups }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) { // Save the initialization string m_params = p_params; // Read the initialization string of the array of strategies or groups string items = ReadArrayString(p_params); // Until the string is empty while(items != NULL) { // Read the initialization string of one strategy or group object string itemParams = ReadObject(items); // If this is a group of strategies, if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) { // Create a strategy group and add it to the groups array CREATE(CVirtualStrategyGroup, group, itemParams); APPEND(m_groups, group); } else { // Otherwise, create a strategy and add it to the array of strategies CREATE(CVirtualStrategy, strategy, itemParams); APPEND(m_strategies, strategy); } } // 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 // 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 // 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)); } } //+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CVirtualStrategyGroup::operator~() { return StringFormat("%s(%s)", typename(this), m_params); } ...
Mas СVirtualStrategyGroup passou por mudanças mais significativas. Como não é mais uma classe base abstrata, precisamos implementar o construtor nela, que cria um objeto a partir de uma string de inicialização. Com isso, eliminamos dois construtores separados que aceitavam um array de estratégias ou um array de grupos. Além disso, o método de conversão para string foi modificado. Agora, simplesmente anexamos o nome da classe à string de inicialização com os parâmetros. O método de escala Scale() permanece inalterado.
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { ... public: CVirtualAdvisor(string p_param); // Constructor ~CVirtualAdvisor(); // Destructor virtual string operator~() override; // Convert object to string ... }; ... //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { // Save the initialization string m_params = p_params; // Read the initialization string of the strategy group object string groupParams = ReadObject(p_params); // Read the magic number ulong p_magic = ReadLong(p_params); // Read the EA name string p_name = ReadString(p_params); // Read the work flag only at the bar opening m_useOnlyNewBar = (bool) ReadLong(p_params); // If there are no read errors, if(IsValid()) { // Create a strategy group CREATE(CVirtualStrategyGroup, p_group, groupParams); // Initialize the receiver with the static receiver m_receiver = CVirtualReceiver::Instance(p_magic); // Initialize the interface with the static interface m_interface = CVirtualInterface::Instance(p_magic); m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); // Save the work (test) start time m_fromDate = TimeCurrent(); // Reset the last save time m_lastSaveTime = 0; // Add the contents of the group to the EA Add(p_group); // Remove the group object delete p_group; } }
Vamos salvar as alterações no arquivo VirtualStrategyGroup.mqh na pasta atual.
Modificação da classe do expert
Na classe do expert CVirtualAdvisor, adicionamos no artigo anterior o método Init(), que deveria remover a duplicação de código entre os diferentes construtores do expert. Tínhamos um construtor que aceitava uma estratégia como primeiro argumento e outro que aceitava um objeto de grupo de estratégias. Provavelmente, não será difícil concordar que haverá apenas um construtor – que aceitará o grupo de estratégias. Se precisarmos usar apenas uma instância da estratégia de trading, primeiro criaremos um grupo com essa estratégia e passaremos o grupo criado para o construtor do expert. Isso elimina a necessidade do método Init(), bem como dos construtores adicionais. Portanto, deixaremos apenas um construtor, que cria o objeto expert a partir de uma string de inicialização:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_receiver) delete m_receiver; // Remove the recipient if(!!m_interface) delete m_interface; // Remove the interface DestroyNewBar(); // Remove the new bar tracking objects }
No construtor, primeiro lemos todos os dados da string de inicialização. Se, nessa etapa, algum erro for encontrado, o objeto expert criado será marcado como incorreto. Se tudo estiver correto, o construtor criará o grupo de estratégias, adicionará as suas estratégias ao seu array de estratégias e definirá as outras propriedades conforme os dados lidos da string de inicialização.
Mas agora, devido à verificação de correção antes de criar os objetos do destinatário e da interface no construtor, esses objetos podem não ser criados. Portanto, no destrutor, precisamos adicionar uma verificação da validade dos ponteiros para esses objetos antes de removê-los:
//+------------------------------------------------------------------+ //| Trading strategy using tick volumes | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { ... public: //--- Public methods CSimpleVolumesStrategy(string p_params); // Constructor virtual string operator~() override; // Convert object to string virtual void Tick() override; // OnTick event handler }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) { // Save the initialization string m_params = p_params; // Read the parameters from the initialization string m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalPeriod = (int) ReadLong(p_params); m_signalDeviation = ReadDouble(p_params); m_signaAddlDeviation = ReadDouble(p_params); m_openDistance = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_ordersExpiration = (int) ReadLong(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); m_fittedBalance = ReadDouble(p_params); // If there are no read errors, if(IsValid()) { // Request the required number of virtual positions CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders); // Load the indicator to get tick volumes m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK); // If the indicator is loaded successfully if(m_iVolumesHandle != INVALID_HANDLE) { // Set the size of the tick volume receiving array and the required addressing ArrayResize(m_volumes, m_signalPeriod); ArraySetAsSeries(m_volumes, true); // Register the event handler for a new bar on the minimum timeframe IsNewBar(m_symbol, PERIOD_M1); } else { // Otherwise, set the object state to invalid SetInvalid(__FUNCTION__, "Can't load iVolumes()"); } } } //+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CSimpleVolumesStrategy::operator~() { return StringFormat("%s(%s)", typename(this), m_params); }
Vamos salvar as alterações no arquivo VirtualAdvisor.mqh na pasta atual.
Modificação da classe de estratégia de trading
Na nossa classe de estratégia de trading CSimpleVolumesStrategy, também removeremos o construtor com parâmetros separados e reescreveremos o código do construtor que aceita uma string de inicialização, utilizando os métodos da classe CFactorable.
No construtor, lemos os parâmetros da string de inicialização, lembrando previamente seu estado original na propriedade m_params. Se não houver erros ao ler, que possam colocar o objeto da estratégia em um estado incorreto, então realizamos as principais ações de inicialização do objeto: preenchemos o array de posições virtuais, inicializamos o indicador e registramos o manipulador do evento de novo candle no timeframe de um minuto.
O método de conversão do objeto em string também mudou. Em vez de formar a string a partir dos parâmetros, simplesmente conectaremos o nome da classe à string de inicialização salva, como fizemos nos dois outros exemplos de classes discutidos anteriormente.
Core 1 2023.01.01 00:00:00 OnInit | Expert Params: Core 1 2023.01.01 00:00:00 class CVirtualAdvisor( Core 1 2023.01.01 00:00:00 class CVirtualStrategyGroup( Core 1 2023.01.01 00:00:00 [ Core 1 2023.01.01 00:00:00 class CSimpleVolumesStrategy("EURGBP",16385,17,0.70,0.90,150,10000.00,85.00,10000,3,0.00) Core 1 2023.01.01 00:00:00 ],1 Core 1 2023.01.01 00:00:00 ), Core 1 2023.01.01 00:00:00 ,27181,SimpleVolumesSingle,1 Core 1 2023.01.01 00:00:00 )
Também removemos os métodos Save() e Load() dessa classe, já que sua implementação na classe pai CVirtualStrategy foi suficiente para realizar as tarefas atribuídas a ela.
Vamos salvar as alterações no arquivo CSimpleVolumesStrategy.mqh na pasta atual.
EA para uma única instância de estratégia de trading
No EA para otimização de parâmetros de uma única instância de estratégia de trading, precisaremos alterar apenas a função de inicialização OnInit(). Nessa função, devemos gerar a string de inicialização do objeto da estratégia de trading a partir dos parâmetros de entrada do EA e, em seguida, usá-la para inserir na string de inicialização do objeto expert.
Graças à nossa implementação dos métodos de leitura de dados da string de inicialização, podemos usar livremente espaços adicionais e quebras de linha dentro dela. Assim, ao exibir no log ou salvar no banco de dados, poderemos ver a string de inicialização aproximadamente no seguinte formato:
... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CMoney::FixedBalance(fixedBalance_); // Prepare the initialization string for a single strategy instance string strategyParams = StringFormat( "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)", symbol_, timeframe_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_, 0 ); // Prepare the initialization string for an EA with a group of a single strategy string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],1\n" " ),\n" " ,%d,%s,%d\n" ")", strategyParams, magic_, "SimpleVolumesSingle", true ); PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams); // Create an EA handling virtual positions expert = NEW(expertParams); if(!expert) return INIT_FAILED; return(INIT_SUCCEEDED); } ... //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(!!expert) delete expert; }
Na função OnDeinit(), precisamos adicionar uma verificação antes de excluir o objeto expert para garantir que o ponteiro para ele seja válido. Agora, já não podemos garantir que o objeto expert será sempre criado, já que teoricamente podemos ter uma string de inicialização incorreta, o que levará à remoção antecipada do objeto expert pela fábrica.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Load strategy parameter sets int totalParams = LoadParams(fileName_, strategyParams); // If nothing is loaded, report an error if(totalParams == 0) { PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); return(INIT_PARAMETERS_INCORRECT); } // Report an error if if(count_ < 1) { // number of instances is less than 1 return INIT_PARAMETERS_INCORRECT; } ArrayResize(strategyParams, count_); // Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / 10.0); CMoney::FixedBalance(fixedBalance_); // Prepare the initialization string for the array of strategy instances string strategiesParams; FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n ", strategyParams[i % totalParams])); // Prepare the initialization string for an EA with the strategy group string expertParams = StringFormat("class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s],\n" " %.2f\n" " ),\n" " %d,%s,%d\n" ")", strategiesParams, scale_, magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_); // Create an EA handling virtual positions expert = NEW(expertParams); PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams); if(!expert) return INIT_FAILED; return(INIT_SUCCEEDED); }
Vamos salvar o código gerado no arquivo SimpleVolumesExpertSingle.mq5 na pasta atual.
EA para várias instâncias
Para verificar a criação do objeto expert com várias instâncias de estratégias de trading, pegaremos o EA da oitava parte, que usamos para realizar o teste de carga. Alteraremos a função OnInit() para utilizar o mecanismo de criação de expert desenvolvido neste artigo. Para isso, após carregar os parâmetros das estratégias do arquivo CSV, usaremos esses dados para complementar a string de inicialização do array de estratégias. Depois, usaremos essa string para formar a string de inicialização do grupo de estratégias e do próprio expert:
undefined
Semelhante ao EA anterior, adicionaremos à função OnDeinit() a verificação da validade do ponteiro para o objeto expert antes de excluí-lo.
Vamos salvar o código gerado no arquivo BenchmarkInstancesExpert.mq5 na pasta atual.
Teste de funcionalidade
Pegaremos o EA BenchmarkInstancesExpert.mq5 da oitava parte e o mesmo EA desta parte do artigo. Vamos executá-los com os mesmos parâmetros: 256 instâncias de estratégias de trading a partir do arquivo CSV Params_SV_EURGBP_H1.csv, período de teste – ano de 2022.


Fig. 2. Os resultados dos testes dos dois EA são completamente idênticos
Os resultados das duas implementações do EA foram absolutamente os mesmos. Portanto, no gráfico, eles são mostrados como uma única execução. Isso é muito positivo, pois agora podemos usar a última versão para seu desenvolvimento futuro.
Considerações finais
Assim, conseguimos possibilitar a criação de todos os objetos necessários utilizando strings de inicialização. Até agora, geramos essas strings praticamente de forma manual, mas, no futuro, poderemos lê-las diretamente do banco de dados. Foi exatamente para isso que iniciamos essa reestruturação do código já funcional.
Os resultados idênticos dos testes dos EAs, que diferem apenas na forma de criação dos objetos, ou seja, trabalhando com os mesmos conjuntos de instâncias de estratégias de trading, confirmam a correção das mudanças realizadas.
Agora podemos avançar e passar à automação do primeiro estágio planejado – a execução sequencial de vários processos de otimização do EA para ajustar os parâmetros de uma instância de estratégia de trading. Mas isso será tratado em artigos futuros.
Obrigado pela atenção e até a próxima!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14739
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.
Arbitragem triangular com previsões
Algoritmo de otimização baseado em brainstorming — Brain Storm Optimization (Parte II): Multimodalidade
Técnicas do MQL5 Wizard que você deve conhecer (Parte 20): Regressão Simbólica
Técnicas do MQL5 Wizard que você deve conhecer (Parte 19): Inferência Bayesiana
- 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
Olá Yuri. Obrigado pela interessante série de artigos.
Yuri, você poderia postar o arquivo de estratégia com o qual testou o Expert Advisor do artigo atual? Foi com ele que você obteve a captura de tela no final do artigo. Se ele estiver postado em algum lugar, diga-me onde, pois não o encontrei em outros artigos. Devo colocá-lo na pasta C:\Users\Admin/AppData/Roaming\MetaQuotes\Terminal\Common\Files ou na pasta do terminal? Quero ver se obtenho os mesmos resultados no terminal que na sua captura de tela.
Olá, Victor.
Esse arquivo pode ser obtido executando a otimização do EA com uma instância de estratégia e, depois de concluí-la, salvando seus resultados primeiro em XML e, em seguida, salvando-os em CSV do Excel. Isso foi explicado na Parte 6.