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 is an abstract base class providing an interface for
    all subsequent (inherited) trading strategies.

    The goal of a (derived) Strategy object is to output a list of signals,
    which has the form of a time series indexed pandas DataFrame.

    In this instance only a single symbol/instrument is supported."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_signals(self):
        """An implementation is required to return the DataFrame of symbols
        containing the signals to go long, short or hold (1, -1 or 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):
    """An abstract base class representing a portfolio of 
    positions (including both instruments and cash), determined
    on the basis of a set of signals provided by a Strategy."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_positions(self):
        """Provides the logic to determine how the portfolio 
        positions are allocated on the basis of forecasting
        signals and available cash."""
        raise NotImplementedError("Should implement generate_positions()!")

    @abstractmethod
    def backtest_portfolio(self):
        """Provides the logic to generate the trading orders
        and subsequent equity curve (i.e. growth of total equity),
        as a sum of holdings and cash, and the bar-period returns
        associated with this curve based on the 'positions' DataFrame.

        Produces a portfolio object that can be examined by 
        other classes/functions."""
        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):
    """Derives from Strategy to produce a set of signals that
    are randomly generated long/shorts. Clearly a nonsensical
    strategy, but perfectly acceptable for demonstrating the
    backtesting infrastructure!"""    
    
    def __init__(self, symbol, bars):
    	"""Requires the symbol ticker and the pandas DataFrame of bars"""
        self.symbol = symbol
        self.bars = bars

    def generate_signals(self):
        """Creates a pandas DataFrame of random signals."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = np.sign(np.random.randn(len(signals)))

        # The first five elements are set to zero in order to minimise
        # upstream NaN errors in the forecaster.
        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):
    """Inherits Portfolio to create a system that purchases 100 units of 
    a particular symbol upon a long/short signal, assuming the market 
    open price of a bar.

    In addition, there are zero transaction costs and cash can be immediately 
    borrowed for shorting (no margin posting or interest requirements). 

    Requires:
    symbol - A stock symbol which forms the basis of the portfolio.
    bars - A DataFrame of bars for a symbol set.
    signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
    initial_capital - The amount in cash at the start of the portfolio."""

    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):
    	"""Creates a 'positions' DataFrame that simply longs or shorts
    	100 of the particular symbol based on the forecast signals of
    	{1, 0, -1} from the signals DataFrame."""
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)
        positions[self.symbol] = 100*self.signals['signal']
        return positions
                    
    def backtest_portfolio(self):
    	"""Constructs a portfolio from the positions DataFrame by 
    	assuming the ability to trade at the precise market open price
    	of each bar (an unrealistic assumption!). 

    	Calculates the total of cash and the holdings (market price of
    	each position per bar), in order to generate an equity curve
    	('total') and a set of bar-based returns ('returns').

    	Returns the portfolio object to be used elsewhere."""

    	# Construct the portfolio DataFrame to use the same index
    	# as 'positions' and with a set of 'trading orders' in the
    	# 'pos_diff' object, assuming market open prices.
        portfolio = self.positions*self.bars['Open']
        pos_diff = self.positions.diff()

        # Create the 'holdings' and 'cash' series by running through
        # the trades and adding/subtracting the relevant quantity from
        # each column
        portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
        portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum()

        # Finalise the total and bar-based returns based on the 'cash'
        # and 'holdings' figures for the portfolio
        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__":
    # Obtain daily bars of SPY (ETF that generally
    # follows the S&P500) from Quandl (requires 'pip install Quandl'
    # on the command line)
    symbol = 'SPY'
    bars = quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")

    # Create a set of random forecasting signals for SPY
    rfs = RandomForecastingStrategy(symbol, bars)
    signals = rfs.generate_signals()

    # Create a portfolio of 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.

Recommended Posts