Gestire il Position Sizing con BackTrader

Gestire il Position Sizing con BackTrader – Parte I

In questo articolo descriviamo come gestire il position sizing con BackTrader. Il dimensionamento delle posizioni è un argomento fondamentale nel mondo del trading algoritmico. 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 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è abbiamo bisogno dei Sizer?

Alcuni trader preferiscono 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. 

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. Possiamo 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.

Gestire il position sizing con backtrader

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 di trading algoritmico 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):
    '''
    Stampa i parametri di sizing e i valori restituiti dai metodi della classe
    '''
    def _getsizing(self, comminfo, cash, data, isbuy):
        # esempio metodi della classe Strategy
        pos = self.strategy.getposition(data)
        # esempio medoti della classe Broker
        acc_value = self.broker.getvalue()

        # stampa risultati
        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):
    '''
    Restituisce il numero approssimato per difetto di azioni che possono essere 
    acquistati con il massimo della tolleranza al rischio  
    '''
    params = (('risk', 0.03),)

    def __init__(self):
        if self.p.risk > 1 or self.p.risk < 0:
            raise ValueError('Il parametro Risk è una percentuale che deve essere valorizzata come un float. es. 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


# Capitale iniziale
startcash = 10000

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

# Aggiungo la strategia
cerebro.addstrategy(firstStrategy)

# Download i dati di Apple da Yahoo Finance.
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

# Aggiungo i dati di Apple a Cerebro
cerebro.adddata(data)

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

# Aggiungo il sizer
cerebro.addsizer(printSizingParams)

# Esecuzione del backtest
cerebro.run()

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

# Stampa dei risultati finali
print('----SUMMARY----')
print('Final Portfolio Value: ${}'.format(portvalue))
print('P/L: ${}'.format(pnl))

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

Descrizione del codice

Per gestire il position sizing con backtrader notiamo 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:

				
					# Aggiungo il sizer
cerebro.addsizer(exampleSizer, size=50)
				
			
In questa:
				
					# Aggiungo il sizer
cerebro.addsizer(printSizingParams)
				
			
O in questa:
				
					# Aggiungo il sizer
cerebro.addsizer(maxRiskSizer, risk=0.2)
				
			

Import math

In questo codice abbiamo 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ò  potenzialmente portarci 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.

Codice completo

In questo articolo abbiamo descritto come gestire il position sizing con BackTrader per il trading algoritmico. 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