backtrader backtest di un portafoglio di ETF a leva

Backtest di un portafoglio di ETF con Backtrader

Nel precedere articolo abbiamo descritto come analizzato le prestazioni dell’ETF S&P 500 con leva 3x, UPRO, e dimostrato perché un portafoglio long di UPRO al 100% potrebbe non essere l’idea migliore. In questo articolo analizziamo la performance storica simulata di un altro ETF con leva 3x, il TMF, ed effettuiamo il backtest di un portafoglio di ETF con backtrader prevedendo un’allocazione azionaria/obbligazionaria di 60/40.

Simulazione di un ETF a leva

Per prima cosa importiamo le librerie di cui abbiamo bisogno.

				
					import pandas as pd
import yfinance as yf
import datetime
import backtrader as bt
import numpy as np

import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (10, 6) # (w, h)
				
			

Abbiamo anche bisogno della  funzione sim_leverage descritta nell’articolo precedente.

				
					
def sim_leverage(proxy, leverage=1, expense_ratio=0.0, initial_value=1.0):
    """
    Simula un ETF con leva in base al proxy, alla leva finanziaria e al rapporto di spesa.

    La variazione percentuale giornaliera viene calcolata prendendo la variazione percentuale 
    giornaliera del proxy, sottraendo il rapporto di spesa giornaliera, quindi moltiplicando 
    per la leva finanziaria.
    """
    pct_change = proxy.pct_change(1)
    pct_change = (pct_change - expense_ratio / 252) * leverage
    sim = (1 + pct_change).cumprod() * initial_value
    sim[0] = initial_value
    return sim
				
			

Per questo articolo usiamo due ETF con leva: UPRO, un ETF S&P 500 con leva 3x, e TMF , un indice obbligazionario US Treasury 20+ Year con leva 3x. Possiamo creare simulazioni sia per UPRO che per TMF usando i seguenti valori:

ETFExpense RatioProxyData di inizio simulazione
UPR0.92%VFINX08/31/1976
TMF1.09%VUSTX05/19/1986

Usiamo il 19/05/1986 come data di inizio perchè è la prima data dove abbiamo entrambe le serie dei proxy.

				
					

start = "1986-05-19"
end = "2020-01-01"

vfinx = yf.download("VFINX", start, end)["Adj Close"]
vustx = yf.download("VUSTX", start, end)["Adj Close"]

upro_sim = sim_leverage(vfinx, leverage=3, expense_ratio=0.0092).to_frame("close")
tmf_sim = sim_leverage(vustx, leverage=3, expense_ratio=0.0109).to_frame("close")
				
			

Strategia di Buy&Hold

Prima di esaminare una strategia multi-asset, vediamo come si comporta ciascuno degli asset con una semplice strategia buy-and-hold. Per testare le nostre strategie, usiamo Backtrader, un popolare frameword per il backtest in Python che  implementa anhce il trading  live.

Per poter usare i dati simultati con Backtrader, dobbiamo costruire le serie OHLCV quindi dobbiamo creare le colonne Open, High, Low e Volume. Per semplicità copiamo il prezzo di chiusura su tutte le colonne, poiché facciamo trading solo alla chiusura del mercato.

				
					
for column in ["open", "high", "low"]:
    upro_sim[column] = upro_sim["close"]
    tmf_sim[column] = tmf_sim["close"]

upro_sim["volume"] = 0
tmf_sim["volume"] = 0

upro_sim = bt.feeds.PandasData(dataname=upro_sim)
tmf_sim = bt.feeds.PandasData(dataname=tmf_sim)
vfinx = bt.feeds.YahooFinanceData(dataname="VFINX", fromdate=start, todate=end)
				
			

Scriviamo ora la strategia buy-and-hold:

				
					
class BuyAndHold(bt.Strategy):
    def next(self):
        if not self.getposition(self.data).size:
            self.order_target_percent(self.data, target=1.0)
				
			

Implementiamo anche una semplice funzione di supporto che esegue il backtest e restituisce parametri importanti. Usiamo lo Sharpe Ratio per valutare la performance delle strategie poiché è un buon modo per misurare i rendimenti adeguati al rischio.

				
					
def backtest(datas, strategy, plot=False, **kwargs):
    cerebro = bt.Cerebro()
    for data in datas:
        cerebro.adddata(data)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.Returns)
    cerebro.addanalyzer(bt.analyzers.DrawDown)
    cerebro.addstrategy(strategy, **kwargs)
    results = cerebro.run()
    if plot:
        cerebro.plot()
    return (results[0].analyzers.drawdown.get_analysis()['max']['drawdown'],
            results[0].analyzers.returns.get_analysis()['rnorm100'],
            results[0].analyzers.sharperatio.get_analysis()['sharperatio'])
				
			

Testiamo la strategia buy-and-hold usando come benchmark  il VFINX, l’ETF sull’S&P 500.

				
					
dd, cagr, sharpe = backtest([vfinx], BuyAndHold, plot=True)
print(f"Max Drawdown: {dd:.2f}%\nCAGR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
backtest di un portafoglio di ETF a leva
				
					Max Drawdown: 55.24%
CAGR: 10.15%
Sharpe: 0.683
				
			

Possiamo vedere che, come previsto, la strategia effettua un singolo acquisto, quindi detiene l’asset per gli anni rimanenti.

Eseguiamo ora la strategia buy-and-hold su UPRO.

				
					
dd, cagr, sharpe = backtest([upro_sim], BuyAndHold)
print(f"Max Drawdown: {dd:.2f}%\nCAGR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
				
					Max Drawdown: 97.11%
CAGR: 17.44%
Sharpe: 0.585
				
			

Questi numeri non corrispondono esattamente a quelli del precedente articolo a causa del periodo di tempo leggermente più breve. Tuttavia, abbiamo ancora un elevato drawdown massimo del 97%.

Infine, testiamo il buy-and-hold con TMF.

				
					
dd, cagr, sharpe = backtest([tmf_sim], BuyAndHold)
print(f"Max Drawdown: {dd:.2f}%\nCAGR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
				
					Max Drawdown: 50.27%
CAGR: 16.69%
Sharpe: 0.605
				
			

In termini di Sharpe Ratio, UPRO e TMF hanno entrambi sottoperformato l’ETF S&P 500. Vediamo cosa succede quando li mettiamo insieme!

Backtest di un portafoglio di ETF con backtrader

Il fondatore di Vanguard, Jack Bogle, ha sempre caldamente consigliato un portafoglio composto esclusivamente dal 60% di azioni statunitensi e dal 40% di obbligazioni. Usiamo la stessa logica per creare un portafoglio 60/40 UPRO/TMF, ribilanciandolo ogni 20 giorni di negoziazione. L’esatta percentuale di allocazione è un parametro, quindi possiamo modificarla a piacere.

				
					

class AssetAllocation(bt.Strategy):
    params = (
        ('equity', 0.6),
    )

    def __init__(self):
        self.UPRO = self.datas[0]
        self.TMF = self.datas[1]
        self.counter = 0

    def next(self):
        if self.counter % 20 == 0:
            self.order_target_percent(self.UPRO, target=self.params.equity)
            self.order_target_percent(self.TMF, target=(1 - self.params.equity))
        self.counter += 1
				
			

Testiamo questa strategia

				
					dd, cagr, sharpe = backtest([upro_sim, tmf_sim], AssetAllocation, plot=True, equity=0.6)
print(f"Max Drawdown: {dd:.2f}%\nCAGR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
portafoglio di ETF con backtrader
				
					Max Drawdown: 59.54%
CAGR: 19.76%
Sharpe: 0.675
				
			

Otteniamo uno Sharpe Ratio superiore al benchmark S&P 500. Vediamo se possiamo migliorarlo ulteriormente ottimizzando le allocazioni.

Ottimizzazione

Nota: quando si ottimizzano i parametri, bisogna fare attenzione alll’overfitting. Dal sito web di Backtrader: “C’è molta letteratura sull’ottimizzazione e sui pro e contro associati. Ma il consiglio punterà sempre nella stessa direzione: non ottimizzare eccessivamente. Se un’idea di trading non è valida, l’ottimizzazione potrebbe finire per produrre un risultato positivo che è valido solo per il set di dati sottoposto a backtesting”.

Eseguiamo il backtest per tutte le allocazioni di UPRO e TMF a intervalli del 5% e prendiamo nota di ciascun Sharpe  Ratio risultante. Backtrader ha la funzionalità di ottimizzazione dei parametri integrata, che richiede il multithreading.

				
					sharpes = {}
for perc_equity in range(0, 101, 5):
    sharpes[perc_equity] = backtest([upro_sim, tmf_sim], AssetAllocation, equity=(perc_equity / 100.0))[2]
				
			

L’operazione potrebbe richiedere un minuto o due. Una volta ottenuti i risultati, possiamo rappresentare graficamente il modo in cui l’allocazione del portafoglio influisce sullo Sharpe  Ratio e trovare l’allocazione ottimale.

Leveraged-60-40-ottimizzazione
				
					Max Sharpe of 0.743 at 40% UPRO
				
			

Per ottenere il miglior Sharpe Ratio nel backtest, la migliore allocazione UPRO/TMF è 55/45. Eseguiamo un altro backtest con questa allocazione.

				
					dd, cagr, sharpe = backtest([upro_sim, tmf_sim], AssetAllocation, plot=True, equity=0.4)
print(f"Max Drawdown: {dd:.2f}%\nCAGR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
Leveraged-stock-40
				
					Max Drawdown: 43.08%
CAGR: 20.13%
Sharpe: 0.743
				
			

Conclusione

Come possiamo vedere il portafoglio simulato 40/60  con UPRO/TMF ha più che raddoppiato i rendimenti annuali dell’ETF con S&P 500, producendo uno Sharpe  Ratio maggiore e un drawdown massimo inferiore. Nonostante i risAnche se andare long con un singolo ETF con leva 3x probabilmente non è una buona idea, una strategia multi-asset con leva potrebbe fornire guadagni significativi riducendo al contempo il rischio.

Codice completo

In questo articolo abbiamo descritto come eseguire il backtest di un portafoglio di ETF con Backtrader. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/BackTrader

Scroll to Top