Migliorare una strategia mean-reverting con backtrader

Migliorare una strategia mean-reverting con backtrader

In questo articolo vediamo come migliorare una strategia mean-reverting con Backtrader in modo da poter iniziare a fare trading algoritmico dal vivo. Nel precedente articolo abbiamo descritto come implementare questa strategia, tratta dal libro Algorithmic Trading: Winning Strategies and Their Rationale di Ernest Chan.

La strategia

Usiamo lo stesso set di dati S&P 500 che abbiamo creato nel precedente articolo. Iniziamo con il caricare i dati dai file CSV.

				
					from datetime import datetime
import pandas as pd
import backtrader as bt
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (10, 6) # (w, h)
plt.ioff()

tickers = pd.read_csv('spy/tickers.csv', header=None)[1].tolist()
start = datetime(2015, 1, 1)
end = datetime(2020, 1, 1)

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

Dobbiamo quindi aggiungere una funzione helper che esegue il backtest e restituisca le metriche importanti.

				
					def backtest(datas, strategy, plot=None, **kwargs):
    cerebro = bt.Cerebro(stdstats=False)
    cerebro.broker.set_coc(True)
    cerebro.broker.setcash(1000000)
    for data in datas:
        cerebro.adddata(data)
    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(strategy, **kwargs)
    results = cerebro.run()
    if plot:
        cerebro.plot(iplot=False)[0][0]
    return (results[0].analyzers.drawdown.get_analysis()['max']['drawdown'],
            results[0].analyzers.returns.get_analysis()['rnorm100'],
            results[0].analyzers.sharperatio.get_analysis()['sharperatio'])
				
			

Possiamo ora applicare alcuni miglioramenti.

Migliorare una strategia mean-reverting con backtrader

 

Numero di posizioni

Come accennato nel precedente articolo, uno dei limiti della strategia era il numero di posizioni aperte contemporaneamente. Se alla strategia fosse assegnato un universo di 500 azioni, le negozierebbe tutte. In questo modo non solo si hanno elevate commissioni, ma è difficile eguagliare i pesi calcolati dalla strategia se non abbiamo un capitale molto elevato. La soluzione a questi limiti è abbastanza semplice. Mentre calcoliamo i pesi possiamo semplicemente scegliere gli n titoli con il peso assoluto maggiore.

Di seguito implementiamo questa logica.

				
					
def max_n(array, n):
    return np.argpartition(array, -n)[-n:]


class CrossSectionalMR(bt.Strategy):
    params = (
        ('num_positions', 100),
    )

    def __init__(self):
        self.inds = {}
        for d in self.datas:
            self.inds[d] = {}
            self.inds[d]["pct"] = bt.indicators.PercentChange(d.close, period=1)

    def prenext(self):
        self.next()

    def next(self):
        available = list(filter(lambda d: len(d), self.datas))  # usa solo i dati disponibili nella giornata precedente
        rets = np.zeros(len(available))
        for i, d in enumerate(available):
            rets[i] = self.inds[d]['pct'][0]

        market_ret = np.mean(rets)
        weights = -(rets - market_ret)
        max_weights_index = max_n(np.abs(weights), self.params.num_positions)
        max_weights = weights[max_weights_index]
        weights = weights / np.sum(np.abs(max_weights))

        for i, d in enumerate(available):
            if i in max_weights_index:
                self.order_target_percent(d, target=weights[i])
            else:
                self.order_target_percent(d, 0)
				
			

Invece di avere posizioni su ogni titolo dell’universo, possiamo negoziare solamente i primi 100 titoli con i pesi maggiori:

				
					
dd, cagr, sharpe = backtest(datas, CrossSectionalMR, plot=True, num_positions=100)
print(f"Max Drawdown: {dd:.2f}%\nAPR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
Migliorare una strategia mean-reverting cross sectional
				
					Max Drawdown: 12.27%
APR: 9.61%
Sharpe: 1.071
				
			

I risultati indicano che diminuendo il numero di posizioni, stiamo aumentando la volatilità del portafoglio. Forse se investiamo solo in azioni con una bassa volatilità possiamo avere risultati migliori.

Filtro di volatilità

Proviamo a usare la stessa formula dei pesi, ma  negoziamo solamente i titoli che si trovano nei primi n ordinati per peso e negli ultimi n ordinati secondo la deviazione standard degli ultimi 5 giorni. In questo modo negoziamo solamente azioni che presentano un rendimenti maggiori rispetto ai rendimenti medi dell’universo e una volatilità relativamente bassa.

				
					

def min_n(array, n):
    return np.argpartition(array, n)[:n]


def max_n(array, n):
    return np.argpartition(array, -n)[-n:]


class CrossSectionalMR(bt.Strategy):
    params = (
        ('n', 100),
    )

    def __init__(self):
        self.inds = {}
        for d in self.datas:
            self.inds[d] = {}
            self.inds[d]["pct"] = bt.indicators.PercentChange(d.close, period=5)
            self.inds[d]["std"] = bt.indicators.StandardDeviation(d.close, period=5)

    def prenext(self):
        self.next()

    def next(self):
        available = list(filter(lambda d: len(d) > 5, self.datas)) # usa solo i dati disponibili nella giornata precedente
        rets = np.zeros(len(available))
        stds = np.zeros(len(available))
        for i, d in enumerate(available):
            rets[i] = self.inds[d]['pct'][0]
            stds[i] = self.inds[d]['std'][0]

        market_ret = np.mean(rets)
        weights = -(rets - market_ret)
        max_weights_index = max_n(np.abs(weights), self.params.n)
        low_volality_index = min_n(stds, self.params.n)
        selected_weights_index = np.intersect1d(max_weights_index,
                                                low_volality_index)
        if not len(selected_weights_index):
            # non si effettua trade
            return

        selected_weights = weights[selected_weights_index]
        weights = weights / np.sum(np.abs(selected_weights))
        for i, d in enumerate(available):
            if i in selected_weights_index:
                self.order_target_percent(d, target=weights[i])
            else:
                self.order_target_percent(d, 0)
                
				
			

Come possiamo vedere, selezioniamo le azioni da negoziare calcolando l’intersezione tra le azioni con peso massimo e le azioni con deviazione standard minima. Eseguiamo il backtest.

				
					
dd, cagr, sharpe = backtest(datas, CrossSectionalMR, plot=True, n=100)
print(f"Max Drawdown: {dd:.2f}%\nAPR: {cagr:.2f}%\nSharpe: {sharpe:.3f}")
				
			
Backtest-Mean-Reverting-Cross-Section-100-posizioni-volatilita
				
					Max Drawdown: 17.77%
APR: 23.30%
Sharpe: 1.668
				
			

La strategia di mean-reverting cross sectional con filtro di volatilità ha un rendimento medio annuo del 23,3% con un o Sharpe Ratio di 1,688. Questa è la strategia con le migliori prestazioni che abbiamo ottenuto finora. Da notare che abbiamo introdotto alcuni parametri (il numero di posizioni e il periodo della deviazione standard) che possiamo ottimizzare (e  potenzialmente overfittare).

Infine sottolineiamo che, come nel precedente algoritmo mean-reverting, non consideriamo il bias di sopravvivenza nell’S&P 500 nel periodo di 5 anni del nostro backtest.

Il prossimo passo è provare a fare trading algoritmico dal vivo con questa strategia! Per ulteriori idee e strategie possiamo dare un’occhiata al libro Algorithmic Trading: Winning Strategies and Their Rationale.

Codice completo

In questo articolo abbiamo descritto come migliorare una strategia mean-reverting con Backtrader per poter iniziare a fare trading algoritmico dal vivo. 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...

Scroll to Top