Come abbiamo descritto nella precedente lezione di questo corso, nella finanza quantitativa disponiamo di un’ampia quantità di dati di addestramento, quindi possiamo adottare modelli come il Support Vector Machine (SVM). Tuttavia, gli SVM presentano problemi di interpretabilità. Questo non accade con gli Alberi Decisionali e le Random Forest, che risultano più comprensibili. Quando sviluppiamo modelli predittivi delle serie temporali, dobbiamo anche considerare l’equilibrio tra accuratezza e interpretabilità.
Utilizziamo questi ultimi proprio per mantenere l’interpretabilità, cosa che i classificatori “black box” come SVM non offrono. Quando i dati diventano molto abbondanti (per esempio, nei dati di tick), la scelta del classificatore diventa meno critica. In questi casi, valutiamo piuttosto fattori come l’efficienza computazionale e la scalabilità dell’algoritmo. In genere, raddoppiando i dati di addestramento otteniamo un aumento lineare delle performance, ma con dataset molto grandi osserviamo un miglioramento sub-lineare.
Modelli Predittivi delle Serie Temporali
La teoria matematica e statistica che sostiene i classificatori supervisionati risulta piuttosto articolata, ma comprendiamo facilmente l’intuizione che guida ciascun modello. Ogni classificatore eccelle con specifiche caratteristiche del dataset, quindi se incontriamo risultati deludenti, probabilmente il dataset viola alcune delle condizioni su cui il modello si basa.
Classificatore Naive Bayes
Anche se non abbiamo ancora descritto questo classificatore nella lezione precedente, lo introduciamo per completare il quadro. Il Naive Bayes, in particolare il Multinomial Naive Bayes (MNB), si adatta bene a dataset limitati, poiché mostra un’elevata distorsione. Il modello MNB si basa sull’ipotesi di indipendenza condizionata tra le caratteristiche. In pratica, non riesce a cogliere le interazioni tra le feature, a meno che non le inseriamo esplicitamente come variabili aggiuntive.
Supponiamo di classificare documenti, come spesso accade nell’analisi finanziaria durante la sentiment analysis. Il classificatore MNB può riconoscere che parole come “gatto” e “cane” appartengono a categorie diverse, ma non interpreta correttamente frasi come “gatti e cani” (espressione britannica che significa “piovere a catinelle”). In questo caso, possiamo trattare “gatti e cani” come una caratteristica a sé, legata alla categoria meteorologica.
Logistic Regression
La regressione logistica offre alcuni vantaggi rispetto al Naive Bayes, poiché riduciamo l’attenzione sulle correlazioni tra caratteristiche e otteniamo una lettura probabilistica dei risultati. Questo approccio funziona meglio in contesti dove utilizziamo soglie decisionali. Per esempio, possiamo impostare una soglia dell’80% per determinare se un risultato sarà “up” o “down”, scegliendo così una previsione più affidabile. Diversamente, potremmo assegnare “up” con il 51% e “down” con il 49%, ottenendo una previsione poco utile.
Decision Tree and Random Forests
Gli alberi decisionali (DT) suddividono lo spazio delle decisioni in una struttura gerarchica di scelte booleane, producendo così categorie o gruppi ben distinti. Questo rende i modelli facilmente interpretabili, almeno finché il numero di nodi rimane ragionevole. I DT offrono molti vantaggi, inclusa la capacità di gestire interazioni non lineari tra caratteristiche, anche nei casi in cui non esiste una separazione lineare.
Quando non riusciamo a separare linearmente le classi (una condizione richiesta dagli SVM), i DT risultano utili. Il loro limite principale è l’overfitting, cioè una variabilità eccessiva nei risultati. Possiamo superare questo problema utilizzando le Random Forest. Queste foreste casuali combinano più alberi e si rivelano tra i classificatori migliori nel machine learning, quindi li includiamo sempre quando progettiamo Modelli Predittivi delle Serie Temporali.
Support Vector Machine
Gli SVM, pur avendo un addestramento complesso, risultano concettualmente semplici. I modelli lineari cercano di suddividere lo spazio in gruppi distinti con confini lineari. In alcuni casi funzionano molto bene e ci garantiscono buone previsioni. Tuttavia, quando i dati non si separano linearmente, le prestazioni degli SVM lineari peggiorano sensibilmente.
Possiamo intervenire scegliendo un kernel diverso, che consenta confini decisionali non lineari. In questo modo otteniamo modelli molto flessibili. Tuttavia, dobbiamo selezionare accuratamente il kernel per ottenere prestazioni ottimali. Gli SVM si adattano bene ai problemi di classificazione testuale su larga scala, ma ci mettono alla prova con la loro complessità computazionale, la difficoltà nella configurazione e la scarsa interpretabilità del modello risultante.
Prevedere il Movimento di un Indice Azionario
L’indice S&P500 rappresenta le 500 maggiori società quotate in borsa nel mercato statunitense, selezionate in base alla capitalizzazione di mercato. Lo consideriamo spesso un “benchmark” del mercato azionario. Troviamo numerosi derivati che permettono di speculare sull’indice. In particolare, il contratto futures E-Mini S&P500 ci offre uno strumento molto liquido per negoziare sull’indice.
In questo esempio adottiamo un set di classificatori per prevedere la direzione del prezzo di chiusura nel giorno k, utilizzando solo le informazioni disponibili fino al giorno k-1. Quando il prezzo del giorno k supera quello del giorno k-1, consideriamo il movimento rialzista; se invece risulta inferiore, parliamo di movimento ribassista.
Quando riusciamo a identificare la direzione del movimento con un metodo che superi nettamente il 50% di successo, mantenendo un basso errore e una buona significatività statistica, poniamo le basi per costruire una strategia di trading sistematico semplice, fondata sulle nostre previsioni. In questa fase non approfondiamo ancora gli algoritmi di Machine Learning più avanzati. Ci limitiamo a introdurre alcuni concetti iniziali e per questo iniziamo la discussione con metodi elementari.
Implementazione in Python
Per realizzare questi modelli predittivi delle serie temporali, utilizziamo alcune librerie fondamentali di Python, come NumPy, Pandas e Scikit-Learn.
Come primo passo, importiamo i principali moduli e librerie. In particolare, per questo esempio, includiamo i moduli per LogisticRegression, LDA, QDA, LinearSVC (Support Vector Machine lineare), SVC (Support Vector Machine non lineare) e i classificatori RandomForest.
#!/usr/bin/python
# -*- coding: utf-8 -*-
# forecast.py
import datetime
import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.metrics import confusion_matrix
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis as QDA
from sklearn.svm import LinearSVC, SVC
Dopo aver importato le librerie, si deve creare un DataFrame di pandas che contenga i i ritorni (variazioni di prezzo) percentuali ritardati per un numero precedente di giorni (predefinito a cinque). La funzione create_lagged_series
crea un DataFrame per un determinato simbolo azionario (come restituito da Yahoo Finance) eil periodo specificato.
Il codice è ben commentato quindi dovrebbe essere semplice capire il suo funzionamento:
def create_lagged_series(symbol, start_date, end_date, lags=5):
"""
Questo crea un DataFrame Pandas che memorizza i rendimenti percentuali
del prezzo di chiusura aggiustato delle barre di un titolo azionario scaricate
da Yahoo Finance, e memorizza una serie di rendimenti ritardati dai giorni di
negoziazione precedenti (il valore di ritardo predefinito è di 5 giorni).
Sono inclusi anche i volume degli scambi, e la direzione del giorno precedente.
"""
# Download dei dati con yfinance
ts = yf.download(
symbol,
start=start_date - datetime.timedelta(days=365),
end=end_date,
group_by='ticker', auto_adjust=False
)
ts=ts[symbol]
# Crea un nuovo DataFrame dei dati ritardati
tslag = pd.DataFrame(index=ts.index)
tslag["Today"] = ts["Adj Close"]
tslag["Volume"] = ts["Volume"]
# Crea una serie ritardata dei precendenti prezzi di chiusura dei periodi di trading
for i in range(0, lags):
tslag["Lag%s" % str(i+1)] = ts["Adj Close"].shift(i+1)
# Crea il Dataframe dei rendimenti
tsret = pd.DataFrame(index=tslag.index)
tsret["Volume"] = tslag["Volume"]
tsret["Today"] = tslag["Today"].pct_change()*100.0
# Se qualsiasi dei valori dei rendimenti percentuali è pari a zero, si imposta questi con
# un numero molto piccolo (in modo da evitare le criticità del modello QDA di Scikit-Learn)
for i,x in enumerate(tsret["Today"]):
if (abs(x) < 0.0001):
tsret["Today"][i] = 0.0001
# Crea la colonna dei rendimenti percentuali ritardati
for i in range(0, lags):
tsret["Lag%s" % str(i+1)] = tslag["Lag%s" % str(i+1)].pct_change()*100.0
# Crea la colonna "Direction" (+1 or -1) che indica un giorno rialzista/ribassista
tsret["Direction"] = np.sign(tsret["Today"])
tsret = tsret[tsret.index >= start_date]
return tsret
Creiamo innanzitutto una serie ritardata dell’S&P500 utilizzando cinque ritardi. Includiamo anche il volume degli scambi. Tuttavia, limitiamo il set predittivo ai primi due ritardi. In questo modo, comunichiamo implicitamente al classificatore che consideriamo gli altri ritardi meno rilevanti dal punto di vista predittivo. Questo effetto, per inciso, rientra nel concetto statistico di autocorrelazione, che approfondiremo in un’altra lezione.
Dopo aver generato l’array dei predittori X e il vettore di risposta Y, suddividiamo i dati in un set di training e uno di test. Utilizziamo il primo per addestrare il classificatore e il secondo per valutare le prestazioni. Effettuiamo la divisione il 1° gennaio 2005, usando i dati precedenti per il training e quelli successivi per il testing.
Applicazione dei modelli predittivi
Una volta effettuata la divisione tra training e test, definiamo un array di modelli di classificazione. Ogni elemento è una tupla con un identificativo e il nome della funzione o modulo. In questo esempio, non impostiamo parametri per la regressione logistica, gli analizzatori discriminatori lineari e quadratici o i modelli SVM lineari, mentre adottiamo parametri predefiniti per gli SVM radiali (RSVM) e per la Random Forest (RF).
Dopo aver iterato sui modelli, eseguiamo il fitting su ciascun classificatore utilizzando i dati di training, quindi generiamo le previsioni sui dati di test. Infine, calcoliamo l’Hit-Rate e la matrice di confusione per ciascun modello.
if __name__ == "__main__":
# Crea una serie ritardata dell'indice S&P500 del mercato azionario US
snpret = create_lagged_series(
"^GSPC", datetime.datetime(2001,1,10),
datetime.datetime(2005,12,31), lags=5
)
# Uso il rendimento dei due giorni precedentei come valore
# di predizione, con la direzione come risposta
X = snpret[["Lag1","Lag2"]]
y = snpret["Direction"]
# I dati di test sono divisi in due parti: prima e dopo il 1 gennaio 2005.
start_test = datetime.datetime(2005,1,1)
# Crea il dataset di training e di test
X_train = X[X.index < start_test]
X_test = X[X.index >= start_test]
y_train = y[y.index < start_test]
y_test = y[y.index >= start_test]
# Crea i modelli (parametrizzati)
print("Hit Rates/Confusion Matrices:\n")
models = [("LR", LogisticRegression()),
("LDA", LDA()),
("QDA", QDA()),
("LSVC", LinearSVC()),
("RSVM", SVC(
C=1000000.0, cache_size=200, class_weight=None,
coef0=0.0, degree=3, gamma=0.0001, kernel='rbf',
max_iter=-1, probability=False, random_state=None,
shrinking=True, tol=0.001, verbose=False)
),
("RF", RandomForestClassifier(
n_estimators=1000, criterion='gini',
max_depth=None, min_samples_split=2,
min_samples_leaf=1, max_features='sqrt',
bootstrap=True, oob_score=False, n_jobs=1,
random_state=None, verbose=0)
)]
# Iterazione attraverso i modelli
for m in models:
# Addestramento di ogni modello con il set di dati di training
m[1].fit(X_train, y_train)
# Costruisce un array di predizioni sui dati di test
pred = m[1].predict(X_test)
# Stampa del hit-rate e della confusion matrix di ogni modello.
print("%s:\n%0.3f" % (m[0], m[1].score(X_test, y_test)))
print("%s\n" % confusion_matrix(pred, y_test))
Risultati
Di seguito si riporta l’output di tutti i modelli di classificazione. Probabilmente avrai valori diversi per l’output del RF (Random Forest) dato che quest’ultimo è intrinsecamente stocastico per definizione:
Hit Rates/Confusion Matrices:
LR:
0.560
[[ 35 35]
[ 76 106]]
LDA:
0.560
[[ 35 35]
[ 76 106]]
QDA:
0.599
[[ 30 20]
[ 81 121]]
LSVC:
0.560
[[ 35 35]
[ 76 106]]
RSVM:
0.563
[[ 9 8]
[102 133]]
RF:
0.504
[[48 62]
[63 79]]
Notiamo che tutti i tassi di successo si collocano tra il 50% e il 60%. Concludiamo quindi che le variabili ritardate non indicano con precisione la direzione futura. Tuttavia, osservando il comportamento dell’analizzatore discriminante quadratico (QDA), rileviamo che la sua prestazione predittiva complessiva sul set di test sfiora il 60%.
Conclusioni
Analizzando la matrice di confusione di questo modello (e anche degli altri), constatiamo che il tasso positivo per i giorni “down” supera di gran lunga quello per i giorni “up”. Se vogliamo sviluppare una strategia di trading basata su queste informazioni, possiamo prendere in considerazione l’idea di limitare le operazioni alle sole posizioni short sul S&P500 come possibile metodo per aumentare la redditività.
Nelle lezioni successive useremo questi modelli predittivi delle serie temporali come base per una strategia di trading, integrandoli direttamente nella struttura di backtesting event-driven. Inoltre, utilizzeremo uno strumento diretto, come un exchange traded fund (ETF), per accedere al trading del S&P500.
Il codice completo presentato in questa lezione è disponibile nel seguente repository GitHub: “https://github.com/tradingquant-it/TQResearch“