🚀 AI One-Click Generation of quantconnect Strategy Code

QuantConnect Best Practices: Handling Asynchronous Order Events in Live Trading

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

Problem Description

What is the correct way to handle order fill events in a live trading environment?

Solution

In QuantConnect's Lean engine, the correct way to handle order fill events—especially in a live trading environment—is to utilize the on_order_event event handler.

Key Considerations for Live Trading

  1. Asynchronous Execution: In live trading, on_order_event is triggered asynchronously as soon as the brokerage reports a change. It does not wait for the next on_data slice.
  2. Infinite Loops: You must be cautious when placing new orders inside this event handler. If a new order fills immediately, it triggers the event again, potentially causing a runaway loop.
  3. State Updates: While self.portfolio is updated automatically by Lean, you should use this handler to update any custom state variables or logic specific to your strategy (e.g., resetting entry flags).

Implementation Example

The following code demonstrates how to correctly filter for fill events, retrieve order details, and safely handle post-trade logic (such as logging or placing stop-losses).

from AlgorithmImports import *

class OrderFillHandlingAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_cash(100000)
        self.add_equity("SPY", Resolution.MINUTE)

    def on_data(self, data: Slice):
        # Simple entry logic for demonstration
        if not self.portfolio.invested:
            self.market_order("SPY", 10)

    def on_order_event(self, order_event: OrderEvent) -> None:
        """
        Event handler for all order status changes.
        """
        # 1. Filter for Fill events (Full or Partial)
        if order_event.status == OrderStatus.FILLED or order_event.status == OrderStatus.PARTIALLY_FILLED:
            
            # 2. Retrieve the full Order object to access details like OrderType
            # order_event contains Status, FillPrice, FillQuantity, etc.
            # The Order object contains Type, Tag, etc.
            order = self.transactions.get_order_by_id(order_event.order_id)
            
            # Log the fill details
            self.log(f"Order Filled: {order_event.symbol} "
                     f"Status: {order_event.status} "
                     f"Fill Price: {order_event.fill_price} "
                     f"Fill Qty: {order_event.fill_quantity} "
                     f"Direction: {order_event.direction}")

            # 3. Example: Placing a Stop Loss after an Entry Fill
            # We check if this was a Market Order (Entry) to avoid placing stops on stops
            if order.type == OrderType.MARKET and order_event.status == OrderStatus.FILLED:
                
                # Calculate Stop Price (e.g., 1% below fill price)
                stop_price = order_event.fill_price * 0.99
                
                # Place Stop Market Order (Inverse of fill quantity)
                self.stop_market_order(order_event.symbol, -order_event.fill_quantity, stop_price)
                self.log(f"Placed Stop Loss for {order_event.symbol} at {stop_price}")

Detailed Breakdown

  1. order_event.status: Always check the status. The event handler fires for submitted, invalid, canceled, and filled orders. You are specifically looking for OrderStatus.FILLED or OrderStatus.PARTIALLY_FILLED.
  2. self.transactions.get_order_by_id: The order_event object is lightweight. If you need to know if the filled order was a MARKET order or a STOP_MARKET order, you must fetch the full order object using the ID provided in the event.
  3. Concurrency: In live mode, this function may run on a separate thread. Ensure any shared variables modified here are thread-safe or atomic if you are doing complex calculations, though standard Python variable assignment is generally safe in this context.

Q&A: Order Handling in QuantConnect

Q: Does on_order_event run before or after self.portfolio is updated?
A: The self.portfolio and self.securities objects are updated before on_order_event is called. You can rely on self.portfolio[symbol].invested reflecting the new state inside the handler.

Q: Can I place orders inside on_order_event?
A: Yes, but use caution. It is standard practice to place "bracket" orders (Stop Loss / Take Profit) inside this handler immediately after an entry fills. However, avoid logic that places an entry order immediately after an exit fill without strict conditions, as this can drain cash via transaction fees in a loop.

Q: How do I handle partial fills?
A: OrderStatus.PARTIALLY_FILLED indicates the order is not complete. If your logic relies on the total position size, you should accumulate the order_event.fill_quantity or wait until OrderStatus.FILLED (which implies completion) to take action. Note that order_event.fill_quantity represents the amount filled in that specific event, not the cumulative total of the order.

Q: Why do I see multiple events for one order?
A: An order goes through a lifecycle: SUBMITTED -> FILLED. You will receive an event for every state change. Always filter by order_event.status to execute logic only when relevant.