In questo articolo, descriviamo un frammento di codice che implementa un trailing stop con Quantconnect per il trading algoritmico. Al momento in cui scriviamo, l’API LEAN non supporta questo tipo di stop quindi, se vogliamo usare un trailing stop, dobbiamo crearlo con gli strumenti a nostra disposizione.
Trailing stop
Per implementare la base di un trailing stop su Quantconnect, dobbiamo semplicemente aggiornare frequentemente l’ordine di stop loss. In altre parole dobbiamo determinare se lo stop deve salire e, in tal caso, spostarlo più in alto con un aggiornamento del livello dell’ordine. Questa è una soluzione alternativa perché possiamo aggiornare il livello di stop solo in determinati momenti. Ad esempio, quando si richiama OnData()
. Inoltre, se facciamo trading su un timeframe, potrebbe non essere abbastanza frequente.
Tuttavia, questa “soluzione alternativa” ha molto potenziale perchè si possono creare alcuni trailing stop interessanti. Infatti possiamo controllare quando muovere il livello di stop loss, ad esempio se vogliamo seguire solamente le oscillazioni della price action oppure dare respiro allo stop se la volatilità è aumentata.
Trailing stop con Quantconnect
class ParticleResistanceCircuit(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 11, 20) # Data Inizio
self.SetCash(100000) # Capitale Iniziale
self.symbol = "SPY"
self.AddEquity(self.symbol, Resolution.Minute)
# distanza del trailing in $
self.trail_dist = 10
# dichiara un attributo che useremo per memorizzare il nostro stop loss ticket.
self.sl_order = None
# Dichiara un attributo che useremo per memorizzare l'ultimo livello
# di trailing stop usato. Lo useremo per decidere se spostare lo stop
self.last_trail_level = None
def OnOrderEvent(self, OrderEvent):
'''Event when the order is filled. Debug log the order fill. :OrderEvent:'''
if OrderEvent.FillQuantity == 0:
return
# otteniamo l'ordine eseguito
Order = self.Transactions.GetOrderById(OrderEvent.OrderId)
# Log dei dettagli dell'ordine eseguito
self.Log("ORDER NOTIFICATION >> {} >> Status: {} Symbol: {}. Quantity: "
"{}. Direction: {}. Fill Price {}".format(str(Order.Tag),
str(OrderEvent.Status),
str(OrderEvent.Symbol),
str(OrderEvent.FillQuantity),
str(OrderEvent.Direction),
str(OrderEvent.FillPrice)))
def OnData(self, data):
'''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
Arguments:
data: Slice object keyed by symbol containing the stock data
'''
# Dobbiamo verificare che il simbolo contengai dati prima di provare ad
# accedere a OHLC. In caso contrario, viene sollevata un'eccezione se i dati sono mancanti.
if data.ContainsKey(self.symbol) and data.HasData:
# Alias
# ------------------------------------------
holdings = self.Portfolio[self.symbol].Quantity
value = self.Portfolio.TotalPortfolioValue
cash = self.Portfolio.Cash
# Eccezione se non ci sono i dati
try:
O = round(data[self.symbol].Open, 2)
H = round(data[self.symbol].High, 2)
L = round(data[self.symbol].Low, 2)
C = round(data[self.symbol].Close, 2)
except AttributeError:
self.Log('>> {} >> Missing Data')
return
# Calcola il nostro livello SL di base. Viene utilizzato per l'inserimento iniziale.
# Lo useremo anche per il confronto con il livello del trailing precedente.
base_sl_level = round(C - self.trail_dist, 2)
# Log OHLC - Questo può essere utile per il debug per vedere dove si sta muovendo il prezzo
self.Log('>> {} >> ON DATA >> >> >> >> >> >>'.format(self.symbol))
self.Log('>> OHLC >> O:{} H:{} L:{} C:{}'.format(O, H, L, C))
self.Log('>> SL >> Base Level:{} Last Trail Level:{}'.format(base_sl_level, self.last_trail_level))
self.Log('>> Account >> Cash:{}, Val:{}, Holdings:{}'.format(cash, value, holdings))
if not self.Portfolio.Invested:
self.MarketOrder(self.symbol, 10, False, 'Long Entry')
self.sl_order = self.StopMarketOrder(self.symbol, -10, base_sl_level, 'SL')
self.last_trail_level = base_sl_level
else:
if base_sl_level > self.last_trail_level:
self.Log('>> Updating Trailing Stop >>')
# Aggiornamento ordine stoploss
update_order_fields = UpdateOrderFields()
update_order_fields.StopPrice = base_sl_level
self.sl_order.Update(update_order_fields)
# Log dell'ultimo sl_level
self.last_trail_level = base_sl_level
Commento
Dato che vogliamo modificare e aggiornare lo stop loss man mano che i prezzi si muovono dobbiamo creare un attributo (variabile) che memorizza il ticket dello stop loss. I ticket vengono restituiti dai metodi API (funzioni) che creano gli ordini e ne abbiamo bisogno per aggiornare l’ordine in un secondo momento. Per questo motivo, una delle prime cose che facciamo è prevedere una variabile dove memorizzare le informazioni relative all’ordine dello stop loss.
# dichiara un attributo che useremo per memorizzare il nostro stop loss ticket.
self.sl_order = None
Allo stesso tempo, creiamo anche un altro attributo per tracciare l’ultimo livello di trailing stop che usiamo ad ogni nuova barra per verificare se l’attuale livello di stop loss è superiore all’ultimo livello memorizzato. In caso affermativo, spostiamo lo stop loss verso l’alto. Sottolineiamo che questa logica può essere implementata tramite una finestra scorrevole. In questo codice non abbiamo usato una finestra scorrevole perché non abbiamo di accedere ad un valore precedente al più recente e possiamo seguirlo senza usare una finestra scorrevole.
I dati
Il resto delle logica è implementata nel metodo OnData()
. Per lo scopo di questo articolo prevediamo di entrare immediatamente in posizione e contemporaneamente inviamo un ordine di stop loss. Da notare che usiamo la variabile self.sl_order
.
if not self.Portfolio.Invested:
self.MarketOrder(self.symbol, 10, False, 'Long Entry')
self.sl_order = self.StopMarketOrder(self.symbol, -10, sl_level, 'SL')
self.last_trail_level = sl_level
Successivamente, ricaviamo il trailing stop calcolando semplicemente un valore di stop loss “base” come l’attuale prezzo close
meno il trail_dist
. Se il movimento del prezzo è nostro favore, il valore base sarà superiore al self.last_trail_level
. In questo caso, aggiorniamo il ticket con le righe:
# Aggiornamento ordine stoploss
update_order_fields = UpdateOrderFields()
update_order_fields.StopPrice = sl_level
self.sl_order.Update(update_order_fields)
Nota: per ulteriori informazioni relative alle funzionalità di UpdateOrderFields()
si può consultare la documentazione ufficiale: https://www.quantconnect.com/docs/algorithm-reference/trading-and-orders.
Dopo aver aggiornato l’ordine, allo stesso tempo aggiorniamo il self.last_trail_level
, che corrisponde al nuovo livello da superare per le successive barre. Infine, in caso contrario, ignoriamo questa barra e manteniamo l’attuale livello di stop loss. Supponendo che il prezzo non si muova più a nostro favore, questo livello rimane costante fino a quanto non viene raggiunto dal prezzo. Questo è proprio come funzionerebbe un vero trailing stop.
Verifica
Dal momento che non ci sono elementi interessanti sul grafico, dobbiamo rivolgerci ai log per verificare cosa sta succedendo. Eseguiamo il backtest e apriamo i log. Dovremmo ottenere qualcosa del genere:
Vediamo come il trailing stop inizia a 254,66$ e viene aggiornato nella barra successiva quando il prezzo si muove e si calcola il nuovo livello base viene pari a 254,99$ .
Codice completo
In questo articolo abbiamo descritto come implementare un trailing stop con Quantconnect 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/QuantConnect