🚀 AI One-Click Generation of quantconnect Strategy Code

How to Attach Stop-Loss and Take-Profit Orders to a Position After Fill in QuantConnect

quantconnect | Published on: 2025/12/5
The following content is generated by EasyQuant .

Problem Description

How do I add a stop-loss and take-profit order only after my main trade has been executed?

Solution

To add stop-loss and take-profit orders immediately after a main trade is executed, you should utilize the on_order_event event handler. This method is triggered automatically whenever an order status changes (e.g., from SUBMITTED to FILLED).

By checking for OrderStatus.FILLED inside this handler, you ensure that your protection orders are only placed once the entry position is confirmed and you have the exact execution price.

Key Implementation Details

  1. on_order_event: This is where the logic resides. You must filter events to ensure you are reacting only to entry fills, not exit fills (to avoid infinite loops).
  2. Direction Logic: You must check if the fill quantity is positive (Long) or negative (Short) to calculate the correct stop and limit prices.
  3. Order Types:
    • Use stop_market_order for the Stop Loss.
    • Use limit_order for the Take Profit.

Complete Strategy Code

Here is a complete, runnable algorithm demonstrating this pattern.

from AlgorithmImports import *

class EntryWithExitProtection(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 6, 1)
        self.set_cash(100000)
        
        self.symbol = self.add_equity("SPY", Resolution.MINUTE).symbol
        
        # Parameters for risk management
        self.stop_loss_pct = 0.02   # 2% Stop Loss
        self.take_profit_pct = 0.04 # 4% Take Profit
        
        # Variable to track our entry ticket to distinguish it from other orders
        self.entry_ticket = None

    def on_data(self, data: Slice):
        # Simple Logic: Enter if not invested
        if not self.portfolio.invested and self.entry_ticket is None:
            # Place a Market Order to enter
            self.entry_ticket = self.market_order(self.symbol, 100)

    def on_order_event(self, order_event: OrderEvent):
        # 1. Check if the order is completely filled
        if order_event.status != OrderStatus.FILLED:
            return

        # 2. Check if this fill belongs to our specific entry ticket
        # We do this to ensure we don't place stops/limits when our stops/limits get filled
        if self.entry_ticket is not None and self.entry_ticket.order_id == order_event.order_id:
            
            fill_price = order_event.fill_price
            quantity = order_event.fill_quantity
            
            # Logic for LONG positions (Quantity > 0)
            if quantity > 0:
                stop_price = fill_price * (1 - self.stop_loss_pct)
                limit_price = fill_price * (1 + self.take_profit_pct)
                
                # Place Stop Loss (Sell to Close)
                self.stop_market_order(self.symbol, -quantity, stop_price, "Stop Loss")
                
                # Place Take Profit (Sell to Close)
                self.limit_order(self.symbol, -quantity, limit_price, "Take Profit")
                
                self.log(f"Long Entry Filled at {fill_price}. SL: {stop_price}, TP: {limit_price}")

            # Logic for SHORT positions (Quantity < 0)
            elif quantity < 0:
                stop_price = fill_price * (1 + self.stop_loss_pct)
                limit_price = fill_price * (1 - self.take_profit_pct)
                
                # Place Stop Loss (Buy to Close)
                self.stop_market_order(self.symbol, -quantity, stop_price, "Stop Loss")
                
                # Place Take Profit (Buy to Close)
                self.limit_order(self.symbol, -quantity, limit_price, "Take Profit")
                
                self.log(f"Short Entry Filled at {fill_price}. SL: {stop_price}, TP: {limit_price}")
            
            # Reset entry ticket so we don't process this again
            self.entry_ticket = None

        # 3. Handle the "One-Cancels-Other" (OCO) logic manually
        # If a Stop or Limit is filled, we should cancel the remaining open orders for this symbol
        # to prevent leaving a "dangling" order in the market.
        else:
            # If we are no longer invested (position closed by SL or TP), cancel all other orders
            if not self.portfolio[self.symbol].invested:
                self.transactions.cancel_open_orders(self.symbol)
                self.entry_ticket = None

Important Considerations

  1. Dangling Orders: In the code above, step #3 is crucial. Since QuantConnect does not natively link the Stop and Limit orders as a server-side OCO (One-Cancels-Other) group for all brokerages, you must manually cancel the remaining order. If the Stop Loss is hit, the Take Profit Limit order remains open unless you explicitly cancel it using self.transactions.cancel_open_orders.
  2. Fill Quantity: Always use order_event.fill_quantity (or -quantity for the exit) rather than self.portfolio[symbol].quantity. This ensures that if you are scaling into a position (partial fills), you only place exits for the amount actually filled.
  3. Asynchronous Events: In live trading, on_order_event runs asynchronously. This method is the safest way to chain orders because it guarantees the entry order has been processed by the exchange before you attempt to place the exits.

Q&A

Why use on_order_event instead of placing orders immediately in on_data?

In on_data, you do not yet know the execution price of a market order. If you place a Stop Loss immediately after a Market Order line in on_data, the Market Order might not have filled yet, or you might calculate the stop price based on the current price rather than the actual fill price. on_order_event guarantees you have the confirmed fill price.

How do I prevent the Stop Loss from triggering immediately?

Ensure your logic calculates the stop price correctly based on direction. For a Long position, the stop price must be below the fill price. For a Short position, it must be above. The code provided handles this in the if quantity > 0 and elif quantity < 0 blocks.

Can I use stop_limit_order instead of stop_market_order?

Yes. A stop_market_order guarantees execution but not price (slippage may occur). A stop_limit_order guarantees price but not execution (if the market gaps past your limit, you might not get filled). Change self.stop_market_order to self.stop_limit_order if you prefer the latter, but be aware of the risk of holding a losing position that skipped your stop.