Sviluppo di un BackTesting Vettoriale con Python e Pandas

Il backtesting è un  processo di ricerca che prevede di applicare l’idea di strategia di trading ai dati storici di uno o più sottostanti, al fine di verificare le performance della strategia nel  passato. In particolare, un backtest non fornisce alcuna garanzia circa le prestazioni future della strategia. Sono tuttavia una componente essenziale nel processo di ricerca della pipeline delle strategie, che consente di filtrare le strategie prima di essere inserite in produzione.

In questo articolo (e in quelli che seguono) voglio descrivere un semplice sistema di backtesting, orientato agli oggetti e scritto in Python. Questo sistema di base sarà principalmente un “sussidio didattico”, utilizzato per dimostrare i vari componenti di un sistema di backtesting. Man mano che procediamo attraverso gli articoli, verranno aggiunte funzionalità più sofisticate.

Panoramica di un Sistema di Backtesting

Il processo di progettazione di un robusto sistema di backtesting è estremamente difficile. Una efficace simulazione di tutte le componenti che influiscono sulle prestazioni di un sistema di trading algoritmico è molto impegnativa. La scarsa granularità dei dati, l’opacità del routing degli ordini all’interno di un broker, la latenza degli ordini e una miriade di altri fattori contribuiscono a modificare le prestazioni “reali” di una strategia rispetto alle prestazioni di backtesting.

Quando si implementa un sistema di backtesting si è tentati di voler costantemente “riscriverlo da zero” poiché sempre più fattori si rivelano cruciali nella valutazione delle prestazioni. Nessun sistema di backtesting si può dire completato, cioè è un sistema in continua evoluzione, e deve constantemente verificare che un numero sufficiente di fattori sia stato correttamente implementato nel sistema.

Tenendo presente questi vincoli, il sistema di backtesting descritto in questo articolo contiene delle semplificazioni. Durante l’esplorazione di aspetti avanzati (ottimizzazione del portafoglio, gestione del rischio, gestione dei costi di transazione), il sistema diventerà più solido.

Tipologie di Sistemi di Backtesting

Ci sono due principali tipologie di sistemi di backtest. Il primo è basato sulla ricerca, utilizzato principalmente nelle fasi iniziali, dove molte strategie saranno testate per selezionare quelle che meritano una valutazione più seria. Questi sistemi di backtesting di ricerca, chiamati  Explorers, sono spesso scritti in Python, R o MatLab poiché in questa fase la velocità e facilita di sviluppo del codice è più importante della velocità di esecuzione.

Il secondo tipo di sistema di backtest è basato sugli Event-based. Cioè, esegue il processo di backtest in un ciclo di esecuzione simile (se non identico) allo stesso sistema di esecuzione del trading. Modellerà realisticamente i dati di mercato e il processo di esecuzione degli ordini per fornire una valutazione più rigorosa di una strategia.

Questi ultimi sistemi sono spesso scritti in un linguaggio ad alte prestazioni come C ++ o Java, dove la velocità di esecuzione è essenziale. Per le strategie a bassa frequenza (anche se ancora intraday), Python è più che sufficiente per essere utilizzato anche in questo contesto.

Sistema di Backtesting Object-Oriented in Python

Vediamo ora la progettazione e l’implementazione di un ambiente di backtesting Explorer. Si vuole usare un paradigma di  programmazione orientata agli oggetti perchè permette di definire oggetti software in grado di interagire gli uni con gli altri attraverso lo scambio di messaggi.In particolare, questo approccio permette di:

  • specificare le interfacce di ciascun componente in anticipo, mentre le parti interne di ciascun componente possono essere modificate (o sostituite) durante l’evoluzione del progetto;
  • testare efficacemente come si comporta ciascun componente (tramite un unit test);
  • creare  nuovi componenti  sopra o in aggiunta agli altri, tramite l’ereditarietà e l’incaspulamento.

Il sistema è progettato per semplificare l’implementazione ed avere un ragionevole grado di flessibilità, a scapito della precisione. In particolare, questo backtester sarà in grado di gestire strategie che agiscono su un singolo strumento. Successivamente il backtester verrà modificato per gestire più strumenti.

Le componenti fondamentali di questo sistema sono le seguenti:

  • Strategia – è una classe che prevede in input un DataFrame pandas di barre, ovvero un elenco di dati Open-High-Low-Close-Volume (OHLCV) ad uno specifico timeframe. La strategia produrrà una lista di segnali, che consistono in un timestamp e un elemento dell’insieme {1,0, -1} che indica rispettivamente un segnale long, hold o short.
  • Portafoglio – La maggior parte del lavoro di backtesting avviene in questa classe. Riceve in input un insieme di segnali (come descritto sopra) e creerà una serie di posizioni, o ordini. Il compito delle Portfolio è di produrre una curva equity, incorporare i costi di transazione di base e tenere traccia delle operazioni.
    Prestazioni – prende un oggetto portfolio e produce una serie di statistiche sulle sue prestazioni. In particolare, produrrà caratteristiche di rischio / rendimento, metriche di trade / profit ed informazioni sui drawdown.

Come si può vedere, questo backtester non include alcun riferimento alla gestione del portafoglio o del  rischio, alla gestione dell’esecuzione (ad esempio non si gestisce i limit order ) né fornirà una sofisticata modellizzazione dei costi di transazione. Questo non è un grosso problema in questa fase. Ci consente di acquisire dimestichezza con il processo di creazione del di questo sistema e delle librerie Pandas / NumPy. Col tempo sarà poi ampliato con nuove componenti.

Implementazione

Vediamo ora l’implementazione di ciascuna delle componenti del sistema.

Strategia

In questa fase, è necessario prevede un oggetto sufficientemente generico da poter gestire diverse tipologie di strategie, come mean-reversion, momentum e volatilità. Le strategie che vogliamo gestire devono essere basate sulle serie temporali, cioè “price driven“. Un requisito iniziale per questo tipo di  backtester è che le classi di strategie derivate dovranno accettere in input un elenco di DataFrame OHLCV, di tick (prezzi trade-by-trade) o i  dati degli order-book. Si prevede un limite inferiore di frenquenza dei trader ad 1 secondo.

Inoltre la classe Strategia  deve produrre avvisi sui segnali. Ciò significa che ‘avvisa’ un’istanza Portfolio della possibilità di andare long / short o mantenere una posizione. Questa flessibilità ci consentirà di creare più “explorers” di strategie che forniscono una serie di segnali, che una classe di Portfolio più avanzata può accettare per determinare quali ordini possono essere effettivamente immessi a mercato.

L’interfaccia delle classi è sviluppata utilizzando la metodologia abstract base class . Una classe base astratta è un oggetto che non può essere istanziato e quindi è possibile creare solo classi derivate. Il codice Python è riportato di seguito in un file chiamato backtest.py. La classe Strategy richiede che qualsiasi classe figlia implementi il metodo generate_signals.

Al fine di prevenire la possibilità che la classe Strategy sia inizializzata direttamente (dato che è astratta) è necessario usare gli oggetti ABCMeta e abstractmethod del modulo abc. In  particolare si introduce  una proprietà alla classe, chiamata __metaclass__ che corrisponde a ABCMeta e si applica il  decorate al metodo generate_signals con il decorator abstractmethod.

            # backtest.py

from abc import ABCMeta, abstractmethod

class Strategy(object):
    """
    Strategy è una classe base astratta che fornisce un'interfaccia per
    tutte le strategie di trading successive (ereditate).

    L'obiettivo di un oggetto Strategy (derivato) è produrre un elenco di segnali,
    che ha la forma di un DataFrame pandas indicizzato di serie temporale.

    In questo caso è gestito solo un singolo simbolo / strumento.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_signals(self):
        """
        È necessaria un'implementazione per restituire il DataFrame dei simboli
        contenente i segnali per andare long, short o flat (1, -1 o 0)
        """
        raise NotImplementedError("Should implement generate_signals()!")
        

Sebbene l’interfaccia di cui sopra sia molto semplice, diventerà più complicata quando questa classe verrà ereditata da ogni specifica tipologia di strategia. In definitiva, l’obiettivo della classe Strategy, in questa fase, è fornire un elenco di segnali long / short / hold per ogni strumento e poi inviare l’elenco da inviare a un portfolio.

Portafoglio

La classe Portfolio contiene la maggior parte della logica di trading. Per questo sistema di backtesting, il portafoglio è incaricato di determinare il dimensionamento delle posizioni, l’analisi dei rischi, la gestione dei costi di transazione e la gestione delle esecuzioni (vale a dire gli ordini market-on-open, market-on-close). In una fase successiva queste attività verranno suddivise in componenti separati. Per il momento, sono inserite all’interno di in una sola classe.

Questa classe fa ampio uso dei funzionalità offerte dalla libreria Pandas e fornisce un ottimo esempio di come questa libreria permette di risparmiare una quantità enorme di tempo, in particolare per quanto riguarda la trasformazione dei dati storici in un formato “standard”. Per  principale vantaggio di Pandas e NumPy consiste di evitare di accedere a qualsiasi set di dati usando la sintassi for d in .... Questo perché NumPy (che è alla base di Pandas) ottimizza il loop tramite operazioni vettorializzate.

L’obiettivo della classe Portfolio è di produrre una sequenza di ordini e una curva equity, che saranno analizzati dalla classe Performance. Per raggiungere questo obiettivo, è necessario fornire un elenco di ‘segnali’ da un oggetto Strategia. Più tardi, questo sarà un gruppo di oggetti strategici.

La classe del Portfolio deve prevedere un logica per determinare quanto capitale possa essere utilizzato per uno specifico sottinsieme di segnali, prevedere la gestione dei costi di transazione e determinare quale tipo di ordine utilizzare. Devono essere previste logiche per utilizzare il set di dati forniti dalla Strategia (barre OHLCV) per determinare il prezzo di esecuzione di un ordine. Poiché i prezzi high/low di ogni barra sono sconosciuti a priori, è possibile utilizzare solo i prezzi di apertura e chiusura per effettuare il trade. In realtà è impossibile garantire che un ordine sarà eseguito esattamente ad uno specifico prezzo quando si utilizza un ordine market, quindi sarà sempre un’approssimazione della realtà.

Oltre ai vincoli sull’esecuzione degli ordini, questo backtester ignorerà tutti i concetti di margin/brokerage e presumerà che sia possibile andare long o short con qualsiasi strumento, senza vincoli di liquidità. Questa è chiaramente un’ipotesi molto irrealistica, ma è una funzionalitò che può essere implementata in una seconda fase.

Di seguito, il codice da aggiungere al nostro backtester.py

            # backtest.py

class Portfolio(object):
    """
    Una classe base astratta che rappresenta un portfolio di
    posizioni (inclusi strumenti e contanti), determinate
    sulla base di una serie di segnali forniti da una Strategy
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_positions(self):
        """
        Fornisce la logica per determinare come le posizioni del 
        portafoglio sono allocate sulla base dei segnali 
        previsionali e dei contanti disponibili
        """
        raise NotImplementedError("Should implement generate_positions()!")

    @abstractmethod
    def backtest_portfolio(self):
        """
        Fornisce la logica per generare gli ordini di trading
        e la successiva curva del patrimonio netto (ovvero la 
        crescita del patrimonio netto totale), come somma 
        di partecipazioni e contanti, e il periodo delle barre
        associato a questa curva in base al DataFrame delle "posizioni".

        Produce un oggetto portfolio che può essere esaminato da
        altre classi / funzioni.
        """
        raise NotImplementedError("Should implement backtest_portfolio()!")
        
A questo punto, dopo aver introdotto le classi astratte Strategy e Portfolio abstract, possiamo ora creare alcuni concrete classi derivate da queste due, in modo da implementare una strategia funzionante. Iniziano con il definire una classe RandomForecastStrategy, derivata da Strategy, che prevedere di produrre segnali semplicemente scegliendo a caso (random) long o short! Chiaramente questa strategia non può funzionare nel mercato, ma è utile per scopi dimostrativi. Si crea quindi un nuovo file, chiamato random_forecast.py, con il codice per implementa la logica random della strategia:
            # random_forecast.py

import numpy as np
import pandas as pd
import quandl   # Necessary for obtaining financial data easily

from backtest import Strategy, Portfolio

class RandomForecastingStrategy(Strategy):
    """
    Classe derivata da Strategy per produrre un insieme di segnali che
    sono long / short generati casualmente. Chiaramente una strategia non 
    corretta, ma perfettamente accettabile per dimostrare il
    infrastruttura di backtest!
    """    
    
    def __init__(self, symbol, bars):
    	"""Necessita del ticker del simbolo e il dataframe delle barre"""
        self.symbol = symbol
        self.bars = bars

    def generate_signals(self):
        """Creazione del DataFrame pandas dei segnali random."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = np.sign(np.random.randn(len(signals)))

        # I primi cinque elementi sono impostati a zero in modo da minimizzare
        # la generazione di errori NaN nella previsione.
        signals['signal'][0:5] = 0.0
        return signals
        

Ora che abbiamo una  strategia “concreta”, dobbiamo creare un’implementazione dell’oggetto Portfolio. Questo oggetto comprenderà la maggior parte del codice di backtesting. È progettato per creare due DataFram separati, il primo, chiamato positions, utilizzato per memorizzare la quantità detenuta di ogni strumento ad ogni specifica barra. Il secondo, portfolio, contiene in realtà il prezzo market di tutte le posizioni per ciascuna barra, nonché un conteggio del denaro contante, a partire da uno specifico capitale iniziale. Questo alla fine fornisce una curva equity su cui valutare la performance della strategia.

L’oggetto Portfolio, sebbene estremamente flessibile nella sua interfaccia, richiede scelte specifiche su come gestire i costi di transazione, gli ordini a mercato, ecc. In questo esempio di base ho considerato la possibilità di andare facilmente long/short su uno strumento, senza restrizioni o margine, di acquistare o vendere direttamente al prezzo di apertura della barra, senza costi di transazione (compresi slippage e commisioni) e di specificare direttamente la quantità di azioni da acquistare ad ogni operazione.

Di seguito il codice da aggiungere a random_forecast.py:

            # random_forecast.py

class MarketOnOpenPortfolio(Portfolio):
    """
    Eredita la classe Portfolio per creare un sistema che acquista 100 unità di
    uno specifico simbolo per un segnale long / short, utilizzando il prezzo 
    open di una barra.

    Inoltre, non ci sono costi di transazione e il denaro può essere immediatamente
    preso in prestito per vendita allo scoperto (nessuna registrazione di 
    margini o requisiti di interesse).

    Richiede:
    symbol - Un simbolo di una azione che costituisce la base del portafoglio.
    bars - Un DataFrame di barre per un simbolo.
    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):
    	"""
        Crea un DataFrame "positions" che semplicemente è long o short 
        per 100 pezzi di uno specfico simbolo basato sui segnali di 
        previsione di {1, 0, -1} dal DataFrame dei segnali.
        """
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)
        positions[self.symbol] = 100*self.signals['signal']
        return positions
                    
    def backtest_portfolio(self):
    	"""
        Costruisce un portafoglio a partire dal Dataframe delle posizioni 
        assumendo la capacità di negoziare all'esatto prezzo open di ogni 
        barra (un'ipotesi irrealistica!).

        Calcola il totale della liquidità e delle partecipazioni (prezzo 
        di mercato di ogni posizione per barra), al fine di generare una 
        curva di equity ('totale') e una serie di rendimenti basati sulle
        barre ('ritorni').

        Restituisce l'oggetto portfolio da utilizzare altrove.
        """

        # Costruzione di un DataFrame 'portfolio' che usa lo stesso indice
        # delle "posizioni" e con una serie di "ordini di trading" 
        # nell'oggetto 'pos_diff', assumendo prezzi open.
        portfolio = self.positions*self.bars['Open']
        pos_diff = self.positions.diff()

        # Crea le serie "holding" e "cash" scorrendo il dataframe 
        # delle operazioni e aggiungendo / sottraendo la relativa quantità di
        # ogni colonna
        portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
        portfolio['cash'] = self.initial_capital - 
                          (pos_diff*self.bars['Open']).sum(axis=1).cumsum()

        # Finalizza i rendimenti totali e basati su barre in base al "contante"
        # e dati sulle "partecipazioni" per il portafoglio        
        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio
        

Questo di permette di generare una curva equity del sistema.

Lo step finale prevede di collegare tutto insieme con una funzione __main__:

            if __name__ == "__main__":
    # Ottenere le barre giornaliere di SPY (ETF che generalmente
    # segue l'S&P500) da Quandl (richiede 'pip install Quandl'
    # sulla riga di comando)
    symbol = 'SPY'
    bars = quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")

    # Crea un insieme di segnali randome per SPY
    rfs = RandomForecastingStrategy(symbol, bars)
    signals = rfs.generate_signals()

    # Crea un portfolio di SPY
    portfolio = MarketOnOpenPortfolio(symbol, bars, signals, 
                                      initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    print(returns.tail(10))
        

The output dello script è riportato di seguito. Questo output dipende ovviamente dal range di tempo considerato e il generatore random utilizzato:

In questo caso la strategia ha perso denaro, il che non sorprende vista la natura stocastica del generatore di segnali! Il  passo successivo consiste nel creare un oggetto Performance che accetta un’istanza Portfolio e fornisce un report delle metriche sul rendimento su cui basare le valutazioni su come filtrare la strategia.

Possiamo anche migliorare l’oggetto portfolio per avere una gestione più realistica dei costi di transazione (come le commissioni di Interactive Brokers e lo slippage). Possiamo anche includere direttamente una logica ‘più realistica’ in una strategia, che (si spera) produrrà risultati migliori.

Nei prossimi articoli esploreremo questi concetti in modo più approfondito.

 

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