Order Types
Other Order Types
One Cancels the Other Orders
One cancels the other (OCO) orders are a set of orders that when one fills, it cancels the rest of the orders in the set. An example is to set a take-profit and a stop-loss order right after you enter a position. In this example, when either the take-profit or stop-loss order fills, you cancel the other order. OCO orders usually create an upper and lower bound on the exit price of a trade.
When you place OCO orders, their price levels are usually relative to the fill price of an entry trade. If your entry trade is a synchronous market order, you can immediately get the fill price from the order ticket. If your entry trade doesn't execute immediately, you can get the fill price in the OnOrderEventson_order_events event handler. Once you have the entry fill price, you can calculate the price levels for the OCO orders.
// Get the fill price from the order ticket of a sync market order _ticket = MarketOrder("SPY", 1); var fillPrice = _ticket.AverageFillPrice; // Get the fill price from the OnOrderEvent event handler public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status == OrderStatus.Filled && _ticket.OrderId == orderEvent.OrderId) { var fillPrice = orderEvent.FillPrice; } }
# Get the fill price from the order ticket of a sync market order self.ticket = self.market_order("SPY", 1) fill_price = self.ticket.average_fill_price # Get the fill price from the OnOrderEvent event handler def on_order_event(self, order_event: OrderEvent) -> None: if order_event.status == OrderStatus.FILLED and self.ticket.order_id == order_event.order_id: fill_price = order_event.fill_price
After you have the target price levels, to implement the OCO orders, you can place active orders or track the security price to simulate the orders.
Place Active Orders
To place active orders for the OCO orders, use a combination of limit orders and stop limit orders. Place these orders so that their price levels that are far enough apart from each other. If their price levels are too close, several of the orders can fill in a single time step. When one of the orders fills, in the OnOrderEvent
on_order_event
event handler, cancel the other orders in the OCO order set.
OrderTicket _stopLoss = null; OrderTicket _takeProfit = null; public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status == OrderStatus.Filled) { if (orderEvent.OrderId == _ticket.OrderId) { _stopLoss = StopMarketOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*0.95m); _takeProfit = LimitOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*1.10m); } else if (_stopLoss != null && orderEvent.OrderId == _stopLoss.OrderId) { _takeProfit.Cancel(); } else if (_takeProfit != null && orderEvent.OrderId == _takeProfit.OrderId) { _stopLoss.Cancel(); } } }
self.stop_loss = None self.take_profit = None def on_order_event(self, order_event: OrderEvent) -> None: if order_event.status == OrderStatus.FILLED: if order_event.order_id == self.ticket.order_id: self.stop_loss = self.stop_market_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*0.95) self.take_profit = self.limit_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*1.10) elif self.stop_loss is not None and order_event.order_id == self.stop_loss.order_id: self.take_profit.cancel() elif self.take_profit is not None and order_event.order_id == self.take_profit.order_id: self.stop_loss.cancel()
Simulate Orders
To simulate OCO orders, track the asset price in the OnData
on_data
method and place market or limit orders when asset price reaches the take-profit or stop-loss level. The benefit of manually simulating the OCO orders is that both of the orders can't fill in the same time step.
decimal _entryPrice; public override void OnData(Slice slice) { if (!Portfolio.Invested) { var ticket = MarketOrder("SPY", 1); _entryPrice = ticket.AverageFillPrice; } if (!slice.Bars.ContainsKey("SPY")) return; if (slice.Bars["SPY"].Price >= _entryPrice * 1.10m) { Liquidate("SPY", -1, "take profit"); } else if (slice.Bars["SPY"].Price <= _entryPrice * 0.95m) { Liquidate("SPY", -1, "stop loss"); } }
def on_data(self, slice: Slice) -> None: if not self.portfolio.invested: ticket = self.market_order("SPY", 1) self.entry_price = ticket.average_fill_price bar = slice.get("SPY") if bar: if bar.price >= self.entry_price * 1.10: self.liquidate("SPY", -1, "take profit") elif bar.price <= self.entry_price * 0.95: self.liquidate("SPY", -1, "stop loss")