Strategia di Forecasting sul S&P500, backtesting con Python e Pandas

Recentemente su DataTrading abbiamo introdotto il machine learning, il forecasting e la progettazione e l’implementazione del backtesting di una strategia. In questo articolo si combinano tutti questi strumenti al fine di testare un algoritmo di previsione finanziaria per l’indice azionario statunitense S&P500 tramite lo strumento l’ETF (SPY).

Questo articolo si basa per la maggior parte sul software che abbiamo già sviluppato negli articoli menzionati sopra, incluso un motore di backtesting orientato agli oggetti e il generatore di segnali. La  programmazione orientata agli oggetti permette Permette di estendere una classe, facendole ereditare le proprietà di un’altra classe e ridefinendone altri (overriding). L’ereditarietà ci permette di vedere le relazioni di parentela tra classi che ereditano dalla stessa superclasse, come un albero radicato.

Inoltre le librerie Python come matplotlib, pandas e scikit-learn riducono la necessità di scrivere codice da zero o effettuare nuove implementazioni di algoritmi già noti e ampiamente testati.

La Strategia di Forecasting

Questa strategia di previsione si basa su una tecnica di apprendimento automatico nota come Analisi Discriminante Quadratica, che è strettamente correlata all’Analisi Discriminante Lineare. Entrambi questi modelli sono descritti nell’articolo relativo alla previsione delle serie temporali finanziarie.

Il forecaster utilizza i dati dei deu giorni precedenti come un set di fattori per prevedere la direzione odierna del mercato azionario. Se la probabilità che il giorno sia “Up” è maggiore del 50%, la strategia acquista 500 shares dell’ETF SPY e vende a fine della giornata, mentre se la probabilità di un giorno “Down” è maggiore del 50%, la strategia vende 500 azioni dell’ETF SPY e poi riacquista alla chiusura. Quindi è un semplice esempio di una strategia di trading intraday.

Nota che questa non è una strategia di trading particolarmente realistica! È improbabile che riusciremo mai a raggiungere un prezzo di apertura o di chiusura a causa di molti fattori quali l’eccessiva volatilità di apertura, il routing degli ordini da parte dell’intermediario e le potenziali criticità di liquidità durante l’apertura / chiusura. Inoltre non abbiamo incluso i costi di transazione. Questi sarebbero probabilmente una percentuale sostanziale dei rendimenti in quanto ci sono operazioni di apertura e chiusura posizioni ogni giorno. Pertanto, il nostro forecaster deve essere relativamente preciso nel prevedere i rendimenti giornalieri, altrimenti i costi di transazione mangeranno tutti i nostri profitti.

Implementazione

Come per gli altri esempi relativi a Python/Pandas, si usano le seguenti librerie:

  • Python
  • NumPy
  • Pandas
  • Matplotlib
  • Scikit-learn

L’implementazione di snp_forecast.py prevede di utilizzare backtest.py, descritto in questo tutorial. Inoltre è necessario importare forecast.py (che contiene principalmente la funzione create_lagged_series), implementato in questo precedente tutorial. Il primo passo è importare i necessari moduli ed oggetti:

            # snp_forecast.py

import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn

import pandas_datareader as pdr
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis as QDA

from backtest.backtest import Strategy, Portfolio
from model.forecast import create_lagged_series
        

Una volta incluse tutte le librerie e i moduli pertinenti, è tempo di creare una sottoclasse della classe base astratta Strategy, come abbiamo fatto in precedenti tutorial. SNPForecastingStrategy è progettata per implementare un Analizzatore Discriminante Quadratico per l’indice azionario S&P500, come metodo per predire il suo valore futuro. L’addestramento del modello viene eseguito tramite il metodo fit_model, mentre i segnali effettivi vengono generati dal metodo generate_signals. Questo corrisponde all’interfaccia di una classe Strategy.

I dettagli su come funziona un analizzatore discriminante quadratico, così come la seguente l’implementazione in Python, sono descritti in dettaglio nel precedente articolo relativo alla previsione delle serie temporali finanziarie. I commenti nel seguente codice sorgente descrivono ampiamente le funzionalità del programma:

            # snp_forecast.py

class SNPForecastingStrategy(Strategy):
    """
    Richiede:
    symbol - simbolo di un'azione per il quale applicare la strategia.
    bars - DataFrame delle barre del simbolo precedente.
    """

    def __init__(self, symbol, bars):
        self.symbol = symbol
        self.bars = bars
        self.create_periods()
        self.fit_model()

    def create_periods(self):
        """Crea i periodi di training/test."""
        self.start_train = datetime.datetime(2001,1,10)
        self.start_test = datetime.datetime(2005,1,1)
        self.end_period = datetime.datetime(2005,12,31)

    def fit_model(self):
        """Applica il Quadratic Discriminant Analyser al indice
        del mercato azionario US (^GPSC in Yahoo).
        """
        # Crea la serie ritardata dell'indice S&P500 del mercato azionario US
        snpret = create_lagged_series(self.symbol, self.start_train,
                                      self.end_period, lags=5)

        # Use i rendimenti dei 2 giorni precedenti come
        # valori di predizione, con la direzione come risposta
        X = snpret[["Lag1","Lag2"]]
        y = snpret["Direction"]

        # Crea i dataset di training e di test
        X_train = X[X.index < self.start_test]
        y_train = y[y.index < self.start_test]

        # Crea i fattori di predizioni per usare
        # la direzione predetta
        self.predictors = X[X.index >= self.start_test]

        # Crea il modello di Quadratic Discriminant Analysis
        # e la strategia previsionale
        self.model = QDA()
        self.model.fit(X_train, y_train)

    def generate_signals(self):
        """Restituisce il DataFrame dei simboli che contiene i segnali
        per andare long, short o flat (1, -1 or 0).
        """
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = 0.0

        # Predizione del periodo successivo con il modello QDA
        signals['signal'] = self.model.predict(self.predictors)

        # Rimuove i primi 5 segnali per eliminare gli elementi
        # NaN nel DataFrame dei segnali
        signals['signal'][0:5] = 0.0
        signals['positions'] = signals['signal'].diff()

        return signals
        

Ora che il motore di previsione è in grado di produrre i segnali, è necessario creare MarketIntradayPortfolio. Questo oggetto si differenzia dall’esempio fornito nell’articolo “Backtesting di una Strategia di Moving Average Crossover in Python con Pandas” in quanto svolge operazioni su base intraday.

Il portafoglio è progettato per “andare long” (acquistare) 500 azioni di SPY al prezzo di apertura se il segnale indica che si verificherà un giorno UP e poi vendere alla chiusura. Viceversa, il portafoglio è progettato per “andare short” (vendere) 500 azioni di SPY se il segnale indica che si verificherà un giorno DOWN e successivamente chiudere la posizione (riacquistare) al prezzo di chiusura.

Per raggiungere questo obiettivo, è necessario calcolare ogni giorno la differenza tra i prezzi di apertura del mercato aperto e i prezzi di chiusura del mercato, determinando il calcolo del profitto giornaliero sulle 500 azioni acquistate o vendute. Questo comporta quindi la costruzione di una curva equity formata dalla somma cumulata dei profitti/perditi per ogni giorno. Ha anche il vantaggio di poter facilmente calcolare le statistiche relative ai profitti / perdite di ogni giorno.

Di seguito il codice per la classe MarketIntradayPortfolio:

            # snp_forecast.py
class MarketIntradayPortfolio(Portfolio):
    """
    Acquista o vende 500 azioni di un'asset al prezzo di apertura di
    ogni barra, a seconda della direzione della previsione, chiude
    il trade alla chiusura della barra.

    Richiede:
    symbol - Un simbolo azionario che costituisce la base del portafoglio.
    bars - Un DataFrame di barre per un set di simboli.
    signals - Un DataFrame panda di segnali (1, 0, -1) per ogni simbolo.
    initial_capital - L'importo in contanti all'inizio del portafoglio.
    """

    def __init__(self, symbol, bars, signals, initial_capital=100000.0):
        self.symbol = symbol
        self.bars = bars
        self.signals = signals
        self.initial_capital = float(initial_capital)
        self.positions = self.generate_positions()

    def generate_positions(self):
        """Genera il DataFrame delle posizioni, basate sui segnali
        forniti dal DataFrame 'signals'.
        """
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)

        # Long o short di 500 azioni dello SPY basate sui
        # segnali direzionali giornalieri
        positions[self.symbol] = 500 * self.signals['signal']
        return positions

    def backtest_portfolio(self):
        """
        Backtest del portafoglio e restituisce un DataFrame contenente
        la curva equity e i precentuali dei rendimenti."""

        # Imposta l'oggetto Portfolio per avere lo stesso periodo
        # del DataFrame delle posizioni
        portfolio = pd.DataFrame(index=self.positions.index)
        pos_diff = self.positions.diff()

        # Calcola il profitto infragiornaliero della differenza tra 
        # i prezzi di apertura e chiusura e quindi determina il 
        # profitto giornaliero andando long se è previsto un giorno 
        # positivo e short se è previsto un giorno negativo
        portfolio['price_diff'] = self.bars['Close'] - self.bars['Open']
        portfolio['price_diff'][0:5] = 0.0
        portfolio['profit'] = self.positions[self.symbol] * portfolio['price_diff']

        # Genera la curva equity e la percentuale dei rendimenti
        portfolio['total'] = self.initial_capital + portfolio['profit'].cumsum()
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio
        

Il passaggio finale consiste nel legare gli oggetti Strategy e Portfolio tramite una funzione __main__. La funzione ottiene i dati per lo strumento SPY e quindi crea la strategia per la generazione del segnale sull’indice S&P500. Questa viene effettuato tramite il simbolo ^GSPC. Inoltre si crea un’instanza di un MarketIntradayPortfolio con un capitale iniziale di 100.000 USD (come nei tutorial precedenti). Infine, si calcolano i rendimenti e si traccia la curva di equity.

Da notare che il codice richiesto in questa fase è abbastanza ridotto perché gran parte del lavoro viene svolto dalle sottoclassi di Strategy e Portfolio. Ciò rende estremamente semplice creare nuove strategie di trading e testarle rapidamente per l’utilizzo nella “strategy pipeline”.

            
if __name__ == "__main__":
    start_test = datetime.datetime(2005, 1, 1)
    end_period = datetime.datetime(2005, 12, 31)

    # Download delle barre dell'ETF SPY che rispecchia l'indice S&P500
    bars = pdr.DataReader("SPY", "yahoo", start_test, end_period)

    # Crea la strategia di previsione dell'S&P500
    snpf = SNPForecastingStrategy("^GSPC", bars)
    signals = snpf.generate_signals()

    # Crea il portafoglio basate sul predittore
    portfolio = MarketIntradayPortfolio("SPY", bars, signals,
                                        initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    # Stampa il grafico dei risultati
    fig = plt.figure()
    fig.patch.set_facecolor('white')

    # Stampa il prezzo dell'ETF dello SPY
    ax1 = fig.add_subplot(211, ylabel='SPY ETF price in $')
    bars['Close'].plot(ax=ax1, color='r', lw=2.)

    # Stampa la curva di equity
    ax2 = fig.add_subplot(212, ylabel='Portfolio value in $')
    returns['total'].plot(ax=ax2, lw=2.)

    fig.show()
    print("")
        
Di seguito è riportato l’output del programma. Nel periodo sotto esame, il mercato azionario ha guadagnato il 4% (ipotizzando una strategia di “buy and hold” di investimento), e anche lo stesso l’algoritmo ha generato un rendimento del 4%. Si noti che i costi di transazione (come le commissioni) non sono stati considerati in questo sistema di backtesting. Dal momento che la strategia effettua due operazioni ogni giorno, è probabile che tali commissioni riducano significativamente i rendimenti.
Performance della Strategia di Forecasting sull'S&P500 dal 01-01-2005 al 31-12-2006
Negli articoli successivi vedremo come migliorare questo algoritmo implementando costi di transazione più realistici, utilizzando motori di previsione avanzati, e fornire strumenti per l’ottimizzazione del portafoglio.

 

Per il codice completo riportato in questo articolo utilizzando il modulo di backtesting vettoriale VectorBacktest si può consultare il seguente repository di github:
https://github.com/datatrading-info/VectorBacktest

Torna in alto
Scroll to Top