
Gerenciador de riscos para trading algorítmico
Conteúdo
- Introdução
- Classe herdeira para trading algorítmico
- Interface para operar com stop-loss curto
- Controle de slippage em ordens abertas
- Controle do spread na abertura de posições
- Implementação da interface
- Implementação do bloco de trading
- Compilação e teste do projeto
Introdução
Este artigo foca na criação de uma classe de gerenciador de riscos para o controle de riscos em operações algorítmicas. O objetivo deste artigo é adaptar os princípios de controle de risco para trading algorítmico em uma classe dedicada, facilitando a compreensão e a comprovação da eficácia do método de normalização de risco em operações intradiárias e em investimentos nos mercados financeiros. Os conteúdos apresentados aqui usará e complementará as informações tratadas no artigo anterior Gerenciador de risco para operar manualmente. No artigo anterior, mostramos como o controle de risco pode melhorar significativamente os resultados de uma estratégia de trading, mesmo que já seja lucrativa, e proteger os investidores de grandes rebaixamentos em um curto período de tempo.
Atendendo aos pedidos nos comentários do artigo anterior, este artigo dará mais atenção aos critérios de seleção dos métodos de implementação, para tornar o artigo mais compreensível para iniciantes. Além disso, vamos definir termos de trading usados ao longo das explicações do código. Por outro lado, desenvolvedores experientes poderão usar o material apresentado para adaptar o código à sua própria arquitetura.
Conceitos adicionais e definições utilizadas neste artigo:
High/low – valores máximos ou mínimos do preço de um ativo em um determinado período, que pode ser representado por uma barra ou um candle.
Stop-loss (stop-loss, stop) – preço limite de saída de uma posição em caso de prejuízo. Ou seja, se o preço se mover contra a posição aberta, limitamos as perdas fechando a posição antes que o prejuízo ultrapasse os valores calculados na abertura.
Take-profit (take-profit, take) – preço limite para saída de uma posição com lucro. Esse preço é estabelecido para garantir o ganho. Geralmente, é calculado com base no lucro planejado para o depósito ou na zona de exaustão da volatilidade diária esperada do ativo. Simplificando, é o ponto onde se percebe que o potencial de movimento já foi esgotado, e a probabilidade de uma correção que reduza o lucro é alta.
Stop-loss técnico – preço de stop-loss definido com base na análise técnica, como o high/low de um candle, um ponto de reversão, fractais, etc., dependendo da estratégia de trading aplicada. O principal diferencial dessa abordagem é que o tamanho do stop em pontos é retirado diretamente do gráfico com base em uma formação específica. Nesse caso, o ponto de entrada pode variar, mas o valor do preço do stop-loss permanece inalterado. Isso ocorre porque assumimos que, se o preço atingir o nosso stop-loss, a formação técnica será considerada quebrada, tornando o sentido da posição irrelevante para sua manutenção.
Stop-loss calculado – preço de stop-loss estabelecido com base na volatilidade projetada do ativo ao longo de um período específico. Diferencia-se por não estar vinculado a uma formação gráfica específica. Nesta abordagem, a escolha do ponto de entrada é especialmente importante, enquanto a localização do stop em relação a um padrão gráfico não é tão relevante.
Getter – método de uma classe que permite acessar o valor de um campo protegido da classe ou estrutura. É necessário para encapsular o valor da classe dentro da lógica definida pelo desenvolvedor, evitando qualquer alteração indevida na funcionalidade ou no valor do campo protegido, conforme o nível de acesso especificado.
Slippage (deslizamento) – deslizamento de preço na execução da ordem, quando a corretora executa a ordem a um preço diferente do originalmente solicitado. Essa situação pode ocorrer ao negociar ordens de mercado. Por exemplo, ao enviar a ordem, o volume da posição é calculado com base no risco planejado em moeda do depósito e no stop-loss calculado ou técnico em pontos. Após a execução da ordem pela corretora, pode-se descobrir que a posição foi aberta a preços diferentes daqueles usados no cálculo do stop em pontos. Por exemplo, em vez de 100 pontos, o stop pode se tornar 150 em um mercado volátil. Esses casos devem ser monitorados, e se o risco da ordem aberta exceder significativamente (de acordo com os parâmetros do gerenciador de riscos) o planejado, a posição deve ser fechada antecipadamente para manter a consistência das estatísticas de trading, aguardando uma nova oportunidade de entrada.
Estilo de trading intradiário – estilo de trading que envolve a realização de operações dentro do mesmo dia de trading. Esse método não envolve manter posições abertas durante a noite (overnight), evitando os riscos associados a gaps de abertura matinal, taxas adicionais para manter posições durante a noite e possíveis mudanças de tendência no dia seguinte. Se as posições forem mantidas para o dia seguinte, esse estilo de trading pode ser considerado de médio prazo.
Estilo de trading posicional – estilo de trading que envolve manter uma única posição em um ativo sem aumentar ou diminuir o volume da posição existente e sem realizar novas entradas. Nesta abordagem, ao receber um sinal de trading, o trader calcula o risco total para essa oportunidade e executa apenas essa operação, sem considerar novos sinais até que a posição anterior seja completamente encerrada. Nesta abordagem, sinais adicionais não são considerados, ou são levados em conta apenas após o fechamento completo da posição anterior.
Impulso em um ativo de trading – o impulso é caracterizado como um movimento unidirecional sem correções de um ativo em um timeframe específico. O ponto inicial do impulso é o começo do movimento contínuo sem retrações. Se o preço retornar ao ponto inicial do impulso, isso geralmente é chamado de reteste. A amplitude do movimento sem correções, medida em pontos, é influenciada, mas não exclusivamente, pela volatilidade do mercado, pela divulgação de notícias importantes ou pela aproximação do preço a níveis significativos.
Classe herdeira para trading algorítmico
O RiskManagerBase que desenvolvemos no artigo anterior já contém toda a lógica necessária para o controle de riscos, proporcionando segurança adicional durante operações ativas intradiárias. Para evitar a duplicação desse funcional, aplicaremos um dos princípios fundamentais da programação orientada a objetos em MQL5: herança. Essa abordagem permitirá estender o código existente sem replicá-lo, adicionando apenas as funcionalidades necessárias para integrar a classe em qualquer algoritmo de trading.
A arquitetura do projeto será baseada nos seguintes princípios:
- economia de tempo ao evitar a escrita de funcionalidades duplicadas
- adesão aos princípios de programação SOLID
- facilidade de uso da arquitetura por múltiplas equipes de desenvolvedores
- possibilidade de expansão do projeto para qualquer estratégia de trading
O primeiro ponto, como mencionado, visa economizar tempo de desenvolvimento — utilizaremos a funcionalidade de herança para não "quebrar" a lógica já validada de gerenciamento de limites e eventos, poupando esforços na replicação e teste de código existente.
O segundo ponto aborda os princípios básicos de construção de classes em programação. Primeiramente, aplicaremos o "Princípio Aberto-Fechado" (Open Closed Principle), garantindo que a classe possa ser estendida sem comprometer os princípios e abordagens de controle de riscos. Cada método adicional respeitará o "Princípio da Responsabilidade Única" (Single Responsibility Principle), facilitando o desenvolvimento e a compreensão lógica do código. Esse princípio se conecta com o ponto seguinte.
O terceiro ponto destaca que os princípios aplicados tornarão a lógica mais compreensível para terceiros, e a separação de cada classe em arquivos distintos permitirá trabalho simultâneo por várias equipes de desenvolvedores, minimizando conflitos ao mesclar mudanças em diferentes versões do projeto.
Além disso, não limitaremos a herança da nossa classe RiskManagerAlgo com o especificador final, permitindo melhorias e extensões futuras. Isso garantirá uma adaptação flexível da classe derivada para praticamente qualquer sistema de trading.
Com a aplicação dos princípios mencionados acima, nossa classe terá a seguinte aparência:
//+------------------------------------------------------------------+ //| RiskManagerAlgo | //+------------------------------------------------------------------+ class RiskManagerAlgo : public RiskManagerBase { protected: CSymbolInfo r_symbol; // instance double slippfits; // allowable slippage per trade double spreadfits; // allowable spread relative to the opened stop level double riskPerDeal; // risk per trade in the deposit currency public: RiskManagerAlgo(void); // constructor ~RiskManagerAlgo(void); // destructor //---getters bool GetRiskTradePermission() {return RiskTradePermission;}; //---interface implementation virtual bool SlippageCheck() override; // checking the slippage for an open order virtual bool SpreadMonitor(int intSL) override; // spread control }; //+------------------------------------------------------------------+
Além dos campos e métodos já presentes na classe base RiskManagerBase, na nossa classe herdeira RiskManagerAlgo, adicionamos os seguintes elementos para garantir funcionalidades adicionais em Expert Advisors (EA) algorítmicos. Primeiramente, precisaremos de um getter para acessar os dados do campo protegido de nível protected da classe herdeira RiskTradePermission a partir da classe base RiskManagerBase. Esse método será o principal meio de obter a permissão do gerenciador de riscos para abrir novas posições na seção de condições de colocação de ordens de forma algorítmica. O princípio de funcionamento é bem simples: se essa variável contiver o valor true, o EA pode continuar a colocar ordens conforme os sinais de sua estratégia de trading. Se for false, a colocação não é permitida, mesmo que a estratégia indique um novo ponto de entrada.
Também incluiremos uma instância da classe padrão CSymbolInfo do terminal MT5 com código aberto para trabalhar com os campos do símbolo do instrumento. A classe CSymbolInfo oferece acesso simplificado às propriedades do símbolo, o que também permitirá reduzir visualmente o código no nosso EA, facilitando a leitura e a manutenção do funcional da classe.
Adicionaremos à nossa classe atributos adicionais para as condições de controle de slippage e spread. O estado de controle da condição de slippage, definido pelo usuário, será armazenado na variável slippfits, enquanto a condição referente ao tamanho do spread será armazenada na variável spreadfits. A terceira variável necessária será aquela que contém o tamanho do risco por operação em moeda do depósito. Vale destacar que uma variável separada foi declarada para o controle de slippage das ordens, devido ao fato de que, em operações intradiárias, a estratégia de trading geralmente gera múltiplos sinais, não sendo necessário limitar-se a uma única operação com risco calculado para o dia inteiro. Isso implica que, antes de iniciar as operações, o trader já sabe previamente quais sinais irá executar em cada ativo e calcula o risco por operação igual ao risco diário, considerando as possíveis reentradas na posição.
Concluímos que a soma de todos os riscos de todas as entradas não deve exceder o risco diário. Se houver apenas uma entrada no dia, essas somas podem ser iguais, mas isso é raro em operações intradiárias, pois geralmente há várias entradas. Declararemos o código no nível global da seguinte forma, "encapsulando-o" previamente em um bloco nomeado com a palavra-chave group para maior comodidade:
input group "RiskManagerAlgoClass" input double inp_slippfits = 2.0; // inp_slippfits - allowable slippage per open deal input double inp_spreadfits = 2.0; // inp_spreadfits - allowable spread relative to the stop level to open input double inp_risk_per_deal = 100; // inp_risk_per_deal - risk per trade in the deposit currency
Essa abordagem permitirá configurar de maneira flexível as condições de monitoramento das posições abertas, conforme as condições estabelecidas pelo usuário.
Na seção pública public da nossa classe RiskManagerAlgo, declararemos as funções virtuais do nosso interface para serem sobrescritas da seguinte forma:
//--- implementation of the interface virtual bool SlippageCheck() override; // checking the slippage for an open order virtual bool SpreadMonitor(int intSL) override; // spread control
Aqui, usamos a palavra-chave virtual, que é um especificador de função que fornece um mecanismo para a seleção dinâmica, em tempo de execução, da função-membro apropriada entre as funções da nossa classe base RiskManagerBase e da classe derivada RiskManagerAlgo, cujo ancestral comum será nosso interface com funções puramente virtuais.
Faremos a inicialização no construtor da classe herdeira RiskManagerAlgo por meio da cópia dos valores inseridos pelo usuário através das variáveis de parâmetros de entrada nos valores correspondentes dos campos do objeto instanciado:
//+------------------------------------------------------------------+ //| RiskManagerAlgo | //+------------------------------------------------------------------+ RiskManagerAlgo::RiskManagerAlgo(void) { slippfits = inp_slippfits; // copy slippage condition spreadfits = inp_spreadfits; // copy spread condition riskPerDeal = inp_risk_per_deal; // copy risk per trade condition }
Vale destacar que, às vezes, a inicialização direta dos campos da classe pode ser mais prática, mas, neste caso, isso não fará grande diferença, então deixaremos a inicialização por cópia para facilitar a compreensão. Por sua vez, em suas próprias implementações, você pode usar a seguinte abordagem:
//+------------------------------------------------------------------+ //| RiskManagerAlgo | //+------------------------------------------------------------------+ RiskManagerAlgo::RiskManagerAlgo(void):slippfits(inp_slippfits), spreadfits(inp_spreadfits), rispPerDeal(inp_risk_per_deal) { }
No destrutor da classe, não precisaremos liberar a memória "manualmente", então deixaremos o corpo da função vazio, como neste exemplo:
//+------------------------------------------------------------------+ //| ~RiskManagerAlgo | //+------------------------------------------------------------------+ RiskManagerAlgo::~RiskManagerAlgo(void) { }
Agora que todas as funções necessárias foram declaradas na classe RiskManagerAlgo, passaremos para a escolha do método de implementação da nossa interface para trabalhar com stop-loss curto em posições abertas.
Interface para operar com stop-loss curto
A linguagem de programação MQL5 oferece uma grande flexibilidade no desenvolvimento e uso do funcional necessário em implementações otimizadas. Parte desse funcional foi trazida do C++, e outra parte foi ampliada para facilitar o desenvolvimento. Para implementar o controle das posições abertas com stop-loss curto, precisamos de um objeto genérico que possa ser usado como pai, não apenas para herança na nossa classe de gerenciamento de riscos, mas também em outras arquiteturas do EA, como um dos ancestrais.
Para declarar um tipo de dado genérico criado para integrar uma funcionalidade específica no design, podemos usar tanto classes abstratas no estilo C++ quanto o tipo de dado específico chamado interface.
Classes abstratas, assim como interfaces, são usadas para criar entidades genéricas, a partir das quais se espera construir classes derivadas mais concretas. No nosso caso, isso servirá para trabalhar com posições usando stop-loss curto. Uma classe abstrata é aquela que só pode ser usada como classe base para outra classe derivada, o que significa que não é possível criar um objeto de um tipo abstrato. Caso precisemos usar tal entidade genérica, o código da nossa classe seria assim:
//+------------------------------------------------------------------+ //| CShortStopLoss | //+------------------------------------------------------------------+ class CShortStopLoss { public: CShortStopLoss(void) {}; // the class will be abstract event if at least one function in it is virtual virtual ~CShortStopLoss(void) {}; // the same applies to the destructor virtual bool SlippageCheck() = NULL; // checking slippage for the open order virtual bool SpreadMonitor(int intSL)= NULL; // spread control };
Considerando que a linguagem MQL5 oferece um tipo de dado especial, interface, para entidades genéricas, cuja declaração é mais compacta e simples, optaremos por usá-la, pois não há diferença de funcionalidade para o nosso caso. Na essência, interface também é uma classe que não pode conter membros/campos, nem ter construtor ou destrutor. Todos os métodos declarados em uma interface são puramente virtuais, mesmo sem definição explícita, o que torna seu uso mais elegante e compacto. A implementação com uma entidade genérica, como uma interface, ficará assim:
interface IShortStopLoss { virtual bool SlippageCheck(); // checking the slippage for an open order virtual bool SpreadMonitor(int intSL); // spread control };
Agora que definimos o tipo de entidade genérica a ser usado, vamos implementar toda a funcionalidade necessária dos métodos já declarados na interface para a nossa classe herdeira.
Controle de slippage em ordens abertas
Primeiramente, para implementar o método SlippageCheck(void), precisaremos atualizar os dados do símbolo no qual nosso gráfico está executando. Faremos isso utilizando o método Refresh() da instância da nossa classe CSymbolInfo, para atualizar todos os campos que caracterizam o instrumento, conforme o exemplo abaixo:
r_symbol.Refresh(); // update symbol data
Vale destacar que o método Refresh() atualiza todos os dados dos campos da classe CSymbolInfo, ao contrário do método semelhante RefreshRates(void) da mesma classe, que atualiza apenas os dados sobre os preços atuais do símbolo especificado. O método Refresh() será chamado a cada tick nesta implementação, garantindo o uso de informações atualizadas em cada iteração do nosso Expert Advisor (EA).
Na área de escopo das variáveis do método Refresh(), precisaremos de variáveis dinâmicas para armazenar as propriedades das posições abertas ao iterar sobre todas elas, para calcular o possível slippage na abertura. As informações sobre as posições abertas serão armazenadas nesta seção do método da seguinte forma:
double PriceClose = 0, // close price for the order PriceStopLoss = 0, // stop loss price for the order PriceOpen = 0, // open price for the order LotsOrder = 0, // order lot volume ProfitCur = 0; // current order profit ulong Ticket = 0; // order ticket string Symbl; // symbol
Para obter os dados sobre o valor do tick em caso de perda, utilizaremos o método TickValueLoss() da instância da classe CSymbolInfo, declarado dentro da nossa classe RiskManagerAlgo. O valor retornado indica quanto o saldo da conta mudará se o preço variar um ponto mínimo com um lote padrão. Esse valor será utilizado posteriormente para calcular a perda potencial com base nos preços efetivamente abertos da posição. O termo "potencial" é empregado aqui porque o método operará a cada tick, e logo após a abertura da posição, ou seja, no próximo tick recebido, poderemos verificar quanto se pode perder na operação, mesmo que o preço ainda esteja próximo do preço de abertura, e não do preço de stop-loss.
double lot_cost = r_symbol.TickValueLoss(); // get tick value bool ticket_sc = 0; // variable for successful closing
Também declararemos aqui uma variável necessária para verificar a execução da ordem de fechamento da posição aberta, caso o cálculo indique que a posição precisa ser fechada devido ao slippage. Essa variável é do tipo booleano bool com o nome ticket_sc.
Agora podemos avançar para a iteração de todas as posições abertas no método de controle de slippage. A iteração das posições abertas será feita organizando um loop do tipo for, limitado pelo número de posições abertas no terminal. Para obter o número de posições abertas, usaremos a função predefinida do terminal PositionsTotal(). As posições serão selecionadas pelo índice usando o método SelectByIndex() da classe padrão do terminal CPositionInfo.
r_position.SelectByIndex(i)
Após a seleção da posição, podemos começar a solicitar as propriedades dessa posição com o mesmo padrão da classe CPositionInfo, mas primeiro precisamos verificar se a posição selecionada corresponde ao símbolo no qual o EA está executando. Isso pode ser feito com o seguinte código dentro do loop de iteração das nossas posições:
Symbl = r_position.Symbol(); // get the symbol if(Symbl==Symbol()) // check if it's the right symbol
E somente após confirmarmos que a posição selecionada pelo índice pertence ao nosso gráfico, podemos prosseguir com a solicitação de outras propriedades para verificar a posição aberta. As solicitações subsequentes das propriedades da posição também serão feitas usando a instância da classe padrão do terminal CPositionInfo da seguinte forma:
PriceStopLoss = r_position.StopLoss(); // remember its stop loss PriceOpen = r_position.PriceOpen(); // remember its open price ProfitCur = r_position.Profit(); // remember financial result LotsOrder = r_position.Volume(); // remember order lot volume Ticket = r_position.Ticket();
Vale destacar que a verificação pode ser feita não apenas pelo símbolo, mas também pelo número magic da estrutura MqlTradeRequest, usada ao abrir a posição selecionada. Essa abordagem é frequentemente utilizada para separar operações realizadas usando diferentes estratégias em uma mesma conta. Não utilizaremos esse método, pois preferimos usar contas separadas para cada estratégia, o que facilita a análise e otimiza o uso dos recursos computacionais. Contudo, podem haver outras abordagens, que vocês podem compartilhar nos comentários deste artigo. Agora, seguimos com a descrição da implementação do nosso método.
Em nosso método, será realizado o fechamento de posições, considerando que uma posição de compra é fechada com uma venda ao preço Bid, enquanto uma posição de venda é fechada ao preço Ask. Para isso, implementaremos a lógica para obter o preço de fechamento com base no tipo da posição aberta:
int dir = r_position.Type(); // define order type if(dir == POSITION_TYPE_BUY) // if it is Buy { PriceClose = r_symbol.Bid(); // close at Bid } if(dir == POSITION_TYPE_SELL) // if it is Sell { PriceClose = r_symbol.Ask(); // close at Ask }
A lógica de fechamento parcial de posições não será abordada aqui, embora o terminal permita isso tecnicamente, dependendo do tipo de conta na corretora, mas essa abordagem não se alinha com a lógica do nosso método, então não a aplicaremos.
Depois de confirmar que a posição selecionada é a desejada e obter todas as características necessárias, prosseguiremos para calcular o risco efetivo da posição. Primeiro, devemos calcular o tamanho do stop em pontos mínimos com base no preço de abertura real, para depois compará-lo com o planejado inicialmente.
Calcularemos o tamanho como a diferença absoluta entre o preço de abertura e o stop-loss da posição. Faremos isso usando a função predefinida do terminal MathAbs(). Para converter o valor decimal em um valor inteiro em pontos, dividiremos o valor obtido por MathAbs() pelo valor de um ponto, que é um valor decimal. O valor de um ponto será obtido usando o método Point() da instância da nossa classe padrão do terminal CPositionInfo.
int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop
Para obter o valor potencial de perda efetiva da posição selecionada, multiplicaremos o tamanho do stop em pontos pelo tamanho da posição em lotes e pelo valor de um tick do instrumento da posição da seguinte forma:
double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level
Organizaremos a verificação desse valor em relação ao desvio de risco da operação especificado pelo usuário, armazenado na variável slippfits. Se o valor ultrapassar os limites deste intervalo, fechamos a posição selecionada:
if( potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) && // if the resulting stop exceeds risk per trade given the threshold value ProfitCur<0 && // and the order is at a loss PriceStopLoss != 0 // if stop loss is not set, don't touch )
Neste conjunto de condições, também adicionamos duas verificações adicionais com a seguinte lógica para situações de trading:
Primeiro, a condição "ProfitCur < 0" foi incluída para garantir que o controle de slippage seja ativado apenas quando a posição estiver na zona de perda. Isso está relacionado às condições da estratégia de trading. O slippage normalmente ocorre em momentos de alta volatilidade, e a operação é aberta com slippage na direção do take-profit, o que aumenta o stop e reduz o take-profit. Isso diminui a relação risco/retorno esperada da operação e aumenta a perda potencial em relação ao planejado, mas, ao mesmo tempo, aumenta a probabilidade de alcançar o take, já que o impulso que "puxou" nossa posição devido ao slippage provavelmente continuará no momento. Essa condição implica que fecharemos a posição apenas se o impulso que causou o slippage terminar antes de atingir o take e a operação retornar para a zona de perda.
A segunda condição, "PriceStopLoss != 0", é necessária para implementar a lógica de que, se o trader não definiu um stop-loss, não fecharemos essa posição devido ao risco ilimitado associado a ela. Isso significa que, ao abrir uma posição, o trader entende que essa posição pode, potencialmente, consumir todo o risco planejado para o dia, caso o preço se mova contra ele. Isso pode resultar na falta de limites para outros instrumentos planejados para negociação no dia, que poderiam ser positivos e gerar lucro, enquanto a posição sem stop-loss impediria essas entradas. Cada trader decide por si só se deseja ativar essa condição ou não, com base na sua estratégia de trading. No entanto, em nossa implementação, não negociaremos múltiplos instrumentos simultaneamente, então não fecharemos a posição se não houver um preço de stop-loss definido.
Se todas as condições necessárias para identificar o slippage na posição forem atendidas, fecharemos a posição usando o método PositionClose() da classe padrão de código aberto CTrade, declarado na nossa classe base RiskManagerBase. Passamos como parâmetro de entrada o número do ticket da posição previamente armazenado para o fechamento, e o resultado da função de fechamento é salvo na variável ticket_sc, para monitorar a execução da ordem.
ticket_sc = r_trade.PositionClose(Ticket); // close order
O método geral será descrito da seguinte forma:
//+------------------------------------------------------------------+ //| SlippageCheck | //+------------------------------------------------------------------+ bool RiskManagerAlgo::SlippageCheck(void) override { r_symbol.Refresh(); // update symbol data double PriceClose = 0, // close price for the order PriceStopLoss = 0, // stop loss price for the order PriceOpen = 0, // open price for the order LotsOrder = 0, // order lot volume ProfitCur = 0; // current order profit ulong Ticket = 0; // order ticket string Symbl; // symbol double lot_cost = r_symbol.TickValueLoss(); // get tick value bool ticket_sc = 0; // variable for successful closing for(int i = PositionsTotal(); i>=0; i--) // start loop through orders { if(r_position.SelectByIndex(i)) { Symbl = r_position.Symbol(); // get the symbol if(Symbl==Symbol()) // check if it's the right symbol { PriceStopLoss = r_position.StopLoss(); // remember its stop loss PriceOpen = r_position.PriceOpen(); // remember its open price ProfitCur = r_position.Profit(); // remember financial result LotsOrder = r_position.Volume(); // remember order lot volume Ticket = r_position.Ticket(); int dir = r_position.Type(); // define order type if(dir == POSITION_TYPE_BUY) // if it is Buy { PriceClose = r_symbol.Bid(); // close at Bid } if(dir == POSITION_TYPE_SELL) // if it is Sell { PriceClose = r_symbol.Ask(); // close at Ask } if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL) { int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level if( potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) && // if the resulting stop exceeds risk per trade given the threshold value ProfitCur<0 && // and the order is at a loss PriceStopLoss != 0 // if stop loss is not set, don't touch ) { ticket_sc = r_trade.PositionClose(Ticket); // close order Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal)); // Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits)); // Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal)); // Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder)); // Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord)); // if(!ticket_sc) { Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); // output to log } else { Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log } continue; } } } } } return(ticket_sc); } //+------------------------------------------------------------------+
Com isso, concluímos a sobrescrita do método de controle de slippage e seguimos para a descrição do método de controle do tamanho do spread antes de abrir uma nova posição.
Controle do spread na abertura de posições
Na implementação do método SpreadMonitor(), o controle do spread consistirá em comparar o spread atual imediatamente antes da execução da ordem com o stop calculado/técnico, que é passado como um parâmetro inteiro do método. A função retornará true se o tamanho do spread atual estiver dentro do intervalo aceitável definido pelo usuário. Caso o tamanho do spread exceda esse intervalo, o método retornará false.
O resultado da função será armazenado em uma variável lógica do tipo bool, que será inicializada como true por padrão:
bool SpreadAllowed = true;
O valor do spread atual do instrumento será obtido com o método Spread() da classe CSymbolInfo, conforme mostrado a seguir:
int SpreadCurrent = r_symbol.Spread();
A verificação da condição de conformidade com o intervalo especificado pelo usuário será feita por meio de uma comparação lógica, como a seguir:
if(SpreadCurrent>intSL*spreadfits)
Isso significa que, se o spread atual do instrumento for maior que o produto do stop necessário pelo coeficiente especificado pelo usuário, o método deverá retornar false, o que impedirá a abertura da posição com o spread atual até o próximo tick. De forma geral, o método será descrito assim:
//+------------------------------------------------------------------+ //| SpreadMonitor | //+------------------------------------------------------------------+ bool RiskManagerAlgo::SpreadMonitor(int intSL) { //--- spread control bool SpreadAllowed = true; // allow spread trading and check ratio further int SpreadCurrent = r_symbol.Spread(); // current spread values if(SpreadCurrent>intSL*spreadfits) // if the current spread is greater than the stop and the coefficient { SpreadAllowed = false; // prohibit trading Print(__FUNCTION__+IntegerToString(__LINE__)+ ". Spread is to high! Spread:"+ IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));// notify } return SpreadAllowed; // return result } //+------------------------------------------------------------------+
Ao trabalhar com este método, é importante considerar que, se a condição de spread for muito restritiva, o EA não abrirá posições, registrando constantemente mensagens no log do EA. Normalmente, usa-se um valor de coeficiente de pelo menos 2, o que significa que, se o spread for metade do stop, ou se espera por um spread menor ou se desiste de entrar com um stop-loss tão curto, pois quanto mais próximo o stop estiver do tamanho do spread, maior a probabilidade de a posição resultar em prejuízo.
Implementação da interface
A interface que declaramos será o primeiro ancestral da nossa classe base, pois o MQL5 não suporta herança múltipla. No entanto, isso não nos limita, pois podemos organizar uma estrutura de herança sequencial para nosso projeto.
Para isso, precisamos complementar nossa classe base RiskManagerBase herdando o interface IShortStopLoss da seguinte forma:
//+------------------------------------------------------------------+ //| RiskManagerBase | //+------------------------------------------------------------------+ class RiskManagerBase:IShortStopLoss // the purpose of the class is to control risk in terminal
Essa abordagem permitirá transferir a funcionalidade necessária para a classe derivada RiskManagerAlgo. Neste caso, o nível de acesso da herança não é relevante, já que nosso interface contém apenas funções virtuais puras, sem campos, construtor ou destrutor.
A estrutura final de herança da nossa classe personalizada RiskManagerAlgo, com a exibição da encapsulação de métodos públicos para garantir a funcionalidade completa, está ilustrada na Figura 1.
Figura 1. Hierarquia da estrutura de herança da classe RiskManagerAlgo
Agora, antes de montar nosso algoritmo, só precisamos implementar o bloco de decisão para testar a funcionalidade do controle de risco algorítmico descrito.
Implementação do bloco de trading
No artigo anterior, Gerenciador de risco para operar manualmente, usamos uma entidade simples TradeModel como bloco de trading para processar de maneira elementar as entradas obtidas a partir de fractais. Como este artigo foca em trading algorítmico, vamos automatizar a ferramenta de decisão baseada em fractais. Ela será baseada na mesma lógica, mas agora será completamente implementada em código, sem a necessidade de gerar sinais manualmente. Como um bônus adicional, poderemos testar a solução em um intervalo maior de dados históricos, já que não precisaremos criar manualmente as entradas necessárias.
Declararemos a classe CFractalsSignal, que será responsável por gerar sinais baseados em fractais. A lógica permanece a mesma: se o preço romper o fractal superior no gráfico diário, o EA compra; se o preço romper o fractal inferior também no gráfico diário, surge um sinal de venda. As operações serão encerradas com base no princípio de trading intradiário, no final do dia de negociação em que foram abertas.
Em primeiro lugar, nossa classe CFractalsSignal conterá um campo que armazena informações sobre o timeframe usado para capturar os fractais. Isso é necessário para diferenciar o timeframe de onde o EA coletará informações sobre os fractais do timeframe no qual o próprio EA será executado, facilitando o uso. Declararemos a variável de enumeração ENUM_TIMEFRAMES da seguinte forma:
ENUM_TIMEFRAMES TF; // timeframe used
Em seguida, declararemos uma variável ponteiro da classe padrão do terminal de código aberto para trabalhar com o indicador técnico CiFractals, no qual todas as funções necessárias já estão convenientemente implementadas, poupando-nos de ter que escrever tudo de novo:
CiFractals *cFractals; // fractals
Também precisaremos armazenar dados sobre os sinais, com a possibilidade de rastrear sua execução pelo EA. Para isso, usaremos a mesma estrutura personalizada TradeInputs, que utilizamos no artigo anterior. Da última vez, criamos essa estrutura manualmente, mas agora ela será gerada automaticamente pela classe CFractalsSignal:
//+------------------------------------------------------------------+ //| TradeInputs | //+------------------------------------------------------------------+ struct TradeInputs { string symbol; // symbol ENUM_POSITION_TYPE direction; // direction double price; // price datetime tradedate; // date bool done; // trigger flag };
Declararemos as variáveis internas da nossa estrutura separadamente para os sinais de compra e venda, permitindo rastreá-los simultaneamente, já que não sabemos de antemão qual preço será acionado primeiro:
TradeInputs fract_Up, fract_Dn; // current signal
Precisamos apenas declarar as variáveis que armazenarão os valores atuais recebidos da classe CiFractals para obter dados sobre novos fractais formados no gráfico diário.
Para fornecer a funcionalidade necessária, o acesso público da classe CFractalsSignal incluirá vários métodos que monitoram os recentes rompimentos de preço fractais, fornecem sinais para abrir posições e verificam o sucesso na execução desses sinais.
O método para atualizar o estado dos dados da classe será chamado Process(). Ele não retornará nada nem receberá parâmetros, atualizando simplesmente o estado dos dados a cada novo tick. Os métodos para obter os sinais de compra e venda serão chamados BuySignal() e SellSignal(). Eles não receberão parâmetros, mas retornarão um valor do tipo bool, indicando se é necessário abrir uma posição na direção correspondente. Os métodos BuyDone() e SellDone() precisarão ser chamados após a confirmação da abertura bem-sucedida de uma posição pelo servidor da corretora. A estrutura geral da nossa classe será assim:
//+------------------------------------------------------------------+ //| CFractalsSignal | //+------------------------------------------------------------------+ class CFractalsSignal { protected: ENUM_TIMEFRAMES TF; // timeframe used CiFractals *cFractals; // fractals TradeInputs fract_Up, fract_Dn; // current signal double FrUp; // upper fractals double FrDn; // lower fractals public: CFractalsSignal(void); // constructor ~CFractalsSignal(void); // destructor void Process(); // method to start updates bool BuySignal(); // buy signal bool SellSignal(); // sell signal void BuyDone(); // buy done void SellDone(); // sell done };
No construtor da classe, inicializaremos o campo do timeframe usado TF com o valor do intervalo diário PERIOD_D1, já que os níveis do gráfico diário são suficientemente fortes para gerar o impulso necessário para alcançar o take-profit e são mais frequentes que os níveis mais fortes dos gráficos semanal e mensal. Aqui, deixaremos a possibilidade de cada usuário testar timeframes menores, mas focaremos nos diários. Também criaremos instâncias do objeto da classe para trabalhar com o indicador fractal e inicializaremos todos os campos necessários por padrão, na seguinte sequência:
//+------------------------------------------------------------------+ //| CFractalsSignal | //+------------------------------------------------------------------+ CFractalsSignal::CFractalsSignal(void) { TF = PERIOD_D1; // timeframe used //--- fractal class cFractals=new CiFractals(); // created fractal instance if(CheckPointer(cFractals)==POINTER_INVALID || // if instance not created OR !cFractals.Create(Symbol(),TF)) // variant not created Print("INIT_FAILED"); // don't proceed cFractals.BufferResize(4); // resize fractal buffer cFractals.Refresh(); // update //--- FrUp = EMPTY_VALUE; // leveled upper at start FrDn = EMPTY_VALUE; // leveled lower at start fract_Up.done = true; // fract_Up.price = EMPTY_VALUE; // fract_Dn.done = true; // fract_Dn.price = EMPTY_VALUE; // }
No destrutor, apenas liberaremos a memória do objeto do indicador fractal que criamos no construtor:
//+------------------------------------------------------------------+ //| ~CFractalsSignal | //+------------------------------------------------------------------+ CFractalsSignal::~CFractalsSignal(void) { //--- if(CheckPointer(cFractals)!=POINTER_INVALID) // if instance was created, delete cFractals; // delete }
No método de atualização dos dados, simplesmente chamaremos o método Refresh() da instância da classe CiFractals para atualizar os preços fractais, método herdado de uma das classes parentais CIndicator.
//+------------------------------------------------------------------+ //| Process | //+------------------------------------------------------------------+ void CFractalsSignal::Process(void) { //--- cFractals.Refresh(); // update fractals }
Aqui, é importante mencionar que existe uma possibilidade de otimização adicional desta abordagem, considerando que não é necessário atualizar esses dados a cada tick, pois os níveis de fractais são provenientes do gráfico diário. Poderíamos implementar um método de evento que detecte o surgimento de um novo candle diário e atualize os dados apenas quando ele for criado. No entanto, manteremos essa implementação, já que a carga adicional no sistema não é significativa, e a implementação de funcionalidades extras exigiria esforço adicional, com ganhos de desempenho relativamente modestos.
Ao descrever o método que verifica a possibilidade de abrir uma posição de compra BuySignal(void), começaremos solicitando a última cotação Ask da seguinte forma:
double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); // Buy price
Em seguida, precisaremos solicitar o valor atualizado do fractal superior usando o método Upper() da instância da classe CiFractals, passando o índice da barra necessária na série temporal como parâmetro:
FrUp=cFractals.Upper(3); // request the current value
Passamos o valor `3` para este método porque utilizaremos apenas as quebras fractais completamente formadas. Como nas séries temporais o buffer é contado do mais recente (0) para os mais antigos, o valor "três" no gráfico diário corresponde ao dia antes de ontem, garantindo que não consideremos quebras fractais que possam surgir temporariamente e desaparecer se o preço revisitar o high ou low no mesmo dia.
Agora, faremos uma verificação lógica para atualizar o fractal atual se o preço do último fractal válido no gráfico diário tiver mudado. Compararemos o valor atual do indicador fractal, atualizado na variável FrUp, com o último valor registrado do fractal superior armazenado no campo price da nossa estrutura personalizada TradeInputs. Para garantir que o campo price armazene sempre o valor da última cotação válida e não seja "zerado" na ausência de dados retornados pelo indicador (caso não haja uma quebra fractal), incluiremos uma verificação adicional: FrUp != EMPTY_VALUE. A combinação dessas duas condições permitirá que atualizemos apenas valores significativos (diferentes de zero, onde no indicador o valor EMPTY_VALUE corresponde à ausência de um nível fractal), evitando sobrescrever a variável com valores vazios. De forma geral, essas verificações serão assim:
if(FrUp != fract_Up.price && // if the data has been updated FrUp != EMPTY_VALUE) // skip empty value
No final do método, a verificação lógica para o sinal de compra será na seguinte sequência:
if(fract_Up.price != EMPTY_VALUE && // skip zero values ask >= fract_Up.price && // if the buy price is greater than or equal to the fractal fract_Up.done == false) // the signal has not been processed yet { return true; // generate a signal to process }
Neste bloco, a verificação começa conferindo se o valor do último fractal superior fract_Up no campo price é nulo, o que pode acontecer no primeiro início do EA, após a inicialização dessa variável no construtor da classe. O próximo passo é verificar se o preço de compra atual no mercado rompeu o valor do último fractal diário registrado, na forma ask >= fract_Up.price, que é a principal condição lógica deste método. Finalmente, precisamos garantir que o nível fractal atual não tenha sido processado mais de uma vez. O sentido aqui é que os sinais baseados em quebras fractais vêm do gráfico diário, e, se o preço de compra atual no mercado atingir o valor desejado, devemos processar esse sinal apenas uma vez por dia, já que nosso trading é intradiário, mas posicional, sem acumular várias posições abertas simultaneamente. Quando todas as três condições forem atendidas ao mesmo tempo, nosso método deve retornar true para que o EA processe o sinal.
A implementação completa do método com a sequência lógica será assim:
//+------------------------------------------------------------------+ //| BuySignal | //+------------------------------------------------------------------+ bool CFractalsSignal::BuySignal(void) { double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); // Buy price //--- check fractals update FrUp=cFractals.Upper(3); // request the current value if(FrUp != fract_Up.price && // if the data has been updated FrUp != EMPTY_VALUE) // skip empty value { fract_Up.price = FrUp; // process the new fractal fract_Up.done = false; // not processed } //--- check the signal if(fract_Up.price != EMPTY_VALUE && // skip zero values ask >= fract_Up.price && // if the buy price is greater than or equal to the fractal fract_Up.done == false) // the signal has not been processed yet { return true; // generate a signal to process } return false; // otherwise false }
Como já mencionado anteriormente, o método que recebe o sinal de compra deve funcionar em conjunto com o método que verifica a execução desse sinal pelo servidor da corretora. O método que será chamado para processar o sinal de compra será bem compacto:
//+------------------------------------------------------------------+ //| BuyDone | //+------------------------------------------------------------------+ void CFractalsSignal::BuyDone(void) { fract_Up.done = true; // processed }
A lógica é muito simples: ao chamar este método público, definiremos um sinalizador de execução bem-sucedida no campo done do último sinal da estrutura fract_Up. Este método será chamado no código principal do EA somente quando a verificação de abertura bem-sucedida do pedido for confirmada pelo servidor da corretora.
A lógica do método correspondente para venda será semelhante. A diferença estará em que agora solicitaremos o preço Bid em vez do Ask. A condição para o preço atual também será verificada para ser menor que a quebra fractal para venda, em vez de maior.
O método de sinal de venda será estruturado com a mesma lógica, ajustada para a direção de venda, da seguinte forma:
//+------------------------------------------------------------------+ //| SellSignal | //+------------------------------------------------------------------+ bool CFractalsSignal::SellSignal(void) { double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); // bid price //--- check fractals update FrDn=cFractals.Lower(3); // request the current value if(FrDn != EMPTY_VALUE && // skip empty value FrDn != fract_Dn.price) // if the data has been updated { fract_Dn.price = FrDn; // process the new fractal fract_Dn.done = false; // not processed } //--- check the signal if(fract_Dn.price != EMPTY_VALUE && // skip empty value bid <= fract_Dn.price && // if the ask price is less than or equal to the fractal AND fract_Dn.done == false) // signal has not been processed { return true; // generate a signal to process } return false; // otherwise false }
A execução do sinal de venda seguirá a mesma lógica que a da compra, mas o campo done será atualizado na estrutura fract_Dn, que se refere ao último fractal relevante para venda.
//+------------------------------------------------------------------+ //| SellDone | //+------------------------------------------------------------------+ void CFractalsSignal::SellDone(void) { fract_Dn.done = true; // processed } //+------------------------------------------------------------------+
Com isso, a implementação do método para gerar entradas com base nas quebras fractais diárias está concluída, e podemos passar para a montagem geral do projeto.
Compilação e teste do projeto
Iniciaremos a montagem do projeto incluindo todos os arquivos mencionados acima no início do arquivo principal do projeto com a diretiva de pré-processador #include. Os arquivos <RiskManagerAlgo.mqh>, <TradeModel.mqh> e <CFractalsSignal.mqh> são os nossos arquivos de classes personalizadas discutidos anteriormente. As duas outras linhas, <Indicators\BillWilliams.mqh> e <Trade\Trade.mqh>, referem-se aos arquivos padrão do terminal, de código aberto, usados para lidar com fractais e operações de trading, respectivamente.
#include <RiskManagerAlgo.mqh> #include <Indicators\BillWilliams.mqh> #include <Trade\Trade.mqh> #include <TradeModel.mqh> #include <CFractalsSignal.mqh>
Para configurar o método de controle de slippage, adicionaremos uma variável inteira adicional do tipo int na classe de memória input, permitindo que o usuário defina o valor aceitável do stop-loss em pontos mínimos do instrumento:
input group "RiskManagerAlgoExpert" input int inp_sl_in_int = 2000; // inp_sl_in_int - a stop loss level for a separate trade
Em integrações mais detalhadas ou implementações completamente automáticas, seria preferível transmitir este parâmetro por meio do retorno do valor do stop da classe responsável por definir o stop-loss técnico ou por uma classe que calcula o stop com base na volatilidade. Nesta implementação, deixaremos ao usuário a flexibilidade de modificar essa configuração conforme sua estratégia específica.
Agora vamos declarar os ponteiros necessários para as classes de gerenciador de riscos, gerenciamento de posições e fractais da seguinte forma:
RiskManagerAlgo *RMA; // risk manager CTrade *cTrade; // trade CFractalsSignal *cFract; // fractal
Inicializaremos os ponteiros na função de manipulação de eventos de inicialização do nosso EA, OnInit():
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- RMA = new RiskManagerAlgo(); // algorithmic risk manager //--- cFract =new CFractalsSignal(); // fractal signal //--- trade class cTrade=new CTrade(); // create trade instance if(CheckPointer(cTrade)==POINTER_INVALID) // if instance not created, { Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!"); // notify } cTrade.SetTypeFillingBySymbol(Symbol()); // fill type for the symbol cTrade.SetDeviationInPoints(1000); // deviation cTrade.SetExpertMagicNumber(123); // magic number cTrade.SetAsyncMode(false); // asynchronous method //--- return(INIT_SUCCEEDED); }
Ao configurar o objeto CTrade, especificaremos o símbolo do instrumento atual no método SetTypeFillingBySymbol(), usando como parâmetro o símbolo no qual o EA será executado. O símbolo atual do instrumento será retornado pelo método predefinido Symbol() do terminal. Como margem de segurança, especificaremos um desvio aceitável no método SetDeviationInPoints(). Esse parâmetro não é tão crítico para nosso estudo, então o deixaremos fixo no código em vez de torná-lo um parâmetro de entrada. Também definiremos um número magic fixo para todas as posições abertas pelo EA.
No destrutor, implementaremos a exclusão do objeto e a liberação da memória referenciada pelo ponteiro, caso seja válido:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- if(CheckPointer(cTrade)!=POINTER_INVALID) // if there is an instance, { delete cTrade; // delete } //--- if(CheckPointer(cFract)!=POINTER_INVALID) // if an instance is found, { delete cFract; // delete } //--- if(CheckPointer(RMA)!=POINTER_INVALID) // if an instance is found, { delete RMA; // delete } }
Agora, descreveremos o corpo principal do nosso EA no evento de entrada de um novo tick, OnTick(). Primeiro, precisamos executar o método principal de controle de eventos ContoMonitor() da classe base RiskManagerBase, que não foi sobrescrito na classe derivada, a partir da instância da classe derivada, da seguinte forma:
RMA.ContoMonitor(); // run the risk manager
Em seguida, chamaremos o método de controle de slippage SlippageCheck(), que, como mencionado anteriormente, será executado a cada novo tick nesta implementação, verificando as posições abertas para conformidade com os valores de risco planejados e o valor real em relação ao stop-loss definido:
RMA.SlippageCheck(); // check slippage
É importante observar que este método, por ser uma demonstração simples do gerenciador de riscos baseado em fractais, não configurará stop-losses detalhados. Ele simplesmente fechará posições ao final do dia de negociação, processando todas as operações passadas para ele. Para que esse método funcione de forma completa na sua própria implementação, você pode enviar ordens ao servidor da corretora apenas se tiverem um valor de stop-loss diferente de zero.
A seguir, atualizaremos os dados do indicador de fractais por meio do método público Process() da nossa classe personalizada CFractalsSignal, com a seguinte chamada:
cFract.Process(); // start the fractal process
Agora que todos os métodos relacionados ao estado de dados das diferentes classes foram processados no código, podemos seguir para o bloco de monitoramento de sinais de entrada e emissão de ordens. A verificação dos sinais de compra e venda será feita separadamente, assim como os métodos correspondentes da nossa classe de decisão CFractalsSignal. Primeiro, descreveremos a verificação de sinais de compra com duas condições, como no código abaixo:
if(cFract.BuySignal() && RMA.SpreadMonitor(inp_sl_in_int)) // if there is a buy signal
Primeiro, verificamos se há um sinal de compra usando o método BuySignal() da instância da classe CFractalsSignal, e, se houver, verificamos simultaneamente se há permissão do gerenciador de riscos para a conformidade do spread com o valor permitido pelo usuário por meio do método SpreadMonitor(). Como único parâmetro no método SpreadMonitor(), passamos o parâmetro de entrada do usuário, inp_sl_in_int.
Se ambas as condições descritas forem atendidas, passamos para a execução das ordens na seguinte estrutura lógica simplificada:
if(cTrade.Buy(0.1)) // if Buy executed, { cFract.BuyDone(); // the signal has been processed Print("Buy has been done"); // notify } else // if Buy not executed, { Print("Error: buy"); // notify }
Emitimos a ordem usando o método Buy() da instância da classe CTrade, passando o valor do lote como 0,1. Para uma avaliação mais objetiva do desempenho do gerenciador de riscos, não alteraremos este valor, para "suavizar" a estatística com base no volume. Isso significa que todas as entradas terão o mesmo peso estatístico no desempenho do nosso EA.
Se o método Buy() funcionar corretamente, ou seja, se a corretora responder positivamente e a operação for aberta, chamamos imediatamente o método BuyDone(), para informar à nossa classe CFractalsSignal que o sinal recebido foi processado com sucesso, e não é necessário gerar outro sinal para o mesmo preço. Se a execução da ordem de compra falhar, registramos essa informação no log do EA e não chamamos o método de sucesso do sinal, permitindo tentativas futuras de abertura.
Aplicamos a mesma lógica para operações de venda – apenas chamamos os métodos correspondentes à venda na sequência do código.
O bloco de fechamento de ordens no final do dia será copiado sem alterações do artigo anterior:
MqlDateTime time_curr; // current time structure TimeCurrent(time_curr); // request current time if(time_curr.hour >= 23) // if end of day { RMA.AllOrdersClose(); // close all positions }
A função principal deste código é fechar todas as posições abertas às 23 horas, com base no último horário conhecido do servidor, pois estamos implementando aqui uma lógica de trading intradiário, sem posições overnight. A lógica desse código é descrita em mais detalhes no último artigo. Se o trader desejar manter posições durante a noite, basta comentar ou remover esse bloco de código no EA.
Também devemos exibir na tela o estado atual dos dados do gerenciador de riscos usando a função predefinida Comment() do terminal, passando o método Message() da classe de gerenciamento de riscos:
Comment(RMA.Message()); // display the data state in a commentO código final para o evento de novo tick será assim:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { RMA.ContoMonitor(); // run the risk manager RMA.SlippageCheck(); // check slippage cFract.Process(); // start the fractal process if(cFract.BuySignal() && RMA.SpreadMonitor(inp_sl_in_int)) // if there is a buy signal { if(cTrade.Buy(0.1)) // if Buy executed, { cFract.BuyDone(); // the signal has been processed Print("Buy has been done"); // notify } else // if Buy not executed, { Print("Error: buy"); // notify } } if(cFract.SellSignal()) // if there is a sell signal { if(cTrade.Sell(0.1)) // if sell executed, { cFract.SellDone(); // the signal has been processed Print("Sell has been done"); // notify } else // if sell failed, { Print("Error: sell"); // notify } } MqlDateTime time_curr; // current time structure TimeCurrent(time_curr); // request current time if(time_curr.hour >= 23) // if end of day { RMA.AllOrdersClose(); // close all positions } Comment(RMA.Message()); // display the data state in a comment } //+------------------------------------------------------------------+
Agora podemos compilar o projeto e testá-lo em dados históricos. Para exemplificar o teste, usaremos o par USDJPY e simularemos o ano de 2023 no otimizador de estratégias com as seguintes configurações de entrada (veja a Tabela 2):
Nº | Nome da Configuração | Valor da Configuração |
---|---|---|
1 | EA | RiskManagerAlgo.ex5 |
2 | Símbolo | USDJPY |
3 | Período do Gráfico | M15 |
4 | Intervalo | 2023.01.01 - 2023.12.31 |
5 | Forward | Não |
6 | Delays | Sem atrasos, execução ideal |
7 | Modelagem | Todos os ticks |
8 | Depósito Inicial | 10 000 USD |
9 | Alavancagem | 1:100 |
10 | Otimização | Lenta (varredura completa de parâmetros) |
Tabela 1. Configurações do testador de estratégias para o EA RiskManagerAlgo
Selecionamos os parâmetros de otimização do testador de estratégias com passos mínimos, para reduzir o tempo de teste, mas mantendo a capacidade de observar a relação mencionada anteriormente: que o gerenciador de riscos pode melhorar os resultados até mesmo de estratégias já lucrativas. Os parâmetros de entrada para a otimização no testador de estratégias do terminal MetaTrader 5 são apresentados na Tabela 2.
Nº | Nome do Parâmetro | Início | Passo | Parada |
---|---|---|---|---|
1 | inp_riskperday | 0.1 | 0.5 | 1 |
2 | inp_riskperweek | 0.5 | 0.5 | 3 |
3 | inp_riskpermonth | 2 | 1 | 8 |
4 | inp_plandayprofit | 0.1 | 0.5 | 3 |
5 | dayProfitControl | false | - | true |
Tabela 2. Parâmetros do otimizador de estratégias para o EA RiskManagerAlgo
Os parâmetros de otimização não incluem aqueles que não dependem diretamente da eficácia da estratégia ou que não fazem sentido modelar no testador. Por exemplo, inp_slippfits dependerá principalmente da "qualidade" (não confundir com "frequência") da execução das ordens pela corretora, em vez das nossas entradas. O parâmetro inp_spreadfits está diretamente relacionado ao tamanho do spread, que varia conforme vários fatores, incluindo, mas não se limitando, ao tipo de conta da corretora e ao horário de divulgação de notícias importantes. Esses parâmetros podem ser ajustados individualmente com base nas condições específicas do broker preferido.
Os resultados da otimização são apresentados nas Figuras 2 e 3.
Figura 2. Gráfico dos resultados da otimização do EA RiskManagerAlgo
O gráfico de resultados da otimização mostra que a maioria das simulações da estratégia resultou em um retorno com expectativa matemática positiva. Isso ocorre devido à lógica da estratégia, onde grandes volumes de posições de participantes do mercado se concentram em torno de fortes níveis fractais, e o teste desses níveis pelo preço provoca uma maior atividade de mercado, gerando impulsos no instrumento.
Para validar a existência desses impulsos na negociação baseada em níveis fractais, podemos comparar a melhor e a pior iteração do conjunto de otimizações com os parâmetros mencionados. O nosso gerenciador de riscos é projetado exatamente para padronizar esses impulsos em relação aos riscos ao entrar em uma posição. Para entender melhor a função do gerenciador de riscos na padronização do risco em relação ao impulso do mercado, observemos a relação entre o risco diário e a meta de lucro diário na Figura 3.
Figura 3. Diagrama da dependência dos parâmetros de risco diário e meta de lucro diário
Na Figura 3, é visível um ponto de inflexão no parâmetro de risco diário, onde a eficiência da estratégia de rompimento de fractais aumenta inicialmente com o aumento do risco, mas depois começa a diminuir. Este é o ponto extremo (ponto de inflexão) da nossa função para esses dois parâmetros. A existência desse ponto na nossa modelagem, onde o aumento do risco diário deixa de melhorar o lucro e começa a reduzi-lo, comprova que o impulso do mercado se torna insuficiente em relação aos riscos incluídos na nossa modelagem. Há uma clara evidência de que o custo do risco excede a expectativa de lucro. O ponto de inflexão é claramente visível no gráfico e não exige cálculos matemáticos adicionais, como derivadas, para determinar o extremo.
Para confirmarmos a presença de impulsos de mercado, analisaremos separadamente os parâmetros da melhor e da pior iteração dos resultados de otimização do nosso EA, avaliando a relação entre o tamanho do risco e a rentabilidade. É evidente que, se o lucro esperado na entrada for muitas vezes maior que o risco planejado, isso indica a ocorrência de impulsos, caracterizando um movimento unidirecional do instrumento. Se, no entanto, o risco for igual ou superior à rentabilidade, então não há impulsos.
O ponto de inflexão do risco diário representa claramente a otimização ideal na melhor iteração, com os parâmetros apresentados na Tabela 3, conforme identificado pelo otimizador:
Nº | Nome do Parâmetro | Valor do Parâmetro |
---|---|---|
1 | inp_riskperday | 0.6 |
2 | inp_riskperweek | 3 |
3 | inp_riskpermonth | 8 |
4 | inp_plandayprofit | 3.1 |
5 | dayProfitControl | true |
6 | inp_slippfits | 2 |
7 | inp_spreadfits | 2 |
8 | inp_risk_per_deal | 100 |
9 | inp_sl_in_int | 2000 |
Tabela 3. Parâmetros da melhor execução do otimizador de estratégias para o EA RiskManagerAlgo
Observamos que a meta de lucro de 3,1 é cinco vezes maior que o risco necessário para alcançá-la, que é de 0,6. Ou seja, arriscamos 0,6% do depósito para obter um retorno de 3,1%. Isso confirma de forma clara a presença de impulsos de preço nos níveis fractais diários, gerando uma expectativa matemática positiva.
O gráfico da melhor execução está ilustrado na Figura 4.
Figura 4. Gráfico da melhor execução no otimizador de estratégia
O gráfico de crescimento do saldo mostra que a estratégia com controle de riscos gera um crescimento suave, onde cada retração subsequente não supera a mínima da anterior, destacando a necessidade da padronização de riscos e do uso do nosso gerenciador de riscos para a segurança do investimento. Para confirmar definitivamente a presença de impulsos e a necessidade da padronização de riscos, analisaremos os resultados da pior execução do otimizador.
A relação risco-retorno da pior execução é apresentada na Tabela 4:
Nº | Nome do Parâmetro | Valor do Parâmetro |
---|---|---|
1 | inp_riskperday | 1.1 |
2 | inp_riskperweek | 0.5 |
3 | inp_riskpermonth | 2 |
4 | inp_plandayprofit | 0.1 |
5 | dayProfitControl | true |
6 | inp_slippfits | 2 |
7 | inp_spreadfits | 2 |
8 | inp_risk_per_deal | 100 |
9 | inp_sl_in_int | 2000 |
Tabela 4. Parâmetros da pior execução no otimizador de estratégias para o EA RiskManagerAlgo
Os dados da Tabela 4 mostram que a pior iteração ocorre exatamente na região da Figura 3, onde o risco não está padronizado em relação ao impulso. Isso significa que estamos em uma área onde o risco extremo não gera o retorno necessário, não aproveitando eficientemente o potencial de risco, enquanto consome uma grande parte do depósito em entradas subótimas.
O gráfico da pior execução está representado na Figura 5:
Figura 5. Gráfico da pior execução no otimizador de estratégia
Os dados da Figura 5 mostram que o risco desbalanceado em relação ao retorno potencial pode causar grandes rebaixamentos na conta, tanto no saldo quanto no patrimônio. Os resultados de otimização apresentados neste capítulo nos permitem concluir que é essencial utilizar um gerenciador de riscos para controlar os riscos e escolher corretamente, de forma lógica, o tamanho adequado dos riscos em relação às capacidades da sua estratégia de trading. Agora, vamos às conclusões gerais do nosso artigo.
Considerações finais
Com base nos materiais, modelos, argumentos e cálculos apresentados, podemos tirar as seguintes conclusões. Não basta encontrar uma estratégia ou algoritmo de investimento lucrativo; mesmo assim, é possível perder dinheiro se o risco for aplicado de forma inadequada ao capital. Mesmo com uma estratégia lucrativa, o fator essencial para a eficiência e segurança no mercado financeiro é a aplicação de um gerenciamento de riscos. A condição indispensável para a eficácia e estabilidade a longo prazo é a padronização do risco em função das capacidades da estratégia utilizada. Também recomendo fortemente não operar em contas reais sem um gerenciador de riscos ativo e sem stop-losses definidos para cada posição aberta.
É evidente que nem todos os riscos nos mercados financeiros podem ser controlados ou minimizados, mas eles devem ser sempre avaliados em relação ao retorno esperado. Riscos normatizados podem ser controlados com o uso de um gerenciador de riscos. Assim como enfatizado neste artigo e nos anteriores, recomendo vivamente a aplicação dos princípios de gerenciamento de dinheiro e risco na sua operação.
Se você aplicar uma abordagem de trading não sistemática e sem controle de riscos, qualquer estratégia lucrativa pode se tornar perdedora; mas, quase qualquer estratégia perdedora pode se tornar lucrativa se o gerenciamento de riscos for bem aplicado. Se este material ajudar a salvar pelo menos um depósito de ser completamente perdido, considerarei que o trabalho valeu a pena.
Ficarei feliz em receber feedback nos comentários deste artigo, especialmente sobre temas que possam interessar a vocês no futuro. Bons trades, amigos!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14634





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso