Gestire il Position Sizing con BackTrader

Gestire il Position Sizing con Backtrader – Parte II

In questo articolo descriviamo come gestire il position sizing con BackTrader per implementare la gestione del rischio nelle strategie di  trading algoritmico. Questo articolo continua la descrizione di come implementare un position sizing con Backtrader, introdotto nel precedente articolo.

Nel primo articolo abbiamo fornito un’introduzione ai concetti base, abbiamo esaminato l’anatomia di un sizer e abbiamo descritto alcuni semplici algoritmi di dimensionamento delle posizioni. In questo articolo, al contrario, approfondiamo l’argomento descrivendo la funzione comminfo e come può essere usata per migliorare gli algoritmi di position sizing.

Nota: dato che questo articolo si concentra principalmente sulla modalità di applicazione degli algoritmi di dimensionamento, può essere utile approfondire la gestione dello schema commissionale nell’articolo Schemi di commissione con backtrader.

Comminfo

Ogni volta che effettuiamo apriamo una posizione di acquisto o di vendita all’interno di una strategia, la classe “sizer” è passata all’istanza “comminfo” della strategia. In questo modo possiamo accedere agli attributi della commissione (variabili), tra cui commissionmult e margin, ai metodi (funzioni) interni di comminfo. Tramite l’accesso ai parametri e ai metodi della commissione il sizer può gestire e contabilizzare l’esatta commissione che sarebbe addebitata nel mondo reale per aprire e chiudere una posizione. Maggiori dettagli sono disponibili nella documentazione ufficiale di Backtrader: https://www.backtrader.com/docu/commission-schemes/commission-schemes

Nella documentazione possiamo notare che la maggior parte dei metodi di comminfo prevede un parametro size quando sono richiamati. Dato che stiamo cercando di calcolare la dimensione, si potrebbe obiettare che questi metodi non sono utili. Tuttavia, non sono da scartare. Possono essere usati per effettuare verifiche o aggiustamenti alla dimensione della posizione dopo aver valutato un range iniziale di possibili dimensioni da restituire.

Includere il costo delle commissioni

Se vogliamo controllare adeguatamente il rischio totale, tenere conto delle commissioni è essenziale. Pertanto, l’algoritmo di dimensionamento deve considerare il costo delle commissioni in modo da evitare dimensioni troppo piccole e costose. Ad esempio, se non consideriamo questi costi, possiamo facilmente calcolare e usare una dimensione che non può essere aperta perchè non abbiamo abbastanza denaro, che si traduce in operazioni mancate e talvolta un po’ di problemi!

Possiamo verificare quanto sopra eseguendo il codice seguente, con il sizer di rischio massimo implementato nella parte 1. Vediamo come aumentando le commissioni abbiamo operazioni non eseguite per capitale insufficiente.

				
					
import backtrader as bt
from datetime import datetime
import math
import argparse


def parse_args():
    parser = argparse.ArgumentParser(description='Sizers Testing')


    parser.add_argument('--commperc',
                        default=0.01,
                        type=float,
                        help='The percentage commission to apply to a trade')


    return parser.parse_args()

class maxRiskSizer(bt.Sizer):
    '''
    Restituisce il numero di azioni arrotondate per difetto che possono essere 
    acquistate con la massima tolleranza al rischio
    '''
    params = (('risk', 0.1),)

    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()
        elif self.rsi > 70:
            self.close()
        else:
            pass


args = parse_args()

# Variabile con il capitale iniziale
startcash = 10000

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

# Aggiungo la strategia
cerebro.addstrategy(firstStrategy)


data = bt.feeds.Quandl(
    dataname='F',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

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

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


cerebro.addsizer(maxRiskSizer)

cerebro.broker.setcommission(commission=args.commperc)

# Esecuzione del backtest
cerebro.run()

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

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

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

Nota importante: i dati in questo tutorial usano l’API  di Quandl. Con questa API  abbiamo un numero limitato di chiamate che  possiamo effettuare ogni giorno. Per avere accesso illimitato ai loro dati gratuiti, dobbiamo registrarci per ricevere una una chiave API e quindi aggiungere la parola chiave apikey ai parametri della chiamata ai dati quandl, come segue:

				
					
data = bt.feeds.Quandl(
    dataname='F',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True,
    apikey="INSERT YOUR API KEY"
    )
				
			

Eseguiamo quindi il codice con i seguenti comandi. Facciamo attenzione a sostituire il nome del file con quello dove salviamo il codice.

Esempio 1: python .\sizers-part2-perc-commission-too-high.py 

Esempio 2: python .\sizers-part2-perc-commission-too-high.py --commperc 10 

Vediamo che nel secondo esempio non sono effettuate operazioni. Questo perché non abbiamo abbastanza  liquidità nel conto per acquistare il numero di azioni restituite dal sizer quando abbiamo una commissione elevata. È un esempio estremo, ma questo può accadere anche in circostanze più normale. Ad esempio, quando cerchiamo di andare all-in o quando non abbiamo abbastanza liquidità a causa di posizioni  già aperti.

Sizers-Part2-Commission-Perc-Ok
Output dell'esempio 1. Abbastanza liqudità per coprire la commissione
Sizers-Part2-Commission-Perc-TooHigh
Output dell'esempio 2. NON abbiamo abbastanza liquidità per coprire la commissione

Dimensionare le posizioni con le commissioni

Per gestire il position sizing con Backtrader vediamo ora come estendere il maxRiskSizer per conteggiare le commissioni. Il maxRiskSizer è progettato per calcolare la dimensione massima della posizione che  possiamo assumere senza superare una certa percentuale del capitale disponibile nel conto. Può essere più performante se possiamo considera le commissioni in questo calcolo.

Codice di esempio

				
					
import backtrader as bt
from datetime import datetime
import math
import argparse


def parse_args():
    parser = argparse.ArgumentParser(description='Sizers Testing')


    parser.add_argument('--commperc',
                        default=0.01,
                        type=float,
                        help='The percentage commission to apply to a trade')


    return parser.parse_args()

class maxRiskSizer(bt.Sizer):
    '''
    Restituisce il numero di azioni arrotondate per difetto che possono essere
    acquistate con la massima tolleranza al rischio
    '''
    params = (('risk', 0.1),)

    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()
        elif self.rsi > 70:
            self.close()
        else:
            pass


args = parse_args()

# Variabile con il capitale iniziale
startcash = 10000

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

# Aggiungo la strategia
cerebro.addstrategy(firstStrategy)


data = bt.feeds.Quandl(
    dataname='F',
    fromdate = datetime(2016,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

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

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


cerebro.addsizer(maxRiskSizer)

cerebro.broker.setcommission(commission=args.commperc)

# Esecuzione del backtest
cerebro.run()

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

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

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

Commento

Il codice nell’esempio consente di eseguire lo stesso script in due modalità. Per fare ciò, usiamo il modulo argparse per specificare diverse opzioni durante l’esecuzione dello script (per ulteriori informazioni  si può leggere l’articolo:  Modificare i parametri della strategia con backtrader ). In questo caso, usiamo argparse per poter confrontare facilmente le differenze tra le dimensioni della posizione restituite dal sizer. Lo usiamo anche per regolare la commissione per influenzare ulteriormente le dimensioni restituite. Eseguiamo lo script con l’opzione “–help” per  avere dettagli del funzionamento di ciascuna opzione. Otteniamo il seguente output.

				
					
python3 .\sizers-part2-commission.py  --help
usage: sizers-part2-commission.py [-h] [--commsizer]
                                  [--commtype {Percentage,Fixed}]
                                  [--commission COMMISSION] [--risk RISK]
                                  [--debug]

Sizers Testing

optional arguments:
  -h, --help            show this help message and exit
  --commsizer           Use the Sizer that takes commissions into account
  --commtype {Percentage,Fixed}
                        The type of commission to apply to a trade
  --commission COMMISSION
                        The amount of commission to apply to a trade
  --risk RISK           The percentage of available cash to risk on a trade
  --debug               Print Sizer Debugs
				
			

Nota: per vedere come il sizer arriva al suo valore finale, si consiglia di eseguire lo script con il flag --debug. Il codice d’esempio contiene due sizer, uno schema di commissioni fisse e una strategia. I due sizer hanno lo stesso obiettivo, cioè fornire la dimensione massima che può essere acquistata entro una certa tolleranza al rischio.

I due sizer differiscono per il maxRiskSizerComms che è stato esteso per apportare un adeguamento al prezzo finale in base all’importo della commissione prevista per il round trip (per acquistare e successivamente vendere lo strumento). È anche in grado di modificare il suo algoritmo a seconda che lo schema di commissioni sia di tipo stock o futures (basato su una percentuale o una commissione fissa). (come descritto nell’articolo: Schemi di Commissioni con backtrader)

Il MaxRiskSizerComms

Il maxRiskSizerComms è il cuore di questo tutorial e dove avviene la magia. Inoltre, abbiamo incluso il sizer di base descritto nella parte 1  per gestire il position sizing con Backtrader, in modo da poter facilmente confrontare le differenze. maxRiskSizersComms effettua il calcolo della dimensione della posizione attraverso i seguenti passaggi

  1. Calcoliamo il massimo capitale che siamo disposti a rischiare
  2. Calcoliamo il  valore commission corrente.
  3. Successivamente, verifichiamo se lo schema delle commissioni è stocklike.
  4. In tal caso, consideriamo  un valore  percentuale commission. Pertanto, applichiamo una commissione basata su percentuale al prezzo. Da notare raddoppiamo la percentuale per considerare anche la commissione di vendita per la stessa dimensione.
  5. In caso contrario, consideriamo commission come un valore fisso. Pertanto, raddoppiamo la commissione (sempre per  considerare la vendita) e la sottraiamo dal totale che siamo disposti a rischiare.
  6. Infine, dividiamo l’importo che siamo disposti a rischiare per il prezzo (o il prezzo rettificato al punto 4) dello strumento.

Nota: quando lavoriamo con l’oggetto comminfo, dobbiamo usare il collegamento al parametro  p per accede agli attributi dello schema di commissione. Questo può essere difficile da capire. Ad esempio, per accedere al valore della commissione, possiamo usare comminfo.p.commission al posto di comminfo.commission. Nel secondo caso lo script genera il seguente AttributeError.

				
					AttributeError: 'CommInfoBase' object has no attribute 'commission'
				
			

Risultati

Supponendo che abbiamo salvato il codice di esempio come sizers-part2-commission.py, possiamo eseguire il codice con i comandi degli esempi seguenti. Nota: se stiamo usando Windows e abbiamo installato una sola versione di Python, dobbiamo sostituire python3 con solo python negli esempi seguenti.

Esempio 1

Il primo esempio esegue lo script usando il sizer che non tiene conto delle commissioni e usa i valori predefiniti per riskcommission. Da notare che quando commtype non è specificato, il tipo di commissione predefinito è stock-like
python3 .\sizers-part2-commission.py --debug
				
					------------- Sizer Debug --------------
Action: Buying
Price: 10.487417912902
Cash: 10000.0
Max Risk %: 0.01
Max Risk $: 100
Current Price: 10.487417912902
Size: 9
----------------------------------------
------------- Sizer Debug --------------
Action: Buying
Price: 11.587057110506
Cash: 10007.77084722578
Max Risk %: 0.01
Max Risk $: 100
Current Price: 11.587057110506
Size: 8
----------------------------------------
Final Portfolio Value: $10004.82
P/L: $4.82
				
			

Esempio 2

Il secondo esempio sostituisce il sizer con  maxRiskSizerComms. In questo modo si include la commissione.
python3 .\sizers-part2-commission.py --debug --commsizer
				
					------------- Sizer Debug --------------
Action: Buying
Price: 10.487417912902
Cash: 10000.0
Max Risk %: 0.01
Max Risk $: 100
Commission Adjusted Max Risk: N/A
Current Price: 10.487417912902
Commission: 0.05
Commission Adj Price (Round Trip): 11.536159704192201
Size: 8
----------------------------------------
------------- Sizer Debug --------------
Action: Buying
Price: 11.587057110506
Cash: 10006.907419756248
Max Risk %: 0.01
Max Risk $: 100
Commission Adjusted Max Risk: N/A
Current Price: 11.587057110506
Commission: 0.05
Commission Adj Price (Round Trip): 12.745762821556601
Size: 7
----------------------------------------
Final Portfolio Value: $10004.33
P/L: $4.33
				
			
Da notare che abbiamo più output di debug e la dimensione restituita è più piccola.

Esempio 3

L’ultimo esempio cambiamo il tipo di commissione in fissa e impostiamo una commissione di $5 per ogni operazione.
python3 .\sizers-part2-commission.py --debug --commsizer --commtype Fixed --commission 5
				
					------------- Sizer Debug --------------
Action: Buying
Price: 10.487417912902
Cash: 10000.0
Max Risk %: 0.01
Max Risk $: 100
Commission Adjusted Max Risk: 90.0
Current Price: 10.487417912902
Commission: 5.0
Commission Adj Price (Round Trip): N/A
Size: 8
----------------------------------------
------------- Sizer Debug --------------
Action: Buying
Price: 11.587057110506
Cash: 10006.033120578884
Max Risk %: 0.01
Max Risk $: 100
Commission Adjusted Max Risk: 90.0
Current Price: 11.587057110506
Commission: 5.0
Commission Adj Price (Round Trip): N/A
Size: 7
----------------------------------------
Final Portfolio Value: $10001.83
P/L: $1.83
				
			

Dall’output, possiamo vedere che il rischio massimo è stato regolato da 100 a 90 prima che fosse effettuato il calcolo della dimensione. Inoltre, il prezzo rettificato della commissione è ‘N/D’ poiché non è necessario effettuare un adeguamento del prezzo per una commissione fissa. Infine, notiamo che quando usiamo strategy.close() il sizer non viene chiamato.

Questo è prevedibile in quanto il framework non ha bisogno di consultare il sizer per sapere qual è l’attuale dimensione della posizione aperta. Prende solo la dimensione della posizione aperta corrente e la usa. Ciò semplifica lo sviluppo del sizer in quanto non è necessario codificare una condizione per la chiamata al metodo close().

Codice completo

In questo articolo abbiamo descritto come gestire il position sizing con BackTrader per implementare la gestione del rischio nelle strategie di  trading algoritmico. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/BackTrader

Scroll to Top