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 OnOrderEventon_order_event and OnAssignmentOrderEventon_assignment_order_event methods. The OnOrderEventon_order_event event handler receives all order events. The OnAssignmentOrderEventon_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 OrderEventsorder_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, 9, 1);
SetEndDate(2024, 12, 31);
// 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, 9, 1)
self.set_end_date(2024, 12, 31)
# 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 OnOrderEventon_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(2024, 9, 1);
SetEndDate(2024, 12, 31);
// 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(2024, 9, 1)
self.set_end_date(2024, 12, 31)
# 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(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()