In questo articolo descriviamo come applicare tutte le metodologie presentate nei precedenti articoli relativi all’analisi delle serie temporali ad una strategia di trading sull’indice S&P500 del mercato azionario statunitense.
Vediamo come combinando i modelli ARIMA e GARCH possiamo significativamente sovraperformare un approccio “Buy-and-Hold” a lungo termine.
Panoramica della strategia
L’idea della strategia è relativamente semplice ma se vuoi sperimentarla ti consiglio vivamente di leggere i precedenti post sull’analisi delle serie temporali per capire i concetti matematici alla base di questo studio!
La strategia viene eseguita su base “rolling”:
- Per ogni giorno, n, si usano i rendimenti logaritmici differenziati di un indice del mercato azionario dei precedenti k giorni come finestra per adattare un modello ARIMA e GARCH ottimale.
- Il modello combinato è usato per fare una previsione per i rendimenti del giorno successivo.
- Se la previsione è negativa il titolo viene shortato alla chiusura, mentre se è positiva si va long.
- Se la previsione è nella stessa direzione del giorno precedente, non cambia nulla.
Per questa strategia abbiamo usato il periodo storico massimo dei dati disponibili su Yahoo Finance per l’S&P500. Inoltre abbiamo considerato k=500 come parametro che può essere ottimizzato per migliorare le prestazioni o ridurre il drawdown.
Eseguiamo un semplice backtest vettorializzato usando Python. Quindi le prestazioni ottenute in un sistema di trading reale sarebbero probabilmente leggermente inferiori a quelle ottenute nel backtest, a causa delle commissioni e dello slippage.
Implementazione della strategia
Per implementare la strategia usiamo tilizzeremo parte del codice che abbiamo creato in precedenza nella serie di articoli sull’analisi delle serie temporali.
Quando si tratta di modellazione delle serie temporali di dati finanziari, i modelli autoregressivi (modelli che utilizzano valori precedenti per prevedere il futuro) come ARMA, ARIMA o GARCH e le sue varie varianti sono quelli solitamente preferiti per spiegare le basi della modellazione delle serie temporali. Tuttavia, l’applicazione pratica di queste tecniche in strategie di trading reali e il confronto con strategie benchmark (come il Buy and Hold) non sono così comuni. Inoltre, non è facile trovare un’implementazione pronta per l’uso che possa essere facilmente replicata per altri mercati, asset, ecc.
Modello ARIMA+GARCH
Per adattare il modello ARIMA+GARCH, seguiamo il modo convenzionale che prevede di adattare il modello ARIMA e poi applicare il modello GARCH ai rendimenti. La previsione finale consiste nella somma della previsione ARIMA più la previsione GARCH. Iniziamo recuperando i prezzi storici dell’indice SP500 dal 2000 al 2018 e calcolare i ritorni logaritmici, tramite le librerie yfinance
, numpy
e pandas
di Python.
import yfinance as yf
import pandas as pd
import numpy as np
symbol='^GSPC'
start = '2002-01-01'
end = '2018-12-31'
SP500 = yf.download(symbol, start=start, end=end)
log_ret = np.log(SP500['Adj Close']) - np.log(SP500['Adj Close'].shift(1))
Una volta ottenuto il DataFrame con i rendimenti dell’indice, creiamo il set di dati relativi agli ultimi k giorni per adattare il modello. Inoltre, creiamo un DataFrame chiamato forecasts
per memorizzare i risultati del nostro modello.
# Creazione del dataset
windowLength = 500
foreLength = len(log_ret) - windowLength
windowed_ds = []
for i in range(foreLength-1):
windowed_ds.append(log_ret[i:i + windowLength])
# creazione del dataframe forecasts con zeri
forecasts = log_ret.iloc[windowLength:].copy() * 0
Per convalidare il nostro set di dati, dobbiamo considerare che la prima finestra termina il “2003–12–24” e la data della prima previsione è “2003–12–26”, allo stesso modo l’ultima finestra termina al “2018–12–28” e l’ultima previsione al ‘2018–12–29’.
Ora che abbiamo i nostri set di dati e creato i nostri target, dobbiamo analizzare ogni set e adattare i modelli. A tale scopo usiamo i pacchetti pmdarima
e arch
, che possono essere installati con pip install pmdarima, arch
.
Invece di analizzare i vari iperparametri (p e q) per il modello ARIMA e selezionare quello con il migliore adattamento in base all’Akaike Information Criterion (AIC) più basso, la libreria pmdarima
comprende la classe AutoARIMA
che permette di ottenere automaticamente l’adattamento migliore. Tuttavia, non potevo evitare che si adattasse a p o q uguale a zero, anche quando si specificano i parametri start_p
e . start_q
. Quindi, sono passato alla vecchia maniera.
Per prima cosa creiamo una funzione fit_arima che riceve una serie e restituisce l’adattamento miglior e, a partire dagli intervalli per p e q.
import pmdarima
import arch
import warnings
warnings.filterwarnings("ignore")
def fit_arima(series, range_p=range(0, 6), range_q=range(0, 6)):
final_order = (0, 0, 0)
best_aic = np.inf
arima = pmdarima.ARIMA(order=final_order)
for p in range_p:
for q in range_q:
if (p == 0) and (q == 0):
next
arima.order = (p, 0, q)
arima.fit(series)
aic = arima.aic()
if aic < best_aic:
best_aic = aic
final_order = (p, 0, q)
arima.order = final_order
return arima.fit(series)
for i, window in enumerate(windowed_ds):
# ARIMA model
arima = fit_arima(window)
arima_pred = arima.predict(n_periods=1)
# GARCH model
garch = arch.arch_model(arima.resid())
garch_fit = garch.fit(disp='off', show_warning=False, )
garch_pred = garch_fit.forecast(horizon=1).mean.iloc[-1]['h.1']
forecasts.iloc[i] = arima_pred + garch_pred
print(f'Date {str(forecasts.index[i].date())} : Fitted ARIMA order {arima.order} - Prediction={forecasts.iloc[i]}')
for i, window in enumerate(windowed_ds):
# ARIMA model
arima = fit_arima(window)
arima_pred = arima.predict(n_periods=1)
# GARCH model
garch = arch.arch_model(arima.resid())
garch_fit = garch.fit(disp='off', show_warning=False, )
garch_pred = garch_fit.forecast(horizon=1).mean.iloc[-1]['h.1']
forecasts.iloc[i] = arima_pred + garch_pred
print(f'Date {str(forecasts.index[i].date())} : Fitted ARIMA order {arima.order} - Prediction={forecasts.iloc[i]}')
Una volta calcolate le nostre previsioni, faremo implementiamo il codice per fornire il confronto tra la strategia Buy and Hold e ARIMA+GARCH.
# Memorizzazione dei nuovi segnali calcolati
forecasts.columns=['Date','Signal']
forecasts.set_index('Date', inplace=True)
forecasts.to_csv('prova.csv')
# Otteniamo il periodo che ci interessa
forecasts = forecasts[(forecasts.index>='2004-01-01') & (forecasts.index<='2018-12-31')]
# Calcolo direzione delle previsioni
forecasts['Signal'] = np.sign(forecasts['Signal'])
forecasts.index = pd.to_datetime(forecasts.index)
# Creo un dataframe che contiene le statistiche della strategia
stats=SP500.copy()
stats['LogRets']=log_ret
stats = stats[(stats.index>='2004-01-01') & (stats.index<='2018-12-31')]
stats.loc[stats.index, 'StratSignal'] = forecasts.loc[stats.index, 'Signal']
stats['StratLogRets'] = stats['LogRets'] * stats['StratSignal']
stats.loc[stats.index, 'CumStratLogRets'] = stats['StratLogRets'].cumsum()
stats.loc[stats.index, 'CumStratRets'] = np.exp(stats['CumStratLogRets'])
# Calcolo del confronto con il benchmark
start_stats = pd.to_datetime('2004-01-02')
end_stats = pd.to_datetime('2012-12-31')
results = stats.loc[(stats.index > start_stats) & (stats.index < end_stats), ['Adj Close', 'LogRets', 'StratLogRets']].copy()
results['CumLogRets'] = results['LogRets'].cumsum()
results['CumRets'] = 100 * (np.exp(results['CumLogRets']) - 1)
results['CumStratLogRets'] = results['StratLogRets'].cumsum()
results['CumStratRets'] = 100 * (np.exp(results['CumStratLogRets']) - 1)
buy_hold_first = SP500.loc[start_stats, 'Adj Close']
buy_hold_last = SP500.loc[end_stats, 'Adj Close']
buy_hold = (buy_hold_last-buy_hold_first)/buy_hold_first
strategy = np.exp(results.loc[results.index[-1], 'CumStratLogRets']) - 1
pct_pos_returns = (results['LogRets'] > 0).mean() * 100
pct_strat_pos_returns = (results['StratLogRets'] > 0).mean() * 100
print(f'Returns:')
print(f'Buy_n_Hold - Return in period: {100 * buy_hold:.2f}% - Positive returns: {pct_pos_returns:.2f}%')
print(f'Strategy - Return in period: {100 * strategy:.2f}% - Positive returns: {pct_strat_pos_returns:.2f}%')
import matplotlib.pyplot as plt
columns = ['CumLogRets', 'CumStratLogRets']
plot_df = results[columns]
plot_df.plot(figsize=(15,7))
Conclusione
Nel grafico vediamo che l’equity della strategia rimane sotto il benchmark Buy & Hold per quasi 3 anni, ma durante il crollo del mercato azionario del 2008/2009 va molto bene. E’ probabile che in questo periodo si è verificata una correlazione seriale significativa ed è stata ben catturata dai modelli ARIMA e GARCH. Dal 2009 il mercato si è ripreso ed è entrato in quella che sembra essere più una tendenza stocastica, le prestazioni del modello ricominciano a risentirne.
Si noti che questa strategia può essere facilmente applicata a diversi indici del mercato azionario, azioni o altre classi di attività. Ti incoraggio vivamente a provare lo studio per altri strumenti, poiché potresti ottenere risultati migliori da quelli qui presentati.
Prossimi passi
Ora che abbiamo terminato la descrizione dei modelli della famiglia ARIMA e GARCH, continuiamo l’analisi delle serie temporali considerando i processi a memoria lunga, i modelli dello spazio degli stati e le serie temporali cointegrate.
Queste successive aree dell’analisi delle serie temporali ci introducono modelli che possono migliorare le nostre previsioni rispetto a quanto descritto finora, il che aumenterà significativamente la nostra redditività e/o ridurrà il rischio.
Codice completo
Per il codice completo riportato in questo articolo, utilizzando il framework open-source di backtesting event-driven DataTrader si può consultare il seguente repository di github:
https://github.com/datatrading-info/DataTrader