
Implementação do Exponente de Hurst Generalizado e do Teste de Razão de Variância em MQL5
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.
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:
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:
e
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.
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:
é 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.
Para comprimentos de série menores que 1000, ambos às vezes deram resultados inesperados.
Além disso, houve casos em que os resultados do GHE diferiam significativamente ao comparar testes usando valores brutos e valores logaritmicamente transformados.
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.
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.
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:
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.
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.
Por fim, testamos o EA fora da amostra com os parâmetros ideais.
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.
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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





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