Problem Description
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
- Asynchronous Execution: In live trading,
on_order_eventis triggered asynchronously as soon as the brokerage reports a change. It does not wait for the nexton_dataslice. - 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.
- State Updates: While
self.portfoliois 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
order_event.status: Always check the status. The event handler fires for submitted, invalid, canceled, and filled orders. You are specifically looking forOrderStatus.FILLEDorOrderStatus.PARTIALLY_FILLED.self.transactions.get_order_by_id: Theorder_eventobject is lightweight. If you need to know if the filled order was aMARKETorder or aSTOP_MARKETorder, you must fetch the full order object using the ID provided in the event.- 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.