portfolio-ETF-python-trading-algoritmico

Ribilanciamento Mensile di ETF a pesi iniziali fissi con DataTrader

Sommario

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Se è la prima volta che atterri su DataTrading, BENVENUTO!

Lascia che mi presenti. Sono Gianluca, ingegnere, trekker e appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

TUTORIAL

Molti gestori istituzionali sono vincolati da obblighi contrattuali ad investire in strategie long-only con leva finanziaria nulla o minima. Questo causa che le loro strategie sono spesso altamente correlate al “mercato” (di solito l’indice S & P500). Sebbene questa correlazione no può essere ridotta al minimo senza applicare una copertura short del mercato, può essere attenuata investendo in ETF non basati su azioni.

Queste strategie prevedono una frequenza di ribilanciamento settimanale o mensile, oppure a volte anche annuale. Quindi differiscono sostanzialmente da una classica strategia quantitativa di arbitraggio statistico intraday, ma sono comunque adatte ad un approccio completamente sistematico.

Il framework open-source per il backtesting e live trading di DataTrading, DataTrader, prevede il supporto nativo per il ribilanciamento del portafoglio. In particolare è stata implementata la logica di ribilanciamento ed incorporata nelle classi Strategy e  PositionSizer.

Questo articolo descrive una strategia di base per un mix di ETF azioni / obbligazioni con pesi fissi, al fine di dimostrare questa funzionalità di DataTrader. La strategia in sé non è nuova – è un esempio del classico mix di azioni / obbligazioni “60/40” – ma il meccanismo di ribilanciamento all’interno di DataTrader consente una flessibilità significativamente maggiore. Può fornire  un buon punto di partenza per provare alcuni interessanti mix di ETF long-only.

La strategia di trading

La strategia prevede il trading di un paio di Exchange Traded Fund (ETF), che replicano/seguono fedelmente le performance delle azioni statunitensi (SPY) e delle obbligazioni investment-grade (AGG) statunitensi:

  • SPY – SPDR S & P500 ETF
  • AGG – ETF iShares Core US Aggregate Bond

La strategia prevede semplicemente di aquistare (posizione long) entrambi gli ETF alla chiusura del primo giorno di contrattazione utile dopo l’inizio di ogni mese, in modo tale che il 60% del capitale iniziale sia investito in SPY e il restante 40% in AGG. Ad ogni successivo giorno di chiusura del mese il portafoglio viene completamente liquidato e ribilanciato per garantire che il 60% dell’attuale capitale  sia investito in SPY e il restante 40% in AGG.

I Dati

Per attuare questa strategia è necessario disporre di dati sui prezzi OHLCV per il periodo previsto da questo backtest. In particolare è necessario scaricare quanto segue:

  • SPY – Per il periodo dal 1 novembre 2006 al 1 gennaio 2017 (link qui)
  • AGG – Per il periodo dal 1 novembre 2006 al 1 gennaio 2017 (link qui)

Se si desidera replicare questo backtest è sufficiente scaricare e copiare questi dati nella directory specificata nel file di configurazione di DataTrader

Implementazione Python con DataTrader

In questa sezione si descrivono i frammenti di codice più significativi, mentre il codice completo è riportato in fondo all’articolo. Questa strategia è in realtà un esempio completo del codice base di DataTrader, che può essere eseguito in una nuova installazione di DataTrader, supponendo che i dati storici siano ottenuti separatamente.

Sono necessari due nuovi componenti per far funzionare questa logica di ribilanciamento mensile. La prima è una sottoclasse della classe base Strategy, vale a dire MonthlyLiquidateRebalanceStrategy. La seconda è una sottoclasse della classe base PositionSizer, vale a dire LiquidateRebalancePositionSizer.

Alcuni nuovi metodi sono stati aggiunti alla classe MonthlyLiquidateRebalanceStrategy. Il primo è _end_of_month(...), dove si usa semplicemente il modulo calendar di Python insieme al metodomonthrange se la data passata è un giorno di fine mese:

              
    def _end_of_month(self, cur_time):
        """
        Determina se il giorno corrente è alla fine del mese.
        """
        cur_day = cur_time.day
        end_day = calendar.monthrange(cur_time.year, cur_time.month)[1]
        return cur_day == end_day
        

Il secondo metodo è _create_invested_list, che crea semplicemente un dizionario con tutti i ticker come chiavi e il booleano False come valori. Questo dizionario tickers_invested viene utilizzato per “pulizia”: permette di verificare se un asset è stato acquistato quando si esegue la logica di trading sequenziale.

Questo è necessario perché alla prima esecuzione del codice non è necessaria la liquidazione dell’intero portafoglio dato che non c’è nulla da liquidare!

            
    def _create_invested_list(self):
        """
        Crea un dizionario con ogni ticker come chiave, con un valore
        booleano a seconda che il ticker sia stato ancora "investito".
        Ciò è necessario per evitare di inviare un segnale di
        liquidazione sulla prima allocazione.
        """
        tickers_invested = {ticker: False for ticker in self.tickers}
        return tickers_invested
        

La logica di base è incapsulata nel metodo calculate_signals. Questo codice controlla se l’ultima barra OHLCV acquisita è relativa ad un giorno di fine mese, quindi determina se il portafoglio contiene posizioni aperte e, in tal caso, si deve liquidarlo completamente prima del ribilanciamento.

Indipendentemente da ciò, invia semplicemente un segnale long (“BOT”) alla coda degli eventi e quindi aggiorna il dizionario tickers_invested per mostrare che questo ticker è stato acquistato almeno una volta:

            
    def calculate_signals(self, event):
        """
        Per uno specifico BarEvent ricevuto, determina se è la fine del
        mese (per quella barra) e genera un segnale di liquidazione,
        oltre a un segnale di acquisto, per ogni ticker.
        """
        if (
            event.type in [EventType.BAR, EventType.TICK] and
            self._end_of_month(event.time)
        ):
            ticker = event.ticker
            if self.tickers_invested[ticker]:
                liquidate_signal = SignalEvent(ticker, "EXIT")
                self.events_queue.put(liquidate_signal)
            long_signal = SignalEvent(ticker, "BOT")
            self.events_queue.put(long_signal)
            self.tickers_invested[ticker] = True
        

Questo conclude il codice per il MonthlyLiquidateRebalanceStrategyIl prossimo oggetto è la classe LiquidateRebalancePositionSizer.

Nell’inizializzazione dell’oggetto è necessario passare un dizionario contenente i pesi iniziali dei ticker:

            
def __init__(self, ticker_weights):
    self.ticker_weights = ticker_weights
        

Per questa strategia il dizionario ticker_weights ha la seguente struttura:

            ticker_weights = {
    "SPY": 0.6,
    "AGG": 0.4
}
        

Da notare che questo dizionario può essere facilmente modificato in modo da prevedere qualsiasi portafoglio iniziale di ETF / azioni con diversi pesi. In questa fase, sebbene non sia un requisito fondamentale, il codice è impostato per gestire le allocazioni solo quando la somma dei pesi è pari a 1.0. Una somma superiore a 1.0 indicherebbe l’uso della leva finanziaria / margine, che non è attualmente gestito da DataTrader.

Il codice principale della classe  LiquidateRebalancePositionSizer è all’interno del metodo size_order. Inizialmente si verifica se l’ordine ricevuto è di tipo “EXIT” (liquidazione) o di tipo long “BOT”. Questo determina le modifiche da prevedere nell’ordine.

Se si ha un segnale di liquidazione, viene determinata la quantità corrente e viene creato un segnale opposto per portare a zero la quantità della posizione. Se invece è un segnale long per acquistare azioni, è necessario determinare il prezzo corrente dell’asset. Questo è implementato verificando il valore di portfolio.price_handler.tickers[ticker]["adj_close"].

Una volta determinato il prezzo corrente dell’attività, è necessario verificare l’intero patrimonio netto del portafoglio. Con questi due valori è possibile calcolare la nuova percentuale di allocazione per uno specifico asset moltiplicando l’intero patrimonio netto per il peso proporzionale del bene. Questo viene infine convertito in un valore intero delle azioni da acquistare.

Da notare che, per evitare errori di arrotondamento, il prezzo di mercato e il valore del patrimonio netto devono essere divisi per PriceParser.PRICE_MULTIPLIER. Questa funzionalità è stata descritta in un precedente articolo.

                
    def size_order(self, portfolio, initial_order):
        """
        Dimensionare l'ordine in modo da riflettere la percentuale
        in dollari dell'attuale dimensione del conto azionario
        in base a pesi pre-specificati dei ticket.
        """
        ticker = initial_order.ticker
        if initial_order.action == "EXIT":
            # Ottenere la quantità corrente e la liquida
            cur_quantity = portfolio.positions[ticker].quantity
            if cur_quantity > 0:
                initial_order.action = "SLD"
                initial_order.quantity = cur_quantity
            else:
                initial_order.action = "BOT"
                initial_order.quantity = cur_quantity
        else:
            weight = self.ticker_weights[ticker]

            # Determina il valore totale del portafoglio, calcola il peso
            # in dollari e determina la quantità di azioni da acquistare
            price = portfolio.price_handler.tickers[ticker]["adj_close"]
            price = PriceParser.display(price)
            equity = PriceParser.display(portfolio.equity)
            dollar_weight = weight * equity
            weighted_quantity = int(floor(dollar_weight / price))
            initial_order.quantity = weighted_quantity
        return initial_order
        

Il componente finale del codice è incapsulato nel file monthly_liquidate_rebalance_backtest.py. Il codice completo è riportato alla fine di questo articolo. Non c’è nulla di “particolare” in questo file rispetto a qualsiasi altra configurazione di backtest oltre alle specifiche del dizionario ticker_weights dei percentuali delle azioni in portafoglio.

Infine, possiamo eseguire la strategia scaricando i dati nella  corretta directory e digitando il seguente comando nel terminale:

               $ python monthly_liquidate_rebalance_backtest.py --tickers=SPY,AGG
        

Risultati della Strategia

Di seguito si riporta le statistiche prodotte dal backtest di questa strategia:

datatrader-60-40-tearsheet-01

Il benchmark è costituito da un portafoglio buy-and-hold (cioè nessun ribilanciamento mensile) formato esclusivamente dall’ETF SPY.

Nonostante il backtest fornisca uno Sharpe Ratio di 0,3, simile al 0,32 del benchmark, presenta però un CAGR inferiore del 3,31% rispetto al 4,59%.

La sottoperformance rispetto al benchmark è dovuta a due fattori. Prima di tutto la strategia effettua una liquidazione completa e riacquisto alla fine di ogni mese generando molti costi di transazione. Inoltre, il fondo obbligazionario AGG, pur attenuando il colpo nel 2008, ha sminuito la performance di SPY negli ultimi cinque anni.

Ciò motiva due strade di ricerca.

Il primo consiste nel ridurre al minimo i costi di transazione evitando una liquidazione completa e riequilibrando solo quando necessario. Questo potrebbe essere impostato come una forma di soglia per cui se la divisione 60/40 si discosta troppo da questa proporzione è necessario effettuare un riequilibrio. Ciò è in contrasto con un riequilibrio effettuato ogni mese indipendentemente dall’attuale proporzionalità.

La seconda prevede l’adeguamento del mix di ETF per aumentare lo Sharpe Ratio. Esiste un vasto numero di ETF là fuori, con molti portafogli che battono il benchmark a posteriori. Il nostro compito è ovviamente decidere come costruire a priori tali portafogli!

Prossimi Passi

L’attuale implementazione di DataTrader supporta solo il ribilanciamento di portafogli long-only con pesi iniziali fissi. Pertanto, due miglioramenti sono necessari in modo da garantire la gestione di portafogli con margine e la gestione di aggiustamenti dinamici dei pesi del portafoglio.

Inoltre DataTrader dovrà supportare il ribilanciamento ad altre frequenze, comprese quelle settimanali e annuali. Il ribilanciamento giornaliero è più difficile da supportare in quanto implica l’accesso ai dati di mercato infragiornalieri. Tuttavia è una funzionalità da prevedere.

Nonostante l’attuale mancanza di ribilanciamento settimanale o annuale, il riequilibrio mensile è estremamente flessibile. Consentirà la replica di molte strategie long-only attualmente in uso da parte dei grandi asset manager, nell’ipotesi di disponibilità di dati.

Molti post futuri saranno dedicati a tali strategie e, come in questo post, conterranno tutti il ​​codice completo necessario per replicare le strategie.

Codice completo

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

				
					# Monthly_liquidate_rebalance_strategy.py

import calendar

from datatrader.strategy.base import AbstractStrategy
from datatrader.event import SignalEvent, EventType

class MonthlyLiquidateRebalanceStrategy(AbstractStrategy):
    """
    Una strategia generica che consente il ribilanciamento mensile di
    una serie di ticker, tramite la piena liquidazione e la pesatura
    in dollari delle nuove posizioni.

    Per funzionare correttamente deve essere utilizzato insieme
    all'oggetto LiquidateRebalancePositionSizer.
    """
    def __init__(self, tickers, events_queue):
        self.tickers = tickers
        self.events_queue = events_queue
        self.tickers_invested = self._create_invested_list()

    def _end_of_month(self, cur_time):
        """
        Determina se il giorno corrente è alla fine del mese.
        """
        cur_day = cur_time.day
        end_day = calendar.monthrange(cur_time.year, cur_time.month)[1]
        return cur_day == end_day

    def _create_invested_list(self):
        """
        Crea un dizionario con ogni ticker come chiave, con un valore
        booleano a seconda che il ticker sia stato ancora "investito".
        Ciò è necessario per evitare di inviare un segnale di
        liquidazione sulla prima allocazione.
        """
        tickers_invested = {ticker: False for ticker in self.tickers}
        return tickers_invested

    def calculate_signals(self, event):
        """
        Per uno specifico BarEvent ricevuto, determina se è la fine del
        mese (per quella barra) e genera un segnale di liquidazione,
        oltre a un segnale di acquisto, per ogni ticker.
        """
        if (
            event.type in [EventType.BAR, EventType.TICK] and
            self._end_of_month(event.time)
        ):
            ticker = event.ticker
            if self.tickers_invested[ticker]:
                liquidate_signal = SignalEvent(ticker, "EXIT")
                self.events_queue.put(liquidate_signal)
            long_signal = SignalEvent(ticker, "BOT")
            self.events_queue.put(long_signal)
            self.tickers_invested[ticker] = True
				
			
				
					# Monthly_liquidate_rebalance_backtest.py

import datetime

from datatrader import settings
from datatrader.position_sizer.rebalance import LiquidateRebalancePositionSizer
from datatrader.compat import queue
from datatrader.trading_session import TradingSession

from .strategies.monthly_liquidate_rebalance_strategy import MonthlyLiquidateRebalanceStrategy


def run(config, testing, tickers, filename):
    # Backtest information
    title = [
        'Portafoglio di 60%/40% SPY/AGG con Ribilanciamento Mensile'
    ]
    initial_equity = 500000.0
    start_date = datetime.datetime(2006, 11, 1)
    end_date = datetime.datetime(2016, 10, 12)

    # Usa la strategia Monthly Liquidate And Rebalance
    events_queue = queue.Queue()
    strategy = MonthlyLiquidateRebalanceStrategy(
        tickers, events_queue
    )

    # Usa il sizer delle posizione di liquidazioni e ribilanciamento
    # con pesi dei ticker predefiniti
    ticker_weights = {
        "SPY": 0.6,
        "AGG": 0.4,
    }
    position_sizer = LiquidateRebalancePositionSizer(
        ticker_weights
    )

    # Setup del backtest
    backtest = TradingSession(
        config, strategy, tickers,
        initial_equity, start_date, end_date,
        events_queue, position_sizer=position_sizer,
        title=title, benchmark=tickers[0],
    )
    results = backtest.start_trading(testing=testing)
    return results


if __name__ == "__main__":
    # Dati di configurazione
    testing = False
    config = settings.from_file(
        settings.DEFAULT_CONFIG_FILENAME, testing
    )
    tickers = ["SPY", "AGG"]
    filename = None
    run(config, testing, tickers, filename)
				
			
				
					# rebalance.py

from math import floor

from .base import AbstractPositionSizer
from datatrader.price_parser import PriceParser


class LiquidateRebalancePositionSizer(AbstractPositionSizer):
    """
    Effettua una periodica liquidazione completa e ribilanciamento
    del Portafoglio.
    Ciò si ottiene determinando se l'ordine è di tipo "EXIT" o
    "BOT / SLD".

    Nel primo caso, viene determinata la quantità corrente di
    azioni nel ticker e quindi BOT o SLD per portare a zero
    la posizione.
    In quest'ultimo caso, l'attuale quantità di azioni da
    ottenere è determinata da pesi prespecificati e rettificata
    per riflettere il patrimonio netto di conto corrente.
    """
    def __init__(self, ticker_weights):
        self.ticker_weights = ticker_weights

    def size_order(self, portfolio, initial_order):
        """
        Dimensionare l'ordine in modo da riflettere la percentuale
        in dollari dell'attuale dimensione del conto azionario
        in base a pesi ticker pre-specificati.
        """
        ticker = initial_order.ticker
        if initial_order.action == "EXIT":
            # Ottenere la quantità corrente e la liquida
            cur_quantity = portfolio.positions[ticker].quantity
            if cur_quantity > 0:
                initial_order.action = "SLD"
                initial_order.quantity = cur_quantity
            else:
                initial_order.action = "BOT"
                initial_order.quantity = cur_quantity
        else:
            weight = self.ticker_weights[ticker]

            # Determina il valore totale del portafoglio, calcola il peso in
            # dollari e infine determina la quantità intera di azioni da acquistare
            price = portfolio.price_handler.tickers[ticker]["adj_close"]
            price = PriceParser.display(price)
            equity = PriceParser.display(portfolio.equity)
            dollar_weight = weight * equity
            weighted_quantity = int(floor(dollar_weight / price))
            initial_order.quantity = weighted_quantity
        return initial_order
				
			
Torna su