Indicatore di Trendline per BackTrader

Questo è un frammento di codice per un indicatore di Trendline. Come suggerisce il nome, calcola il valore del prezzo in diversi punti di una trendline e, di conseguenza, genera segnali di acquisto e vendita. In alcuni casi mi piace poter adottare un approccio semi-automatico al trading algoritmico. Si potrebbe individuare una bella trendline su Tradingview ma si vuole eseguire operazioni in un ambiente di forward testing, utilizzando Backtrader, quando il prezzo raggiunge la trendline. In quanto tale, considero questo un indicatore a breve termine che può essere utilizzato fino a quando la trendline non viene rotta. L’indicatore di trendline descritto in questo articolo è destinato a essere utilizzato nel framework di Backtrader, ma i calcoli utilizzati per calcolare la trendline possono essere facilmente trasferiti su altri framework.

Background Matematico

Se come me, non sei un mago della matematica, potrebbe essere necessario un po’ di teoria per capire le equazioni contenute nel codice. E’ necessario quindi introdurre quelle che nel mondo della matematica sono conosciute come “equazioni lineari“, al fine di poter sviluppare questo indicatore.

Un tutorial ben scritto e molto utile, anche se in inglese, è il seguente:
mathplanet.com/education/algebra-1/formulating-linear-equations/writing-linear-equations-using-the-slope-intercept-form

Il concetto principale è che per poter calcolare il prezzo della trendline in qualsiasi momento, è necessario calcolare la velocità con cui cambia la pendenza tra due punti temporali. Questi punti temporali sono i due prezzi inseriti come input per l’indicatore. Come identificare questi due punti dipende da te. Come accennato in precedenza, identifico e traccio in modo discrezionale la trendline su Tradingview e quindi prendo nota del punto iniziale e del punto finale da utilizzare con questo indicatore.

L’equazione a cui dobbiamo arrivare è:

\(
\begin{eqnarray}
y = mx + b
\end{eqnarray}
\)

Dove:

y = prezzo

m = pendenza

x = data / ora

b = intersezione con l’asse y

A questo punto, una cosa che ho trovato difficile da spiegare è come la maggior parte dei tutorial presume che tu possa vedere visivamente l’intersezione con l’asse y, cioè il valore di y quando attraversa l’asse y per x=0. In questo modo:

Come sappiamo, i grafici dei prezzi sono leggermente diversi perchè difficilmente si può tornare indietro nel tempo (dove si potrebbe attraversare l’asse y). Non possiamo vedere dove è l’intersezione dell’asse y ma è possibile calcolarla! Per fare questo è necessario capovolgere un po’ l’equazione.

\( \begin{eqnarray} y = mx + b \end{eqnarray} \)
diventa
\( \begin{eqnarray} b = y – m*x \end{eqnarray} \)

Dopo aver risolto l’equazione, si può utilizzare i valori di x e y del punto iniziale e finale della trendline che hai indiviuato. Vedrai come l’ho implementato nel codice seguente.

Le Regole dell'Indicatore

L’indicatore della trendline genera un segnale di acquisto se il prezzo attraversa la trendline verso l’alto, mentre genera un segnale di vendita se il prezzo la attraversa dal basso. Poiché la trendline funge da supporto o resistenza, sarà sempre inferiore al prezzo quando si cercano segnali di acquisto mentre sarà superiore al prezzo quando si cercano segnali di vendita.

Il Codice

class TrendLine(bt.Indicator):
 
    lines = ('signal','trend')
    params = (
        ('x1', None),
        ('y1', None),
        ('x2', None),
        ('y2', None)
    )
 
    def __init__(self):
        self.p.x1 = datetime.datetime.strptime(self.p.x1, "%Y-%m-%d %H:%M:%S")
        self.p.x2 = datetime.datetime.strptime(self.p.x2, "%Y-%m-%d %H:%M:%S")
        x1_time_stamp = time.mktime(self.p.x1.timetuple())
        x2_time_stamp = time.mktime(self.p.x2.timetuple())
        self.m = self.get_slope(x1_time_stamp,x2_time_stamp,self.p.y1,self.p.y2)
        self.B = self.get_y_intercept(self.m, x1_time_stamp, self.p.y1)
        self.plotlines.trend._plotskip = True
 
    def next(self):
        date = self.data0.datetime.datetime()
        date_timestamp = time.mktime(date.timetuple())
        Y = self.get_y(date_timestamp)
        self.lines.trend[0] = Y
 
        #Check if price has crossed up / down into it.
        if self.data0.high[-1] < Y and self.data0.high[0] > Y:
            self.lines.signal[0] = -1
            return
 
        #Check for cross downs (Into support)
        elif self.data0.low[-1] > Y and self.data0.low[0] < Y:
            self.lines.signal[0] = 1
            return
 
        else:
            self.lines.signal[0] = 0
 
    def get_slope(self, x1,x2,y1,y2):
        m = (y2-y1)/(x2-x1)
        return m
 
    def get_y_intercept(self, m, x1, y1):
        b=y1-m*x1
        return b
 
    def get_y(self,ts):
        Y = self.m * ts + self.B
        return Y

Spiegazione del codice

Si generano due lines per questo indicatore. Una line è la trendline e l’altra line è il segnale. Da notare che la trendline non viene tracciata nel grafico perché in backtrader non è possibile tracciare una line sul grafico principale e un’altra sul grafico secondario. Si deve scegliere l’uno o l’altro. Tecnicamente si potrebbe tracciare entrambi sul grafico secondario, ma risulterebbe un po’ strano poiché la trendline può essere qualsiasi numero (dato che il valore dipende dal prezzo dello strumento) mentre il segnale oscilla solamente tra -1 e 1. Una soluzione alternativa è presentata nella sezione dei risultati. Un’altra cosa da notare sono le date, che devono essere convertite in un timestamp in modo che i calcoli possano essere fatti su un numero e non su un oggetto data. Infine, ci si potrebbe domandare perché si sta costruendo la trendline e richiamando get_y() nel metodo next() invece che nel metodo __init__()? Questo perché backtrader genera un IndexError se si prova ad ottenere le informazioni sulla data all’interno di __init__(). Se qualcuno sa come risolvere il problema, lasciatemi un commento! Probabilmente ho trascurato qualcosa all’interno della documentazione.

I metodi chiave

Se si desidera implementare questo codice in un altro framewoek, i tre metodi fondamentali da implementare sono i seguenti:
    def get_slope(self, x1,x2,y1,y2):
        m = (y2-y1)/(x2-x1)
        return m
 
    def get_y_intercept(self, m, x1, y1):
        b=y1-m*x1
        return b
 
    def get_y(self,ts):
        Y = self.m * ts + self.B
        return Y

Come in precedenza, è necessario utilizzare i timestamp per i parametri x e richiamare le funzioni nel seguente ordine:

  1. get_slope()
  2. get_y_intercept(), perché ha bisogno del valore di m, risultato di get_slope()
  3. get_y, per ottenere il prezzo finale per qualsiasi data e ora.

Risultati

Come accennato in precedenza, se si esegue questo codice così com’è, l’indicatore non traccia automaticamente nessuna trendline sul grafico di output. Poiché l’indicatore è progettato per produrre segnali di acquisto e vendita, la priorità è data alla stampa della line del segnale.

Come soluzione alternativa, per creare una certa verifica visiva del corretto funzionamento dell’indicatore, è possibile inizializzare un secondo indicatore all’interno della strategia che traccia una media mobile semplice della trendline, con un periodo pari a 1.

self.sma = bt.indicators.MovingAverageSimple(
         self.ind1.lines.trend, period=1,
         plotmaster=self.data0)
Da notare che, nel codice precedenete, è necessario inserire la riga all’interno del metodo __init__() della strategia, supponendo di aver già inizializzato l’indicatore con il nome ind1 (self.ind1.lines.trend).
Come si può vedere, si ha una trendline che attraversa il grafico e i segnali vengono generati secondo le regole implementate nel codice, come rappresentato nel grafico secondario.

Gestire il Position Sizing con BackTrader – Parte I

Con questo articolo si introduce un argomento fondamentale nel mondo del trading automatico: il dimensionamento delle posizioni. Inizialmente, pensavo di scrivere un solo articolo, ma mentre mi immergevo sempre più in profondità nell’argomento mi sono reso conto che ci sarebbero stati troppi contenuti per un solo articolo di ragionevole durata. In quanto tale, questo primo articolo si concentra sui concetti fondamenti per lo sviluppo dei sizer e fornisce un paio di semplici esempi. Il secondo articolo approfondisce alcuni aspetti, ed i particolare a comminfo. Dato che comminfo è una classe,si ha numerosi metodi che possono essere utilizzati per far sì che i tuoi algoritmi di dimensionamento tengano conto delle commissioni, degli interessi e delle posizioni aperte.

La classe Sizer

I Sizer sono classi che possono essere caricate in cerebro e utilizzate per decidere quante azioni, quote, contratti, ecc, acquistare o vendere ogni volta che viene chiamato self.buy() o self.sell(). I sizers sono utilizzati per un calcolo della posizione solo quando non viene fornita alcuna dimensione. In altre parole, se il tuo script contiene una chiamata di acquisto come self.buy(size=100), il Sizer non verrà chiamato. Tuttavia, se si chiama solo tramite self.buy(), cerebro chiederà al sizer la dimensione da acquistare.

Perchè abbiammo bisogno dei Sizer?

Alcuni trader possono preferire di dichiarare esplicitamente la dimensione, invece di implementare un qualsiasi tipo di logica di dimensionamento all’interno del codice della loro strategia. Se preferisci questo approccio non c’è niente di male, dopotutto ci sono molti modi per risolvere lo stesso problema! Tuttavia, dal mio punto di vista, usare i Sizer permette alcuni vantaggi. Se ti piace avere un codice ben strutturato e suddiviso, allora i Sizer fanno al tuo caso. Inoltre i Sizer consentono di apportare alcune modifiche, piccole o anche più consistenti, alla logica di una strategia senza dover toccare il codice della classe Strategia. La documentazione di Backtrader ha un buon esempio su come un sizer viene utilizzato per trasformare una strategia “long/short” in una strategia “solo long”, semplicemente usando un diverso sizer. Facendo un ulteriore passo avanti, è facile immaginare di poter implementare una libreria di Sizer che ti consenta di implementare la stessa strategia in mercati diversi con schemi commissionali e condizioni commerciali diverse senza dover modificare il codice di strategia principale.

Documentazione: backtrader.com/docu/sizers/sizers.html#practical-sizer-applicability

La Struttura di un Sizer

Un sizer è una sottoclasse di backtrader.Sizer. La sottoclasse ci consente di costruire un oggetto utilizzando la classe principale come base di partenza. L’oggetto eredita quindi tutte le caratteristiche e le funzionalità della classe principale senza dover copiare e incollare il codice nella nuova classe. E’ quindi possibile modificare solo le parti del codice, riscrivendo un metodo (una funzione di classe), un attributo (una variabile di classe) o aggiungendo qualcosa di nuovo. Tutte le parti rimaste intatte continueranno a funzionare nello stesso modo in cui erano state scritte nella classe genitore. Nel codice qui sotto è presente backtrader.Sizer scritto come bt.Sizer poiché generalmente utilizzo questo istruzione import backtrader as bt
class exampleSizer(bt.Sizer):
    params = (('size',1),)
    def _getsizing(self, comminfo, cash, data, isbuy):
        return self.p.size
Il codice precedente contiene un esempio di ridimensionamento nella sua forma più semplice. Questo ci permetterà di suddividere ed analizzare le componenti chiave di un sizer.

La tupla "params"

I Sizer, proprio come le strategie e gli indicatori possono contenere una tupla di parametri. Avere un set di parametri può offrire una certa flessibilità durante il caricamento del sizer in cerebro e fornire i dati al sizer, che altrimenti non sarebbero disponibili.

_getsizing()

Successivamente abbiamo il metodo _getsizing (). Questo metodo viene chiamato ogni volta che una strategia effettua una chiamata self.buy() o self.sell() senza indicare la dimensione dell’ordine. Il metodo _getsizing() prevede una serie di parametri, provenienti dal framework Backtrader. Questi sono:
  • comminfo: fornisce l’accesso a vari metodi che permettono di conoscere i dati del iano commissionale previsto dal broker. Ciò consente di valutare tutte le commissioni relative al singolo trade prima di decidere la dimensione. Nella seconda parte di questo tutorial si descrive comminfo in modo più dettagliato.
  • cash: fornisce la quantità di denaro disponibile sul conto.
  • data: fornisce l’accesso al feed dei dati. Ad esempio, tramite questo parametro possiamo accedere all’ultimo prezzo di chiusura.
  • isbuy: è un valore booleano (Vero / Falso) che identifica se l’ordine è un ordine di acquisto. Se è falso, allora l’ordine è un ordine di vendita.

Strategy e Broker

Ci sono inoltre le due classi accessibili, ma non visibili nel codice precedente, self.strategy e self.broker. Con questi due oggetti si ha praticamente accesso a tutto il necessario per creare complessi algoritmi di dimensionamento. Da sottolineare però, nel caso si eseguono calcoli basati su attributi di strategia, è nessario assicurarsi che siano attributi / variabili standard nel framework. In altre parole, attributi disponibili per tutte le strategie (anziché attributi personalizzati, aggiunti al codice per proprio conto). In caso contrario si rinuncia alla portabilità del sizer poiché questo funzionerà solo con la strategia dove codificato quello specifico attributo.

Non dimenticarti di restituire qualcosa

Infine, è necessario ricordarsi di restituire un valore alla fine del calcolo. Se lo si dimentica, la strategia non effettuerà alcun ordine.

Il Codice

Il seguente codice contiene tre esempi di Sizer. Il primo prevede dimensioni fisse, analogo a quanto mostrato nel blocco precedente. Il secondo è un esempio di sizer che stampa tutti i parametri del metodo _getsizing () ad eccezione di comminfo (che vedremo più dettagliatamente in seguito). L’esempio finale fornisce l’implementazione di un pratico ridimensionamento per limitare le dimensioni di un trade a una percentuale della liquidità totale del conto. Questo è un comune algoritmo di position sizing che molte strategie utilizzano per limitare il rischio.

import backtrader as bt
from datetime import datetime
import math


class exampleSizer(bt.Sizer):
    params = (('size',1),)
    def _getsizing(self, comminfo, cash, data, isbuy):
        return self.p.size

class printSizingParams(bt.Sizer):
    '''
    Prints the sizing parameters and values returned from class methods.
    '''
    def _getsizing(self, comminfo, cash, data, isbuy):
        #Strategy Method example
        pos = self.strategy.getposition(data)
        #Broker Methods example
        acc_value = self.broker.getvalue()

        #Print results
        print('----------- SIZING INFO START -----------')
        print('--- Strategy method example')
        print(pos)
        print('--- Broker method example')
        print('Account Value: {}'.format(acc_value))
        print('--- Param Values')
        print('Cash: {}'.format(cash))
        print('isbuy??: {}'.format(isbuy))
        print('data[0]: {}'.format(data[0]))
        print('------------ SIZING INFO END------------')

        return 0

class maxRiskSizer(bt.Sizer):
    '''
    Returns the number of shares rounded down that can be purchased for the
    max rish tolerance
    '''
    params = (('risk', 0.03),)

    def __init__(self):
        if self.p.risk > 1 or self.p.risk < 0:
            raise ValueError('The risk parameter is a percentage which must be'
                'entered as a float. e.g. 0.5')

    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy == True:
            size = math.floor((cash * self.p.risk) / data[0])
        else:
            size = math.floor((cash * self.p.risk) / data[0]) * -1
        return size



class firstStrategy(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() else: if self.rsi > 70:
                self.close()

    def notify_trade(self, trade):
        if trade.justopened:
            print('----TRADE OPENED----')
            print('Size: {}'.format(trade.size))
        elif trade.isclosed:
            print('----TRADE CLOSED----')
            print('Profit, Gross {}, Net {}'.format(
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
        else:
            return


#Variable for our starting cash
startcash = 10000

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

#Add our strategy
cerebro.addstrategy(firstStrategy)

#Get Apple data from Yahoo Finance.
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2016,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 sizer
cerebro.addsizer(printSizingParams)

# Run over everything
cerebro.run()

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

#Print out the final result
print('----SUMMARY----')
print('Final Portfolio Value: ${}'.format(portvalue))
print('P/L: ${}'.format(pnl))

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

Spiegazione del codice

Prima di tutto, dato che abbiamo più esempi, vale la pena notare come passare da uno all’altro. Per cambiare il sizer in uso, è sufficiente modificare questa riga:
#add the sizer
cerebro.addsizer(exampleSizer, size=50)

In questa:

#add the sizer
cerebro.addsizer(printSizingParams)

O in questa:

#add the sizer
cerebro.addsizer(maxRiskSizer, risk=0.2)

import math

In questo codice ho incluso un modulo Python aggiuntivo. Il modulo math fornisce math.floor() che semplifica l’arrotondamento per difetto al numero più vicino. Con un algoritmo di rischio massimo, non si può mai arrotondare per eccesso perché può portarci potenzialmente oltre al nostro limite di rischio. L’arrotondamento per diffetto non potrà mai farlo. La documentazione ufficiale di Python per il modulo math è disponibile al seguente link: docs.python.org/3/library/math.html

exampleSizer ()
Il sizer di esempio è stato incluso solo per discutere l’anatomia di un sizer. Tuttavia, fornisce anche un esempio di un sizer che utilizza una dimensione fissa. Una versione quasi identica appare nella documentazione ufficiale di Backtrader: www.backtrader.com/docu/sizers/sizers.html#sizer-development

printSizingParams ()
Per verificare il codice ho incluso un semplice sizer che stampa il contenuto dei parametri del calibratore. Di solito vedere esattamente cosa viene restituito mi aiuta a capire esattamente cosa sta facendo il parametro e come posso usarlo. È doppiamente utile quando si ha problemi a leggere la documentazione. L’esecuzione del codice fornisce il seguente output:

Il primo parametro stampato dal codice è un esempio dei dati che possono essere consultati tramite la classe di strategia, usando self.strategy.getposition(). Si può subito notare che “pos” è un oggetto posizione anziché un semplice valore come “cash“. Allo stesso modo la variabile account value fornisce un esempio dei dati a cui è possibile accedere facilmente tramite self.broker(). Nota nel caso live trading, è indispensabile assicurarsi quali metodi live del broker prescelto sono supportari nella documentazione di Backtrader. Ho dovuto implementare alcune soluzioni alternative perchè alcuni metodi non erano disponibili in Backtrader durante il trading live con Oanda. maxRiskSizer maxRiskSizer() calcola semplicemente la posizione della dimensione massima che puoi assumere senza superare una determinata percentuale del capitale disponibile nel tuo account. La percentuale viene impostata tramite il parametro “risk” presente nella tupla dei parametri. La percentuale viene immessa come float tra 0 e 1 e il valore default è pari a 3%, ma può essere impostato su qualsiasi valore quando viene caricato in cerebro. Per quelli come voi che non sono maghi matematici, il simbolo *-1 nella seguente riga di codice modifica il valore della dimensione da positiva a negativa. Si ha bisogno di una dimensione negativa nel caso di un ordine di vendita.
size = math.floor ((cash * self.p.risk) / data [0]) * -1
​Attenzione a maxRiskSizer buy.sell() Se hai l’abitudine di chiudere una posizione con una dimensione fissa utilizzando buy.sell(), devi essere consapevole che l’uso di maxRiskSizer può far sì che le posizioni non vengano chiuse e causi entrate indesiderate. Questo perché i livelli di liquidità sono dinamiche, cioè si modificano quando i trade sono aperti e chiusi, quindi la precentuale x% di apertura è ora l’x% di un totale diverso. In altre parole, il valore dello strumento sta cambiando in modo tale che x% comporti una diversa quantità di azioni / contratti acquistati. La semplice soluzione è chiudere le posizioni con la fuzione ​self.close(). Questo calcolerà la dimensione corretta necessaria per chiudere completamente una posizione.

Ottimizzare le Strategie con BackTrader

Dopo aver creato una strategia di base ed averla analizzata, il prossimo passo consiste nell’ottimizzare questa strategia. L’ottimizzazione è un processo di verifica che assegna valori diversi per ogni parametro della strategia al fine di individuare quale set di valori (o configurazione) fornisce i migliori risultati, in termini di profitto. Da notare che non tutti i trader algoritmici concordano sul fatto che questo processo può portare a risultati migliori. Infatti è molto facile cadere nella trappola del sovradimensionamento dei dati (meglio noto come overfitting).

Perchè Ottimizzare?

La motivazione è che i mercati sono in continua evoluzione. Abbiamo mercati rialzisti, mercati ribassisti, periodi di inflazione, periodi di deflazione, tempi instabili e momenti di serenità. Se ciò non bastasse, strumenti diversi hanno ritmi diversi e mercati diversi hanno nature e comportamenti diversi. Ciò significa che i parametri per uno strumento in un mercato potrebbero non essere ottimali per un altro strumento in un altro mercato.

...ma attenzione all'OverFitting

Quando si ottimizzano le strategie, è necessario prestare la massima attenzione a non creare parametri che funzionino solo per in un determinato “momento nel tempo”. Può essere allettante ottenere i migliori risultati dall’ottimizzazione e quindi prevedere l’esecuzione live della strategia con tali parametri. Tuttavia, se il set di dati è limitato ad un breve periodo di tempo o copre solamente una determinata condizione di mercato, è possibile che i parametri siano ottimizzati solo quel specifico momento nel passato, quindi del tutto inutilizzabili nel futuro. Nella statistica o nel machine learning, infatti, un modello statistico o un algoritmo viene applicato ai dati di addestramento (training) in modo che possa essere utilizzato per fare previsioni nel futuro. L’overfitting si verifica quando il modello o l’algoritmo è troppo complesso per il set di dati preso in considerazione. In questo contesto, complesso significa che l’algoritmo è ottimizzato a tal punto da adattarsi (fit) solo a quei dati. L’overfitting provoca reazioni eccessive se applicato all’esterno dei dati di training. Nel nostro ambiente di backtesting si possono considerare i nostri dati storici di backtest come i dati di training e la nostra strategia come l’algoritmo.

Requisiti

Il codice in questo articolo fa seguito al codice sviluppato nel precedente articolo Backtrader: Primo Script e fa parte della serie introduttiva a BackTrader. Se è la prima volta che senti parlare di Backtrader e / o Python, ti suggerisco di iniziare dall’articolo Setup di base per Python e BackTrader

Il Codice

Il codice di questo tutorial è costruito su tre esempi. Ogni esempio sarà accompagnato da specifici commenti e output.

Parte 1° - Aggiungere i Parametri

Prima di poter ottimizzare il codice dobbiamo fornire alla strategia qualcosa da ottimizzare, cioè alcuni parametri modificabili. Se si osserva il codice del precedente articolo, si può notare come abbiamo impostato a 21 il parametro relativo al periodo del RSI. Questa è una codifica rigida cioè il parametro è valorizzato all’interno del codice e non può essere successivamente modificato. Per effettuare l’ottimizzazione è necessario rendere questo parametro configurabile quando si carica la strategia all’interno del motore cerebro.
import backtrader as bt
from datetime import datetime

class rsiStrategy(bt.Strategy):
    params = (
        ('period',21),
        )

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

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


#Variable for our starting cash
startcash = 10000

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

#Add our strategy
cerebro.addstrategy(rsiStrategy, period=14)

#Get Apple data from Yahoo Finance.
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2016,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)

# Run over everything
cerebro.run()

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

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

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

Spiegazione del codice

Prima di tutto, facciamo riferimento al codice nel nostro primo script. Questo permette facilmente di notare alcune modifiche. Di seguito sono riportati uno snippet del codice per dichiarare la classe e il metodo __init__() del primo script
class rsiStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)
Mentre in questo esempio, il codice è diventato:
class rsiStrategy(bt.Strategy):
    params = (
        ('period',21),
        )

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

In questo caso è stata aggiunta la tupla “params“, essa contiene altre tuple che sono utilizzate per dichiarare i parametri della strategia. Che cos’è una tupla? Una tupla è un elenco di elementi fissi che non possono essere cambiati o modificati. Nei linguaggi di programmazione è abitudine differenziare ciò che è immutabile (non modificabile), come le costanti, e ciò che è mutabile (che può essere modificato), come le variabili. All’interno della tupla dei parametri, si ha un parametro ("period", 21). Il primo elemento è una stringa che identifica il nome / riferimento per il parametro. Il secondo elemento è il valore predefinito per quel determinato parametro. Avere un valore predefinito significa che non è necessario specificare un parametro ogni volta che si esegue la strategia. Se non viene specificato nulla, la strategia verrà eseguita con il valore di default. Puoi inserire tutti i parametri che desideri nella tupla dei parametri. Assicurati solo di aggiungerli come tupla all’interno della tupla principale (nota come tupla nidificata). I parametri della strategia sono accessibili ovunque nella classe. sono infatti gestiti come qualsiasi altro attributo di classe (variabile). Nel metodo __init__() si accede a self.params.period e viene assegnato alla keyword period quando si aggiunge l’indicatore RSI.

Chiamata alla Strategia

cerebro.addstrategy(firstStrategy, period=14)
Il codice relativo all’aggiunta della strategia all’interno di cerebro è stato modificato in modo da poter specificare la keyword per il parametro. Come accennato in precedenza, questo è facoltativo. Richiamare la strategia in questo modo ci permetterà di ottimizzarla in un secondo momento.

NOTE:

Ci sono un paio di cose a cui prestare attenzione quando si aggiungono parametri. Il primo è che ogni tupla nell’elenco delle tuple necessita di una virgola alla fine . Se sei abituato a scrivere codice in Python, saprai che per gli oggetti list e dict, l’ultimo valore non dovrebbe avere la virgola finale.

Se si digita: (errato)

params = (
        ('period',21)
    )

Invece di: (corretto):

params = (
        ('period',21),
    )
Si ottiene un ValueError: ValueError: too many values to unpack (expected 2)   Inoltre, fai attenzione quando aggiungi i tuoi indicatori nel metodo __init__(). Se dimentichi di usare una keyword, puoi ottenere un TypeError. Se si digita: (errato)
def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, self.params.period)
Invece di: (corretto)
def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=self.params.period)

Si ottiene il seguente errore:

TypeError: __init__() takes 1 positional argument but 2 were given

 
Questo errore può creare molta confusione. Infatti abbiamo aggiunto l’indicatore nel metodo __init__() della strategia, ma in realtà l’errore si riferisce al metodo __init __ () dell’indicatore (classe indicators)! Si potrebbe perdere molto tempo ad eseguire il debug della cosa sbagliata.

Parte 2° - Ottimizzazione

Ora che siamo in grado di inizializzare la strategia con parametri differenti, ottimizzare il codice è piuttosto semplice. Tecnicamente dobbiamo solamente sostituire la linea cerebro.addstrategy() con:

#Add our strategy
cerebro.optstrategy(firstStrategy, period=range(14,21))
Quindi cerebro eseguirà la strategia per ogni periodo nell’intervallo indicato. Tuttavia, l’output non sarebbe utile. Se vogliamo essere in grado di vedere quale parametro ha le migliori prestazioni si dovrà aggiungere un nuovo metodo alla nostra strategia. Il codice completo è il seguente:
import backtrader as bt
from datetime import datetime

class rsiStrategy(bt.Strategy):
    params = (
        ('period',21),
        )

    def __init__(self):
        self.startcash = self.broker.getvalue()
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=self.params.period)

    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 stop(self):
        pnl = round(self.broker.getvalue() - self.startcash,2)
        print('RSI Period: {} Final PnL: {}'.format(
            self.params.period, pnl))

if __name__ == '__main__':
    #Variable for our starting cash
    startcash = 10000

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

    #Add our strategy
    cerebro.optstrategy(rsiStrategy, period=range(14,21))

    #Get Apple data from Yahoo Finance.
    data = bt.feeds.YahooFinanceData(
        dataname='AAPL',
        fromdate = datetime(2016,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)

    # Run over everything
    strats = cerebro.run()

Spiegazione del codice

I lettori attenti avranno sicuramente notato che ci state alcune cancellazioni, oltre al nuovo metodo (funzione) aggiunto alla strategia. Innanzitutto diamo un’occhiata al nuovo metodo:

    def stop(self):
        pnl = round(self.broker.getvalue() - self.startcash,2)
        print('RSI Period: {} Final PnL: {}'.format(
            self.params.period, pnl))

Backtrader eseguirà diversi cicli di backtesting, uno per ogni diverso valore dei parametri, prima di arrivare al termine dello script. Nell’esempio precedente, si ha come output il valore del portafoglio e il PnL (profitti e perdite) alla fine dello script. Questo significa che non si ha visibilità dei risultati dei singoli vedrai i risultati dei singoli backtest se lasciamo l’istruzione print() dopo la fine dell’esecuzione di cerebro. Di conseguenza, un metodo stop() viene aggiunto allo script. Questo metodo fa parte della classe base bt.Strategy e si sta semplicemente sovrascrivendo la logica al suo interno, dato che si eredita la bt.Strategy durante la creazione della classe della nostra strategia. Come suggerisce il nome, si richiama questo metodo quando la strategia si interrompe. Questo è l’ideale per restituire i profitti o le perdite finali al termine del test.

Plotting

Oltre a rimuovere l’istruzioni print() alla fine dello script, è stata rimossa anche la funzione di stampa dei grafici. Quando si effettua l’ottimizzazione, ti consiglio di non graficare l’output perchè, al momento della stesura di questo articolo, il framework prevede la creazione di un grafico alla fine di ogni ciclo della strategia. E’ quindi necessario chiudere manualmente il grafico prima dell’inizio del ciclio successivo. Se si ha molti parametri, questo può richiedere molto tempo e diventare fastidioso.

Risultati della 2° Parte

Quindi sembra che un periodo pari a 17 sia il valore ottimale per questo set di dati. È interessante notare come se il valore fosse diverso di sole 2 unità (un periodo di 19), i risultati sarebbero drasticamente diversi!

Parte 3 - Fare un ulteriore passo avanti

L’esempio precedente è funzionalmente corretto ma secondo me c’è un problema. I risultati sopra riportati non sono ordinati e si potrebbe aver esigenza di qualcosa di più della sola stampa dei risultati. Immagina di avere 3 parametri che possono produrre oltre a 100 combinazioni. Sarebbe piuttosto laborioso e soggetto a errori se si dovesse leggere le righe una per una. In questa parte, vedremo come accedere ai risultati dopo che cerebro avrà terminato la sua elaborazione.
import backtrader as bt
from datetime import datetime

class rsiStrategy(bt.Strategy):
    params = (
        ('period',21),
        )

    def __init__(self):
        self.startcash = self.broker.getvalue()
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=self.params.period)

    def next(self):
        if not self.position:
            if self.rsi < 30: 
self.buy(size=100)
elif self.rsi > 70: self.sell(size=100) if __name__ == '__main__': #Variable for our starting cash startcash = 10000 #Create an instance of cerebro cerebro = bt.Cerebro(optreturn=False) #Add our strategy cerebro.optstrategy(rsiStrategy, period=range(14,21)) #Get Apple data from Yahoo Finance. data = bt.feeds.YahooFinanceData( dataname='AAPL', fromdate = datetime(2016,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) # Run over everything opt_runs = cerebro.run() # Generate results list final_results_list = [] for run in opt_runs: for strategy in run: value = round(strategy.broker.get_value(),2) PnL = round(value - startcash,2) period = strategy.params.period final_results_list.append([period,PnL]) #Sort Results List by_period = sorted(final_results_list, key=lambda x: x[0]) by_PnL = sorted(final_results_list, key=lambda x: x[1], reverse=True) #Print results print('Results: Ordered by period:') for result in by_period: print('Period: {}, PnL: {}'.format(result[0], result[1])) print('Results: Ordered by Profit:') for result in by_PnL: print('Period: {}, PnL: {}'.format(result[0], result[1]))

Spiegazione del codice

In questo esempio, ci sono alcune modifiche al codice. Innanzitutto abbiamo rimosso il metodo stop() nell’ultimo esempio. Avremo accesso a tutti i valori di cui abbiamo bisogno solo dopo che lo script avrà terminato l’esecuzione. Un altro cambiamento che potrebbe essere poco visibile se si sta semplicemente copiando e incollando il codice è:
cerebro = bt.Cerebro(optreturn=False)
In questo caso abbiamo aggiunto un nuovo parametro all’inizializzazione di cerebro. Questo parametro modifica ciò che viene restituito da cerebro.run() alla fine dello script. In un normale script cerebro.run() restituisce oggetti completi della classe strategia. Questi oggetti sono creati a partite dal modello della classe rsiStrategy che abbiamo scritto nel codice. Gli oggetti Strategia permettono di accedere a tutti disponibili per cerebro durante il test (indicatori, dati, analizzatori, osservatori, ecc.) anche dopo il termine dell’esecuzione di cerebro. In questo modo si ha accesso a tutti i dati e i risultati. Tuttavia, durante l’ottimizzazione, cerebro.run() restituisce gli oggetti OptReturn come impostazione di default predefinita. Questi sono oggetti limitati dato che contengono solo i parametri e gli analizzatori, al fine di migliorare la velocità di ottimizzazione. Si presume che le metriche importanti necessarie per decidere quali parametri siano i migliori possano essere dedotte solo dagli analizzatori e dai parametri. Tuttavia, poiché gli esempi riportati in questo articolo hanno restituito il profitto finale, è opportuno mantenere questa convenzione anche nell’esempio finale. Per questo motivo, il parametro optreturn deve essere impostato su false poiché le informazioni del broker (per i profitti / perdite) non fanno parte di un analizzatore. Abbiamo bisogno di Cerebro per ricavare oggetti Strategia completi. Il resto del codice di interesse per questo esempio si verifica al termine dell’esecuzione di cerebro.

Ricavare i dati da un oggetto Strategia

# Run over everything
opt_runs = cerebro.run()

# Generate results list
final_results_list = []
for run in opt_runs:
    for strategy in run:
        value = round(strategy.broker.get_value(),2)
        PnL = round(value - startcash,2)
        period = strategy.params.period
        final_results_list.append([period,PnL])
Cerebro restituisce un elenco di oggetti Strategia per ciascun ciclo tramite la lista dei parametri. In questo esempio, esiste solo una strategia. Tuttavia, poiché viene restituito una lista nidificata (lista di liste), è necessario iterare l’oggetto restituito per due volte per ottenere le informazioni necessarie. Dopo aver ricavato i valori desiderati, questi possono essere aggiunti alla lista final_results_list. Questa lista può essere quindi ordinata come si desidera.
#Sort Results List
by_period = sorted(final_results_list, key=lambda x: x[0])
by_PnL = sorted(final_results_list, key=lambda x: x[1], reverse=True)
Se non conosci Python, questa parte potrebbe sembrare un po’ complessa. Anche final_results_list è una lista nidificata. Per ordinarla correttamente, dobbiamo fornire una chiave di ordinamento. È quindi necessario passare una funzione all’argomento della keyword key. Un lambda è una piccola funzione formata da una riga che ci consente di utilizzare la chiave di ordinamento. Per ulteriori informazioni, ho aggiunto alcuni link di riferimento per letture di approffondimento alla fine di questo articolo.

Risultati della 3° parte

Eccoci. Questo articolo è diventato molto più lungo di quanto mi aspettassi quando ho iniziato a scriverlo. Se sei riuscito a farcela fino a qui senza saltare, spero che il contenuto abbia fornito qualche consiglio e spunto operativo.

Letture di Approfondimento

Utilizzare gli Analyzers di Backtrader

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?!?

BackTrader: Primo Script

Dopo aver creato il nostro ambiente di sviluppo, come descritto in questo articolo, è il tempo di scrivere il nostro primo script.

Obiettivo

Questo articolo ha lo scopo di creare una semplice strategia basata sugli indicatori, mantenendo il codice il più semplice possibile. Cercherò di evitare alcuni concetti più avanzati presenti nella documentazione e in Python in generale. Ad esempio le linee come:
if __name__ == '__main__': 

non saranno inclusi poiché ritengo che per imparare è necessario che i principianti dovrebbero cercare di reperire le informazioni su internet ed inoltre non voglio distrarre il lettore dalle funzionalità strettamente legate alla strategia che si vuole implementare (anche se alcuni programmatori professionisti potrebbero deridere la qualità del codice).

Ambito di backtesting: Per motivi di semplicità, il tutorial non include le commissioni, lo spread o altre considerazioni di backtesting più avanzate come benchmarking, l’ottimizzazione e l’analisi dei trade. Ho anche scelto di escludere la stampa / log per mantenere il codice il più semplicepossibile. (Anche se penso che la stampa / log sia importante per il debug quando aumenta la complessità del codice!)

La Strategia

Al momento della stesura di questo articolo siamo in presenza di un lungo mercato rialzista. Quando si negoziano azioni in un tale mercato, è ragionevolmente pensare di andare long. La ragione di ciò è che si trovano sul lato giusto del momentum long alla base della dinamica ascendente.

Con questo in mente, divertiamoci un po ‘a implementare una strategia “solo long” che andrà long quando un semplice indicatore RSI giornaliero è ipervenduto e si mantiene la posizione fino a quando l’RSI non raggiungerà il livello di ipercomprato.

Entry

  • Quando RSI <30

Exit

  • Quando RSI> 70

Gestione del trade e dimensionamento delle posizioni

  • Non si implementa nessuna gestione del trade. Non è previsto nessun ridimensionamento in / out. Solo semplici acquisti e vendite con un’unica posizione aperta alla volta.
  • Per quanto riguarda le dimensioni della posizione, si semplifica le cose comprando / vendendo 100 azioni alla volta senza fare calcoli per vedere se abbiamo abbastanza liquidità per le dimensioni della posizione (se non abbiamo abbastanza liquidità, backtrader è abbastanza intelligente da rifiutare l’ordine)

Settaggio dell’indicatore

  • Periodo = 21
    Consente di utilizzare una finestra mobile più lunga rispetto ai 14 periodo standard. In teoria ciò dovrebbe tradursi nell’avere meno falsi segnali falsi e il prezzo dovrebbe scendere / aumentare di più prima di essere considerato ipercomprato / ipervenduto.
 

Requisiti

Questo script estrae i dati online da Yahoo. Penso che questo semplifichi le cose per un primo script in quanto non sarà scaricare ed utilizzare i propri dati. Tuttavia, per questo motivo, lo script richiede laa versione backtrader 1.9.49.116 o successiva a causa delle recenti modifiche nell’API di Yahoo.
Per verificare la versione di backtrader che stai utilizzando hai alcune opzioni. Per prima cosa puoi controllare il pip semplicemente digitando:

pip3 list

(Nota per gli utenti di Windows: invece di “pip3” potrebbe essere “pip” se è installata solo una versione di python)

 

Quindi basta cercare la voce “backtrader” nell’output del precedente comando. Gli utenti Linux e Mac possono rendere questo processo un po ‘più veloce eseguendo il piping dell’output su grep.

pip3 list | grep backtrader

Ho notato che su una delle mie macchine Linux, pip3 riportava una versione inferiore rispetto a quella effettivamente installata (non ho ancora capito perché). Quindi, se la versione segnalata non sembra corretta, puoi anche controllarla aprendo una shell python, importando backtrader e stampare la versione.

import backtrader as bt
print(bt.__version__)

Se non si utilizza la versione più recente, avviare un terminale (o il prompt dei comandi) e digitare i seguenti comandi:

pip3 install --upgrade backtrader


I risultati

Per vedere i risultati del test in un grafico bidimensionale, si può usare la libreria Python (esterna all’installazione standard) chiamata “Matplotlib”. Questo modulo è la libreria grafica defacto per molti scienziati, analisti e ricercatori che usano Python.

Assicurati di averlo installato aprendo un terminale o un prompt dei comandi e digitando:

pip3 install matplotlib


Il Codice

Lo script è composto da due parti principali. La prima parte si crea una nuova classe dove implementare tutta la logica della strategia. Nella seconda parte si configura l’ambiente di esecuzione dello script (come aggiungere il framework, prevedere una fonte dati per i test, ecc.). Nota importante: in questo esempio si utilizza i dati disponibili con l’API di Quandl. Con questa API sei limitato al numero di chiamate che puoi effettuare al giorno. Per avere accesso illimitato ai loro dati è sufficiente registrarsi gratuitamente nel loro sito e richiedere una API key ed aggiungi la keyword apikey alla chiamata ai dati di quandl in questo modo (www.quandl.com):
​data = bt.feeds.Quandl(
    dataname='F',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True,
    apikey="INSERT YOUR API KEY"
    )


Imports

​import backtrader as bt
from datetime import datetime

Il primo import dovrebbe essere abbastanza ovvia. Stiamo importando il framework backtrader. Inoltre, si importa il modulo datetime dalla libreria standard di Python. Questo è usato per impostare le date di inizio e fine del periodo di backtesting. Backtrader prevede di ricevere oggetti datetime durante la creazione di feed di dati. Per ulteriori informazioni sul modulo datetime, consultare la documentazione disponibile su:

docs.python.org/3/library/datetime.html

La strategia

class firstStrategy(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)
​Quando si crea una strategia in backtrader, si eredita molti metodi e attributi dalla classe base `bt.Strategy`. In parole più semplici, si sta fondamentalmente prendendo il template di una strategia che è stata scritta nel framework di backtest e aggiungendo la nostra logica sopra ad essa. Quando nella programmazione si usa l’ereditarietà, possiamo usare tutto il codice che è stato scritto nella strategia di base e sovrascrivere le parti che vogliamo cambiare. Ciò significa che non è necessario preoccuparsi di tutto il codice, dietro le quinte, che consente di interagire con i dati, gestire gli ordini, le notifiche dei broker e così via. E’ Il framework BackTrader che se ne occupa! La nuova classe della strategia può essere breve, pulita e di facile lettura. Tuttavia, se lo desideri, puoi scavare più a fondo e modificare il contenuto del nucleo della classe base. La nostra classe si chiama firstStrategy (piuttosto originale) e contiene il codice per l’inizializzazione della strategia e il metodo “next”. L’inizializzazione è solo una riga, vogliamo solo inizializzare un indicatore RSI dalla libreria di backtrader. Per fare ciò si aggiunge l’indicatore durante l’inizializzazione della strategia (__init__). Una volta impostato, backtrader si occupa di tenere traccia dei dati, calcolare i risultati e aggiungerli al grafico finale. Se non conosci la programmazione, è importante notare che puoi chiamare il tuo indicatore RSI come preferisci, ma devi prefissarlo con “self”. Ciò ti consentirà di accedervi da altri metodi (chiamati anche “funzioni” quando non stiamo parlando di una classe). In questo caso, abbiamo semplificato la lettura del codice e inizializzato l’indicatore come “self.rsi”. Il metodo next (funzione) viene chiamato ad ogni nuova barra o, in altre parole, ogni volta che viene ricevuta una nuova candela. È in questo metodo che si deve scrivere la logica “if this then that“. In questo esempio si controlla se siamo in posizione, dato che come prerequisito si prevede di effettuare un solo trade alla volta. Se non siamo in una posizione, allora si verifica il livello dell’indicatore RSI. Se è inferiore a 30, si acquista 100 azioni. Se è superiore non si fa nulla (Perché non abbiamo scritto alcun codice per ciò che accade quando l’RSI è sopra i 30 e non siamo in una posizione). Il secondo step della logica prevede le azioni da fare quando SIAMO già in una posizione. In questo caso si cerca un’opportunità di vendita. Se l’RSI supera i 70, si vende tutte le 100 azioni. Altrimenti, di nuovo non si fa nulla. Questo si ripete ogni volta che arriva una nuova candela (ogni giorno) fino a quando tutti i dati sono stati controllati.

Il Setup

#Variable for starting cash
startcash = 10000

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

#Add strategy
cerebro.addstrategy(firstStrategy)

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

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

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

Dopo aver scritto la strategia, è necessario implementare il setup e all’esecuzione. Questo in sostanza si riduce a:

  • Chiamare il motore backtrader (cerebro)
  • Ottenere i dati storici e caricarli nel motore.
  • Impostazione della quantità di denaro che dobbiamo negoziare (startcash)
  • Caricare la strategia nel motore
  • Esecuzione del test
  • Monitorare i risultati

Alcune cose da sottolineare:

  • Durante la stampa, si utilizza l’argomento con keyword style = candlestick, se non lo si utilizza si ottiene un grafico a linee del prezzo di chiusura.
  • Durante l’inizializzazione del feed dei dati si richiama il modulo datetime, come sottolineato in precedenza. Stiamo passando un oggetto datetime agli argomenti della keyword fromdate e todate. Infine la keyword buffer indica che backtrader eseguirà il buffer di tutti i dati richiesti prima dell’inizio dell’analisi.

Eseguire lo script

Presumo che tu abbia svolto le lezioni di base su Python, come menzionato nel mio precedente post. Quindi copia il codice, aggiungi le parti basi del linguaggio, avvia lo script e controlla i risultati.

I Risultati

2 operazioni in profitto. Tasso di vincita del 100%. Saremo ricchi!

 

Tornando ad un tono più serio, prendi questi risultati con le pinze. Non abbiamo confrontato i risultati con un benchmark o una semplice strategia di “buy & hold”. Non abbiamo aggiunto commissioni e slippage, non disponiamo di dati di test a lungo termine sufficienti per convalidare la strategia. Un test per un titolo azionario in solo anno non dovrebbe riempirci di fiducia, ma almeno abbiamo creato una base per fare ricerche ulteriori.

Setup di base per Python e BackTrader

In questo post, diamo un’occhiata a come scaricare Python, dove recuperare i migliori tutorial introduttivi su Python, installare il framework BackTrader e infine verificare che sei in grado di accedere al framework all’interno di Python. Se sei ancora indeciso quale framework di backtesting o linguaggio di programmazione usare, puoi consultare l’articolo sui linguaggi di programmazione e gli ambienti di backtesting.

Nota: Questo post non va oltre il semplice settaggio di un ambiente di sviluppo per Python. Puoi quindi saltare questo articolo se hai già esperienza nell’uso di Python e pip.

Installare Python

Linux:

Se sei un utente Linux, sei molto fortunato. Molto probabilmente Python è già installato. In effetti, potrei scrivere cose superflue poiché non conosco nessun utente Linux che non sia già a conoscenza e / o abbia già armeggiato con Python in passato.

Mac OS:

Ce l’hai, ma non lo usi… La versione di Python installata con il sistema operativo è destinata esclusivamente al sistema. Alcune persone lo chiamano “system python”. Perché non dovresti usarlo? Da sottolineare che questa versione di Python è la vecchia versione python 2 mentre la maggior parte dei tutorial su questo sito è codificato con python 3. Un altro problema è che l’aggiornamento di Mac OS può potenziale per interrompere alcuni degli aggiornamenti installati sulla versione di sistema di python. Inoltre, alcuni pacchetti sono difficili da aggiornare a causa delle modifiche apportate da Apple nella versione python del sistema. Infine, il sistem python è disponibile solo tramite il terminale. Non c’è alcuna voce nella cartella delle applicazioni e nessun programma di avvio su cui fare clic.

Quindi, come puoi immaginare, ti consiglio di scaricare e installare una nuova versione di Python. Dopo aver scaricato e installato Python, avrai una cartella Python 3.x nella cartella delle applicazioni.
Questa ha un IDE (un ambiente di sviluppo) per scrivere ed eseguire il codice Python. Inoltre viene anche installato un launcher python che essenzialmente consente di avviare (eseguire) script Python facendo doppio clic su di essi.

Quindi iniziamo:

Per prima cosa vai alla pagina dei download sul sito Web python.org facendo clic qui.

Quindi scegli di installare l’ultima versione di Python 3. (secondo link dall’alto)

Dove aver scarico l’installer, è sufficiente seguire i classici steps per l’installazione di un software sul MAC.

Per verificare se Python è stato installato correttamente è sufficiente:

  1. Aprire un Terminale (se non sai dove si trova l’applicazione Terminale, puoi aprire una finistra di ricerca e digitare “terminal”)
  2. Digitare python3 nel termnale e premere Enter.

Si dovrà ottenere un risultato simile al seguente:

Per uscire dalla shell di python è necessario digitare quit() e premere Enter. (Questa è una funzione “built-in” per uscire dalla shell.)

Windows:

Windows non prevede nessuna versione di Python installata per default. Rispetto al Mac, questo rende le cose più semplici perchè non ci si deve preoccupare di un “python di sistema”. Come sopra, si inizia dalla pagina dei download di Python, facendo clic qui.

Una volta caricata la pagina, si seleziona di nuovo il secondo collegamento dall’alto, l’ultima versione di Python 3. Sembra identico allo precedente screenshot per Mac OS tranne che la scritta “Mac OS X” è sostituita con “Windows” (come ci si aspetterebbe!).

Dopo aver scaricato il programma di installazione, seguire i soliti passaggi per l’installazione per i software in ambiente Windows. Nella prima schermata di installazione è necessario assicurasi di selezionare:

  1. Installa per tutti gli utenti
  2. Aggiungi Python 3.x al PATH.
Per verificare che python sia stato installato correttamente, è sufficiente aprire un prompt dei comandi e digitare python. Da notare che non è necessario specificare ‘python3’ poiché su Windows non è possibile installate contemporaneamente diverse versioni di Python. L’output che si dovrebbe ottenere è lo stesso di quello per il Mac OS.
Per uscire dalla shell di python si deve digitare quit() e premere Enter. Questa è la funzione “built-in” per uscire dalla shell…

Primi passi con Python

Una volta installato Python 3, ti consiglio di imparare alcune nozioni di base in modo da comprendere completamente la logica alla base di BackTrader. Sia che tu preferisca imparare con un libro, sul Web o sporcarti le mani nei documenti ufficiali, hai  moltissime opzioni per apprendere tutte le funzionalità di questo linguaggio. Ho verificato la bontà dei contenuti che consiglio di seguito.

Gran parte di ciò che faremo nel trading si riduce al construtto “If this happens then do that“. Almeno all’inizio lLe competenze di base dovrebbero essere sufficienti, quindi non pensare di dover trascorrere mesi a studiare. Credo fortemente che il modo migliore per imparare le cose sia studiarle man mano che si va avanti con la sperimentazione e implementazione. Se passi troppo tempo a fare tutorial su progetti che non ti interessano, è facile annoiarsi e sentirsi frustrati quindi si rimarrà inevitabilmente bloccati.

Alcune concetti basi da imparare sono:

  • Assegnare variabili
  • Operazioni di base
  • Dichiarazioni If / Else
  • I cicli loop
  • Scrivere funzioni
  • Le basi delle Classi

Sul Web – Python Programming

https://pythonprogramming.net/introduction-to-python-programming/

​Questo sito offre una serie di eccellenti tuturial su una vasta gamma di argomenti. Meglio ancora, Harrison (l’autore) fornisce video e commenti ad ogni lezione della serie. Probabilmente il miglior sito sulla rete relativo alla programmazione in Python.

Purtroppo non ho trovato nulla di paragonabile (in termini di quantità e qualità) in italiano.

Ecco una copia del video di Harrison relativo all’introduzione della programmazione in Python:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

Libri – Automatizzare le cose noiose con Python

Automatizzare le cose noiose con Python. Programmazione pratica per principianti assoluti fornisce progetti di automazione rivolti al principiante. L’aspetto interessante è che tali progetti hanno impatto nella nostra vita quotidiana. Si lavora con la gestione dei file, i fogli Excel, le ricerche sul web ecc. Trovo che non ci sia nulla di più soddisfacente che ottenere un uso pratico e regolare da qualcosa che hai codificato.

Sporcarsi le mani - I docs ufficiali

Python fornisce un’ottima documentazione sul proprio sito ufficiale. All’inizio alcuni potrebbero trovare la documentazione un po ‘intimidatoria (specialmente quelli senza background informatico) ma è necessario attenersi ad essa. Una volta che ci si è abituati, questo diventa una preziosa risorsa.

https://docs.python.org/3/

Il framework BackTrader

Per installare pacchetti e framework di terze parti in Python utilizziamo uno strumento chiamato “pip” (pip3 in python3). Questo è lo strumento di gestione dei pacchetti in python, cioè si occupa del download, l’installazione, l’aggiornamento e la rimozione del codice sorgente richiesto dai pacchetti di terze parti. Apri un terminale o una console e digita il seguente comando: pip3 install backtrader Semplice! Dovresti vedere pip entrare in azione ed iniziare a scaricare / installare i pacchetti. Una volta completato puoi verificare l’installazione aprendo una shell python, digitando python3 nel terminale e premendo invio. Quindi digitare i seguenti comandi, una riga alla volta (premendo invio alla fine di ogni riga):
import backtrader
print(backtrader.__version__)

Dovresti vedere il numero della versione stampato a video:

Se ricevi degli errori, puoi pubblicali di seguito nella sezione dei commenti e possiamo dare loro un’occhiata (e quindi aggiornare questa pagina le secondo necessità)