In this article, we will provide a code snippet/example of how to implement a trailing stop on Quantconnect. At the time of writing, the LEAN API currently does not support this type of stop. Therefore, if we want to use a trailing stop, we need to craft it ourselves with the tools at our disposal.
Noice, how the trailing stop starts at $254.66 and is updated on the following bar when price moves and the new base level is calculated at $254.99.
Trailing Stops
To implement a basic trailing stop, all we need to do is update our stop-loss order frequently. This means determining whether the stop should move up and if so, just moving it ourselves with an order update. Admittedly, this is a bit of a workaround because you can only update your stop level at certain points in time. For example, whenOnData()
is called. Furthermore, If you are trading on a daily timeframe, that may not be frequent enough for you. However, before you start moving for the close button on the browser, this “workaround” does have a lot of power in disguise. By giving yourself control over when the stop loss level moves, you can create some really interesting trailing stops. Want to trail only behind swings in price action? You can do that! Volatility has picked up and you want to give the stop some more breathing room? Again, you can do that! I am sure you get the idea.
Example Code
class ParticleResistanceCircuit(QCAlgorithm): def Initialize(self): self.SetStartDate(2018, 11, 20) # Set Start Date self.SetStartDate(2018, 11, 21) # Set Start Date self.SetCash(100000) # Set Strategy Cash self.symbol = "SPY" self.AddEquity(self.symbol, Resolution.Minute) # Trailing distance in $ self.trail_dist = 10 # Declare an attribute that we shall use for storing our # stop loss ticket. self.sl_order = None # Declare an attribute that we will use to store the last trail level # used. We will use this to decide whether to move the 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 # Get the filled order Order = self.Transactions.GetOrderById(OrderEvent.OrderId) # Log the filled order details 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 ''' # We need to check that the symbol has data before trying to access # OHLC. Otherwise an exception is raised if the data is missing. if data.ContainsKey(self.symbol) and data.HasData: # Create some Alias # ------------------------------------------ holdings = self.Portfolio[self.symbol].Quantity value = self.Portfolio.TotalPortfolioValue cash = self.Portfolio.Cash # Even then, occassionally, there is no Open attribute 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 # Calcuate our base SL level. This is used for the initial entry. # We will also use it to compare to the previous trail level. base_sl_level = round(C - self.trail_dist,2) # Log OHLC - This can be useful for debugging to see where price is moving 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 >>') # Upate our stoploss order! update_order_fields = UpdateOrderFields() update_order_fields.StopPrice = base_sl_level self.sl_order.Update(update_order_fields) # Log last sl_level self.last_trail_level = base_sl_level
Commentary
Because we want to edit and update our stop loss as we go, the first thing we need to do is create an attribute (variable) that will store our stop-loss ticket. Tickets are returned from the API methods (functions) that create orders and we need them to update the order later. For this reason, one of the first things we do is to give the ticket a place to live during initialization.# Declare an attribute that we shall use for storing our # stop loss ticket. self.sl_order = NoneAt the same time, we also create another attribute to track the last trail stop level. This will be used on each bar to check whether the current stop loss level is higher than the last trailing level. If it is, we move the stop loss up. Note that you could also tackle this part by using a rolling window. If you wish to learn more about that, click on that link for an introduction on accessing previous values. The reason why a rolling window was not used in the example, is simply because we never need to access more than the most recent previous value and we are able to track it without the need for a rolling window.
On Data
The rest of the magic happens whenOnData()
is called. For the purposes of this tutorial, we enter a position immediately and send a stop loss order at the same time. Here, you will notice that we make use of 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_levelNext, we can take a look at the trailing calculation. To do this, we just calculate a “base” stop loss value. That is the current
close
minus thetrail_dist
. If price has moved in our favour, this base value will be above our self.last_trail_level
. If so, we then update the ticket with the lines:
# Upate our stoploss order! update_order_fields = UpdateOrderFields() update_order_fields.StopPrice = sl_level self.sl_order.Update(update_order_fields)Note: For more information regarding the use of
UpdateOrderFields()
see the official docs here: https://www.quantconnect.com/docs/algorithm-reference/trading-and-orders
After updating the order, notice how we now update the self.last_trail_level
at the same time. This will be the new hurdle to beat in subsequent bars.
Finally, if not, we ignore this bar and keep the stop loss level as is. Assuming price never moves in our favor again, this would continue until the SL is hit. This is just as a real trailing stop would work.
Verification
Since there are not any nice plots on the chart, we need to turn to our logs to verify what is going on. Run the backtest and open up your logs. You should see something like this:
Find This Post Useful?
If this post saved you time and effort, please consider support the site! There are many ways to support us and some won’t even cost you a penny.
Brave
Backtest Rookies is a registered with Brave publisher!
Brave users can drop us a tip.
Alternatively, support us by switching to Brave using this referral link and we will receive some BAT!
Tradingview
Referral Link
Enjoying the content and thinking of subscribing to Tradingview? Support this site by clicking the referral link before you sign up!
PayPal
BTC
3HxNVyh5729ieTfPnTybrtK7J7QxJD9gjD
ETH
0x9a2f88198224d59e5749bacfc23d79507da3d431
LTC
M8iUBnb8KamHR18gpgi2sSG7jopddFJi8S
Hi, I have a question regarding your trailing stop. Do you have any idea how to use the average fill price from an order ticket as a stop Loss?