Strategia di Mean Reversion per il Pairs Trading Intraday

In questo articolo si vuole descrivere la nostra prima strategia di trading intraday. Si basa su una classica idea di trading, quella delle “coppie di trading”. 

La strategia crea generalmente uno “spread” tra la coppia di asset considerati, andando long su una e short sull’altra. Il rapporto tra long e short può essere definito in molti modi, ad esempio utilizzando tecniche di cointegrazione statistica sulle serie temporali. In questo esempio si calcola il “hedge ratio” (rapporto di copertura) tra gli asset tramite una regressione lineare a rotazione. Questo permette
di creare un “spread” che viene normalizzato in uno z-score. I segnali di trading sono generati quando lo z-score supera una determinata soglia, nella convinzione che lo spread tornerà verso la media.

La logica della strategia prevede che gli asset considerati siano approssimativamente caratterizzate dallo stesso comportamento o andamento. L’idea base consiste nel considerare che lo spread dei prezzi ha un comportamento mean-reverting, dal momento che eventi “locali” (nel tempo) possono influenzare separatamente i singoli asset (come differenze di capitalizzazione, date di ribilanciamento o operazioni di blocco) ma nel lungo termine le serie di prezzi tendono ad essere cointegrate.

La Strategia

Al fine di ottenere Sharpe Ratio più alti, è necessario adottare strategie intraday ad alta frequenza.
Il primo importante problema è ottenere dati significativi, dato che i dati intraday di alta qualità non sono solitamente gratuiti. Come descritto negli articoli precedetti, si utilizza il DTN IQFeed per acquisire le barre intraday al minuto e quindi si avrà bisogno di un account DTN per ottenere i dati richiesti per questa strategia. Il secondo problema consiste nel fatto che le simulazioni di backtesting impiegano molto più tempo, specialmente con il modello event-driven, descritto in questa serie di articoli. Se si vuol effettuare il backtesting di un portafolio diversificato con dati al minuto un numero significativo di anni passati, e quindi eseguire qualsiasi ottimizzazione dei parametri, ci si rende rapidamente conto che le simulazioni possono richiedere ore o persino giorni, se effettatu su modermo PC desktop. Questo dovrà essere preso in considerazione nel durante il processo di ricerca e studio della strategia.
Il terzo problema è la completa automazione dell’esecuzione nel live trading poiché ci si sta avvicinando al trading ad alta frequenza e quindi il sistema di esecuzione dove essere altamente performante. Questo significa che l’ambiente e il codice di esecuzione devono essere altamente affidabili e privi di errori, altrimenti potrebbe verificarsi significative perdite.
Questa strategia espande la precedente strategia multiday al fine di utilizzare i dati intraday.
In particolare, si usa barre OHLCV al minuto, a differenza di dati OHLCV giornalieri. Le regole per la strategia sono semplici:

  1. Identificare una coppia di titoli azionari le cui serie temporali hanno statisticamente un comportamento riconducile al mean-reverting. In questo caso, si considerano i due titoli azionari statunitensi con i ticker AREX e WLL.
  2. Creare le serie temporali residue della coppia eseguendo una regressione lineare a rotazione, per una specifica finestra di ricerca, tramite l’algoritmo dei minimi quadrati ordinari (OLS). Questo periodo di ricerca è un parametro da ottimizzare.
  3. Creare un z-score a rotazione delle serie temporali residue per lo stesso periodo di ricerca e utilizzarlo per determinare le soglie di ingresso / uscita per i segnali di trading.
  4. Se la soglia superiore viene superata quando non si è sul mercato, allora si ENTRA a mercato (long o short dipende dalla direzione di rottura della soglia viene). Se invece viene superata la soglia inferiore quando si ha una posizione a mercato, allora si ESCE dal mercato. Anche le soglie superiore e inferiore sono parametri da ottimizzare.

In effetti si potrebbe usare il test Cointegrated Augmented Dickey-Fuller (CADF) per identificare un parametro di copertura ancora più accurato. Questo potrebbe essere un’interessate evoluzione di questa strategia.

Implementazione in Python

Come per tutti i tutorial con Python e Pandas, è necessario aver impostato un ambiente di backtesting con Python, come descritto in questo tutorial. Una volta impostato, il primo passo è importare le necessarie librerie Python. Per questo backtest sono richiesti matplotlib e pandas.

In particolare si utilizza metodo rolling_apply, al fine di applicare il calcolo dello z-score ad una finestra di ricerca a rotazione. Si importa statsmodels perché fornisce un mezzo per calcolare l’algoritmo dei minimi quadrati ordinari (OLS) per la regressione lineare, al fine di  ottenere il rapporto di copertura per la costruzione dei residui.
Si prevede inoltre un DataHandler e un Portfolio leggermente modificati per effettuare operazioni di trading al minuto sui dati DTN IQFeed. Per creare questi file si può semplicemente copiare tutto il codice di portfolio.py e data.py rispettivamente nei nuovi file hft_portfolio.py e hft_data.py e quindi modificare le sezioni necessarie, che illustrato di seguito.

# intraday_mr.py

import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm

from strategy.strategy import Strategy
from event.event import SignalEvent
from backtest.backtest import Backtest

from data.hft_data import HistoricCSVDataHandlerHFT
from portfolio.hft_portfolio import PortfolioHFT

from execution.execution import SimulatedExecutionHandler
Con il seguente codice si crea la classe IntradayOLSMRStrategy, derivata dalla classe base astratta di Strategy. Il metodo __init__ del costruttore richiede l’accesso al provider di dati storici, alla coda degli eventi, a una soglia zscore_low e a una soglia zscore_high, utilizzate per determinare quando la serie residua tra le due coppie è di tipo mean-reverting. Inoltre, si specifica la finestra di ricerca OLS (impostata su 100), che è un parametro soggetto a potenziale ottimizzazione. All’inizio della simulazione non si è long o short sul mercato, quindi si imposta sia self.long_market che self.short_market uguale a False:
# intraday_mr.py


class IntradayOLSMRStrategy(Strategy):
    """
    Utilizza i minimi quadrati ordinari (OLS) per eseguire una regressione lineare
    continua in modo da determinare il rapporto di hedge tra una coppia di azioni.
    Lo z-score delle serie temporali dei residui viene quindi calcolato in modo
    continuo e se supera un intervallo di soglie (predefinito a [0,5, 3,0]), viene
    generata una coppia di segnali long / short (per la soglia alta) o vengono
    generate coppie di segnali di uscita (per la soglia bassa).
    """
    def __init__(self, bars, events, ols_window=100,zscore_low=0.5, zscore_high=3.0):
        """
        Initializza la strategia di arbitraggio stastistico.
        Parametri:
        bars - L'oggetto DataHandler che fornisce i dati di mercato
        events - L'oggetto Event Queue.
        """
        self.bars = bars
        self.symbol_list = self.bars.symbol_list
        self.events = events
        self.ols_window = ols_window
        self.zscore_low = zscore_low
        self.zscore_high = zscore_high
        self.pair = ('AREX', 'WLL')
        self.datetime = datetime.datetime.utcnow()
        self.long_market = False
        self.short_market = False


Il seguente metodo, calculate_xy_signals, prende lo zscore corrente (dal calcolo rolling eseguito di seguito) e determina se è necessario generare nuovi segnali di trading. Questi segnali vengono resi disponibili in output. Ci sono quattro stati potenziali a cui si può essere interessati:
  1. Long sul mercato e sotto la più alta soglia negativa dello zscore
  2. Long il mercato e all’interno del valore assoluto della soglia più alta dello zscore
  3. Short sul mercato e sopra la maggiore soglia positiva dello z-score
  4. Short sul mercato e all’interno tra il valore assoluto del valore assoluto della soglia inferiore più bassa dello zscore.
In tutti i casi è necessario generare due segnali, uno per la prima componente della coppia (AREX) e uno per la seconda componente della coppia (WLL). Se nessuna di queste condizioni viene soddisfatta, si restituisce una coppia di valori None:

# intraday_mr.py

def calculate_xy_signals(self, zscore_last):
    """
    Calcola le effettive coppie di segnali x, y da inviare al generatore di segnali.

    Parametri
    zscore_last - Il punteggio dello z-score su cui eseguire il test
    """
    y_signal = None
    x_signal = None
    p0 = self.pair[0]
    p1 = self.pair[1]
    dt = self.datetime
    hr = abs(self.hedge_ratio)

    # Se siamo long sul mercato e al di sotto del
    # negativo della soglia alta dello zscore
    if zscore_last <= -self.zscore_high and not self.long_market:
        self.long_market = True
        y_signal = SignalEvent(1, p0, dt, 'LONG', 1.0)
        x_signal = SignalEvent(1, p1, dt, 'SHORT', hr)

    # Se siamo long sul mercato e tra il
    # valore assoluto della soglia bassa dello zscore
    if abs(zscore_last) <= self.zscore_low and self.long_market:
        self.long_market = False
        y_signal = SignalEvent(1, p0, dt, 'EXIT', 1.0)
        x_signal = SignalEvent(1, p1, dt, 'EXIT', 1.0)

    # Se siamo short sul mercato e oltre
    # la soglia alta dello z-score
    if zscore_last >= self.zscore_high and not self.short_market:
        self.short_market = True
        y_signal = SignalEvent(1, p0, dt, 'SHORT', 1.0)
        x_signal = SignalEvent(1, p1, dt, 'LONG', hr)

    # Se siamo short sul mercato e tra il
    # valore assoluto della soglia bassa dello z-score
    if abs(zscore_last) <= self.zscore_low and self.short_market:
        self.short_market = False
        y_signal = SignalEvent(1, p0, dt, 'EXIT', 1.0)
        x_signal = SignalEvent(1, p1, dt, 'EXIT', 1.0)

    return y_signal, x_signal

Il seguente metodo, calculate_signals_for_pairs acquisisce l’ultimo set di barre per ogni componente della coppia (in questo caso 100 barre) e le utilizza per costruire una regressione lineare basata su minimi quadrati ordinari. Ciò consente l’identificazione del rapporto di copertura, necessario per la costruzione delle serie temporali residue.

Una volta ricavato il rapporto di copertura, si crea lo spread delle serie di residui. Il passo successivo consiste nel calcolare l’ultimo z-score dalle serie residue sottraendo la loro media e dividendo per la loro deviazione standard nel periodo di ricerca.
Infine, y_signal e x_signal sono calcolati sulla base di questo z-score. Se i segnali non sono entrambi None, le istanze SignalEvent vengono inviate alla coda degli eventi:

# intraday_mr.py

def calculate_signals_for_pairs(self):
    """
    Genera una nuova serie di segnali basati sulla strategia di
    ritorno verso la media (mean reversion).
    Calcola il rapporto di hedge tra la coppia di ticker.
    Usiamo OLS per questo, anche se dovremmo idealmente usare il CADF.
    """

    # Otteniamo l'ultima finestra di valori per ogni
    # componente della coppia di ticker
    y = self.bars.get_latest_bars_values(
        self.pair[0], "close", N=self.ols_window
    )
    x = self.bars.get_latest_bars_values(
        self.pair[1], "close", N=self.ols_window
    )

    if y is not None and x is not None:
        # Verificare che tutti i periodi di finestra siano disponibili
        if len(y) >= self.ols_window and len(x) >= self.ols_window:
            # Calcola l'attuale rapporto di hedge utilizzando OLS
            self.hedge_ratio = sm.OLS(y, x).fit().params[0]

            # Calcola l'attuale z-score dei residui
            spread = y - self.hedge_ratio * x
            zscore_last = ((spread - spread.mean()) / spread.std())[-1]

            # Calcula i segnali e il aggiunge alla coda degli eventi
            y_signal, x_signal = self.calculate_xy_signals(zscore_last)
            if y_signal is not None and x_signal is not None:
                self.events.put(y_signal)
                self.events.put(x_signal)
Il metodo finale, calculate_signals è sovrascritto dalla classe base e viene utilizzato per verificare se un evento ricevuto dalla coda è in realtà un MarketEvent, nel qual caso viene eseguito il calcolo dei nuovi segnali:
# intraday_mr.py

def calculate_signals(self, event):
    """
    Calcula il SignalEvents basato sui dati di mercato.
    """
    if event.type == ’MARKET’:
        self.calculate_signals_for_pairs()
La funzione __main__ collega insieme i componenti al fine di eseguire il backtesting di una strategia. Si specifica dove sono archiviati i dati al minuto dei ticker, utilizzando il formato dei simboli IQFeed DTN. Necessariamente i file sono stati modificati in modo tale che iniziano e finiscono sullo stesso minuto. Per questa particolare coppia di AREX e WLL, la data comune di inizio è l’8 novembre 2007 alle 10:41:00. Infine, si costruisce l’oggetto backtest e si inizia a simulare il trading:
# intraday_mr.py

if __name__ == "__main__":
    csv_dir = '/path/to/your/csv/file' # DA MODIFICARE
    symbol_list = ['AREX', 'WLL']
    initial_capital = 100000.0
    heartbeat = 0.0
    start_date = datetime.datetime(2007, 11, 8, 10, 41, 0)

    backtest = Backtest(
        csv_dir, symbol_list, initial_capital, heartbeat,start_date, 
        HistoricCSVDataHandlerHFT, SimulatedExecutionHandler,
        PortfolioHFT, IntradayOLSMRStrategy
    )
    backtest.simulate_trading()
Tuttavia, prima di poter eseguire questo codice, è necessario apportare alcune modifiche al gestore dati e all’oggetto portfolio. In particolare, è necessario creare nuovi file hft_data.py e hft_portfolio.py che sono rispettivamente copie di data.py e portfolio.py. In hft_data.py si deve rinominare HistoricCSVDataHandler in HistoricCSVDataHandlerHFT e sostituire l’elenco dei nomi nel metodo _open_convert_csv_files. Il vecchio codice è:
names=[
       'datetime', 'open', 'high',
       'low', 'close', 'adj_close', 'volume'
      ]
Deve essere sostituito con il seguente:
names=[
       'datetime', 'open', 'high',
       'low', 'close', 'volume', 'oi'
      ]
In questo modo si garantisce la compatibilità tra il nuovo formato di DTN IQFeed e il sistema di backtesting. L’altro cambiamento consiste nel rinominare Portfolio in PortfolioHFT all’interno di hft_portfolio.py. Si deve quindi modificare alcune righe per tenere conto della frequenza minima dei dati DTN. In particolare, all’interno del metodo update_timeindex, è necessario modificare il seguente codice:
for s in self.symbol_list:
    # Approssimazione al valore reale
    market_value = self.current_positions[s] * \
        self.bars.get_latest_bar_value(s, "adj_close")
    dh[s] = market_value
    dh[’total’] += market_value
Per farlo diventare come segue:
for s in self.symbol_list:
    # Approssimazione al valore reale
    market_value = self.current_positions[s] * \
        self.bars.get_latest_bar_value(s, "close")
    dh[s] = market_value
    dh[’total’] += market_value
Questo assicura di considerare il prezzo di chiusura, piuttosto che il prezzo di adj_close. Quest’ultimo è utilizzato da Yahoo Finance, mentre il primo è di DTN IQFeed. Si deve inoltre prevedere un aggiustamento simile in update_holdings_from_fill. Si deve cambiare il seguente codice:
# Aggiornamento della lista delle holdings con le nuove quantità
    fill_cost = self.bars.get_latest_bar_value(
        fill.symbol, "adj_close"
    )
come segue:
# Aggiornamento della lista delle holdings con le nuove quantità
    fill_cost = self.bars.get_latest_bar_value(
        fill.symbol, "close"
    )
La modifica finale si effettua nel metodo output_summary_stats, nella parte inferiore del file. Si deve modificare il metodo di calcolato del Sharpe Ratio per tenere conto del trading con barre al minuto. La seguente riga:
sharpe_ratio = create_sharpe_ratio(returns)
Viene modificata come:
sharpe_ratio = create_sharpe_ratio(returns, periods=252*6.5*60)
Dopo aver mandato in esecuzione il file intraday_mr.py si ottiene il seguente output (troncato) dalla simulazione di backtest:
..
..
375072
375073
Creating summary stats...
Creating equity curve...
                    AREX WLL cash commission total returns \
datetime
2014-03-11 15:53:00 2098 -6802 120604.3 9721.4 115900.3 -0.000052
2014-03-11 15:54:00 2101 -6799 120604.3 9721.4 115906.3 0.000052
2014-03-11 15:55:00 2100 -6802 120604.3 9721.4 115902.3 -0.000035
2014-03-11 15:56:00 2097 -6810 120604.3 9721.4 115891.3 -0.000095
2014-03-11 15:57:00 2098 -6801 120604.3 9721.4 115901.3 0.000086
2014-03-11 15:58:00 2098 -6800 120604.3 9721.4 115902.3 0.000009
2014-03-11 15:59:00 2099 -6800 120604.3 9721.4 115903.3 0.000009
2014-03-11 16:00:00 2100 -6801 120604.3 9721.4 115903.3 0.000000
2014-03-11 16:01:00 2100 -6801 120604.3 9721.4 115903.3 0.000000
2014-03-11 16:01:00 2100 -6801 120604.3 9721.4 115903.3 0.000000

                    equity_curve drawdown
datetime
2014-03-11 15:53:00 1.159003 0.003933
2014-03-11 15:54:00 1.159063 0.003873
2014-03-11 15:55:00 1.159023 0.003913
2014-03-11 15:56:00 1.158913 0.004023
2014-03-11 15:57:00 1.159013 0.003923
2014-03-11 15:58:00 1.159023 0.003913
2014-03-11 15:59:00 1.159033 0.003903
2014-03-11 16:00:00 1.159033 0.003903
2014-03-11 16:01:00 1.159033 0.003903
2014-03-11 16:01:00 1.159033 0.003903

[(’Total Return’, ’15.90%’),
 (’Sharpe Ratio’, ’1.89’),
 (’Max Drawdown’, ’3.03%’),
 (’Drawdown Duration’, ’120718’)]
Signals: 7594
Orders: 7478
Fills: 7478
Si nota facilmente che la strategia si comporta bene durante il periodo sotto esame. Ha un rendimento totale di poco inferiore al 16%. Il Sharpe Ratio ragionevole (se confrontato con una tipica strategia giornaliera), ma data la natura ad alta frequenza della strategia ci si dovrebbe aspettare di più. La migliore caratteristica di questa strategia è il basso drawdown massimo(circa il 3%). Questo suggerisce di poter applicare una leva maggiore per ottenere più profitto.

Visualizzazione grafica delle Performance

Si può facilmente visualizzare il grafico dei rendimenti risposto all’intervallo di ricerca (numero di barre) e tutte le altre misure delle performance usando lo script plot_performance.py. Tale codice può essere utilizzato come base per creare grafici personalizzati delle prestazioni.

È necessario eseguirlo nella stessa directory del file di output dal backtest, ovvero dove risiede equity.csv. Il codice è il seguente:

# plot_performance.py

import os.path
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

if __name__ == "__main__":
    data = pd.io.parsers.read_csv(
        "equity.csv", header=0,
        parse_dates=True, index_col=0
    )

    # Visualizza tre grafici: curva di Equity,
    # rendimenti, drawdown
    fig = plt.figure()

    # Imposta il bianco come colore di sfondo
    fig.patch.set_facecolor('white')

    # Visualizza la curva di equity
    ax1 = fig.add_subplot(311, ylabel='Portfolio value, % ')
    data['equity_curve'].plot(ax=ax1, color="blue", lw=2.)
    plt.grid(True)

    # Visualizza i rendimenti
    ax2 = fig.add_subplot(312, ylabel='Period returns, % ')
    data['returns'].plot(ax=ax2, color="black", lw=2.)
    plt.grid(True)

    # Visualizza i drawdown
    ax3 = fig.add_subplot(313, ylabel='Drawdowns, % ')
    data['drawdown'].plot(ax=ax3, color="red", lw=2.)
    plt.grid(True)

    # Stampa i grafici
    plt.show()
Utilizzando l’output CSV del backtesting della precedente strategia si ottengono i seguenti grafici:
Fig - Curva di Equity, Ritorni Giornalieri e Drawdown per la strategia mean-reversion intraday.

Implementazione di una Strategia di Forecasting sull’S&P500

Strategia di Forecasting S&P500 trading algoritmico

In questo articolo consideriamo una strategia di trading costruita sulla base del motore di predizione descritto nei precedenti articoli sul tema machine learning e forecasting.
Proveremo a tradare le predizioni effettuate dal foracaster del mercato azionario.


Questo algoritmo si basa per la maggior parte sul software che abbiamo già sviluppato utilizzando il backtesting vettoriale e descritto in questo articolo. Esso viene rivesto ed adattato per essere innestato nel nuovo motore di backtesting basato sugli eventi in modo da avere una maggiore accuratezza nell’esecuzione delle operazioni e calcolo delle performance.

La Strategia di Forecasting

In questa strategia si vuol prevedere l’andamento dello SPY, l’ETF che replica il valore dell’S&P 500. In definitiva, vogliamo rispondere alla domanda se un semplice algoritmo di previsione che utilizza dati sui prezzi ritardati e con una leggera performance predittiva, possa offrire vantaggi rispetto a una strategia “buy & hold”.

Le regole per questa strategia sono le seguenti:

  1. Adattare un modello di previsione a un sottoinsieme di dati dell’S&P500. In questa strategia utilizziamo l’ Analisi Discriminante Quadratica , ma si potrebbe utilizzare anche una regressione logistica, una macchina vettoriale di supporto o una foresta casuale.
  2. Utilizza due ritardi precedenti sui rendimenti dei prezzi di chiusura aggiustati come predittore dei rendimenti di domani. Se i rendimenti sono previsti positivi, allora si va long. Se i rendimenti sono previsti negativi, si esce dalla posizione. Non prenderemo in considerazione la vendita allo scoperto per questa particolare strategia.

Implementazione

Per questa strategia si prevede di creare il snp_forecast.py ed importare le seguenti librerie: 

# snp_forecast.py

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:

# snp_forecast.py

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()
Definiamo quindi create_symbol_forecast_model. Essenzialmente si richiama la funzione create_lagged_series, che produce un DataFrame pandas con cinque diveri ritardi di rendimenti giornalieri per ogni predittore corrente. Consideriamo quindi solo i due ritardi più recenti. Questo perché stiamo introducendo una regola di modellazione che consiste nell’ipotizzare che il potere predittivo dei ritardi precedenti è probabilmente minimo.

In questa fase creiamo i dati di addestramento e test, l’ultimo dei quali può essere utilizzato per testare il nostro modello, se lo si desidera. Si è scelto di non produrre dati di test, poiché abbiamo già addestrato il modello nel precedente articolo. Infine adattiamo i dati di addestramento al Quadratic Discriminant Analyzer e quindi restituiamo il modello. Si noti che possiamo facilmente sostituire il modello con, ad esempio, una foresta casuale, una macchina vettoriale di supporto o una regressione logistica. Tutto quello che dobbiamo fare è importare la libreria corretta da Scikit-Learn e sostituire semplicemente la riga model = QDA():
# snp_forecast.py

    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
A questo punto siamo pronti per sostituire il metodo prepare_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:
# snp_forecast.py

    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)
Per eseguire questa strategia si deve 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 si incampusa la logica di backtesting tramite la classe Backtest ed si esegue il test richiamando il metodo simulate_trading:
# snp_forecast.py

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()
Il risultato della strategia è il seguente (al netto dei costi di transazione)
..
..
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
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!
Forecast_SP500_performance_trading_algoritmico

 

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

Implementazione di una Strategia di Moving Average Crossover

In questo articolo vediamo come implementare una semplice strategia di trading utilizzando il motore backtesting basato sugli eventi, descritto negli articoli precedenti. In particolare vediamo come creare le curve equity utilizzando gli iimporti nozionali di portafoglio, simulando così i concetti di margine / leva finanziaria, che è un approccio molto più realistico rispetto all’approccio vettorizzato / basato sui rendimenti.

Questa prima strategia può essere eseguita con dati liberamente disponibili, sia da Yahoo Finance, Google Finance o Quandl. E’ una strategia adatta per trader algoritmici a lungo termine che desiderano studiare solo l’aspetto della generazione del segnale di trade della strategia ma anche l’intero sistema end-to-end. Tali strategie spesso possiedono Sharpe Ratio più piccoli, ma sono molto facili da implementare ed eseguire.

La Strategia di Moving Average Crossover

Sono un grande sostenitore di sistemi di trading basati sull’incrocio della media mobile perché è la prima strategia non banale estremamente utile per testare una nuova implementazione di un motore di backtesting. Su un arco temporale giornaliero, su un numero di anni, con lunghi periodi di ricerca, vengono generati pochi segnali su un singolo stock ed è quindi facile verificare manualmente che il sistema si stia comportando come ci si aspetterebbe.

Per generare effettivamente una tale simulazione basata sul codice di backtesting basato sugli eventi dobbiamo creare una sottoclasse dell’oggetto Strategy, come descritto nell’articolo precedente, per creare l’oggetto MovingAverageCrossStrategy, che conterrà la logica di calcolo delle medie mobili semplici e la generazione dei segnali di trading.
Inoltre dobbiamo creare la funzione __main__ che caricherà l’oggetto Backtest e incapsulerà effettivamente l’esecuzione del programma. Il seguente file, mac.py, contiene entrambi questi oggetti.

Il primo compito, come sempre, è importare correttamente i componenti necessari. Stiamo importando quasi tutti gli oggetti che costituiscono il motore di backtesting event-driven:

# mac.py

import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm
import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm

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

Passiamo ora alla creazione della classe  MovingAverageCrossStrategy. La strategia richiede le barre generate da DataHandler, gli eventi gestiti da Event Queue e i periodi di ricerca per le medie mobili semplici che verranno impiegate all’interno della strategia. Per questa strategia consideriamo 100 e 400 come periodi di ricerca “brevi” e “lunghi” per questa strategia.

L’attributo finale, bought, viene utilizzato per indicare all’oggetto Strategy quando il backtest è effettivamente “a mercato”. I segnali di ingresso vengono generati solo se è “OUT” e i segnali di uscita vengono generati solo se è “LONG” o “SHORT”:

# mac.py

class MovingAverageCrossStrategy(Strategy):
    """
    Esegue una strategia base di Moving Average Crossover tra due
    medie mobile semplici, una breve e una lunga. Le finestre brevi / lunghe
    sono rispettivamente di 100/400 periodi.
    """
    def __init__(self, bars, events, short_window=100, long_window=400):
        """
        Initializza la strategia di Moving Average Cross.
        
        Parametri:
        bars - L'oggetto DataHandler object che fornisce le barre dei prezzi
        events - L'oggetto Event Queue.
        short_window - Il periodo per la media mobile breve.
        long_window - Il periodo per la media mobile lunga.
        """
        self.bars = bars
        self.symbol_list = self.bars.symbol_list
        self.events = events
        self.short_window = short_window
        self.long_window = long_window

        # Impostato a True se la strategia è a mercato
        self.bought = self._calculate_initial_bought()
Poiché la strategia inizia fuori dal mercato, impostiamo il valore iniziale “bought” su “OUT”, per ogni simbolo:
# mac.py

    def _calculate_initial_bought(self):
        """
        Aggiunge keys per ogni simbolo al dizionario bought e le
        imposta a 'OUT'.
        """
        bought = {}
        for s in self.symbol_list:
            bought[s] = 'OUT'
            return bought

Il fulcro della strategia è il metodo prepare_signals. Reagisce a un oggetto MarketEvent e per ogni simbolo di trading acquisisce gli prezzi di chiusura delle ultime N barre, dove N è uguale al periodo di ricerca più ampio.

Quindi si calcola le medie mobili semplici di breve e lungo periodo. La regola della strategia è entrare a mercato (andare long su un’azione) quando il valore della media mobile breve supera il valore della media mobile lunga. Al contrario, se il valore della media mobile lunga supera il valore della media mobile breve, alla strategia viene detto di uscire dal mercato.

Questa logica viene gestita posizionando un oggetto SignalEvent sulla coda degli eventi degli eventi in ciascuna delle rispettive situazioni e quindi aggiornando l’attributo “bought” (per ogni simbolo) in modo che sia rispettivamente “LONG” o “SHORT”. Poiché questa è una strategia solo long, non prenderemo in considerazione le posizioni “SHORT”:

# mac.py

    def calculate_signals(self, event):
        """
        Genera un nuovo set di segnali basato sull'incrocio della
        SMA di breve periodo con quella a lungo periodo che
        significa un'entrata long e viceversa per un'entrata short.

        Parametri
        event - Un oggetto MarketEvent.
        """
        if event.type == 'MARKET':
            for s in self.symbol_list:
                bars = self.bars.get_latest_bars_values(
                    s, "adj_close", N=self.long_window
                )
                bar_date = self.bars.get_latest_bar_datetime(s)
                if bars is not None and bars != []:
                    short_sma = np.mean(bars[-self.short_window:])
                    long_sma = np.mean(bars[-self.long_window:])
                    symbol = s
                    dt = datetime.datetime.utcnow()
                    sig_dir = ""
                    if short_sma > long_sma and self.bought[s] == "OUT":
                        print("LONG: %s" % bar_date)
                        sig_dir = 'LONG'

                        signal = SignalEvent(1, symbol, dt, sig_dir, 1.0)
                        self.events.put(signal)
                        self.bought[s] = 'LONG'
                    elif short_sma < long_sma and self.bought[s] == "LONG":
                        print("SHORT: %s" % bar_date)
                        sig_dir = 'EXIT'
                        signal = SignalEvent(1, symbol, dt, sig_dir, 1.0)
                        self.events.put(signal)
                        self.bought[s] = 'OUT'
Questo conclude l’implementazione dell’oggetto MovingAverageCrossStrategy. Il compito finale dell’intero sistema di backtest è implementare un metodo __main__ in mac.py per eseguire effettivamente il backtest.

Innanzitutto, bisogna assicursi di modificare il valore di csv_dir con percorso assoluto della directory dove si trovano i file CSV per i dati finanziari. Si dovrà anche scaricare il file CSV del titolo AAPL (da Yahoo Finance), che è fornito dal seguente link (dal 1 ° gennaio 1990 al 1 ° gennaio 2002), dato che questo è il titolo su cui testeremo la strategia:
https://query1.finance.yahoo.com/v7/finance/download/AAPL?period1=631152000&period2=1009843200&interval=1d&events=history&includeAdjustedClose=true

E’ quindi necessario inserire questo file nel percorso indicato dalla funzione principale in csv_dir. La funzione __main__ crea semplicemente un’istanza di un nuovo oggetto di backtest e quindi chiama il metodo simulate_trading per eseguire il backtest:
# mac.py

if __name__ == "__main__":
    csv_dir = '/path/to/your/csv/file' # DA MODIFICARE
    symbol_list = ['AAPL']
    initial_capital = 100000.0
    heartbeat = 0.0
    start_date = datetime.datetime(1990, 1, 1, 0, 0, 0)
    backtest = Backtest(csv_dir, symbol_list, initial_capital, heartbeat, start_date, 
                        HistoricCSVDataHandler, SimulatedExecutionHandler, Portfolio, 
                        MovingAverageCrossStrategy)
    backtest.simulate_trading()

Per eseguire il codice, bisogna avere aver già configurato un ambiente Python (come descritto nei precedenti articoli) e poi navigare nella directory in cui è memorizzato il codice.
Si dovrà semplicemente lanciare il seguente comando:

python mac.py
Lo script restituisce il seguente elenco (troncato a causa della stampa del conteggio delle barre!)
..
..
3029
3030
Creating summary stats...
Creating equity curve...
AAPL cash commission total returns equity_curve drawdown
datetime
2001-12-18 0 99211 13 99211 0 0.99211 0.025383
2001-12-19 0 99211 13 99211 0 0.99211 0.025383
2001-12-20 0 99211 13 99211 0 0.99211 0.025383
2001-12-21 0 99211 13 99211 0 0.99211 0.025383
2001-12-24 0 99211 13 99211 0 0.99211 0.025383
2001-12-26 0 99211 13 99211 0 0.99211 0.025383
2001-12-27 0 99211 13 99211 0 0.99211 0.025383
2001-12-28 0 99211 13 99211 0 0.99211 0.025383
2001-12-31 0 99211 13 99211 0 0.99211 0.025383
2001-12-31 0 99211 13 99211 0 0.99211 0.025383
[(’Total Return’, ’-0.79%’),
(’Sharpe Ratio’, ’-0.09’),
(’Max Drawdown’, ’2.56%’),
(’Drawdown Duration’, ’2312’)]
Signals: 10
Orders: 10
Fills: 10

La performance di questa strategia è visualizzata nella seguente figura:

SMA_performance_trading_algoritmico
E’ evidente che i rendimenti e lo Sharpe Ratio non sono stellari per le azioni AAPL su questo particolare set di indicatori tecnici!
Chiaramente abbiamo molto lavoro da fare nella prossima serie di strategie per trovare un sistema in grado di generare performance positive.

 

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