ordini target e stop-loss con Backtrader

Ordini Target e Stop-Loss con Backtrader

In questo articolo descriviamo come usare gli ordini target e stop-loss con Backtrader per il trading algoritmico, come parte di una strategia quasi “all in” con le corrette dimensioni. Potrebbe sembrare abbastanza semplice sulla carta ma, quando valutiamo le posizioni, ci sono alcune opzioni a nostra disposizione. Se seguiamo la strada sbagliata, potrebbe incontrare molte criticità.

Prevediamo un position sizing quasi “all in” perchè andare “all in” non è semplice. Non è sufficiente dividere la liquidità disponibile per il prezzo corrente quando inviamo gli ordini. A meno di non usiamo la funzionalità cheat su open di Backtrader, la dimensione è calcolata  usando il prezzo di chiusura del feed di dati. Tuttavia mentre il prezzo di entrata è effettivamente determinato all’apertura della barra successiva. 

Durante questo periodo, il prezzo potrebbe facilmente aumentare o diminuire, lasciandoci senza abbastanza denaro per completare l’operazione. Pertanto, non possiamo andare all-in con il 100% della liquidità ma dobbiamo prevedere una piccola quantità per tenere conto di eventuali gap. A mio parere, questa è una buona abitudine da prevedere nel trading dal vivo dato che in real-time il prezzo è sempre in movimento ed è impossibile sapere dove sarà il prezzo nel momento in cui il tuo ordine viene eseguito dal broker.

La dimensione degli ordini

Il gap non è l’unico ostacolo alla nostra operativa che dobbiamo superare per raggiungere gli obiettivi di questo articolo. L’inversione di una posizione richiederà dimensioni completamente diverse rispetto all’apertura di una posizione da flat. Quando invertiamo una posizione dobbiamo prevedere di inviare le dimensioni sufficienti per chiudere la posizione aperta e anche ulteriori dimensioni per andare “all in” nella direzione opposta. Backtrader non implementa questa logica quando usiamo le funzioni self.buy() o self.sell(). Fortunatamente, Backtrader ha una funzione che può essere richiamata quando abbiamo bisogno di inserire questa logica di position sizing nella nostra strategia.

Dobbiamo considerare un ulteriore aspetto quando invertiamo una posizione e vogliamo creare uno stop loss o un take profit. In questo caso dobbiamo assicurarci che la nostra dimensione di stop loss o take profit non sia la stessa della dimensione della posizione invertita. Sarebbe troppo grande. Gli ordini stop loss e take profit devono uscire da una posizione e non invertirla.

Ottenere i dati di test

Prima di descrivere il codice, per eseguire gli esempi di questo articolo dobbiamo scaricare i seguenti dati di test: TestData. I dati forniti sono semplici dati artificiali creati in Excel ed esportati in CSV. Con i dati artificiali  possiamo sapere esattamente cosa sta accadendo e quando. Siamo in grado di controllare i movimenti dei prezzi secondo uno schema prevedibile in modo da semplificare i test e i calcoli. I dati di test presentano le seguenti modifiche principali:

  • Dal 1° gennaio al 1° maggio, il prezzo salirà e scenderà gradualmente a intervalli regolari di 10 barre
  • Successivamente, dal 1° maggio 2018 il prezzo diminuirà di $50 e avrà un gap di $10 ad ogni successiva apertura.
  • Il 28 settembre il prezzo tornerà alla normalità.
  • Quindi dal 28 dicembre il prezzo salirà di altri $ 50

La Strategia

Per capire come usare gli ordini target e stop-loss con Backtrader iniziamo con una semplice strategia che sarà la base per tutti gli altri esempi. La strategia è stata adattata ai dati del test in modo da operare solo in date specifiche. Con questo approccio possiamo sapere in anticipo cosa dovrebbe accadere. In altre parole, ci permetterà di concentrarci sulle funzionalità di Backtrader.

				
					
import backtrader as bt
from datetime import datetime

class TestStrategy(bt.Strategy):

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

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Non sarà colpito
        short_stop = self.data.close[0] + 50 # Non sarà colpito
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Il prezzo chiude a $100 e successivamente apre a $100
                # test base senza gap
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2018,12,27).date():
                # Il prezzo chiude a $100 e successivamente apre a $150
                # Inizio del test di inversione della prima posizione
                # Ancora flat al momento
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
            if date == datetime(2018,1,25).date():
                # Il prezzo chiude a $140 e successivamente apre a $140
                # Primo test di chiusura posizione - profitto di $40
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,1).date():
                # Il prezzo chiude a $190 e successivamente apre a $190
                # Prima posizione da invertire
                # Vendiamo da una posizione long con la dimensione desiderata
                # Adesso abbiamo una posizione short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Abbiamo una posizione short
                # Il prezzo chiude a $190 e successivamente apre a $190
                # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
                # Acquistiamo per invertire effettivamente una vendita
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,11).date():
                # Il prezzo chiude a $190 e apre a $190 nella candela successiva
                # Chiudere alla fine
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

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

# Aggiungere la strategia
cerebro.addstrategy(TestStrategy)

# Creare un Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 significa non usata
    )

# Aggiungere i dati
cerebro.adddata(data)

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

# Esecuzione del backtest
cerebro.run()

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

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

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

Notiamo che, nonostante il titolo di questo articolo, nel codice non ci sono chiamate al metodo target_order(). Questo metodo metodo è aggiunto successivamente con gli altri esempi. 

Nota: il codice prevede che i dati di test, scaricati dal link precedente, siano posizionati all’interno nella sotto cartella data della cartella dello script. Se vogliamo posizionare il csv nella stessa cartella dello script dobbiamo modificare la riga 160.

L’esecuzione di questa strategia base così com’è prevede una dimensione della posizione pari 1 per ogni trade. Il grafico finale dovrebbe assomigliare a questo:

Backtrader - Target-Orders-Base

Posizione in Percentuale

Quindi abbiamo una strategia di base dove i trade di acquisto e di vendita hanno una dimensione pari a 1. Passiamo Vediamo ora come far sì che la strategia vada quasi “all in”. Come descritto negli articoli precedenti su Backtrader,  abbiamo ha disposizione alcuni “sizer” già implementati nel framework tra cui il PercentSizer. Vediamo cosa succedere quando lo aggiungiamo alla nostra strategia.

Esempio di Gap con un Percentsizer

Vediamo ora come usare un sizer di backtrader basato sulla percentuale. In questo caso è utile mostrare un esempio della criticità che può verificarsi quando si va completamente “all in” e si ha un gap tra il prezzo di chiusura e quello di apertura. Il primo esempio mostra il rischio di avere trade non eseguiti perchè hanno una dimensione errata. Abbiamo semplicemente aggiunto un PercentSizer e impostato percents a 100.

				
					
import backtrader as bt
from datetime import datetime

class TestStrategy(bt.Strategy):

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

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Non sarà colpito
        short_stop = self.data.close[0] + 50 # Non sarà colpito
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Il prezzo chiude a $100 e successivamente apre a $100
                # test base senza gap
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2018,12,27).date():
                # Il prezzo chiude a $100 e successivamente apre a $150
                # Inizio del test di inversione della prima posizione
                # Ancora flat al momento
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
            if date == datetime(2018,1,25).date():
                # Il prezzo chiude a $140 e successivamente apre a $140
                # Primo test di chiusura posizione - profitto di $40
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,1).date():
                # Il prezzo chiude a $190 e successivamente apre a $190
                # Prima posizione da invertire
                # Vendiamo da una posizione long con la dimensione desiderata
                # Adesso abbiamo una posizione short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Abbiamo una posizione short
                # Il prezzo chiude a $190 e successivamente apre a $190
                # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
                # Acquistiamo per invertire effettivamente una vendita
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,11).date():
                # Il prezzo chiude a $190 e apre a $190 nella candela successiva
                # Chiudere alla fine
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

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

# Aggiungere la strategia
cerebro.addstrategy(TestStrategy)

# Creare un Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 significa non usata
    )

# Aggiungere i dati
cerebro.adddata(data)

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

# Aggiungere un sizer
cerebro.addsizer(bt.sizers.PercentSizer, percents=100)

# Esecuzione del backtest
cerebro.run()

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

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

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

Se eseguiamo lo script e confrontiamo i risultati con l’esempio precedente, noterai che l’ordine di entrata long del 28 dicembre 2018 non è stato completato a causa del gap del prezzo.

Backtrader - Gapping-Missed-Trade

Se riduciamo il parametro percents a 50, il trade riappare perchè abbiamo  abbastanza denaro nel conto per completare l’ordine.

Percent Sizer – Dimensione inversa

Nel seguente esempio per il  PercentSizer, abbiamo modificato alcune delle date di ingresso/uscita. Pertanto, assicurati di copiare il codice aggiornato. Abbiamo modificato il codice per evitare l’enorme gap e stabilire un livello percentuale più ragionevole. Naturalmente, il livello ragionevole dipende dal timeframe e dalla tipolgia di asset che stiamo analizzando. Ad esempio, se abbiamo un timeframe a 1 minuto per una coppia Forex, potremmo probabilmente andare quasi “all in”, riservando una quantità molto piccola per coprire il gap. Al contrario, se vogliamo operare su azioni con un timeframe giornaliero, il gap può essere molto più  ampio!

				
					
import backtrader as bt
from datetime import datetime

class TestStrategy(bt.Strategy):

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

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Non sarà colpito
        short_stop = self.data.close[0] + 50 # Non sarà colpito
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Il prezzo chiude a $100 e successivamente apre a $100
                # test base senza gap
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Il prezzo chiude a $100 e successivamente apre a $150
                # Inizio del test di inversione della prima posizione
                # Ancora flat al momento
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
            if date == datetime(2018,1,25).date():
                # Il prezzo chiude a $140 e successivamente apre a $140
                # Primo test di chiusura posizione - profitto di $40
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                # Il prezzo chiude a $190 e successivamente apre a $190
                # Prima posizione da invertire
                # Vendiamo da una posizione long con la dimensione desiderata
                # Adesso abbiamo una posizione short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # Abbiamo una posizione short
                # Il prezzo chiude a $190 e successivamente apre a $190
                # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
                # Acquistiamo per invertire effettivamente una vendita
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                # Il prezzo chiude a $190 e apre a $190 nella candela successiva
                # Chiudere alla fine
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

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

# Aggiungere la strategia
cerebro.addstrategy(TestStrategy)

# Creare un Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 significa non usata
    )

# Aggiungere i dati
cerebro.adddata(data)

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

# Aggiungere un sizer
cerebro.addsizer(bt.sizers.PercentSizer, percents=80)

# Esecuzione del backtest
cerebro.run()

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

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

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

Da notare che in questo esempio abbiamo impostato percents a 80. Se eseguiamo lo script e analizziamo l’output, possiamo vedere che la dimensione dell’ordine non è corretta quando si tenta di invertire una posizione.

Percent-Sizer-Short con Backtrader

Ordini Target

Abbiamo visto che la funzione PercentSizer integrata in Backetrare non soddisfa completamente i nostri obiettivi. Fortunatamente, scopriamo che Backtrader mette a disposizione molte funzionalità e opzioni se leggiamo attentamente la documentazione. Una di queste opzioni prevede l’uso degli Ordini Target. 

Un Target Order permette di specificare una dimensione, un valore o una percentuale di denaro da usare per la posizione finale. La differenza fondamentale con un normale ordine buy()sell() è la possibilità di regolare la dimensione, quindi se abbiamo una posizione aperta ci permette di chiudere la posizione esistente con la corretta dimensione target. La documentazione finale è disponibile al seguente link:  https://www.backtrader.com/docu/order_target/order_target.html.

Dopo aver letto la documentazione, l’ordine target più utile per i nostri  scopi è l’order_target_percent. Questo è anche il tipo di ordine target con il minor numero di informazioni/esempi nella documentazione. Se ti stai chiedendo come specificare una posizione short, è sufficiente specificare una percentuale target negativa. Potrebbe sembrare poco intuitivo scegliere un target usando meno il 90% del tuo denaro, ma in realtà segue la stessa convenzione per la dimensione e mantiene le cose coerenti.

L’esempio seguente è diviso in due parti. Per prima cosa vediamo cosa succede quando usiamo gli ordini target solamente per sostituire le chiamate self.buy()self.sell(). Infine descriviamo un esempio completo e funzionante.

Sostituzione di buy() e sell() con order_target_percent()

In questo primo esempio sostituiamo gli ordini a mercato buy() e sell() con una chiamata a order_target_percent() e verifichiamo  i risultati.

				
					

import backtrader as bt
from datetime import datetime

class TestStrategy(bt.Strategy):

    params = (('percents', 0.9),) # Float: 1 == 100%

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

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Non sarà colpito
        short_stop = self.data.close[0] + 50 # Non sarà colpito
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Il prezzo chiude a $100 e successivamente apre a $100
                # test base senza gap
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Il prezzo chiude a $100 e successivamente apre a $150
                # Inizio del test di inversione della prima posizione
                # Ancora flat al momento
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
            if date == datetime(2018,1,25).date():
                # Il prezzo chiude a $140 e successivamente apre a $140
                # Primo test di chiusura posizione - profitto di $40
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                # Il prezzo chiude a $190 e successivamente apre a $190
                # Prima posizione da invertire
                # Vendiamo da una posizione long con la dimensione desiderata
                # Adesso abbiamo una posizione short.
                sell_ord = self.order_target_percent(target=-self.p.percents, oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # Abbiamo una posizione short
                # Il prezzo chiude a $190 e successivamente apre a $190
                # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
                # Acquistiamo per invertire effettivamente una vendita
                buy_ord = self.order_target_percent(target=self.p.percents, oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                # Il prezzo chiude a $190 e apre a $190 nella candela successiva
                # Chiudere alla fine
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)


startcash = 10000

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

# Aggiungere la strategia
cerebro.addstrategy(TestStrategy)

# Creare un Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 significa non usata
    )

# Aggiungere i dati
cerebro.adddata(data)

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

# Esecuzione del backtest
cerebro.run()

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

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

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

Se guardiamo l’output possiamo notare la corretta inversione delle posizioni! Tuttavia, se verifichiamo l’output con più attenzione, possiamo vedere alcune criticità. La dimensione dello stop loss non è corretta! Se non usiamo un sizer, la dimensione predefinita di un ordine buy() o sell() è pari a 1. Quindi dobbiamo prevedere un’altra modifica per completare il codice.

Stop-Loss-Sizing con Backtrader

Esempio finale

Vediamo un esempio completo per gestire gli ordini target e stop-loss con Backtrader. In questo esempio dobbiamo solamente assicurarci di impostare correttamente la dimensione dello stop loss. Inizialmente, il problema può sembrare complesso perché dobbiamo essere flessibili e tenere conto di entrambi i tipi di operazioni, quando invertiamo la posizione e quando apriamo una posizione da flat (0). Tuttavia, esiste una soluzione semplice: stop_size = abs(sell_ord.size) - abs(self.position.size). Prendiamo semplicemente la dimensione dell’oggetto ordine restituito dalla funzione  order_target_percent() quando creiamo un ordine. Quindi ricaviamo da esso la dimensione della posizione corrente. Se siamo flat abbiamo 0 e quindi la dimensione sarà la stessa della dimensione dell’ingresso. Al contrario, se stiamo invertendo una posizione, deduciamo la dimensione della posizione corrente, che si tradurrà nella stessa dimensione della nuova posizione.

				
					
import backtrader as bt
from datetime import datetime

class TestStrategy(bt.Strategy):

    params = (('percents', 0.9),) # Float: 1 == 100%

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

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Non sarà colpito
        short_stop = self.data.close[0] + 50 # Non sarà colpito
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Il prezzo chiude a $100 e successivamente apre a $100
                # test base senza gap
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Il prezzo chiude a $100 e successivamente apre a $150
                # Inizio del test di inversione della prima posizione
                # Ancora flat al momento
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
            if date == datetime(2018,1,25).date():
                # Il prezzo chiude a $140 e successivamente apre a $140
                # Primo test di chiusura posizione - profitto di $40
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                # Il prezzo chiude a $190 e successivamente apre a $190
                # Prima posizione da invertire
                # Vendiamo da una posizione long con la dimensione desiderata
                # Adesso abbiamo una posizione short.
                sell_ord = self.order_target_percent(target=-self.p.percents, oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                stop_size = abs(sell_ord.size) - abs(self.position.size)
                self.sl_ord = self.buy(size=stop_size, exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # Abbiamo una posizione short
                # Il prezzo chiude a $190 e successivamente apre a $190
                # NOTA oco=self.sl_ord è necessatio per canellare gli stop loss già a mercato
                # Acquistiamo per invertire effettivamente una vendita
                buy_ord = self.order_target_percent(target=self.p.percents, oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                # Il prezzo chiude a $190 e apre a $190 nella candela successiva
                # Chiudere alla fine
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)


startcash = 10000

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

# Aggiungere la strategia
cerebro.addstrategy(TestStrategy)

# Creare un Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 significa non usata
    )

# Aggiungere i dati
cerebro.adddata(data)

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

# Esecuzione del backtest
cerebro.run()

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

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

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

Se esaminiamo l’output finale, possiamo vedere che la dimensione dello stop loss è impostata correttamente.

Target-Orders-Final backtrader

E sul grafico, possiamo vedere che tutte le posizioni sono state eseguite.

Backtrader - Target-Orders-Solutione-Finale

In questo articolo abbiamo visto come usare gli ordini target e stop loss (o take profit) con Backtrader per gestire correttamente le dimensioni delle posizioni e quindi evitare trade non eseguiti per capitale non sufficiente nel conto.

Codice completo

In questo articolo abbiamo descritto come usare gli ordini target e stop-loss 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

Torna in alto
Scroll to Top