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:
ETF | Expense Ratio | Proxy | Data di inizio simulazione |
---|---|---|---|
UPR | 0.92% | VFINX | 08/31/1976 |
TMF | 1.09% | VUSTX | 05/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}")
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}")
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.
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}")
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