Usando indicadores para otimização RealTime de EAs

Dmitriy Gizlyk | 26 novembro, 2018

Sumário

Introdução

Sempre, antes de iniciar um EA num gráfico, surge a questão sobre quais os melhores parâmetros que dariam a máxima rentabilidade. Para procurar esses parâmetros, é realizada a otimização da estratégia de negociação em dados históricos. Mas, como é sabido, o mercado está em constante movimento. Portanto, com o tempo, os parâmetros selecionados perdem relevância.

Nesse momento, é necessária uma nova otimização do EA. Esse ciclo é constante. Cada usuário escolhe o momento para otimizar novamente. Mas pensemos: será que esse processo pode ser automatizado? Quais são os métodos para resolver esse problema? Provavelmente, você já ponderou gerenciar o testador de estratégias padrão inicializando o terminal com seu próprio arquivo de configuração. Quero oferecer uma abordagem um pouco diferente, atribuindo a função de testador a um indicador.

1. Ideia

É claro que um indicador não é um testador de estratégias. Sendo assim, como pode ele nos ajudar a otimizar um EA? Minha ideia é programar no indicador a lógica do trabalho de um EA e acompanhar a rentabilidade de trades virtuais em tempo real. Ao realizar a otimização, no testador de estratégias é realizada uma série de testes com pesquisa detalhada dos melhores parâmetros especificados. Faremos o mesmo e iniciaremos ao mesmo tempo várias cópias de um indicador com diferentes parâmetros, semelhante ao que acontece com as rodadas do testador de estratégias. No momento da decisão, o EA consulta os indicadores inicializados e seleciona as melhores leituras para execução.

Então, por que reinventar a roda? Vejamos os aspectos positivos e negativos dessa decisão. Sem dúvida, a principal vantagem dessa abordagem é a otimização do EA em condições quase em tempo real. A segunda vantagem pode ser o teste em ticks reais de sua corretora, mas, por outro lado, o teste em tempo real é uma grande desvantagem, já que é necessário aguardar a coleta de dados estatísticos. Adicionalmente, é positivo que, ao avançar no tempo, o testador-indicador apenas recalcula o tick atual, enquanto o testador de estratégias analisa o histórico desde o início. Essa abordagem fornece uma otimização mais rápida no momento certo. Por esse motivo, podemos otimizar em quase todas as barras.

Uma das desvantagens de usar essa abordagem pode ser a falta de um histórico de ticks para teste em histórico. Claro, podemos usar CopyTicks ou CopyTicksRange. Mas tanto o carregamento de um histórico de ticks quanto recalcular uma grande quantidade de informações exigem poder de computação e tempo. Não vamos esquecer que usamos indicadores. Todos os indicadores de um só instrumento no MetaTrader 5 funcionam numa só thread. Há outra limitação aqui, acontece que muitos indicadores podem levar a uma degradação do desempenho do terminal.

Para minimizar os riscos das desvantagens descritas acima, tomaremos as seguintes premissas:

  1. Durante a inicialização do testador-indicador, o histórico é calculado de acordo com o preço M1 OHLC. No cálculo do lucro/perda das ordens, primeiro é verificado o stop-loss e, em seguida, o take-profit do High/Low (dependendo do tipo de ordem).
  2. Devido à premissa 1, as ordens são abertas apenas na abertura da vela.
  3. Para reduzir o número total de testadores-indicadores em execução, aplicamos uma abordagem sensata à escolha dos parâmetros usados ​​neles. Aqui podemos adicionar um passo mínimo, filtrando os parâmetros de acordo com a lógica do indicador. Por exemplo, ao usar MACD, se o intervalo de parâmetros das médias móveis rápida e lenta se sobrepuser, para um conjunto de parâmetros em que o período de média móvel lenta é menor ou igual ao período de média móvel rápida, o testador-indicador não será iniciado contrário à lógica do indicador. Também podemos adicionar uma divergência mínima entre períodos, inicialmente descartando opções com muitos sinais falsos.

2. Estratégia de negociação

Para testar o método, usaremos uma estratégia simples baseada em 3 indicadores clássicos, particularmente WPR, RSI e ADX. O sinal de compra será quando o WPR cruze o nível de sobrevenda (nível de -80) de baixo para cima. Além disso, será controlado que o RSI não esteja na zona de sobrecompra (acima do nível de 70). Como ambos os indicadores são osciladores, seu uso é justificado nos movimentos laterais. A presença de uma fase de correção é verificada pelo indicador ADX, sendo que seu valor não deve exceder o nível de 40.

Ponto de entrada para compra

Para venda, o sinal será o oposto. O indicador WPR cruza o nível de sobrecompra de -20 de cima para baixo, o valor do RSI deve estar acima da zona de sobrevenda de 30. O ADX controla a presença de fase de correção, como na compra.

Ponto de entrada para venda

Como mencionado anteriormente, a entrada na posição será realizada na abertura da vela que segue o sinal. Sairemos da posição num stop-loss ou take-profit fixos.

Para controlar as perdas no mercado, sempre haverá mais de uma posição.

3. Preparação do testador-indicador

3.1. Classe de trades virtuais

Definida a estratégia de negociação, é hora de começar a escrever o indicador de teste. A primeira coisa que precisamos fazer é preparar as ordens virtuais cuja execução acompanharemos no indicador. Anteriormente no artigo [1] foi descrita uma classe de ordem virtual. Ao trabalharmos, podemos aproveitar esse desenvolvimento com um pequeno acréscimo. Acontece que a classe descrita anteriormente tem um método Tick que verifica o momento em que uma ordem é fechada segundo os preços Ask e Bid atuais. Essa abordagem é bastante aplicável quando se trabalha apenas em tempo real e não é de todo aplicável para verificação baseada em dados históricos. Vamos alterar ligeiramente a função indicada, adicionando um preço e um spread aos seus parâmetros. Executadas as operações, o método retornará o status da ordem. Como resultado desse complemento, o método assumirá o seguinte formato.

bool CDeal::Tick(double price, int spread)
  {
   if(d_ClosePrice>0)
      return true;
//---
   switch(e_Direct)
     {
      case POSITION_TYPE_BUY:
        if(d_SL_Price>0 && d_SL_Price>=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price<=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
             }
          }
        break;
      case POSITION_TYPE_SELL:
        price+=spread*d_Point;
        if(d_SL_Price>0 && d_SL_Price<=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price>=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
             }
          }
        break;
     }
   return IsClosed();
  }

O código de classe completo pode ser encontrado no anexo.

3.2. Programemos o indicador

O próximo passo envolve a codificação do próprio indicador. Como o nosso testador-indicador desempenhará o papel de EA de alguma forma, seus parâmetros de entrada serão semelhantes aos parâmetros de um EA. Primeiro, nos parâmetros do indicador definimos tanto o período de teste como os níveis de stop-loss e take-profit. Em seguida, indicamos os parâmetros dos indicadores utilizados. No final, levamos em conta a direção da negociação e o período médio dos dados estatísticos. Mais detalhes sobre o uso de cada parâmetro serão descritos conforme usados ​​no código do indicador.

input int                  HistoryDepth      =  500;           //Depth of history(bars)
input int                  StopLoss          =  200;           //Stop Loss(points)
input int                  TakeProfit        =  600;           //Take Profit(points)
//--- Parâmetros do indicador RSI
input int                  RSIPeriod         =  28;            //RSI Period
input double               RSITradeZone      =  30;            //Overbaying/Overselling zone size
//--- Parâmetros do indicador WPR
input int                  WPRPeriod         =  7;             //Period WPR
input double               WPRTradeZone      =  30;            //Overbaying/Overselling zone size
//--- Parâmetros do indicador ADX
input int                  ADXPeriod         =  11;            //ADX Period
input int                  ADXLevel          =  40;            //Flat Level ADX
//---
input int                  Direction         =  -1;            //Trade direction "-1"-All, "0"-Buy, "1"-Sell
//---
input int                  AveragePeriod     =  10;            //Averaging Period

Para cálculos e intercâmbio de dados com o EA, em nosso indicador criaremos 9 buffers de indicador contendo as seguintes informações:

1. Probabilidade de trade lucrativo.

double      Buffer_Probability[];

2. Fator de lucro para o período testado.

double      Buffer_ProfitFactor[];

3. Níveis de stop-loss e take-profit. Esses dois buffers podem ser eliminados criando uma matriz de correspondência entre o identificador do indicador e os níveis especificados no EA ou solicitando os parâmetros do indicador segundo seu identificador ao abrir um trade. Pareceu-me a solução mais simples.

double      Buffer_TakeProfit[];
double      Buffer_StopLoss[];

4. Buffers para contar no período testado o número total de trades concluídos e sua rentabilidade.

double      Buffer_ProfitCount[];
double      Buffer_DealsCount[];

5. Os seguintes dois buffers são auxiliares para calcular os valores anteriores e contêm dados semelhantes apenas para a barra atual.

double      Buffer_ProfitCountCurrent[];
double      Buffer_DealsCountCurrent[];

6. Por último, mas não menos importante, o buffer que envia o sinal ao EA para concluir o trade.

double      Buffer_TradeSignal[];

Além dos buffers especificados, no bloco de variáveis globais, declararemos uma matriz para armazenar trades abertos, uma variável para registrar a hora do último trade, variáveis para armazenar identificadores de indicadores e matrizes para obter informações de indicadores.

CArrayObj   Deals;
datetime    last_deal;
int         wpr_handle,rsi_handle,adx_handle;
double      rsi[],adx[],wpr[];

No início da função OnInit, inicializaremos os indicadores.

int OnInit()
  {
//--- Obtenção do identificador do indicador RSI
   rsi_handle=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod,PRICE_CLOSE);
   if(rsi_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get RSI handle");
      Print("Handle = ",rsi_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Obtenção do identificador do indicador WPR
   wpr_handle=iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod);

   if(wpr_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get WPR handle");
      Print("Handle = ",wpr_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Obtenção do identificador do indicador ADX
   adx_handle=iADX(Symbol(),PERIOD_CURRENT,ADXPeriod);
   if(adx_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get ADX handle");
      Print("Handle = ",adx_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }

Em seguida, associamos buffers de indicador a matrizes dinâmicas.

//--- indicator buffers mapping
   SetIndexBuffer(0,Buffer_Probability,INDICATOR_CALCULATIONS);
   SetIndexBuffer(1,Buffer_DealsCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,Buffer_TradeSignal,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,Buffer_ProfitFactor,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,Buffer_ProfitCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,Buffer_TakeProfit,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,Buffer_StopLoss,INDICATOR_CALCULATIONS);
   SetIndexBuffer(7,Buffer_DealsCountCurrent,INDICATOR_CALCULATIONS);
   SetIndexBuffer(8,Buffer_ProfitCountCurrent,INDICATOR_CALCULATIONS);

Atribuímos as propriedades de timeseries a todas as matrizes.

   ArraySetAsSeries(Buffer_Probability,true);
   ArraySetAsSeries(Buffer_ProfitFactor,true);
   ArraySetAsSeries(Buffer_TradeSignal,true);
   ArraySetAsSeries(Buffer_DealsCount,true);
   ArraySetAsSeries(Buffer_ProfitCount,true);
   ArraySetAsSeries(Buffer_TakeProfit,true);
   ArraySetAsSeries(Buffer_StopLoss,true);
   ArraySetAsSeries(Buffer_DealsCountCurrent,true);
   ArraySetAsSeries(Buffer_ProfitCountCurrent,true);
//--- 
   ArraySetAsSeries(rsi,true);
   ArraySetAsSeries(wpr,true);
   ArraySetAsSeries(adx,true);

No final da função, redefinimos tanto a matriz de trades como a data da última transação e atribuímos um nome ao nosso indicador.

   Deals.Clear();
   last_deal=0;
//---
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Indicator");
//---
   return(INIT_SUCCEEDED);
  }

Vamos carregar os dados atuais dos indicadores na função GetIndValue. Na entrada, a função especificada receberá a profundidade necessária do histórico dos dados carregados e, na saída, a função retornará o número de elementos carregados. Os dados do indicador em si são armazenados em matrizes declaradas globalmente.

int GetIndValue(int depth)
  {
   if(CopyBuffer(wpr_handle,MAIN_LINE,0,depth,wpr)<=0 || CopyBuffer(adx_handle,MAIN_LINE,0,depth,adx)<=0 || CopyBuffer(rsi_handle,MAIN_LINE,0,depth,rsi)<=0)
      return -1;
   depth=MathMin(ArraySize(rsi),MathMin(ArraySize(wpr),ArraySize(adx)));
//---
   return depth;
  }

Para verificar os sinais para abrir trades, criaremos as funções de espelho BuySignal e SellSignal. Detalhes do código das funções podem ser encontrados no anexo.

A principal funcionalidade, como em qualquer indicador, será concentrada na função OnCalculate. As operações dessa função podem ser logicamente divididas em 2 threads:

  1. Ao recalcular mais de uma barra (a primeira execução após a inicialização ou a abertura de uma nova barra). Nessa thread, verificaremos tanto os sinais para abertura de trades em cada barra não registrada quanto o processamento de stop-loss de trades abertos com base nos dados históricos do período M1.
  2. Em cada novo tick, quando uma nova barra ainda não está formada, verificamos apenas o acionamento de stop-losses de posições abertas.

No início da função, verificamos o número de novas barras desde a última execução da função. Se essa é a primeira execução da função após a inicialização do indicador, definiremos a profundidade do recálculo do indicador como não mais do que a profundidade de teste necessária e colocaremos os buffers do indicador no estado inicial.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   int total=rates_total-prev_calculated;
   if(prev_calculated<=0)
     {
      total=fmin(total,HistoryDepth);
//---
      ArrayInitialize(Buffer_Probability,0);
      ArrayInitialize(Buffer_ProfitFactor,0);
      ArrayInitialize(Buffer_TradeSignal,0);
      ArrayInitialize(Buffer_DealsCount,0);
      ArrayInitialize(Buffer_ProfitCount,0);
      ArrayInitialize(Buffer_TakeProfit,TakeProfit*_Point);
      ArrayInitialize(Buffer_StopLoss,StopLoss*_Point);
      ArrayInitialize(Buffer_DealsCountCurrent,0);
      ArrayInitialize(Buffer_ProfitCountCurrent,0);
     }

Em seguida, é realizada a operação da primeira thread lógica, quando há uma abertura de uma nova vela. Primeiro, carregamos os dados atuais dos indicadores usados. Se ocorrer erro de carregamento de dados, sairemos da função até o próximo tick, aguardando que os indicadores sejam recalculados.

   if(total>0)
     {
      total=MathMin(GetIndValue(total+2),rates_total);
      if(total<=0)
         return prev_calculated;

Em seguida, atribuímos a propriedade de timeseries às matrizes de preços de entrada.

      if(!ArraySetAsSeries(open,true) || !ArraySetAsSeries(high,true) || !ArraySetAsSeries(low,true) || !ArraySetAsSeries(close,true)
         || !ArraySetAsSeries(time,true) || !ArraySetAsSeries(spread,true))
         return prev_calculated;

Isso é seguido pelo ciclo principal de recálculo de cada barra. No início do ciclo, inicializamos os buffers de indicador para a barra recalculada.

      for(int i=total-3;i>=0;i--)
        {
         Buffer_TakeProfit[i]=TakeProfit*_Point;
         Buffer_StopLoss[i]=StopLoss*_Point;
         Buffer_DealsCount[i]=Buffer_DealsCountCurrent[i]=0;
         Buffer_ProfitCount[i]=Buffer_ProfitCountCurrent[i]=0;

Em seguida, verificamos se ainda temos um trade na barra que está sendo calculada. Se não, então verificamos os sinais dos indicadores para entrada na posição chamando as funções previamente criadas. Se houver um sinal para abrir uma posição, criamos um trade virtual e registramos o sinal correspondente no buffer de sinal.

         if(last_deal<time[i])
           {
            if(BuySignal(i))
              {
               double open_price=open[i]+spread[i]*_Point;
               double sl=open_price-StopLoss*_Point;
               double tp=open_price+TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_BUY,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=1;
              }
            else /*BuySignal*/
            if(SellSignal(i))
              {
               double open_price=open[i];
               double sl=open_price+StopLoss*_Point;
               double tp=open_price-TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_SELL,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=-1;
              }
            else /*SellSignal*/
               Buffer_TradeSignal[i]=0;
           }

Em seguida, trabalhamos com posições abertas. A primeira coisa que fazemos é verificar o timeframe atual. Se o indicador estiver trabalhando no período M1, verificaremos o acionamento de stop-losses de posições abertas de acordo com as séries temporais obtidas nos parâmetros da função OnCalculate. Caso contrário, teremos que carregar os dados do timeframe de minutos.

         if(Deals.Total()>0)
           {
            if(PeriodSeconds()!=60)
              {
               MqlRates rates[];
               int rat=CopyRates(_Symbol,PERIOD_M1,time[i],(i>0 ? time[i-1] : TimeCurrent()),rates);

Após o carregamento das cotações, organizamos um ciclo para verificar a execução de stop-losses de trades abertos em cada barra de minutos. A soma dos trades fechados e lucrativos é resumida nos buffers de indicador correspondentes para a barra recalculada. O processamento direto da matriz de trades é realizado na função CheckDeals para cujos parâmetros transferimos os dados da vela de minutos verificada. O algoritmo da função será comentado abaixo.

               int closed=0, profit=0;
               for(int r=0;(r<rat && Deals.Total()>0);r++)
                 {
                  CheckDeals(rates[r].open,rates[r].high,rates[r].low,rates[r].close,rates[r].spread,rates[r].time,closed,profit);
                  if(closed>0)
                    {
                     Buffer_DealsCountCurrent[i]+=closed;
                     Buffer_ProfitCountCurrent[i]+=profit;
                    }
                 }

Isso é seguido por blocos alternativos semelhantes que verificam os trades usando os dados do timeframe atual em casos de falha ao carregar cotações de minuto ou erro no funcionamento do indicador no timeframe M1.

               if(rat<0)
                 {
                  CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
                  Buffer_DealsCountCurrent[i]+=closed;
                  Buffer_ProfitCountCurrent[i]+=profit;
                 }
              }
            else /* PeriodSeconds()!=60 */
              {
               int closed=0, profit=0;
               CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
               Buffer_DealsCountCurrent[i]+=closed;
               Buffer_ProfitCountCurrent[i]+=profit;
              }
           } /* Deals.Total()>0 */

No final, com o trabalho realizado calcularemos as estatísticas de trabalho de nossa estratégia. Calculamos o número de trades abertos no período em teste e quantos deles foram fechados com lucro.

         Buffer_DealsCount[i+1]=NormalizeDouble(Buffer_DealsCount[i+2]+Buffer_DealsCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_ProfitCount[i+1]=NormalizeDouble(Buffer_ProfitCount[i+2]+Buffer_ProfitCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_DealsCount[i]=NormalizeDouble(Buffer_DealsCount[i+1]+Buffer_DealsCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth] : 0),0);
         Buffer_ProfitCount[i]=NormalizeDouble(Buffer_ProfitCount[i+1]+Buffer_ProfitCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth] : 0),0);

Se houver trades acionados, calculamos a probabilidade de obter lucro no trade e o fator de lucro da estratégia para o período testado. Para evitar mudanças repentinas na probabilidade de obter lucro, esse indicador será suavizado usando a fórmula da média exponencial usando o período médio especificado nos parâmetros do indicador.

         if(Buffer_DealsCount[i]>0)
           {
            double pr=2.0/(AveragePeriod-1.0);
            Buffer_Probability[i]=((i+1)<rates_total && Buffer_Probability[i+1]>0 && Buffer_DealsCount[i+1]>=AveragePeriod ? Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100*pr+Buffer_Probability[i+1]*(1-pr) : Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100);
            if(Buffer_DealsCount[i]>Buffer_ProfitCount[i])
              {
               double temp=(Buffer_ProfitCount[i]*TakeProfit)/(StopLoss*(Buffer_DealsCount[i]-Buffer_ProfitCount[i]));
               Buffer_ProfitFactor[i]=((i+1)<rates_total && Buffer_ProfitFactor[i+1]>0 ? temp*pr+Buffer_ProfitFactor[i+1]*(1-pr) : temp);
              }
            else
               Buffer_ProfitFactor[i]=TakeProfit*Buffer_ProfitCount[i];
           }
        }
     }

A thread para calcular cada tick tem uma lógica semelhante, portanto, não é aconselhável fornecer uma descrição completa do mesmo no artigo. O código completo de todas as funções do indicador pode ser encontrado no anexo.

Anteriormente, escrevi que a verificação do acionamento de stop-losses de trades existentes é realizada na função CheckDeals. Vamos dar uma olhada no seu algoritmo de trabalho. Nos parâmetros, a função recebe cotações da barra analisada e dois links para variáveis a fim de retornar o número de trades fechados e trades lucrativos.

No início da função, redefinimos as variáveis retornadas e declaramos a variável lógica resultante.

bool CheckDeals(double open,double high,double low,double close,int spread,datetime time,int &closed, int &profit)
  {
   closed=0;
   profit=0;
   bool result=true;

Em seguida, a função organiza um ciclo para percorrer todos os trades na matriz. No ciclo, os ponteiros para os objetos das transações são obtidos alternadamente da matriz. No caso de um ponteiro errôneo para o objeto, excluímos esse trade da matriz e prosseguimos com o próximo trade. Se houver um erro na execução de operações, atribuímos false à variável resultante.

   for(int i=0;i<Deals.Total();i++)
     {
      CDeal *deal=Deals.At(i);
      if(CheckPointer(deal)==POINTER_INVALID)
        {
         if(Deals.Delete(i))
            i--;
         else
            result=false;
         continue;
        }

O próximo passo é verificar se o trade foi aberto no momento da abertura da vela. Caso contrário, prosseguimos com o próximo trade.

      if(deal.GetTime()>time)
         continue;

Por último, chamando o método Tick do trade marcado para cada preço, verificamos o acionamento de stop-loss do trade, alternadamente, para preços de abertura, fechamento máximo e mínimo. O algoritmo deste método é descrito acima no começo dessa seção. Deve-se observar que, para trades de compra e venda, a sequência de verificação é diferente. Primeiro, é verificado se o stop-loss está ativado e, em seguida, é checado o take-profit. Essa abordagem pode, em parte, dar como sendo menor o desempenho da negociação, mas permitirá reduzir as perdas na negociação futura. Quando qualquer uma das stop-loss é acionada, o número de trades fechados aumenta, já no caso de obtermos lucro, aumentamos o número de trades lucrativos. Após o fechamento, o trade é removido da matriz para evitar o recálculo.

      if(deal.Tick(open,spread))
        {
         closed++;
         if(deal.GetProfit()>0)
            profit++;
         if(Deals.Delete(i))
            i--;
         if(CheckPointer(deal)!=POINTER_INVALID)
            delete deal;
         continue;
        }
      switch(deal.Type())
        {
         case POSITION_TYPE_BUY:
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
         case POSITION_TYPE_SELL:
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
        }
     }
//---
   return result;
  }

O código completo do indicador e todas as suas funções podem ser encontrados no anexo.

4. Criemos o EA

Após criar o testador-indicador, é hora de começar a criar nosso EA diretamente. Nos parâmetros de nosso EA, especificamos uma série de variáveis ​​estáticas (comuns a todos as rodadas) e, por analogia com o testador de estratégias, especificamos os valores iniciais, finais dos parâmetros que estão sendo alterados e o incremento dos valores alterados. Além disso, nos parâmetros do EA, especificaremos os critérios para selecionar sinais para entrar no mercado — essa é a probabilidade mínima de obter lucro e o fator de lucro mínimo para o período de teste. Além disso, a fim de manter a objetividade dos dados estatísticos obtidos, especificamos o número mínimo necessário de trades para o período testado.

input double               Lot                     =  0.01;
input int                  HistoryDepth            =  500;           //Depth of history(bars)
//--- Parâmetros do indicador RSI
input int                  RSIPeriod_Start         =  5;             //RSI Period
input int                  RSIPeriod_Stop          =  30;            //RSI Period
input int                  RSIPeriod_Step          =  5;             //RSI Period
//---
input double               RSITradeZone_Start      =  30;            //Overbaying/Overselling zone size Start
input double               RSITradeZone_Stop       =  30;            //Overbaying/Overselling zone size Stop
input double               RSITradeZone_Step       =  5;             //Overbaying/Overselling zone size Step
//--- Parâmetros do indicador WPR
input int                  WPRPeriod_Start         =  5;             //Period WPR Start
input int                  WPRPeriod_Stop          =  30;            //Period WPR Stop
input int                  WPRPeriod_Step          =  5;             //Period WPR Step
//---
input double               WPRTradeZone_Start      =  20;            //Overbaying/Overselling zone size Start
input double               WPRTradeZone_Stop       =  20;            //Overbaying/Overselling zone size Stop
input double               WPRTradeZone_Step       =  5;             //Overbaying/Overselling zone size Step
//--- Parâmetros do indicador ADX
input int                  ADXPeriod_Start         =  5;             //ADX Period Start
input int                  ADXPeriod_Stop          =  30;            //ADX Period Stop
input int                  ADXPeriod_Step          =  5;             //ADX Period Step
//---
input int                  ADXTradeZone_Start      =  40;            //Flat Level ADX Start
input int                  ADXTradeZone_Stop       =  40;            //Flat Level ADX Stop
input int                  ADXTradeZone_Step       =  10;            //Flat Level ADX Step
//--- Deals Settings
input int                  TakeProfit_Start        =  600;           //TakeProfit Start
input int                  TakeProfit_Stop         =  600;           //TakeProfit Stop
input int                  TakeProfit_Step         =  100;           //TakeProfit Step
//---
input int                  StopLoss_Start          =  200;           //StopLoss Start
input int                  StopLoss_Stop           =  200;           //StopLoss Stop
input int                  StopLoss_Step           =  100;           //StopLoss Step
//---
input double               MinProbability          =  60.0;          //Minimal Probability
input double               MinProfitFactor         =  1.6;           //Minimal Profitfactor
input int                  MinOrders               =  10;            //Minimal number of deals in history

Nas variáveis ​​globais, vamos declarar uma matriz para armazenar os identificadores dos testadores-indicadores e uma instância da classe de operações de negociação.

CArrayInt   ar_Handles;
CTrade      Trade;

Na função OnInit de nosso EA, organizamos uma série de ciclos aninhados para iterar todas as opções dos parâmetros testados e incluímos um teste separado de trades de compra e venda. Essa abordagem permitirá considerar o impacto das tendências globais que não são monitoradas pela estratégia testada. Dentro dos ciclos, inicializaremos os testadores-indicadores. Se ocorrer um erro ao carregar o indicador, sairemos da função com o resultado INIT_FAILED. Se o indicador for carregado com sucesso, adicionamos seu identificador à matriz.

int OnInit()
  {
//---
   for(int rsi=RSIPeriod_Start;rsi<=RSIPeriod_Stop;rsi+=RSIPeriod_Step)
      for(double rsi_tz=RSITradeZone_Start;rsi_tz<=RSITradeZone_Stop;rsi_tz+=RSITradeZone_Step)
         for(int wpr=WPRPeriod_Start;wpr<=WPRPeriod_Stop;wpr+=WPRPeriod_Step)
            for(double wpr_tz=WPRTradeZone_Start;wpr_tz<=WPRTradeZone_Stop;wpr_tz+=WPRTradeZone_Step)
               for(int adx=ADXPeriod_Start;adx<=ADXPeriod_Stop;adx+=ADXPeriod_Step)
                  for(double adx_tz=ADXTradeZone_Start;adx_tz<=ADXTradeZone_Stop;adx_tz+=ADXTradeZone_Step)
                     for(int tp=TakeProfit_Start;tp<=TakeProfit_Stop;tp+=TakeProfit_Step)
                        for(int sl=StopLoss_Start;sl<=StopLoss_Stop;sl+=StopLoss_Step)
                          for(int dir=0;dir<2;dir++)
                             {
                              int handle=iCustom(_Symbol,PERIOD_CURRENT,"::Indicators\\TestIndicator\\TestIndicator.ex5",HistoryDepth,
                                                                                                                        sl,
                                                                                                                        tp,
                                                                                                                        rsi,
                                                                                                                        rsi_tz,
                                                                                                                        wpr,
                                                                                                                        wpr_tz,
                                                                                                                        adx, 
                                                                                                                        adx_tz,
                                                                                                                        dir);
                              if(handle==INVALID_HANDLE)
                                 return INIT_FAILED;
                              ar_Handles.Add(handle);
                             }

Após a inicialização bem-sucedida de todos os testadores-indicadores, inicializamos a classe de operações de negociação e concluímos a função.

   Trade.SetAsyncMode(false);
   if(!Trade.SetTypeFillingBySymbol(_Symbol))
      return INIT_FAILED;
   Trade.SetMarginMode();
//---
   return(INIT_SUCCEEDED);
  }

Filtragem de sinais de entrada e operações de negociação são executadas na função OnTick. Como decidimos anteriormente que abriremos posições apenas na abertura da barra, verificaremos a ocorrência desse evento no início da função.

void OnTick()
  {
//---
   static datetime last_bar=0;
   datetime cur_bar=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);
   if(cur_bar==last_bar)
      return;

Nossa segunda limitação é a presença de não mais de um trade aberto por vez, portanto, se houver uma posição aberta, pararemos a execução da função.

   if(PositionSelect(_Symbol))
     {
      last_bar=cur_bar;
      return;
     }

Após verificar os pontos de controle, prosseguimos com o ciclo principal para iterar todos os indicadores em busca de sinais. No início do ciclo, tentamos carregar o buffer de sinal do indicador. Se o indicador ainda não foi recalculado, ou se não houver sinal para realizar uma operação de negociação, prosseguimos com o próximo indicador.

   int signal=0;
   double probability=0;
   double profit_factor=0;
   double tp=0,sl=0;
   bool ind_caclulated=false;
   double temp[];
   for(int i=0;i<ar_Handles.Total();i++)
     {
      if(CopyBuffer(ar_Handles.At(i),2,1,1,temp)<=0)
         continue;
      ind_caclulated=true;
      if(temp[0]==0)
         continue;

O próximo passo é verificar se o sinal recebido não contradiz os sinais recebidos anteriormente a partir de outros indicadores. A presença de sinais conflitantes aumenta a probabilidade de erro, por isso, saímos da função antes do início da próxima formação de vela.

      if(signal!=0 && temp[0]!=signal)
        {
         last_bar=cur_bar;
         return;
        }
      signal=(int)temp[0];

Em seguida, verificamos a presença do número mínimo necessário de trades no período testado. Se a amostra for insuficiente, avançamos para o próximo indicador.

      if(CopyBuffer(ar_Handles.At(i),1,1,1,temp)<=0 || temp[0]<MinOrders)
         continue;

Depois, é similarmente verificada a probabilidade de obter um trade lucrativo.

      if(CopyBuffer(ar_Handles.At(i),0,1,1,temp)<=0 || temp[0]<MathMax(probability,MinProbability))
         continue;

Se as discrepâncias na probabilidade de um trade lucrativo de acordo com o indicador analisado e as verificadas anteriormente forem inferiores a 1%, entre duas rodadas é selecionada o melhor por fator de lucro e por razão entre lucro e risco. Para trabalhos adicionais, são salvos os dados da melhor rodada.

      if(MathAbs(temp[0]-probability)<=1)
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MathMax(profit_factor,MinProfitFactor))
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         if(MathAbs(ind_profit_factor-profit_factor)<=0.01)
           {
            if(sl<=0 || tp/sl>=ind_tp/ind_sl)
               continue;
           }
//---
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }

Se a probabilidade de receber um trade lucrativo for claramente maior, é verificado o cumprimento dos requisitos para uma rodada segundo um fator de lucro. Atendidos todos os requisitos, as informações sobre a rodada são salvas para trabalhos futuros.

      else /* MathAbs(temp[0]-probability)<=1 */
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MinProfitFactor)
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }
     }

Se, após testados todos os testadores-indicadores, nenhum deles foi recalculado, saímos da função até o próximo tick, esperando que os indicadores sejam recalculados.

   if(!ind_caclulated)
      return;

Após verificação bem-sucedida de indicadores e na ausência de um sinal válido para realizar operações de negociação, saímos da função antes da formação de uma nova barra.

   last_bar=cur_bar;
//---
   if(signal==0 || probability==0 || profit_factor==0 || tp<=0 || sl<=0)
      return;

No final da função, se houver um sinal para abrir uma posição, enviamos uma ordem de acordo com a melhor rodada.

   if(signal==1)
     {
      double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      tp+=price;
      sl=price-sl;
      Trade.Buy(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
     }
   else
      if(signal==-1)
        {
         double price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
         tp=price-tp;
         sl+=price;
         Trade.Sell(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
        }
  }

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

5. Testando a abordagem

Para demonstrar o método, o EA construído foi testado otimizando paralelamente um EA clássico (com testes 'forward' usando intervalos de parâmetros semelhantes) ​​e preservando o intervalo de tempo de teste. A fim de cumprir condições iguais, foi criado um EA que iniciava apenas um testador-indicador e que abria trades em todos os seus sinais sem filtrar dados estatísticos. Sua estrutura era semelhante à do EA construído acima, com exceção do bloco de filtragem de consistência de sinal. O código completo do EA pode ser encontrado no anexo (ClassicExpert.mq5).

O teste de EAs foi realizado no timeframe H1 durante 7 meses de 2018. Para o teste 'forward' do EA clássico, foi deixado 1/3 do período de teste.

Parâmetros de teste

Como parâmetros para otimização, foram tomados os períodos de cálculo dos indicadores. Para todos os indicadores, foi tomada uma faixa de valores de 5 a 30 em incrementos de 5.

Parâmetros de teste

Os resultados da otimização mostraram a inconsistência da estratégia proposta. Os valores dos parâmetros, que deram um pequeno lucro ao otimizar, mostraram perdas nos testes 'forward'. Ao mesmo tempo, nenhuma das rodadas mostrou lucro durante o período analisado.

Resultados da otimização

Os resultados da análise gráfica da otimização e do teste 'forward' mostraram uma mudança na estrutura do movimento de preços, daí que a zona de rentabilidade muda ao longo do período do indicador WPR.

Gráfico de otimização do WPRGráfico de teste 'forward' do WPR

Para testar o EA construído usando o método proposto, foram estabelecidos parâmetros de teste semelhantes, mantendo o período analisado. Para filtrar os sinais de abertura de posição, a probabilidade mínima de trade lucrativo foi fixada em 60% e o fator de lucro mínimo para o período de teste era 2. A profundidade do teste foi definida para 500 velas.

Testando o método proposto

Como resultado do teste, no período analisado o EA mostrou um rendimento com um fator de lucro real de 1,66. Deve-se notar que, ao realizar esse teste no modo visual, o agente de teste ocupou 1250 MB de RAM.

Fim do artigo

Esse artigo propôs uma ferramenta para a construção de EAs com otimização em tempo real. O teste mostrou que é possível usar essa abordagem para negociação real. O EA criado pelo método apresentado mostrou, por um lado, a possibilidade de obter lucro no período de rentabilidade da estratégia e, por outro lado, a capacidade de parar a negociação durante o período de perda. Ao mesmo tempo, permaneceram os requisitos exatos do método proposto no que diz respeito às características técnicas do computador utilizado. A velocidade do processador deve ser capaz de recalcular todos os indicadores carregados, enquanto a quantidade de RAM deve conter todos os indicadores usados.

Links

  1. Criamos uma nova estratégia de negociação usando uma ferramenta para decompor entradas para indicadores

Programas utilizados no artigo:

#
Nome
Tipo
Descrição
1Deal.mqhBiblioteca de classeClasse de trades virtuais
2TestIndicator.mq5IndicadorTestador-indicador
3RealTimeOptimization.mq5Expert AdvisorEA construído segundo o método proposto
4ClassicExpert.mq5Expert AdvisorEA baseado no método clássico para otimização comparativa