I am constantly referring back to the Boot Camp, but it is difficult to access the information in its interactive format. I have copied the text of the lesson here for quick access and review. (Do the boot camp before using this for review.) -GK Buy and Hold with a Trailing Stop

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.
Lesson Requirements

We suggest you complete Buy and Hold / Equities and Buy and Hold / Forex before starting this lesson.

Setting up a Stop Market Order
  1. Buy and Hold with a Trailing Stop
  2. Setting up a Stop Market Order
Request Asset Data

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.

# Subscribe to IBM with raw, daily data ibm = self.AddEquity("IBM", Resolution.Daily) ibm.SetDataNormalizationMode(DataNormalizationMode.Raw)Entering Into a Position

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 Order

We 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: tickerquantity, and stopPrice.

# Sell 300 units of IBM at or below 95% of the current close price self.StopMarketOrder("IBM", -300, 0.95 * self.Securities["IBM"].Close)

Task Objectives Completed

  1. Subscribe to SPY using daily data resolution, with raw data normalization.
  2. Create a market order to buy 500 units of SPY if the portfolio is not yet invested.
  3. Create a stop-market order to sell 500 units of SPY at 90% of the current SPY close price.
It is a good idea to place the stop-market order just after you place the market order. You might need to revisit the Data Normalization Tutorial in Buy and Hold Equities. 

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)
------------------------------------

Understanding Order Events
  1. Buy and Hold with a Trailing Stop
  2. Understanding Order Events
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.

def OnOrderEvent(self, orderEvent): pass

The OrderEvent object has a Status property with the OrderStatus enum values SubmittedPartiallyFilledFilledCanceled, and Invalid. It also contains an OrderId property which is a unique number representing the order.

Listening to Fills

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.

if orderEvent.Status == OrderStatus.Filled: # Print out the order Id self.Debug(orderEvent.OrderId)

Task Objectives CompletedContinue

  1. Inside def OnOrderEvent(), check if the incoming order event is completely filled.
  2. When the order is filled, save the order event to the lastOrderEvent variable.
Make sure you are logging only if 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)
            
-----------------------------------------

Identifying a Stop Loss Hit
  1. Buy and Hold with a Trailing Stop
  2. 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 Tickets

When 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.

# Place our order and return an order ticket self.stopMarketTicket = self.StopMarketOrder("IBM", -300, ibmStockPrice * 0.9) # Log its OrderId self.Debug(self.stopMarketTicket.OrderId) Identifying When a Stop Order Is Filled

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.

# Check if we hit our stop market if self.stopMarketTicket is not None and orderEvent.OrderId == self.stopMarketTicket.OrderId: self.stopMarketFillTime = self.Time;Controlling Algorithm Re-Entry

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

  1. Create StopMarketOrder for 90% of SPY price for -500 shares; save the OrderTicket returned from the StopMarketOrder call to the class variable stopMarketTicket.
  2. In OnOrderEvent, match the Id of the incoming order event with the stop market order ticket saved earlier.
  3. If they match, store the date and time that the stop market order was hit to stopMarketFillTime.
  4. In self.OnData(), check that at least 15 days have passed from when the stop loss was last hit before re-entering the MarketOrder and StopMarketOrder.
Hints:
  • 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.
Code:

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
  1. Buy and Hold with a Trailing Stop
  2. 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 Orders

Orders 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().

# Update stop loss price using UpdateOrderFields helper. updateFields = UpdateOrderFields() updateFields.StopPrice = self.Securities["SPY"].Close * 0.9 self.stopMarketTicket.Update(updateFields)

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.

  1. Check if the current price of SPY is higher than the highestSPYPrice.
  2. If the current SPY price is higher than highestSPYPrice, then update the new stop price to 90% of the current SPY close price.
  3. 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

------------------------------Visualizing the Stop Levels
  1. Buy and Hold with a Trailing Stop
  2. 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 Chart

The 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.

# You can plot multiple series on the same chart. self.Plot("Levels", "Asset Price", self.Securities["IBM"].Price) self.Plot("Levels", "Stop Price", self.Securities["IBM"].Price * 0.9)The 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:

20190722-bootcamp-buyandhold-trailingstop_rev1.png

  1. On every new data point, plot the SPY close price in a chart called Data Chart. Name the series Asset Price.
  2. If the portfolio is invested, plot the current stop loss price to the same chart, naming the series Stop Price.
Hint:You can fetch the order ticket stop price with 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