book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Trading and Orders

Order Events

Introduction

An OrderEvent object represents an update to the state of an order. As the state of your orders change, we notify your algorithm with OrderEvent objects through the on_order_event and on_assignment_order_event event handlers.

Track Order Events

Each order generates events over its life as its status changes. Your algorithm receives these events through the on_order_event and on_assignment_order_event methods. The on_order_event event handler receives all order events. The on_assignment_order_event receives order events for Option assignments. The event handlers receive an OrderEvent object, which contains information about the order status.

Select Language:
def on_order_event(self, order_event: OrderEvent) -> None:
    order = self.transactions.get_order_by_id(order_event.order_id)
    if order_event.status == OrderStatus.FILLED:
        self.debug(f"{self.time}: {order.type}: {order_event}")

def on_assignment_order_event(self, assignment_event: OrderEvent) -> None:
    self.log(str(assignment_event))

To get a list of all OrderEvent objects for an order, call the order_events method of the order ticket.

Select Language:
order_events = order_ticket.order_events()

If you don't have the order ticket, get the order ticket from the TransactionManager.

Order States

Orders can have any of the following states:

Event Attributes

The on_order_event and on_assignment_order_event event handlers in your algorithm receive OrderEvent objects, which have the following attributes:

Examples

The following examples demonstrate some common practices for using order events.

Example 1: Illiquid Stock Partial Fill

The following algorithm trades EMA cross on CARZ, an illiquid ETF. To realistically simulate the fill behavior, we set a fill model to partially fill the orders with at most 50% of the previous bar's volume per fill. We cancel the remaining open order after the partial fill since we only trade on the updated information.

Select Language:
class OrderEventsAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2024, 2, 1)
        self.set_end_date(2024, 4, 1)

        # Request CARZ data to feed indicator and trade.
        equity = self.add_equity("CARZ")
        self.carz = equity.symbol
        # Set a custom partial fill model for the illiquid CARZ stock since it is more realistic.
        equity.set_fill_model(CustomPartialFillModel(self))

        # Create EMA indicator to generate trade signals.
        self._ema = self.ema(self.carz, 60, Resolution.DAILY)
        # Warm-up indicator for immediate readiness to use.
        self.warm_up_indicator(self.carz, self._ema, Resolution.DAILY)

    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self.carz)
        if bar and self._ema.is_ready:
            ema = self._ema.current.value
            # Trade EMA cross on CARZ for trend-following strategy.
            if bar.close > ema and not self.portfolio[self.carz].is_long:
                self.set_holdings(self.carz, 0.5)
            elif bar.close < ema and not self.portfolio[self.carz].is_short:
                self.set_holdings(self.carz, -0.5)

    def on_order_event(self, order_event: OrderEvent) -> None:
        # If an order is only partially filled, we cancel the rest to avoid trade on non-updated information.
        if order_event.status == OrderStatus.PARTIALLY_FILLED:
            self.transactions.cancel_open_orders()

# Implements a custom fill model that partially fills each order with a ratio of the previous trade bar.
class CustomPartialFillModel(FillModel):
    def __init__(self, algorithm: QCAlgorithm, ratio: float = 0.5) -> None:
        self.algorithm = algorithm
        self.absolute_remaining_by_order_id = {}
        # Save the ratio of the volume of the previous bar to fill the order.
        self.ratio = ratio

    def market_fill(self, asset: Security, order: MarketOrder) -> None:
        absolute_remaining = self.absolute_remaining_by_order_id.get(order.id, order. AbsoluteQuantity)

        fill = super().market_fill(asset, order)

        # Partially fill each order with at most 50% of the previous bar.
        fill.fill_quantity = np.sign(order.quantity) * asset.volume * self.ratio
        if (min(abs(fill.fill_quantity), absolute_remaining) == absolute_remaining):
            fill.fill_quantity = np.sign(order.quantity) * absolute_remaining
            fill.status = OrderStatus.FILLED
            self.absolute_remaining_by_order_id.pop(order.id, None)
        else:
            fill.status = OrderStatus.PARTIALLY_FILLED
            # Save the remaining quantity after it is partially filled.
            self.absolute_remaining_by_order_id[order.id] = absolute_remaining - abs(fill.fill_quantity)
            price = fill.fill_price

        return fill

Example 2: Price Actions

The following algorithm saves the trailing 3 TradeBar objects into a RollingWindow. When it identifies a volume contraction breakout price action pattern on the SPY, it buys to ride on the capital inflow. To exit positions, it places a 2% take profit and 1% stop loss order in the on_order_event method.

Select Language:
class RollingWindowAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2021, 1, 1)
        self.set_end_date(2022, 1, 1)

        # Add SPY data for signal generation and trading.
        self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol

        # Set up a rolling window to hold the last 3 trade bars for price action detection as the trade signal.
        self.windows = RollingWindow[TradeBar](3)
        # Warm up the rolling window.
        history = self.history[TradeBar](self.spy, 3, Resolution.MINUTE)
        for bar in history:
            self.windows.add(bar)

    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self.spy)
        if bar:
            # Trade the price action if the previous bars fulfill a contraction breakout.
            if self.contraction_action and self.breakout(bar.close):
                self.set_holdings(self.spy, 0.5)

            # Add the current bar to the window.
            self.windows.add(bar)

    def contraction_action(self) -> None:
        # We trade contraction type price action, where the buying preesure is increasing.
        # 1. The last 3 bars are green.
        # 2. The price is increasing in trend.
        # 3. The trading volume is increasing as well.
        # 4. The range of the bars are decreasing.
        return (
            self.windows[2].close > self.windows[2].open and
            self.windows[1].close > self.windows[1].open and
            self.windows[0].close > self.windows[0].open and
            self.windows[0].close > self.windows[1].close > self.windows[2].close and
            self.windows[0].volume > self.windows[1].volume > self.windows[2].volume and
            self.windows[2].close - self.windows[2].open > self.windows[1].close - self.windows[1].open > self.windows[0].close - self.windows[0].open
        )

    def breakout(self, current_close: float) -> None:
        # Trade breakout from contraction: the breakout should be much greater than the contracted range of the last bar.
        return current_close - self.windows[0].close > (self.windows[0].close - self.windows[0].open) * 2

    def on_order_event(self, order_event: OrderEvent) -> None:
        if order_event.status == OrderStatus.FILLED:
            if order_event.ticket.order_type == OrderType.MARKET:
                # Stop loss order at 1%.
                stop_price = order_event.fill_price * (0.99 if order_event.fill_quantity > 0 else 1.01)
                self.stop_market_order(self.spy, -self.portfolio[self.spy].quantity, stop_price)
                # Take profit order at 2%.
                take_profit_price = order_event.fill_price * (1.02 if order_event.fill_quantity > 0 else 0.98)
                self.limit_order(self.spy, -self.portfolio[self.spy].quantity, take_profit_price)
            elif order_event.ticket.order_type == OrderType.STOP_MARKET or order_event.ticket.order_type == OrderType.LIMIT:
                # Cancel open orders if stop loss or take profit order fills.
                self.transactions.cancel_open_orders()

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: