In questo articolo descriviamo come gestire le candele Heikin Ashi con BackTrader per implementare strategie di trading algoritmico. Se hai mai visto un grafico Heikin Ashi su una piattaforma di trading, probabilmente sei rimasto sbalordito dalla bellezza delle candele. Sembrano catturare gli alti e bassi del movimento dei prezzi con lunghe fasce di verde o rosso. Meglio ancora, le barre Heikin Ashi sembrano davvero catturare punti di inversione di una tendenza.
Questi punti di inversione sono spesso preceduti da un lungo movimento nella direzione opposta. Di conseguenza, la maggior parte delle persone che hanno visto le candele Heikin Ashi hanno pensato ad una semplice strategia di acquisto sul verde e vendita sul la prima candela rossa. Sfortunatamente, la vita non è mai così semplice e in questo articolo descriviamo come lavorare con le candele Heikin Ashi con Backtrader, e dimostriamo che una buona entrata e uscita su un grafico Heikin-Ashi non sempre si traduce in trade profittevoli nel mondo reale.
Cosa sono le candele Heikin-Ashi
Come si può intuire dal nome, il concept delle candele Heikin Ashi è nato in Giappone. Anche le normali candele OHLC
ha avuto origine dalla terra del sol levante. Quindi, non è un caso che i nostri amici in oriente facciano le cose in modo diverso, le candele Heikin Ashi sono in realtà derivati della candele OHLC
. Si parte dai dati OHLC
e si applica una formula per eliminare parte del rumore dal movimento dei prezzi. Diamo un’occhiata a come viene calcolata una candela Heikin Ashi. (Fonte completa: Wikipedia)
- Chiusura = (apertura + massimo + minimo + chiusura) / 4
- Massimo = massimo maggiore, apertura o chiusura (qualunque sia il maggiore)
- Minimo = minimo minore, apertura o chiusura (qualunque sia il minore)
- Open = (apertura della barra precedente + chiusura della barra precedente) / 2
Da notare che le candele Heikin Ashi non ricalcolano semplicemente i dati di una barra OHLC
, hanno bisogno di due barre. In altre parole non possiamo calcolare con precisione una candela Heikin Ashi finché non abbiamo almeno 1 barra di dati storici. Un’altra importante differenza tra una candela standard e una Heikin Ashi è che la open
e close
delle candele non è la stessa (così come la high
e la low
). Potrebbe sembrare un’affermazione ovvia dopo aver esaminato la formula, ma ha alcune conseguenze sull’accuratezza del backtest e dei risultati.
Gestire le candele Heikin Ashi con Backtrader
Ora vediamo come lavorare con le candele Heikin Ashi in Backtrader per il backtesting di strategie di trading. Sfortunatamente non è così semplice come usare il comando cerebro.plot(style='candlestick')
descritto negli articoli precedenti.
Non possiamo usare il comando cerebro.plot(style='heikinashi')
perchè, anche se funzionasse , visualizzerebbe i dati in candele Heikin Ashi solo al termine del backtest (poiché il grafico è l’ultima cosa che facciamo). Affinché le candele Heikin Ashi influenzino la logica della strategia, dobbiamo disporre della versione Heikin-Ashi dei dati open
, high
, low
e close
disponibili durante il test. Per fare ciò, dobbiamo utilizzare il Data Filter
di Backtrader per creare dati Heikin Ashi dalla sorgente dei dati in input. (La documentazione ufficiale sui filtri è disponibile su https://www.backtrader.com/docu/filters) Un filtro di dati elabora i dati prima che vengano inseriti in cerebro. Un caso d’uso comune consiste nel filtrare eventuali barre presenti nel feed di dati che sono fuori dall’orario/ sessione che vogliamo considerare nel backtest. Le candele Heikin Ashi sono dati elaborati/modificati dal filtro. L’unica differenza è che si rimuove nulla.
Codice di Esempio
Il seguente codice mostra un esempio per aggiungere un filtro dati Heikin Ashi a una strategia.
import backtrader as bt
from datetime import datetime
# Creare un istanza di cerebro
cerebro = bt.Cerebro()
# Aggiungere una strategia
cerebro.addstrategy(bt.Strategy)
data = bt.feeds.Quandl(
dataname='TSLA',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True,
)
# Filtro sui dati
data.addfilter(bt.filters.HeikinAshi(data))
# Aggiungere i dati a Cerebro
cerebro.adddata(data)
# Esecuzione del backtest
cerebro.run()
# Grafico dei risultati finali
cerebro.plot(style='candlestick')
Nota importante: i dati in questo tutorial utilizzano 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 registrati per ottenere una chiave API e quindi aggiungere l’attributo apikey
alla chiamata dei dati Quandl in questo modo:
data = bt.feeds.Quandl(
dataname='F',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True,
apikey="INSERT YOUR API KEY"
)
Fonte: https://www.quandl.com/?modal=register
Risultati
L’esecuzione del codice precedente produce un grafico simle al seguente:
I dati Heikin Ashi non sono prezzi reali
Ora possiamo gestire le candele Heikin Ashi sul grafico, ma l’esecuzione di una strategia su questi dati produce aspettative irrealistiche. Come abbiamo introdotto nei paragrafi precedenti, i prezzi delle candele Heikin Ashi non sono prezzi reali. Quindi i prezzi di entrata e di uscita sono diversi da quelli con le candele reali. Per complicare ulteriormente le cose, anche i massimi e i minimi sono diversi, il che può portare a risultati completamente diversi quando si utilizzano i trailing stop. Puoi finire per essere in uno scambio per un periodo di tempo completamente diverso. Per vederlo di persona, eseguire il codice di esempio riportato di seguito.
Codice
import backtrader as bt
from datetime import datetime
import math
import argparse
def parse_args():
parser = argparse.ArgumentParser(description='Ngyuen: Price/Volume Anomaly Detection Strategy')
parser.add_argument('--heikin',
action ='store_true',
help=('Add Heikin Data Filter'))
parser.add_argument('--debug',
action ='store_true',
help=('Print Debugs'))
return parser.parse_args()
class HeikinStrategy(bt.Strategy):
params = (('debug', False),)
def next(self):
bar = len(self)
if bar == 10:
self.buy(size=100)
if bar == 50:
self.close()
if self.p.debug:
print('---------------------------- NEXT ----------------------------------')
print("1: Data Name: {}".format(self.data._name))
print("2: Bar Num: {}".format(len(self.data)))
print("3: Current date: {}".format(self.data.datetime.datetime()))
print('4: Open: {}'.format(self.data.open[0]))
print('5: High: {}'.format(self.data.high[0]))
print('6: Low: {}'.format(self.data.low[0]))
print('7: Close: {}'.format(self.data.close[0]))
print('8: Volume: {}'.format(self.data.volume[0]))
print('--------------------------------------------------------------------')
def notify_trade(self,trade):
if trade.isclosed:
dt = self.data.datetime.date()
print('---------------------------- TRADE ---------------------------------')
print("1: Data Name: {}".format(trade.data._name))
print("2: Bar Num: {}".format(len(trade.data)))
print("3: Current date: {}".format(dt))
print('4: Status: Trade Complete')
print('5: Ref: {}'.format(trade.ref))
print('6: PnL: {}'.format(round(trade.pnl,2)))
print('--------------------------------------------------------------------')
args = parse_args()
# Capitale iniziale
startcash = 10000
# Creare un istanza di cerebro
cerebro = bt.Cerebro()
# Aggiungere una strategia
cerebro.addstrategy(HeikinStrategy, debug=args.debug)
data = bt.feeds.Quandl(
dataname='F',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True
)
# Filtro sui dati
if args.heikin:
data.addfilter(bt.filters.HeikinAshi(data))
# Aggiungere i dati a Cerebro
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(round(portvalue,2)))
print('P/L: ${}'.format(round(pnl,2)))
# Grafico dei risultati finali
cerebro.plot(style='candlestick')
Il codice precedente può essere eseguito in due modalità. La prima modalità usa i prezzi reali e la seconda i prezzi Heikin Ashi (selezionando il parametro --heikin
durante l’esecuzione dello script). La strategia stessa è progettata per aprire e chiudere la posizione a delle barre arbitrarie. In questo modo possiamo verificare le aspettative irrealistiche. Immagina che due trader e una scimmia stanno operando. Un trader sta guardando un grafico Heikin Ashi e l’altro sta guardando un grafico normale e la scimmia sta guardando una banana. Se tutti aprono e chiudono un’operazione esattamente nello stesso momento, ci aspettiamo che tutti abbiano lo stesso PnL. Sarebbe un’aspettativa realistica del risultato. Tuttavia, come dimostrato in questo script, nonostante le operazioni sono aperte e chiuse negli stessi momenti otteniamo risultati diversi. Questo accade perché Backtrader non usa i dati reali per il calcolo del PnL. Non ha idea di cosa sia “reale”, calcola solo il PnL da qualsiasi feed di dati sta usando.
Risultati
Il primo risultato usa i dati reali:
---------------------------- TRADE ---------------------------------
1: Data Name: F
2: Bar Num: 51
3: Current date: 2016-03-16
4: Status: Trade Complete
5: Ref: 1
6: PnL: 133.31
--------------------------------------------------------------------
Final Portfolio Value: $10133.31
P/L: $133.31
Il secondo risultato usa le candele Heikin Ashi:
---------------------------- TRADE ---------------------------------
1: Data Name: F
2: Bar Num: 51
3: Current date: 2016-03-16
4: Status: Trade Complete
5: Ref: 1
6: PnL: 136.17
--------------------------------------------------------------------
Final Portfolio Value: $10136.17
P/L: $136.17
Notiamo che abbiamo ottenuto un profitto extra di $ 3 dollari con le candele Heikin Ashi. Il problema è che i 3 dollari non sono mai esistiti! Abbiamo aperto e chiuso i trade esattamente nello stesso momento.
Backtest di una strategia
Per vedere come gestire le candele Heikin Ashi con backtrader facciamo un ulteriore passo avanti nell’esempio e implementiamo la strategia menzionata all’inizio del l’articolo. Ricapitolando, vogliamo comprare sulle candele verdi e vendere sulle candele rosse. Con questo esempio vogliamo descrivere quanto possono essere imprecisi i risultati se non configuriamo correttamente Backtrader.
Codice
import backtrader as bt
from datetime import datetime
import math
import argparse
def parse_args():
parser = argparse.ArgumentParser(description='Ngyuen: Price/Volume Anomaly Detection Strategy')
parser.add_argument('--heikin',
action ='store_true',
help=('Add Heikin Data Filter'))
parser.add_argument('--debug',
action ='store_true',
help=('Print Debugs'))
return parser.parse_args()
class HeikinStrategy(bt.Strategy):
params = (('debug', False),)
def next(self):
if not self.position:
if self.data.close[0] > self.data.open[0]: # Heikin-Ashi Verde
self.buy(size=100)
elif self.data.close[0] < self.data.open[0]: # Heikin-Ashi Rossa
self.sell(size=100)
else:
pass # Chiusura neutrale
else:
if self.position.size > 0 and self.data.close[0] < self.data.open[0]: # Heikin-Ashi Rossa
self.sell(size=200) # Posizione Reverse
elif self.position.size < 0 and self.data.close[0] > self.data.open[0]: # Heikin-Ashi Verde
self.buy(size=200) # Posizione Reverse
else:
pass
if self.p.debug:
print('---------------------------- NEXT ----------------------------------')
print("1: Data Name: {}".format(self.data._name))
print("2: Bar Num: {}".format(len(self.data)))
print("3: Current date: {}".format(self.data.datetime.datetime()))
print('4: Open: {}'.format(self.data.open[0]))
print('5: High: {}'.format(self.data.high[0]))
print('6: Low: {}'.format(self.data.low[0]))
print('7: Close: {}'.format(self.data.close[0]))
print('8: Volume: {}'.format(self.data.volume[0]))
print('--------------------------------------------------------------------')
def notify_trade(self,trade):
if trade.isclosed:
dt = self.data.datetime.date()
print('---------------------------- TRADE ---------------------------------')
print("1: Data Name: {}".format(trade.data._name))
print("2: Bar Num: {}".format(len(trade.data)))
print("3: Current date: {}".format(dt))
print('4: Status: Trade Complete')
print('5: Ref: {}'.format(trade.ref))
print('6: PnL: {}'.format(round(trade.pnl,2)))
print('--------------------------------------------------------------------')
args = parse_args()
# Capitale iniziale
startcash = 10000
# Creare un istanza di cerebro
cerebro = bt.Cerebro()
# Aggiungere una strategia
cerebro.addstrategy(HeikinStrategy, debug=args.debug)
data = bt.feeds.Quandl(
dataname='F',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True
)
# Filtro sui dati
if args.heikin:
data.addfilter(bt.filters.HeikinAshi(data))
# Aggiungere i dati a Cerebro
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(round(portvalue,2)))
print('P/L: ${}'.format(round(pnl,2)))
# Grafico dei risultati finali
cerebro.plot(style='candlestick')
Il codice precedente è progettato per essere eseguito solamente con il flag --heikin
in modo da poter verificare il PnL se impostato in modo errato. Senza il flag --heikin
, la strategia funziona operando nei cambiamenti di colore dei normali dati OHLC
.
Risultati
L’esecuzione del codice con il flag --Heikin
produce alcuni profitti davvero interessanti. Oltre a guadagnare $1279, guardando il grafico è facile entusiasmarsi: vediamo che si acquista i minimi e si vende i massimi quasi costantemente.
A prima vista sembra una buona strategia. Ora dobbiamo verificare quali profitti otteniamo se acquistiamo e vendiamo esattamente negli stessi orari ma usando i prezzi reali. Per risolvere questo problema dobbiamo sviluppare un metodo per analizzare le candele Heikin Ashi ma per fare trading sui dati reali.
Heikin Ashi e i dati reali
Per affrontare questo problema dovremmo utilizzare solo i dati di Hiekin-Ashi per decidere quando entrare e uscire da una posizione. Quindi utilizziamo un secondo feed di dati contenente prezzi reali per entrare/uscire dalla posizione. Il codice seguente presenta solo una soluzione al problema. È abbastanza buono per portare a termine il lavoro, ma può sicuramente essere migliorato!
Codice
import backtrader as bt
from datetime import datetime
import math
import argparse
def parse_args():
parser = argparse.ArgumentParser(description='Ngyuen: Price/Volume Anomaly Detection Strategy')
parser.add_argument('--debug',
action ='store_true',
help=('Print Debugs'))
return parser.parse_args()
class HeikinStrategy(bt.Strategy):
params = (('debug', False),)
def __init__(self):
# Imposto alcuni puntatori/riferimenti
for i, d in enumerate(self.datas):
if d._name == 'Real':
self.real = d
elif d._name == 'Heikin':
self.hk = d
def next(self):
pos = self.getposition(self.real).size
if not pos:
if self.hk.close[0] > self.hk.open[0]: # Heikin-Ashi Verde
self.buy(self.real, size=100)
elif self.hk.close[0] < self.hk.open[0]: # Heikin-Ashi Rossa
self.sell(self.real, size=100)
else:
pass # Chiusura neutra
else:
if pos > 0 and self.hk.close[0] < self.hk.open[0]: # Heikin-Ashi Rossa
self.sell(self.real, size=200) # Posizione Reverse
elif pos < 0 and self.hk.close[0] > self.hk.open[0]: # Heikin-Ashi Verde
self.buy(self.real, size=200) # Posizione Reverse
else:
pass
if self.p.debug:
print('---------------------------- NEXT ----------------------------------')
print("1: Data Name: {}".format(data._name))
print("2: Bar Num: {}".format(len(data)))
print("3: Current date: {}".format(data.datetime.datetime()))
print('4: Open: {}'.format(data.open[0]))
print('5: High: {}'.format(data.high[0]))
print('6: Low: {}'.format(data.low[0]))
print('7: Close: {}'.format(data.close[0]))
print('8: Volume: {}'.format(data.volume[0]))
print('--------------------------------------------------------------------')
def notify_trade(self,trade):
if trade.isclosed:
dt = self.data.datetime.date()
print('---------------------------- TRADE ---------------------------------')
print("1: Data Name: {}".format(trade.data._name))
print("2: Bar Num: {}".format(len(trade.data)))
print("3: Current date: {}".format(dt))
print('4: Status: Trade Complete')
print('5: Ref: {}'.format(trade.ref))
print('6: PnL: {}'.format(round(trade.pnl,2)))
print('--------------------------------------------------------------------')
args = parse_args()
# Capitale iniziale
startcash = 10000
# Creare un istanza di cerebro
cerebro = bt.Cerebro()
# Aggiungere una strategia
cerebro.addstrategy(HeikinStrategy, debug=args.debug)
data = bt.feeds.Quandl(
dataname='F',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True
)
dataHK = bt.feeds.Quandl(
dataname='F',
fromdate = datetime(2016,1,1),
todate = datetime(2017,1,1),
buffered= True
)
# Filtro sui dati
dataHK.addfilter(bt.filters.HeikinAshi(dataHK))
# Aggiungere i dati a Cerebro
cerebro.adddata(data, name="Real")
cerebro.adddata(dataHK, name="Heikin")
# 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(round(portvalue,2)))
print('P/L: ${}'.format(round(pnl,2)))
# Grafico dei risultati finali
cerebro.plot(style='candlestick')
Commento al codice
Iniziamo ottenendo due versioni dello stesso feed di dati. Una volta che abbiamo due copie del feed di dati, ne filtriamo una, trasformandola in dati Heikin Ashi e quindi aggiungiamo entrambi i feed con nomi diversi. Durante l’esecuzione della strategia assegniamo un alias ai feed di dati durante la chiamata del metodo __init_()
, in modo da poter facilmente usare il datafeed corretto nelle successive chiamate di next()
.
Durante l’esecuzione del metodo next()
controlliamo per vedere se la candela Heikin-Ashi ha chiuso sopra l’apertura. In tal caso, abbiamo una candela verde e andiamo long. Se ha chiuso al ribasso, allora abbiamo una candela rossa e andiamo short. Da notare che quando entriamo in una posizione usiamo l’alias self.real
. Questo per garantire che l’operazione sia inserita nel feed di dati corretto e quindi siano usati i prezzi dei dati corretti/reali.
Risultati
E la performance nel mondo reale… $51,65! Abbastanza lontano dai $ 1279 che abbiamo ottenuto in precedenza. Speriamo che questo dimostri l’importanza di verifiche la correttezza del setup quando si gioca con le candele Heikin-Ashi altrimenti si avrà molti problemi quando si usa il codice nel trading live.
Non stiamo suggerendo che le candele Heikin-Ashi sono inutili o fuorvianti. Dopotutto, questa strategia non è stata progettata per avere prestazioni stellari e ha comunque restituito un profitto reale!
Codice completo
In questo articolo abbiamo descritto come gestire le candele Heikin Ashi con BackTrader per implementare 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