English Русский Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 3): Revisão da arquitetura

Desenvolvendo um EA multimoeda (Parte 3): Revisão da arquitetura

MetaTrader 5Negociação | 10 julho 2024, 10:27
188 0
Yuriy Bykov
Yuriy Bykov

Introdução

Nos artigos anteriores, continuamos o desenvolvimento de um EA multimoeda, operando simultaneamente com diferentes estratégias de negociação. Pode-se notar que a solução apresentada no segundo artigo já era substancialmente diferente da solução apresentada no primeiro. Isso indica que ainda estamos apenas tateando as opções ótimas.

Vamos tentar olhar para o sistema desenvolvido como um todo, afastando-nos dos detalhes da implementação, para entender as maneiras de melhorá-lo. Para isso, seguiremos a curta, mas perceptível, evolução do sistema.


Primeiro esquema de funcionamento

Destacamos um objeto especialista (classe CAdvisor ou seus derivados), que é um agregador de objetos de estratégias de negociação (classe CStrategy ou seus derivados). No início do trabalho do EA no manipulador OnInit(), ocorre o seguinte:

  • Cria-se o objeto especialista.
  • Criam-se objetos de estratégias de negociação e adicionam-se ao especialista no seu array de estratégias de negociação.

No EA, no manipulador de eventos OnTick(), ocorre o seguinte:

  • Chama-se o método CAdvisor::Tick() para o objeto especialista.
  • Este método percorre todas as estratégias e chama seu método CStrategy::Tick().
  • As estratégias, no contexto do CStrategy::Tick(), realizam todas as operações necessárias para abrir e fechar posições de mercado.

Esquematicamente, isso pode ser representado assim:



Fig. 1. Esquema de funcionamento do primeiro artigo

Fig. 1. Esquema de funcionamento do primeiro artigo

A vantagem desse esquema era que, tendo o código-fonte do EA, operando com uma única estratégia de negociação, era possível, com operações não muito complexas, modificá-lo para trabalhar conjuntamente com outros exemplares de estratégias de negociação.

Mas logo se descobriu a principal desvantagem: ao combinar várias estratégias, somos obrigados a reduzir, de alguma forma, o tamanho das posições abertas por cada exemplar de estratégia. Isso pode levar à exclusão completa da negociação de alguns ou até de todos os exemplares de estratégias. Quanto mais exemplares de estratégias incluímos no trabalho paralelo ou quanto menor o depósito inicial escolhido para negociação, mais provável é esse desfecho, já que o tamanho mínimo das posições de mercado abertas é fixo.

Além disso, ao trabalhar conjuntamente com vários exemplares de estratégias, houve situações em que foram abertas posições opostas de tamanho igual. Do ponto de vista do volume total, isso é equivalente à ausência de posições abertas, mas sobre as posições opostas abertas continuava a incidir o swap.


Segundo esquema de funcionamento

Para eliminar as desvantagens, decidimos transferir todas as operações com posições de mercado para um local separado, removendo a capacidade das estratégias de negociação de abrir diretamente posições de mercado. Isso, na verdade, complica um pouco a modificação de estratégias prontas, mas essa não é uma grande perda, que é amplamente compensada pela eliminação da principal desvantagem do primeiro esquema.

Em nosso esquema, surgem duas novas entidades: posições virtuais (classe CVirtualOrder) e o receptor de volumes de negociação das estratégias (classe CReceiver e seus derivados).

No início do trabalho do EA no manipulador OnInit(), ocorre o seguinte:

  • Cria-se o objeto receptor.
  • Cria-se o objeto especialista com a passagem do receptor criado para ele.
  • Criam-se objetos de estratégias de negociação e adicionam-se ao especialista no seu array de estratégias de negociação.
  • Cada estratégia cria seu próprio array de objetos de posições virtuais com a quantidade necessária desses objetos.

No EA, no manipulador de eventos OnTick(), ocorre o seguinte:

  • Chama-se o método CAdvisor::Tick() para o objeto especialista.
  • Este método percorre todas as estratégias e chama seu método CStrategy::Tick().
  • As estratégias, no contexto do CStrategy::Tick(), realizam todas as operações necessárias para abrir e fechar posições virtuais. Se ocorrer algum evento relacionado à alteração da composição das posições virtuais abertas, a estratégia registra que houve alterações, definindo uma flag.
  • Se pelo menos uma estratégia definir a flag de alterações, o receptor inicia o método de ajuste dos volumes de posições de mercado abertas. Com a execução bem-sucedida do ajuste, a flag de alterações de todas as estratégias é resetada.

Esquematicamente, isso pode ser representado assim:

Fig. 2. Esquema de funcionamento do segundo artigo

Fig. 2. Esquema de funcionamento do segundo artigo

Com essa organização do trabalho, não enfrentaremos mais a situação em que algum exemplar de estratégia não influencia de forma alguma o tamanho das posições de mercado abertas. Pelo contrário, até mesmo um exemplar que abra um volume virtual muito pequeno pode ser a gota que transborda o volume total de posições virtuais de vários exemplares de estratégias além do volume mínimo permitido da posição de mercado. E então a posição de mercado real será aberta.

Além disso, obtivemos outras mudanças agradáveis, como uma possível economia em swaps, menor carga no depósito, menor rebaixamento observado e melhoria nos indicadores de avaliação da qualidade do comércio (coeficiente de Sharpe, fator de lucro).

No processo de teste do segundo esquema, compreendemos o seguinte:

  • Cada estratégia primeiro realiza o mesmo processamento das posições virtuais já abertas para determinar os níveis de StopLoss e TakeProfit acionados. Se algum desses níveis for atingido, a posição virtual é fechada. Por isso, esse processamento foi imediatamente movido para um método estático da classe CVirtualOrder. Mas essa solução ainda parece uma generalização insuficiente.
  • Expandimos a composição das classes base, adicionando novas entidades obrigatórias. Em princípio, se não quisermos trabalhar com posições virtuais, ainda podemos usar essas classes base, simplesmente passando objetos "vazios" para elas. Por exemplo, pode-se criar um objeto da classe CReceiver, que contém apenas métodos stub vazios. Mas isso também parece mais uma solução temporária, necessitando de revisão.
  • Dotamos a classe base CStrategy com métodos adicionais e uma propriedade para rastrear alterações na composição das posições virtuais abertas, o que se refletiu no uso desses métodos na classe base CAdvisor. Novamente, isso parece um passo para restringir as possibilidades e impor uma implementação excessivamente específica na classe base.
  • Adicionamos na classe base CStrategy o método Volume(), que retorna o volume total das posições virtuais abertas, pois a classe do receptor CVolumeReceiver precisava de informações sobre os volumes virtuais abertos de cada estratégia. Mas, com isso, cortamos a possibilidade de abrir posições virtuais em vários símbolos dentro de uma única estratégia de negociação — nesse caso, o volume total perde seu sentido. Para testar estratégias de um único símbolo, essa solução é adequada, mas nada além disso.
  • Usamos na classe CReceiver um array para armazenar ponteiros para as estratégias criadas no especialista, para que o receptor pudesse verificar os volumes virtuais abertos de cada estratégia. Isso levou à duplicação do código que preenche os arrays de estratégias no especialista e no receptor.
  • Usamos diretamente na classe receptora CVolumeReceiver o fato de que cada estratégia abre posições apenas para um símbolo: ao adicionar ao array de estratégias do receptor, a estratégia informa seu símbolo, e ele é adicionado ao array de símbolos usados. O receptor, então, trabalha apenas com os símbolos adicionados ao seu array de símbolos. Sobre a limitação gerada por isso, já mencionamos acima.
Com base na análise das desvantagens listadas e nas discussões nos comentários, faremos as seguintes mudanças:
  • Vamos limpar ao máximo as classes base CStrategy e CAdvisor. Para o desenvolvimento da linha de EA que utiliza negociações virtuais, faremos nossas próprias classes derivadas CVirtualStrategy e CVirtualAdvisor. Agora elas serão nossas classes parentais para estratégias e especialistas específicos.
  • Expandiremos a classe de posições virtuais. Adicionaremos a cada posição virtual um ponteiro para o objeto receptor, que será responsável por transferir o volume virtual de negociação para o mercado, e um objeto da estratégia de negociação, que tomará decisões sobre a abertura/fechamento da posição virtual. Isso permitirá notificar os objetos interessados sobre as operações de abertura/fechamento das posições virtuais.
  • Moveremos o armazenamento de todas as posições virtuais para um único array, em vez de distribuí-las por vários arrays pertencentes aos exemplares das estratégias. Cada exemplar de estratégia solicitará vários elementos desse array para seu trabalho. O proprietário do array geral será o receptor dos volumes de negociação.
  • Haverá apenas um receptor em um EA. Daí que vamos implementá-lo como Singleton, cujo único exemplar estará disponível em todos os locais necessários. Essa implementação será feita na forma da classe derivada CVirtualReceiver.
  • No receptor, adicionaremos um array de novas entidades — receptores simbólicos (classe CVirtualSymbolReceiver). Cada receptor simbólico trabalhará apenas com as posições virtuais do seu símbolo, que serão automaticamente anexadas ao receptor simbólico na abertura e desanexadas ao fechamento da posição virtual.
Vamos tentar implementar tudo isso.


Limpeza das Classes Base

Deixaremos apenas o essencial nas classes base CStrategy e CAdvisor. Para CStrategy, deixaremos apenas o método de processamento do evento OnTick, resultando em um código sucinto:

//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy {
public:
   virtual void      Tick() = 0; // Handle OnTick events
};

Todo o resto estará nos descendentes dessa classe.

Na classe base CAdvisor, incluiremos um pequeno arquivo Macros.mqh, que contém vários macros úteis para realizar operações com arrays comuns:

  • APPEND(A, V) — adiciona o elemento V no final do array A;
  • FIND(A, V, I) — grava no índice I do array A o valor igual a V. Se o elemento não for encontrado, I receberá o valor -1;
  • ADD(A, V) — adiciona o elemento V ao array A no final, se o elemento ainda não estiver no array;
  • FOREACH(A, D) — loop pelos índices dos elementos do array A (o índice será na variável local i), executando as ações em D;
  • REMOVE_AT(A, I) — remove o elemento do array A na posição do índice I, deslocando os elementos subsequentes e reduzindo o tamanho do array;
  • REMOVE(A, V) — remove do array A o elemento igual a V.

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }
#define __MACROS_INCLUDE__
#endif
//+------------------------------------------------------------------+

Esses macros serão utilizados em outros arquivos, tornando o código mais compacto e, ao mesmo tempo, legível, evitando chamadas adicionais de funções.

Removeremos da classe CAdvisor todos os lugares onde o receptor aparecia, deixando no método de processamento do evento OnTick apenas a chamada aos manipuladores de eventos das estratégias. Teremos o seguinte código:

#include "Macros.mqh"
#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Delete all strategy objects
   FOREACH(m_strategies, delete m_strategies[i]);
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CAdvisor::Tick(void) {
// Call OnTick handling for all strategies
   FOREACH(m_strategies, m_strategies[i].Tick());
}

//+------------------------------------------------------------------+
//| Strategy adding method                                           |
//+------------------------------------------------------------------+
void CAdvisor::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);  // Add the strategy to the end of the array
}
//+------------------------------------------------------------------+

Essas classes permanecerão nos arquivos Strategy.mqh e Advisor.mqh na pasta atual.

Agora, vamos transferir o código necessário para as classes derivadas de estratégia e especialista, que devem trabalhar com posições virtuais.

Criaremos a classe CVirtualStrategy, derivada de CStrategy. Adicionaremos os seguintes campos e métodos:

  • Array de posições virtuais (ordens);
  • Quantidade total de posições e ordens abertas;
  • Método para contar o número de posições e ordens virtuais abertas;
  • Métodos para processar eventos de abertura/fechamento de posições virtuais (ordens).
Por enquanto, os métodos de processamento de eventos de abertura/fechamento de posições virtuais apenas chamarão o método de recontagem do número de posições virtuais abertas, que atualizará o valor do campo m_ordersTotal. Outras ações não são necessárias por enquanto, mas, no futuro, pode ser necessário fazer algo mais. Por isso, esses métodos foram feitos separadamente do método de contagem de posições virtuais abertas.

#include "Strategy.mqh"
#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
protected:
   CVirtualOrder     *m_orders[];   // Array of virtual positions (orders)
   int               m_ordersTotal; // Total number of open positions and orders

   virtual void      CountOrders(); // Calculate the number of open positions and orders

public:
   virtual void      OnOpen();      // Event handler for opening a virtual position (order)
   virtual void      OnClose();     // Event handler for closing a virtual position (order)
};

//+------------------------------------------------------------------+
//| Counting open virtual positions and orders                       |
//+------------------------------------------------------------------+
void CVirtualStrategy::CountOrders() {
   m_ordersTotal = 0;
   FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; })
}

//+------------------------------------------------------------------+
//| Event handler for opening a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnOpen() {
   CountOrders();
}

//+------------------------------------------------------------------+
//| Event handler for closing a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnClose() {
   CountOrders();
}

Salvaremos esse código no arquivo VirtualStrategy.mqh na pasta atual.

Como removemos a funcionalidade do receptor da classe base CAdvisor, precisamos transferi-la para nossa nova classe derivada CVirtualAdvisor. Nesta classe, adicionaremos o campo m_receiver para armazenar o ponteiro para o objeto receptor dos volumes de negociação.

No construtor, esse campo será inicializado com o ponteiro para o único objeto receptor possível, que será criado neste momento ao chamar o método estático CVirtualReceiver::Instance(). O destrutor garantirá que este objeto seja removido corretamente.

No manipulador de eventos OnTick, também adicionaremos novas ações. Antes de iniciar os manipuladores deste evento nas estratégias, primeiro executaremos o manipulador deste evento no receptor e, após a execução do evento pelas estratégias, executaremos o método do receptor que realiza a correção dos volumes abertos. Se o receptor agora é o proprietário de todas as posições virtuais, ele mesmo pode determinar a existência de alterações. Dito isso, a implementação do rastreamento de alterações na classe de estratégia de negociação está ausente, e removemos isso não apenas da classe base das estratégias, mas completamente.

#include "Advisor.mqh"
#include "VirtualReceiver.mqh"

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver  *m_receiver; // Receiver object that brings positions to the market

public:
                     CVirtualAdvisor(ulong p_magic = 1); // Constructor
                    ~CVirtualAdvisor();                  // Destructor
   virtual void      Tick() override;                    // OnTick event handler

};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)) {};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();
   
// Start handling in strategies
   CAdvisor::Tick();
   
// Adjusting market volumes
   m_receiver.Correct();
}
//+------------------------------------------------------------------+

Salvaremos esse código no arquivo VirtualAdvisor.mqh na pasta atual.


Expansão da classe de posições virtuais

Adicionaremos à classe de posição virtual um ponteiro para o objeto receptor m_receiver e o objeto da estratégia de negociação m_strategy. Os valores desses campos devem ser passados através dos parâmetros do construtor, portanto, faremos alterações nele. Também será necessário adicionar alguns getters para as propriedades privadas da posição virtual: Id() e Symbol(). Mostraremos o código adicionado na descrição da classe:

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
//--- Static fields...
   
//--- Related recipient objects and strategies
   CVirtualReceiver  *m_receiver;
   CVirtualStrategy  *m_strategy;

//--- Order (position) properties ...
   
//--- Closed order (position) properties ...
   
//--- Private methods
   
public:
                     CVirtualOrder(
      CVirtualReceiver *p_receiver,
      CVirtualStrategy *p_strategy
   );                                  // Constructor

//--- Methods for checking the position (order) status ...
   

//--- Methods for receiving position (order) properties ...
   ulong             Id() {            // ID
      return m_id;
   }
   string            Symbol() {        // Symbol
      return m_symbol;
   }

//--- Methods for handling positions (orders) ...
  
};

Na implementação do construtor, apenas adicionamos duas linhas na lista de inicialização para definir os valores dos novos campos a partir dos parâmetros do construtor:

CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
// Initialization list
   m_id(++s_count),  // New ID = object counter + 1
   m_receiver(p_receiver),
   m_strategy(p_strategy),
   ...,
   m_point(0) {
}

A notificação do receptor e da estratégia deve ocorrer apenas na abertura ou fechamento da posição virtual. Isso acontece apenas no método Open() e Close(), daí que adicionaremos um pequeno código neles:

//+------------------------------------------------------------------+
//| Open a virtual position                                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::Open(...) {
   // If the position is already open, then do nothing ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      // Update information about current prices ...

      // Initialize position properties ...
  
      // Depending on the direction, set the opening price, as well as the SL and TP levels ...
            
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();

      ...

      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
void CVirtualOrder::Close() {
   if(IsOpen()) { // If the position is open
      ...
      // Define the closure reason to be displayed in the log ...
     
      // Save the close price depending on the type ...
    
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }
}

Nos manipuladores OnOpen() e OnClose() para o receptor, passamos como parâmetro o ponteiro para o objeto atual da posição virtual. Nos manipuladores para a estratégia, isso ainda não é necessário, portanto, eles são implementados sem parâmetros.

Este código permanece na pasta atual no arquivo com o mesmo nome — VirtualOrder.mqh.


Implementação do novo receptor

Vamos começar a implementação da classe receptor CVirtualReceiver garantindo a unicidade da instância desta classe. Para isso, usaremos o padrão de projeto padrão chamado Singleton. Precisaremos:

  • tornar o construtor da classe não público;
  • adicionar um campo estático da classe que armazena o ponteiro para o objeto desta classe;
  • adicionar um método estático que cria, na ausência, uma instância desta classe ou retorna a já existente.

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
// Static pointer to a single class instance
   static   CVirtualReceiver *s_instance;

   ...

   CVirtualReceiver(ulong p_magic = 0);   // Private constructor

public:
//--- Static methods
   static
   CVirtualReceiver  *Instance(ulong p_magic = 0);    // Singleton - creating and getting a single instance

   ...
};

// Initializing a static pointer to a single class instance
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;


//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualReceiver(p_magic);
   }
   return s_instance;
}

Em seguida, adicionaremos à classe um array para armazenar todas as posições virtuais m_orders. Cada instância de estratégia solicitará ao receptor uma determinada quantidade de posições virtuais. Para isso, adicionaremos um método estático Get(), que criará a quantidade necessária de objetos de posições virtuais, adicionando os ponteiros para eles no array do receptor e no array de posições virtuais da estratégia.

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualOrder     *m_orders[];         // Array of virtual positions
   
   ...

public:
//--- Static methods
   ...
   static void       Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         int n); // Allocate the necessary amount of virtual positions to the strategy
   ...
};

...

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i])) // Register the created virtual position
   ...
}

Agora é hora de adicionar à classe um array de ponteiros para os objetos dos receptores simbólicos (classe CVirtualSymbolReceiver). Essa classe ainda não foi criada, mas já entendemos de maneira geral o que ela deve fazer — abrir e fechar diretamente as posições de mercado de acordo com os volumes virtuais para um único símbolo. Por isso, podemos dizer que a quantidade de objetos receptores simbólicos será igual ao número de símbolos diferentes usados no EA. Faremos essa classe herdar de CReceiver, de modo que ela terá o método Correct(), que realiza o trabalho principal, além de adicionar os métodos auxiliares necessários.

Mas isso será mais tarde, agora voltamos para a classe CVirtualReceiver e adicionamos a sobrecarga do método virtual Correct().

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualSymbolReceiver *m_symbolReceivers[];       // Array of recipients for individual symbols

public:
   ...
//--- Public methods
   virtual bool      Correct() override;              // Adjustment of open volumes
};

A implementação do método Correct() agora será bastante simples, pois transferiremos o trabalho principal para o nível inferior da hierarquia. Agora será suficiente apenas iterar todos os receptores simbólicos em um loop e chamar seu método Correct().

Para reduzir o número de chamadas desnecessárias, adicionaremos uma verificação prévia para garantir que a negociação esteja realmente permitida, adicionando o método IsTradeAllowed(), que responde a essa pergunta. Além disso, adicionaremos o campo m_isChanged à classe, que funcionará como uma flag indicando a existência de mudanças nas posições virtuais abertas. Esse campo também será verificado antes de chamar a correção.

class CVirtualReceiver : public CReceiver {
   ...
   bool              m_isChanged;         // Are there any changes in open positions?
   ...
   bool              IsTradeAllowed();    // Is trading available?

public:
   ...

   virtual bool      Correct() override;  // Adjustment of open volumes
};
//+------------------------------------------------------------------+
//| Adjust open volumes                                              |
//+------------------------------------------------------------------+
bool CVirtualReceiver::Correct() {
   bool res = true;
   if(m_isChanged && IsTradeAllowed()) {
      // If there are changes, then we call the adjustment of the recipients of individual symbols
      FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
      m_isChanged = !res;
   }
   return res;
}

No método IsTradeAllowed(), verificaremos o estado do terminal e da conta de negociação para determinar se a negociação real é permitida:

//+------------------------------------------------------------------+
//| Is trading available?                                            |
//+------------------------------------------------------------------+
bool CVirtualReceiver::IsTradeAllowed() {
   return (true
           && MQLInfoInteger(MQL_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
           && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
           && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_CONNECTED)
          );
}

Usamos a flag de existência de alterações no método Correct(), que é redefinida se a correção dos volumes for bem-sucedida. Mas onde essa flag deve ser definida? Obviamente, isso deve ocorrer quando uma posição virtual é aberta ou fechada. Na classe CVirtualOrder, adicionamos especificamente nos métodos de abertura/fechamento a chamada para os métodos OnOpen() e OnClose() do CVirtualReceiver. Nesses métodos, definiremos a flag de existência de alterações.

Além disso, nesses manipuladores, devemos notificar o receptor simbólico correspondente sobre a existência de alterações. Ao abrir a primeira posição virtual para um determinado símbolo, o receptor simbólico correspondente ainda não existe, então precisamos criá-lo e notificá-lo. Nas operações subsequentes de abertura/fechamento de posições virtuais para esse símbolo, o receptor simbólico correspondente já existirá, então ele apenas precisa ser notificado.

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              OnOpen(CVirtualOrder *p_order);  // Handle virtual position opening
   void              OnClose(CVirtualOrder *p_order); // Handle virtual position closing
   ...
};

//+------------------------------------------------------------------+
//| Handle opening a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();      // Define position symbol
   CVirtualSymbolReceiver *symbolReceiver;
   int i;
   FIND(m_symbolReceivers, symbol, i);    // Search for the symbol recipient

   if(i == -1) {
      // If not found, then create a new recipient for the symbol
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      // and add it to the array of symbol recipients 
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      // If found, then take it
      symbolReceiver = m_symbolReceivers[i];
   }
   
   symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position
   m_isChanged = true;           // Remember that there are changes
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();   // Define position symbol
   int i;
   FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient

   if(i != -1) {
      m_symbolReceivers[i].Close(p_order);   // Notify the symbol recipient about closing a position
      m_isChanged = true;                    // Remember that there are changes
   }
}

Além de abrir/fechar posições virtuais por sinais da estratégia de negociação, elas podem ser fechadas ao atingir os níveis de StopLoss ou TakeProfit. Na classe CVirtualOrder, temos um método específico para isso, Tick(), que verifica os níveis e fecha a posição virtual, se necessário. Mas esse método precisa ser chamado em cada tick e para todas as posições virtuais. Isso será feito pelo método Tick() na classe CVirtualReceiver, que adicionaremos agora:

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              Tick();     // Handle a tick for the array of virtual orders (positions)
   ...
};

//+------------------------------------------------------------------+
//| Handle a tick for the array of virtual orders (positions)        |
//+------------------------------------------------------------------+
void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick());
}

E, finalmente, vamos garantir a liberação correta da memória alocada para os objetos de posições virtuais. Como todos eles estão no array m_orders, adicionaremos um destrutor que realizará sua remoção:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions
}

Salvaremos o código resultante no arquivo VirtualReceiver.mqh na pasta atual.


Implementação do receptor simbólico

Resta implementar a última classe CVirtualSymbolReceiver para que o esquema adquira um formato final, pronto para uso. Seu conteúdo principal será baseado na classe CVolumeReceiver do artigo anterior, removendo as partes relacionadas à definição do símbolo de cada posição virtual e à iteração pelos símbolos durante a execução da correção.

Os objetos dessa classe também terão seu próprio array de ponteiros para objetos de posições virtuais, mas sua composição mudará constantemente. Exigiremos que este array contenha apenas as posições virtuais abertas. Por isso, fica claro o que fazer ao abrir e fechar uma posição virtual: assim que uma posição virtual for aberta, devemos adicioná-la ao array do receptor simbólico correspondente e, assim que for fechada, removê-la desse array.

Também será conveniente ter uma flag indicando a existência de alterações na composição das posições virtuais abertas. Isso ajudará a evitar verificações desnecessárias a cada tick.

Adicionaremos campos para o símbolo, o array de posições e o indicador de existência de alterações, além de dois métodos para tratar a abertura/fechamento na classe:

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   ...   

public:
   ...
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   ...
};

A implementação desses métodos é trivial: adicionamos/removemos a posição virtual passada no array e configuramos a flag de existência de alterações.

//+------------------------------------------------------------------+
//| Register opening a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
   APPEND(m_orders, p_order); // Add a position to the array
   m_isChanged = true;        // Set the changes flag
}

//+------------------------------------------------------------------+
//| Register closing a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
   REMOVE(m_orders, p_order); // Remove a position from the array
   m_isChanged = true;        // Set the changes flag
}

Também precisaremos, a partir do receptor, realizar a busca pelo receptor simbólico correspondente pelo nome do símbolo. Para usar o algoritmo de busca linear comum a partir do macro FIND(A,V,I), adicionaremos um operador de comparação sobrecarregado para o receptor simbólico com a string, que retornará verdadeiro se o símbolo da instância coincidir com a string passada:

class CVirtualSymbolReceiver : public CReceiver {
   ...

public:
   ...
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   ...
};

Vamos fornecer a descrição completa da classe CVirtualSymbolReceiver. A implementação específica de todos os métodos pode ser encontrada nos arquivos anexados.

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   bool              m_isNetting;      // Is this a netting account?

   double            m_minMargin;      // Minimum margin for opening

   CPositionInfo     m_position;       // Object for obtaining properties of market positions
   CSymbolInfo       m_symbolInfo;     // Object for getting symbol properties
   CTrade            m_trade;          // Object for performing trading operations

   double            MarketVolume();   // Volume of open market positions
   double            VirtualVolume();  // Volume of open virtual positions
   bool              IsTradeAllowed(); // Is trading by symbol available? 

   // Required volume difference
   double            DiffVolume(double marketVolume, double virtualVolume);

   // Volume correction for the required difference
   bool              Correct(double oldVolume, double diffVolume);

   // Auxiliary opening methods
   bool              ClearOpen(double diffVolume);
   bool              AddBuy(double volume);
   bool              AddSell(double volume);
   
   // Auxiliary closing methods
   bool              CloseBuyPartial(double volume);
   bool              CloseSellPartial(double volume);
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull();

   // Check margin requirements
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);

public:
                     CVirtualSymbolReceiver(ulong p_magic, string p_symbol);  // Constructor
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   
   virtual bool      Correct() override;              // Adjustment of open volumes
};

Salvaremos esse código no arquivo VirtualSymbolReceiver.mqh na pasta atual.


Comparação de resultados

O esquema de trabalho obtido pode ser representado da seguinte forma:


Fig. 3. Esquema de trabalho deste artigo

Fig. 3. Esquema de trabalho deste artigo

Agora, a parte mais interessante. Vamos compilar o EA usando nove instâncias de estratégias com os mesmos parâmetros da última artigo. Executaremos testes do EA similar ao do artigo anterior e do recém-compilado:


Fig. 3. Resultados do EA do artigo anterior.


Fig. 4. Resultados do EA deste artigo.

No geral, os resultados são praticamente idênticos. Os gráficos de saldo visualmente não apresentam diferença. Pequenas diferenças visíveis nos relatórios podem ser causadas por vários motivos e serão analisadas posteriormente.


Avaliação do potencial futuro

Na discussão do artigo anterior no fórum, foi feita a pergunta: quais são os resultados de negociação mais atraentes que podem ser obtidos utilizando essa abordagem? Até agora, os gráficos mostraram uma rentabilidade de 20% em 5 anos, o que não parece muito atraente.

Atualmente, a resposta a essa pergunta pode ser dada aproximadamente assim. Primeiro, devemos separar claramente os resultados decorrentes das estratégias simples escolhidas e os resultados decorrentes da implementação do trabalho conjunto.

Os resultados da primeira categoria mudarão ao trocar a estratégia simples por outra. É claro que quanto melhores forem os resultados apresentados por instâncias individuais de estratégias simples, melhor será o resultado conjunto. Os resultados apresentados aqui são baseados em uma única ideia de negociação e são inicialmente determinados pela sua qualidade e adequação. Esses resultados são avaliados simplesmente pelo índice de lucro/rebaixamento durante o intervalo de teste.

Os resultados da segunda categoria são resultados comparativos do trabalho conjunto e do trabalho individual. Aqui, a avaliação é feita por outros indicadores: melhoria na linearidade do gráfico de crescimento dos fundos, redução do rebaixamento, entre outros. Esses resultados parecem mais importantes, pois há esperança de que possam elevar a um nível aceitável os resultados não tão impressionantes da primeira categoria.

Mas, para todos os resultados, é desejável primeiro implementar a negociação com lote variável. Sem isso, é mais difícil avaliar a relação lucro/rebaixamento com base nos resultados dos testes, mas ainda é possível. 

Vamos tentar usar um pequeno depósito inicial e encontrar um novo valor ideal para o tamanho das posições abertas para um rebaixamento máximo permitido de 50% no período de 5 anos (2018.01.01 — 2023.01.01). Abaixo estão os resultados dos testes do EA deste artigo com diferentes multiplicadores de tamanho de posição, mas constantes ao longo de todos os cinco anos com um depósito inicial de $1000. No artigo anterior, os tamanhos das posições foram calibrados para um depósito de $10000, então o valor inicial depoPart_ foi reduzido em aproximadamente 10 vezes.

Fig. 5. Resultados dos testes com diferentes tamanhos de posição.

Vemos que, com o mínimo depoPart_ = 0.04, o EA não abriu posições reais, pois seu volume, quando recalculado proporcionalmente ao saldo, era inferior a 0.01. Mas, a partir do próximo valor do multiplicador depoPart_ = 0.06, as posições de mercado foram abertas.

Com o máximo depoPart_ = 0.4, obtemos um lucro de aproximadamente $22800. No entanto, o rebaixamento mostrado aqui é o rebaixamento relativo ocorrido durante todo o tempo de teste. Mas 10% de 23000 e de 1000 são valores muito diferentes. Logo, é necessário ver os resultados de uma única execução:

Fig. 6. Resultados dos testes com o máximo depoPart_ = 0.4

Como podemos ver, o rebaixamento realmente alcançou $1167, o que, no momento, representou apenas 9.99% do saldo atual, mas se o início do período de teste estivesse localizado imediatamente antes desse momento desagradável, teríamos perdido todo o depósito. Portanto, esse tamanho de posição não pode ser usado.

Vamos ver os resultados com depoPart_ = 0.2


Fig. 7. Resultados dos testes com depoPart_ = 0.2


Aqui, o rebaixamento máximo não excedeu $494, ou seja, cerca de 50% do depósito inicial de $1000. Por isso, pode-se dizer que, com esse tamanho de posição, mesmo se o início do período for escolhido de maneira maximamente desfavorável ao longo dos cinco anos considerados, não haverá perda total do depósito.

Com esse tamanho de posição, os resultados dos testes para 1 ano (2022) serão os seguintes:


Fig. 8. Resultados dos testes para o ano de 2022 com depoPart_ = 0.2

Ou seja, com um rebaixamento máximo esperado de cerca de 50%, foi mostrado um lucro de aproximadamente 150% ao ano.

Esses resultados são encorajadores, mas também têm seus pontos negativos. Por exemplo, os resultados para 2023, que não participaram da otimização dos parâmetros, já são visivelmente piores:

Fig. 9. Resultados dos testes para 2023 com depoPart_ = 0.2

Claro, obtivemos um lucro de 40% no final do ano, mas em 8 dos 12 meses não houve crescimento sustentado. Este problema é visto como o principal, e esta série de artigos, em geral, será dedicada a considerar diferentes abordagens para resolvê-lo.


Considerações finais

Neste artigo, preparamos o código para um desenvolvimento futuro, simplificando e otimizando o código da parte anterior. Eliminamos as deficiências identificadas, que poderiam limitar nossas possibilidades de usar várias estratégias de negociação no futuro. Os resultados dos testes mostraram que a nova implementação funciona tão bem quanto a anterior. A velocidade de execução permaneceu inalterada, mas talvez o ganho se manifeste apenas com o aumento exponencial no número de instâncias de estratégias.

Para isso, precisamos finalmente tratar de como armazenaremos os parâmetros de entrada das estratégias, como os combinaremos em bibliotecas de parâmetros e como selecionaremos as melhores combinações obtidas a partir da otimização de instâncias individuais das estratégias.

No próximo artigo, continuaremos a trabalhar na direção escolhida.


Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14148

Introdução ao MQL5 (Parte 4): Estruturas, classes e funções de tempo Introdução ao MQL5 (Parte 4): Estruturas, classes e funções de tempo
Nesta série, continuamos a desvendar os segredos da programação. No novo artigo, vamos estudar as bases das estruturas, classes e funções de tempo e adquirir novas habilidades para programação eficiente. Este guia pode ser útil não apenas para iniciantes, mas também para desenvolvedores experientes, pois simplifica conceitos complexos, fornecendo informações valiosas para dominar o MQL5. Continue aprendendo coisas novas, aperfeiçoe suas habilidades de programação e domine o mundo da negociação algorítmica.
Redes neurais de maneira fácil (Parte 76): explorando diversos modos de interação (Multi-future Transformer) Redes neurais de maneira fácil (Parte 76): explorando diversos modos de interação (Multi-future Transformer)
Neste artigo, continuamos o tema de previsão do movimento de preços. E convido você a conhecer a arquitetura do Multi-future Transformer. A ideia principal é decompor a distribuição multimodal do futuro em várias distribuições unimodais, permitindo modelar eficientemente diversos modos de interação entre os agentes na cena.
Desenvolvendo um sistema de Replay (Parte 56): Adequando os Módulos Desenvolvendo um sistema de Replay (Parte 56): Adequando os Módulos
Apesar dos módulos estarem se comunicando de maneira adequada, existe uma falha quando é tentado usar o indicador de mouse no serviço de replay. Precisamos corrigir isto agora, antes de dar o próximo passo. Além disto, havia uma falha que finalmente foi devidamente corrigida no código do indicador de mouse. Então esta versão finalmente se tornou estável, e devidamente finalizada.
Desenvolvendo um EA multimoeda (Parte 2): Transição para posições virtuais de estratégias de trading Desenvolvendo um EA multimoeda (Parte 2): Transição para posições virtuais de estratégias de trading
Vamos continuar a desenvolver o EA multimoeda com várias estratégias funcionando paralelamente. Tentaremos transferir todo o trabalho relacionado à abertura de posições a mercado do nível das estratégias para o nível do expert que gerencia as estratégias. As próprias estratégias irão negociar apenas virtualmente, sem abrir posições a mercado.