
Análise causal de séries temporais usando entropia de transferência
Introdução
A entropia de transferência é uma ferramenta estatística que quantifica a quantidade de informação transferida de uma série temporal para outra, fornecendo insights sobre a natureza e o comportamento de uma variável alvo. Neste artigo, aprofundamos o conceito de causalidade estatística, calculada em termos de entropia de transferência. Exploramos como esse método pode revelar a direção da influência causal entre vários processos. Além disso, fornecemos uma descrição detalhada de uma implementação em MQL5 para medir a entropia de transferência, demonstrando como essa técnica pode ser aplicada na prática para analisar séries temporais potencialmente acopladas. Ao utilizar a entropia de transferência, nosso objetivo é identificar variáveis que podem melhorar tarefas preditivas.
Causalidade
Dados empíricos podem ser enganosos. O fato de duas variáveis parecerem se mover juntas não significa que uma cause a outra, por isso o ditado "correlação não é causalidade" é verdadeiro. A correlação simplesmente mede o quanto duas variáveis estão conectadas, não o porquê de estarem conectadas. Por exemplo, imagine uma forte correlação entre as vendas de sorvete e o preço de uma ação durante o verão. Isso não significa que comprar sorvete faz o preço da ação subir! Um culpado mais provável é um fator oculto, como a própria estação, afetando ambas as variáveis de forma independente. Da mesma forma, pode existir uma relação entre as ações de uma empresa e o preço do ouro, mas a verdadeira causa pode ser outra, como o sentimento geral do mercado ou a inflação influenciando ambos os preços. Esses exemplos destacam que dados correlacionados podem ser enganosos. Eles mostram uma conexão, mas não a razão por trás disso. Para entender realmente se uma coisa causa outra, precisamos de ferramentas mais avançadas.
O conceito de causalidade, a noção de que um evento causa outro, é fundamental para a exploração científica. No entanto, definir causalidade de forma precisa apresenta um desafio multifacetado, com profundas considerações filosóficas, físicas e estatísticas. Idealmente, uma causa produziria invariavelmente um único efeito. No entanto, isolar um único fator causal da complexa teia de influências que impactam um resultado pode ser difícil. Por exemplo, um aumento no volume de negociação pode correlacionar com uma alta no preço das ações, mas outros fatores, como o sentimento do mercado e a liberação de dados econômicos, também podem desempenhar um papel significativo. Nesses cenários, os pesquisadores empregam técnicas estatísticas para inferir relações causais.
A natureza da causalidade, seja determinística (resultado garantido) ou probabilística (influenciando a probabilidade do evento subsequente), depende do processo subjacente. Em sistemas determinísticos, o primeiro evento leva demonstravelmente ao segundo, como observado na queda previsível de um objeto deixado cair. Por outro lado, em sistemas probabilísticos, o foco é em saber se o primeiro evento melhora nossa capacidade de prever a ocorrência do segundo. Por exemplo, enquanto a chuva recente pode estar associada à floração subsequente de flores, outros fatores ambientais também podem contribuir. Nesses casos, a questão é se o conhecimento do primeiro evento melhora nossa capacidade de prever o segundo evento.
O economista Clive Granger, baseando-se no trabalho de Norbert Wiener, desenvolveu o conceito de relações causais, postulando que um primeiro sinal causa um segundo sinal se os valores futuros do segundo sinal podem ser melhor explicados usando informações passadas de ambos os sinais, primeiro e segundo, em vez de usar apenas valores defasados do segundo sinal. Granger baseou sua definição de causalidade em dois princípios. Primeiro, um efeito não pode se manifestar antes de sua causa. Segundo, uma causa conterá informações únicas transferidas para o efeito. Esses princípios sugerem que, para quantificar a causalidade, precisamos entender as propriedades temporais das variáveis envolvidas, bem como uma medida de seu conteúdo informativo. Isso torna as séries temporais bem adequadas para análise causal.
Devido à natureza dos dados de séries temporais, podemos analisar como a informação de uma série em um determinado momento afeta a previsibilidade de outra série em um momento posterior. Granger define causalidade como uma redução na incerteza. Se saber os valores passados da série X melhora nossa previsão do valor futuro da série Y, em comparação com usar apenas os valores passados de Y, então X é considerada preditiva de Y. Com base nessa ideia, Granger desenvolveu testes para causalidade usando séries temporais defasadas e modelos autorregressivos. Ele propôs que não existe uma relação causal entre X e Y a menos que incluir os valores passados de X melhore significativamente a previsão dos valores futuros de Y. Essa melhoria é geralmente medida por uma redução no erro de previsão, como um melhor ajuste em um modelo de regressão. A causalidade de Granger nos permite detectar estatisticamente relações entre séries temporais. No entanto, é importante notar suas limitações. A causalidade de Granger identifica apenas uma relação direcional, não necessariamente um mecanismo causal definitivo. Podem haver outras causas em jogo fora daquelas sobre as quais temos dados. Além disso, como a causalidade de Granger é essencialmente baseada na estrutura autorregressiva, ela é mais eficaz para descobrir relações causais lineares. Causalidades não-lineares exigem uma abordagem diferente.
Matematicamente, a causalidade de Granger pode ser expressa considerando duas séries temporais, X e Y. Os valores defasados de cada uma são denotados por X(t−k) e Y(t−k), representando uma defasagem de k. A defasagem máxima considerada é denotada por p. Aplicando o modelo autorregressivo, o valor futuro de Y é regressado sobre seus próprios valores passados.
Esta expressão considera o valor futuro de Y em termos apenas de seus valores passados. Ao introduzir X no modelo, os valores futuros são expressos em termos dos valores passados de X e Y.
Se a inclusão dos valores passados de X melhorar significativamente a previsão de Y, em comparação com o modelo que usa apenas os valores passados de Y, então X é considerado um causador de Granger de Y. Isso é tipicamente avaliado testando a hipótese nula de que os coeficientes são zero. Se essa hipótese nula for rejeitada, isso indica que X fornece informações preditivas significativas sobre Y além do que é contido nos valores passados de Y sozinhos. Para testar se X é um causador de Granger de Y, comparamos os dois modelos sob o teste de hipótese:
- Hipótese nula: X não causa Y de Granger.
- Hipótese alternativa: X causa Y de Granger.
Um teste F é usado para comparar o ajuste do modelo restrito (sem X) e do modelo irrestrito (com X), examinando os resíduos dos respectivos modelos. A soma dos resíduos quadrados restritos são os resíduos do modelo sem X, e a soma dos resíduos quadrados irrestritos vem do modelo com X. O valor do estatístico F é calculado como:
Onde n é o número de observações. O valor calculado do estatístico F é comparado ao valor crítico da distribuição F com p e n − 2p − 1 graus de liberdade. Se o estatístico F for maior que o valor crítico, rejeitamos a hipótese nula e concluímos que X causa Y de Granger. Alternativamente, o estatístico F também pode ser calculado usando um teste de análise de variância (ANOVA). Sua fórmula é dada abaixo.
Entropia de Transferência
Nos primeiros dias da teoria da informação, os cientistas usaram a informação mútua para entender como processos acoplados interagiam. Esse conceito, baseado na entropia de Claude Shannon, nos diz se a informação dentro de uma série temporal se sobrepõe a outra. Em termos mais simples, revela se podemos codificar ambas as séries juntas usando menos informação do que codificando-as separadamente. Por isso, a informação mútua às vezes é chamada de redundância. Um processo compartilha informações com outro, permitindo que o segundo processo seja descrito de forma eficiente reutilizando informações já capturadas do primeiro processo.
Formalmente, a informação mútua entre dois processos estocásticos, X(t) e Y(t), é estabelecida quando a soma de suas entropias marginais excede a entropia conjunta do sistema combinado. Esse relacionamento matemático reflete a redução na incerteza sobre o sistema combinado em comparação com os processos individuais. Em outras palavras, captura o grau em que a informação sobre um processo pode ser usada para reduzir a entropia inerente associada ao outro. Como a entropia é determinada exclusivamente pela distribuição de probabilidade subjacente, qualquer distribuição desse tipo pode ser caracterizada por um valor de entropia associado. Esse valor quantifica o nível de inesperado associado a um resultado específico, dada a distribuição de probabilidade conhecida.
Esse conceito se torna particularmente relevante no contexto da causalidade de Granger. Ao investigar relações causais potenciais entre séries temporais, o objetivo é reduzir a incerteza associada a um processo alvo incorporando informações de um processo fonte potencial. Se a inclusão de uma série temporal secundária reduz comprovadamente a entropia da distribuição do processo alvo, sugere a presença de uma influência causal estatística da série fonte para a série alvo. Essa redução é chamada de entropia de transferência.
A entropia de transferência (ET) baseia-se no conceito de divergência de Kullback-Leibler para medir a direção da transferência de informação entre duas séries temporais. Especificamente, a ET baseia-se na ideia de informação mútua condicional e pode ser expressa usando a distância de Kullback-Leibler (KL), também conhecida como divergência de Kullback-Leibler ou entropia relativa. A divergência KL mede a diferença entre duas distribuições de probabilidade. Na ET, a distância KL mede a diferença entre a distribuição de probabilidade conjunta do estado atual de Y e os estados passados de X e Y, e o produto das distribuições marginais desses estados. Matematicamente, a transferência de informação de uma série temporal X para uma série temporal Y pode ser expressa como:
onde y(t+1) é o estado futuro de Y, y(t) é o estado passado de Y e x(t) é o estado passado de X. Esta formulação destaca que a entropia de transferência mede quanto a distribuição de probabilidade de y(t+1) muda ao considerar as informações de x(t) além de y(t).
Em 2009, Lionel Barnett, Adam Barrett e Anil Seth co-autores do artigo "Granger Causality and Transfer Entropy Are Equivalent for Gaussian Variables," demonstraram que, quando as séries temporais seguem uma distribuição Gaussiana, a entropia de transferência é equivalente à metade do estatístico F para a causalidade de Granger.
Este resultado fornece a definição de entropia de transferência linear que implementaremos em código mais adiante. Para contabilizar a causalidade não linear, estendemos o conceito de redução da incerteza seguindo o trabalho de Thomas Schreiber, que trata as séries temporais como um processo de Markov com distribuições de probabilidade de transição distintas.
A abordagem de Schreiber para modelar a redução de incerteza aproveita a teoria da informação tratando as séries temporais X(t) e Y(t) como processos de Markov com probabilidades de transição conhecidas p(x) e q(x). Diferentemente do modelo autorregressivo de Granger, que depende de modelos lineares, essa abordagem usa informação mútua condicional para descrever a transferência de informação. Como a informação mútua é derivada da diferença nas entropias, a informação mútua condicional é obtida condicionando cada termo de entropia sobre informações adicionais. A entropia de transferência é então calculada substituindo variáveis defasadas na equação de informação mútua condicional, permitindo analisar a transferência de informação de X(t) para Y(t) em uma defasagem específica k usando entropia mútua condicional.
Computacionalmente, esse método é atraente porque a entropia conjunta requer apenas uma distribuição de probabilidade. A entropia de transferência para uma única defasagem k pode ser expressa como quatro termos de entropia conjunta separados, os quais são facilmente computados com uma distribuição de probabilidade precisa a partir dos dados. A vantagem dessa fórmula é sua capacidade de lidar com mais dimensões defasadas. No entanto, cada defasagem adicional aumenta a dimensionalidade do espaço de estados em duas, impactando significativamente a capacidade de quantificar corretamente a entropia de transferência devido ao crescimento exponencial dos problemas de dados finitos associados à estimativa das densidades de probabilidade.
Uma força chave dessa abordagem é sua natureza não paramétrica. Ao contrário de outros métodos, ela não faz suposições sobre a distribuição subjacente dos dados além da estacionariedade, permitindo sua aplicação sem conhecimento prévio sobre os processos geradores dos dados. No entanto, essa vantagem vem com uma ressalva: os resultados dependem fortemente de uma estimativa precisa da distribuição subjacente. A entropia de transferência exige a aproximação da verdadeira distribuição de probabilidade dos processos estocásticos envolvidos, utilizando dados limitados para calcular os quatro termos de entropia. A precisão dessa estimativa impacta significativamente a confiabilidade dos achados de entropia de transferência. Tendo isso em mente, deve-se considerar a possibilidade de que os valores de entropia calculados possam ser espúrios. Portanto, precisamos de alguma forma de determinar a robustez dos resultados.
Nossa insistência em usar uma abordagem não paramétrica para estimar a entropia de transferência vem com o desafio considerável de garantir que os resultados transmitam alguma verdade, em vez de lixo. Portanto, é necessário considerar uma abordagem mais informativa para interpretar a entropia de transferência, que envolve avaliar a significância estatística do valor estimado. Testes de significância comuns envolvem embaralhar os dados da série temporal um número predefinido de vezes. A entropia de transferência é então calculada para cada versão embaralhada. O valor p é calculado como a proporção de dados embaralhados com entropia de transferência inferior ao valor original.
Outra abordagem requer calcular o número de desvios padrão que um resultado está distante da média dos dados embaralhados. Como o embaralhamento perturba a estrutura temporal, espera-se que a média das entropias de transferência embaralhadas seja próxima de zero. A dispersão dos dados em torno dessa média reflete a significância do resultado original. O valor calculado é chamado de z-score. Os z-scores geralmente requerem menos embaralhamentos em comparação com os valores p, tornando-os computacionalmente mais eficientes.
No caso do valor p, o objetivo é obter uma probabilidade o mais próxima possível de zero. Enquanto um z-score indicativo de significância estatística deve ser superior a 3.0.
Implementação em MQL5
O código que implementa as ferramentas para quantificar a entropia de transferência e determinar sua significância está contido em transfer_entropy.mqh. O arquivo contém a definição da classe, CTransEntropy, juntamente com outras classes auxiliares e funções. Essa classe oferece uma estrutura para a análise estatística de dados de séries temporais, especificamente voltada para a avaliação de relações causais entre variáveis. Ela expõe dois métodos distintos para quantificar a causalidade linear de Granger (entropia de transferência linear) e a entropia de transferência não linear. A qual é calculada em ambas as direções, proporcionando uma visão mais completa do fluxo de informação entre as variáveis.
Para tratar a potencial não-estacionariedade dos dados, a classe incorpora um procedimento de janelas. Os usuários podem definir o tamanho e o passo da janela, permitindo a análise dos dados em segmentos menores e sobrepostos. Essa abordagem gera resultados específicos para cada janela, facilitando a identificação de variações temporais na força da causalidade. Além disso, ela mitiga os desafios associados à análise de dados não estacionários. A classe também fornece um mecanismo integrado de teste de significância. Os usuários podem especificar o número de embaralhamentos dos dados a serem realizados, preservando as distribuições marginais. Com base nesses conjuntos de dados embaralhados, a classe calcula valores p e z-scores para a entropia de transferência em cada direção. Esses valores estatísticos fornecem informações essenciais sobre a probabilidade de que as relações causais observadas ou a transferência de informação sejam devido ao acaso, aumentando a robustez da análise.
Uma instância da classe é instanciada usando o construtor padrão sem parâmetros.
public: CTransEntropy(void) { if(!m_transfer_entropies.Resize(2)) Print(__FUNCTION__, " error ", GetLastError()); }
Os usuários devem então chamar o método Initialize(), que inicializa o objeto com um conjunto de dados fornecido e configura vários parâmetros para análise.
bool Initialize(matrix &in, ulong endog_index, ulong exog_index, ulong lag, bool maxLagOnly=true, ulong winsize=0,ulong winstride=0) { if(!lag || lag>in.Rows()/2) { Print(__FUNCTION__, " Invalid parameter(s) : lag must be > 0 and < rows/2"); return false; } if(endog_index==exog_index) { Print(__FUNCTION__, " Invalid parameter(s) : endog cannot be = exog "); return false; } if(!m_dataset.Resize(in.Rows(),2)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } if(!m_dataset.Col(in.Col(endog_index),0) || !m_dataset.Col(in.Col(exog_index),1)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } if(!m_wins.Initialize(m_dataset,lag,maxLagOnly,winsize,winstride)) return false; m_tlag = lag; m_endog = endog_index; m_exog = exog_index; m_maxlagonly = maxLagOnly; return true; }
O primeiro parâmetro necessário é uma matriz com pelo menos duas colunas, onde as séries temporais a serem analisadas devem ser colocadas nas colunas da matriz de entrada. Se estiver lidando com dados não estacionários, recomenda-se diferenciar os dados antecipadamente. Os segundo e terceiro parâmetros são os índices das colunas da matriz de dados de entrada, indicando as séries temporais endógena (dependente) e exógena (independente), respectivamente.
O quarto parâmetro, lag, define o parâmetro de defasagem considerado na análise. O próximo parâmetro booleano, maxLagOnly, determina se a defasagem define um único termo (se verdadeiro) ou todos os valores defasados até e incluindo a defasagem (se falso). O penúltimo parâmetro, winsize, denota o comprimento da janela. Se definido como 0, nenhuma janela será aplicada aos dados. Por fim, winstride define opcionalmente o passo da janela para operações de janela, definindo o passo entre as janelas consecutivas à medida que elas passam sobre os dados da série temporal.
O método começa garantindo que os índices endógenos e exógenos não sejam os mesmos. Caso contrário, imprime uma mensagem de erro e retorna falso. A matriz interna m_dataset é redimensionada para armazenar o conjunto de dados bivariados a ser analisado. Em seguida, ele copia as colunas especificadas por endog_index e exog_index da matriz de entrada para a primeira e segunda colunas de m_dataset, respectivamente. Se for solicitado o uso de janelas, a classe auxiliar CDataWindows é utilizada para aplicar a janela à matriz m_dataset. Uma vez feito isso, o método define as variáveis internas com os parâmetros fornecidos para uso posterior.
//+------------------------------------------------------------------+ //|class that generates windows of the dataset to be analyzed | //+------------------------------------------------------------------+ class CDataWindows { private: matrix m_dwins[], m_data; ulong m_lag, m_win_size, m_stride_size; bool m_max_lag_only, m_has_windows; matrix applylags(void) { matrix out=np::sliceMatrixRows(m_data,m_lag); if(m_max_lag_only) { if(!out.Resize(out.Rows(),m_data.Cols()+2)) { Print(__FUNCTION__, " error ", GetLastError()); return matrix::Zeros(1,1); } for(ulong i = 2; i<4; i++) { vector col = m_data.Col(i-2); col = np::sliceVector(col,0,col.Size()-m_lag); if(!out.Col(col,i)) { Print(__FUNCTION__, " error ", GetLastError()); return matrix::Zeros(1,1); } } } else { if(!out.Resize(out.Rows(),m_data.Cols()+(m_lag*2))) { Print(__FUNCTION__, " error ", GetLastError()); return matrix::Zeros(1,1); } for(ulong i = 0,k = 2; i<2; i++) { for(ulong t = 1; t<(m_lag+1); t++,k++) { vector col = m_data.Col(i); col = np::sliceVector(col,m_lag-t,col.Size()-t); if(!out.Col(col,k)) { Print(__FUNCTION__, " error ", GetLastError()); return matrix::Zeros(1,1); } } } } return out; } bool applywindows(void) { if(m_dwins.Size()) ArrayFree(m_dwins); for(ulong i = (m_stride_size+m_win_size); i<m_data.Rows(); i+=ulong(MathMax(m_stride_size,1))) { if(ArrayResize(m_dwins,int(m_dwins.Size()+1),100)<0) { Print(__FUNCTION__," error ", GetLastError()); return false; } m_dwins[m_dwins.Size()-1] = np::sliceMatrixRows(m_data,i-m_win_size,(i-m_win_size)+m_win_size); } return true; } public: CDataWindows(void) { } ~CDataWindows(void) { } bool Initialize(matrix &data, ulong lag, bool max_lag_only=true, ulong window_size=0, ulong window_stride =0) { if(data.Cols()<2) { Print(__FUNCTION__, " matrix should contain at least 2 columns "); return false; } m_data = data; m_max_lag_only = max_lag_only; if(lag) { m_lag = lag; m_data = applylags(); } if(window_size) { m_win_size = window_size; m_stride_size = window_stride; m_has_windows = true; if(!applywindows()) return false; } else { m_has_windows = false; if(m_dwins.Size()) ArrayFree(m_dwins); if(ArrayResize(m_dwins,1)<0) { Print(__FUNCTION__," error ", GetLastError()); return false; } m_dwins[0]=m_data; } return true; } matrix getWindowAt(ulong ind) { if(ind < ulong(m_dwins.Size())) return m_dwins[ind]; else { Print(__FUNCTION__, " Index out of bounds "); return matrix::Zeros(1,1); } } ulong numWindows(void) { return ulong(m_dwins.Size()); } bool hasWindows(void) { return m_has_windows; } };
Se o método Initialize() for bem-sucedido, os usuários podem chamar Calculate_Linear_TE() ou Calculate_NonLinear_TE() para testar a entropia de transferência linear e não linear, respectivamente. Ambos os métodos retornam um valor booleano ao serem completados. O método Calculate_Linear_TE() pode aceitar um parâmetro opcional, n_shuffles. Se n_shuffles for zero (o padrão), nenhum teste de significância será conduzido.
bool Calculate_Linear_TE(ulong n_shuffles=0) { ulong c = m_wins.numWindows(); matrix TE(c,2); matrix sTE(c,2); matrix pvals(c,2); matrix zscores(c,2); for(ulong i=0; i<m_wins.numWindows(); i++) { matrix df = m_wins.getWindowAt(i); m_transfer_entropies[0] = linear_transfer(df,0,1); m_transfer_entropies[1] = linear_transfer(df,1,0); if(!TE.Row(m_transfer_entropies,i)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } SigResult rlts; if(n_shuffles) { significance(df,m_transfer_entropies,m_endog,m_exog,m_tlag,m_maxlagonly,n_shuffles,rlts); if(!sTE.Row(rlts.mean,i) || !pvals.Row(rlts.pvalue,i) || !zscores.Row(rlts.zscore,i)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } } } m_results.TE_XY = TE.Col(0); m_results.TE_YX = TE.Col(1); m_results.p_value_XY = pvals.Col(0); m_results.p_value_YX = pvals.Col(1); m_results.z_score_XY = zscores.Col(0); m_results.z_score_YX = zscores.Col(1); m_results.Ave_TE_XY = sTE.Col(0); m_results.Ave_TE_YX = sTE.Col(1); return true; }
O método calcula a entropia de transferência linear utilizando o método de Granger. Isso é implementado no método privado, linear_transfer(). Os últimos dois parâmetros dessa rotina identificam a variável dependente e independente (coluna) na matriz de entrada. Ao chamar o método duas vezes com os índices das colunas trocados, podemos obter a entropia de transferência em ambas as direções.
double linear_transfer(matrix &testdata,long dep_index, long indep_index) { vector joint_residuals,independent_residuals; double entropy=0.0; OLS ols; double gc; vector y; matrix x,xx; matrix joint; if(m_maxlagonly) joint = np::sliceMatrixCols(testdata,2); else { if(!joint.Resize(testdata.Rows(), testdata.Cols()-1)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } matrix sliced = np::sliceMatrixCols(testdata,2); if(!np::matrixCopyCols(joint,sliced,1) || !joint.Col(testdata.Col(indep_index),0)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } } matrix indep = (m_maxlagonly)?np::sliceMatrixCols(testdata,dep_index+2,dep_index+3):np::sliceMatrixCols(testdata,(dep_index==0)?2:dep_index+m_tlag+1,(dep_index==0)?2+m_tlag:END); y = testdata.Col(dep_index); if(dep_index>indep_index) { if(m_maxlagonly) { if(!joint.SwapCols(0,1)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } } else { for(ulong i = 0; i<m_tlag; i++) { if(!joint.SwapCols(i,i+m_tlag)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } } } } if(!addtrend(joint,xx)) return entropy; if(!ols.Fit(y,xx)) return entropy; joint_residuals = ols.Residuals(); if(!addtrend(indep,x)) return entropy; if(!ols.Fit(y,x)) return entropy; independent_residuals = ols.Residuals(); gc = log(independent_residuals.Var()/joint_residuals.Var()); entropy = gc/2.0; return entropy; }
O método Calculate_NonLinear_TE() recebe um parâmetro adicional, numBins, juntamente com n_shuffles. Este parâmetro define o número de bins usados na estimativa da densidade de probabilidade das variáveis.
bool Calculate_NonLinear_TE(ulong numBins, ulong n_shuffles=0) { ulong c = m_wins.numWindows(); matrix TE(c,2); matrix sTE(c,2); matrix pvals(c,2); matrix zscores(c,2); for(ulong i=0; i<m_wins.numWindows(); i++) { matrix df = m_wins.getWindowAt(i); m_transfer_entropies[0] = nonlinear_transfer(df,0,1,numBins); m_transfer_entropies[1] = nonlinear_transfer(df,1,0,numBins); if(!TE.Row(m_transfer_entropies,i)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } SigResult rlts; if(n_shuffles) { significance(df,m_transfer_entropies,m_endog,m_exog,m_tlag,m_maxlagonly,n_shuffles,rlts,numBins,NONLINEAR_TE); if(!sTE.Row(rlts.mean,i) || !pvals.Row(rlts.pvalue,i) || !zscores.Row(rlts.zscore,i)) { Print(__FUNCTION__, " error ", GetLastError()); return false; } } } m_results.TE_XY = TE.Col(0); m_results.TE_YX = TE.Col(1); m_results.p_value_XY = pvals.Col(0); m_results.p_value_YX = pvals.Col(1); m_results.z_score_XY = zscores.Col(0); m_results.z_score_YX = zscores.Col(1); m_results.Ave_TE_XY = sTE.Col(0); m_results.Ave_TE_YX = sTE.Col(1); return true; }
O método de histograma é utilizado para estimar a densidade de probabilidade. Foi escolhido porque é o mais simples de implementar. A responsabilidade de calcular a versão generalizada da entropia de transferência é delegada aos métodos privados, nonlinear_entropy() e get_entropy().
double get_entropy(matrix &testdata, ulong num_bins) { vector hist; vector bounds[]; hist=vector::Ones(10); if(!np::histogramdd(testdata,num_bins,hist,bounds)) { Print(__FUNCTION__, " error "); return EMPTY_VALUE; } vector pdf = hist/hist.Sum(); vector lpdf = pdf; for(ulong i = 0; i<pdf.Size(); i++) { if(lpdf[i]==0.0) lpdf[i] = 1.0; } vector ent = pdf*log(lpdf); return -1.0*ent.Sum(); }
Os quatro valores componentes usados para calcular as entropias condicionais conjuntas e independentes são combinados em nonlinear_transfer() para obter a estimativa final.
double nonlinear_transfer(matrix &testdata,long dep_index, long indep_index, ulong numbins) { double entropy=0.0; matrix one; matrix two; matrix three; matrix four; if(m_maxlagonly) { if(!one.Resize(testdata.Rows(),3) || !two.Resize(testdata.Rows(),2) || !three.Resize(testdata.Rows(),2) || !four.Resize(testdata.Rows(),1) || !one.Col(testdata.Col(dep_index),0) || !one.Col(testdata.Col(dep_index+2),1) || !one.Col(testdata.Col(indep_index+2),2) || !two.Col(testdata.Col(indep_index+2),0) || !two.Col(testdata.Col(dep_index+2),1) || !three.Col(testdata.Col(dep_index),0) || !three.Col(testdata.Col(dep_index+2),1) || !four.Col(testdata.Col(dep_index),0)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } } else { if(!one.Resize(testdata.Rows(), testdata.Cols()-1) || !two.Resize(testdata.Rows(), testdata.Cols()-2) || !three.Resize(testdata.Rows(), m_tlag+1)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } matrix deplag = np::sliceMatrixCols(testdata,dep_index?dep_index+m_tlag+1:2,dep_index?END:2+m_tlag); matrix indlag = np::sliceMatrixCols(testdata,indep_index?indep_index+m_tlag+1:2,indep_index?END:2+m_tlag); //one if(!np::matrixCopyCols(one,deplag,1,1+m_tlag) || !np::matrixCopyCols(one,indlag,1+m_tlag) || !one.Col(testdata.Col(dep_index),0)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } //two if(!np::matrixCopyCols(two,indlag,indlag.Cols()) || !np::matrixCopyCols(two,deplag,indlag.Cols())) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } //three if(!np::matrixCopyCols(three,deplag,1) || !three.Col(testdata.Col(dep_index),0)) { Print(__FUNCTION__, " error ", GetLastError()); return entropy; } //four four = deplag; } double h1=get_entropy(one,numbins); double h2=get_entropy(two,numbins); double h3=get_entropy(three,numbins); double h4=get_entropy(four,numbins); // entropy = independent conditional entropy (h3-h4) - joint conditional entropy (h1-h2) entropy = (h3-h4) - (h1-h2); return entropy; }
Os resultados abrangentes do teste podem ser acessados usando o método get_results(), que retorna uma estrutura de vetores. Cada membro dessa estrutura se refere a um aspecto diferente dos resultados, com o comprimento de cada vetor dependendo dos parâmetros definidos pelo método Initialize() e do tipo de análise de entropia de transferência realizada.
//+------------------------------------------------------------------+ //| Transfer entropy results struct | //+------------------------------------------------------------------+ struct TEResult { vector TE_XY; vector TE_YX; vector p_value_XY; vector p_value_YX; vector z_score_XY; vector z_score_YX; vector Ave_TE_XY; vector Ave_TE_YX; };
As propriedades da estrutura de resultados são listadas abaixo.
Propriedade da Estrutura | Descrição |
---|---|
TE_XY | Entropia de transferência de exógena para para variável endógena |
TE_YX | Entropia de transferência de endógena para para variável exógena |
z_score_XY | Significância da entropia de transferência de exógena para variável endógena |
z_score_YX | Significância da entropia de transferência de endógena para variável exógena |
p_value_XY | Valor p da significância da entropia de transferência de exógena para endógena |
p_value_YX | Valor p da significância da entropia de transferência de endógena para exógena |
Ave_TE_XY | Entropia de transferência média de endógena para variável endógena |
Ave_TE_YX | Entropia de transferência média de endógena para variável exógena |
Chamar get_transfer_entropies() retorna um vetor das entropias de transferência estimadas para a última janela do conjunto de dados, medidas em ambas as direções. A ordem dos resultados segue a ordem das colunas dos dados originais passados para a classe. Portanto, o primeiro valor de entropia no vetor corresponde à série na primeira coluna.
Exemplo:
A funcionalidade da classe será testada executando testes em séries geradas aleatoriamente com características predeterminadas. As séries são geradas utilizando as funções listadas abaixo, ambas definidas em generate_time_series.mqh.
//+------------------------------------------------------------------+ //|Generate a random walk time series under Geometric Brownian Motion| //+------------------------------------------------------------------+ vector random_series(double initial_val, ulong steps, ulong len, double muu, double sgma) { vector out(len); out[0] = initial_val; int err=0; for(ulong i=1; i<len; i++) { out[i] = out[i-1]*(1.0+(muu*(double(steps)/double(len)))+(MathRandomNormal(muu,sgma,err)*sqrt(double(steps)/double(len)))); if(err) { Print(__FUNCTION__, " MathRandonNormal() ", GetLastError()); return vector::Zeros(1); } } return out; }
A função random_series() gera uma série temporal de caminhada aleatória característica do movimento Browniano geométrico. Seus parâmetros são:
- initial_val : O valor inicial da série temporal.
- steps : O número total de passos na caminhada aleatória.
- len : O comprimento da série temporal a ser gerada.
- muu : O termo de deriva (média) do GBM.
- sgma : A volatilidade (desvio padrão) do GBM.
//+-----------------------------------------------------------------------------------------------+ //|Generate two time series under Geometric Brownian Motion with S2 dependent in part on S1-lagged| //+-----------------------------------------------------------------------------------------------+ matrix coupled_random_series(double init_1,double init_2,ulong steps, ulong len, double muu_1, double muu_2, double sgma_1, double sgma_2, double alpha, double epsilon, ulong lag) { vector gbm1 = random_series(init_1,steps,len,muu_1,sgma_1); vector gbm2 = random_series(init_2,steps,len,muu_2,sgma_2); if(gbm1.Size()!=gbm2.Size()) { return matrix::Zeros(1,1); } matrix out(gbm2.Size()-lag,2); for(ulong i = lag; i<gbm2.Size(); i++) { gbm2[i]=(1.0-alpha)*(epsilon*gbm2[i-lag] + (1.0-epsilon) * gbm2[i]) + (alpha) * gbm1[i-lag]; out[i-lag][0] = gbm2[i]; out[i-lag][1] = gbm1[i]; } return out; }
A função coupled_random_series() gera duas séries temporais de caminhada aleatória acopladas, onde a segunda série (gbm2) depende parcialmente dos valores defasados da primeira série (gbm1). A função retorna uma matriz com duas colunas, sendo que a série dependente está na primeira coluna. Os parâmetros da função são os seguintes:
- init_1 : O valor inicial da primeira série temporal.
- init_2 : O valor inicial da segunda série temporal.
- steps : O número total de passos na caminhada aleatória.
- len : O comprimento da série temporal a ser gerada.
- muu_1 : O termo de deriva da primeira série.
- muu_2 : O termo de deriva da segunda série.
- sgma_1 : A volatilidade da primeira série.
- sgma_2 : A volatilidade da segunda série.
- alpha : Um parâmetro de mistura para a influência da série independente na série dependente.
- epsilon : Um parâmetro que ajusta a influência dos valores defasados da série dependente.
- lag : O atraso para a dependência da série dependente na série independente.
Para demonstrar as capacidades da classe CTransEntropy, foram preparados dois scripts MetaTrader 5. Ambos os scripts ilustram como a classe pode ser usada para analisar um conjunto de dados e detectar o atraso da variável independente (série temporal) que melhor caracteriza a dependência observada na variável dependente (série temporal). O primeiro método depende da inspeção visual para determinar o valor mais significativo de entropia direcional a partir de um conjunto de resultados obtidos pela análise da entropia de transferência em diferentes atrasos. Esse método está implementado no script LagDetection.ex5.
//+------------------------------------------------------------------+ //| LagDetection.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<transfer_entropy.mqh> #include<generate_time_series.mqh> //--- input parameters input double Init1=100.0; input double Init2=90.0; input ulong Steps=1; input ulong Len=500; input double Avg1=0; input double Avg2=0; input double Sigma1=1; input double Sigma2=1; input double Alph=0.5; input double Epsilon=0.3; input ulong Lag=3; input bool UseSeed = true; input ulong Bins=3; input ENUM_TE_TYPE testtype=NONLINEAR_TE; input ulong NumLagsToTest = 10; input int PlotViewTimeInSecs = 20; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { if(UseSeed) { MathSrand(256); } //--- if(!NumLagsToTest) { Print(" Invalid input parameter value for \'NumLagsToTest\'. It must be > 0 "); return; } matrix series = coupled_random_series(Init1,Init2,Steps,Len,Avg1,Avg2,Sigma1,Sigma2,Alph,Epsilon,Lag); series = log(series); series = np::diff(series,1,false); matrix entropies(NumLagsToTest,2); for(ulong k = 0; k<NumLagsToTest; k++) { CTransEntropy ote; if(!ote.Initialize(series,0,1,k+1)) return; if((testtype==NONLINEAR_TE && !ote.Calculate_NonLinear_TE(Bins)) || (testtype==LINEAR_TE && !ote.Calculate_Linear_TE())) return; vector res = ote.get_transfer_entropies(); entropies.Row(res,k); } Print(" entropies ", entropies); CGraphic* g = np::plotMatrix(entropies,"Transfer Entropies","Col 0,Col 1","Lag","TE"); if(g==NULL) return; else { Sleep(int(MathAbs(PlotViewTimeInSecs))*1000); g.Destroy(); delete g; } return; } //+------------------------------------------------------------------+
Os primeiros 11 parâmetros acessíveis ao usuário do script controlam as propriedades das séries geradas. Os últimos 4 parâmetros de entrada configuram diversos aspectos da análise:
- Bins : Define o número de bins para o método do histograma usado para estimar a densidade de probabilidade dos dados.
- testtype : Permite a seleção de análise de entropia de transferência linear ou não linear.
- NumLagsToTest : Define o número máximo de atrasos nos quais os testes serão conduzidos, começando em 1.
- PlotViewTimeInSecs : Determina o tempo em segundos que o gráfico permanecerá visível antes de o programa ser encerrado.
- UseSeed : Se verdadeiro, habilita a semente para o gerador de números aleatórios, garantindo a reprodutibilidade dos resultados dos testes.
O script gera duas séries temporais com uma dependência predefinida e estima a entropia de transferência em diferentes atrasos. Note que os dados foram diferenciados antes de serem analisados. Provavelmente desnecessário neste caso, mas é uma boa prática. Os resultados (entropias de transferência) são então visualizados em um gráfico, onde a entropia de transferência é plotada no eixo vertical contra o atraso correspondente no eixo horizontal. Um resultado bem-sucedido do teste deve produzir um gráfico com um pico claro no atraso escolhido para gerar as séries aleatórias.
Executar o programa mostra que o teste linear identificou com sucesso a dependência do atraso usada para gerar as séries. Lembre-se de que a série dependente está na primeira coluna do conjunto de dados gerados aleatoriamente.
Realizar o teste novamente com a opção de teste não linear gera resultados semelhantes. Neste caso, a magnitude do valor da entropia é notavelmente menor. Isso pode ser devido às limitações do método do histograma para estimar a distribuição de probabilidade dos dados. Também deve-se observar que o número de bins selecionado afetará a entropia de transferência estimada.
Na próxima demonstração, testamos a significância dos valores de entropia obtidos em atrasos específicos. Isso está implementado no script LagDetectionUsingSignificance.ex5.
//+------------------------------------------------------------------+ //| LagDetectionUsingSignificance.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<transfer_entropy.mqh> #include<generate_time_series.mqh> //--- input parameters input double Init1=100.0; input double Init2=90.0; input ulong Steps=1; input ulong Len=500; input double Avg1=0; input double Avg2=0; input double Sigma1=1; input double Sigma2=1; input double Alph=0.5; input double Epsilon=0.3; input ulong Lag=3; input bool UseSeed = true; input ulong Bins=3; input ENUM_TE_TYPE testtype=LINEAR_TE; input ulong LagToTest = 3; input ulong NumIterations = 100; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { if(UseSeed) { MathSrand(256); } //--- if(!LagToTest) { Print(" Invalid input parameter value for \'LagToTest\'. It must be > 0 "); return; } matrix series = coupled_random_series(Init1,Init2,Steps,Len,Avg1,Avg2,Sigma1,Sigma2,Alph,Epsilon,Lag); series = log(series); series = np::diff(series,1,false); matrix entropies(1,2); CTransEntropy ote; if(!ote.Initialize(series,0,1,LagToTest)) return; if((testtype==NONLINEAR_TE && !ote.Calculate_NonLinear_TE(Bins,NumIterations)) || (testtype==LINEAR_TE && !ote.Calculate_Linear_TE(NumIterations))) return; vector res = ote.get_transfer_entropies(); entropies.Row(res,0); TEResult alres = ote.get_results(); Print(" significance: ", " pvalue 1->0 ",alres.p_value_XY, " pvalue 0->1 ",alres.p_value_YX); Print(" zscore 1->0 ",alres.z_score_XY, " zscore 0->1 ",alres.z_score_YX); return; } //+------------------------------------------------------------------+
O script tem parâmetros ajustáveis pelo usuário semelhantes, exceto pelos dois últimos:
- LagToTest : Define o atraso específico no qual o teste será realizado.
- NumIterations : Define o número de vezes que os dados serão embaralhados para o teste de significância.
O script gera um par de séries dependentes e realiza um teste no atraso escolhido. A entropia de transferência, juntamente com o valor p e o z-score correspondentes, são escritos na aba Experts do terminal.
Para a primeira execução, o script é executado com os parâmetros LagToTest e Lag definidos com o mesmo valor. Os resultados são exibidos abaixo. Eles mostram que a série na primeira coluna depende da série na segunda coluna da matriz.
JS 0 21:33:43.464 LagDetectionUsingSignificance (Crash 1000 Index,M10) significance: pvalue 1->0 [0] pvalue 0->1 [0.66] LE 0 21:33:43.464 LagDetectionUsingSignificance (Crash 1000 Index,M10) zscore 1->0 [638.8518379295961] zscore 0->1 [-0.5746565128024472]
Na segunda execução, modificamos apenas o valor do parâmetro LagToTest e comparamos esses resultados com os da execução anterior.
Observe as diferenças nos valores p e nos z-scores. Neste caso, tanto os valores p quanto os z-scores são insignificantes.
RQ 0 21:33:55.147 LagDetectionUsingSignificance (Crash 1000 Index,M10) significance: pvalue 1->0 [0.37] pvalue 0->1 [0.85] GS 0 21:33:55.147 LagDetectionUsingSignificance (Crash 1000 Index,M10) zscore 1->0 [-0.2224969673139822] zscore 0->1 [-0.6582062358345131]
Embora os resultados dos testes indiquem que a classe CTransEntropy funciona bem, há uma limitação significativa ao realizar a análise com atrasos maiores, especialmente quando a opção para múltiplos termos de atraso está ativada (maxLagOnly é falso). Isso é particularmente problemático com o teste não linear. Isso decorre do uso do método do histograma para estimar a distribuição dos dados. Usar o método do histograma para estimar as densidades de probabilidade tem desvantagens notáveis. A escolha da largura do bin (ou número de bins) afeta significativamente a aparência e a precisão do histograma. Um bin muito pequeno pode resultar em um histograma ruidoso e fragmentado, enquanto um bin muito grande pode obscurecer detalhes importantes e suavizar características. O maior problema está relacionado ao fato de que os histogramas são eficazes principalmente para dados unidimensionais. Para dados de dimensões mais altas, o número de bins cresce exponencialmente. Se houver muitos atrasos a serem considerados, as demandas sobre os recursos computacionais disponíveis podem ser significativas. Portanto, recomenda-se manter o número máximo de atrasos pequeno quando a análise for conduzida considerando múltiplos atrasos usando a entropia de transferência generalizada.
Conclusão
Em conclusão, a classe CTransEntropy permite a análise da entropia de transferência em contextos lineares e não lineares. Através de demonstrações práticas, mostramos sua capacidade de detectar e quantificar a influência de uma série temporal sobre outra, com resultados validados por inspeção visual e testes de significância. A classe lida efetivamente com diversos cenários, oferecendo valiosas informações sobre relações causais em aplicações de séries temporais. No entanto, os usuários devem estar cientes dos desafios computacionais associados à análise de múltiplos atrasos, especialmente ao aplicar métodos não lineares. Para garantir desempenho eficiente e resultados precisos, é aconselhável limitar o número de atrasos considerados. No geral, a classe CTransEntropy é uma ferramenta útil para descobrir dependências complexas e aprimorar a compreensão de sistemas dinâmicos.
Arquivo | Descrição |
---|---|
Mql5\include\generate_time_series.mqh | contém funções para gerar séries temporais aleatórias |
Mql5\include\ np.mqh | uma coleção de funções utilitárias para vetores e matrizes |
Mql5\include\ OLS.mqh | contém a definição da classe OLS que implementa a regressão de mínimos quadrados ordinários |
Mql5\include\ TestUtilities.mqh | fornece uma coleção de ferramentas usadas para preparar conjuntos de dados para avaliação OLS |
Mql5\include\ transfer_entropy | contém a definição da classe CTransEntropy que implementa a análise de entropia de transferência |
Mql5\scripts\LagDetection.mq5 | um script que demonstra a funcionalidade da classe CTransEntropy |
Mql5\scripts\LagDetectionUsingSignificance.mq5 | um segundo script que ilustra uma abordagem diferente para interpretar o resultado da utilização do CTransEntropy |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/15393
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