misurare le Performance di un Indicatore con Backtrader

Misurare le Performance di un Indicatore con backtrader

In questo post descriviamo come misurare le performance di un indicatore con Backtrader e Pandas applicato ad una strategia di trading algoritmico.

Vogliamo analizzare le prestazioni dei segnali di trading generati da un semplice indicatore per verificare se alcuni segnali sono migliori nel prevedere i movimenti del mercato in un determinato periodo di tempo. In altre parole, se i segnali sono più redditizi nelle 10 barre successive al segnale o nelle 200 barre successive. L’idea è di generare un elenco di segnali in backtrader e quindi tracciare la performance di ciascun segnale nelle barre successive e vedere se emergono degli schemi.

Misurare le performance di un indicatore con Backtrader

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

def parse_args():
    parser = argparse.ArgumentParser(description="COT BACKTESTER")

    parser.add_argument('--data',
                        default='data/EUR_USD-D.csv',
                        type=str,
                        help='Price Data CSV File')

    parser.add_argument('--results',
                        default='SignalAnalysis-Results.csv',
                        type=str,
                        help='Price Data CSV File')

    parser.add_argument('--bars',
                        default=200,
                        type=int,
                        help='Analyze the following x bars')

    parser.add_argument('--period',
                        default=200,
                        type=int,
                        help='SMA Lookback Period')

    parser.add_argument('--analyze_only',
                        action='store_true',
                        help='Perform Pandas Analysis Only')

    return parser.parse_args()


class SignalAnalysis(bt.Strategy):

    params = (
        ('sma_lkb', 200),
        )

    def __init__(self):

        self.setup_log_file()
        # Indicatore
        self.sma = bt.indicators.SMA(self.data, period=self.p.sma_lkb)
        self.cross = bt.indicators.CrossOver(self.data.close, self.sma)

    def next(self):
        dt = self.datas[0].datetime.datetime()
        bar = len(self.datas[0])
        o = self.datas[0].open[0]
        h = self.datas[0].high[0]
        l = self.datas[0].low[0]
        c = self.datas[0].close[0]
        v = self.datas[0].volume[0]

        # Crea alcuni alias dei segnale in modo da poterli sostituire facilmente
        # con migliori segnali in un secondo momento
        long = self.cross == 1
        short = self.cross == -1

        # Questa parte può essere trascurata
        # Basta creare le condizioni long e short nelle righe precedenti!
        if long:
            sig = 'Long'
        elif short:
            sig = 'Short'
        else:
            sig = ''

        with open(self.logfile, 'a+') as f:
            log = "{},{},{},{},{},{},{}\n".format(dt,o,h,l,c,v,sig)
            f.write(log)


    def setup_log_file(self):
        '''
        Funzione per impostare i file di log
        '''
        cn = self.__class__.__name__
        self.logfile ='{}-Results.csv'.format(cn)

        # Scrive l'header nel log dei trade.
        log_header = 'Datetime,Open,High,Low,Close,Volume,Signal'

        with open(self.logfile, 'w') as file:
            file.write(log_header + "\n")


args = parse_args()

if not args.analyze_only:
    # Creare un istanza di cerebro
    cerebro = bt.Cerebro()

    # Aggiungere la strategia
    cerebro.addstrategy(SignalAnalysis, sma_lkb=args.period)

    # Creare il DataFeed
    data = bt.feeds.GenericCSVData(
        timeframe=bt.TimeFrame.Days,
        compression=1,
        dataname=args.data,
        nullvalue=0.0,
        dtformat=('%Y-%m-%d %H:%M:%S'),
        datetime=0,
        time=-1,
        high=2,
        low=3,
        open=1,
        close=4,
        volume=5,
        openinterest=-1 #-1 significa non usato
        )

    cerebro.adddata(data)

    # Esecuzione del backtest
    strats = cerebro.run()



# Creare Dataframe da file CSV
df = pd.read_csv(args.results, parse_dates=True, index_col=[0])

# Ottenere le date dei segnali long e short
longs = df[df['Signal'] == 'Long']
shorts = df[df['Signal'] == 'Short']

# Ottenere i valori degli indici per ciascun segnale. Da usare in seguito per
# il looping e la creazione di un nuovo dataframe
long_entries = longs.index.values
short_entries = shorts.index.values

# Creare due nuovi dataframes per i risultati
long_analysis = pd.DataFrame()
short_analysis = pd.DataFrame()


def pnl_calc(x, open_value, short=False):
    '''
    open_value: Float, il valore close della barra del segnale
    short: Bool, invertire il calcolo della percentuale di variazione per gli short
    '''
    try:
        pnl = (x - open_value) / open_value

        if short:
            return -pnl
        else:
            return pnl
    except ZeroDivisionError:
        return 0

for signal_date in long_entries:
    # Ottenere i valori di chiusura per le 200 barre successive al segnale
    closes = pd.DataFrame(df['Close'].loc[signal_date:].head(args.bars + 1),).reset_index()

    # Calcolare la differenza
    closes['PNL'] = closes['Close'].apply(pnl_calc, open_value=closes['Close'].iloc[0], short=False)
    long_analysis[signal_date] = closes['PNL']

for signal_date in short_entries:
    closes = pd.DataFrame(df['Close'].loc[signal_date:].head(args.bars + 1),).reset_index()

    closes['PNL'] = closes['Close'].apply(pnl_calc, open_value=closes['Close'].iloc[0], short=True)
    short_analysis[signal_date] = closes['PNL']

# Ottenre il PNL medio
long_average = long_analysis.mean(axis=1)
short_average = short_analysis.mean(axis=1)

# Grafici
# ------------------------------------------------------------------------------
fig, axes = plt.subplots(nrows=2, ncols=2)

# Tutte le entrate long
ax1 = long_analysis.plot(ax=axes[0,0], kind='line', legend=False, title='Long Signal PnL / Bar', )
ax1.set_ylabel("PnL %")

# Tutte le entrate short
ax2 = short_analysis.plot(ax=axes[0,1], kind='line',  legend=False, title='Short Signal PnL / Bar')
ax2.set_ylabel("PnL %")

# Media delle entrate long
ax3 = long_average.plot(ax=axes[1,0], kind='line', legend=False, title='Long Average')
ax3.set_xlabel("Bar No")

# Media delle entrate short
ax4 = short_average.plot(ax=axes[1,1], kind='line', legend=False, title='Short Average')
ax4.set_xlabel("Bar No")

fig.set_size_inches(10.5, 6.5)

# Grafico
plt.show()
				
			

Commento del codice

Abbiamo già descritto nei precedenti articoli la maggior parte delle funzioni  presenti in questo codice. Per approfondimenti si può leggere gli articoli relativi al framework BackTrader. Di seguito descriviamo alcune note generali sull’esecuzione dello script e la logica implementata ad alto livello.

Il codice di questo esempio prevede un file CSV per i dati della serie storica ininput. Possiamo scaricare una copia dei dati usate per questi test nel csv EUR_USD-D. Dobbiamo salvare questo file in una sottodirectory dello script principale chiamata “data”. Inoltre sono stati aggiunti alcuni argomenti per l’esecuzione runtime che possono essere usati per modificare il datafeed o saltare completamente la generazione dei segnali con backtrader. 

Può essere utile se abbiamo già eseguito la generazione dei segnali e vogliamo solamente vedere il grafico delle prestazioni. Per approfondire il metodo argparse e le sue opzioni di runtime, si può leggere l’articolo “Modificare i parametri della strategia con backtrader“.

Se usiamo l’opzione di runtime per analizzare i dati, dobbiamo assicurarci che la struttura del file e il formato della data dei dati siano gli stessi previsti nel file di esempio. In altre parole le colonne OpenHighLow, Close e Volume devono essere sono nello stesso ordine e dobbiamo prevedere che il formato della data sia lo stesso. Se i dati non seguono questo formato e non vogliamo modificarli, possiamo modificare la seguente sezione in modo che corrisponda al formato del file CSV: 

				
					# Creare il DataFeed
    data = bt.feeds.GenericCSVData(
        timeframe=bt.TimeFrame.Days,
        compression=1,
        dataname=args.data,
        nullvalue=0.0,
        dtformat=('%Y-%m-%d %H:%M:%S'),
        datetime=0,
        time=-1,
        high=2,
        low=3,
        open=1,
        close=4,
        volume=5,
        openinterest=-1 #-1 significa non usato
        )
				
			

Formato dei datafeed

La gestione del formato dei datafeed è descritta in dettaglio nei seguenti link:

All’avvio del codice, usiamo Backtrader per eseguire una semplice strategia. Durante ogni chiamata del metodo next() della strategia, registriamo in un log i dati OHLCV insieme all’eventuale generazione di un segnale Long o Short. Il log è salvato in un file CSV che successivamente  è usato per l’analisi con Pandas. I segnali generati nella strategia di test sono solo esempi per mantenere il codice breve e pulito. Tutto ciò che facciamo è generare segnali quando abbiamo una chiusura al di sopra o al di sotto di una media mobile semplice. Possiamo facilmente sostituirlo con un qualsiasi segnale complesso modificando  le variabili booleane Long e Short.

La logica di analisi è stata la parte più complicata da implementare con il pacchetto Pandas ma sembra funzionare (grazie a google e stack overflow) In quanto tale, è probabile che ci sono metodi più efficienti per codificare questa sezione. L’analisi delle prestazioni prevede di estrarre la data di ciascun segnale long e short e quindi ricavare i prezzi di chiusura delle x barre successive. Dopo aver ottenuto i prezzi di chiusura, possiamo calcolare il PNL corrente per ogni barra e inserirlo in un nuovo dataframe delle prestazioni.

Risultati

L’esecuzione dello script con le impostazioni predefinite e i dati di esempio produce un grafico simile a questo:

misurare le Performance di un Indicatore con Backtrader

I risultati sono affascinanti! Vediamo che generalmente i segnali long sono stati più redditizi dei segnali short durante il periodo di test. Questo ha senso poiché sappiamo che l’euro è generalmente salito rispetto al dollaro nel lungo periodo. Inoltre, i cali dell’EURO tendono ad essere movimenti più rapidi rispetto ai periodi di apprezzamento. Infine, stiamo usando una SMA lenta quindi i risultati coprono 200 giorni, cioè ogni segnale trae vantaggio dal trend mentre la medisa si sposta lungo la finestra delle ultime 200 barre.

Backtrader-EURUSD-200-SMA

Vediamo ora i risultati se  riduciamo il periodo di analisi a soli 20 giorni. Otteniamo i seguenti grafici:

misurare le Performance di un Indicatore con Backtrader

Lasciamo al lettore trarre le conclusioni su questo grafico. Come per i grafici a candle, ognuno vede qualcosa di diverso! Questa è la bellezza di queste analisi. Con questo metodo possiamo analizzare come si comportano i segnali più complessi a breve termine sostituendo la SMA nella strategia di backtrader.

Codice completo

In questo articolo abbiamo descritto come misurare le performance di un indicatore 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