English Русский Deutsch
preview
Gerenciador de risco para operar manualmente

Gerenciador de risco para operar manualmente

MetaTrader 5Exemplos | 12 agosto 2024, 15:59
54 0
Aleksandr Seredin
Aleksandr Seredin

Conteúdo


Introdução

Amigos, olá a todos! Neste artigo, seguiremos falando sobre como gerenciar riscos. No artigo anterior, "Balanceando riscos ao negociar múltiplos instrumentos simultaneamente", falamos sobre os conceitos básicos de risco e, no artigo atual, implementaremos do zero uma classe base gerenciadora de risco para negociar em segurança e veremos como realmente a limitação de riscos no sistema de negociação influi no desempenho das estratégias de negociação.

Em 2019, quando aprendi os fundamentos da programação, principalmente para mim mesmo, escrevi a minha primeira aula, o Gerenciador de Risco. Naquela época, graças à minha experiência, entendi que o estado psicológico de um operador exerce uma forte influência no seu desempenho quando é "sóbrio" e "imparcial" ao tomar decisões de trading. As apostas, as negociações de caráter emocional e a superestimação dos riscos na esperança de "se recuperar" são capazes de zerar qualquer conta, mesmo quando se aplica uma estratégia de negociação confiável, cujos resultados são muito bons durante os testes.

O objetivo deste artigo é mostrar que o controle dos riscos mediante um gerenciador de riscos melhora tanto sua performance quanto sua confiabilidade. Para confirmar essa tese, escreveremos do zero uma classe base gerenciadora de riscos para operar manualmente e a testaremos usando uma estratégia de rompimento simples baseada em fractais.


Definição de funcionalidades

Ao implementar nosso algoritmo para operar manualmente, nos focaremos em controlar os limites de risco diários, semanais e mensais. Caso o valor real da perda atinja ou exceda os limites definidos pelo usuário, o Expert Advisor deverá fechar automaticamente todas as posições abertas e informar o usuário de que não é possível continuar a negociar. Neste caso, é importante observar que a informação será meramente "recomendada" na linha de comentários no canto inferior esquerdo da tela na qual o Expert Advisor será iniciado. Pelo simples motivo de que estamos escrevendo um gerenciador de risco para negociar manualmente e, consequentemente, "se o usuário quiser", ele poderá remover esse EA do gráfico a qualquer momento e continuar negociando. Mas eu não recomendaria fazer isso, porque se o mercado for contra você, é melhor voltar a negociar no dia seguinte e não sofrer grandes perdas, e tentar entender o que exatamente deu errado durante a negociação manual. Depois de integrar essa classe à sua negociação algorítmica, limitar o envio de ordens quando o limite for alcançado será simples; além disso, é aconselhável incorporar essa classe à estrutura do próprio Expert Advisor. Falaremos mais sobre isso um pouco mais adiante.


Parâmetros de entrada e construtor de classe

Decidimos nos concentrar em controlar o risco por períodos e em atingir a meta de lucro diário. Para isso, introduziremos diversas variáveis double com o modificador da classe de memória input para o usuário inserir manualmente as porcentagens de risco do depósito para cada período, assim como a porcentagem de lucro diário que deseja fixar. Para especificar o controle do lucro diário planejado, adicionaremos uma variável bool que permitirá ativar ou desativar essa funcionalidade, caso o trader queira analisar cada entrada de forma independente e esteja seguro de que não há correlação entre os instrumentos selecionados. Esse tipo de variável "interruptor" também é conhecido como "flag". Declaramos o código globalmente da seguinte maneira, após "empacotá-lo" em um bloco identificado com a palavra-chave group por motivos de praticidade.

input group "RiskManagerBaseClass"
input double inp_riskperday    = 1;          // riskperday - риск на день, в процентах от депозита
input double inp_riskperweek   = 3;          // riskperweek - риск на неделю
input double inp_riskpermonth  = 9;          // riskpermonth - риск на месяц
input double inp_plandayprofit = 3;          // pdp - плановая прибыль

input bool dayProfitControl = true;          // dayProfitControl - кроем ли позы при достижении дневной прибыли

As variáveis declaradas são inicializadas com valores "padrão" seguindo a lógica descrita a seguir. Partimos do risco diário, pois essa classe é mais eficaz para operações intradiárias, mas também pode ser utilizada tanto em negociações de médio prazo quanto em investimentos. Obviamente, se você negocia a médio prazo ou investe, não faz sentido controlar o risco em um único dia; nesse caso, pode-se definir o risco diário e semanal com os mesmos valores. Além disso, se forem investimentos de longo prazo, você pode definir todos os limites com valores iguais e se focar apenas no rebaixamento mensal. No entanto, aqui abordaremos a lógica dos parâmetros padrão para operações intradiárias.

Definimos que nos sentimos confortáveis negociando com um risco diário de 1% do depósito. Se o limite diário for ultrapassado, fecharemos o termi nal até o dia seguinte. A seguir, definimos o limite semanal considerando que uma semana normalmente tem 5 dias de negociação, o que significa que, se tivermos 3 dias consecutivos de perdas, interromperemos a negociação até o início da próxima semana. Isso porque é provável que você não tenha compreendido o mercado naquela semana, ou algo tenha mudado, e se continuar negociando, poderá acumular um prejuízo tão grande durante esse período que não conseguirá compensá-lo na semana seguinte. Uma lógica semelhante se aplica ao estabelecer o limite mensal para operações intradiárias.

Consideramos que, se em um mês houver 3 semanas de perdas, é melhor não negociar na quarta semana, pois será necessário muito tempo para "corrigir" a curva de rentabilidade nos períodos seguintes e evitar assustar os investidores com uma grande perda em um único mês. O tamanho da meta de lucro diário é definido com base no risco diário, considerando as características do seu sistema de negociação. É importante ressaltar que NÃO RECOMENDO negociar sem stop-loss e sem um gerente de risco para limitar as perdas diárias ao mesmo tempo. Operar assim inevitavelmente zerará todo o depósito; é apenas uma questão de tempo. Portanto, ou você define stops para cada operação, ou utiliza um gerente de risco para limitar as perdas por período. No exemplo atual de parâmetros padrão, estabelecemos a meta de lucro diário como 1 para 3 em relação ao risco diário. Além disso, esses parâmetros são melhores quando combinados com a definição obrigatória de risco-retorno para CADA operação, com uma relação stop-loss e take-profit de 1 para 3 (take-profit maior que stop-loss).

Estruturalmente, nossos limites podem ser representados esquematicamente da seguinte forma.

Figura 1. Estrutura dos limites

Figura 1. Estrutura dos limites

Agora, declaramos o tipo de dados personalizado RiskManagerBase usando a palavra-chave class. Os parâmetros de entrada serão armazenados dentro da nossa classe personalizada RiskManagerBase. Como os parâmetros de entrada são medidos em porcentagens e os limites serão controlados na moeda do depósito, precisamos introduzir vários campos do tipo double com o modificador de acesso protected na nossa classe. 

protected:

   double    riskperday,                     // риск на день в процентах от депозита
             riskperweek,                    // риск на неделю в процентах от депозита
             riskpermonth,                   // риск на месяц в процентах от депозита
             plandayprofit                   // плановая дневная прибыль, в процентах от депозита
             ;

   double    RiskPerDay,                     // в валюте риск на день
             RiskPerWeek,                    // в валюте риск на неделю
             RiskPerMonth,                   // в валюте риск на месяц
             StartBalance,                   // в валюте баланс счёта на момент запуска советника
             StartEquity,                    // в валюте средства счёта на момент обновления лимитов
             PlanDayEquity,                  // в валюте плановое значение средства счёта на день
             PlanDayProfit                   // в валюте плановое значение прибыли на день
             ;

   double    CurrentEquity,                  // текущее значение средств
             CurrentBallance;                // текущий баланс

Para facilitar o cálculo dos limites de risco por períodos na moeda do depósito, com base nos parâmetros de entrada, declaramos o método RefreshLimits() dentro da nossa classe, também com o modificador de acesso protected. Descrevemos esse método fora da classe da seguinte forma. Prevemos, para o futuro, o tipo de retorno bool, caso seja necessário expandir nosso método para verificar se os dados são precisos. Por enquanto, descrevamos o método da seguinte forma.

//+------------------------------------------------------------------+
//|                        RefreshLimits                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::RefreshLimits(void)
  {
   CurrentEquity    = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // запросили текущее значение equity
   CurrentBallance  = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // запросили текущий баланс

   StartBalance     = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // установили стартовый баланс
   StartEquity      = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // запросили текущее значение equity

   PlanDayProfit    = NormalizeDouble(StartEquity * plandayprofit/100,2);     // в валюте плановый профит на день
   PlanDayEquity    = NormalizeDouble(StartEquity + PlanDayProfit/100,2);     // в валюте плановый equity

   RiskPerDay       = NormalizeDouble(StartEquity * riskperday/100,2);        // в валюте риск на день
   RiskPerWeek      = NormalizeDouble(StartEquity * riskperweek/100,2);       // в валюте риск на неделю
   RiskPerMonth     = NormalizeDouble(StartEquity * riskpermonth/100,2);      // в валюте риск на месяц

   return(true);
  }

Esse método será chamado sempre que precisarmos recalcular os valores dos limites ao mudar os períodos, bem como na inicialização dos valores dos campos ao chamar o construtor da classe. No construtor da nossa classe, para inicializar os valores iniciais dos campos, o código será escrito da seguinte forma.

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
RiskManagerBase::RiskManagerBase()
  {
   riskperday         = inp_riskperday;                                 // присвоили значение внутренней переменной
   riskperweek        = inp_riskperweek;                                // присвоили значение внутренней переменной
   riskpermonth       = inp_riskpermonth;                               // присвоили значение внутренней переменной
   plandayprofit      = inp_plandayprofit;                              // присвоили значение внутренней переменной

   RefreshLimits();                                                     // обновили лимиты
  }

Após definir a lógica dos parâmetros de entrada e o estado inicial dos dados da nossa classe, passemos para a implementação do controle dos limites.


Utilização de limites de risco

Para trabalhar com os períodos de limites de risco, precisaremos de variáveis adicionais com o modificador de acesso protected. Primeiro, declaramos uma flag para cada período na forma de variáveis do tipo bool, que armazenarão os dados sobre o alcance dos limites de risco estabelecidos, além de uma flag principal que informará sobre a possibilidade de continuar a negociação somente se todos os limites estiverem disponíveis simultaneamente. Isso é necessário para evitar a permissão de negociação quando o limite mensal já foi atingido, mas o limite diário continua disponível. Isso permitirá restringir a negociação quando qualquer limite de tempo for atingido, até o início do próximo intervalo de tempo. Também precisaremos de variáveis do mesmo tipo para controlar o alcance do lucro diário e o início de um novo dia de negociação. Além disso, adicionamos campos do tipo double para armazenar informações sobre os valores reais de lucro e perda para cada período: dia, semana e mês. Além disso, separamos os valores de swap e comissão das operações de negociação.

   bool              RiskTradePermission;    // общая переменная - можно ли открывать новые сделки
   bool              RiskDayPermission;      // флаг для запрета торговли если лимит на день закончился
   bool              RiskWeekPermission;     // флаг для запрета торговли если лимит на неделю закончился
   bool              RiskMonthPermission;    // флаг запрета торговли если лимит на месяц закончился

   bool              DayProfitArrive;        // переменная контролирующая достижение плановой прибыли за день
   bool              NewTradeDay;            // переменная нового торгового дня

   //--- факт лимиты
   double            DayorderLoss;           // накопленный убыток за день
   double            DayorderProfit;         // накопленный профит за день
   double            WeekorderLoss;          // накопленный убыток за неделю
   double            WeekorderProfit;        // накопленный профит за неделю
   double            MonthorderLoss;         // накопленный убыток за месяц
   double            MonthorderProfit;       // накопленный профит за месяц
   double            MonthOrderSwap;         // своп за месяц
   double            MonthOrderCommis;       // комиссия за месяц

Não incluímos intencionalmente as despesas com comissão e swap nos prejuízos dos períodos correspondentes, para podermos, no futuro, separar os prejuízos resultantes das decisões de negociação daqueles que decorrem das exigências de diferentes corretores quanto às comissões e swaps. Agora que os campos relevantes da nossa classe foram declarados, passemos ao controle do uso dos limites.


Controle do limite

Para controlar o uso real dos limites, precisaremos processar via código os eventos relacionados ao início de cada novo período, bem como os eventos relacionados ao início de operações já concluídas. Para registrar corretamente os limites utilizados, vamos declarar, na área de acesso protected da nossa classe, o método interno ForOnTrade().

Primeiro, precisaremos dispor de variáveis no método para registrar a hora atual, a hora do início do dia, da semana e do mês. Para isso, usaremos um tipo de dados especial, uma estrutura struct predefinida chamada MqlDateTime. E vamos inicializá-las imediatamente com os valores atuais da hora de operação do terminal, da seguinte forma.

   MqlDateTime local, start_day, start_week, start_month;               // создали структуры для фильтруемых дат
   TimeLocal(local);                                                    // заполнили первоначально
   TimeLocal(start_day);                                                // заполнили первоначально
   TimeLocal(start_week);                                               // заполнили первоначально
   TimeLocal(start_month);                                              // заполнили первоначально

Aqui é importante notar que, para a inicialização da hora atual, usaremos a função predefinida TimeLocal(), e não TimeCurrent(), porque a primeira usa a hora local, enquanto a segunda obtém a hora do último tick recebido da corretora, o que pode causar registros incorretos dos limites por conta das diferenças de fusos horários entre diferentes corretores. Em seguida, precisamos zerar o horário de início de cada período para obter os valores da data de início de cada um deles. Faremos isso acessando os campos públicos das nossas estruturas da seguinte maneira.

//--- обнуляем, чтобы отчёт был с начала периода
   start_day.sec     = 0;                                               // с начала дня
   start_day.min     = 0;                                               // с начала дня
   start_day.hour    = 0;                                               // с начала дня

   start_week.sec    = 0;                                               // с начала недели
   start_week.min    = 0;                                               // с начала недели
   start_week.hour   = 0;                                               // с начала недели

   start_month.sec   = 0;                                               // с начала месяца
   start_month.min   = 0;                                               // с начала месяца
   start_month.hour  = 0;                                               // с начала месяца

Para obter corretamente os dados da semana e do mês, precisamos definir a lógica de cálculo do início da semana e do mês. No caso do mês, é simples, sabemos que todo mês começa no primeiro dia. No entanto, para a semana, é um pouco mais complicado, pois não há um ponto de referência fixo, e a data muda a cada vez. Aqui, o campo especial day_of_week da estrutura MqlDateTime pode nos ajudar. Ele permite obter o número do dia da semana a partir da data atual, começando do zero. Sabendo esse valor, podemos facilmente determinar a data de início da semana atual da seguinte maneira.

//--- определения начала недели
   int dif;                                                             // переменная разницы дня недели
   if(start_week.day_of_week==0)                                        // если это первый день недели
     {
      dif = 0;                                                          // то обнуляем
     }
   else
     {
      dif = start_week.day_of_week-1;                                   // если не первый, то вычисляем разницу
      start_week.day -= dif;                                            // вычитаем из числа дня разницу на начало недели
     }

//---month
   start_month.day         = 1;                                         // с месяцем всё просто

Agora que temos as datas exatas de início de cada período em relação ao momento atual, podemos consultar os dados históricos das negociações realizadas na conta. Inicialmente, precisaremos declarar as variáveis necessárias para registrar as ordens fechadas e zerar os valores das variáveis que armazenarão os resultados financeiros das negociações para cada período selecionado.

//---
   uint     total  = 0;                                                 // количество выбранных сделок
   ulong    ticket = 0;                                                 // номер ордера
   long     type;                                                       // тип ордера
   double   profit = 0,                                                 // профит ордера
            commis = 0,                                                 // коммиссия ордера
            swap   = 0;                                                 // своп ордера

   DayorderLoss      = 0;                                               // дневной убыток без комиссии
   DayorderProfit    = 0;                                               // дневной профит
   WeekorderLoss     = 0;                                               // недельный убыток без комиссии
   WeekorderProfit   = 0;                                               // недельный профит
   MonthorderLoss    = 0;                                               // месячный убыток без комиссии
   MonthorderProfit  = 0;                                               // месячный профит
   MonthOrderCommis  = 0;                                               // месячная комиссия
   MonthOrderSwap    = 0;                                               // месячный своп

Consultaremos os dados históricos das ordens fechadas através da função predefinida do terminal HistorySelect(), cujos parâmetros exigem exatamente as datas obtidas anteriormente para cada período. Para isso, será necessário converter o nosso tipo de variáveis MqlDateTime para o tipo de dados datetime exigido pelos parâmetros da função HistorySelect(), utilizando a função predefinida do terminal StructToTime(). Vamos solicitar os dados das negociações inserindo os valores de início e fim do período necessário de forma semelhante.

Após cada chamada da função HistorySelect(), precisamos obter a quantidade de ordens selecionadas usando a função predefinida do terminal HistoryDealsTotal() e armazenar esse valor na nossa variável local total. Uma vez que obtemos a quantidade de negociações fechadas, podemos gerar um loop usando o operador for, solicitando o número de cada ordem através da função predefinida do terminal HistoryDealGetTicket(), para ter acesso aos dados de cada ordem. O acesso aos dados de cada ordem será realizado com as funções predefinidas do terminal HistoryDealGetDouble() e HistoryDealGetInteger(), passando para elas o número da ordem obtido anteriormente. Também será necessário especificar o identificador de propriedade da operação correspondente a partir das enumerações ENUM_DEAL_PROPERTY_INTEGER e ENUM_DEAL_PROPERTY_DOUBLE. Adicionalmente, precisamos incluir um filtro usando o operador de seleção lógica if para considerar apenas as negociações provenientes de operações de trading, verificando os valores DEAL_TYPE_BUY e DEAL_TYPE_SELL da enumeração ENUM_DEAL_TYPE, para excluir outras operações da conta, como operações de balanço e créditos de bônus. De forma geral, essa seleção será estruturada da seguinte maneira.

//---теперь выбираем данные по --==ДНЮ==--
   HistorySelect(StructToTime(start_day),StructToTime(local));          // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовый результат
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               DayorderProfit += profit;                                // добавляем к профиту
              }
            else
              {
               DayorderLoss += MathAbs(profit);                         // если убыток, то всё в сумму
              }
           }
        }
     }

//---теперь выбираем данные по --==НЕДЕЛЕ==--
   HistorySelect(StructToTime(start_week),StructToTime(local));         // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовому результату
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               WeekorderProfit += profit;                               // добавляем к профиту
              }
            else
              {
               WeekorderLoss += MathAbs(profit);                        // если убыток, то всё в сумму
              }
           }
        }
     }

//---теперь выбираем данные по --==МЕСЯЦУ==--
   HistorySelect(StructToTime(start_month),StructToTime(local));        // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовому резу
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         MonthOrderSwap    += swap;                                     // суммируем своп
         MonthOrderCommis  += commis;                                   // суммируем комиссию

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               MonthorderProfit += profit;                              // добавляем к профиту
              }
            else
              {
               MonthorderLoss += MathAbs(profit);                       // если убыток, то всё в сумму
              }
           }
        }
     }

Descrevemos um método que podemos chamar sempre que for necessário atualizar os valores atuais de uso dos limites. A atualização dos valores dos limites reais, bem como a chamada dessa função, pode ser realizada durante a geração de alguns eventos do terminal. Como o propósito desse método é atualizar os limites, ele pode ser acionado tanto em eventos relacionados a mudanças nas ordens atuais, como Trade e TradeTransaction, quanto em cada novo tick através do evento NewTick. Considerando que nosso método não é muito intensivo em termos de recursos, vamos atualizar os limites reais a cada tick. Agora, vamos implementar o manipulador de eventos necessário para processar os eventos relacionados ao cancelamento dinâmico e à autorização de negociação.


Manipulador de eventos de classe

Para processar os eventos, vamos definir o método interno da nossa classe ContoEvents() com nível de acesso protected. Para isso, declaremos com o mesmo nível de acesso campos auxiliares adicionais. Para rastrear o momento em que um novo período de negociação começa e, assim, modificar as flags de autorização de negociação, precisaremos armazenar os valores do último período registrado e do período atual. Para isso, utilizaremos arrays simples, declarados com o tipo de dados datetime para armazenar os valores dos períodos correspondentes.

   //--- дополнительные вспомогательные
   datetime          Periods_old[3];         // 0-day,1-week,2-mn
   datetime          Periods_new[3];         // 0-day,1-week,2-mn

No primeiro índice, armazenaremos os valores do dia, no segundo, da semana, e no terceiro, do mês. Se for necessário expandir os períodos controlados, esses arrays podem ser declarados de forma dinâmica em vez de estática. No entanto, aqui vamos nos limitar aos três intervalos de tempo declarados anteriormente. Agora, vamos adicionar no construtor da classe a inicialização primária dessas variáveis em forma de arrays da seguinte maneira.

   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1);                       // инициализировали текущий день прошлым периодом
   Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1);                       // инициализировали текущую неделю прошлым периодом
   Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1);                      // инициализировали текущий месяц прошлым периодом

Cada período correspondente será inicializado usando a função predefinida do terminal iTime(), passando como parâmetros o período correspondente da enumeração do tipo ENUM_TIMEFRAMES, referindo-se ao período anterior ao período atual. Deliberadamente, não inicializamos o array Periods_old[] para garantir que, após a chamada do construtor e a chamada do nosso método ContoEvents(), o evento de início de um novo período de negociação seja acionado e todas as flags para o início das operações de trading sejam ativadas, sendo fechadas posteriormente no código, caso os limites não estejam disponíveis. Caso contrário, na reinicialização, a classe pode não funcionar corretamente. O método descrito seguirá uma lógica simples: se o período atual não for igual ao anterior, significa que um novo período correspondente começou, e os limites podem ser reinicializados e a negociação autorizada, alterando os valores das flags. Além disso, para cada período, chamaremos o nosso método já descrito RefreshLimits() para recalcular os limites de entrada.

//+------------------------------------------------------------------+
//|                     ContoEvents                                  |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoEvents()
  {
// проверяем начало нового торгового дня
   NewTradeDay    = false;                                              // переменная нового торгового дня в фолс
   Periods_old[0] = Periods_new[0];                                     // скопировали в старый, новый
   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0);                       // обновили новый по дню
   if(Periods_new[0]!=Periods_old[0])                                   // если не совпали, то начался новый день
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!");  // проинформировали
      NewTradeDay = true;                                               // переменная в тру

      DayProfitArrive     = false;                                      // обнуляем флаг достижения профита с новым днём
      RiskDayPermission = true;                                         // разрешаем открывать новые позиции

      RefreshLimits();                                                  // обновили лимиты

      DayorderLoss = 0;                                                 // обнуляем значения дневного финансового результата
      DayorderProfit = 0;                                               // обнуляем значения дневного финансового результата
     }

// проверяем начало новой торговой недели
   Periods_old[1]    = Periods_new[1];                                  // скопировали в старый период данные
   Periods_new[1]    = iTime(_Symbol, PERIOD_W1, 0);                    // заполнили новый период по неделе
   if(Periods_new[1]!= Periods_old[1])                                  // если периоды не совпали, то началась новая неделя
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // проинформировали

      RiskWeekPermission = true;                                        // разрешаем открывать новые позиции

      RefreshLimits();                                                  // обновили лимиты

      WeekorderLoss = 0;                                                // обнулили лосы недели
      WeekorderProfit = 0;                                              // обнулили профиты недели
     }

// проверяем начало нового торгового месяца
   Periods_old[2]    = Periods_new[2];                                  // скопировали период в старый
   Periods_new[2]    = iTime(_Symbol, PERIOD_MN1, 0);                   // обновили новый период по месяцу
   if(Periods_new[2]!= Periods_old[2])                                  // если не совпали, то начался новый месяц
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!");   // проинформировали

      RiskMonthPermission = true;                                       // разрешаем открывать новые позы

      RefreshLimits();                                                  // обновили лимиты

      MonthorderLoss = 0;                                               // обнулили убытки месяца
      MonthorderProfit = 0;                                             // обнулили профиты месяца
     }

// корректируем разрешение на открытие новых позиций true только если все true
// корректируем на тру
   if(RiskDayPermission    == true &&                                   // если есть дневной лимит
      RiskWeekPermission   == true &&                                   // если есть недельный лимит
      RiskMonthPermission  == true                                      // если есть месячный лимит
     )                                                                  //
     {
      RiskTradePermission=true;                                         // если всё можно, то можно торговать
     }

// корректируем на фолс если хоть одна фолс
   if(RiskDayPermission    == false ||                                  // или нет дневного лимита
      RiskWeekPermission   == false ||                                  // или нет недельного лимита
      RiskMonthPermission  == false ||                                  // или нет месячного лимита
      DayProfitArrive      == true                                      // или если есть достижение планового профита
     )                                                                  // то
     {
      RiskTradePermission=false;                                        // запрещаем торговать
     }
   }

Este método também incluirá o controle do estado dos dados na principal variável de flag para a possibilidade de abertura de novas posições, RiskTradePermission. Usaremos operadores de seleção lógica para implementar a ativação da autorização através dessa variável, somente se todas as permissões estiverem presentes, e a desativação dela caso algum das flags não permita essa autorização. Essa variável será muito útil se quisermos adaptar essa classe para um Expert Advisor já existente, podendo ser obtida por um getter e inserida nas condições de envio das suas ordens. No entanto, no nosso caso, ela servirá apenas como uma flag para informar o usuário sobre a ausência de limites para negociação. Agora que nossa classe "aprendeu" a controlar os riscos ao atingir perdas definidas, vamos implementar a funcionalidade de controle da realização do lucro necessário.


Mecanismo para controlar o lucro planejado para o dia

Na parte anterior do nosso artigo, já declaramos a flag para iniciar o procedimento de controle do lucro planejado e a variável de entrada para definir o seu valor em relação ao tamanho do depósito da conta. A lógica do nosso método de controle de realização do lucro planejado consiste em fechar todas as posições abertas se o valor total do lucro nelas todas atingir o valor necessário. Para fechar todas as posições da conta, declaramos em nossa classe um método interno chamado AllOrdersClose() com nível de acesso public. Para o funcionamento deste método, será necessário obter dados sobre as posições abertas e enviar automaticamente ordens para fechá-las. 

Como queremos evitar gastar tempo escrevendo implementações próprias para essa funcionalidade, utilizamos as classes internas já prontas do terminal. Para trabalhar com posições abertas, usaremos a classe interna padrão do terminal CPositionInfo, e para fechar posições abertas, a classe CTrade. Declaramos as variáveis dessas duas classes também com o nível de acesso protected, sem o uso de ponteiros, com o construtor padrão, da seguinte forma.

   CTrade            r_trade;                // экземпляр
   CPositionInfo     r_position;             // экземпляр

No trabalho com esses objetos, na funcionalidade de que precisamos agora, não será necessário configurá-los adicionalmente, por isso, não vamos defini-los no construtor da nossa classe. A implementação desse método, utilizando as classes declaradas, será da seguinte forma.

//+------------------------------------------------------------------+
//|                       AllOrdersClose                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::AllOrdersClose()                                  // закрытие рыночных поз
  {
   ulong ticket = 0;                                                    // тикет ордера
   string symb;

   for(int i = PositionsTotal(); i>=0; i--)                             // идём по открытым позам
     {
      if(r_position.SelectByIndex(i))                                   // если позиция выбрана
        {
         ticket = r_position.Ticket();                                  // запомнили тикет позы

         if(!r_trade.PositionClose(ticket))                             // закрываем по тикету
           {
            Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // если нет, проинформировали
            return(false);                                              // вернули фолс
           }
         else
           {
            Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // если нет, проинформировали
            continue;                                                   // если всё ок - продолжаем
           }
        }
     }
   return(true);                                                        // вернём тру
  }

Chamaremos o método descrito quando a meta de lucro for atingida e quando os limites de tempo forem esgotados mais adiante no código. Além disso, o código poderá retornar um valor bool caso seja necessário lidar com erros ao enviar ordens de fechamento. Para implementar o controle de realização do lucro planejado, complementaremos nosso método de tratamento de eventos ContoEvents() com o seguinte código, logo após o código já descrito acima.

//--- дневная
   if(dayProfitControl)							// проверка на включение функционала пользователем
     {
      if(CurrentEquity >= (StartEquity+PlanDayProfit))                  // если еквити превысило или равно старт плюс план профит,
        {
         DayProfitArrive = true;                                        // ставим флаг по достижению дневного планового профита
         Print(__FUNCTION__+", PlanDayProfit has been arrived.");       // проинформировали о событии
         Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+
               ", StartEquity = "+DoubleToString(StartEquity)+
               ", PlanDayProfit = "+DoubleToString(PlanDayProfit));
         AllOrdersClose();                                              // закрыли все открытые ордера

         StartEquity = CurrentEquity;                                   // переписали значение стартового эквити

         //---даём пуш уведомление
         ResetLastError();                                              // сбросили последнюю ошибку
         if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// уведомление
           {
            Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// если нет, принтуем
           }
        }
     }

Neste método, prevemos uma notificação push ao usuário sobre a ocorrência deste evento através da função predefinida do terminal SendNotification. Para completar o mínimo de funcionalidades necessárias de nossa classe, resta-nos adicionar mais um método com acesso public, que será chamado ao vincular o gerenciador de risco à estrutura do nosso Expert Advisor.


Definição do método para iniciar o monitoramento na estrutura do EA

Para "ativar" a funcionalidade de monitoramento a partir da instância da nossa classe de gerenciamento de risco na estrutura do EA, declararemos um método público chamado ContoMonitor(). Nesse método, reuniremos todos os métodos de tratamento de eventos previamente declarados, além de complementá-lo com funcionalidade para comparar os limites efetivamente usados com os valores definidos pelo usuário nos parâmetros de entrada. Declararemos esse método com nível de acesso public e o descreveremos fora da classe da seguinte forma.

//+------------------------------------------------------------------+
//|                       ContoMonitor                               |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoMonitor()                                    // мониторинг
  {
   ForOnTrade();                                                        // обновляем факт каждый тик

   ContoEvents();                                                       // событийный блок

//---
   double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT);
   
   if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay &&    // если эквити меньше или равен стартовому балансу за вычетом риска на день
       currentProfit<0                                            &&    // профит меньше нуля
       RiskDayPermission==true)                                         // есть разрешение на дневную торговлю
      ||                                                                // ИЛИ
      (RiskDayPermission==true &&                                       // есть разрешение на дневную торговлю
       MathAbs(DayorderLoss) >= RiskPerDay)                             // зафиксированный лосс превысил риск на день
   )                                                                    

     {
      Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = "  +DoubleToString(currentProfit));// проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = "      +DoubleToString(RiskPerDay));   // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = "    +DoubleToString(DayorderLoss)); // проинформировали
      RiskDayPermission=false;                                          // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто
     }

// смотрим есть ли лимит на НЕДЕЛЮ для открытия новой позиции если нет открытых?
   if(
      MathAbs(WeekorderLoss)>=RiskPerWeek &&                            // если недельный убыток больше или равен риска не неделю
      RiskWeekPermission==true)                                         // и торговля велась
     {
      RiskWeekPermission=false;                                         // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто

      Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss));  // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek));      // проинформировали
     }

// смотрим есть ли лимит на МЕСЯЦ для открытия новой позиции если нет открытых?
   if(
      MathAbs(MonthorderLoss)>=RiskPerMonth &&                          // если месячный убыток больше риска на месяц и
      RiskMonthPermission==true)                                        // торговля велась
     {
      RiskMonthPermission=false;                                        // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто

      Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss));  // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth));      // проинформировали
     }
  }

A lógica do nosso método é muito simples: se o limite de perdas mensais ou semanais ultrapassou efetivamente o valor estabelecido pelo usuário, a flag de permissão de negociação para esse período será desativada, e, consequentemente, toda a negociação será interrompida. A única diferença está nos limites diários, onde precisamos também controlar a existência de posições abertas; para isso, adicionaremos um controle da meta de lucro atual das posições abertas mediante um operador lógico "ou". Ao atingir os limites de risco, chamamos nosso método de fechamento de posições já descrito e registramos esse evento no log.

Neste ponto, para completar a classe, precisamos apenas adicionar um método para o usuário poder monitorar os limites atuais. A maneira mais simples e bastante conveniente será exibir as informações necessárias através da função predefinida do terminal Comment(). Para trabalhar com essa função, precisaremos passar a ela um parâmetro do tipo string com as informações necessárias para exibição no gráfico. Para obter esses valores da nossa classe, declararemos o método Message() com nível de acesso public, que retornará um tipo de dados string com os dados compilados de todas as variáveis necessárias ao usuário.

//+------------------------------------------------------------------+
//|                        Message                                   |
//+------------------------------------------------------------------+
string RiskManagerBase::Message(void)
  {
   string msg;                                                          // сообщение

   msg += "\n"+" ----------Risk-Manager---------- ";                    // общие
//---
   msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission;           // разрешение общее торговли
   msg += "\n"+"RiskDayPer   = "+(string)RiskDayPermission;             // риск на день есть
   msg += "\n"+"RiskWeekPer  = "+(string)RiskWeekPermission;            // риск на неделю есть
   msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission;           // риск на месяц есть

//---лимиты и входные параметры
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"RiskPerDay   = "+DoubleToString(RiskPerDay,2);          // риск на день в usd
   msg += "\n"+"RiskPerWeek  = "+DoubleToString(RiskPerWeek,2);         // риск на неделю в usd
   msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2);        // риск на месяц в usd
//---текущие прибыли убытки по периодам
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"DayLoss     = "+DoubleToString(DayorderLoss,2);         // дневной лосс
   msg += "\n"+"DayProfit   = "+DoubleToString(DayorderProfit,2);       // дневной профит
   msg += "\n"+"WeekLoss    = "+DoubleToString(WeekorderLoss,2);        // недельный лосс
   msg += "\n"+"WeekProfit  = "+DoubleToString(WeekorderProfit,2);      // недельный профит
   msg += "\n"+"MonthLoss   = "+DoubleToString(MonthorderLoss,2);       // месячный лосс
   msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2);     // месячный профит
   msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2);     // месячный коммис
   msg += "\n"+"MonthSwap   = "+DoubleToString(MonthOrderSwap,2);       // месячный своп
//---для тек мониторинга

   if(dayProfitControl)                                                 // если дневной профит контролируем
     {
      msg += "\n"+" ---------dayProfitControl-------- ";                //
      msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive;         // достижение дневной прибыли
      msg += "\n"+"StartBallance   = "+DoubleToString(StartBalance,2);  // стартовый баланс
      msg += "\n"+"PlanDayProfit   = "+DoubleToString(PlanDayProfit,2); // плановый профит
      msg += "\n"+"PlanDayEquity   = "+DoubleToString(PlanDayEquity,2); // плановый еквити
     }
   return(msg);                                                         // вернули значение
  }

Quando este método for executado, a mensagem para o usuário será exibida conforme ilustrado na Figura 2.

Figura 2. Formato de exibição dos dados para o usuário.

Figura 2. Formato de exibição dos dados para o usuário.

Esse método pode ser aprimorado ou complementado com elementos adicionais para trabalhar com gráficos no terminal, mas nos limitaremos a isso por permitir que o usuário obtenha um volume suficiente de informações da nossa classe para tomar decisões. Eventualmente, cada usuário poderá aprimorar esse formato no futuro e torná-lo mais atraente graficamente, de acordo com suas preferências. A seguir, discutiremos as possibilidades de expansão desta classe no contexto de aplicações em estratégias de trading específicas.


Implementação final e possíveis expansões da classe

Como já dissemos anteriormente, a funcionalidade descrita aqui é a mínima necessária e a mais universal para quase todas as estratégias de negociação, o que é necessário para controlar os riscos e não "estourar o depósito" em um dia. Nesta parte do artigo, exploraremos algumas possibilidades adicionais de expansão dessa classe. 

  • controle do tamanho do spread ao negociar com stop-loss curto
  • controle do slippage em posições já abertas
  • controle do lucro mensal planejado

Quanto a sistemas de trading com stop-loss curto, podemos implementar a seguinte funcionalidade adicional. Declaramos um método SpreadMonitor(int intSL) que aceita como parâmetro o stop-loss técnico ou calculado do instrumento em pontos, para, assim, compará-lo com o nível atual do spread. Esse método impede colocar uma ordem quando o spread aumenta muito em relação ao stop-loss dentro da proporção definida pelo usuário, para evitar um alto risco de fechar uma posição no stop-loss por causa do spread.

Para controlar o slippage em posições abertas, podemos declarar um método SlippageCheck(). Esse método fecha cada negociação separadamente se a corretora a executar a um preço significativamente diferente do solicitado, e, consequentemente, se o risco da negociação exceder o valor planejado. Assim, no caso de acionamento do stop-loss, a estatística da negociação não será prejudicada com o aumento do risco por uma entrada específica. Além disso, ao negociar com uma proporção fixa de stop-loss e take-profit, essa proporção piora devido ao slippage, sendo melhor fechar a negociação com uma pequena perda do que comprometer as estatísticas posteriores.

Assim como a lógica de controle de lucro diário, podemos implementar um método semelhante para controlar o lucro planejado mensal. Esse método pode ser aplicado ao negociar estratégias de longo prazo, em contraste com as descritas neste artigo. A classe que descrevemos já tem toda a funcionalidade para ser usada ao negociar intraday manualmente e pode ser incorporada à implementação final de um Expert Advisor executado no gráfico do instrumento automaticamente com o início da operação manual.

A montagem final do projeto envolve conectar nossa classe usando a diretiva de pré-processamento #include da seguinte forma.

#include <RiskManagerBase.mqh>

Em seguida, declaramos globalmente o ponteiro para o nosso objeto gerenciador de risco.

RiskManagerBase *RMB;

Durante a inicialização do nosso Expert Advisor, alocamos manualmente a memória do nosso objeto para prepará-lo antes do início.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   RMB = new RiskManagerBase();

//---
   return(INIT_SUCCEEDED);
  }

Ao remover o nosso Expert Advisor do gráfico, precisamos liberar a memória do nosso objeto para evitar vazamentos de memória. Para isso, adicionamos o seguinte na função OnDeinit do Expert Advisor.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

   delete RMB;

  }

Além disso, se necessário, nesse mesmo evento, podemos chamar o método Comment(" ") passando uma string vazia, para o gráfico ser limpo dos comentários ao remover o Expert Advisor do gráfico do instrumento.

Chamamos o método de monitoramento principal de nossa classe quando ocorre um evento de novo tick do instrumento, da seguinte forma.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMB.ContoMonitor();

   Comment(RMB.Message());
  }
//+------------------------------------------------------------------+

Com isso, terminamos de montar nosso Expert Advisor com gerenciador de risco embutido; ele agora está pronto e pode ser usado (arquivo ManualRiskManager.mq5). Para testarmos alguns cenários de uso, faremos uma pequena adição ao código atual, para simular o processo de trading manual.


Exemplo de uso

Para que consigamos visualizar o processo de negociação manual com e sem o uso do gerenciador de risco, precisamos de um código adicional que simule esse processo. Como neste artigo não tocaremos a escolha de estratégias de negociação, não implementaremos toda a funcionalidade de negociação no código, e, em vez disso, pegaremos as entradas do gráfico diário e inseriremos os dados prontos em nosso Expert Advisor. Vamos usar a estratégia mais simples para tomar decisões de negociação e apresentar o resultado financeiro da mesma com e sem controle de risco.

Como exemplos de entradas, usaremos a estratégia mais simples de rompimento de nível fractal no USDJPY para o período de dois meses e veremos como essa estratégia se comporta com e sem controle de risco. Em termos esquemáticos, os sinais dessa estratégia com entradas manuais serão parecidos com os do gráfico a seguir.

Figura 3. Entradas da estratégia de teste

Figura 3. Entradas da estratégia de teste

Para simular a estratégia indicada, escrevemos um pequeno complemento na forma de 'unit test' universal, aplicável a qualquer estratégia manual, para que cada usuário possa, com pequenas modificações, testar suas próprias entradas. Esse teste consistirá em carregar sinais já preparados previamente para execução, sem implementar a lógica de cada entrada. Para isso, primeiro declaramos uma estrutura adicional (struct) que servirá como armazenamento para as nossas entradas baseadas em fractais.

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+
struct TradeInputs
  {
   string             symbol;                                           // символ
   ENUM_POSITION_TYPE direction;                                        // направление
   double             price;                                            // цена
   datetime           tradedate;                                        // дата
   bool               done;                                             // флаг отработки
  };

A classe responsável pela simulação dos sinais de trading será a classe TradeModel. O construtor desta classe recebe um contêiner com os parâmetros de entrada dos sinais, e seu método principal Processing() verifica a cada tick se o momento de entrada baseado nos valores inseridos foi atingido. Como estamos modelando trading intraday, no final do dia todas as posições serão fechadas usando o método AllOrdersClose() anteriormente declarado na nossa classe de gerenciamento de risco. Em termos gerais, nossa classe auxiliar tem a seguinte aparência.

//+------------------------------------------------------------------+
//|                        TradeModel                                |
//+------------------------------------------------------------------+
class TradeModel
  {
protected:

   CTrade               *cTrade;                                        // длятрэйда
   TradeInputs       container[];                                       // хранилище входов

   int               size;                                              // размер хранилища

public:
                     TradeModel(const TradeInputs &inputs[]);
                    ~TradeModel(void);

   void              Processing();                                      // основной метод моделирования
  };

Para facilitar a colocação de ordens, utilizaremos a classe com código aberto do terminal CTrade, que contém toda a funcionalidade necessária, para, assim, economizarmos tempo no desenvolvimento da nossa classe auxiliar. Para passar os parâmetros de entrada ao criar uma instância da classe, definimos nosso construtor com um parâmetro de entrada que armazena os dados da entrada da seguinte forma.

//+------------------------------------------------------------------+
//|                          TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::TradeModel(const TradeInputs &inputs[])
  {
   size = ArraySize(inputs);                                            // получили размер хранилища
   ArrayResize(container, size);                                        // ресайзнули

   for(int i=0; i<size; i++)                                            // идём по инпутам
     {
      container[i] = inputs[i];                                         // копируем во внутренний
     }

//---класс трэйда
   cTrade=new CTrade();                                                 // создали экземпляр трэйда
   if(CheckPointer(cTrade)==POINTER_INVALID)                            // если не создался экземпляр, ТО
     {
      Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!");   // проинформировали
     }
   cTrade.SetTypeFillingBySymbol(Symbol());                             // исполнение по символу
   cTrade.SetDeviationInPoints(1000);                                   // отклонение
   cTrade.SetExpertMagicNumber(123);                                    // мэджик
   cTrade.SetAsyncMode(false);                                          // асинхронный метод
  }

No construtor, inicializamos o armazenamento dos parâmetros de entrada com o valor necessário, decoramos seu tamanho e criamos um objeto da nossa classe CTrade com as configurações necessárias. Aqui, a maioria dos parâmetros também não será configurada pelo usuário, pois o objetivo da criação do nosso unit test não será particularmente afetado por eles, então deixaremos esses parâmetros com valores fixos. 

O destrutor da nossa classe TradeModel pedirá apenas a remoção do objeto do tipo CTrade e, por isso, será definido de forma simples.

//+------------------------------------------------------------------+
//|                         ~TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::~TradeModel(void)
  {
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // если есть экземпляр, ТО
     {
      delete cTrade;                                                    // удаляем
     }
  }

Agora, implementamos nosso método principal de processamento para a nossa classe funcionar dentro da estrutura de todo o nosso projeto. Executamos a lógica de colocação de ordens conforme mostrado na Figura 3 da seguinte forma.

//+------------------------------------------------------------------+
//|                         Processing                               |
//+------------------------------------------------------------------+
void TradeModel::Processing(void)
  {
   datetime timeCurr = TimeCurrent();                                   // запросили текущее время

   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // взяли бид
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // взяли аск

   for(int i=0; i<size; i++)                                            // идём по инпутам
     {
      if(container[i].done==false &&                                    // если ещё не торговали И
         container[i].tradedate <= timeCurr)                            // дата что нужно
        {
         switch(container[i].direction)                                 // проверяем направление трейда
           {
            //---
            case  POSITION_TYPE_BUY:                                    // если покупка
               if(container[i].price >= ask)                            // смотрим дошла ли цена, то
                 {
                  if(cTrade.Buy(0.1))                                   // покупаем одинаковый лот
                    {
                     container[i].done = true;                          // если прошло, ставим отметку
                     Print("Buy has been done");                        // информируем
                    }
                  else                                                  // если не прошло,
                    {
                     Print("Error: buy");                               // информируем
                    }
                 }
               break;                                                   // завершение кейса
            //---
            case  POSITION_TYPE_SELL:                                   // если это продажа
               if(container[i].price <= bid)                            // смотрим дошла ли цена, то
                 {
                  if(cTrade.Sell(0.1))                                  // продаем одинаковый лот
                    {
                     container[i].done = true;                          // если прошло, ставим отметку
                     Print("Sell has been done");                       // информируем
                    }
                  else                                                  // если не прошло,
                    {
                     Print("Error: sell");                              // информируем
                    }
                 }
               break;                                                   // завершение кейса

            //---
            default:
               Print("Wrong inputs");                                   // информируем
               return;
               break;
           }
        }
     }
  }

A lógica desse método é bastante simples. Se houver entradas não executadas no contêiner e o tempo de simulação for apropriado, colocamos essas ordens conforme a direção e o preço do fractal indicado na Figura 3. Essa funcionalidade será suficiente para testar nosso gerenciador de risco e permitir sua integração ao projeto principal.

Primeiramente, conectamos nossa classe de testes ao código do Expert Advisor da seguinte maneira.

#include <TradeModel.mqh>

Em seguida, na função OnInit(), criaremos uma instância da nossa estrutura de array de dados de entrada TradeInputs e passaremos esse array para o construtor da classe TradeModel para sua inicialização.

//---
   TradeInputs modelInputs[] =
     {
        {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.794, D'2024-02-05',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.882, D'2024-02-08',false},
        {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false}
     };

//---
   tModel = new TradeModel(modelInputs);

Na função DeInit(), não se esqueça de liberar a memória do nosso objeto tModel. O funcional principal será executado na função OnTick(), complementada com o seguinte código.

   tModel.Processing();                                                 // выставляем ордера

   MqlDateTime time_curr;                                               // структура текущего времени
   TimeCurrent(time_curr);                                              // запросили текущее время

   if(time_curr.hour >= 23)                                             // если конец дня
     {
      RMB.AllOrdersClose();                                             // кроем все позы
     }

Agora, comparamos os resultados da mesma estratégia com e sem a classe de controle de riscos. Executaremos o arquivo de unit test ManualRiskManager(UniTest1), no qual o método de controle de riscos pelos dados de entrada não será acionado. Período de janeiro a março de 2024. O resultado da execução da nossa estratégia será o seguinte.

Figura 4. Dados de teste sem aplicação do gerenciador de risco

Figura 4. Dados de teste sem aplicação do gerenciador de risco

Como resultado, obtemos uma expectativa matemática positiva para essa estratégia com os seguintes parâmetros.

Nome do parâmetro Valor do parâmetro
 1  Expert Advisor  ManualRiskManager(UniTest1)
 2  Símbolo  USDJPY
 3  Período do gráfico  M15
 4  Intervalo  2024.01.01 - 2024.03.18
 5  Forward  não 
 6  Atrasos   Sem atrasos, execução ideal
 7  Modelagem  Todos os ticks 
 8  Depósito inicial  10 000 USD 
 9  Alavancagem  1:100 

Tabela 1. Parâmetros de entrada para o testador de estratégias


Agora, vamos executar o arquivo de unit test ManualRiskManager(UniTest2), onde ativaremos nossa classe de gerenciador de risco com os seguintes parâmetros de entrada.

Nome do parâmetro de entrada Valor da variável
inp_riskperday 0,25
inp_riskperweek 0,75
inp_riskpermonth 2,25
inp_plandayprofit  0,78 
dayProfitControl  true

Tabela 2. Parâmetros de entrada para o gerenciador de risco

A lógica para a formação dos parâmetros de entrada é semelhante à descrita anteriormente no projeto da estrutura de parâmetros de entrada na Capítulo 3. A curva de rendimento será a seguinte.

Figura 5. Dados de teste com aplicação do gerenciador de risco

Figura 5. Dados de teste com aplicação do gerenciador de risco


Os dados resumidos dos resultados do teste dos dois casos são apresentados na tabela a seguir.

Indicador Sem RM Com RM Mudança
 1  Lucro líquido:  41.1 144.48  +103.38
 2  Máximo rebaixamento de saldo:  0.74% 0.25%  Redução em 3 vezes
 3  Máximo rebaixamento de capital:  1.13% 0.58%  Redução em 2 vezes
 4  Expectativa de ganho:  10.28 36.12  Aumento de mais de 3 vezes
 5  Índice de Sharpe:  0.12 0.67  Aumento de 5 vezes
 6  Trades lucrativos (% do total):  75% 75%  -
 7  Média do trade lucrativo:  38.52 56.65  Aumento de 50%
 8  Média do trade perdedor:  -74.47 -25.46  Redução em 3 vezes
 9  Média de risco-retorno:  0.52  2.23  Aumento de 4 vezes

Tabela 3. Comparação dos resultados financeiros da negociação com e sem o gerenciador de risco

Os resultados dos nossos unit tests indicam que o uso do controle de riscos por meio da nossa classe de gerenciador de riscos permitiu aumentar substancialmente a eficiência da negociação com a mesma estratégia simples, ao limitar os riscos e fixar o lucro de cada operação em relação ao risco estabelecido. Isso resultou em uma redução do rebaixamento de saldo em 3 vezes e do capital em 2 vezes. A expectativa de ganho da estratégia aumentou mais de 3 vezes, enquanto o índice de Sharpe cresceu mais de 5 vezes. A média do trade lucrativo aumentou em 50%, enquanto a média do trade perdedor diminuiu 3 vezes, o que elevou a média de risco-retorno da conta quase ao valor-alvo de 1 para 3. A comparação detalhada dos resultados financeiros de cada operação individual do nosso pool é apresentada na tabela a seguir.


Data Instrumento Direção Lote Sem RM Com RM Mudança
2024.01.31 USDJPY buy 0.1 25.75 78 + 52.25
2024.02.05
USDJPY sell 0.1
13.19 13.19 -
2024.02.08
USDJPY sell 0.1
76.63 78.75 + 2.12
2024.02.08
USDJPY buy 0.1
-74.47 -25.46 + 49.01
Total - - - 41.10 144.48 + 103.38

Tabela 4. Comparação das operações realizadas com e sem o gerenciador de risco


Considerações finais

Com base nos argumentos apresentados neste artigo, podemos tirar as seguintes conclusões. O uso de um gerenciador de risco, mesmo na negociação manual, pode aumentar significativamente a eficácia de uma estratégia já lucrativa. No caso de uma estratégia que esteja dando prejuízo, o gerenciador de risco ajudará a preservar substancialmente o seu capital, limitando as perdas. Como mencionado na introdução, o fator psicológico é o mais importante. Não se deve desativar o gerenciador de risco na esperança de "recuperar as perdas de imediato". É melhor esperar o término dos limites e, com a mente tranquila e sem emoções, recomeçar a negociação. O período em que o gerenciador de risco impede a negociação deve ser usado para analisar a sua estratégia e identificar as causas das perdas, bem como maneiras de evitá-las no futuro.

Agradeço a todos que leram este artigo até o fim. Espero sinceramente que este artigo ajude a salvar pelo menos um depósito de ser completamente perdido; se isso acontecer, considerarei que meus esforços não foram em vão. Ficarei feliz com seus comentários ou mensagens privadas, especialmente sobre se devemos iniciar um novo artigo, onde poderemos adaptar essa classe para um EA puramente algorítmico. Ou outras sugestões sobre este tópico. Mais uma vez, obrigado.


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

Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Redes neurais de maneira fácil (Parte 81): Análise da dinâmica dos dados considerando o contexto (CCMR) Redes neurais de maneira fácil (Parte 81): Análise da dinâmica dos dados considerando o contexto (CCMR)
Em trabalhos anteriores, sempre avaliamos o estado atual do ambiente. No entanto, a dinâmica das mudanças dos indicadores sempre ficou "nos bastidores". Neste artigo, quero apresentar a vocês um algoritmo que permite avaliar a mudança direta dos dados entre dois estados consecutivos do ambiente.
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.
Experiência no desenvolvimento de estratégias de negociação Experiência no desenvolvimento de estratégias de negociação
Neste artigo, proponho tentarmos desenvolver nossa própria estratégia de negociação. Uma estratégia de negociação deve ser construída com base em uma determinada vantagem estatística. E tal vantagem deve ser duradoura.