strategia di ribilanciamento del portafoglio 60/40

Ribilanciamento del portafoglio con AlphaVantage in Backtrader

In questo articolo vediamo come eseguire una semplice strategia di ribilanciamento del portafoglio con AlphaVantage tramite un’implementazione in Backtrader. Per quelli di voi che non lo sanno, il portfolio 60/40 è quasi come il “Hello World” dei portafogli. Descrive il rapporto tra azioni e obbligazioni nel portafoglio. In altre parole, un rapporto del 60% di azioni e del 40% di obbligazioni.

Diversificazione e ribilanciamento del portafoglio

Un vecchio proverbio dice: “non mettere tutte le uova nello stesso paniere”. Questo è spesso un buon consiglio per l’investitore. Nel mondo della finanza, possiamo pensare che le uova siano i nostri investimenti e i panieri siamo le diverse classi di asset. Se un paniere si rompe (ad esempio le azioni), allora (si spera) un altro paniere sarà più forte (ad esempio le obbligazioni). L’idea è quella di selezionare una combinazione di asset non correlati, cioè che non salgono e scendono contemporaneamente.

Ribilanciare un portafoglio è un semplice esercizio per assicurarsi di non riempire un paniere con troppe uova. A intervalli regolari, verifichiamo i “pesi” degli asset all’interno del portafoglio e decidiamo se è necessario ridurre o aumentare la quantità di ciascun asset per bilanciare i panieri.

Il portafoglio 60/40

L’idea generale alla base della selezione di un portafoglio con un rapporto del 60% di azioni e del 40% di obbligazioni è che  questi asset sono tradizionalmente inversamente correlati. In altre parole quando il mercato azionario scende, i prezzi delle obbligazioni tendono a salire e fungono da copertura contro la perdita dell’azionario. Il portafoglio è più pesato verso le azioni poiché tendono ad apprezzarsi maggiormente nel lungo periodo.

Naturalmente, nulla è perfetto e stiamo iniziando a vedere alcuni analisti ipotizzare che la correlazione inversa tra azioni e obbligazioni sia pronta a morire. Pertanto, questo articolo non deve essere considerato come un consiglio o una base per una decisione di investimento.

Requisiti

Questo articolo si basa su alcuni articoli precedenti dove abbiamo descritto come usare AlphaVantage come fonte di dati. Quindi non descriviamo come effettuare il download e  l’importazione dei dati. Per approfondire questi temi si può leggere i seguenti questi articoli.

  1. Backtrader: Usare l’API di AlphaVantage
  2. Backtrader: Usare AlphaVantage nei datafeed

La chiave API

Come descritto negli articolo precedenti, per utilizzare questo codice dobbiamo procurarci una chiave API o modificare i datafeed.

La chiave API garantisce l’accesso illimitato ai dati di Alpha Vantage. Dobbiamo collegarci al seguente link e iscriverci.

Fonte:  https://www.alphavantage.co/support/#api-key

I dati “corretti”

Questo articolo usa i dati dei prezzi corretti da dividenti e split in modo poter facilmente simulare il reinvestimento dei dividendi. Per ulteriori informazioni sui dividendi e sui dati corretti, si può leggere questi articoli:

Ribilanciamento di portafoglio con backtrader

				
					import backtrader as bt
from datetime import datetime
import pandas as pd
import numpy as np
from alpha_vantage.timeseries import TimeSeries

# IMPORTANTE!
# ----------
# Registrare per un API su:
# https://www.alphavantage.co/support/#api-key
# e inserirla qui
Apikey = 'INSERT YOUR API KEY'


def adjust(date, close, adj_close, in_col, rounding=4):
    '''
    Se uso forex o Crypto - Cambia l'arrotondamento!
    '''
    try:
        factor = adj_close / close
        return round(in_col * factor, rounding)
    except ZeroDivisionError:
        print('WARNING: DIRTY DATA >> {} Close: {} | Adj Close {} | in_col: {}'.format(date, close, adj_close, in_col))
        return 0


def alpha_vantage_daily_adjusted(symbol_list, compact=False, debug=False, rounding=4, *args, **kwargs):
    '''
    Funzione per scaricare i dati di Alpha Vantage.

    Si prevede di restituire una lista nidificato contenente un dataframe pandas e il nome del feed.
    '''
    data_list = list()

    size = 'compact' if compact else 'full'

    for symbol in symbol_list:

        if debug:
            print('Downloading: {}, Size: {}'.format(symbol, size))

        # Inviare la chiare API e creare una sessione
        alpha_ts = TimeSeries(key=Apikey, output_format='pandas')

        # Download dei dati
        data, meta_data = alpha_ts.get_daily_adjusted(symbol=symbol, outputsize=size)
        # data, meta_data = alpha_ts.get_daily(symbol=symbol, outputsize=size)

        if debug:
            print(data)

        # Convertire l'indice in datetime.
        data.index = pd.to_datetime(data.index)
        data.sort_index(inplace=True)

        # Correggere il resto dei dati
        data['adj open'] = np.vectorize(adjust)(data.index.date, data['4. close'], data['5. adjusted close'],
                                                data['1. open'], rounding=rounding)
        data['adj high'] = np.vectorize(adjust)(data.index.date, data['4. close'], data['5. adjusted close'],
                                                data['2. high'], rounding=rounding)
        data['adj low'] = np.vectorize(adjust)(data.index.date, data['4. close'], data['5. adjusted close'],
                                               data['3. low'], rounding=rounding)

        # Estrazione delle colonne con cui vogliamo lavoare e le rinominiamo.
        data = data[['adj open', 'adj high', 'adj low', '5. adjusted close', '6. volume']]
        data.columns = ['Open', 'High', 'Low', 'Close', 'Volume']
        data['Datetime'] = data.index

        data_list.append((data, symbol))

    return data_list


class RebalanceStrategy(bt.Strategy):
    params = (('assets', list()),
              ('rebalance_months', [1, 6]),)  # Float: 1 == 100%

    def __init__(self):
        self.rebalance_dict = dict()
        for i, d in enumerate(self.datas):
            self.rebalance_dict[d] = dict()
            self.rebalance_dict[d]['rebalanced'] = False
            for asset in self.p.assets:
                if asset[0] == d._name:
                    self.rebalance_dict[d]['target_percent'] = asset[1]

    def next(self):

        for i, d in enumerate(self.datas):
            dt = d.datetime.datetime()
            dn = d._name
            pos = self.getposition(d).size

            if dt.month in self.p.rebalance_months and self.rebalance_dict[d]['rebalanced'] == False:
                print('{} Sending Order: {} | Month {} | Rebalanced: {} | Pos: {}'.format(dt, dn, dt.month,
                                                                                          self.rebalance_dict[d][
                                                                                              'rebalanced'], pos))
                self.order_target_percent(d, target=self.rebalance_dict[d]['target_percent'] / 100)
                self.rebalance_dict[d]['rebalanced'] = True

            # Reset
            if dt.month not in self.p.rebalance_months:
                self.rebalance_dict[d]['rebalanced'] = False

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Completed:
            print('{} >> Order Completed >> Stock: {},  Ref: {}, Size: {}, Price: {}'.format(

                date,
                order.data._name,
                order.ref,
                order.size,
                'NA' if not order.price else round(order.price, 5)
            ))

    def notify_trade(self, trade):
        date = self.data.datetime.datetime().date()
        if trade.isclosed:
            print('{} >> Notify Trade >> Stock: {}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                date,
                trade.data._name,
                trade.price,
                round(trade.pnl, 2),
                round(trade.pnlcomm, 2)))


startcash = 10000

# Crea un istanza dicerebro
cerebro = bt.Cerebro()

# Paramentri della strategia
strat_params = [
    ('SPY', 30),
    ('IWM', 30),
    ('TIP', 20),
    ('TLT', 20)
]

symbol_list = [x[0] for x in strat_params]

# Aggiungere la strategia
cerebro.addstrategy(RebalanceStrategy, assets=strat_params)

data_list = alpha_vantage_daily_adjusted(
    symbol_list,
    compact=False,
    debug=True)

for i in range(len(data_list)):
    data = bt.feeds.PandasData(
        dataname=data_list[i][0],  # Pandas DataFrame
        name=data_list[i][1],  # Symbol
        timeframe=bt.TimeFrame.Days,
        compression=1,
        fromdate=datetime(2014, 1, 1),
        todate=datetime(2020, 1, 1)
    )

    # Aggiungere i dati a Cerebro
    cerebro.adddata(data)

# Impostare il capitale iniziale
cerebro.broker.setcash(startcash)
cerebro.broker.set_checksubmit(False)

# Esecuzione del backtest
cerebro.run()

# Ottenere il valore finale del portafoglio
portvalue = cerebro.broker.getvalue()
pnl = portvalue - startcash

# Stampa dei risultati finali
print('Final Portfolio Value: ${}'.format(portvalue))
print('P/L: ${}'.format(pnl))

# Grafico dei risultati
cerebro.plot(style='candlestick')
				
			

Commento al codice

La strategia per il ribilanciamento del portafoglio con AlphaVantage è una parte relativamente piccola del codice complessivo, ma ci concentriamo su di essa poiché il download dei dati e la configurazione generale di Backtrader sono stati descritti in altri articoli. La strategia ribilancia ogni asset alla prima chiamata di  metodo next() quando il mese corrisponde ad uno dei mesi di “ribilanciamento” indicati. I mesi di ribilanciamento possono essere specificati tramite un parametro della strategia con 1 (gennaio) e 6 (giugno) come valori predefiniti.

checksubmit

L’unica parte della configurazione che vale la pena descriviere è checksubmit. Impostand questo parametro su False non controlliamo gli ordini per verificare abbiamo liquidità prima di inviarli. Dobbiamo prevedere questa logica poiché potremmo inviare ordini per aumentare il peso di un asset prima di diminuirne un altro.

init()

Nel metodo init(), eseguiamo un ciclo su ciascuno dei feed di dati e assegniamo a ciascuno i pesi ideali indicati. Aggiungiamo inoltre un semplice flag booleano che usiamo per monitorare se abbiamo ribilanciato ogni asset. L’abbinamento dei pesi a ciascun asset è effettuato confrontando il nome del feed di dati con il nome nella lista degli asset che viene passato alla strategia all’avvio del backtest.

				
					
# Paramentri della strategia
strat_params = [
    ('SPY', 30),
    ('IWM', 30),
    ('TIP', 20),
    ('TLT', 20)
]

symbol_list = [x[0] for x in strat_params]

# Aggiungere la strategia
cerebro.addstrategy(RebalanceStrategy, assets=strat_params)

				
			

Next()

Nel ciclo implementato all’interno del metodo next() esaminiamo ciascuno datafeed e controlliamo se il datetime più recente corrisponde uno dei mesi definiti il  ribilanciamento. In caso affermativo, inviamo un ordine di ribilanciamento e quindi aggiorniamo il rebalance_dict in modo da non ribilanciare di nuovo  in questo mese.

Calcolare quanto dobbiamo comprare o vendere per ribilanciare è facile. Possiamo semplicemente lasciare che Backtrader se ne occupi con la funzione integrata order_target_percent(). Questo è molto utile per il ribilanciamento. Esegue i calcoli di cui abbiamo bisogno e acquista o vende un asset per raggiungere il peso target. Per questo motivo abbiamo bisogno del parametro checksubmit descritto in precedenza. A causa delle modalità di esecuzione del ciclo su tutti i feed di dati, non abbiamo alcuna garanzia che il primo asset  analizzato sia un asset da vendere prima di aumentare il peso di un altro asset. Per questo motivo, vogliamo essere in grado di inviare un ordine per aumentare il peso di un asset prima di passare all’asset successivo.

Infine, se il mese corrente non corrisponde ad un mese di ribilanciamento, possiamo aggiornare  il rebalance_dict e reimpostare l’asset su False per il ribilanciamento.

Risultati

Eseguendo il codice, otteniamo un grafico simile al seguente. Notiamo che non tutti gli asset sono acquistati o venduti durante ogni ciclo di ribilanciamento. Potrebbero essere già vicini al peso target e non è necessario l’acquisto o la vendita. In questi casi non effettuiamo operazioni.

Backtrader-Ribilanciamento-Portafoglio-60-40-Risultati-Backtest

Codice completo

In questo articolo abbiamo descritto come eseguire una semplice strategia di ribilanciamento del portafoglio con AlphaVantage tramite un’implementazione in Backtrader. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/BackTrader

Torna in alto
Scroll to Top