English Русский
preview
Desenvolvendo um EA multimoeda (Parte 10): Criação de objetos a partir de uma string

Desenvolvendo um EA multimoeda (Parte 10): Criação de objetos a partir de uma string

MetaTrader 5Negociação | 4 outubro 2024, 09:35
18 0
Yuriy Bykov
Yuriy Bykov

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 CVirtualStrategyGroup([
  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

Arbitragem triangular com previsões Arbitragem triangular com previsões
Este artigo simplifica a arbitragem triangular, mostrando como usar previsões e softwares especializados para negociar moedas de forma mais inteligente, mesmo se você for novo no mercado. Pronto para negociar com expertise?
Algoritmo de otimização baseado em brainstorming — Brain Storm Optimization (Parte II): Multimodalidade Algoritmo de otimização baseado em brainstorming — Brain Storm Optimization (Parte II): Multimodalidade
Na segunda parte do artigo, vamos para a implementação prática do algoritmo BSO, realizaremos testes com funções de teste e compararemos a eficiência do BSO com outros métodos de otimização.
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 20): Regressão Simbólica
A Regressão Simbólica é uma forma de regressão que começa com poucas ou nenhuma suposição sobre qual seria o modelo subjacente que mapeia os conjuntos de dados em estudo. Embora possa ser implementada por Métodos Bayesianos ou Redes Neurais, analisamos como uma implementação com Algoritmos Genéticos pode ajudar a personalizar uma classe de sinal especialista utilizável no MQL5 Wizard.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 19): Inferência Bayesiana Técnicas do MQL5 Wizard que você deve conhecer (Parte 19): Inferência Bayesiana
A inferência bayesiana é a adoção do Teorema de Bayes para atualizar hipóteses de probabilidade à medida que novas informações são disponibilizadas. Isso intuitivamente leva à adaptação na análise de séries temporais, então veremos como podemos usar isso na construção de classes personalizadas, não apenas para o sinal, mas também para gerenciamento de dinheiro e trailing-stops.