strategia mean-reverting cross-sectional con Backtrader

Strategia mean-reverting cross-sectional con Backtrader

In questo articolo descriviamo come implementare il backtest di strategia mean-reverting cross-sectional con Backtrader e ne testiamo le prestazioni per il trading algoritmico. La strategia è tratta dal libro di Ernest Chan Algorithmic Trading: Winning Strategies and Their Rationale.

Per approfondimenti sul framework open-source in Python è possibile leggere gli altri articoli relativi a Backtrader su Datatrading.

Tipicamente, una strategia mean-reverting cross-sectional è alimentata da un universo di titoli azionari, dove ogni azione ha i propri rendimenti relativi rispetto ai rendimenti medi dell’universo. Un titolo con un rendimento relativo positivo è venduto allo scoperto mentre si acquista un titolo con un rendimento relativo negativo. L’idea è che i titoli che ha avuto una performance inferiore o superiore a quella dell’universo ritornino verso la media dell’universo.

La strategia descritta nel libro di Chan è la seguente: ogni giorno, ad ogni azione \(i\) dell’universo  si assegna un peso \(w_i\) secondo la seguente formula:

\(w_i = -(r_i – r_m) / \sum_k | r_k – r_m |\)

Dove \(r_m\) è il rendimento medio dell’universo. Il peso \(w_i\) identifica la quantità di portafoglio che è long o short per lo specifico titolo \(i\). Come possiamo vedere dalla formula, il perso di un singolo titolo è maggiore quanto più i  suoi rendimenti sono lontani dalla media.

Raccolta dei dati

Per testare la strategia mean-reverting cross-sectional con Backtrader dobbiamo selezionare un universo di azioni. In questo caso usiamo l’S&P 500. Vogliamo scaricare e memorizzare su file i dati giornalieri di tutti i ticker che compongono l’S&P 500. In questo modo non dobbiamo scaricare nuovamente i dati ad ogni esecuzione del backtest. Iniziamo leggendo l’elenco dei ticker da Wikipedia e salvarli in un file spy/tickers.csv.

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

data = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
table = data[0]
tickers = table[1:]['Symbol'].tolist()
pd.Series(tickers).to_csv("spy/tickers.csv")
				
			

Ora che abbiamo l’elenco dei ticker, possiamo scaricare tutti i dati degli ultimi 5 anni. Usiamo concurrent.futures.ThreadPoolExecutorper accelerare il compito.

				
					
from concurrent import futures

end = datetime.now()
start = datetime(end.year - 5, end.month , end.day)
bad = []

def download(ticker):
    df = yf.download(ticker, start, end)
    df.to_csv(f"spy/{ticker}.csv")

with futures.ThreadPoolExecutor(50) as executor:
    res = executor.map(download, tickers)
				
			
Ora dovremmo avere tutti i nostri dati nella directory spy! Adesso possiamo iniziare a scrivere la strategia.

Strategia mean-reverting cross-sectional con Backtrader

Possiamo implementare la strategia completa utilizzando la formula precedente.
				
					

class CrossSectionalMR(bt.Strategy):
    def prenext(self):
        self.next()

    def next(self):
        # usiamo solo i dati che hanno valori nella giornata precederte
        available = list(filter(lambda d: len(d), self.datas))

        rets = np.zeros(len(available))
        for i, d in enumerate(available):
            # calcola i singolo rendimenti giornalieri
            rets[i] = (d.close[0] - d.close[-1]) / d.close[-1]

        # calcola i pesi tramite la formula
        market_ret = np.mean(rets)
        weights = -(rets - market_ret)
        weights = weights / np.sum(np.abs(weights))

        for i, d in enumerate(available):
            self.order_target_percent(d, target=weights[i])
				
			
Nota: Sottolineiamo che Backtrader chiama il metodo next() di una strategia solo quando ha un tick di prezzo da ogni feed di dati. Ciò significa che per impostazione predefinita la strategia non verrà eseguita se, ad esempio, una società nell’universo non ha ancora iniziato a fare trading pubblicamente. Possiamo aggirare questo problema chiamando next()prenext()quindi applicando la formula di calcolo del peso solo alle azioni per le quali disponiamo di dati.

Backtesting

Siamo pronti per il backtest! Vediamo come funziona questa strategia con un capitale iniziale di $ 1.000.000.
				
					
cerebro = bt.Cerebro(stdstats=False)
cerebro.broker.set_coc(True)

for ticker in tickers:
    data = bt.feeds.GenericCSVData(
        fromdate=start,
        todate=end,
        dataname=f"spy/{ticker}.csv",
        dtformat=('%Y-%m-%d'),
        openinterest=-1,
        nullvalue=0.0,
        plot=False
    )
    cerebro.adddata(data)

cerebro.broker.setcash(1_000_000)
cerebro.addobserver(bt.observers.Value)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.0)
cerebro.addanalyzer(bt.analyzers.Returns)
cerebro.addanalyzer(bt.analyzers.DrawDown)
cerebro.addstrategy(CrossSectionalMR)
results = cerebro.run()
				
			
				
					
print(f"Sharpe: {results[0].analyzers.sharperatio.get_analysis()['sharperatio']:.3f}")
print(f"Norm. Annual Return: {results[0].analyzers.returns.get_analysis()['rnorm100']:.2f}%")
print(f"Max Drawdown: {results[0].analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")
cerebro.plot()[0][0]
				
			
backtrader backtest di una strategia mean-reverting cross sectional

Conclusione

Come possiamo vedere l’algoritmo della strategia mean-reverting cross-sectional con Backtrader ha funzionato abbastanza bene! Aveva un Sharpe Ratio di 1,17 con un rendimento annuo normalizzato del 7,9%. Notiamo che i rendimenti non sono molto maggiori rispetto al buy&hold di SPY ma la volatilità è notevolmente ridotta. Dobbiamo però tenere in considerazione alcune cose:

  1. Il set di dati che stiamo utilizzando potrebbe avere un leggero bias di sopravvivenza in quanto non contiene società che erano presenti nell’S&P 500 5 anni fa e quelle che da allora sono state rimosse.

  2. Il backtest presuppone che calcoli i pesi alla chiusura del mercato e quindi sia in grado di negoziare all’esatto prezzo di chiusura del mercato. In realtà non possiamo calcolare i pesi con l’esatto prezzo di chiusura, ma ad un prezzo molto vicino.

  3. Poiché questo particolare algoritmo negozia contemporaneamente tutte le azioni dell’universo, l’investitore avrà bisogno di una grande quantità di capitale per abbinare accuratamente i pesi calcolati.

Codice completo

In questo articolo abbiamo descritto come eseguire il backtest di una strategia mean-reverting cross-sectional con Backtrader per il trading algoritmico. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/BackTrader

Gli altri articoli di questa serie

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono 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.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto
Scroll to Top