English Русский 中文 Español Deutsch 日本語
preview
Implementação do Exponente de Hurst Generalizado e do Teste de Razão de Variância em MQL5

Implementação do Exponente de Hurst Generalizado e do Teste de Razão de Variância em MQL5

MetaTrader 5Sistemas de negociação |
143 0
Francis Dube
Francis Dube

Introdução

No artigo Calculando o Exponente de Hurst, fomos introduzidos ao conceito de análise fractal e como ela pode ser aplicada aos mercados financeiros. Nesse artigo, o autor descreveu o método da faixa redimensionada (R/S) para estimar o Exponente de Hurst. Neste artigo, adotamos uma abordagem diferente, demonstrando a implementação do Exponente de Hurst Generalizado (GHE) para classificar a natureza de uma série. Focaremos no uso do GHE para identificar símbolos forex que exibem uma tendência de reversão à média, com a esperança de explorar esse comportamento.

Para começar, discutiremos brevemente os fundamentos do GHE e como ele difere do Exponente de Hurst original. Em relação a isso, descreveremos um teste estatístico que pode ser usado para afirmar os resultados da análise do GHE, chamado Teste de Razão de Variância (VRT). A partir daí, passaremos para a aplicação do GHE na identificação de símbolos forex candidatos para negociação de reversão à média. Aqui, apresentamos um indicador para gerar sinais de entrada e saída. Que finalmente colocaremos à prova em um Expert Advisor básico.


Entendendo o Exponente de Hurst Generalizado

O Exponente de Hurst mede as propriedades de escala das séries temporais. Propriedades de escala são características fundamentais que descrevem como um sistema se comporta à medida que seu tamanho ou escala temporal muda. No contexto dos dados de séries temporais, as propriedades de escala fornecem insights sobre a relação entre diferentes escalas temporais e os padrões presentes nos dados. Para uma série estacionária, as mudanças nos valores subsequentes ao longo do tempo ocorrem de maneira mais gradual em comparação com o que aconteceria em uma caminhada aleatória geométrica. Para quantificar esse comportamento matematicamente, analisamos a taxa de difusão na série. A variância serve como uma métrica para expressar a taxa na qual outros valores divergem do primeiro na série.

Variância em Relação ao Hurst

Na fórmula acima, "K" representa um intervalo arbitrário no qual a análise é conduzida. Para obter uma visão melhor da natureza da série, precisaríamos avaliar a variância em outros intervalos também. Portanto, "K" pode ser atribuído a qualquer valor inteiro positivo menor que o comprimento da série. O maior intervalo aplicado é discricionário. É importante manter isso em mente. O Exponente de Hurst está, portanto, associado ao comportamento de escala da variância em diferentes intervalos. Usando a lei de potência, ele é definido por:

Exponente de Hurst Original

O GHE é uma generalização do original, onde o 2 é substituído por uma variável geralmente denotada como "q". Alterando assim as fórmulas acima para:

Variância em Relação ao Hurst Generalizado

e

Hurst Generalizado

O GHE expande o Hurst original ao analisar como diferentes características estatísticas das mudanças entre pontos consecutivos em uma série temporal variam com diferentes ordens de momentos. Em termos matemáticos, momentos são medidas estatísticas que descrevem a forma e as características de uma distribuição. O momento de ordem q é um tipo específico de momento, onde "q" é um parâmetro determinando a ordem. O GHE enfatiza características diferentes de uma série temporal para cada valor de "q". Especificamente, quando q=1, o resultado descreve as propriedades de escala da desvio absoluto. Enquanto q=2 é o mais importante ao investigar dependência de longo alcance.


Implementação do GHE em MQL5

Nesta seção, abordamos a implementação do GHE em MQL5. Após isso, o testaremos analisando amostras aleatórias de séries temporais geradas artificialmente. Nossa implementação está contida no arquivo GHE.mqh. O arquivo começa incluindo o VectorMatrixTools.mqh, que contém definições para várias funções de inicialização de tipos comuns de vetores e matrizes. O conteúdo deste arquivo é mostrado abaixo.

//+------------------------------------------------------------------+
//|                                            VectorMatrixTools.mqh |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//|Vector arange initialization                                      |
//+------------------------------------------------------------------+
template<typename T>
void arange(vector<T> &vec,T value=0.0,T step=1.0)
  {
   for(ulong i=0; i<vec.Size(); i++,value+=step)
      vec[i]=value;
  }
//+------------------------------------------------------------------+
//| Vector sliced initialization                                     |
//+------------------------------------------------------------------+
template<typename T>
void slice(vector<T> &vec,const vector<T> &toCopyfrom,ulong start=0,ulong stop=ULONG_MAX, ulong step=1)
  {
   start = (start>=toCopyfrom.Size())?toCopyfrom.Size()-1:start;
   stop  = (stop>=toCopyfrom.Size())?toCopyfrom.Size()-1:stop;
   step  = (step==0)?1:step;

   ulong numerator = (stop>=start)?stop-start:start-stop;
   ulong size = (numerator/step)+1;
   if(!vec.Resize(size))
     {
      Print(__FUNCTION__ " invalid slicing parameters for vector initialization");
      return;
     }

   if(stop>start)
     {
      for(ulong i =start, k = 0; i<toCopyfrom.Size() && k<vec.Size() && i<=stop; i+=step, k++)
         vec[k] = toCopyfrom[i];
     }
   else
     {
      for(long i = long(start), k = 0; i>-1 && k<long(vec.Size()) && i>=long(stop); i-=long(step), k++)
         vec[k] = toCopyfrom[i];
     }
  }
//+------------------------------------------------------------------+
//| Vector sliced initialization  using array                        |
//+------------------------------------------------------------------+
template<typename T>
void assign(vector<T> &vec,const T &toCopyfrom[],ulong start=0,ulong stop=ULONG_MAX, ulong step=1)
  {
   start = (start>=toCopyfrom.Size())?toCopyfrom.Size()-1:start;
   stop  = (stop>=toCopyfrom.Size())?toCopyfrom.Size()-1:stop;
   step  = (step==0)?1:step;

   ulong numerator = (stop>=start)?stop-start:start-stop;
   ulong size = (numerator/step)+1;

   if(size != vec.Size() &&  !vec.Resize(size))
     {
      Print(__FUNCTION__ " invalid slicing parameters for vector initialization");
      return;
     }

   if(stop>start)
     {
      for(ulong i =start, k = 0; i<ulong(toCopyfrom.Size()) && k<vec.Size() && i<=stop; i+=step, k++)
         vec[k] = toCopyfrom[i];
     }
   else
     {
      for(long i = long(start), k = 0; i>-1 && k<long(vec.Size()) && i>=long(stop); i-=long(step), k++)
         vec[k] = toCopyfrom[i];
     }
  }
//+------------------------------------------------------------------+
//| Matrix initialization                                            |
//+------------------------------------------------------------------+
template<typename T>
void rangetrend(matrix<T> &mat,T value=0.0,T step=1.0)
  {
   ulong r = mat.Rows();

   vector col1(r,arange,value,step);

   vector col2 = vector::Ones(r);

   if(!mat.Resize(r,2) || !mat.Col(col1,0) || !mat.Col(col2,1))
     {
      Print(__FUNCTION__ " matrix initialization error: ", GetLastError());
      return;
     }

  }
//+-------------------------------------------------------------------------------------+
//| ols design Matrix initialization with constant and first column from specified array|
//+-------------------------------------------------------------------------------------+
template<typename T>
void olsdmatrix(matrix<T> &mat,const T &toCopyfrom[],ulong start=0,ulong stop=ULONG_MAX, ulong step=1)
  {
   vector col0(1,assign,toCopyfrom,start,stop,step);

   ulong r = col0.Size();

   if(!r)
     {
      Print(__FUNCTION__," failed to initialize first column ");
      return;
     }

   vector col1 = vector::Ones(r);

   if(!mat.Resize(r,2) || !mat.Col(col0,0) || !mat.Col(col1,1))
     {
      Print(__FUNCTION__ " matrix initialization error: ", GetLastError());
      return;
     }

  }
//+------------------------------------------------------------------+
//|vector to array                                                   |
//+------------------------------------------------------------------+
bool vecToArray(const vector &in, double &out[])
  {
//---
   if(in.Size()<1)
     {
      Print(__FUNCTION__," Empty vector");
      return false;
     }
//---
   if(ulong(out.Size())!=in.Size() && ArrayResize(out,int(in.Size()))!=int(in.Size()))
     {
      Print(__FUNCTION__," resize error ", GetLastError());
      return false;
     }
//---
   for(uint i = 0; i<out.Size(); i++)
      out[i]=in[i];
//---
   return true;
//---
  }
//+------------------------------------------------------------------+
//| difference a vector                                               |
//+------------------------------------------------------------------+
vector difference(const vector &in)
  {
//---
   if(in.Size()<1)
     {
      Print(__FUNCTION__," Empty vector");
      return vector::Zeros(1);
     }
//---
   vector yy,zz;
//---
   yy.Init(in.Size()-1,slice,in,1,in.Size()-1,1);
//---
   zz.Init(in.Size()-1,slice,in,0,in.Size()-2,1);
//---
   return yy-zz;
  }
//+------------------------------------------------------------------+


 GHE.mqh contém a definição para a função "gen_hurst()" e sua sobrecarga. Uma funciona fornecendo os dados a serem analisados em um vetor e a outra espera que sejam fornecidos em um array. A função também recebe um inteiro "q", e parâmetros inteiros opcionais "lower" e "upper" com valores padrão. Este é o mesmo "q" mencionado na descrição do GHE na seção anterior. Os dois últimos parâmetros são opcionais, "lower" e "upper", que juntos definem o intervalo de lags nos quais a análise será conduzida, análogo ao intervalo de valores de "K" nas fórmulas acima.  

//+--------------------------------------------------------------------------+
//|overloaded gen_hurst() function that works with series contained in vector|
//+--------------------------------------------------------------------------+
double general_hurst(vector &data, int q, int lower=0,int upper=0)
  {
   double series[];

   if(!vecToArray(data,series))
      return EMPTY_VALUE;
   else
      return general_hurst(series,q,lower,upper);
  }


Quando ocorre um erro, a função retornará o equivalente à constante incorporada EMPTY_VALUE, juntamente com uma mensagem de string útil exibida na aba Experts do terminal. Dentro de "gen_hurst()", a rotina começa verificando os argumentos passados para ela. Certificando-se de que eles atendem às seguintes condições:

  • "q" não pode ser menor que 1.
  • "lower" não pode ser configurado para um valor menor que 2 e também não pode ser maior ou igual a "upper".
  • Enquanto o argumento "upper" não pode ser maior que metade do tamanho da série de dados sendo analisada. Se qualquer uma dessas condições não for atendida, a função sinalizará imediatamente um erro.
if(data.Size()<100)
     {
      Print("data array is of insufficient length");
      return EMPTY_VALUE;
     }

   if(lower>=upper || lower<2 ||  upper>int(floor(0.5*data.Size())))
     {
      Print("Invalid input for lower and/or upper");
      return EMPTY_VALUE;
     }

   if(q<=0)
     {
      Print("Invalid input for q");
      return EMPTY_VALUE;
     }

   uint len = data.Size();

   int k =0;

   matrix H,mcord,lmcord;
   vector n_vector,dv,vv,Y,ddVd,VVVd,XP,XY,PddVd,PVVVd,Px_vector,Sqx,pt;
   double dv_array[],vv_array[],mx,SSxx,my,SSxy,cc1,cc2,N;

   if(!H.Resize(ulong(upper-lower),1))
     {
      Print(__LINE__," ",__FUNCTION__," ",GetLastError());
      return EMPTY_VALUE;
     }

   for(int i=lower; i<upper; i++)
     {
      vector x_vector(ulong(i),arange,1.0,1.0);

      if(!mcord.Resize(ulong(i),1))
        {
         Print(__LINE__," ",__FUNCTION__," ",GetLastError());
         return EMPTY_VALUE;
        }

      mcord.Fill(0.0);


O funcionamento interno da função começa com um loop "for" de 'lower' a 'upper', e para cada 'i', ele cria um vetor 'x_vector' com 'i' elementos usando a função 'arange'. Em seguida, redimensiona a matriz 'mcord' para ter 'i' linhas e uma coluna.

 for(int j=1; j<i+1; j++)
        {
         if(!diff_array(j,data,dv,Y))
            return EMPTY_VALUE;

O loop interno começa usando a função auxiliar "diff_array()" para calcular as diferenças no array 'data' e armazená-las nos vetores 'dv' e 'Y'.

 N = double(Y.Size());

         vector X(ulong(N),arange,1.0,1.0);

         mx = X.Sum()/N;

         XP = MathPow(X,2.0);

         SSxx = XP.Sum() - N*pow(mx,2.0);

         my = Y.Sum()/N;

         XY = X*Y;

         SSxy = XY.Sum() - N*mx*my;

         cc1 = SSxy/SSxx;

         cc2 = my - cc1*mx;

         ddVd = dv - cc1;

         VVVd = Y - cc1*X - cc2;

         PddVd = MathAbs(ddVd);

         PddVd = pow(PddVd,q);

         PVVVd = MathAbs(VVVd);

         PVVVd = pow(PVVVd,q);

         mcord[j-1][0] = PddVd.Mean()/PVVVd.Mean();
        }

É aqui que a variância em um lag específico é calculada. Os resultados são armazenados na matriz "mcord".

 Px_vector = MathLog10(x_vector);

      mx = Px_vector.Mean();

      Sqx = MathPow(Px_vector,2.0);

      SSxx = Sqx.Sum() - i*pow(mx,2.0);

      lmcord = log10(mcord);

      my = lmcord.Mean();

      pt = Px_vector*lmcord.Col(0);

      SSxy = pt.Sum() - i*mx*my;

      H[k][0]= SSxy/SSxx;

      k++;


Fora do loop interno, na última parte do loop externo, os valores principais da matriz "H" são atualizados. Finalmente, a função retorna a média da matriz 'H' dividida por 'q'.

 return H.Mean()/double(q);


Para testar nossa função GHE, a aplicação GHE.ex5, implementada como um Expert Advisor, foi preparada. Ela permite visualizar séries aleatórias com características predefinidas e observar como o GHE funciona. A interatividade total permite ajustar todos os parâmetros do GHE, bem como o comprimento das séries dentro dos limites. Uma característica interessante é a capacidade de transformar logaritmicamente as séries antes de aplicar o GHE, para testar se há algum benefício em pré-processar os dados dessa maneira.



Aplicação interativa do GHE



Todos sabemos que, quando se trata de aplicação no mundo real, os conjuntos de dados são afetados por ruído excessivo. Como o GHE gera uma estimativa, que é sensível ao tamanho da amostra, precisamos testar a significância do resultado. Isso pode ser feito realizando um teste de hipótese chamado Teste de Razão de Variância (VR).


O Teste de Razão de Variância

O Teste de Razão de Variância é um teste estatístico usado para avaliar a aleatoriedade de uma série temporal, examinando se a variância da série aumenta proporcionalmente ao comprimento do intervalo de tempo. O teste é baseado na ideia de que, se a série a ser testada segue uma caminhada aleatória, a variância das mudanças da série ao longo de um intervalo de tempo dado deve aumentar linearmente com o comprimento do intervalo. Se a variância aumentar a uma taxa mais lenta, isso pode indicar correlação serial nas mudanças da série, sugerindo que a série é previsível. O Teste de Razão de Variância testa se:



VRT



é igual a 1, onde:
- X() é a série temporal de interesse.
- K é um intervalo arbitrário.
- Var() denota a variância.

A hipótese nula do teste é que a série temporal segue uma caminhada aleatória, e, portanto, a razão de variância deve ser igual a 1. Uma razão de variância significativamente diferente de 1 pode levar ao rejeitamento da hipótese nula, sugerindo a presença de algum tipo de previsibilidade ou correlação serial na série temporal.


Implementação do Teste de Razão de Variância

O teste VR é implementado como a classe CVarianceRatio definida em VRT.mqh. Existem dois métodos que podem ser chamados para realizar um teste VR "Vrt()", um funciona com vetores e o outro com arrays. Os parâmetros do método são descritos abaixo:

  • "lags" especifica o número de períodos ou lags usados no cálculo da variância. No contexto de como queremos usar o teste VR para avaliar a significância de nossa estimativa GHE, podemos definir "lags" para os parâmetros "lower" ou "upper" correspondentes de "gen_hurst()". Esse valor não pode ser configurado para menos que 2. 
  • "trend" é uma enumeração que permite especificar o tipo de caminhada aleatória que queremos testar. Somente duas opções têm efeito, TREND_CONST_ONLY e TREND_NONE.
  • "debiased" indica se deve ser usada uma versão corrigida do teste, que é aplicável apenas se "overlap" for verdadeiro. Quando configurado como verdadeiro, a função emprega uma técnica de correção de viés para ajustar a estimativa da razão de variância, visando uma representação mais precisa da verdadeira relação entre as variâncias. Isso é benéfico principalmente quando se trabalha com séries de pequeno tamanho de amostra.
  • "overlap" indica se deve ser usado todos os blocos sobrepostos. Se falso, o comprimento da série menos um deve ser um múltiplo exato de "lags". Se essa condição não for atendida, alguns valores no final da série de entrada serão descartados.
  • "robust" seleciona se deve considerar heterocedasticidade (verdadeiro) ou apenas homocedasticidade (falso). Na análise estatística, um processo heterocedástico tem variância não constante, enquanto uma série homocedástica é caracterizada por variância constante. 

O método "Vrt()" retorna verdadeiro em caso de execução bem-sucedida, após o qual qualquer um dos métodos getter pode ser chamado para recuperar todos os aspectos do resultado do teste.

//+------------------------------------------------------------------+
//| CVarianceRatio  class                                            |
//| Variance ratio hypthesis test for a random walk                  |
//+------------------------------------------------------------------+
class CVarianceRatio
  {
private:
   double            m_pvalue;     //pvalue
   double            m_statistic;  //test statistic
   double            m_variance;   //variance
   double            m_vr;         //variance ratio
   vector            m_critvalues; //critical values

public:
                     CVarianceRatio(void);
                    ~CVarianceRatio(void);

   bool              Vrt(const double &in_data[], ulong lags, ENUM_TREND trend = TREND_CONST_ONLY, bool debiased=true, bool robust=true, bool overlap = true);
   bool              Vrt(const vector &in_vect, ulong lags, ENUM_TREND trend = TREND_CONST_ONLY, bool debiased=true, bool robust=true, bool overlap = true);

   double            Pvalue(void) { return m_pvalue;}
   double            Statistic(void) { return m_statistic;}
   double            Variance(void) { return m_variance;}
   double            VRatio(void) { return m_vr;}
   vector            CritValues(void) { return m_critvalues;}
  };


Dentro de "Vrt()", se "overlap" for falso, verificamos se o comprimento da série de entrada é divisível por "lags". Caso contrário, cortamos o final da série e emitimos um aviso sobre o comprimento dos dados. Em seguida, reatribuímos "nobs" com base no comprimento atualizado da série.. E calculamos "mu", o termo da tendência. Aqui calculamos as diferenças dos elementos adjacentes na série e as salvamos em "delta_y". Usando "delta_y", a variância é computada e salva na variável "sigma2_1". Se não houver sobreposição, calculamos a variância para blocos não sobrepostos. Caso contrário, calculamos a variância para blocos sobrepostos. Se "debiased" estiver habilitado junto com "overlap", ajustamos as variâncias. Aqui, "m_varianced" é calculado dependendo de "overlap" e "robust". Por fim, a razão de variância, a estatística de teste e o valor p são calculados.

//+------------------------------------------------------------------+
//| main method for computing Variance ratio test                    |
//+------------------------------------------------------------------+
bool CVarianceRatio::Vrt(const vector &in_vect,ulong lags,ENUM_TREND trend=1,bool debiased=true,bool robust=true,bool overlap=true)
  {
   ulong nobs = in_vect.Size();

   vector y = vector::Zeros(2),delta_y;

   double mu;

   ulong nq = nobs - 1;

   if(in_vect.Size()<1)
     {
      Print(__FUNCTION__, "Invalid input, no data supplied");
      return false;
     }

   if(lags<2 || lags>=in_vect.Size())
     {
      Print(__FUNCTION__," Invalid input for lags");
      return false;
     }

   if(!overlap)
     {
      if(nq % lags != 0)
        {
         ulong extra = nq%lags;
         if(!y.Init(5,slice,in_vect,0,in_vect.Size()-extra-1))
           {
            Print(__FUNCTION__," ",__LINE__);
            return false;
           }
         Print("Warning:Invalid length for input data, size is not exact multiple of lags");
        }
     }
   else
      y.Copy(in_vect);

   nobs = y.Size();

   if(trend == TREND_NONE)
      mu = 0;
   else
      mu = (y[y.Size()-1] - y[0])/double(nobs - 1);

   delta_y = difference(y);

   nq = delta_y.Size();

   vector mudiff = delta_y - mu;

   vector mudiff_sq = MathPow(mudiff,2.0);

   double sigma2_1 = mudiff_sq.Sum()/double(nq);

   double sigma2_q;

   vector delta_y_q;

   if(!overlap)
     {
      vector y1,y2;
      if(!y1.Init(3,slice,y,lags,y.Size()-1,lags) ||
         !y2.Init(3,slice,y,0,y.Size()-lags-1,lags))
        {
         Print(__FUNCTION__," ",__LINE__);
         return false;
        }

      delta_y_q = y1-y2;

      vector delta_d = delta_y_q - double(lags) * mu;

      vector delta_d_sqr = MathPow(delta_d,2.0);

      sigma2_q = delta_d_sqr.Sum()/double(nq);
     }
   else
     {
      vector y1,y2;
      if(!y1.Init(3,slice,y,lags,y.Size()-1) ||
         !y2.Init(3,slice,y,0,y.Size()-lags-1))
        {
         Print(__FUNCTION__," ",__LINE__);
         return false;
        }

      delta_y_q = y1-y2;

      vector delta_d = delta_y_q - double(lags) * mu;

      vector delta_d_sqr = MathPow(delta_d,2.0);

      sigma2_q = delta_d_sqr.Sum()/double(nq*lags);
     }


   if(debiased && overlap)
     {
      sigma2_1 *= double(nq)/double(nq-1);
      double mm = (1.0-(double(lags)/double(nq)));
      double m = double(lags*(nq - lags+1));// * (1.0-double(lags/nq));
      sigma2_q *= double(nq*lags)/(m*mm);
     }

   if(!overlap)
      m_variance = 2.0 * (lags-1);
   else
      if(!robust)
         m_variance = double((2 * (2 * lags - 1) * (lags - 1)) / (3 * lags));
      else
        {
         vector z2, o, p;
         z2=MathPow((delta_y-mu),2.0);
         double scale = pow(z2.Sum(),2.0);
         double theta = 0;
         double delta;
         for(ulong k = 1; k<lags; k++)
           {
            if(!o.Init(3,slice,z2,k,z2.Size()-1) ||
               !p.Init(3,slice,z2,0,z2.Size()-k-1))
              {
               Print(__FUNCTION__," ",__LINE__);
               return false;
              }
            o*=double(nq);
            p/=scale;
            delta = o.Dot(p);
            theta+=4.0*pow((1.0-double(k)/double(lags)),2.0)*delta;
           }
         m_variance = theta;
        }
   m_vr = sigma2_q/sigma2_1;

   m_statistic = sqrt(nq) * (m_vr - 1)/sqrt(m_variance);

   double abs_stat = MathAbs(m_statistic);

   m_pvalue = 2 - 2*CNormalDistr::NormalCDF(abs_stat);

   return true;
  }


Para testar a classe, modificamos a aplicação GHE.ex5 usada para demonstrar a função "gen_hurst()". Porque o GHE é definido por uma faixa de lags em que a análise é concentrada. Podemos calibrar o VRT para testar a significância dos resultados do GHE sobre a mesma faixa de lags. Ao executar o VRT nos lags mínimo e máximo, devemos obter informações suficientes. No GHE.ex5, a razão de variância no "inferior" é exibida primeiro, antes da razão de variância no lag "superior".
Lembre-se de que uma razão de variância que se desvia significativamente é uma indicação de previsibilidade nos dados. Razões de variância próximas de 1 sugerem que a série não está distante de uma caminhada aleatória. Ao brincar com a aplicação, testando diferentes combinações de parâmetros, percebemos que os resultados do GHE e do VRT são afetados pelo tamanho da amostra.

Classificação incorreta da tendência

Para comprimentos de série menores que 1000, ambos às vezes deram resultados inesperados.

Valores brutos

Além disso, houve casos em que os resultados do GHE diferiam significativamente ao comparar testes usando valores brutos e valores logaritmicamente transformados.

Transformados logaritmicamente



Agora que estamos familiarizados com o VRT e o GHE, podemos aplicá-los à nossa estratégia de reversão à média. Se for sabido que uma série de preços é de reversão à média, podemos estimar de forma aproximada o que o preço fará com base em sua atual desvio da média. A base de nossa estratégia dependerá da análise das características de uma série de preços ao longo de um período de tempo determinado. Usando essa análise, formamos um modelo que estima pontos em que o preço provavelmente retornará após se desviar muito da norma. Precisamos de alguma forma de medir e quantificar essa divergência para gerar sinais de entrada e saída.


O Z-score

O z-score mede o número de desvios padrão que o preço está de sua média. Ao normalizar os preços, o z-score oscila em torno de zero. Vamos ver como fica o gráfico do z-score implementando-o como um indicador. O código completo está mostrado abaixo.

//+------------------------------------------------------------------+
//|                                                       Zscore.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include<VectorMatrixTools.mqh>
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Zscore
#property indicator_label1  "Zscore"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      z_period = 10;
//--- indicator buffers
double         ZscoreBuffer[];
vector vct;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,ZscoreBuffer,INDICATOR_DATA);
//----
   PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0);
//---
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,z_period-1);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//---
   if(rates_total<z_period)
     {
      Print("Insufficient history");
      return -1;
     }
//---
   int limit;
   if(prev_calculated<=0)
      limit = z_period - 1;
   else
      limit = prev_calculated - 1;
//---
   for(int i = limit; i<rates_total; i++)
     {
      vct.Init(ulong(z_period),assign,close,ulong(i-(z_period-1)),i,1);
      if(vct.Size()==ulong(z_period))
         ZscoreBuffer[i] = (close[i] - vct.Mean())/vct.Std();
      else
         ZscoreBuffer[i]=0.0;
     }

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+


A partir deste gráfico, podemos ver que os valores do indicador agora parecem mais distribuídos normalmente.

Indicador Zscore



Os sinais de negociação são gerados quando o z-score se desvia significativamente de 0, ultrapassa algum limiar derivado historicamente. Um z-score extremamente negativo sinaliza um momento apropriado para entrar comprado, enquanto o oposto indica um bom momento para entrar vendido. Isso significa que precisamos de dois limiares para os sinais de compra e venda. Um negativo (para compra) e um positivo (para venda). Com nossas entradas cobertas, passamos a determinar quando uma saída existe. Uma opção é derivar outro conjunto de limiares que funcione quando em uma posição específica (comprado ou vendido). Quando vendidos, podemos procurar fechar nossa posição à medida que o z-score retorna em direção a 0. De maneira similar, fechamos uma posição comprada quando o z-score sobe em direção a 0 a partir do nível extremo no qual compramos.

Indicador com níveis de limiar

Agora que temos nossas entradas e saídas definidas usando o indicador Zscore.ex5 Vamos juntar tudo isso em um EA. O código está mostrado abaixo.

//+------------------------------------------------------------------+
//|                                                MeanReversion.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#resource "\\Indicators\\Zscore.ex5"
#include<ExpertTools.mqh>
//---Input parameters
input int  PeriodLength  = 10;
input double LotsSize = 0.01;
input double  LongOpenLevel = -2.0;
input double  ShortOpenLevel = 2.0;
input double  LongCloseLevel = -0.5;
input double  ShortCloseLevel = 0.5;
input ulong  SlippagePoints = 10;
input ulong  MagicNumber    = 123456;
//---
 int indi_handle;
//---
 double zscore[2];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(PeriodLength<2)
    {
     Print("Invalid parameter value for PeriodLength");
     return INIT_FAILED;
    }
//---
   if(!InitializeIndicator())
    return INIT_FAILED;
//---        
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---    
      int signal = GetSignal();
//---      
      if(SumMarketOrders(MagicNumber,_Symbol,-1))
       {
        if(signal==0)
         CloseAll(MagicNumber,_Symbol,-1);
        return; 
       }
      else
        OpenPosition(signal);
//---   
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Initialize indicator                                             |
//+------------------------------------------------------------------+
bool InitializeIndicator(void)
{
 indi_handle = INVALID_HANDLE;
//---
 int try = 10;
//---
 while(indi_handle == INVALID_HANDLE && try>0)
  {
   indi_handle = (indi_handle==INVALID_HANDLE)?iCustom(NULL,PERIOD_CURRENT,"::Indicators\\Zscore.ex5",PeriodLength):indi_handle;
   try--;
  }
//---
 if(try<0)
  {
   Print("Failed to initialize Zscore indicator ");
   return false;
  }
//---
 return true;
}
//+------------------------------------------------------------------+
//|Get the signal to trade or close                                  |
//+------------------------------------------------------------------+
int GetSignal(const int sig_shift=1)
{
//---
 if( CopyBuffer(indi_handle,int(0),sig_shift,int(2),zscore)<2)
   {
    Print(__FUNCTION__," Error copying from indicator buffers: ", GetLastError());
    return INT_MIN;
   } 
//---   
 if(zscore[1]<LongOpenLevel && zscore[0]>LongOpenLevel)
     return (1);
//---   
 if(zscore[1]>ShortOpenLevel && zscore[0]<ShortOpenLevel)
     return (-1);          
//---   
 if((zscore[1]>LongCloseLevel && zscore[0]<LongCloseLevel) ||
    (zscore[1]<ShortCloseLevel && zscore[0]>ShortCloseLevel))
     return (0);
//---
 return INT_MIN;
//--- 
}
//+------------------------------------------------------------------+
//|  Go long or short                                                |
//+------------------------------------------------------------------+
bool OpenPosition(const int sig)
{

 long pid;
//--- 
 if(LastOrderOpenTime(pid,NULL,MagicNumber)>=iTime(NULL,0,0))
   return false;
//---   
 if(sig==1)
   return SendOrder(_Symbol,0,ORDER_TYPE_BUY,LotsSize,SlippagePoints,0,0,NULL,MagicNumber);
 else
  if(sig==-1)
    return SendOrder(_Symbol,0,ORDER_TYPE_SELL,LotsSize,SlippagePoints,0,0,NULL,MagicNumber); 
//--- 
  return false;                   
}

É bem simples, não há níveis de stoploss ou takeprofit definidos. Nosso objetivo é primeiro otimizar o EA para encontrar o período ideal para o indicador Zscore, bem como os limiares de entrada e saída ideais. Vamos otimizar com base em vários anos de dados e testar os parâmetros otimizados fora da amostra, mas antes de fazer isso, faremos uma breve pausa para apresentar outra ferramenta interessante. No livro Algorithmic Trading: Winning Strategies And Their Rationale, o autor Ernest Chan descreve uma ferramenta interessante para desenvolver estratégias de reversão à média, chamada de Half life of mean reversion.


Half life of mean reversion

O half-life da reversão à média representa o tempo necessário para que uma divergência em relação à média diminua pela metade. No contexto do preço de um ativo, o half-life da reversão à média indica a rapidez com que o preço tende a voltar à sua média histórica após se desviar dela. É uma medida da velocidade com que o processo de reversão à média ocorre. Matematicamente, o half-life pode ser relacionado à velocidade de reversão à média pela equação:

Half life of mean reversion


Onde:
- HL é o half-life.
- log() é o logaritmo natural.
- lambda é a velocidade de reversão à média.

Em termos práticos, um half-life mais curto implica um processo de reversão à média mais rápido, enquanto um half-life mais longo sugere uma reversão mais lenta. O conceito de half-life pode ser usado para ajustar parâmetros em estratégias de negociação de reversão à média, ajudando a otimizar pontos de entrada e saída com base em dados históricos e na velocidade observada de reversão à média. O half-life de reversão à média é derivado da representação matemática de um processo de reversão à média, tipicamente modelado como um processo de Ornstein-Uhlenbeck. O processo de Ornstein-Uhlenbeck é uma equação diferencial estocástica que descreve uma versão contínua no tempo do comportamento de reversão à média.

De acordo com Chan, é possível determinar se a reversão à média é uma estratégia apropriada ao calcular o half-life de reversão à média. Primeiro, se lambda for positivo, a reversão à média não deve ser aplicada. Mesmo quando lambda é negativo e muito próximo de zero, aplicar a reversão à média é desencorajado, pois indica que o half-life será longo. A reversão à média deve ser aplicada apenas quando o half-life for razoavelmente curto.

O half-life de reversão à média é implementado como uma função em MeanReversionUtilities.mqh, o código está abaixo. Ele é calculado fazendo regressão da série de preços contra a série de diferenças entre valores subsequentes. Lambda é igual ao parâmetro beta do modelo de regressão e o half-life é computado dividindo -log(2) por lambda.

//+------------------------------------------------------------------+
//|Calculate Half life of Mean reversion                             |
//+------------------------------------------------------------------+
double mean_reversion_half_life(vector &data, double &lambda)
  {
//---
   vector yy,zz;
   matrix xx;
//---
   OLS ols_reg;
//---
   yy.Init(data.Size()-1,slice,data,1,data.Size()-1,1);
//---
   zz.Init(data.Size()-1,slice,data,0,data.Size()-2,1);
//---
   if(!xx.Init(zz.Size(),2) || !xx.Col(zz,0) || !xx.Col(vector::Ones(zz.Size()),1) || !ols_reg.Fit(yy-zz,xx))
     {
      Print(__FUNCTION__," Error in calculating half life of mean reversion ", GetLastError());
      return 0;
     }
//---
   vector params = ols_reg.ModelParameters();
   lambda = params[0];
//---
   return (-log(2)/lambda);
//---
  }


Usaremos isso em conjunto com o GHE e o VRT para testar uma amostra de preços ao longo de um período selecionado de anos, para alguns símbolos forex. Usaremos os resultados dos testes para selecionar um símbolo apropriado para o qual aplicaremos o EA que construímos anteriormente. Será otimizado no mesmo período de anos e finalmente testado fora da amostra. O script abaixo aceita uma lista de símbolos candidatos que serão testados usando o GHE, o VRT e o half-life.

//+------------------------------------------------------------------+
//|                                                 SymbolTester.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<MeanReversionUtilities.mqh>
#include<GHE.mqh>
#include<VRT.mqh>

//--- input parameters
input string   Symbols = "EURUSD,GBPUSD,USDCHF,USDJPY";//Comma separated list of symbols to test
input ENUM_TIMEFRAMES TimeFrame = PERIOD_D1;
input datetime StartDate=D'2020.01.02 00:00:01';
input datetime StopDate=D'2015.01.18 00:00:01';
input int Q_parameter = 2;
input int MinimumLag = 2;
input int MaximumLag = 100;
input bool ApplyLogTransformation = true;
//---
CVarianceRatio vrt;
double ghe,hl,lb,vlower,vupper;
double prices[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---Check Size input value
   if(StartDate<=StopDate)
     {
      Print("Invalid input for StartDater or StopDate");
      return;
     }
//---array for symbols
   string symbols[];
//---process list of symbols from user input
   int num_symbols = StringSplit(Symbols,StringGetCharacter(",",0),symbols);
//---incase list contains ending comma
   if(symbols[num_symbols-1]=="")
      num_symbols--;
//---in case there are less than two symbols specified
   if(num_symbols<1)
     {
      Print("Invalid input. Please list at least one symbol");
      return;
     }
//---loop through all paired combinations from list
   for(uint i=0; i<symbols.Size(); i++)
     {

      //--- get prices for the pair of symbols
      if(CopyClose(symbols[i],TimeFrame,StartDate,StopDate,prices)<1)
        {
         Print("Failed to copy close prices ", ::GetLastError());
         return;
        }
      //---
      if(ApplyLogTransformation && !MathLog(prices))
        {
         Print("Mathlog error ", GetLastError());
         return;
        }
      //---
      if(!vrt.Vrt(prices,MinimumLag))
         return;
      //---
      vlower = vrt.VRatio();
      //---
      if(!vrt.Vrt(prices,MaximumLag))
         return;
      //---
      vupper = vrt.VRatio();
      //---
      ghe = general_hurst(prices,Q_parameter,MinimumLag,MaximumLag);
      //---
      hl = mean_reversion_half_life(prices,lb);
      //--- output the results
      Print(symbols[i], " GHE:  ", DoubleToString(ghe)," | Vrt: ",DoubleToString(vlower)," ** ",DoubleToString(vupper)," | HalfLife ",DoubleToString(hl)," | Lambda: ",DoubleToString(lb));
     }
  }
//+------------------------------------------------------------------+

Executar o script produz os seguintes resultados:

19:31:03.143    SymbolTester (USDCHF,D1)        EURUSD GHE:  0.44755644 | Vrt: 0.97454284 ** 0.61945905 | HalfLife 85.60548208 | Lambda: -0.00809700
19:31:03.326    SymbolTester (USDCHF,D1)        GBPUSD GHE:  0.46304381 | Vrt: 1.01218672 ** 0.82086185 | HalfLife 201.38001205 | Lambda: -0.00344199
19:31:03.509    SymbolTester (USDCHF,D1)        USDCHF GHE:  0.42689382 | Vrt: 1.02233286 ** 0.47888803 | HalfLife 28.90550869 | Lambda: -0.02397976
19:31:03.694    SymbolTester (USDCHF,D1)        USDJPY GHE:  0.49198795 | Vrt: 0.99875744 ** 1.06103587 | HalfLife 132.66433924 | Lambda: -0.00522482

O símbolo USDCHF tem os resultados de teste mais promissores ao longo do período de datas selecionado. Portanto, otimizaremos os parâmetros do EA para negociar o USDCHF. Um exercício interessante seria selecionar o período do Zscore para otimização e ver se ele difere do half-life calculado.

Configurações de teste dentro da amostra

Configurações de parâmetros dentro da amostra



Aqui podemos ver o período ideal do Zscore, que está muito próximo do half-life de reversão à média calculado. O que é encorajador. Claro, seria necessário mais testes extensivos para determinar a utilidade do half-life.

Resultados da otimização


Gráfico dentro da amostra

Backtest dentro da amostra


Por fim, testamos o EA fora da amostra com os parâmetros ideais.

Configurações fora da amostra


Os resultados não são bons. Provavelmente devido ao fato de que o mercado está em fluxo contínuo, de modo que as características observadas durante o período em que o EA foi otimizado não se aplicam mais. Precisamos de limiares de entrada e saída mais dinâmicos que levem em conta as mudanças nas dinâmicas subjacentes do mercado.


Desempenho fora da amostra


Podemos usar o que foi aprendido aqui como base para o desenvolvimento futuro. Uma possibilidade que podemos explorar é a aplicação das ferramentas descritas aqui para implementar uma estratégia de negociação de pares. Em vez de o indicador Zscore ser baseado em uma única série de preços, ele pode ser baseado no spread de dois instrumentos co-integrados ou correlacionados.


Conclusão

Neste artigo, demonstramos a implementação do Exponente Generalizado de Hurst em MQL5 e mostramos como ele pode ser usado para determinar as características de uma série de preços. Também analisamos a aplicação do teste de razão de variância, bem como o half-life da reversão à média. A tabela a seguir é uma descrição de todos os arquivos anexados ao artigo.

Arquivo
Descrição
Mql5\include\ExpertTools.mqh
Contém definições de funções para conduzir operações comerciais usadas no EA de Reversão à Média.
Mql5\include\GHE.mqh
Contém a definição da função que implementa o Exponente Generalizado de Hurst.
Mql5\include\OLS.mqh
Contém a definição da classe OLS que implementa a regressão dos mínimos quadrados ordinários.
Mql5\include\VRT.mqh
Contém a definição da classe CVarianceRatio que encapsula o teste de razão de variância.
Mql5\include\VectorMatrixTools.mqh
Contém várias definições de funções para inicializar rapidamente vetores e matrizes comuns.
Mql5\include\TestUtilities.mqh
Contém várias declarações usadas na definição da classe OLS.
Mql5\include\MeanReversionUtilities.mqh
Contém várias definições de funções, incluindo uma que implementa o half-life de reversão à média.
Mql5\Indicators\Zscore.mq5
Indicador usado no EA de Reversão à Média.
Mql5\scripts\SymbolTester.mq5
Script que pode ser usado para testar símbolos de reversão à média.
Mql5\Experts\GHE.ex5
Aplicativo de consultor especialista que pode ser usado para explorar e experimentar com as ferramentas GHE e VRT.
Mql5\scripts\MeanReversion.mq5
EA que demonstra uma estratégia simples de reversão à média.


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/14203

Arquivos anexados |
ExpertTools.mqh (20.03 KB)
GHE.mqh (4.04 KB)
OLS.mqh (13.36 KB)
TestUtilities.mqh (4.36 KB)
VRT.mqh (6.54 KB)
Zscore.mq5 (2.68 KB)
SymbolTester.mq5 (2.93 KB)
GHE.ex5 (341.94 KB)
MeanReversion.mq5 (4.23 KB)
Mql5.zip (359.46 KB)
Do básico ao intermediário: Eventos (II) Do básico ao intermediário: Eventos (II)
Neste artigo iremos ver que nem sempre precisamos implementar as coisas de uma ou de outra maneira. Existem maneiras alternativas de se fazer as coisas. Entender conceitos que foram explicados em artigos anteriores é primordial para conseguir compreender adequadamente o que será visto neste artigo. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como uma aplicação final, onde o objetivo não seja o estudo dos conceitos aqui mostrados.
Redes neurais em trading: Superpoint Transformer (SPFormer) Redes neurais em trading: Superpoint Transformer (SPFormer)
Neste artigo, apresentamos um método de segmentação de objetos 3D baseado no Superpoint Transformer (SPFormer), que elimina a necessidade de agregação intermediária de dados. Isso acelera o processo de segmentação e melhora o desempenho do modelo.
Simulação de mercado (Parte 10): Sockets (IV) Simulação de mercado (Parte 10): Sockets (IV)
Aqui neste artigo mostrei o que você precisa fazer para começar a usar o Excel para controlar o MetaTrader 5. Mas faremos isto de uma forma bastante interessante. Para fazer isto iremos usar um Add-in no Excel. Isto para não precisar de fato fazer uso do VBA presente no Excel. Se você não sabe de que Add-in estou falando. Veja este artigo e aprenda como fazer para programar em Python diretamente dentro do Excel.
Otimização de nuvens atmosféricas — Atmosphere Clouds Model Optimization (ACMO): Teoria Otimização de nuvens atmosféricas — Atmosphere Clouds Model Optimization (ACMO): Teoria
Este artigo é dedicado ao algoritmo meta-heurístico Atmosphere Clouds Model Optimization (ACMO), que modela o comportamento das nuvens para resolver problemas de otimização. O algoritmo utiliza os princípios de geração, movimento e dispersão de nuvens, adaptando-se às "condições climáticas" no espaço de soluções. O artigo explora como a simulação meteorológica do algoritmo encontra soluções ótimas em um espaço complexo de possibilidades e descreve detalhadamente as etapas do ACMO, incluindo a preparação do "céu", o nascimento das nuvens, seu deslocamento e a concentração de chuva.