Una volta compreso come scrivere una strategia di base, il passo successivo consiste nel quantificare la qualità di questa strategia. Backtrader ha una ricca libreria di strumenti di analisi in grado di fornire molte metriche, dal semplice rapporto vincite / perdite ai più complessi Sharpe Ratio e analisi del drawdown.

Cosa sono gli Analyzer di Backtrader

In poche parole, sono oggetti che possono essere caricati cerebro (il motore di Backtrader) e sono in grado di monitorare la tua strategia mentre questa viene eseguita. Quando cerebro ha terminato l’esecuzione del backtesting, è possibile accedere agli analizzatori tramite oggetti di strategia che sono restituiti da cerebro a termine dell’esecuzione. Gli stessi oggetti dell’analizzatore hanno un metodo (funzione) speciale che restituisce un dizionario di tutte le statistiche che l’analizzatore sta monitorando. A volte si tratta di molte informazioni e altre volte solo una o due statistiche. Non ti preoccupare se in questo momento ti sembra complesso, diventerà tutto più chiaro quando vedrai il codice. Esiste un’intera libreria dei diversi analizzatori che possono essere utilizzati in Backtrader (e se ne possono creare di nuovi in ogni momento). Dopo aver lavorato con questo script, dai un’occhiata alla documentazione per vedere quali analizzatori ti interessano e modifica il codice per includerli nel tuo backtesting.

Obiettivo

Questo articolo ha lo scopo di mostrare come inizializzare, analizzare e restituire i risultati di un backtesting. E’ il continuo del precedente articolo “Backtrader: Primo Script” e fa parte della serie introduttiva di Backtrader. Per vedere le regole della strategia e la spiegazione del codice, dai un’occhiata al precedente articolo.

Background

Esistono un paio di metriche di backtest / trading che rientrano in questo post. Ecco un glossario:

  • Strike Rate: questa è una percentuale che rappresenta il numero di volte di trade vincenti rispetto al numero totale di scambi che sono stati effettuati (tasso di vittoria / trade totali). Può aiutare a identificare se esiste un vantaggio nel mercato. Alcuni trader mirano a ottenere il strike rate più alto possibile, che corrisponde a tante piccole vincite. Altri sono felici con strike rate più bassi, ma puntano a grandi vincite e piccole perdite.
  • SQN: System Quality Number (numero di qualità del sistema), questo è stato definito dal dott. Van Tharp dell’istituto Van Tharp. In pratica dà un punteggio alla tua strategia. Una spiegazione più accademica del SQN, presente nel sito web di Van Tharp, è la seguente (Per ulteriori informazioni, consultare il sito www.vantharp.com)

l’SQN misura la relazione tra la media (stimata) e la deviazione standard della distribuzione “R-multipla” generata da un sistema di trading. Apporta inoltre un adeguamento rispetto al numero di operazioni coinvolte.

Nota: la documentazione di Backtrader fornisce un utile sistema di classificazione per SQN:

  • 1.6 – 1.9 Sotto la media
  • 2,0 – 2,4 Media
  • 2,5 – 2,9 Buono
  • 3,0 – 5,0 Eccellente
  • 5,1 – 6,9 Superbo
  • 7.0 – Santo Graal?

Il Codice

import backtrader as bt
from datetime import datetime
from collections import OrderedDict

class RsiStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        if not self.position:
            if self.rsi < 30: self.buy(size=100) else: if self.rsi > 70:
                self.sell(size=100)


def tradeAnalysis(analyzer):
    '''
    Function to print the Technical Analysis results in a nice format.
    '''
    #Get the results we are interested in
    total_open = analyzer.total.open
    total_closed = analyzer.total.closed
    total_won = analyzer.won.total
    total_lost = analyzer.lost.total
    win_streak = analyzer.streak.won.longest
    lose_streak = analyzer.streak.lost.longest
    pnl_net = round(analyzer.pnl.net.total,2)
    strike_rate = (total_won / total_closed) * 100
    #Designate the rows
    h1 = ['Total Open', 'Total Closed', 'Total Won', 'Total Lost']
    h2 = ['Strike Rate','Win Streak', 'Losing Streak', 'PnL Net']
    r1 = [total_open, total_closed,total_won,total_lost]
    r2 = [strike_rate, win_streak, lose_streak, pnl_net]
    #Check which set of headers is the longest.
    if len(h1) > len(h2):
        header_length = len(h1)
    else:
        header_length = len(h2)
    #Print the rows
    print_list = [h1,r1,h2,r2]
    row_format ="{:<15}" * (header_length + 1)
    print("Trade Analysis Results:")
    for row in print_list:
        print(row_format.format('',*row))

def SQN(analyzer):
    sqn = round(analyzer.sqn,2)
    print('SQN: {}'.format(sqn))

#Variable for our starting cash
startcash = 100000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(RsiStrategy)

#Get Apple data from Yahoo Finance.
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2009,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

#Add the data to Cerebro
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# Add the analyzers we are interested in
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")
cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn")

# Run over everything
strategies = cerebro.run()
rsiStrat = strategies[0]

# print the analyzers
tradeAnalysis(rsiStrat.analyzers.ta.get_analysis())
SQN(rsiStrat.analyzers.sqn.get_analysis())

#Get final portfolio Value
portvalue = cerebro.broker.getvalue()

#Print out the final result
print('Final Portfolio Value: ${}'.format(portvalue))

#Finally plot the end results
cerebro.plot(style='candlestick')

Spiegazione del Codice

Vediamo innanzitutto come aggiungere un Analyzer alla strategia consiste semplicemente nel richiamare la funzione addanaylzer(). In particolare, si sta aggiungendo il TradeAnalyzer e assegnandogli un nome. Il nome rende molto più semplice accedere questo oggetto in qualsiasi momento
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")

Al termine dell’esecuzione di cerebro, viene restituito un elenco di oggetti strategia. In questo esempio abbiamo caricato in cerebro una sola strategia. Anche in questo caso, l’unica strategia viene comunque restituita all’interno di un array. Pertanto è necessario estrarre la nostra strategia  utilizzando un indice della posizione all’interno dell’array ([0]). Una volta che abbiamo quello, abbiamo la nostra strategia.

# Run over everything
strategies = cerebro.run()
firstStrat = strategies[0]
A questo punto dobbiamo accedere ai nostri dati …. È possibile accedere agli Analyzers dall’interno dell’oggetto strategia ed utilizzare un metodo (funzione) nativo per restituire un dizionario (dict) contenente tutti i risultati. Di seguito si evidenzia come lo script accede all’analyzer “ta” (che è il nome che gli abbiamo dato durante il caricamento dell’analyzer in cerebro) dall’oggetto strategia rsiStrat e si richiama il metodo get_analysis().
bt.analyzers.TradeAnalyzer, _name="ta"

Dopo aver ottenuto il dizionario dei dati, è necessario estrarre i dati che ci interessano ed elaborarli a seconda dei nostri scopi. In questo caso, si considerano solo alcune metriche e vengono stampate sul terminale. Tuttavia, è possibile espandere questa soluzione ed esportare i risultati in un CSV. In questo esempio si esaminano:

  • Totale operazioni ancora aperte
  • Totale operazioni chiuse
  • Totale operazioni vincenti
  • Totale operazioni perdenti
  • Strike rate (che si calcola sulla base dei dati forniti)
  • Migliore serie vincente
  • Peggior serie perdente
  • Profitti o perdite.
  • SQN della strategia

Per fare ciò ho creato due funzioni di stampa speciali. E’ quindi possibile riutilizzare questo codice (copia e incolla) per gli script futuri.

La funzione di stampa che merita di essere commentata più avanti è:

tradeAnalysis(analyzer):
Temo che questa funzione possa essere eccessivamente complessa per questo articolo introduttivo e destinato ai principianti a causa della linea mostrata di seguito. Mi piace solo l’output pulito che produce nel terminale. Contiene alcune tecniche avanzate di Python per la formattazione del testo. Sto parlando della linea che assomiglia a questa:
row_format ="{:<15}" * (header_length + 1)

Questa riga mi consente di stampare l’output distribuito uniformemente senza dover installare un altro modulo di Python, come TextTable. Per ulteriori informazioni sulla formattazione dei dati in Python, consultare i documenti qui: https://docs.python.org/3/library/string.html

Un’alternativa (e un’opzione molto più semplice) consiste nell’utilizzare il metodo print() integrato nativamente nella classe dell’analyzer. Questo metodo stampa ogni valore all’interno dell’analizzatore su una riga separata. Se vuoi farlo o no è solo questione di gusti personali. Ad esempio, se si desidera disporre di una formattazione alternativa, selezionare determinate statistiche di interesse o elaborare ulteriormente i dati. Di seguito è riportato un esempio di come gestire questa alternativa:

# Run over everything
strategies = cerebro.run()
firstStrat = strategies[0]

for x in firstStrat.analyzers:
    x.print()

I Risultati

Eseguendo lo script si dovrebbe otterene qualcosa del genere al seguente output:

Non è un ottimo risultato di trading! Sembra che la nostra strategia super semplice abbia bisogno di qualche raffinamento.

Chi l’avrebbe mai detto?!?