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
_market = MarketOrder("SPY", 1);
var fillPrice = _market.AverageFillPrice;
// Get the fill price from the OnOrderEvent event handler
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.Filled && orderEvent.Ticket.OrderType == OrderType.Market)
{
var fillPrice = orderEvent.FillPrice;
}
} # Get the fill price from the order ticket of a sync market order
self._market = self.market_order("SPY", 1)
fill_price = self._market.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 order_event.ticket.order_type == OrderType.MARKET:
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 OnOrderEventon_order_event event handler, cancel the other orders in the OCO order set.
private OrderTicket _stopLoss;
private OrderTicket _takeProfit;
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled) return;
switch (orderEvent.Ticket.OrderType)
{
case OrderType.Market:
_stopLoss = StopMarketOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*0.95m);
_takeProfit = LimitOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*1.10m);
return;
case OrderType.StopMarket:
_takeProfit?.Cancel();
return;
case OrderType.Limit:
_stopLoss?.Cancel();
return;
}
} _stop_loss = None
_take_profit = None
def on_order_event(self, order_event: OrderEvent) -> None:
if order_event.status != OrderStatus.FILLED:
return
match order_event.ticket.order_type:
case OrderType.MARKET:
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)
case OrderType.STOP_MARKET:
self._take_profit.cancel()
case OrderType.LIMIT:
self._stop_loss.cancel()
Simulate Orders
To simulate OCO orders, track the asset price in the OnDataon_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(symbol: "SPY", -1, tag: "take profit");
}
else if (slice.Bars["SPY"].Price <= _entryPrice * 0.95m)
{
Liquidate(symbol: "SPY", -1, tag: "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(symbol="SPY", -1, tag="take profit")
elif bar.price <= self.entry_price * 0.95:
self.liquidate(symbol="SPY", -1, tag="stop loss")