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 OnOrderEvents 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.MarketOrder("SPY", 1)
fill_price = self.ticket.AverageFillPrice

# Get the fill price from the OnOrderEvent event handler
def OnOrderEvent(self, orderEvent: OrderEvent) -> None:
    if orderEvent.Status == OrderStatus.Filled and self.ticket.OrderId == orderEvent.OrderId:
        fill_price = orderEvent.FillPrice

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 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 OnOrderEvent(self, orderEvent: OrderEvent) -> None:
    if orderEvent.Status == OrderStatus.Filled:
        if orderEvent.OrderId == self.ticket.OrderId:
            self.stop_loss = self.StopMarketOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*0.95)
            self.take_profit = self.LimitOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*1.10)

        elif self.stop_loss is not None and orderEvent.OrderId == self.stop_loss.OrderId:
            self.take_profit.Cancel()

        elif self.take_profit is not None and orderEvent.OrderId == self.take_profit.OrderId:
            self.stop_loss.Cancel()

Simulate Orders

To simulate OCO orders, track the asset price in the OnData 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 OnData(self, slice: Slice) -> None:
    if not self.Portfolio.Invested:
        ticket = self.MarketOrder("SPY", 1)
        self.entry_price = ticket.AverageFillPrice

    if slice.Bars.ContainsKey("SPY"):
        if slice.Bars["SPY"].Price >= self.entry_price * 1.10:
            self.Liquidate("SPY", -1, "take profit")

        elif slice.Bars["SPY"].Price <=> self.entry_price * 0.95:
            self.Liquidate("SPY", -1, "stop loss")

Trailing Stop Loss Orders

A trailing stop loss order is a dynamic stop loss that moves towards the asset price as the asset trades away from the stop price. For example, if you set a trailing stop loss to 10%, the furthest away it will ever be from the asset price is 10%. If it's a sell trailing stop loss order that's below the asset price and the asset price increases such that the trailing stop is more than 10% away, the trailing stop loss moves up to be 10% below the asset price. If the asset price decreases, the trailing stop loss doesn't move.

To set a trailing stop loss order, see the Buy and Hold with a Trailing Stop Bootcamp lesson.

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.

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: