Nel precedente articolo relativo al test CADF (Cointegrated Augmented Dickey Fuller) abbiamo sottolineato che uno dei maggiori inconvenienti del test era la sua applicazione limitata a solo a due serie temporali separate. Tuttavia, possiamo chiaramente immaginare un insieme di tre o più attività finanziarie che potrebbero condividere una sottostante relazione cointegrata.
Un semplice esempio potrebbe essere tre classi di azioni separate sullo stesso asset, mentre un esempio più interessante consiste nel considerare tre ETF separati che replicano determinate aree delle azioni delle materie prime e dei prezzi spot delle materie prime sottostanti.
In questo articolo descriviamo un test introdotto da Johansen [2] che permette di determinare se tre o più serie temporali sono cointegrate. Determiniamo quindi una serie stazionaria a partire da una combinazione lineare delle serie sottostanti. Tale procedura sarà utilizzata negli articoli successivi per formare un portafoglio di asset mean-reverting a fini di trading.
Iniziamo descrivendo la teoria alla base del test di Johansen e quindi eseguiamo la procedura di esecuzione del test su dati simulati con proprietà di cointegrazione note a priori. Successivamente applichiamo il test ai dati finanziari storici e vediamo se riusciamo a trovare un portafoglio di asset cointegrati.
Test di Johannsen
In questa sezione introduciamo i concetti matematici alla base dek test di Johansen, che permette di analizzare se due o più serie temporali possono formare una relazione di cointegrazione. Nel trading quantitativo ciò consente di formare un portafoglio di due o più titoli in una strategia di trading mean-reverting.
I dettagli teorici del test di Johansen richiedono un po’ di esperienza con le serie temporali multivariate. In particolare occorre considerare i Modelli Autoregressivi Vettoriali (VAR) – da non confondere con il Value at Risk (VaR) – che sono un’estensione multidimensionale dei Modelli Autoregressivi descritti in precedenza.
Un modello autoregressivo vettoriale generale è simile al modello AR(p) tranne per il fatto che ogni elemento è rappresentato da un vettore di variabili e i coefficienti sono rappresentati con matrici. La forma generale del modello VAR(p) è data da:
\(\begin{eqnarray}{\bf x_t} = {\bf \mu} + A_1 {\bf x_{t-1}} + \ldots + A_p {\bf x_{t-p}} + {\bf w_t}\end{eqnarray}\)
Dove \({\bf \mu}\) è la funzione vettoriale delle intercette delle serie, \(A_i\) sono le matrici dei coefficienti per ogni lag e \({\bf w_t}\) è un termine di rumore gaussiano multivariato con media zero.
A questo punto possiamo formare un modello di correzione degli errori dei vettori (VECM) differenziando le serie:
\(\begin{eqnarray}\Delta {\bf x_t} = {\bf \mu} + A {\bf x_{t-1}} + \Gamma_1 \Delta {\bf x_{t-1}} + \ldots + \Gamma_p \Delta {\bf x_{t-p}} + {\bf w_t}\end{eqnarray}\)
dove \(\Delta {\bf x_t} := {\bf x_t} – {\bf x_{t-1}}\) è l’operatore differenziale, \(A\) è la matrice dei coefficienti per il primo lag e \(\Gamma_i\) sono le matrici per ogni lag differenziato.
Il test verifica la presenza di non cointegrazione, che avviene quando la matrice A=0.
Il test di Johansen è più flessibile della procedura CADF descritta nell’articolo precedente e può verificare la presenza di più combinazioni lineari di serie temporali per la costruzione di portafogli stazionari. Per raggiungere questo scopo è necessario effettuare una autodecomposizione di A, cioè la scomposizioni in autovalori. Il range della matrice A è dato da r e il test di Johansen verifica in sequenza se questo range è uguale a zero, uguale a uno, fino a \(r=n-1\), dove n è il numero di serie temporali in esame.
L’ipotesi nulla di \(r=0\) significa che non c’è alcuna cointegrazione. Un grado \(r \gt 0\) implica una relazione di cointegrazione tra due o forse più serie temporali.
La scomposizione degli autovalori produce un insieme di autovettori. Le componenti dell’autovettore più grande hanno le proprietà per essere i candidati a formare i coefficienti di una combinazione lineare di serie temporali per produrre un portafoglio stazionario. Si noti come questo differisca dal test CADF (spesso noto come procedura di Engle-Granger) in cui è necessario accertare la combinazione lineare a priori tramite la regressione lineare e minimi quadrati ordinari (OLS).
Nel test di Johansen i valori della combinazione lineare sono stimati come parte del test, ciò implica che il test ha una minore potenza statistica rispetto al CADF. È possibile imbattersi in situazioni in cui non ci sono prove sufficienti per rifiutare l’ipotesi nulla di mancata cointegrazione nonostante la CADF suggerisca diversamente. Vedremo di seguito questi casi.
Forse il modo migliore per comprendere il test di Johansen è applicarlo a dati finanziari sia simulati che storici.
Test di Johansen su dati simulati
Ora che abbiamo descritto le basi teoriche del test, lo applichiamo utilizzando Python. In particolare utilizziamo la libreriastatsmodels
, che implementa il test di Johansen con la funzione coint_johansen
.
Il primo compito è importare le librerie necessarie. Come nei precedenti articoli impostiamo il seed in modo che i risultati del generatore di numeri casuali possano essere replicati, quindi creiamo la passeggiata casuale sottostante \(z_t\). Infine creiamo le tre serie temporali che condividono la stessa passeggiata casuale sottostante. Sono indicati rispettivamente come \(p_t\), \(q_t\) e \(r_t\):
import numpy as np
import pandas as pd
import statsmodels.tsa.vector_ar.vecm as vecm
np.random.seed(123)
n = 1000
z = np.zeros(n)
for i in range(1, n):
z[i] = z[i-1] + np.random.standard_normal(1)
p = 0.3 * z + np.random.standard_normal(1000)
q = 0.6 * z + np.random.standard_normal(1000)
r = 0.2 * z + np.random.standard_normal(1000)
Chiamiamo quindi la funzione coint_johansen
applicata a un frame di dati di tutte e tre le serie temporali.
Questa funzione prevede ulteriori pamametri:
- det_order – indica se utilizzare un termine costante o un trend lineare
- k – indica il numero di ritardi da utilizzare nel modello
Infine, stampiamo il riepilogo dell’output:
df = pd.DataFrame({'p':p, 'q':q, 'r':r})
res = vecm.coint_johansen(df, det_order=0, k_ar_diff=1)
output = pd.DataFrame([res.lr1], index=["trace_stat"], columns=['r=0', 'r<=1', 'r<=2'])
print(output.T, '\n')
cvt = pd.DataFrame(res.cvt, index=["r=0", "r<=1", "r<=2"], columns=["90%", "95%", "99%"])
print("Critical values(90%, 95%, 99%) of trace_stat\n", cvt, '\n')
print("Eigenvalues (lambda):\n")
print(res.eig, "\n")
print("Eigenvectors:")
evec = pd.DataFrame(res.evec).T
print(evec)
trace_stat
r=0 631.340645
r<=1 278.465131
r<=2 0.887798
Critical values(90%, 95%, 99%) of trace_stat
90% 95% 99%
r=0 27.0669 29.7961 35.4628
r<=1 13.4294 15.4943 19.9349
r<=2 2.7055 3.8415 6.6349
Eigenvalues (lambda):
[0.29808104 0.24301555 0.00089007]
Eigenvectors
0 1 2
0 1.613432 -0.835372 0.141379
1 -0.327201 -0.366202 1.600709
2 0.073222 0.097455 0.031615
Proviamo ad interpretare tutte queste informazioni! La prima sezione mostra la statistica del test ‘trace’ per le tre ipotesi \(r=0\), \(r \leq 1\) e \(r \leq 2\). Per ognuno di questi tre test abbiamo non solo la statistica stessa ma anche i valori critici a determinati livelli di confidenza: rispettivamente 10%, 5% e 1%.
La prima ipotesi,\(r = 0\), verifica la presenza di cointegrazione. Poiché la statistica del test supera significativamente il livello dell’1% (631.34>35.46) abbiamo forti prove per rifiutare l’ipotesi nulla di mancata cointegrazione. La seconda prova per \(r \leq 1\) contro l’ipotesi alternativa \(r \gt 1\) fornisce anch’essa prove chiare per rifiutare \(r \leq 1\) poiché la statistica del test supera significativamente il livello dell’1%. Anche la prova finale per \(r \leq 2\) contro \(r \gt 2\) fornisce prove sufficienti per rifiutare l’ipotesi nulla che \(r \leq 2\) e quindi si può concludere che il rango della matrice è maggiore di 2.
Da quanto sopra la migliore stima del rango della matrice è \(r=3\), che implica la necessità di considerare una combinazione lineare di tre serie temporali per formare una serie stazionaria. Questo è prevedibile, per definizione della serie, poiché la passeggiata casuale sottostante, utilizzata per tutte e tre le serie, non è stazionaria.
Come facciamo a formare una combinazione lineare di questo tipo? La risposta è utilizzare le componenti autovettoriali dell’autovettore associato all’autovalore più grande.
La successiva sezione del nostro output mostra gli autovalori e gli autovettori normalizzati restituiti dal test. Notiamo che l’autovalore più grande è circa 0,298. Il corrispondente vettore è indicato sotto la colonna 0 ed è approssimativamente uguale a (1.61343, -0.83533, 0.14138). Se formiamo una combinazione lineare di serie utilizzando questi componenti, ricaviamo una serie stazionaria:
s = 1.61343164*p - 0.83537214*q + 0.14137947*r
import matplotlib.pyplot as plt
plt.plot(s)
plt.show()
from arch.unitroot import *
adf = ADF(s)
print(adf.summary())
Augmented Dickey-Fuller Results
=====================================
Test Statistic -20.546
P-value 0.000
Lags 2
-------------------------------------
Trend: Constant
Critical Values: -3.44 (1%), -2.86 (5%), -2.57 (10%)
Null Hypothesis: The process contains a unit root.
Alternative Hypothesis: The process is weakly stationary.
La statistica del test di Dickey-Fuller è molto bassa, dato che abbiamo un p-value basso. Quindi possiamo rifiutare l’ipotesi nulla di una radice unitaria e abbiamo la prova di una serie stazionaria formata da una combinazione lineare.
Questo non dovrebbe sorprenderci in quanto, per costruzione, l’insieme di serie è stato progettato per formare una combinazione lineare stazionaria. Tuttavia, è istruttivo seguire i test su dati simulati poiché ci aiuta nell’analisi dei dati finanziari reali, come nel prossimo capitolo.
Test di Johansen sui dati finanziari
In questa sezione esamineremo due serie separate di panieri di ETF: EWA, EWC e IGE, nonché SPY, IVV e VOO.
EWA, EWC e IGE
Nell’articolo precedente abbiamo esaminato il lavoro di Ernest Chan [1] sulla cointegrazione tra i due ETF di EWA ed EWC, che rappresentano panieri di azioni rispettivamente per le economie australiana e canadese.
Chan descrive anche il test di Johansen come mezzo per aggiungere un terzo ETF al mix, ovvero l’IGE, che contiene un paniere di stock di risorse naturali. L’idea di base è che questi ETF dovrebbero subire l’influenza delle tendenze stocastiche delle merci e quindi possono formare una relazione di cointegrazione.
Nel suo lavoro, Chan ha eseguito la procedura di Johansen utilizzando MatLab e ha potuto respingere l’ipotesi \(r \leq 2\) al livello del 5%. Questo implica che ha trovato prove a sostegno dell’esistenza di una combinazione lineare stazionaria di EWA, CAE e IGE.
Sarebbe utile vedere se possiamo replicare i risultati usando Python. Iniziamo utilizzando la libreria yfinance
per scaricare i dati storici delle serie finanziare e quindi effettuare il test di Johansen con la libreria statsmodels.
import pandas as pd
import yfinance as yf
EWA = yf.download('EWA', start="2006-04-26", end="2012-04-09")
EWC = yf.download('EWC', start="2006-04-26", end="2012-04-09")
IGE = yf.download('IGE', start="2006-04-26", end="2012-04-09")
df = pd.DataFrame({'EWA': EWA['Adj Close'], 'EWC': EWC['Adj Close'], 'IGE': IGE['Adj Close']})
res = vecm.coint_johansen(df, 0, 1)
trace_stat
r=0 34.781746
r<=1 16.940157
r<=2 4.417218
Critical values(90%, 95%, 99%) of trace_stat
90% 95% 99%
r=0 27.0669 29.7961 35.4628
r<=1 13.4294 15.4943 19.9349
r<=2 2.7055 3.8415 6.6349
Eigenvalues (lambda):
[0.01183963 0.00832493 0.0029444 ]
Eigenvectors
EWA EWC IGE
0 1.487722 -1.417733 0.321943
1 -0.227340 -0.691618 0.687187
2 0.108149 0.389697 -0.143024
SPY, IVV e VOO
Un altro approccio consiste nel considerare un paniere di ETF che replicano un indice azionario. Ad esempio, ci sono una moltitudine di ETF che replicano l’indice del mercato azionario statunitense S&P500 come Standard & Poor’s Depository Receipts SPY, iShares IVV e VOO di Vanguard. Dato che replicano tutti lo stesso asset sottostante, è probabile che questi tre ETF abbiano una forte relazione di cointegrazione.
Otteniamo i prezzi di chiusura giornalieri rettificati per ciascuno di questi ETF nell’ultimo anno:
import pandas as pd
import yfinance as yf
SPY = yf.download('SPY', start="2016-01-01", end="2016-12-31")
IVV = yf.download('IVV', start="2016-01-01", end="2016-12-31")
VOO = yf.download('VOO', start="2016-01-01", end="2016-12-31")
df = pd.DataFrame({'SPY': SPY['Adj Close'], 'IVV': IVV['Adj Close'], 'VOO': VOO['Adj Close']})
res = vecm.coint_johansen(df, 0, 1)
trace_stat
r=0 100.207059
r<=1 9.629058
r<=2 0.715191
Critical values(90%, 95%, 99%) of trace_stat
90% 95% 99%
r=0 27.0669 29.7961 35.4628
r<=1 13.4294 15.4943 19.9349
r<=2 2.7055 3.8415 6.6349
Eigenvalues (lambda):
[0.30292937 0.03489021 0.00284531]
Eigenvectors
EWA EWC IGE
0 35.138330 -0.602150 -37.360558
1 -3.685629 4.317242 -0.574631
2 1.040785 -0.255254 -0.964393
Eseguiamo in sequenza i test di ipotesi iniziando con l’ipotesi nulla di \(r = 0\) contro l’ipotesi alternativa di \(r \gt 0\). Ci sono prove evidenti per rifiutare l’ipotesi nulla al livello dell’1% e possiamo probabilmente concludere che \(r \gt 0\).
Allo stesso modo verifichiamo l’ipotesi nulla \(r \leq 1\) contro l’ipotesi alternativa \(r \gt 1\). Abbiamo prove sufficienti per rifiutare l’ipotesi nulla al livello dell’1% e possiamo concludere che \(r \gt 1\).
Infine, possiamo rifiuturare l’ipotesi nulla \(r \leq 2\) contro l’ipotesi alternativa \(r \gt 2\) e quindi possiamo concludere che \(r=3\). Ciò significa che possiamo formare una combinazione lineare di tutte e 3 gli asset per un portafoglio di cointegrazione.
Inoltre, dovremmo essere estremamente cauti nell’interpretare questi risultati poiché abbiamo utilizzato solo un anno di dati, ovvero circa 250 giorni di negoziazione. È improbabile che un campione così piccolo fornisca una rappresentazione fedele delle relazioni sottostanti. Quindi bisogna stare sempre attenti nell’interpretare i test statistici!
Prossimi passi
Negli ultimi articoli abbiamo trattato una moltitudine di test statistici per rilevare la stazionarietà tra combinazioni di serie temporali come precursori per creare strategie di trading mean-reverting.
In particolare abbiamo esaminato il test Augmented Dickey-Fuller, Phillips-Perron, Phillips-Ouliaris, Cointegrated Augmented Dickey-Fuller e il test di Johansen.
Siamo ora in grado di applicare questi test a strategie di ritorno verso la media. Per fare ciò effettuiamo un backtest realistico usando DataTrader in Python per queste strategie. Descriviamo questi test negli articoli successivi.