Problem Description
Solution
To handle different order states in QuantConnect, you utilize the on_order_event event handler. This method is automatically triggered whenever an order's status changes (e.g., when it is submitted, partially filled, filled, or cancelled).
Inside this method, you inspect the OrderEvent object to determine the status and execute specific logic accordingly.
Key Concepts
on_order_event(self, order_event): The specific method to override in your algorithm.order_event.status: An enum (OrderStatus) representing the current state. Common states includeOrderStatus.SUBMITTED,OrderStatus.FILLED,OrderStatus.PARTIALLY_FILLED, andOrderStatus.CANCELLED.order_event.order_id: Used to retrieve the full order object if you need details not present in the event (like the order type or tag).
Strategy Code
The following example demonstrates how to implement specific logic for Submitted, Filled, and Cancelled orders.
# region imports
from AlgorithmImports import *
# endregion
class OrderStatusReactionAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 1, 10)
self.set_cash(100000)
self.symbol = self.add_equity("SPY", Resolution.DAILY).symbol
self.limit_order_ticket = None
def on_data(self, data: Slice):
# Simple logic to trigger different order events for demonstration
if not self.portfolio.invested:
# 1. Place a Market Order (Will go Submitted -> Filled)
self.market_order(self.symbol, 10, tag="Market Entry")
# 2. Place a Limit Order far below price (Will go Submitted)
# We save the ticket to cancel it later
current_price = data[self.symbol].close
self.limit_order_ticket = self.limit_order(self.symbol, 10, current_price * 0.5, tag="Deep Limit")
# 3. Cancel the limit order to trigger the Cancelled event
if self.limit_order_ticket is not None and self.limit_order_ticket.status == OrderStatus.SUBMITTED:
self.limit_order_ticket.cancel()
def on_order_event(self, order_event: OrderEvent):
"""
This event handler is triggered whenever an order status changes.
"""
# We can retrieve the full order object using the ID if we need to check the OrderType or Tag
order = self.transactions.get_order_by_id(order_event.order_id)
# Logic for SUBMITTED orders
if order_event.status == OrderStatus.SUBMITTED:
self.log(f"Order Submitted: ID {order_event.order_id} | Symbol: {order_event.symbol}")
# Example: You might want to track open orders in a custom list here
# Logic for FILLED orders (includes Partial Fills)
elif order_event.status == OrderStatus.FILLED or order_event.status == OrderStatus.PARTIALLY_FILLED:
self.log(f"Order Filled: {order_event.fill_quantity} shares of {order_event.symbol} at ${order_event.fill_price}")
# Example: Logic specific to the order tag
if order.tag == "Market Entry":
self.log("Entry logic complete. Setting stop loss...")
# self.stop_market_order(...)
# Logic for CANCELLED orders
elif order_event.status == OrderStatus.CANCELLED:
self.log(f"Order Cancelled: ID {order_event.order_id}")
# Example: Retry logic or cleanup
if order.tag == "Deep Limit":
self.log("Deep limit order cancelled successfully.")
Implementation Details
- Accessing Order Details: The
order_eventobject contains the most critical information regarding the change (Fill Price, Fill Quantity, Status). If you need static information about the order (like theTagorOrderType), useself.transactions.get_order_by_id(order_event.order_id). - Partial Fills: Note that
OrderStatus.FILLEDimplies the order is completely finished. If you are trading illiquid assets or large volume, you should also handleOrderStatus.PARTIALLY_FILLED. - Async Nature: In live trading,
on_order_eventis triggered asynchronously. Do not place new orders directly inside this method if there is a risk of creating an infinite loop (e.g., an order fills, triggering logic that places a new order immediately).
Q&A
Q: Does on_order_event run before or after on_data?
A: In backtesting, on_order_event is triggered synchronously after the main data events but before the next time slice. In live trading, it is asynchronous and triggers immediately when the brokerage reports the status change.
Q: How do I distinguish between a Buy and a Sell fill?
A: You can check order_event.direction (0 for Buy, 1 for Sell) or check if order_event.fill_quantity is positive (Buy) or negative (Sell).
Q: Can I modify an order inside on_order_event?
A: You cannot modify the order that just triggered the event (it is already processed). However, you can update other open orders or place new ones, provided you are careful to avoid logic loops.
Q: What happens if an order is invalid?
A: There is an OrderStatus.INVALID state. This usually occurs if you try to place an order with insufficient buying power or invalid parameters. You can catch this in on_order_event to log errors or halt the algorithm.