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")
Examples
The following example demonstrates a helper class for OCO orders. This is not an officially supported OCO order type as it's possible for both the take profit and stop loss orders to fill in the same bar. Before you use this approach, verify that your brokerage model supports stop market orders and limit orders.
public class OCOTrade
{
private readonly QCAlgorithm _algorithm;
private readonly Symbol _symbol;
private readonly decimal _takeProfitPrice, _stopLossPrice;
public OrderTicket Entry, TakeProfit, StopLoss;
public OCOTrade(QCAlgorithm algorithm, Symbol symbol, decimal quantity, decimal entryStopPrice,
decimal takeProfitPrice, decimal stopLossPrice)
{
_algorithm = algorithm;
_symbol = symbol;
_takeProfitPrice = takeProfitPrice;
_stopLossPrice = stopLossPrice;
Entry = _algorithm.StopMarketOrder(_symbol, quantity, entryStopPrice);
TakeProfit = null;
StopLoss = null;
}
public void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled)
return;
if (Entry != null && Entry.OrderId == orderEvent.OrderId)
{
var quantity = orderEvent.FillQuantity;
TakeProfit = _algorithm.LimitOrder(_symbol, -quantity, _takeProfitPrice);
StopLoss = _algorithm.StopMarketOrder(_symbol, -quantity, _stopLossPrice);
Entry = null;
}
else if (TakeProfit != null && TakeProfit.OrderId == orderEvent.OrderId)
{
StopLoss.Cancel();
TakeProfit = null;
StopLoss = null;
}
else if (StopLoss != null && StopLoss.OrderId == orderEvent.OrderId)
{
TakeProfit.Cancel();
TakeProfit = null;
StopLoss = null;
}
}
}
public class InsideDayBreakout : QCAlgorithm
{
private Security _forex;
private int _riskPct;
private OCOTrade _longTrade;
private OCOTrade _shortTrade;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 12, 31);
SetCash(1000);
SetTimeZone(TimeZones.Utc);
SetBrokerageModel(BrokerageName.OandaBrokerage, AccountType.Margin);
_forex = AddForex("USDJPY");
_forex.Session.Size = 3;
foreach (var bar in History<QuoteBar>(_forex.Symbol, 3))
_forex.Session.Update(bar);
_riskPct = 7;
SetWarmUp(TimeSpan.FromDays(4));
}
public override void OnWarmupFinished()
{
// Add a Scheduled event to rebalance the portfolio every weekday.
Schedule.On(
DateRules.Every(DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday),
TimeRules.AfterMarketOpen(_forex.Symbol, 1),
Rebalance
);
}
private void Rebalance()
{
if (!_forex.Session.IsReady || Portfolio.Invested)
return;
var session = _forex.Session[1];
var high = session.High;
var low = session.Low;
var prevHigh = _forex.Session[2].High;
var prevLow = _forex.Session[2].Low;
if (!(high < prevHigh && low > prevLow))
return;
// Replace any pending entry orders from a previous inside day.
Liquidate();
var insideHigh = Math.Round(high, 3, MidpointRounding.AwayFromZero);
var insideLow = Math.Round(low, 3, MidpointRounding.AwayFromZero);
var dayRange = insideHigh - insideLow;
// Risk amount in USD.
var riskUsd = Portfolio.Cash * (_riskPct / 100m);
// Risk per unit in USD terms (dayRange is in JPY, convert to USD via close price).
var riskPerUnit = dayRange / session.Close;
// OANDA max margin 50:1.
var riskQuantity = (int)(riskUsd / riskPerUnit);
var maxQuantity = (int)((Portfolio.Cash * 48) / session.Close);
var quantity = Math.Min(riskQuantity, maxQuantity);
if (quantity == 0)
return;
_longTrade = new OCOTrade(this, _forex.Symbol, quantity, insideHigh,
takeProfitPrice: Math.Round(insideHigh + dayRange, 3),
stopLossPrice: insideLow);
_shortTrade = new OCOTrade(this, _forex.Symbol, -quantity, insideLow,
takeProfitPrice: Math.Round(insideLow - dayRange, 3),
stopLossPrice: insideHigh);
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
foreach (var trade in new[] { _longTrade, _shortTrade })
if (trade != null)
trade.OnOrderEvent(orderEvent);
}
} class OCOTrade:
def __init__(self, algorithm, symbol, quantity, entry_stop_price, take_profit_price, stop_loss_price):
self._algorithm = algorithm
self._symbol = symbol
self._take_profit_price = take_profit_price
self._stop_loss_price = stop_loss_price
self._entry = algorithm.stop_market_order(symbol, quantity, entry_stop_price)
self._take_profit = None
self._stop_loss = None
def on_order_event(self, order_event):
if order_event.status != OrderStatus.FILLED:
return
_id = order_event.order_id
if self._entry and self._entry.order_id == _id:
quantity = order_event.fill_quantity
self._take_profit = self._algorithm.limit_order(self._symbol, -quantity, self._take_profit_price)
self._stop_loss = self._algorithm.stop_market_order(self._symbol, -quantity, self._stop_loss_price)
self._entry = None
elif self._take_profit and self._take_profit.order_id == _id:
self._stop_loss.cancel()
self._take_profit = self._stop_loss = None
elif self._stop_loss and self._stop_loss.order_id == _id:
self._take_profit.cancel()
self._take_profit = self._stop_loss = None
class InsideDayBreakout(QCAlgorithm):
def initialize(self):
self.set_start_date(2024, 9, 1)
self.set_end_date(2024, 12, 31)
self.set_cash(1000)
self.set_time_zone(TimeZones.UTC)
self.set_brokerage_model(BrokerageName.OANDA_BROKERAGE, AccountType.MARGIN)
self._forex = self.add_forex("USDJPY")
self._forex.session.size = 3
for bar in self.history[QuoteBar](self._forex, 3):
self._forex.session.update(bar)
self._risk_pct = 7
self._long_trade = None
self._short_trade = None
self.set_warm_up(timedelta(4))
def on_warmup_finished(self):
# Add a Scheduled event to rebalance the portfolio monthly.
self.schedule.on(
self.date_rules.every([DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY]),
self.time_rules.after_market_open(self._forex, 1),
self._rebalance
)
def _rebalance(self):
if not self._forex.session.is_ready or self.portfolio.invested:
return
session = self._forex.session[1]
high, low = session.high, session.low
prev_high, prev_low = self._forex.session[2].high, self._forex.session[2].low
if not (high < prev_high and low > prev_low):
return
# Replace any pending entry orders from a previous inside day.
self.liquidate()
inside_high = round(high, 3)
inside_low = round(low, 3)
day_range = inside_high - inside_low
# Risk amount in USD.
risk_usd = self.portfolio.cash * (self._risk_pct / 100)
# Risk per unit in USD terms (day_range is in JPY, convert to USD via close price).
risk_per_unit = day_range / session.close
# OANDA max margin 50:1.
risk_quantity = int(risk_usd / risk_per_unit)
max_quantity = int((self.portfolio.cash * 48) / session.close)
quantity = min(risk_quantity, max_quantity)
if not quantity:
return
self._long_trade = OCOTrade(
self, self._forex, quantity, inside_high,
take_profit_price=round(inside_high + day_range, 3),
stop_loss_price=inside_low
)
self._short_trade = OCOTrade(
self, self._forex, -quantity, inside_low,
take_profit_price=round(inside_low - day_range, 3),
stop_loss_price=inside_high
)
def on_order_event(self, order_event):
for trade in [self._long_trade, self._short_trade]:
if trade is not None:
trade.on_order_event(order_event)