English Русский 中文 Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco

Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco

MetaTrader 5Negociação |
259 1
Yuriy Bykov
Yuriy Bykov

Introdução

Em um dos artigos anteriores da série, abordamos o controle de riscos e desenvolvemos uma classe de gerenciador de risco com funcionalidade básica. Essa classe permitia definir um limite máximo de perda diária e um limite geral de perdas, ao serem atingidos, o trading era interrompido e todas as posições abertas eram encerradas. Se o limite diário de perdas fosse atingido, as operações eram retomadas no dia seguinte; já ao atingir o limite geral, não havia retomada das operações.

Recordemos que consideramos algumas direções para evoluir o gerenciador de risco, como alterações mais graduais no tamanho das posições (por exemplo, reduzir pela metade ao ultrapassar metade do limite), e uma recuperação "mais inteligente" dos volumes (por exemplo, apenas ao ultrapassar o nível de perda onde a redução das posições ocorreu). Também é possível adicionar um parâmetro de lucro-alvo máximo, de modo que, ao atingi-lo, as operações também seriam interrompidas. Embora este parâmetro seja menos útil para contas pessoais, ele é bastante requisitado em contas de empresas de prop trading, pois frequentemente, ao alcançar o nível planejado de lucro, as operações só podem ser continuadas em outra conta.

A introdução de restrições de horário para negociações também foi mencionada como uma possível direção de desenvolvimento do gerenciador de risco, mas nesta etapa, deixaremos essa questão para o futuro. Por ora, tentaremos implementar a alteração adaptativa dos volumes no gerenciador de risco e verificar se ela traz algum benefício.


Caso básico

Utilizaremos o EA da publicação anterior e adicionaremos a ele a capacidade de gerenciar os parâmetros do gerenciador de risco. Também faremos outras pequenas melhorias, que serão discutidas adiante. Primeiramente, definiremos os parâmetros do EA que usaremos para avaliar os resultados das alterações realizadas.

Em primeiro lugar, fixaremos a composição específica dos exemplos individuais de estratégias de trading que serão utilizados no EA de teste. No parâmetro passes_, especificaremos os identificadores dos melhores testes após a segunda etapa de otimização para cada um dos três símbolos otimizáveis e para cada um dos três timeframes (um total de 9 identificadores). Cada identificador representará um grupo normalizado de 16 estratégias de trading. Assim, o grupo final conterá 144 estratégias de trading, divididas em nove grupos de 16. Este grupo final não será normalizado, pois não definimos um fator de normalização para ele.

Em segundo lugar, utilizaremos um saldo fixo de $10.000 para as negociações e nossa expectativa padrão de retração máxima de 10% para o coeficiente de escalonamento igual a 1. Tentaremos variar esse coeficiente entre 1 e 10. À medida que o coeficiente aumenta, a retração máxima permitida também cresce, mas ela será controlada adicionalmente pelo nosso gerenciador de risco, que garantirá que não ultrapasse 10%.

Para isso, ativaremos o gerenciador de risco e configuraremos o valor da perda máxima geral em 10% do saldo base de $10.000, ou seja, $1.000. Para a perda máxima diária, definiremos um valor duas vezes menor, ou seja, $500. Com o aumento do saldo, esses dois parâmetros permanecerão inalterados.

Definiremos todos os valores dos parâmetros de entrada conforme descrito acima:

Fig. 1. Parâmetros de entrada para o EA de teste com o gerenciador de risco inicial.

Iniciaremos a otimização para o intervalo de 2021 e 2022, para observar o desempenho do EA com diferentes valores do multiplicador de escala para os tamanhos das posições (scale_). Obtivemos os seguintes resultados:

Fig. 2. Resultados da otimização do parâmetro scale_ no EA de teste com o gerenciador de risco inicial.

Os resultados estão ordenados em ordem crescente do parâmetro scale_, ou seja, quanto mais abaixo na tabela, maiores os tamanhos das posições utilizadas pelo EA. É evidente que, a partir de um determinado valor crítico, o resultado final é uma perda ligeiramente superior a $1.000.

Durante o intervalo de teste, ocorreram diversas retrações de profundidades diferentes. Nos casos em que nenhuma retração reduziu os fundos abaixo de $9.000, as operações continuaram até o final do intervalo. Contudo, nos casos em que os fundos caíram abaixo de $9.000 durante uma retração, as operações foram interrompidas naquele momento e não foram retomadas. Nessas passagens, observamos uma perda em torno de $1.000. A leve superação desse valor calculado pode ser explicada pelo fato de utilizarmos o EA operando apenas em novos candles de um minuto. Assim, os preços puderam variar ligeiramente entre o momento em que a perda atingiu o valor configurado e o momento em que o gerenciador de risco verificou e tomou a decisão de fechar todas as posições.

Essas diferenças são insignificantes na maioria dos casos e podem ser desconsideradas, desde que ajustemos os limites dos parâmetros ou alteremos, como planejado, o modo de operação do gerenciador de risco. A única exceção preocupante é o caso de scale_ = 5.5, onde, após o fechamento de todas as posições, a perda excedeu o valor calculado em mais de 20%, totalizando aproximadamente $1.234.

Para análise adicional, examinemos o gráfico das curvas de saldo e fundos para o primeiro caso (scale_ = 1.0). Como os demais casos diferem apenas nos tamanhos das posições abertas, seus gráficos de saldo e fundos terão o mesmo formato do primeiro, mas estarão mais estendidos verticalmente.

Fig. 3. Resultados do caso com o parâmetro scale_ = 1.0 no EA de teste com o gerenciador de risco inicial.

Vamos compará-los com os resultados sem o gerenciador de risco:

Fig. 4. Resultados do caso com o parâmetro scale_ = 1.0 no EA de teste sem o gerenciador de risco.

Sem o gerenciador de risco, o lucro total foi alguns pontos percentuais maior, enquanto a retração máxima dos fundos permaneceu a mesma. Isso indica que a normalização das estratégias ao agrupá-las e sua operação conjunta apresentam bons resultados: o gerenciador de risco precisou fechar posições apenas cerca de três vezes em dois anos devido ao limite diário de perdas.

Agora, analisemos os resultados do caso com o gerenciador de risco e o valor do parâmetro scale_ = 3. O lucro foi aproximadamente 50% maior, mas a retração aumentou três vezes.

Fig. 5. Resultados do teste com o parâmetro scale_ = 3.0 no EA de teste com o gerenciador de risco original

No entanto, apesar do aumento da retração em termos absolutos para quase $3.000, o gerenciador de risco garantiu que a retração diária não ultrapassasse $500. Isso significa que houve vários dias consecutivos nos quais o gerenciador de risco encerrou todas as posições, reabrindo-as no início do dia seguinte. Dessa forma, mesmo com o saldo reduzido para $3.000 a partir do pico anterior, a retração diária em relação ao saldo ou aos fundos no início do dia não ultrapassou $500. Ainda assim, usar tamanhos de posição tão elevados é perigoso devido à limitação de perdas totais. Neste caso, tivemos sorte de a grande retração ter ocorrido algum tempo após o início do período de teste. O saldo cresceu, aumentando o valor da perda máxima geral permitida, inicialmente definida como $1.000, calculada a partir do saldo inicial. Se o período de teste tivesse começado imediatamente antes do momento em que a retração alcançou $3.000, o limite geral teria sido excedido e as operações seriam interrompidas.

Para considerar o impacto de um possível momento de início desfavorável, ajustaremos os parâmetros do gerenciador de risco para que o nível de perdas gerais seja calculado com base no saldo ou nos fundos do último pico, e não no saldo inicial. Contudo, para implementar essa alteração, será necessário adicionar essa funcionalidade ao código do gerenciador de risco, pois atualmente essa configuração ainda não está implementada.


Atualização do CVirtualRiskManager

Planejamos realizar várias mudanças no gerenciador de risco, muitas das quais exigem alterações nos mesmos métodos. Isso dificulta a descrição detalhada, pois as modificações relacionadas a diferentes funcionalidades são interdependentes. Por isso, apresentaremos a versão final do código, começando pela parte mais simples.


Atualização de enumerações

Antes da definição da classe, algumas enumerações haviam sido declaradas e utilizadas posteriormente. Vamos expandi-las. Por exemplo, no ENUM_RM_STATE, que contém os estados possíveis do gerenciador de risco, serão adicionados dois novos estados:

  • RM_STATE_RESTORE: estado que ocorre após o início de um novo período diário, até que os tamanhos das posições abertas sejam totalmente restaurados. Esse estado não existia anteriormente, pois os tamanhos das posições eram restaurados imediatamente após o início de um novo dia. Agora, permitimos que isso ocorra somente após os preços retornarem a níveis mais favoráveis para abertura de posições. Detalharemos isso posteriormente.

  • RM_STATE_OVERALL_PROFIT — estado que ocorre após o alcance do lucro especificado. Após esse evento, a negociação é interrompida.

A enumeração anterior, ENUM_RM_CALC_LIMIT, foi desmembrada em três enumerações separadas: para perdas diárias, perdas gerais e lucros gerais. Os valores dessas enumerações serão utilizados para determinar duas questões:

  • como interpretar o número fornecido nos parâmetros: como um valor absoluto ou relativo, expresso em porcentagem em relação ao nível diário ou aos valores máximos de saldo ou fundos;
  • a partir de qual valor será calculado o nível limite — o nível diário ou os valores máximos de saldo ou fundos.

Essas opções estão especificadas nos comentários dos valores das enumerações.

// 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_RESTORE,       // Recovery after daily limit
   RM_STATE_OVERALL_LOSS,  // Overall limit exceeded
   RM_STATE_OVERALL_PROFIT // Overall profit reached
};

// Possible methods for calculating limits
enum ENUM_RM_CALC_DAILY_LOSS {
   RM_CALC_DAILY_LOSS_MONEY_BB,    // [$] to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_BB,  // [%] from Base Balance to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_DL   // [%] from/to Daily Level
};

// Possible methods for calculating general limits
enum ENUM_RM_CALC_OVERALL_LOSS {
   RM_CALC_OVERALL_LOSS_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_BAL,       // [$] to HW Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL,    // [$] to HW Equity or Balance
   RM_CALC_OVERALL_LOSS_PERCENT_BB,         // [%] from/to Base Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL,     // [%] from/to HW Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL   // [%] from/to HW Equity or Balance
};

// Possible methods for calculating overall profit
enum ENUM_RM_CALC_OVERALL_PROFIT {
   RM_CALC_OVERALL_PROFIT_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_PROFIT_PERCENT_BB,         // [%] from/to Base Balance
};


Descrição da Classe

Adicionamos novas propriedades e métodos à seção protegida da descrição da classe CVirtualRiskManager. A seção pública permanece inalterada. Os itens adicionados são destacados em verde:

//+------------------------------------------------------------------+
//| 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_DAILY_LOSS   m_calcDailyLossLimit; // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;          // Parameter of calculating the maximum daily loss
   double            m_closeDailyPart;             // Threshold part of the daily loss

   ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit;  // Method of calculating the maximum overall loss
   double            m_maxOverallLossLimit;           // Parameter of calculating the maximum overall loss
   double            m_closeOverallPart;              // Threshold part of the overall loss

   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit
   double            m_maxOverallProfitLimit;            // Parameter for calculating the maximum overall profit

   double            m_maxRestoreTime;             // Waiting time for the best entry on a drawdown
   double            m_lastVirtualProfitFactor;    // Initial best drawdown multiplier


// Current state
   ENUM_RM_STATE     m_state;                // State
   double            m_lastVirtualProfit;    // Profit of open virtual positions at the moment of loss limit 
   datetime          m_startRestoreTime;     // Start time of restoring the size of open positions

// 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;        // Overall profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_baseHWBalance;        // balance High Watermark
   double            m_baseHWEquityBalance;  // equity or balance High Watermark
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_baseDepoPart;         // Used (original) part of the overall balance
   double            m_dailyDepoPart;        // Multiplier of the used part of the overall balance by daily loss
   double            m_overallDepoPart;      // Multiplier of the used part of the overall balance by overall loss

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

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

   void              CheckLimits();          // Check for excess of permissible losses
   bool              CheckDailyLossLimit();     // Check for excess of the permissible daily loss
   bool              CheckOverallLossLimit();   // Check for excess of the permissible overall loss
   bool              CheckOverallProfitLimit(); // Check if the specified profit has been achieved

   void              CheckRestore();         // Check the need for restoring the size of open positions
   bool              CheckDailyRestore();       // Check if the daily multiplier needs to be restored
   bool              CheckOverallRestore();     // Check if the overall multiplier needs to be restored

   double            VirtualProfit();        // Determine the profit of open virtual positions
   double            RestoreVirtualProfit(); // Determine the profit of open virtual positions to restore

   void              SetDepoPart();          // Set the values of the used part of the overall balance

public:
   ...
};

As propriedades m_closeDailyPart e m_closeOverallPart permitem realizar ajustes graduais nos tamanhos das posições. O uso de ambas é similar, diferenciando-se apenas pelo limite a que cada propriedade está associada (diário ou geral). Por exemplo, se definirmos m_closeDailyPart = 0.5, ao atingir metade do limite diário de perdas, os tamanhos das posições serão reduzidos pela metade. Se as perdas continuarem crescendo e atingirem metade da parte restante do limite diário, os tamanhos das posições (já reduzidos pela metade anteriormente) serão novamente reduzidos pela metade.

A redução dos tamanhos das posições será realizada por meio da modificação das propriedades m_dailyDepoPart e m_overallDepoPart. Esses valores são utilizados no método de configuração da parte do saldo geral destinada às negociações. Eles entram na fórmula como multiplicadores; portanto, reduzir qualquer um deles pela metade resultará na redução do volume total em 50%.

//+------------------------------------------------------------------+
//| Set the value of the used part of the overall balance            |
//+------------------------------------------------------------------+
void CVirtualRiskManager::SetDepoPart() {
   CMoney::DepoPart(m_baseDepoPart * m_dailyDepoPart * m_overallDepoPart);
}

A propriedade m_baseDepoPart, usada nessa função, armazena o valor inicial da parte do saldo utilizada para negociações.

As propriedades m_maxRestoreTime e m_lastVirtualProfitFactor são utilizadas para determinar a possibilidade de restauração dos tamanhos das posições abertas.

A primeira propriedade define o tempo, em minutos, após o qual os tamanhos das posições serão restaurados, mesmo que o lucro virtual não seja negativo. Ou seja, após esse período, o EA abrirá novamente posições reais de mercado correspondentes às posições virtuais, mesmo que os preços tenham se movido favoravelmente e o lucro virtual tenha aumentado durante o tempo em que as posições reais estavam fechadas. Antes desse período, a restauração dos volumes ocorrerá apenas se o lucro calculado das posições virtuais for inferior a um valor específico, que varia com o tempo.

A segunda propriedade define o multiplicador que determina o quanto a perda das posições virtuais no início de um novo período diário deve ser superior à perda no momento de atingir os limites, para que os tamanhos das posições sejam imediatamente restaurados. Por exemplo, um valor de 1 para este parâmetro significa que a restauração dos tamanhos no início do dia ocorrerá apenas se as posições virtuais permanecerem com o mesmo nível de perda ou uma perda ainda maior em comparação com o momento em que os limites foram atingidos.

Vale destacar que agora o momento de atingir o limite não é apenas quando a retração excede o limite configurado, mas também, por exemplo, ao alcançar metade do limite diário, caso o parâmetro m_closeDailyPart seja igual a 0.5.


Cálculo do lucro das posições virtuais

Ao utilizar o fechamento parcial e a posterior restauração dos tamanhos das posições abertas, é necessário determinar corretamente qual seria o lucro ou a perda flutuante no momento atual, caso todas as posições abertas pela estratégia continuassem abertas. Por isso, foi adicionado ao gerenciador de risco um método para calcular o lucro atual das posições virtuais abertas. Essas posições virtuais não são fechadas quando os limites de retração são atingidos, o que nos permite calcular, a qualquer momento, o lucro aproximado que as posições reais abertas, correspondentes às virtuais, teriam. Este valor calculado não reflete exatamente a realidade devido à ausência de taxas e swaps no cálculo, mas para nossos objetivos, essa precisão é suficiente.

Anteriormente, esse método não era necessário para cálculos que influenciassem diretamente as ações subsequentes. Agora, ele é indispensável, e para calcular corretamente o lucro das posições virtuais, precisamos fazer pequenos ajustes no método. Isso ocorre porque o método usado para determinar o lucro de uma posição virtual, CMoney::Profit(), utiliza, em seu cálculo, o valor do multiplicador do saldo utilizado, CMoney::DepoPart(). Caso esse multiplicador tenha sido reduzido, o cálculo do lucro virtual será baseado nos tamanhos reduzidos das posições, e não nos tamanhos originais.

Nosso interesse, no entanto, está no lucro correspondente aos tamanhos originais das posições. Por isso, antes do cálculo, restauramos temporariamente o multiplicador original do saldo utilizado. Em seguida, calculamos o lucro das posições virtuais e, por fim, redefinimos o multiplicador atual do saldo utilizado, chamando o método SetDepoPart():

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
// Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();

// Set the initial balance usage multiplier
   CMoney::DepoPart(m_baseDepoPart);

   double profit = 0;

// Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));

// Restore the current balance usage multiplier
   SetDepoPart();

   return profit;
}


High Watermark

Para uma análise mais completa, é desejável permitir que o nível máximo de perda seja calculado não apenas a partir do saldo inicial da conta, mas também, por exemplo, a partir do último pico de saldo atingido. Assim, adicionamos as propriedades m_baseHWBalance e m_baseHWEquityBalance aos atributos que precisam ser atualizados constantemente. No método UpdateProfit(), incluímos o cálculo dessas propriedades, garantindo que o lucro geral seja calculado em relação aos valores máximos de saldo ou fundos, e não ao saldo base:

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
// Current equity
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);

// Current balance
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);

// Maximum balance (High Watermark)
   m_baseHWBalance = MathMax(m_balance, m_baseHWBalance);

// Maximum balance or equity (High Watermark)
   m_baseHWEquityBalance = MathMax(m_equity, MathMax(m_balance, m_baseHWEquityBalance));

// Current profit
   m_profit = m_equity - m_balance;

// Current daily profit relative to the daily level
   m_dailyProfit = m_equity - m_baseDailyLevel;

// Current overall profit relative to base balance
   m_overallProfit = m_equity - m_baseBalance;

// If we take the overall profit relative to the highest balance,
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL) {
      // Recalculate it
      m_overallProfit = m_equity - m_baseHWBalance;
   }

// If we take the overall profit relative to the highest balance or equity,
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL) {
      // Recalculate it
      m_overallProfit = m_equity - m_baseHWEquityBalance;
   }

// Current profit of virtual open positions
   m_virtualProfit = VirtualProfit();

   ...
}


Método de verificação de limites

Este método também foi modificado: seu código foi dividido em vários métodos auxiliares, tornando-o mais estruturado e organizado. Agora, ele apresenta o seguinte formato:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   if(false
         || CheckDailyLossLimit()     // Check daily limit
         || CheckOverallLossLimit()   // Check overall limit
         || CheckOverallProfitLimit() // Check overall profit
     ) {
      // Remember the current level of virtual profit
      m_lastVirtualProfit = m_virtualProfit;

      // Notify the recipient about changes
      CVirtualReceiver::Instance().Changed();
   }
}

No método de verificação do limite diário de perdas, começamos verificando se o limite diário, ou a parte dele definida pelo parâmetro m_closeDailyPart, foi atingido. Se sim, reduzimos o multiplicador da parte do saldo geral utilizado para o limite diário de perdas. Caso este multiplicador já tenha se tornado muito pequeno, ele é zerado. Em seguida, configuramos o valor da parte utilizada do saldo geral e colocamos o gerenciador de risco no estado de limite diário de perdas atingido.

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyLossLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() * (1 - m_dailyDepoPart * (1 - m_closeDailyPart))
      && CMoney::DepoPart() > 0) {

      // Reduce the multiplier of the used part of the overall balance by the daily loss
      m_dailyDepoPart *= (1 - m_closeDailyPart);

      // If the multiplier is already too small,
      if(m_dailyDepoPart < 0.05) {
         // Set it to 0
         m_dailyDepoPart = 0;
      }

      // Set the value of the used part of the overall balance
      SetDepoPart();
    
      ... 

      // Set the risk manager to the achieved daily loss state
      m_state = RM_STATE_DAILY_LOSS;

      return true;
   }

   return false;
}

De forma semelhante, funciona o método de verificação do limite geral de perdas. Contudo, o gerenciador de risco só é colocado no estado de limite geral de perdas atingido se o multiplicador da parte utilizada do saldo geral para o limite geral de perdas for reduzido a zero.

//+------------------------------------------------------------------+
//| Check the overall loss limit                                     |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallLossLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit < -OverallLoss() * (1 - m_overallDepoPart * (1 - m_closeOverallPart))
         && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart *= (1 - m_closeOverallPart);

      // If the multiplier is already too small,
      if(m_overallDepoPart < 0.05) {
         // Set it to 0
         m_overallDepoPart = 0;

         // Set the risk manager to the achieved overall loss state
         m_state = RM_STATE_OVERALL_LOSS;
      }

      // Set the value of the used part of the overall balance
      SetDepoPart();
      
      ...

      return true;
   }

   return false;
}

A verificação do alcance do lucro configurado é ainda mais simples: ao atingir o valor configurado, zeramos o multiplicador correspondente e colocamos o gerenciador de risco no estado de lucro geral atingido.

//+------------------------------------------------------------------+
//| Check if the specified profit has been achieved                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit > m_maxOverallProfitLimit && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart = 0;

      // Set the risk manager to the achieved overall profit state
      m_state = RM_STATE_OVERALL_PROFIT;

      // Set the value of the used part of the overall balance
      SetDepoPart();
      
      ... 

      return true;
   }

   return false;
}

O três métodos retornam valores booleanos, indicando se o respectivo limite foi atingido. Caso positivo, no método CheckLimits(), registramos o nível atual de lucro virtual e notificamos o controlador dos volumes das posições de mercado sobre as mudanças nos tamanhos das posições.

No método de atualização dos níveis diários base, adicionamos o recálculo do lucro diário e a transição para o estado de recuperação, caso o limite diário de perdas tenha sido atingido anteriormente:

//+------------------------------------------------------------------+
//| 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);

   m_dailyProfit = m_equity - m_baseDailyLevel;

   ...

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Switch to the state of restoring the sizes of open positions
      m_state = RM_STATE_RESTORE;

      // Remember restoration start time
      m_startRestoreTime = TimeCurrent();
   }
}


Recuperação dos tamanhos das posições

O método que anteriormente realizava essa tarefa também foi dividido em vários métodos. No nível superior, chamamos o método de verificação da necessidade de recuperação:

//+------------------------------------------------------------------+
//| Check the need for restoring the size of open positions          |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckRestore() {
// If we need to restore the state to normal, then
   if(m_state == RM_STATE_RESTORE) {
      // Check the possibility of restoring the daily loss multiplier to normal 
      bool dailyRes = CheckDailyRestore();

      // Check the possibility of restoring the overall loss multiplier to normal
      bool overallRes = CheckOverallRestore();

      // If at least one of them has recovered,
      if(dailyRes || overallRes) {
         
...

         // Set the value of the used part of the overall balance
         SetDepoPart();

         // Notify the recipient about changes
         CVirtualReceiver::Instance().Changed();

         // If both multipliers are restored to normal,
         if(dailyRes && overallRes) {
            // Set normal state
            m_state = RM_STATE_OK;
         }
      }
   }
}

Os métodos auxiliares que verificam a necessidade de recuperação dos multiplicadores diários e gerais atualmente funcionam de forma idêntica, mas futuramente, talvez sejam diferenciados. Por ora, eles verificam se o valor do lucro virtual atual é menor que o nível desejado. Se sim, é vantajoso reabrir as posições reais pelos preços atuais, e, portanto, devemos restaurar seus tamanhos:

//+------------------------------------------------------------------+
//| Check if the daily multiplier needs to be restored               |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyRestore() {
// If the current virtual profit is less than the one desired for recovery,
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Restore the daily loss multiplier
      m_dailyDepoPart = 1.0;
      return true;
   }

   return false;
}

//+------------------------------------------------------------------+
//| Check if the overall multiplier needs to be restored             |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallRestore() {
// If the current virtual profit is less than the one desired for recovery,
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Restore the overall loss multiplier
      m_overallDepoPart = 1.0;
      return true;
   }

   return false;
}

O método RestoreVirtualProfit() calcula o nível desejado de lucro virtual. Utilizamos uma interpolação linear simples com dois parâmetros: quanto mais tempo passa, em níveis menos favoráveis concordamos em reabrir as posições reais.

//+------------------------------------------------------------------+
//| Determine the profit of virtual positions for recovery           |
//+------------------------------------------------------------------+
double CVirtualRiskManager::RestoreVirtualProfit() {
// If the maximum recovery time is not specified,
   if(m_maxRestoreTime == 0) {
      // Return the current value of the virtual profit
      return m_virtualProfit;
   }

// Find the elapsed time since the start of recovery in minutes
   double t = (TimeCurrent() - m_startRestoreTime) / 60.0;

// Return the calculated value of the desired virtual profit
// depending on the time elapsed since the start of recovery
   return m_lastVirtualProfit * m_lastVirtualProfitFactor * (1 - t / m_maxRestoreTime);
}

Salvamos as mudanças realizadas no arquivo VirtualRiskManager.mqh na pasta atual.


Testes

Primeiro, executamos o EA com parâmetros que determinam o fechamento total das posições ao atingir os limites diários e gerais. Os resultados devem ser semelhantes aos apresentados na Fig. 3.

Fig. 6. Resultados do EA com o parâmetro scale_ = 1.0 e configurações do gerenciador de risco original

Os resultados de retração coincidiram exatamente, e os resultados de lucro e lucro médio anual normalizado (OnTester result) foram ligeiramente superiores ao esperado. Isso é encorajador: as alterações realizadas não comprometeram o que já havia sido implementado.

Agora, vamos verificar os resultados com o fechamento adaptativo das posições ao atingir uma parte dos limites diários e gerais, com o valor de 0.5:

Fig. 7. Resultados do EA com os parâmetros scale_ = 1.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5

A retração diminuiu ligeiramente, o que resultou em um pequeno aumento no lucro médio anual normalizado. Por fim, testaremos a recuperação dos tamanhos das posições pelos melhores preços. Configuraremos no parâmetro um tempo de espera de 1440 minutos (1 dia) para a melhor entrada durante a retração e um multiplicador da melhor retração inicial igual a 1.5. Esses valores foram definidos intuitivamente.

Fig. 8. Resultados do EA com os parâmetros scale_ = 1.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

O resultado da lucratividade anual média normalizada melhorou mais alguns pontos percentuais. Parece que os esforços para implementar este mecanismo valeram a pena.

Agora, tentaremos aumentar os tamanhos das posições abertas em três vezes, sem alterar os parâmetros do gerenciador de risco desde a última execução. Isso deve levar a uma ativação mais frequente do gerenciador de risco. Vamos observar como ele se comporta diante desse desafio.

Fig. 9. Resultados do EA com os parâmetros scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

Comparado à execução semelhante com o gerenciador de risco original, também houve uma melhoria nos resultados em todos os indicadores. Em relação à última execução, o lucro aumentou cerca de duas vezes, mas a retração triplicou, reduzindo o valor da lucratividade anual média normalizada. No entanto, em nenhum dos dias de negociação a retração excedeu os limites configurados nos parâmetros do gerenciador de risco.

Fica interessante explorar o que acontece se aumentarmos ainda mais o tamanho das posições abertas, por exemplo, em 10 vezes.

Fig. 10. Resultados do EA com os parâmetros scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

Como observado, com esses tamanhos de posições, o gerenciador de risco já não conseguiu manter o controle sobre o limite máximo de perda geral, e as negociações foram interrompidas. Isso demonstra que não é aconselhável proceder dessa forma, pois o gerenciador de risco não é uma ferramenta universal capaz de evitar uma retração máxima especificada independentemente das peculiaridades da estratégia de negociação. Ele serve apenas como um complemento às regras obrigatórias de gerenciamento de capital que devem estar presentes na estratégia de trading.

Por fim, vamos tentar encontrar os melhores parâmetros para o gerenciador de risco usando otimização genética. Infelizmente, cada execução para o intervalo de dois anos leva cerca de três minutos, o que significa que a otimização completa demandará um tempo considerável. Como critério de otimização, selecionamos um critério personalizado que calcula a lucratividade anual média normalizada. Após algum tempo, a parte superior da tabela de resultados da otimização apresenta o seguinte cenário:

Fig. 11. Resultados da otimização do EA com o gerenciador de risco

Como podemos ver, ajustar adequadamente os parâmetros do gerenciador de risco pode não apenas proteger as operações de retrações excessivas, mas também melhorar os resultados gerais de negociação: a diferença entre os diversos casos chegou a até 20% de lucro adicional. Embora o melhor conjunto de parâmetros encontrado esteja ligeiramente atrás dos resultados obtidos na Fig. 8 com valores intuitivamente escolhidos, é provável que uma otimização adicional pudesse melhorar ainda mais esses resultados. Contudo, não há uma necessidade real de continuar refinando, pois os ganhos adicionais seriam marginais.


Considerações finais

Vamos resumir brevemente. Melhoramos uma das partes mais importantes de qualquer sistema de trading bem-sucedido: o gerenciador de risco, tornando-o mais flexível e ajustável às necessidades específicas. Agora, dispomos de um mecanismo que permite cumprir de forma mais precisa os limites estabelecidos para retrações aceitáveis.

Embora tenhamos nos esforçado para torná-lo o mais confiável possível, é importante usar suas funcionalidades de maneira sensata. É crucial lembrar que o gerenciador de risco é uma medida extrema de proteção do capital. Assim, é preferível configurar os parâmetros de negociação de forma que o gerenciador de risco raramente precise intervir, ou que o faça apenas em situações muito raras. Conforme demonstrado nos testes, ao exceder significativamente os tamanhos de posições recomendados, a velocidade das mudanças no saldo da conta pode ser tão abrupta que nem mesmo o gerenciador de risco conseguirá evitar que os limites de retração sejam ultrapassados.

Durante o desenvolvimento dessa atualização para o gerenciador de risco, surgiram novas ideias para seu aprimoramento. No entanto, tornar o sistema excessivamente complexo nem sempre é vantajoso. Por isso, vamos pausar temporariamente o trabalho no gerenciador de risco e, nas próximas partes, nos concentraremos em questões mais urgentes relacionadas ao desenvolvimento do EA.

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

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

Arquivos anexados |
Últimos Comentários | Ir para discussão (1)
Cristian-bogdan Buzatu
Cristian-bogdan Buzatu | 14 jan. 2025 em 23:09
Olá, esse é um trabalho muito empolgante, obrigado por seu tempo e generosidade! Você poderia reunir em um arquivo zip tudo o que precisamos para compilar com sucesso sua versão mais recente deste script?
Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos
O artigo é o terceiro de uma série sobre os aspectos fundamentais da programação em MQL5. Aqui são descritos tipos de dados complexos que não foram abordados no artigo anterior, incluindo estruturas, uniões, classes e o tipo de dado "função". Também é explicado como adicionar modularidade ao programa utilizando a diretiva de pré-processador #include.
Desenvolvendo um sistema de Replay (Parte 75): Um novo Chart Trade (II) Desenvolvendo um sistema de Replay (Parte 75): Um novo Chart Trade (II)
Neste artigo explicarei grande parte da classe C_ChartFloatingRAD. Esta é responsável por fazer com que o Chart Trade funcione. Porém aqui não irei de fato terminar a explicação. A mesma será finalizada no próximo artigo. Já que o conteúdo neste artigo é bastante denso e precisa ser compreendido a fundo. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real
À medida que nos aproximamos de um EA pronto, é necessário prestar atenção em questões secundárias na etapa de teste da estratégia de trading, mas que se tornam importantes ao migrar para o trading real.
Do básico ao intermediário: Recursividade Do básico ao intermediário: Recursividade
Este artigo, veremos um conceito de programação muito interessante e bem divertido. Porém que deve ser tratado com extremo respeito. Já que um mal uso, ou mal entendimento do mesmo, torna programas relativamente simples em algo desnecessariamente complicado. Porém o bom uso, e a perfeita adequação em situações igualmente adequadas. Torna a recursividade um grande aliado para resolver questões que de outra forma seria muito mais trabalhoso e demorado. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.