In questo lezione mostriamo come eseguire il backtest di una strategia di forecasting sull’S&P 500. Costruiamo una strategia di trading basata sul motore di predizione che trattiamo nei corsi dedicati al machine learning e al forecasting. Mettiamo alla prova il forecasting del mercato azionario traducendo le sue predizioni in operazioni di trading.
Costruiamo questo algoritmo a partire dal software che abbiamo già sviluppato con il sistema di backtesting vettoriale, descritto in questa lezione. Adattiamo il codice per integrarlo all’interno del sistema di trading event-driven TQTradingSystem, così da migliorare l’accuratezza nell’esecuzione degli ordini e nel calcolo delle performance.
La Strategia di Forecasting
Con questa strategia puntiamo a prevedere l’andamento dello SPY, l’ETF che replica l’S&P 500. Il nostro obiettivo è verificare se un semplice modello di previsione, basato su dati ritardati dei prezzi e con una moderata capacità predittiva, possa offrire un vantaggio rispetto alla strategia “buy & hold”.
Applichiamo le seguenti regole operative per costruire la strategia:
- Adattiamo un modello di previsione su un sottoinsieme di dati storici dell’S&P 500. In questo esempio utilizziamo l’Analisi Discriminante Quadratica, ma possiamo anche usare la regressione logistica, le macchine a vettori di supporto o le foreste casuali.
- Usiamo due ritardi sui rendimenti dei prezzi di chiusura aggiustati per prevedere il rendimento del giorno successivo. Se il modello prevede un rendimento positivo, entriamo long. Se invece prevede un rendimento negativo, usciamo dalla posizione. Non consideriamo la vendita allo scoperto in questa strategia.
Implementazione
Per questa strategia di forecasting sull’S&P 500, creiamo il file snp_forecast.py e importiamo le seguenti librerie:
import datetime
import pandas as pd
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis as QDA
from strategy.strategy import Strategy
from event.event import SignalEvent
from backtest.backtest import Backtest
from data.data import HistoricCSVDataHandler
from execution.execution import SimulatedExecutionHandler
from portfolio.portfolio import Portfolio
from model.forecast import create_lagged_series
Abbiamo importato Pandas e Scikit-Learn per eseguire la procedura di adattamento per il modello di classificazione supervisionato. Abbiamo anche importato le classi necessarie dal motore di backtesting basato su eventi. Infine, abbiamo importato la funzione create_lagged_series, che abbiamo utilizzato nell’articolo di introduzione al forecasting delle serie temporali capitolo Previsione.
Il passaggio successivo consiste nel creare SPYDailyForecastStrategy come sottoclasse della classe base astratta Strategy. Poiché “codificheremo” i parametri della strategia direttamente nella classe, per semplicità, gli unici parametri necessari per il costruttore __init__ sono il gestore dati delle barre e la coda degli eventi.
Impostiamo le date di inzio / fine / test del modello di previsione e poi diciamo alla classe che siamo fuori dal mercato (self.long_market = False). Infine, impostiamo self.model come modello addestrato dalla funzione create_symbol_forecast_model, come segue:
class SPYDailyForecastStrategy(Strategy):
"""
Strategia previsionale dell'S&P500. Usa un Quadratic Discriminant
Analyser per prevedere i rendimenti per uno determinato sottoperiodo
e quindi genera segnali long e di uscita basati sulla previsione.
"""
def __init__(self, bars, events):
self.bars = bars
self.symbol_list = self.bars.symbol_list
self.events = events
self.datetime_now = datetime.datetime.utcnow()
self.model_start_date = datetime.datetime(2001,1,10)
self.model_end_date = datetime.datetime(2005,12,31)
self.model_start_test_date = datetime.datetime(2005,1,1)
self.long_market = False
self.short_market = False
self.bar_index = 0
self.model = self.create_symbol_forecast_model()
Addestramento del modello
Per addestrare il modello di una strategia di forecasting iniziamo definendo il metodo create_symbol_forecast_model. Usiamo la funzione create_lagged_series, che genera un DataFrame pandas con cinque diversi ritardi dei rendimenti giornalieri per ogni predittore attuale. Selezioniamo solo i due ritardi più recenti, perché decidiamo di applicare una regola di modellazione che presume un potere predittivo minimo nei ritardi più lontani.
In questa fase costruiamo i dati di addestramento e di test, anche se possiamo usare solo i primi se preferiamo. Scegliamo di non includere dati di test, dato che nel precedente articolo abbiamo già addestrato il modello. Adattiamo i dati di addestramento al Quadratic Discriminant Analyzer e restituiamo direttamente il modello.
Possiamo facilmente sostituire il modello con una foresta casuale, una macchina a vettori di supporto o una regressione logistica. Per farlo, importiamo la libreria desiderata da Scikit-Learn e modifichiamo semplicemente la riga model = QDA().
def create_symbol_forecast_model(self):
# Creazione delle serie ritardate dell'indice S&P500
# del mercato azionario US
snpret = create_lagged_series(
self.symbol_list[0], self.model_start_date,
self.model_end_date, lags=5
)
# Uso i rendimenti dei due giorni precedenti come valore
# previsionale, con direzione come risposta
X = snpret[["Lag1", "Lag2"]]
y = snpret["Direction"]
# Creazione dei set di dati per il training e il test
start_test = self.model_start_test_date
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]
model = QDA()
model.fit(X_train, y_train)
return model
Generazione dei segnali
Il prossimo passo per implementare una strategia di forecasting nel sistema di trading DataTrader è sostituire il metodo calculate_signals della classe base Strategy. Per prima cosa calcoliamo alcuni parametri utilizzati dal nostro oggetto SignalEvent e quindi generiamo un set di segnali solo se abbiamo ricevuto un oggetto MarketEvent (un controllo base di integrità).
Attendiamo che siano trascorse cinque barre (ovvero cinque giorni per questa strategia!) e quindi otteniamo i valori di rendimenti ritardati. Quindi racchiudiamo questi valori in una serie pandas in modo da garantire il corretto funzionamento del metodo di previsione del modello. Quindi calcoliamo una previsione, che si manifesta come un valore +1 o -1.
Se la previsione è un +1 e non siamo già long sul mercato, creiamo un SignalEvent per andare long e far sapere al sistema che siamo entrati a mercato. Se la previsione è -1 e siamo long sul mercato, allora si esce dal mercato:
def calculate_signals(self, event):
"""
Calcolo di SignalEvents in base ai dati di mercato.
"""
sym = self.symbol_list[0]
dt = self.datetime_now
if event.type == 'MARKET':
self.bar_index += 1
if self.bar_index > 5:
lags = self.bars.get_latest_bars_values(
self.symbol_list[0], "returns", N=3
)
pred_series = pd.Series({
'Lag1': lags[1] * 100.0,
'Lag2': lags[2] * 100.0
})
pred = self.model.predict(pred_series)
if pred > 0 and not self.long_market:
self.long_market = True
signal = SignalEvent(1, sym, dt, 'LONG', 1.0)
self.events.put(signal)
if pred < 0 and self.long_market:
self.long_market = False
signal = SignalEvent(1, sym, dt, 'EXIT', 1.0)
self.events.put(signal)
Esecuzione del Backtest
Per eseguire strategia di forecasting dobbiamo scaricare un file CSV da Yahoo Finance con i dati storici di SPY e posizionarlo in una specifica directory (da notare che si dovrà cambiare il percorso nel seguente codice!).
Quindi implementiamo la logica di backtesting tramite la classe Backtest ed eseguiamo il backtest richiamando il metodo simulate_trading:
if __name__ == "__main__":
csv_dir = '/path/to/your/csv/file' # CHANGE THIS!
symbol_list = ['SPY']
initial_capital = 100000.0
heartbeat = 0.0
start_date = datetime.datetime(2006,1,3)
backtest = Backtest(
csv_dir, symbol_list, initial_capital, heartbeat,
start_date, HistoricCSVDataHandler, SimulatedExecutionHandler,
Portfolio, SPYDailyForecastStrategy
)
backtest.simulate_trading()
..
..
2209
2210
Creating summary stats...
Creating equity curve...
SPY cash commission total returns equity_curve \
datetime
2014-09-29 19754 90563.3 349.7 110317.3 -0.000326 1.103173
2014-09-30 19702 90563.3 349.7 110265.3 -0.000471 1.102653
2014-10-01 19435 90563.3 349.7 109998.3 -0.002421 1.099983
2014-10-02 19438 90563.3 349.7 110001.3 0.000027 1.100013
2014-10-03 19652 90563.3 349.7 110215.3 0.001945 1.102153
2014-10-06 19629 90563.3 349.7 110192.3 -0.000209 1.101923
2014-10-07 19326 90563.3 349.7 109889.3 -0.002750 1.098893
2014-10-08 19664 90563.3 349.7 110227.3 0.003076 1.102273
2014-10-09 19274 90563.3 349.7 109837.3 -0.003538 1.098373
2014-10-09 0 109836.0 351.0 109836.0 -0.000012 1.098360
drawdown
datetime
2014-09-29 0.003340
2014-09-30 0.003860
2014-10-01 0.006530
2014-10-02 0.006500
2014-10-03 0.004360
2014-10-06 0.004590
2014-10-07 0.007620
2014-10-08 0.004240
2014-10-09 0.008140
2014-10-09 0.008153
[(’Total Return’, ’9.84%’),
(’Sharpe Ratio’, ’0.54’),
(’Max Drawdown’, ’5.99%’),
(’Drawdown Duration’, ’811’)]
Signals: 270
Orders: 270
Fills: 270
Risultati
La seguente figura mostra la curva di equity, i rendimenti giornalieri e il drawdown della strategia in funzione del tempo.
Da notare subito come la performance non è eccezionale! Abbiamo uno Sharpe Ratio <1 ma un ragionevole drawdown di poco inferiore al 6%. Si è scoperto che se avessimo semplicemente acquistato e tenuto SPY in questo periodo di tempo avremmo avuto un risultato simile, anche se leggermente peggiore.
Quindi non abbiamo effettivamente ottenuto un vantaggio da questa strategia predittiva una volta inclusi i costi di transazione. In particolare, ho voluto includere questo esempio perché utilizza un’implementazione realistica “end to end” di tale strategia che tiene conto dei costi di transazione conservativi e realistici. Come si può vedere non è facile fare una previsione predittiva su dati giornalieri che produca buone prestazioni!
Il codice completo presentato in questa lezione, basato sul sistema di trading event-driven TQTradingSystem, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/TQTradingSystem.”
