datafeed e indicatori multipli con backtrader

Datafeed e Indicatori Multipli con Backtrader

In questo articolo descriviamo come implementare datafeed e indicatori multipli con backtrader per creare e verificare strategie di trading algoritmico. Come descritto nell’articolo Backtrader: primo script  o negli altri articoli di DataTrading.info, la maggior parte degli esempi prevede un solo feed di dati, cioè una sola serie storica di dati finanziari. Allo stesso modo, abbiamo definito in anticipo il numero di indicatori da utilizzare in una strategia.

Dato che i precedenti articoli hanno descritto le funzioni base di Backtrader abbiamo usato un codice il più semplice possibile e focalizzato solo  su uno specifico argomento. Questo è il primo articolo incentrato sul backtesting con più feed di dati. Descriviamo come modificare il numero di serie storiche senza modificare il codice, modificare dinamicamente gli indicatori che producono segnali di acquisto/vendita o assegnare lo stesso indicatore a più serie finanziarie.

Introduzione

Il sito di Backtrader ha un buon tutorial dove si  introduce le modalità di lavoro con più feed di dati. Tuttavia, è utile aggiungere ulteriori informazioni con un’introduzione rivolta ai principianti e ampliando alcuni dei concetti descritti nel blog ufficiale. Ad esempio, nel blog ufficiale non si usa indicatori mentre può essere utile descrivere come inizializzarli. Fortunatamente, Python ha funzioni che ci permette di come creare dinamicamente indicatori invece di definirli come nomi di attributi (variabili) hard-coding all’interno del metodo __init__(). Il blog ufficiale è disponibile al seguente link: https://www.backtrader.com/blog/posts/2017-04-09-multi-example/multi-example.html

Ottenere i dati

Gli esempi in articolo usano dati scaricati da Oanda. Possiamo sostituire i dati con qualsiasi altra serie storica. Tuttavia, è possibile semplicemente copiare, incollare ed eseguire il codice usando una copia dei file di dati disponibili di seguito. E’ solo necessario inserirli in una cartella denominata “dati” in modo che possano essere trovati dallo script.

  1. CAD_CHF-2005-2017-D1
  2. EUR_USD-2005-2017-D1
  3. GBP_AUD-2005-2017-D1

Se decidi di NON lavorare con i dati forniti, tieni presente che negli esempi seguenti abbiamo usato la sottoclasse bt. feeds.GenericCSVData per indicare a cerebro quali colonne nel file CSV corrispondono ai prezzi OHLC.

				
					class OandaCSVData(bt.feeds.GenericCSVData):
    params = (
        ('nullvalue', float('NaN')),
        ('dtformat', '%Y-%m-%dT%H:%M:%S.%fZ'),
        ('datetime', 6),
        ('time', -1),
        ('open', 5),
        ('high', 3),
        ('low', 4),
        ('close', 1),
        ('volume', 7),
        ('openinterest', -1),
    )
				
			

Vedi: https://www.backtrader.com/docu/dataautoref.html

Usare più DataFeed

Esaminiamo il processo passo dopo passo. La prima parte è forse la più semplice. Per aggiungere i dati di più strumenti dobbiamo prevedere esattamente la stessa procedura descritta per aggiungere i dati per un singolo strumento. E’ sufficiente creare l’oggetto dati, inserirlo in cerebro, e ripetere con altri dati. Può essere utile creare una lista o un dizionario dei datafeed che desideriamo usare. In questo modo possiamo scorrere la lista senza avere righe di codice separate per ciascun feed di dati. Inoltre, possiamo creare la lista in fase di esecuzione usando il metodo “argparse” o un file di configurazione per definire dinamicamente i datafeed o aggiungerne nuovi senza modificare il codice. Un’introduzione al metodo argparse è descritta nell’articolo su come modificare i parametri della strategia con Backtrader.

Il codice seguente mostra un esempio per aggiungere i feed di dati da una lista. Possiamo notare che la lista dei dati contiene liste nidificati per specificare la posizione dei file e i nomei dei datafeed. Il codice analizza la lista per per aggiungere i dati in cerebro. Evidenziamo che è necessario usare la parola chiave “name” quando aggiungiamo l’oggetto “data” a cerebro in modo possiamo differenziare facilmente i feed nei log e nei report.

				
					# Creo la lista di datafeed
datalist = [
    ('data/CAD_CHF-2005-2017-D1.csv', 'CADCHF'), #[0] = Data file, [1] = Data name
    ('data/EUR_USD-2005-2017-D1.csv', 'EURUSD'),
    ('data/GBP_AUD-2005-2017-D1.csv', 'GBPAUD'),
]

# Ciclo la lista per aggiungere i datafeed in cerebro.
for i in range(len(datalist)):
    data = OandaCSVData(dataname=datalist[i][0])
    cerebro.adddata(data, name=datalist[i][1])
				
			

Lavorare con i datifeed all’interno del metodo next()

Le righe chiave del codice  mostrato nel blog ufficiale  di Backtrader che permettono di lavorare facilmente con più datafeed sono:

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

Il codice analizza i datafeed uno alla volta. La variabile d contiene l’oggetto dati in analisi ed l’oggetto con cui dobbiamo principalmente lavorare. Inoltre, d._name restituisce il nome dei dati che abbiamo fornito durante l’aggiunta dei dati in cerebro. Questo ci aiuta a identificare con quale feed stiamo lavorando (utile per la stampa, i log e il debug, ecc.). Infine, un’altra differenza con le precedenti implementazioni è che non stiamo usando l’attributo self.position per determinare se siamo a mercato. Invece, dobbiamo controllare la posizione per ogni feed di dati. Se restituisce None, significa che non siamo a mercato e possiamo potenzialmente fare un trade.

Visualizzazione sullo stesso grafico

Nel blog ufficiale di Backtrader, l’autore usa un parametro per specificare la visualizzazione di tutti i datafeed sullo stesso grafico. Questo può essere utile, ma se abbiamo molti datafeed, il grafico può diventare rapidamente disordinato! Pertanto può essere preferibile visualizzare i datafeed in grafici separati. Inoltre se abbiamo molti feed di dati, potrebbe essere utile disattivare del tutto la produzione dei grafici. Un’altra nota sul codice di esempio è che l’autore crea tutti i feed di dati durante l’inizializzazione di cerebro utilizzando righe di codice separate in questo modo:

				
					
# Data feed
data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **kwargs)
cerebro.adddata(data0, name='d0')

data1 = bt.feeds.YahooFinanceCSVData(dataname=args.data1, **kwargs)
data1.plotinfo.plotmaster = data0
cerebro.adddata(data1, name='d1')

data2 = bt.feeds.YahooFinanceCSVData(dataname=args.data2, **kwargs)
data2.plotinfo.plotmaster = data0
cerebro.adddata(data2, name='d2')
				
			

Dato che in questo articolo proponiamo un codice che usa un ciclo loop per configurare i datafeed, può essere utile impostare il parametro plotmaster all’interno del metodo __init__() della strategia. Un esempio è mostrato nel seguente frammento di codice.

Aggiungere gli indicatori

Quando vogliamo gestire un numero di datafeed variabile abbiamo il problema di non poter codificare i nomi degli indicatori. Ad esempio

				
					self.ind1 = bt.indicators.IndicatorName()
self.ind2 = bt.indicators.IndicatorName()
self.ind3 = bt.indicators.IndicatorName()
self.ind4 = bt.indicators.IndicatorName()
				
			

e così via… Una possibile soluzione consiste nell’usare un dizionario. Possiamo creare un dizionario in cui l’oggetto datafeed è la chiave e gli oggetti indicatori sono memorizzati come valori. In questo modo è possibile accedere facilmente agli oggetti (e quindi ai valori) durante ogni  chiamata al metodo next(). Quando eseguiamo il loop sui datafeed, possiamo semplicemente passare un oggetto datafeed come chiave del dizionario per accedere agli indicatori  disponibile per quel lo specifico datafeed. L’esempio seguente mostra come codificare questa logica, che farà parte del codice completo. Inoltre, mentre stiamo analizzando i datafeed, possiamo impostare l’attributo plotmaster descritto nella sezione precedente.

				
					    
    def __init__(self):
        '''
        Crea un dizionario di indicatori in modo da poter aggiungere dinamicamente
        gli indicatori alla strategia utilizzando un ciclo. In questo modo la
        strategia funzionerà con qualsiasi numero di feed di dati.
        '''
        self.inds = dict()
        for i, d in enumerate(self.datas):
            self.inds[d] = dict()
            self.inds[d]['sma1'] = bt.indicators.SimpleMovingAverage(
                d.close, period=self.params.sma1)
            self.inds[d]['sma2'] = bt.indicators.SimpleMovingAverage(
                d.close, period=self.params.sma2)
            self.inds[d]['cross'] = bt.indicators.CrossOver(self.inds[d]['sma1'],self.inds[d]['sma2'])

            if i > 0: # Controllo se non siamo nel primo loop del data feed:
                if self.p.oneplot == True:
                    d.plotinfo.plotmaster = self.datas[0]
				
			

Datafeed e Indicatori multipli con backtrader

La strategia completa, riportata di seguito, usa una semplice strategia di crossover della media mobile su più feed di dati. Per impostazione predefinita, il grafico prevedere visualizzazione separate. E’ possibile modificarlo tramite la riga:

				
					   # Aggiungo la strategia
cerebro.addstrategy(maCross, oneplot=False)
				
			

Il codice

Di seguito il codice che implementa la gestione di datafeed e indicatori multipli con backtrader 

				
					
import backtrader as bt
from datetime import datetime


class maCross(bt.Strategy):
    '''
    Il blog ufficiale di backtrader su questo argomento, è disponibile:
    https://www.backtrader.com/blog/posts/2017-04-09-multi-example/multi-example.html

    oneplot = Forza la visualizzazione di tutti i dati sullo stesso grafico
    '''
    params = (
    ('sma1', 40),
    ('sma2', 200),
    ('oneplot', True)
    )

    def __init__(self):
        '''
        Crea un dizionario di indicatori in modo da poter aggiungere dinamicamente
        gli indicatori alla strategia utilizzando un ciclo. In questo modo la
        strategia funzionerà con qualsiasi numero di feed di dati.
        '''
        self.inds = dict()
        for i, d in enumerate(self.datas):
            self.inds[d] = dict()
            self.inds[d]['sma1'] = bt.indicators.SimpleMovingAverage(
                d.close, period=self.params.sma1)
            self.inds[d]['sma2'] = bt.indicators.SimpleMovingAverage(
                d.close, period=self.params.sma2)
            self.inds[d]['cross'] = bt.indicators.CrossOver(self.inds[d]['sma1'],self.inds[d]['sma2'])

            if i > 0: # Controllo se non siamo nel primo loop del data feed:
                if self.p.oneplot == True:
                    d.plotinfo.plotmaster = self.datas[0]

    def next(self):
        for i, d in enumerate(self.datas):
            dt, dn = self.datetime.date(), d._name
            pos = self.getposition(d).size
            if not pos:  # nessuna posizione / nessun ordine
                if self.inds[d]['cross'][0] == 1:
                    self.buy(data=d, size=1000)
                elif self.inds[d]['cross'][0] == -1:
                    self.sell(data=d, size=1000)
            else:
                if self.inds[d]['cross'][0] == 1:
                    self.close(data=d)
                    self.buy(data=d, size=1000)
                elif self.inds[d]['cross'][0] == -1:
                    self.close(data=d)
                    self.sell(data=d, size=1000)

    def notify_trade(self, trade):
        dt = self.data.datetime.date()
        if trade.isclosed:
            print('{} {} Closed: PnL Gross {}, Net {}'.format(
                                                dt,
                                                trade.data._name,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))


class OandaCSVData(bt.feeds.GenericCSVData):
    params = (
        ('nullvalue', float('NaN')),
        ('dtformat', '%Y-%m-%dT%H:%M:%S.%fZ'),
        ('datetime', 6),
        ('time', -1),
        ('open', 5),
        ('high', 3),
        ('low', 4),
        ('close', 1),
        ('volume', 7),
        ('openinterest', -1),
    )


# Capitale iniziale
startcash = 10000

# Creo un'istanza di cerebro
cerebro = bt.Cerebro()

# Aggiungo la strategia
cerebro.addstrategy(maCross, oneplot=False)

# Creo la lista di datafeed
datalist = [
    ('data/CAD_CHF-2005-2017-D1.csv', 'CADCHF'), #[0] = Data file, [1] = Data name
    ('data/EUR_USD-2005-2017-D1.csv', 'EURUSD'),
    ('data/GBP_AUD-2005-2017-D1.csv', 'GBPAUD'),
]

# Ciclo la lista per aggiungere i datafeed in cerebro.
for i in range(len(datalist)):
    data = OandaCSVData(dataname=datalist[i][0])
    cerebro.adddata(data, name=datalist[i][1])


# Imposto il capitale iniziale in cerebro
cerebro.broker.setcash(startcash)

# Esecuzione backtest
cerebro.run()

# Valore finale del portafoglio
portvalue = cerebro.broker.getvalue()
pnl = portvalue - startcash

# Stampa del valore finale
print('Final Portfolio Value: ${}'.format(portvalue))
print('P/L: ${}'.format(pnl))

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

I risultati

Backtrader-Multi-data-Result

Abbiamo ottenuto una strategia completamente funzionante che utilizza più  datafeed e più indicatori.

NOTA: se ti stai chiedendo perché i parametri predefinite per le SMA sono 40 e 200, puoi consultare l’articolo relativo all’analisi del crossover SMA in cui abbiamo mostrato come questi valori hanno dato i risultati migliori in tutti i mercati testati.

Codice completo

In questo articolo abbiamo descritto come implementare datafeed e indicatori multipli con backtrader per creare e verificare strategie di trading algoritmico. 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