Order Types

Other Order Types

Introduction

We are often asked to support other order types like one cancels the other, trailing stop, and multi-leg orders. Currently, LEAN doesn't support these order types, but we will add them over time. Part of the difficulty of implementing them is the incomplete brokerage support.

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")

Multi-Leg Orders

Multi-leg orders are orders that contain multiple sub-orders. Examples of multi-leg orders include Option strategies like spreads, straddles, and strangles. You can manually implement other types of multi-leg orders with the built-in order types.

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)

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: