In this lesson, we will explore trailing stops and learn how to create a Stop Loss to manage risk in our orders. Additionally, we will plot the price of the order levels.
What You'll Learn- Using stop loss for order risk management.
- Plotting the price of order levels.
- Identifying when a stop market order is triggered.
We suggest you complete Buy and Hold / Equities and Buy and Hold / Forex before starting this lesson.
Setting up a Stop Market Order- Buy and Hold with a Trailing Stop
- Setting up a Stop Market Order
In Initialize()
, we set up our algorithm as usual using self.SetCash()
, self.SetStartDate()
and self.SetEndDate()
. We can also request our asset data with the self.AddEquity()
method.
Using a MarketOrder()
we can buy a specified number of units of our asset. A market order is sent directly to the exchange and immediately filled.
# Buy 300 units of IBM at market price self.MarketOrder("IBM", 300)
Creating a Stop OrderWe typically set a stop loss to trigger below the holding price of an existing holding. The difference between the holding price and the stop price is how much we are prepared to lose.
There are two kinds of stop orders in QuantConnect, stop-limit and stop-market orders. A stop-market order needs to know the quantity of shares to sell (or buy) and the price to trigger the order. The StopMarketOrder()
method has the following arguments: ticker
, quantity
, and stopPrice
.
Task Objectives Completed
- Subscribe to SPY using daily data resolution, with raw data normalization.
- Create a market order to buy 500 units of SPY if the portfolio is not yet invested.
- Create a stop-market order to sell 500 units of SPY at 90% of the current SPY close price.
class BootCampTask(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 12, 1) # Set Start Date
self.SetEndDate(2019, 4, 1) # Set End Date
self.SetCash(100000) # Set Strategy Cash
#1. Subscribe to SPY in raw mode
self.spy = self.AddEquity("SPY", Resolution.Daily)
self.spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
def OnData(self, data):
if not self.Portfolio.Invested:
#2. Create market order to buy 500 units of SPY
self.MarketOrder("SPY", 500)
#3. Create a stop market order to sell 500 units at 90% of the SPY current price
self.StopMarketOrder("SPY", -500, 0.90 * self.Securities["SPY"].Close)
------------------------------------
- Buy and Hold with a Trailing Stop
- Understanding Order Events
Order events are updates on the status of your order. Every order event is sent to the def OnOrderEvent()
event handler, with information about the order status held in an OrderEvent
object.
The OrderEvent object has a Status property with the OrderStatus enum values Submitted
, PartiallyFilled
, Filled
, Canceled
, and Invalid
. It also contains an OrderId
property which is a unique number representing the order.
In our algorithm, we want to listen to complete fills so we know when our stop was triggered. We can ignore the other events by explicitly looking for the Filled
status.
Task Objectives CompletedContinue
- Inside
def OnOrderEvent()
, check if the incoming order event is completely filled. - When the order is filled, save the order event to the
lastOrderEvent
variable.
orderEvent.Status == OrderStatus.Filled
. class BootCampTask(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 12, 1)
self.SetEndDate(2019, 4, 1)
self.SetCash(100000)
spy = self.AddEquity("SPY", Resolution.Daily)
spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.lastOrderEvent = None
def OnData(self, data):
if not self.Portfolio.Invested:
self.MarketOrder("SPY", 500)
self.StopMarketOrder("SPY", -500, 0.9 * self.Securities["SPY"].Close)
def OnOrderEvent(self, orderEvent):
#1. Write code to only act on fills
if orderEvent.Status == OrderStatus.Filled:
#2. Save the orderEvent to lastOrderEvent, use Debug to print the event OrderId
self.lastOrderEvent = orderEvent
self.Debug(orderEvent.OrderId)
-----------------------------------------
- Buy and Hold with a Trailing Stop
- Identifying a Stop Loss Hit
It is important to know if the stop loss has been hit so we don't immediately re-enter the market.
Tracking with Order TicketsWhen placing an order, QuantConnect returns an OrderTicket
object which can be used to update an order's properties, request that it is cancelled, or fetch its OrderId
.
The OrderId
is stored on the orderEvent
parameter passed into our OnOrderEvent()
method. We can match the orderEvent.OrderId
with the Id of the stop market order to see if our order has been filled.
An algorithm can place hundreds of trades in a second, so it's important to carefully control when it places trades. Ask yourself these questions when tracking your algorithm state, such as:
- When was the last time I placed a trade?
- Did the order fill according to my expectations?
- Am I placing the right number of orders?
# Check that at least 15 days (~2 weeks) have passed since we last hit our limit order if (self.Time - self.stopMarketFillTime).days < 15: return
Task Objectives CompletedContinue
- Create StopMarketOrder for 90% of SPY price for -500 shares; save the OrderTicket returned from the StopMarketOrder call to the class variable
stopMarketTicket
. - In OnOrderEvent, match the Id of the incoming order event with the stop market order ticket saved earlier.
- If they match, store the date and time that the stop market order was hit to
stopMarketFillTime
. - In
self.OnData()
, check that at least 15 days have passed from when the stop loss was last hit before re-entering theMarketOrder
andStopMarketOrder
.
- The incoming order event Id is stored in
orderEvent.OrderId
. - The order ticket Id is stored in
orderTicket.OrderId
. - The current date and time of the algorithm is stored in class property
self.Time
. - Remember to check at least 15 days having passed, i.e.
{days passed} >= 15
.
class BootCampTask(QCAlgorithm):
# Order ticket for our stop order, Datetime when stop order was last hit
stopMarketTicket = None
stopMarketFillTime = datetime.min
def Initialize(self):
self.SetStartDate(2018, 12, 1)
self.SetEndDate(2019, 4, 1)
self.SetCash(100000)
spy = self.AddEquity("SPY", Resolution.Daily)
spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
def OnData(self, data):
#4. Check that at least 15 days (~2 weeks) have passed since we last hit our stop order
if (self.Time - self.stopMarketFillTime).days < 15:
return
if not self.Portfolio.Invested:
self.MarketOrder("SPY", 500)
#1. Create stop loss through a stop market order
self.stopMarketTicket = self.StopMarketOrder("SPY", -500, 0.9 * self.Securities["SPY"].Close)
def OnOrderEvent(self, orderEvent):
if orderEvent.Status != OrderStatus.Filled:
return
# Printing the security fill prices.
self.Debug(self.Securities["SPY"].Close)
#2. Check if we hit our stop loss (Compare the orderEvent.Id with the stopMarketTicket.OrderId)
# It's important to first check if the ticket isn't null (i.e. making sure it has been submitted)
if self.stopMarketTicket is not None and self.stopMarketTicket.OrderId == orderEvent.OrderId:
#3. Store datetime
self.stopMarketFillTime = self.Time
self.Debug(self.stopMarketFillTime)
-----------------------------
Creating a Trailing Stop Loss- Buy and Hold with a Trailing Stop
- Creating a Trailing Stop Loss
By updating a stop's trigger price as the market moves, we can in theory lock in profits and cap downside risk. This transforms our static risk management into a dynamic one.
Updating OrdersOrders which are not filled immediately can be updated using their order ticket. To update an order you create an UpdateOrderFields
object which contains all the properties you'd like to change.
To update the stop price of a given order ticket, we invoke orderticket.Update()
.
Task Objectives CompletedContinue
In self.OnData()
we will check the current SPY close price and compare it to the highest close price since we opened our order. If it's higher, we will move our stop market order price up by updating the order ticket we saved earlier.
- Check if the current price of SPY is higher than the
highestSPYPrice
. - If the current SPY price is higher than
highestSPYPrice
, then update the new stop price to 90% of the current SPY close price. - Print the new stop price using
Debug().
Hint:
Make sure to record the new highest SPY close price to highestSPYPrice
when you reach new highs.
Code:
class BootCampTask(QCAlgorithm):
# Order ticket for our stop order, Datetime when stop order was last hit
stopMarketTicket = None
stopMarketOrderFillTime = datetime.min
highestSPYPrice = 0
def Initialize(self):
self.SetStartDate(2018, 12, 1)
self.SetEndDate(2018, 12, 10)
self.SetCash(100000)
spy = self.AddEquity("SPY", Resolution.Daily)
spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
def OnData(self, data):
if (self.Time - self.stopMarketOrderFillTime).days < 15:
return
if not self.Portfolio.Invested:
self.MarketOrder("SPY", 500)
self.stopMarketTicket = self.StopMarketOrder("SPY", -500, 0.9 * self.Securities["SPY"].Close)
else:
#1. Check if the SPY price is higher that highestSPYPrice.
if self.Securities["SPY"].Close > self.highestSPYPrice:
#2. Save the new high to highestSPYPrice; then update the stop price to 90% of highestSPYPrice
self.highestSPYPrice = self.Securities["SPY"].Close
updateFields = UpdateOrderFields()
updateFields.StopPrice = self.highestSPYPrice * 0.9
self.stopMarketTicket.Update(updateFields)
#3. Print the new stop price with Debug()
self.Debug("SPY: " + str(self.highestSPYPrice) + " Stop: " + str(updateFields.StopPrice))
def OnOrderEvent(self, orderEvent):
if orderEvent.Status != OrderStatus.Filled:
return
if self.stopMarketTicket is not None and self.stopMarketTicket.OrderId == orderEvent.OrderId:
self.stopMarketOrderFillTime = self.Time
- Buy and Hold with a Trailing Stop
- Visualizing the Stop Levels
Charts are a powerful way of visualizing the behavior of your algorithm. See the documentation for more details on the charting API.
Creating a ChartThe Plot()
method can draw a line-chart with a single line of code. It takes three arguments, the name of the chart, the name of the series and the value you'd like to plot.
Plot()
function is highly versatile and takes care of plotting to the correct chart and series.
Task Objectives CompletedContinue
We'd like to plot the current SPY close price, together with the stop price to visualize the algorithm activity like the chart below:
- On every new data point, plot the SPY close price in a chart called
Data Chart
. Name the seriesAsset Price
. - If the portfolio is invested, plot the current stop loss price to the same chart, naming the series
Stop Price
.
orderTicket.Get(OrderField.StopPrice)
. Code:class BootCampTask(QCAlgorithm):
# Order ticket for our stop order, Datetime when stop order was last hit
stopMarketTicket = None
stopMarketOrderFillTime = datetime.min
highestSPYPrice = -1
def Initialize(self):
self.SetStartDate(2018, 12, 1)
self.SetEndDate(2018, 12, 10)
self.SetCash(100000)
spy = self.AddEquity("SPY", Resolution.Daily)
spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
def OnData(self, data):
# 1. Plot the current SPY price to "Data Chart" on series "Asset Price"
self.Plot("Data Chart", "Asset Price", data["SPY"].Close)
if (self.Time - self.stopMarketOrderFillTime).days < 15:
return
if not self.Portfolio.Invested:
self.MarketOrder("SPY", 500)
self.stopMarketTicket = self.StopMarketOrder("SPY", -500, 0.9 * self.Securities["SPY"].Close)
else:
#2. Plot the moving stop price on "Data Chart" with "Stop Price" series name
self.Plot("Data Chart", "Stop Price", self.stopMarketTicket.Get(OrderField.StopPrice))
if self.Securities["SPY"].Close > self.highestSPYPrice:
self.highestSPYPrice = self.Securities["SPY"].Close
updateFields = UpdateOrderFields()
updateFields.StopPrice = self.highestSPYPrice * 0.9
self.stopMarketTicket.Update(updateFields)
def OnOrderEvent(self, orderEvent):
if orderEvent.Status != OrderStatus.Filled:
return
if self.stopMarketTicket is not None and self.stopMarketTicket.OrderId == orderEvent.OrderId:
self.stopMarketOrderFillTime = self.Time
Greg Kendall
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!