🚀 AI One-Click Generation of quantconnect Strategy Code

How to Safely Trigger a New Order After a Fill Using `on_order_event` in QuantConnect

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

Problem Description

I want to place a buy order only after my sell order for another asset has been filled. How can I do that?

Solution

To execute a buy order immediately after a sell order is filled, you should utilize the on_order_event event handler. This method is triggered whenever an order's status changes (e.g., Submitted, Filled, Canceled).

By tracking the OrderTicket of your sell order, you can check inside on_order_event if that specific order has a status of OrderStatus.FILLED. If it does, you then place your buy order.

Here is a complete, runnable strategy demonstrating this logic.

Strategy Logic

  1. Initialize: We subscribe to SPY (to sell) and TLT (to buy).
  2. Setup: We initially buy SPY to establish a position to sell.
  3. Trigger: We place a market sell order for SPY and save the returned OrderTicket to the variable self.sell_ticket.
  4. Execution: In on_order_event, we check if the incoming event matches our self.sell_ticket.order_id and if the status is FILLED. If true, we immediately place a buy order for TLT.

Python Code

from AlgorithmImports import *

class SequentialOrderExecution(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 6, 1)
        self.set_cash(100000)

        # Assets: We will sell SPY to buy TLT
        self.symbol_to_sell = self.add_equity("SPY", Resolution.MINUTE).symbol
        self.symbol_to_buy = self.add_equity("TLT", Resolution.MINUTE).symbol

        # Variable to store the ticket of the sell order we are tracking
        self.sell_ticket = None
        
        # Flag to ensure we only run the demo logic once
        self.demo_complete = False

    def on_data(self, slice: Slice):
        if self.demo_complete:
            return

        # 1. Setup Phase: Buy SPY initially so we have something to sell
        if not self.portfolio.invested and self.sell_ticket is None:
            self.set_holdings(self.symbol_to_sell, 1.0)
            self.debug("Setup: Bought SPY")
            return

        # 2. Trigger Phase: Sell SPY
        # We wait until we actually have holdings before selling
        if self.portfolio[self.symbol_to_sell].invested and self.sell_ticket is None:
            quantity = self.portfolio[self.symbol_to_sell].quantity
            
            # Place the sell order and capture the OrderTicket
            self.sell_ticket = self.market_order(self.symbol_to_sell, -quantity)
            self.debug(f"Trigger: Placed sell order for {self.symbol_to_sell}")

    def on_order_event(self, order_event: OrderEvent):
        # We only care about Filled events
        if order_event.status != OrderStatus.FILLED:
            return

        # 3. Execution Phase: Check if this event belongs to our specific sell order
        if self.sell_ticket is not None and order_event.order_id == self.sell_ticket.order_id:
            
            self.debug(f"Confirmed: Sell order filled for {order_event.symbol} at {order_event.fill_price}")
            
            # Calculate buying power or specific quantity for the next asset
            # Here we simply allocate 100% of equity to the new asset
            self.set_holdings(self.symbol_to_buy, 1.0)
            
            self.debug(f"Action: Placed buy order for {self.symbol_to_buy}")
            self.demo_complete = True

Key Concepts

  1. on_order_event: This is the most reliable place to chain orders. In live trading, order fills are asynchronous. If you place a sell order in on_data, the fill might not happen immediately. on_order_event guarantees you only act once the broker confirms the trade is done.
  2. OrderTicket: When you call self.market_order (or any order method), it returns an OrderTicket. This object contains the unique order_id. We store this in self.sell_ticket so we can identify exactly which order triggered the event.
  3. OrderStatus.FILLED: We strictly check for this status. We do not want to buy the second asset if the first order was CANCELED or INVALID.

Q&A

Q: Can I use this for Limit Orders as well?
A: Yes. The logic is identical. You would place a limit_order instead of a market_order, store the ticket, and wait for the FILLED event in on_order_event.

Q: What happens if the sell order is only partially filled?
A: The code above checks for OrderStatus.FILLED, which usually implies a complete fill. If you want to react to partial fills, you can check for OrderStatus.PARTIALLY_FILLED. However, you must be careful not to place the buy order multiple times if multiple partial fills occur for a single order.

Q: Why not just place both orders in on_data?
A: If you place a sell order and immediately place a buy order in the same on_data step, you might not have the cash available yet (buying power) because the sell hasn't settled or processed. Chaining them via events ensures the proceeds from the sale are recognized by the algorithm before the buy is attempted.