English Русский
preview
Desenvolvendo um EA multimoeda (Parte 12): Gerenciamento de Risco como em empresas de prop trading

Desenvolvendo um EA multimoeda (Parte 12): Gerenciamento de Risco como em empresas de prop trading

MetaTrader 5Negociação | 17 outubro 2024, 16:09
25 0
Yuriy Bykov
Yuriy Bykov

Introdução

Ao longo de todo o ciclo de artigos, já nos deparamos várias vezes com o tema do controle de riscos. Foram introduzidos conceitos de estratégias de trading normalizadas, cujos parâmetros asseguram um rebaixamento de 10% ao longo do período de teste. No entanto, a normalização desses exemplos de estratégias de trading, bem como de grupos de estratégias, pode garantir o rebaixamento predefinido apenas no período histórico. Ao realizar testes em um período futuro ou já ao vivo em uma conta de trading, não podemos garantir o cumprimento do nível de rebaixamento estabelecido.

Recentemente, o tema de controle de riscos foi abordado em artigos como Gerenciador de risco para operar manualmente e Gerenciamento de Risco para trading algorítmico. Neles, o autor propôs uma implementação programática que monitora o cumprimento de vários parâmetros de trading com indicadores predefinidos. Por exemplo, ao exceder o nível de perda estabelecido para o dia, semana ou mês, a negociação é pausada.

Outro artigo bastante interessante foi Aprenda algumas lições com as Empresas de Prop Trading (Parte 1) — Uma introdução, onde o autor examina os requisitos de trading impostos por essas empresas para testar traders que desejam gerenciar capital. Apesar da controvérsia em torno dessas empresas, amplamente discutida em vários recursos de trading, a aplicação de regras claras de controle de riscos é uma das chaves para o sucesso no trading. Portanto, por que não aproveitar essa experiência já existente e implementar nosso próprio gerenciamento de riscos, com base no modelo de controle de riscos usado por empresas de prop trading?


Modelo e conceitos

Para o gerenciamento de riscos, usaremos os seguintes conceitos:

  • Saldo base é o saldo inicial da conta (ou parte do saldo da conta) a partir do qual os outros parâmetros podem ser calculados. Em nosso exemplo, utilizaremos o valor desse indicador igual a 10000.
  • Saldo base diário é o saldo da conta de trading no início do período diário atual. Para simplificar, consideraremos que o início do período diário coincide com o aparecimento de um novo candle no terminal no timeframe D1.
  • Fundos base diários é o valor dos fundos na conta de trading no início do período diário atual.
  • Nível diário é o máximo entre o saldo base diário e os fundos. É determinado no início do período diário e mantém seu valor até o início do próximo período diário.
  • Máxima perda diária é o valor de desvio para baixo dos fundos na conta em relação ao nível diário, no qual a negociação deve ser interrompida no período diário atual. No próximo período diário, a negociação será retomada. A interrupção pode envolver várias ações, destinadas a reduzir o tamanho das posições abertas, até o fechamento completo. Para começar, usaremos exatamente esse modelo simples: ao atingir a perda máxima diária, todas as posições de mercado abertas serão fechadas. 
  • Máxima perda total é o valor de desvio para baixo dos fundos na conta em relação ao saldo base, no qual a negociação é totalmente interrompida, ou seja, não será retomada nos períodos seguintes. Ao atingir esse nível, todas as posições abertas são fechadas.

Vamos nos limitar a dois níveis de interrupção da negociação: diário e total. Podemos ainda adicionar, de maneira semelhante, níveis semanais ou mensais. Mas, como as empresas de prop trading não possuem esses níveis, não vamos complicar a primeira implementação do nosso gerente de risco. Se necessário, eles podem ser adicionados posteriormente.

Diferentes empresas de prop trading podem ter abordagens ligeiramente diferentes para o cálculo da perda máxima diária e total. Portanto, prevemos em nosso gerente de risco três formas possíveis de definir um valor numérico para o cálculo da perda máxima:

  • Fixo na moeda do depósito. Com esse método, especificamos diretamente no parâmetro o valor da perda, expresso em unidades da moeda da conta de negociação. Vamos defini-lo como um número positivo.
  • Em porcentagem do saldo base. Nesse caso, o valor é percebido como uma porcentagem do saldo base definido. Como o saldo base em nosso modelo é uma quantidade constante (até a reinicialização da conta e do EA com outro valor de saldo base definido manualmente), a perda máxima calculada dessa forma também será uma quantidade constante. Poderíamos reduzir este caso ao primeiro, mas como geralmente se especifica a porcentagem da perda máxima, vamos mantê-lo como um caso separado.
  • Em porcentagem do nível diário. Nesta variante, no início de cada período diário, recalculamos o nível de perda máxima como uma porcentagem do nível diário recém-calculado. Com o aumento do saldo ou dos fundos, o valor da perda máxima também aumentará. Esse método será utilizado principalmente para calcular apenas a perda máxima diária. A perda máxima total é geralmente fixa em relação ao saldo base.

Vamos agora implementar a nossa classe de gerente de risco, como sempre, seguindo o princípio da menor ação possível. Faremos primeiro a implementação mínima necessária, deixando espaço para complexificações futuras, se necessário.


Classe CVirtualRiskManager

Durante o desenvolvimento desta classe, passamos por várias fases. Inicialmente, ela foi feita como completamente estática, para que pudesse ser usada livremente em todos os objetos. Em seguida, surgiu a hipótese de que poderíamos também otimizar os parâmetros do gerente de risco, e seria útil ter a capacidade de salvá-los em uma string de inicialização. Para isso, a classe foi feita como herdeira da classe CFactorable. Para garantir a possibilidade de usar o gerente de risco em objetos de diferentes classes, foi implementado o padrão Singleton. No entanto, logo ficou claro que o gerente de risco era necessário apenas em uma única classe, nomeadamente na classe do expert CVirtualAdvisor. Portanto, removemos a implementação do padrão Singleton da classe de gerenciamento de risco.

Primeiramente, criaremos enumerações para os possíveis estados do gerente de risco e os diferentes métodos de cálculo dos limites:

// Possible risk manager states
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Limits are not exceeded 
   RM_STATE_DAILY_LOSS,    // Daily limit is exceeded
   RM_STATE_OVERALL_LOSS   // Overall limit is exceeded
};


// Possible methods for calculating limits
enum ENUM_RM_CALC_LIMIT {
   RM_CALC_LIMIT_FIXED,          // Fixed (USD)
   RM_CALC_LIMIT_FIXED_PERCENT,  // Fixed (% from Base Balance)
   RM_CALC_LIMIT_PERCENT         // Relative (% from Daily Level)
};


Na descrição da classe do gerente de risco, teremos várias propriedades para armazenar os parâmetros de entrada, que serão passados por meio de uma string de inicialização no construtor. Também adicionaremos propriedades para armazenar várias características calculadas, como saldo atual, fundos, lucros e outros. Declararemos alguns métodos auxiliares na seção protegida. Na seção pública, basicamente teremos apenas o construtor e o método para processar cada tick. Os métodos de salvar/carregar e o operador de conversão para string serão apenas mencionados, com a implementação a ser escrita posteriormente.

Então, a descrição da classe será aproximadamente assim:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   bool              m_isActive;             // Is the risk manager active?

   double            m_baseBalance;          // Base balance

   ENUM_RM_CALC_LIMIT m_calcDailyLossLimit;  // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;    // Parameter of calculating the maximum daily loss

   ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss
   double            m_maxOverallLossLimit;  // Parameter of calculating the maximum total loss

// Current state
   ENUM_RM_STATE     m_state;

// Updated values
   double            m_balance;              // Current balance
   double            m_equity;               // Current equity
   double            m_profit;               // Current profit
   double            m_dailyProfit;          // Daily profit
   double            m_overallProfit;        // Total profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_prevDepoPart;         // Used part of the total balance

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss

   void              UpdateProfit();         // Update current profit values
   void              UpdateBaseLevels();     // Updating daily base levels

   void              CheckLimits();          // Check for excess of permissible losses
   void              CheckDailyLimit();      // Check for excess of the permissible daily loss
   void              CheckOverallLimit();    // Check for excess of the permissible total loss

   double            VirtualProfit();        // Determine the real size of the virtual position

public:
                     CVirtualRiskManager(string p_params);     // Constructor

   virtual void      Tick();                 // Tick processing in risk manager 

   virtual bool      Load(const int f);      // Load status
   virtual bool      Save(const int f);      // Save status

   virtual string    operator~() override;   // Convert object to string
};


O construtor do objeto de gerenciamento de risco irá esperar seis valores numéricos na string de inicialização, que, após serem convertidos para os tipos de dados correspondentes, serão atribuídos às principais propriedades do objeto. Além disso, ao ser criado, o estado do objeto será definido como normal (ou seja, os limites não foram excedidos). Se o objeto for recriado após a reinicialização do EA no meio do dia, ao carregar as informações salvas, o estado deverá ser restaurado para o que era no momento do último salvamento. O mesmo se aplica à definição da parte do saldo da conta alocada para negociação — o valor definido no construtor pode ser redefinido ao carregar as informações salvas sobre o gerente de risco.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);

// Set the state: Limits are not exceeded
   m_state = RM_STATE_OK;

// Remember the share of the account balance allocated for trading
   m_prevDepoPart = CMoney::DepoPart();

// Update base daily levels
   UpdateBaseLevels();

// Adjust the base balance if it is not set
   if(m_baseBalance == 0) {
      m_baseBalance = m_balance;
   }
}


A principal função do gerente de risco será executada a cada tick no manipulador desse evento. Isso incluirá verificar a atividade do gerente de risco e, se estiver ativo, atualizar os valores atuais de lucro e os níveis diários básicos, se necessário, além de verificar se os limites de perda permitidos foram excedidos:

//+------------------------------------------------------------------+
//| Tick processing in the risk manager                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::Tick() {
// If the risk manager is inactive, exit
   if(!m_isActive) {
      return;
   }

// Update the current profit values
   UpdateProfit();

// If a new daily period has begun, then we update the base daily levels
   if(IsNewBar(Symbol(), PERIOD_D1)) {
      UpdateBaseLevels();
   }

// Check for exceeding loss limits
   CheckLimits();
}


Destacamos um ponto importante. Graças ao esquema desenvolvido com o uso de posições virtuais, que o destinatário dos volumes de negociação transforma em posições reais de mercado, e ao módulo de gerenciamento de capital, que permite definir o coeficiente de escalabilidade necessário entre os tamanhos das posições virtuais e reais, podemos implementar facilmente o fechamento seguro das posições de mercado, sem prejudicar a lógica de negociação das estratégias ativas. Para isso, basta definir o coeficiente de escalabilidade no módulo de gerenciamento de capital como zero:

CMoney::DepoPart(0);               // Set the used portion of the total balance to 0


Se antes disso tivermos salvo o coeficiente anterior na propriedade m_prevDepoPart, então, após o início de um novo dia e a atualização do limite diário, poderemos restaurar as posições reais fechadas anteriormente, simplesmente retornando esse coeficiente ao valor anterior: 

CMoney::DepoPart(m_prevDepoPart);  // Return the used portion of the total balance


Entretanto, claro, não podemos saber com antecedência se as posições serão reabertas a um preço melhor ou pior. Entretanto, podemos ter certeza de que a adição do gerente de risco não afetou o funcionamento de todas as instâncias das estratégias de trading.

Agora vamos examinar os outros métodos da classe de gerenciamento de risco.

No método UpdateProfits(), atualizamos os valores atuais de saldo, fundos e lucros, além de calcular o lucro diário como a diferença entre os fundos atuais e o nível diário. Vale destacar que esse valor nem sempre coincidirá com o lucro atual. A diferença aparecerá se algumas negociações já tiverem sido fechadas desde o início do novo período diário. Calculamos a perda total como a diferença entre os fundos atuais e o saldo base.

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   m_profit = m_equity - m_balance;
   m_dailyProfit = m_equity - m_baseDailyLevel;
   m_overallProfit = m_equity - m_baseBalance;
   m_virtualProfit = VirtualProfit();

   if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) {
      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
   }
}

Além disso, neste método, calculamos o chamado lucro virtual atual. Ele é calculado com base nas posições virtuais abertas. Se deixarmos as posições virtuais abertas ao atingir os limites do gerente de risco, mesmo na ausência de posições reais abertas, podemos, a qualquer momento, estimar qual seria o lucro aproximado, caso as posições reais fechadas pelo gerente de risco tivessem permanecido abertas. Infelizmente, essa característica de cálculo não é totalmente precisa (com um erro de alguns por cento). No entanto, mesmo essa informação é muito melhor do que sua ausência.

O cálculo do lucro virtual atual é realizado pelo método VirtualProfit(). Nele, obtemos um ponteiro para o objeto receptor de volumes virtuais, pois precisamos saber o número total de posições virtuais e ter a possibilidade de acessar cada posição virtual. Depois disso, percorremos todas as posições virtuais em um loop e solicitamos ao nosso módulo de gerenciamento de capital que calcule o lucro virtual de cada posição, levando em consideração a escalabilidade para o tamanho atual dos fundos disponíveis para negociação:

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
   // Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
   
   double profit = 0;
   
   // Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
   
   return profit;
}

Neste método, usamos o novo macro FORI, que será discutido mais adiante.

Quando um novo período diário começa, recalculamos o saldo base diário, os fundos e o nível. Também verificamos que, se no dia anterior o limite diário de perda foi atingido, devemos retomar a negociação e reabrir as posições reais consoante as posições virtuais abertas. Esse processo será tratado pelo método UpdateBaseLevels():

//+------------------------------------------------------------------+
//| Update daily base levels                                         |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Update balance, funds and base daily level
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",
               m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel);

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Restore the status to normal:
      CMoney::DepoPart(m_prevDepoPart);         // Return the used portion of the total balance
      m_state = RM_STATE_OK;                    // Set the risk manager to normal
      CVirtualReceiver::Instance().Changed();   // Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f",
                  m_prevDepoPart);
   }
}


Para o cálculo das perdas máximas segundo os métodos especificados nos parâmetros, teremos dois métodos: DailyLoss() e OverallLoss(). Suas implementações são bastante semelhantes, com a única diferença sendo qual parâmetro numérico e método são usados para o cálculo:

//+------------------------------------------------------------------+
//| Maximum daily loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::DailyLoss() {
   if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxDailyLossLimit;
   } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxDailyLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxDailyLossLimit / 100;
   }
}

//+------------------------------------------------------------------+
//| Maximum total loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallLoss() {
   if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxOverallLossLimit;
   } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxOverallLossLimit / 100;
   }
}


O método de verificação de limites CheckLimits() simplesmente chamará dois métodos auxiliares para verificar as perdas diárias e totais:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   CheckDailyLimit();      // Check daily limit
   CheckOverallLimit();    // Check total limit
}


O método de verificação de perda diária usa o método DailyLoss() para obter o limite máximo permitido de perda diária e o compara com o lucro diário atual. Se o limite for excedido, o gerente de risco é colocado no estado de "Limite diário excedido", e o fechamento das posições abertas é iniciado ao definir o tamanho do saldo de negociação usado para zero:

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckDailyLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) {
   // Switch the risk manager to the achieved daily loss state:
      m_prevDepoPart = CMoney::DepoPart();   // Save the previous value of the used part of the total balance
      CMoney::DepoPart(0);                   // Set the used portion of the total balance to 0
      m_state = RM_STATE_DAILY_LOSS;         // Set the risk manager to the achieved daily loss state
      CVirtualReceiver::Instance().Changed();// Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f",
                  CMoney::DepoPart());
   }
}


O método de verificação de perda total funciona de forma semelhante, exceto que compara o lucro total com a perda total permitida. Ao exceder o limite total, o gerente de risco é colocado no estado de "Limite total excedido".

Salvamos o código gerado no arquivo VirtualRiskManager.mqh na pasta atual.

Agora, vamos analisar as mudanças e adições que precisaremos fazer nos arquivos do projeto previamente criados para permitir o uso da nova classe de gerenciamento de risco.


Macros úteis

Adicionamos um novo macro FORI(N, D), que realiza um ciclo com a variável i, executando a expressão N vezes a expressão D:

// 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 FORI(N, D)      { for(int i=0; i<N;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

Salvamos essas alterações no arquivo Macros.mqh na pasta atual.


Classe de gerenciamento de capital СMoney

Nesta classe, adicionaremos um método para calcular o lucro de uma posição virtual, levando em consideração o coeficiente de escalabilidade do volume dela. Essa operação já foi realizada no método Volume() para determinar o tamanho estimado da posição virtual: com base nas informações sobre o tamanho disponível do saldo atual para negociação e o saldo correspondente ao volume da posição virtual, encontramos o coeficiente de escalabilidade, que é igual à relação desses saldos. Esse coeficiente é então multiplicado pelo volume da posição virtual para obter o volume estimado, ou seja, aquele que será aberto na conta de negociação.

Portanto, primeiro extraímos do método Volume() a parte do código que encontra o coeficiente de escalabilidade em um método separado Coeff():

//+------------------------------------------------------------------+
//| Calculate the virtual position volume scaling factor             |
//+------------------------------------------------------------------+
double CMoney::Coeff(CVirtualOrder *p_order) {
   // Request the normalized strategy balance for the virtual position
   double fittedBalance = p_order.FittedBalance();

   // If it is 0, then the scaling factor is 1
   if(fittedBalance == 0.0) {
      return 1;
   }

   // Otherwise, find the value of the total balance for trading
   double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE);

   // Return the volume scaling factor
   return totalBalance * s_depoPart / fittedBalance;
}


Após isso, as implementações dos métodos Volume() e Profit() se tornam muito semelhantes: pegamos a quantidade necessária (volume ou lucro) da posição virtual e a multiplicamos pelo coeficiente de escalabilidade obtido:

//+------------------------------------------------------------------+
//| Determine the calculated size of the virtual position            |
//+------------------------------------------------------------------+
double CMoney::Volume(CVirtualOrder *p_order) {
   return p_order.Volume() * Coeff(p_order);
}

//+------------------------------------------------------------------+
//| Determining the calculated profit of a virtual position          |
//+------------------------------------------------------------------+
double CMoney::Profit(CVirtualOrder *p_order) {
   return p_order.Profit() * Coeff(p_order);
}


Claro, precisamos adicionar novos métodos na descrição da classe:

//+------------------------------------------------------------------+
//| Basic money management class                                     |
//+------------------------------------------------------------------+
class CMoney {
   ...
   
   // Calculate the scaling factor of the virtual position volume
   static double     Coeff(CVirtualOrder *p_order);

public:
   CMoney() = delete;                  // Disable the constructor
   
   // Determine the calculated size of the virtual position
   static double     Volume(CVirtualOrder *p_order);
   
   // Determine the calculated profit of a virtual position  
   static double     Profit(CVirtualOrder *p_order);  

   ...
};

Salvamos as alterações feitas no arquivo Money.mqh na pasta atual.


Classe СVirtualFactory

Como a classe de gerenciamento de risco que criamos é herdeira da classe CFactorable, para garantir a possibilidade de sua criação, é necessário expandir o conjunto de objetos criados pela fábrica CVirtualFactory. Adicionamos, dentro do método estático Create() , um bloco de código responsável pela criação do objeto da classe CVirtualRiskManager:

//+------------------------------------------------------------------+
//| 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 == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }
      
      ...

      return object;
   }
};

Salvamos o código gerado no arquivo VirtualFactory.mqh na pasta atual.


Classe CVirtualAdvisor

Alterações mais significativas precisarão ser feitas na classe do Expert CVirtualAdvisor. Como decidimos que o objeto do gerente de risco será usado apenas dentro dessa classe, adicionamos a propriedade correspondente na descrição da classe:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver     *m_receiver;      // Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     // Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   // Risk manager object

   ...
};


Também acordamos que a linha de inicialização do gerente de risco será incorporada na linha de inicialização do Expert imediatamente após a linha de inicialização do grupo de estratégias. Então, adicionamos no construtor a leitura dessa linha de inicialização na variável riskManagerParams, e a subsequente criação do gerente de risco a partir dela:

//+------------------------------------------------------------------+
//| 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 initialization string of the risk manager object
   string riskManagerParams = 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 the risk manager object 
      m_riskManager = NEW(riskManagerParams);
   }
}


Já que criamos o objeto no construtor, também precisamos garantir sua remoção no destrutor:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   if(!!m_interface)    delete m_interface;     // Remove the interface
   if(!!m_riskManager)  delete m_riskManager;   // Remove risk manager
   DestroyNewBar();           // Remove the new bar tracking objects 
}


E o mais importante — a chamada do manipulador Tick() para o gerente de risco no manipulador correspondente do Expert. Observe que o manipulador do gerente de risco é iniciado antes da correção dos volumes de mercado, para que, se for detectado um limite de perda excedido ou, ao contrário, um limite for atualizado, o processador possa ajustar os volumes de posições de mercado abertas durante o mesmo tick:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Risk manager handles virtual positions
   m_riskManager.Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

Salvamos as alterações feitas no arquivo VirtualAdvisor.mqh na pasta atual.


Expert SimpleVolumesExpertSingle

Para testar o gerente de risco, resta apenas adicionar a possibilidade de especificar seus parâmetros no Expert e formar a linha de inicialização necessária. Vamos colocar, por enquanto, todos os seis parâmetros do gerente de risco em variáveis de entrada separadas do Expert:

input group "===  Risk management"
input bool        rmIsActive_             = true;
input double      rmStartBaseBalance_     = 10000;
input ENUM_RM_CALC_LIMIT 
                  rmCalcDailyLossLimit_   = RM_CALC_LIMIT_FIXED;
input double      rmMaxDailyLossLimit_    = 200;
input ENUM_RM_CALC_LIMIT 
                  rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED;
input double      rmMaxOverallLossLimit_  = 500;


Na função OnInit(), precisamos adicionar a criação da linha de inicialização do gerente de risco e integrá-la na linha de inicialização do Expert. Aproveitamos para reescrever um pouco o código de criação das linhas de inicialização para a estratégia e o grupo, que inclui apenas essa estratégia, separando as linhas de inicialização dos objetos individuais em diferentes variáveis:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(1.0);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                              Symbol(), Period(),
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_
                           );

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 rmIsActive_, rmStartBaseBalance_,
                                 rmCalcDailyLossLimit_, rmMaxDailyLossLimit_,
                                 rmCalcOverallLossLimit_, rmMaxOverallLossLimit_
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            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);
}

Salvamos o código gerado no arquivo SimpleVolumesExpertSingle.mq5 na pasta atual. Agora tudo está pronto para testar o funcionamento do gerente de risco.


Testando o funcionamento

Usaremos os parâmetros de uma das instâncias de estratégias de trading, obtidos no processo de otimização em etapas anteriores do desenvolvimento. Chamaremos essa instância de estratégia de trading de estratégia modelo. Os parâmetros da estratégia modelo são mostrados na Fig. 1.

Fig. 1. Parâmetros da estratégia modelo


Executamos um teste único com esses parâmetros e com o gerente de risco desativado no período de 2021-2022. Os resultados obtidos foram os seguintes:

Fig. 2. Resultados da estratégia modelo sem gerente de risco


O gráfico mostra que, no período selecionado, houve algumas quedas significativas no patrimônio. As maiores ocorreram no final de outubro de 2021 (~$380) e em junho de 2022 (~$840).

Agora, ativaremos o gerente de risco e configuraremos um limite máximo de perda diária de $150 e uma perda total máxima de $450. Os resultados obtidos foram os seguintes:


Fig. 3. Resultados da estratégia modelo com gerente de risco (máx. perdas: $150 e $450)


O gráfico mostra que, em outubro de 2021, o gerente de risco fechou posições de mercado com prejuízo duas vezes, mas as posições virtuais permaneceram abertas. Portanto, no dia seguinte, as posições de mercado foram reabertas. Infelizmente, a reabertura ocorreu a um preço menos favorável, então a queda total no saldo e patrimônio foi ligeiramente maior do que a queda no patrimônio com o gerente de risco desativado. Também é possível ver que, após o fechamento das posições pela estratégia, em vez de obter um pequeno lucro (como no caso sem o gerente de risco), foi registrado um pequeno prejuízo.

Em junho de 2022, o gerente de risco foi acionado sete vezes, fechando posições de mercado ao atingir a perda diária de $150. Mais uma vez, a reabertura ocorreu a preços menos favoráveis, e o resultado dessa série de negociações foi uma perda. No entanto, se esse Expert estivesse operando em uma conta demo de uma empresa de prop trading com esses parâmetros de perda diária e total máxima permitida, sem o gerente de risco, a conta teria sido parada por violar as regras de negociação, enquanto com o gerente de risco a conta teria continuado, obtendo um lucro final ligeiramente menor.

Embora tenhamos configurado o valor da perda total para $450, e em junho a perda acumulada no saldo tenha excedido $1000, o limite de perda total não foi atingido, pois ele é calculado a partir do saldo base. Ou seja, ele é atingido se os fundos caírem abaixo de (10000 - 450) = $9550. No entanto, devido aos lucros acumulados anteriormente, o valor dos fundos naquele período não caiu abaixo de $10000. Por isso, o Expert continuou operando, com a abertura de posições de mercado.

Agora, vamos simular a ativação do limite total de perda. Para isso, aumentaremos o coeficiente de escalabilidade dos tamanhos das posições, de modo que, em outubro de 2021, a perda total máxima ainda não tenha sido atingida, mas em junho de 2022 ela seja excedida. Vamos definir o valor do parâmetro scale_ em 50 e observar o resultado:

Fig. 4. Resultados da estratégia modelo com o gerente de risco (máx. perdas: $150 e $450), scale_ = 50


Como pode ser visto, em junho de 2022 a negociação termina — no período subsequente, o Expert não abriu nenhuma posição. Isso aconteceu devido ao acionamento do limite total de perda ($9550).  Também podemos notar que a perda diária agora foi atingida com mais frequência, ocorrendo não apenas em outubro de 2021, mas também em vários outros pontos ao longo da escala temporal.

Portanto, ambos os nossos limitadores estão funcionando corretamente.

O gerente de risco desenvolvido pode ser útil mesmo sem o uso de contas de prop trading. Como ilustração, tentemos otimizar os parâmetros do gerente de risco da nossa estratégia modelo, experimentando aumentar os tamanhos das posições abertas, mas sem ultrapassar o limite de rebaixamento permitido de 10%. Para isso, configuraremos no gerente de risco a perda total máxima igual a 10% do nível diário. A perda diária máxima, também calculada como uma porcentagem do nível diário, será ajustada durante o processo de otimização.


Fig. 5. Resultados da otimização da estratégia modelo com o gerente de risco


Os resultados obtidos mostram que o lucro normalizado em um ano com o uso do gerente de risco aumentou quase uma vez e meia: de $1560 para $2276 (esse resultado está na segunda coluna, “Result”). Aqui está o melhor teste isolado:

Fig. 6. Resultados da estratégia modelo com o gerente de risco (máx. perdas: 7,6% e 10%, scale_ = 88)


Observe que, ao longo de todo o período de teste, o Expert continuou abrindo negociações. Isso significa que o limite total de 10% não foi excedido em nenhum momento. É claro que aplicar o gerente de risco a instâncias individuais de estratégias de trading não faz muito sentido, pois não planejamos executá-las isoladamente em uma conta real. No entanto, o que funciona para uma instância também deve funcionar para um Expert com várias instâncias. Portanto, mesmo esses resultados preliminares indicam que o gerente de risco pode definitivamente ser benéfico.


    Considerações finais

    Agora temos uma implementação básica de um gerente de risco para trading, que permite manter os níveis predefinidos de perdas diárias e totais. Ainda não foi implementado o suporte para salvar e carregar o estado ao reiniciar o Expert, por isso não é aconselhável usá-lo em uma conta real por enquanto. No entanto, essa melhoria não representa grandes dificuldades, e voltaremos a ela mais tarde.

    Além disso, será possível tentar adicionar a capacidade de restringir o trading em diferentes períodos, desde a exclusão de operações em horários específicos de certos dias da semana, até a proibição de abertura de novas posições em momentos próximos ao lançamento de importantes notícias econômicas. Outras possíveis direções de desenvolvimento do gerente de risco incluem a modificação gradual dos tamanhos das posições (por exemplo, reduzir pela metade ao exceder metade do limite) e a recuperação mais "inteligente" dos volumes (como, por exemplo, apenas quando a perda ultrapassar o nível no qual os tamanhos das posições foram reduzidos).

    No entanto, vamos deixar isso para um momento posterior e agora voltar à automação adicional do processo de otimização do Expert em desenvolvimento. A primeira etapa já foi implementada no artigo anterior, e agora é hora de passar para a segunda etapa.

    Obrigado pela atenção e até a próxima!


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

    Arquivos anexados |
    Macros.mqh (2.4 KB)
    Money.mqh (6.52 KB)
    VirtualFactory.mqh (4.46 KB)
    VirtualAdvisor.mqh (23.02 KB)
    Desenvolvimento de robô em Python e MQL5 (Parte 2): Escolha do modelo, criação e treinamento, testador customizado Python Desenvolvimento de robô em Python e MQL5 (Parte 2): Escolha do modelo, criação e treinamento, testador customizado Python
    Continuamos o ciclo de artigos sobre a criação de um robô de trading em Python e MQL5. Hoje, vamos resolver a tarefa de escolher e treinar o modelo, testá-lo, implementar a validação cruzada, busca em grade, além de abordar o ensemble de modelos.
    Algoritmo de Fechadura Codificada (Code Lock Algorithm, CLA) Algoritmo de Fechadura Codificada (Code Lock Algorithm, CLA)
    Neste artigo, vamos repensar as fechaduras codificadas, transformando-as de mecanismos de proteção em ferramentas para resolver tarefas complexas de otimização. Descubra o mundo das fechaduras codificadas, não como simples dispositivos de segurança, mas como inspiração para uma nova abordagem à otimização. Vamos criar uma população inteira de "fechaduras", onde cada uma representa uma solução única para um problema. Em seguida, desenvolveremos um algoritmo que "destrancará" essas fechaduras e encontrará soluções ideais em várias áreas, desde o aprendizado de máquina até o desenvolvimento de sistemas de trading.
    Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
    Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
    Redes neurais de maneira fácil (Parte 91): previsão na área de frequência (FreDF) Redes neurais de maneira fácil (Parte 91): previsão na área de frequência (FreDF)
    Continuamos a explorar a análise e previsão de séries temporais na área de frequência. E nesta matéria, apresentaremos um novo método de previsão nessa área, que pode ser adicionado a muitos dos algoritmos que já estudamos anteriormente.