Trading and Orders
Order Events
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.
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.
order_events = order_ticket.order_events()
If you don't have the order ticket, get the order ticket from the TransactionManager.
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.
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.
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()