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 OnOrderEvent
on_order_event
and OnAssignmentOrderEvent
on_assignment_order_event
methods. The OnOrderEvent
on_order_event
event handler receives all order events. The OnAssignmentOrderEvent
on_assignment_order_event
receives order events for Option assignments. The event handlers receive an OrderEvent
object, which contains information about the order status.
public override void OnOrderEvent(OrderEvent orderEvent) { var order = Transactions.GetOrderById(orderEvent.OrderId); if (orderEvent.Status == OrderStatus.Filled) { Debug($"{Time}: {order.Type}: {orderEvent}"); } } public override void OnAssignmentOrderEvent(OrderEvent assignmentEvent) { Log(assignmentEvent.ToString()); }
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 OrderEvents
order_events
method of the order ticket.
var orderEvents = orderTicket.OrderEvents();
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.
public class OrderEventsAlgorithm : QCAlgorithm { private Symbol _carz; private ExponentialMovingAverage _ema; public override void Initialize() { SetStartDate(2024, 2, 1); SetEndDate(2024, 4, 1); // Request CARZ data to feed indicator and trade. var equity = AddEquity("CARZ"); _carz = equity.Symbol; // Set a custom partial fill model for the illiquid CARZ stock since it is more realistic. equity.SetFillModel(new CustomPartialFillModel(this)); // Create EMA indicator to generate trade signals. _ema = EMA(_carz, 60, Resolution.Daily); // Warm up indicator for immediate readiness to use. WarmUpIndicator(_carz, _ema, Resolution.Daily); } public override void OnData(Slice slice) { if (slice.Bars.TryGetValue(_carz, out var bar)) { // Trade EMA cross on CARZ for trend-following strategy. if (bar.Close > _ema && !Portfolio[_carz].IsLong) { SetHoldings(_carz, 0.5m); } else if (bar.Close < _ema && !Portfolio[_carz].IsShort) { SetHoldings(_carz, -0.5m); } } } public override void OnOrderEvent(OrderEvent orderEvent) { // If an order is only partially filled, we cancel the rest to avoid trade on non-updated information. if (orderEvent.Status == OrderStatus.PartiallyFilled) { Transactions.CancelOpenOrders(); } } /// Implements a custom fill model that partially fills each order with a ratio of the previous trade bar. private class CustomPartialFillModel : FillModel { private readonly QCAlgorithm _algorithm; private readonly Dictionary<int, decimal> _absoluteRemainingByOrderId; // Save the ratio of the volume of the previous bar to fill the order. private decimal _ratio; public CustomPartialFillModel(QCAlgorithm algorithm, decimal ratio = 0.5m) : base() { _algorithm = algorithm; _absoluteRemainingByOrderId = new Dictionary<int, decimal>(); _ratio = ratio; } public override OrderEvent MarketFill(Security asset, MarketOrder order) { decimal absoluteRemaining; if (!_absoluteRemainingByOrderId.TryGetValue(order.Id, out absoluteRemaining)) { absoluteRemaining = order.AbsoluteQuantity; } var fill = base.MarketFill(asset, order); // Partially filled each order with at most 50% of the previous bar. fill.FillQuantity = Math.Sign(order.Quantity) * asset.Volume * _ratio; if (Math.Min(Math.Abs(fill.FillQuantity), absoluteRemaining) == absoluteRemaining) { fill.FillQuantity = Math.Sign(order.Quantity) * absoluteRemaining; fill.Status = OrderStatus.Filled; _absoluteRemainingByOrderId.Remove(order.Id); } else { fill.Status = OrderStatus.PartiallyFilled; // Save the remaining quantity after it is partially filled. _absoluteRemainingByOrderId[order.Id] = absoluteRemaining - Math.Abs(fill.FillQuantity); var price = fill.FillPrice; } return fill; } } }
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 OnOrderEvent
on_order_event
method.
public class RollingWindowAlgorithm : QCAlgorithm { private Symbol _spy; // Set up a rolling window to hold the last 3 trade bars for price action detection as the trade signal. private RollingWindow<TradeBar> _windows = new(3); public override void Initialize() { SetStartDate(2021, 1, 1); SetEndDate(2022, 1, 1); // Add SPY data for signal generation and trading. _spy = AddEquity("SPY", Resolution.Minute).Symbol; // Warm up the rolling window. var history = History<TradeBar>(_spy, 3, Resolution.Minute); foreach (var bar in history) { _windows.Add(bar); } } public override void OnData(Slice slice) { if (slice.Bars.TryGetValue(_spy, out var bar)) { // Trade the price action if the previous bars fulfill a contraction breakout. if (ContractionAction() && BreakoutAction(bar.Close)) { SetHoldings(_spy, 0.5m); } // Add the current bar to the window. _windows.Add(bar); } } private bool ContractionAction() { // 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 _windows[2].Close > _windows[2].Open && _windows[1].Close > _windows[1].Open && _windows[0].Close > _windows[0].Open && _windows[0].Close > _windows[1].Close && _windows[1].Close > _windows[2].Close && _windows[0].Volume > _windows[1].Volume && _windows[1].Volume > _windows[2].Volume && _windows[2].Close - _windows[2].Open > _windows[1].Close - _windows[1].Open && _windows[1].Close - _windows[1].Open > _windows[0].Close - _windows[0].Open; } private bool BreakoutAction(decimal currentPrice) { // Trade breakout from contraction: the breakout should be much greater than the contracted range of the last bar. return currentPrice - _windows[0].Close > (_windows[0].Close - _windows[0].Open) * 2m; } public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status == OrderStatus.Filled) { if (orderEvent.Ticket.OrderType == OrderType.Market) { // Stop loss order at 1%. var stopPrice = orderEvent.FillQuantity > 0m ? orderEvent.FillPrice * 0.99m : orderEvent.FillPrice * 1.01m; StopMarketOrder(_spy, -Portfolio[_spy].Quantity, stopPrice); // Take profit order at 2%. var takeProfitPrice = orderEvent.FillQuantity > 0m ? orderEvent.FillPrice * 1.02m : orderEvent.FillPrice * 0.98m; LimitOrder(_spy, -Portfolio[_spy].Quantity, takeProfitPrice); } else if (orderEvent.Ticket.OrderType == OrderType.StopMarket || orderEvent.Ticket.OrderType == OrderType.Limit) { // Cancel open orders if the stop loss or take profit order fills. Transactions.CancelOpenOrders(); } } } }
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()