Implementando Take Profit na forma de ordens limitadas sem alterar o código original do EA

Dmitriy Gizlyk | 24 dezembro, 2018

Sumário

Introdução

Devido à execução a mercado de take-profit, MetaTrader 5 tem sido criticada por muitos usuários, incluindo no fórum desse site. Eles se queixam sobre o impacto negativo que tem a derrapagem sobre os resultados de trading quando se ativa o take-profit. Como alternativa, é proposto o uso de ordens limitadas para substituir o take-profit padrão.

Por outra parte, ao contrário do take-profit padrão, o uso de ordens limitadas permite que o usuário crie algoritmos de fechamento parcial e gradual de posições, uma vez que na ordem limitada a ser colocada é possível definir um volume diferente do da posição. Nesse artigo, quero oferecer a vocês uma das possíveis implementações dessa substituição de take-profit.

1. O problema

Talvez não faça sentido estar discutindo se é melhor o algoritmo de take-profit embutido no MetaTrader 5 ou o uso de ordens limitadas para substitui-lo. Acho que cada um deve escolher o que é melhor para si mesmo partindo dos requisitos da estratégia usada. Nesse artigo, a todos os interessados quero lhes oferecer certa abordagem e o direito de escolherem.

Antes de passar a construir o sistema de ordens limitadas, examinemos os aspectos teóricos que devemos abordar em seu algoritmo.

O mais importante que devemos lembrar agora é que o take-profit é um ordem para fechar posição. Alguém poderia pensar que se trata de algo óbvio e que o terminal e o sistema, afinal, devem tomar conta disso. No entanto, como decidimos substituir o sistema ao colocar take-profit, devemos também fazer sua manutenção.

Então, qual é aqui o assunto? A questão é que uma posição pode ser fechada não apenas usando take-profit, mas também stop-loss ou por vontade do trader (também usando EAs). Consequentemente, nosso sistema deverá rastrear a presença da posição a ser acompanhada no mercado e, caso ela não exista, remover a ordem limitada. Caso contrário, é bem provável que seja aberta uma posição indesejável, podendo causar perdas muito maiores do que uma derrapagem se for processado um take-profit.

Além disso, devemos compreender que existe a possibilidade de a posição ser fechada parcialmente ou que, ao usarmos contas de cobertura, a posição pode ser aumentada; portanto, é importante rastrear não apenas a presença da posição, mas também seu volume, para assim substituir a ordem limitada se esse volume for alterado.

O seguinte aspecto tem a ver com o funcionamento do sistema de cobertura. Sistema esse que realiza um registro de posições separado e permite a existência de várias posições simultaneamente num mesmo instrumento. Daí que, se nossa ordem limitada for ativada, ela não cubrirá a posição existente, mas, sim, uma nova. Consequentemente, ativada a posição limitada, nós devemos nos encarregar do seu fechamento usando a posição oposta.

Há também outra questão, isto é, quando temos um take-profit de ordem limitada, devemos fazer com que o take-profit não se ative antes da ordem em si. Aparentemente, podemos usar ordens stop-limit, por exemplo, podemos colocar simultaneamente uma ordem stop de venda e uma ordem stop-limit de compra. No entanto, o sistema não permite realizar esse tipo de operações com ordens limitadas de venda. Com isso surge a questão de rastrear a ativação da ordem pendente com subsequente posicionamento de take-profit limitado. Porém, ao rastrearmos essa ativação dentro do programa e colocarmos uma ordem pendente sem take-profit, estamos correndo o risco de abrir uma posição se houver uma falha no controle realizado por nosso programa. Como resultado, o preço pode atingir o nível de take-profit esperado e reverter. A falta de controle do programa não permite o fechamento da posição, causando perdas.

Eu arranjei uma solução na colocação de ordens pendentes com definição de um take-profit habitual, isto é, após a abertura da posição, é necessário substituir o take-profit por uma ordem limitada, colocando uma ordem limitada e redefinindo o campo take-profit da posição. Essa abordagem nos livra de perdermos o controle da situação. Adicionalmente, em caso de perda de comunicação entre o programa e o servidor, o take-profit da ordem será calculado pelo sistema, nele as possíveis perdas por derrapagem negativa são menores do que as perdas resultantes da perda de controle sobre a situação.

Além disso, há outra questão que tem a ver com a alteração do take-profit colocado anteriormente. Frequentemente, ao usar diferentes estratégias, torna-se necessário rastrear e corrigir o take-profit da posição aberta. Eis aqui dois possíveis cenários.

  1. Se fizermos alterações no código de um EA desse tipo, a fim de não procurar todas as alterações possíveis de take-profit no código, nós simplesmente substituiremos a chamada da função OrderSend pela chamada do método de nossa classe, em que já foi verificada a existência de uma ordem limitada colocada anteriormente e a conformidade com o novo nível. Se necessário, alteramos a ordem colocada anteriormente, ou ignoramos o comando caso a ordem limitada previamente colocada atenda aos novos requisitos.
  2. Quando usamos um EA comprado (não temos acesso ao seu código e, além disso, em vez de abrir posições, nosso programa realiza a substituição de take-profit), é bem possível que apareça um take-profit na posição para a qual nós já tínhamos colocado ordens limitadas. É nesse momento que devemos verificar novamente se as ordens limitadas atuais estão certas, corrigi-las e redefinir o campo take-profit da posição.

Adicionalmente, devemos rastrear a distância mínima para colocar as ordens pendentes em relação ao preço atual e a distância para congelar operações de negociação colocadas pela corretora. Se o primeiro se aplica igualmente à colocação de um take-profit sistemático, a distância para congelar operações de negociação pode nos pregar umas partidas, ao fechar uma posição rastreada próxima de uma ordem limitada definida, impossibilitando sua remoção ou modificação. Infelizmente, esse risco deve ser levado em consideração não apenas ao construir sistemas, mas também ao usá-los, já que ele não depende do algoritmo do sistema.

2. Princípios para construir o relacionamento posição — ordem limitada

Acima, já falei sobre a necessidade de rastrear o estado da posição e ver se está em correspondência ao take-profit limitado. Agora, vejamos como podemos conseguir isso. Em primeiro lugar, precisamos determinar em que ponto precisamos fazer esse controle para não sobrecarregar o terminal.

Mudanças na posição podem ocorrer a qualquer momento enquanto o pregão ainda esteja aberto. Na verdade, isso não acontece com muita frequência, no entanto, a verificação de cada tick aumenta significativamente as operações realizadas pelo EA. Nesse momento, os eventos vêm em nosso auxílio. Na documentação da MQL5, encontramos o "evento Trade que é gerado no final da negociação no servidor de negociação". Esse evento inicia a função OnTrade. Portanto, a partir dessa função podemos iniciar a função para verificar se as posições abertas e os take-profits limitados colocados estão em conformidade. Isso nos permite não verificar cada tick e, ao mesmo tempo, não ignorar nenhuma alteração.

Em seguida vem a questão da identificação. À primeira vista, não há nada mais fácil, pois basta verificarmos as ordens limitadas e as posições abertas. Mas queremos construir um algoritmo universal que funcione bem em diferentes tipos de contas e com diferentes estratégias. Adicionalmente, não podemos nos esquecer de que é provável que as ordens limitadas sejam usadas como parte da estratégia. Assim, temos que selecionar os take-profit limitados. Para identificá-los, sugiro usar comentários. Como nossas ordens limitadas substituem o take-profit, colocaremos "TP" no início do comentário da ordem para identificá-las facilmente. Em seguida, no caso de usar um fechamento de posição gradual, adicionamos um número de sequência de etapas. Isso poderia ser interrompido ao usar um sistema de compensação, mas, por fortuna, existe o sistema de cobertura permitindo várias posições numa só conta. Portanto, ao comentário de nosso take-profit limitado adicionamos o identificador da posição correspondente.

3. Criando uma classe para o take-profit

Resumindo o mencionado, a funcionalidade da nossa classe pode ser dividida em dois processos lógicos:

  1. Alterações no envio ordens de negociação para o servidor.
  2. Rastreio e correção de posições abertas e das ordens limitadas colocadas.

Para facilidade de uso, organizamos nosso algoritmo na classe CLimitTakeProfit e tornamos todas as funções estáticas, o que nos permitirá usar métodos de classe sem declarar sua instância no código do programa.

class CLimitTakeProfit : public CObject
  {
private:
   static CSymbolInfo       c_Symbol;
   static CArrayLong        i_TakeProfit; //fixed take profit
   static CArrayDouble      d_TakeProfit; //percent to close at take profit
   
public:
                     CLimitTakeProfit();
                    ~CLimitTakeProfit();
//---
   static void       Magic(int value)  {  i_Magic=value; }
   static int        Magic(void)       {  return i_Magic;}
//---
   static void       OnlyOneSymbol(bool value)  {  b_OnlyOneSymbol=value;  }
   static bool       OnlyOneSymbol(void)        {  return b_OnlyOneSymbol; }
//---
   static bool       OrderSend(const MqlTradeRequest &request, MqlTradeResult &result);
   static bool       OnTrade(void);
   static bool       AddTakeProfit(uint point, double percent);
   static bool       DeleteTakeProfit(uint point);
   
protected:
   static int        i_Magic;          //Magic number to control
   static bool       b_OnlyOneSymbol;  //Only position of one symbol under control
//---
   static bool       SetTakeProfits(ulong position_ticket, double new_tp=0);
   static bool       SetTakeProfits(string symbol, double new_tp=0);
   static bool       CheckLimitOrder(MqlTradeRequest &request);
   static void       CheckLimitOrder(void);
   static bool       CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0);
   static double     GetLimitOrderPriceByComment(string comment);
  };

Os métodos Magic, OnlyOneSymbol, AddTakeProfit e DeleteTakeProfit são métodos de configuração de classe. O magic indica o número para rastrear posições (aplicável a contas de cobertura), se definido "-1", a classe trabalhará com todas as posições. OnlyOneSymbol indica à classe que trabalhe apenas com as posições do instrumento do gráfico em que o EA está sendo executado. Os métodos AddTakeProfit e DeteleTakeProfit são usados para adicionar e remover níveis de take-profit fixo com uma indicação do volume a ser fechado em porcentagem em relação ao volume da posição inicial.

O usuário pode usar esses métodos, se desejar, mas também pode ignorá-los. Por padrão, o método funciona com todos os magic e com todos os instrumentos sem definição de take-profit fixo. Em vez do take-profit especificado na posição, será colocada uma ordem limitada.

3.1. Bloco de alterações no envio de ordens de negociação

O método OrderSend é responsável por rastrear as ordens enviadas pelo EA. Não foi por acaso que eu dei o nome e fiz a forma de chamar esse método de forma semelhante à função padrão de envio de ordens em MQL5. Essa abordagem torna mais fácil incorporar nosso algoritmo no código do EA previamente escrito, substituindo a função padrão por nosso método.

Como já foi descrito o assunto da substituição do take-profit por ordens pendentes, nesse bloco podemos substituir o take-profit apenas por ordens a mercado, no entanto, eis um detalhe: o fato de o servidor aceitar a ordem não significa que ela será executada. Além disso, após o envio da ordem, recebemos uma boleta, mas não obtemos um identificador de posição. Por essa razão, vamos realizar a substituição do take-profit no bloco de rastreio, e aqui nós apenas vamos acompanhar o momento em que se altera o take-profit colocado anteriormente.

No início do código do método, verificamos se a solicitação enviada está em conformidade com os filtros configurados para a operação do nosso algoritmo. Além disso, checamos o tipo de operação de negociação, pois ele deve estar em conformidade com a solicitação para alterar os níveis de stop da posição. Adicionalmente, não se esqueça de verificar se o take-profit está especificado na solicitação. Se a solicitação não atender a pelo menos um dos requisitos, ela será imediatamente enviada ao servidor inalterada.

Verificados os requisitos, a solicitação é transferida para o método SetTakeProfit, nele são colocadas as ordens limitadas. Repare que a classe tem dois métodos semelhantes para trabalhar segundo a boleta da posição e segundo símbolo. O segundo é mais aplicável a contas de compensação se a solicitação não indicar a boleta da posição. Se a execução desse método for bem-sucedida, redefinimos o campo take-profit na solicitação.

Como na solicitação poderiam ter sido alterados o take-profit e o stop-loss, verificaremos que o stop-loss e o take-profit definidos na posição estão em conformidade. Se necessário, enviamos uma solicitação ao servidor e saímos da função. O código completo do método é mostrado abaixo.

bool CLimitTakeProfit::OrderSend(MqlTradeRequest &request,MqlTradeResult &result)
  {
   if((b_OnlyOneSymbol && request.symbol!=_Symbol) ||
      (i_Magic>=0 && request.magic!=i_Magic) || !(request.action==TRADE_ACTION_SLTP && request.tp>0))
      return(::OrderSend(request,result));
//---
   if(((request.position>0 && SetTakeProfits(request.position,request.tp)) ||
       (request.position<=0 && SetTakeProfits(request.symbol,request.tp))) && request.tp>0)
      request.tp=0;
   if((request.position>0 && PositionSelectByTicket(request.position)) ||
      (request.position<=0 && PositionSelect(request.symbol)))
     {
      if(PositionGetDouble(POSITION_SL)!=request.sl || PositionGetDouble(POSITION_TP)!=request.tp)
         return(::OrderSend(request,result)); 
     }
//---
   return true;
  }

Agora, vejamos em mais detalhes o método SetTakeProfit. No início do método, verificamos se a posição especificada existe e se estamos trabalhando um instrumento da posição. Logo, atualizamos os dados do instrumento da posição. Depois disso, calculamos os preços mais próximos para os quais será permitido colocar ordens limitadas. Se ocorrer algum erro, saímos do método com o resultado false.

bool CLimitTakeProfit::SetTakeProfits(ulong position_ticket, double new_tp=0)
  {
   if(!PositionSelectByTicket(position_ticket) || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
      return false;
   if(!c_Symbol.Name(PositionGetString(POSITION_SYMBOL)) || !c_Symbol.Select() || !c_Symbol.Refresh() || !c_Symbol.RefreshRates())
      return false;
//---
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

Depois disso, preparamos modelos de estrutura para enviar uma solicitação de negociação para colocar uma ordem limitada. Calculamos o tamanho do take-profit (ou posição) definido, a fim de usar apenas os take-profit fixos que estão dentro da distância calculada.

   MqlTradeRequest tp_request={0};
   MqlTradeResult tp_result={0};
   tp_request.action =  TRADE_ACTION_PENDING;
   tp_request.magic  =  PositionGetInteger(POSITION_MAGIC);
   tp_request.type_filling =  ORDER_FILLING_RETURN;
   tp_request.position=position_ticket;
   tp_request.symbol=c_Symbol.Name();
   int total=i_TakeProfit.Total();
   double tp_price=(new_tp>0 ? new_tp : PositionGetDouble(POSITION_TP));
   if(tp_price<=0)
      tp_price=GetLimitOrderPriceByComment("TPP_"+IntegerToString(position_ticket));
   double open_price=PositionGetDouble(POSITION_PRICE_OPEN);
   int tp_int=(tp_price>0 ? (int)NormalizeDouble(MathAbs(open_price-tp_price)/c_Symbol.Point(),0) : INT_MAX);
   double position_volume=PositionGetDouble(POSITION_VOLUME);
   double closed=0;
   double closed_perc=0;
   double fix_closed_per=0;

Em seguida, realizamos um ciclo para verificar e colocar take-profit fixos. Primeiro, definimos o comentário de nossa ordem (o princípio de codificação foi discutido acima). Logo, verificamos se o nível de take-profit fixo excede o nível especificado na posição ou solicitação. Se exceder, avançamos para o próximo take-profit. Adicionalmente, não podemos nos esquecer de verificar se o volume de ordens limitadas colocadas anteriormente cobre ou não o volume da posição. Se as ordens limitadas cobrem o volume da posição, saímos do ciclo.

   for(int i=0;i<total;i++)
     {
      tp_request.comment="TP"+IntegerToString(i)+"_"+IntegerToString(position_ticket);
      if(i_TakeProfit.At(i)<tp_int && d_TakeProfit.At(i)>0)
        {
         if(closed>=position_volume || fix_closed_perc>=100)
            break;

O próximo passo é preencher os elementos ausentes da estrutura da solicitação de negociação. Para fazer isso, calculamos o volume da nova ordem limitada e indicamos tanto o tipo como o preço de abertura da ordem.

//---
         double lot=position_volume*MathMin(d_TakeProfit.At(i),100-closed)/(100-fix_closed_perc);
         lot=MathMin(position_volume-closed,lot);
         lot=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((lot-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
         lot=NormalizeDouble(lot,2);
         tp_request.volume=lot;
         switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
           {
            case POSITION_TYPE_BUY:
              tp_request.type=ORDER_TYPE_SELL_LIMIT;
              tp_request.price=c_Symbol.NormalizePrice(open_price+i_TakeProfit.At(i)*c_Symbol.Point());
              break;
            case POSITION_TYPE_SELL:
              tp_request.type=ORDER_TYPE_BUY_STOP;
              tp_request.price=c_Symbol.NormalizePrice(open_price-i_TakeProfit.At(i)*c_Symbol.Point());
              break;
           }

Após preenchida a estrutura da solicitação de negociação, verificamos se a ordem limitada foi colocada antes com esses parâmetros, para isso, usamos o método CheckLimitOrder (o algoritmo do método será considerado abaixo), transferindo para ele a estrutura de solicitação preenchida. Se a ordem tiver sido colocada anteriormente, adicionamos o volume da ordem à soma dos volumes definidos para a posição Essa operação é necessária para controlar que os volumes de posição e ordem limitadas posicionadas estão em conformidade.

         if(CheckLimitOrder(tp_request))
           {
            if(tp_request.volume>=0)
              {
               closed+=tp_request.volume;
               closed_perc=closed/position_volume*100;
              }
            else
              {
               fix_closed_per-=tp_request.volume/(position_volume-tp_request.volume)*100;
              }
            continue;
           }

Se a ordem ainda não tiver sido colocada, ajustamos seu preço de acordo com os requisitos da corretora em relação ao preço atual e enviamos uma solicitação ao servidor. Se a solicitação for enviada com sucesso, somamos o volume da ordem com o total dos volumes previamente definidos para a posição.

         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }

Após concluir o ciclo, usando o mesmo algoritmo, colocamos uma ordem limitada para o volume ausente ao preço especificado na solicitação de modificação (ou na posição). Se o volume for menor que o mínimo permitido, saímos da função com o resultado false.

   if(tp_price>0 && position_volume>closed)
     {
      tp_request.price=tp_price;
      tp_request.comment="TPP_"+IntegerToString(position_ticket);
      tp_request.volume=position_volume-closed;
      if(tp_request.volume<c_Symbol.LotsMin())
         return false;
      tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
      tp_request.volume=NormalizeDouble(tp_request.volume,2);
//---
      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           tp_request.type=ORDER_TYPE_SELL_LIMIT;
           break;
         case POSITION_TYPE_SELL:
           tp_request.type=ORDER_TYPE_BUY_LIMIT;
           break;
        }
      if(CheckLimitOrder(tp_request) && tp_request.volume>=0)
        {
         closed+=tp_request.volume;
         closed_perc=closed/position_volume*100;
        }
      else
        {
         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(tp_request.volume<=0)
           {
            tp_request.volume=position_volume-closed;
            tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
            tp_request.volume=NormalizeDouble(tp_request.volume,2);
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }      

Na conclusão do método, verificamos se o volume das ordens limitadas cobre o volume da posição. Se sim, redefinimos o valor do take-profit na posição e saímos da função.

   if(closed>=position_volume && PositionGetDouble(POSITION_TP)>0)
     {
      ZeroMemory(tp_request);
      ZeroMemory(tp_result);
      tp_request.action=TRADE_ACTION_SLTP;
      tp_request.position=position_ticket;
      tp_request.symbol=c_Symbol.Name();
      tp_request.sl=PositionGetDouble(POSITION_SL);
      tp_request.tp=0;
      tp_request.magic=PositionGetInteger(POSITION_MAGIC);
      if(!OrderSend(tp_request,tp_result))
         return false;
     }
   return true;
  }

Para completar, consideramos o algoritmo do método CheckLimitOrder. Esse método verifica a se existe a ordem previamente colocada de acordo com a solicitação de negociação preparada. Se houver uma ordem já realizada, o método retornará true e um não será feita uma nova ordem.

No início do método, determinamos os níveis mais próximos possíveis para colocar ordens limitadas. Nós precisaremos deles se for necessário modificar a ordem colocada anteriormente.

bool CLimitTakeProfit::CheckLimitOrder(MqlTradeRequest &request)
  {
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

O próximo passo é iterar todas as ordens abertas, dentre as quais identificaremos a ordem necessária pelo seu comentário.

   for(int i=0;i<total;i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      if(OrderGetString(ORDER_COMMENT)!=request.comment)
         continue;

Ao encontrar uma ordem com o comentário desejado, verificamos seu volume e tipo. Se um dos parâmetros não corresponder, excluiremos a ordem pendente existente e sairemos da função com o resultado false. Em caso de erro ao excluir uma ordem, no campo de volume da solicitação, escrevemos o volume da ordem existente.

      if(OrderGetDouble(ORDER_VOLUME_INITIAL) != request.volume || OrderGetInteger(ORDER_TYPE)!=request.type)
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
            return false;
         request.volume=OrderGetDouble(ORDER_VOLUME_INITIAL);
        }

Na próxima etapa, verificamos o preço de abertura da ordem encontrada e especificada nos parâmetros. Se necessário, modificamos a ordem atual e saímos do método com o resultado true.

      if(MathAbs(OrderGetDouble(ORDER_PRICE_OPEN)-request.price)>=c_Symbol.Point())
        {
         MqlTradeRequest mod_request={0};
         MqlTradeResult mod_result={0};
         mod_request.action=TRADE_ACTION_MODIFY;
         mod_request.price=request.price;
         mod_request.magic=request.magic;
         mod_request.symbol=request.symbol;
         switch(request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              if(mod_request.price>max_buy_limit)
                 return true;
              break;
            case ORDER_TYPE_SELL_LIMIT:
              if(mod_request.price<min_sell_limit)
                 return true;
              break;
           }
         bool mod=::OrderSend(mod_request,mod_result);
        }
      return true;
     }

No entanto, não podemos esquecer que é possível que uma ordem limitada com um volume semelhante já tenha sido processada. Portanto, se a ordem desejada não for encontrada entre as abertas, precisamos verificar o histórico de ordens da posição atual. Essa funcionalidade é implementada no método CheckOrderInHistory, que chamaremos no final.

   if(!PositionSelectByTicket(request.position))
      return true;
//---
   return CheckOrderInHistory(PositionGetInteger(POSITION_IDENTIFIER),request.comment, request.type, request.volume);
  }

Dependendo do tipo de conta, temos duas opções para processar uma ordem limitada:

  1. Processamento direto na posição (contas de compensação).
  2. A ordem limitada abriu uma posição oposta e, em seguida, foi realizado o fechamento oposto da posição (contas de cobertura).

Ao procurar pelo fechamento oposto de posições, deve-se observar que ordens de fechamento oposto podem não estar relacionadas a essa posição, portanto, iteraremos por transações e receberemos sua boleta.

bool CLimitTakeProfit::CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0)
  {
   if(!HistorySelectByPosition(position_id))
      return true;
   int total=HistoryDealsTotal();
   bool hedging=(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
//---
   for(int i=0;i<total;i++)
     {
      ulong ticket=HistoryDealGetTicket((uint)i);
      ticket=HistoryDealGetInteger(ticket,DEAL_ORDER);
      if(!HistoryOrderSelect(ticket))
         continue;
      if(ticket<=0)
         continue;

Para contas de cobertura, primeiro verificamos se a ordem tem a ver com uma posição diferente. Se for encontrada uma ordem de outra posição, procuraremos uma ordem com o comentário necessário já nessa posição, para isso, chamaremos recursivamente a função CheckOrderInHistory. Para evitar o ciclo, não nos esqueçamos de verificar antes de chamar o método se o método foi chamado a partir dessa posição. Se a ordem for encontrada, sairemos do método com o resultado true. Caso contrário, precisaremos recarregar o histórico de nossa posição e passar para a próxima transação.

      if(hedging && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=position_id && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=call_position)
        {
         if(CheckOrderInHistory(HistoryOrderGetInteger(ticket,ORDER_POSITION_ID),comment,type,volume))
            return true;
         if(!HistorySelectByPosition(position_id))
            continue;
        }

Para ordens da posição atual, verificaremos seu comentário e tipo. Se a ordem for encontrada, registramos seu volume na solicitação com um sinal de menos e saímos do método.

      if(HistoryOrderGetString(ticket,ORDER_COMMENT)!=comment)
         continue;
      if(HistoryOrderGetInteger(ticket,ORDER_TYPE)!=type)
         continue;
//---
      volume=-OrderGetDouble(ORDER_VOLUME_INITIAL);
      return true;
     }
   return false;
  }

O código completo de todos os métodos e funções pode ser encontrado no anexo.

3.2. Bloco de processamento de operações de negociação

O segundo bloco do nosso algoritmo abrange o rastreio e correção de posições e ordens limitadas abertas.

As operações de negociação concluídas geram o evento Trade, que, por sua vez, faz com que a função OnTrade seja executada. Para ativação de operações de negociação, adicionamos o método respectivo à nossa classe.

O algoritmo do método deve ser preparado previamente, para isso, obtemos o número de posições abertas na conta e verificamos o tipo de conta.

bool CLimitTakeProfit::OnTrade(void)
  {
   int total=PositionsTotal();
   bool result=true;
   bool hedhing=AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;

Em seguida, iteramos as posições abertas. No início do ciclo, verificamos se a posição está em conformidade com as condições de filtragem segundo instrumento e segundo magic (para contas de cobertura).

   for(int i=0;i<total;i++)
     {
      ulong ticket=PositionGetTicket((uint)i);
      if(ticket<=0 || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
         continue;
//---
     if(i_Magic>0)
        {
         if(hedhing && PositionGetInteger(POSITION_MAGIC)!=i_Magic)
            continue;
        }

Depois, para contas de cobertura, verificamos se a posição é resultado de nosso take-profit limitado. Se sim, realizamos uma operação de fechamento oposto de posição. Após o fechamento bem-sucedido das posições, continuamos com a próxima posição.

      if(hedhing)
        {
         string comment=PositionGetString(POSITION_COMMENT);
         if(StringFind(comment,"TP")==0)
           {
            int start=StringFind(comment,"_");
            if(start>0)
              {
               long ticket_by=StringToInteger(StringSubstr(comment,start+1));
               long type=PositionGetInteger(POSITION_TYPE);
               if(ticket_by>0 && PositionSelectByTicket(ticket_by) && type!=PositionGetInteger(POSITION_TYPE))
                 {
                  MqlTradeRequest   request  ={0};
                  MqlTradeResult    trade_result   ={0};
                  request.action=TRADE_ACTION_CLOSE_BY;
                  request.position=ticket;
                  request.position_by=ticket_by;
                  if(::OrderSend(request,trade_result))
                     continue;
                 }
              }
           }
        }

No final do ciclo, chamamos o método SetTakeProfits para verificar e definir ordens limitadas segundo posição. O algoritmo do método foi descrito acima.

      result=(SetTakeProfits(PositionGetInteger(POSITION_TICKET)) && result);
     }

Concluído o ciclo para verificar posições abertas, verificaremos se as ordens limitadas estão em conformidade com as posições abertas e, se necessário, removeremos as ordens limitadas remanescentes das posições fechadas. Para fazer isso, chamamos o método CheckLimitOrder. Repare que, nesse caso, a função é chamada sem parâmetros, em contraste com a chamada de função descrita anteriormente. Isso se deve ao fato de que a chamada é realizada por um método completamente diferente, e que graças à propriedade sobrecarga de função é possível usar um único nome.

   CheckLimitOrder();
//---
   return result;
  }

O algoritmo desse método é baseado na iteração de todas as ordens colocadas, entre as quais nossas ordens são selecionadas por comentários.

void CLimitTakeProfit::CheckLimitOrder(void)
  {
   int total=OrdersTotal();
   bool res=false;
//---
   for(int i=0;(i<total && !res);i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      string comment=OrderGetString(ORDER_COMMENT);
      if(StringFind(comment,"TP")!=0)
         continue;
      int pos=StringFind(comment,"_",0);
      if(pos<0)
         continue;

Após encontrado o take-profit limitado, a partir do comentário recuperamos o identificador da posição oposta. Tentamos acessar a posição especificada com ajuda desse ID. Se a posição não existir, removemos a ordem.

      long pos_ticker=StringToInteger(StringSubstr(comment,pos+1));
      if(!PositionSelectByTicket(pos_ticker))
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
           {
            i--;
            total--;
           }
         continue;
        }

Se conseguimos acessar a posição, verificamos se o tipo de ordem está em conformidade com o tipo de posição. Essa verificação é necessária para contas de compensação em que uma posição pode ser revertida durante operações de negociação. Caso haja uma discrepância, removemos a ordem e continuamos verificando a próxima ordem.

      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_LIMIT)
              continue;
           break;
         case POSITION_TYPE_SELL:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_LIMIT)
              continue;
           break;
        }
      MqlTradeRequest del_request={0};
      MqlTradeResult del_result={0};
      del_request.action=TRADE_ACTION_REMOVE;
      del_request.order=ticket;
      if(::OrderSend(del_request,del_result))
        {
         i--;
         total--;
        }
     }
//---
   return;
  }

O código completo de todos os métodos da classe pode ser encontrado no anexo.

4. Integrando classes a EAs

Após trabalharmos com a classe, vamos ver como integrá-la ao EA já escrito.

Deixe-me lembrá-lo de que todos os métodos da nossa classe são estáticos, o que permite que eles sejam usados sem declaração da instância da classe. Essa abordagem foi inicialmente escolhida para facilitar a integração da classe aos EAs já escritos. Na verdade, esse é o primeiro passo para integrar uma classe a um EA.

Como segundo passo, abaixo do código de nossa classe, criamos uma função LimitOrderSend com parâmetros de chamada semelhantes à função OrderSend, a única funcionalidade que será a chamada para o método CLimitTakeProfit::OrderSend. Depois, usando a diretiva #define vamos substituir a função original OrderSend pela nossa. O uso desse método torna possível substituir nosso código imediatamente em todas as funções - do EA - que enviam solicitações de negociação. Assim, evitamos perder tempo procurando e procurando comandos em todo o código do EA.

bool LimitOrderSend(const MqlTradeRequest &request, MqlTradeResult &result)
 { return CLimitTakeProfit::OrderSend(request,result); } 
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#define OrderSend(request,result)      LimitOrderSend(request,result)

Como muitos EAs não usam a função OnTrade, também podemos escreve-la no arquivo de nossa classe. Mas se o seu EA usar essa função, você terá que remover ou ocultar o seguinte código nos comentários e adicionar a chamada do método CLimitTakeProfit::OnTrade no corpo da função do seu EA.

void OnTrade()
  {
   CLimitTakeProfit::OnTrade();
  }

Em seguida, para integrar a classe ao EA, adicionamos um link para o arquivo de nossa classe usando a diretiva #include. Mas não se esqueça de que nossa classe deve estar antes da chamada de outras bibliotecas e do código do EA. Abaixo está um exemplo de adição de uma classe ao EA MACD Sample.mq5 que vem com o terminal por padrão.

//+------------------------------------------------------------------+
//|                                          MACD Sample LimitTP.mq5 |
//|                   Copyright 2009-2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2009-2017, MetaQuotes Software Corp."
#property link        "http://www.mql5.com"
#property version     "5.50"
#property description "It is important to make sure that the expert works with a normal"
#property description "chart and the user did not make any mistakes setting input"
#property description "variables (Lots, TakeProfit, TrailingStop) in our case,"
#property description "we check TakeProfit on a chart of more than 2*trend_period bars"

#define MACD_MAGIC 1234502
//---
#include <Trade\LimitTakeProfit.mqh>
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
//---

No código de função OnInit é possível adicionar uma posição de fechamento parcial. Agora nosso EA está pronto para ser usado.

Não esqueça de testar o EA antes de usar em contas reais.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- create all necessary objects
   if(!ExtExpert.Init())
      return(INIT_FAILED);
   CLimitTakeProfit::AddTakeProfit(100,50);
//--- secceed
   return(INIT_SUCCEEDED);
  }

Funcionamento do EA

O código completo do EA pode ser encontrado no anexo.

Fim do artigo

Esse artigo propõe um mecanismo para substituir o take-profit da posição por ordens limitadas opostas. O trabalho foi projetado para facilitar ao máximo a integração do método ao código de qualquer EA. Espero que meu trabalho seja útil e que você possa avaliar todas as vantagens e desvantagens de ambos os métodos.

Programas utilizados no artigo:

#
Nome
Tipo
Descrição
1LimitTakeProfit.mqhBiblioteca de classeClasse para substituir take-profit por ordens limitadas
2MACD Sample.mq5Expert AdvisorEA original retirado dos exemplos para o MetaTrader 5
3MACD Sample LimitTP.mq5Expert AdvisorExemplo de integração de classes ao EA retirado dos exemplos para o MetaTrader 5