Order Types

Combo Leg Limit Orders

Introduction

Combo leg limit orders are individual orders that contain limit orders for muliple securities. Combo leg limit orders are different from combo limit orders because you can create combo leg limit orders without forcing each leg to have the same limit price. Combo leg limit orders currently only work for trading Option contracts.

Place Orders

To send a combo leg limit order, create multiple Leg objects to represent the legs of the combo order, then call the ComboLegLimitOrdercombo_leg_limit_order method. The legs must each target a unique contract. At least one leg must have a positive quantity and at least one leg must have a negative quantity. If you don't have sufficient capital for the order, it's rejected.

foreach (var kvp in slice.OptionChains)
{
    // Select contracts
    var contracts = kvp.Value.Contracts.Values.ToList();
    if (contracts.Count < 2) 
    {
        return;
    }

    // Create order legs
    var legs = new List<Leg>()
    {
        Leg.Create(contracts[0].Symbol, 1, contracts[0].LastPrice * 0.98m),
        Leg.Create(contracts[1].Symbol, -1, contracts[1].LastPrice * 1.02m)
    };

    // Place order
    ComboLegLimitOrder(legs, 1);
}
for canonical_symbol, chain in slice.option_chains.items():
    # Select contracts
    contracts = [c for c in chain][:2]
    if len(contracts) < 2:
        return

    # Create order legs            
    legs = []
    quantities = [1, -1]
    factors = [0.98, 1.02]
    for i, contract in enumerate(contracts):
        legs.append(Leg.create(contract.symbol, quantities[i], contract.last_price * factors[i]))
    
    # Place order
    self.combo_leg_limit_order(legs, 1)

The quantity of the legs sets the ratio of the leg orders while the quantity argument of the ComboLegLimitOrdercombo_leg_limit_order method sets the combo order size and acts as a global multiplier. In the preceding example, if we set the global multiplier to two, then the algorithm buys two units of the first contract and sells two units of the second contract. The quantity also sets the order direction of the combo limit order, which affects how the fill model fills the order.

You can also provide a tag and order properties to the ComboLegLimitOrdercombo_leg_limit_order method.

ComboLegLimitOrder(legs, quantity, tag: tag, orderProperties: orderProperties);
self.combo_leg_limit_order(legs, quantity, tag=tag, order_properties=order_properties)

Monitor Order Fills

Combo leg limit orders fill all the legs at the same time. Each leg can fill when the security price passes the limit price of the leg. To monitor the fills of your order, save a reference to the order tickets.

var tickets = ComboLegLimitOrder(legs, 1);
foreach (var ticket in tickets)
{
    Debug($"Symbol: {ticket.Symbol}; Quantity filled: {ticket.QuantityFilled}; Fill price: {ticket.AverageFillPrice}");
}
tickets = self.combo_leg_limit_order(legs, 1)
for ticket in tickets:
    self.debug(f"Symbol: {ticket.symbol}; Quantity filled: {ticket.quantity_filled}; Fill price: {ticket.average_fill_price}")

For more information about how LEAN models order fills in backtests, see Trade Fills.

Update Orders

You can update the quantity, limit price, and tag of the leg limit orders until the combo order fills or the brokerage prevents modifications. To update an order, pass an UpdateOrderFields object to the Updateupdate method on the OrderTicket. If you don't have the order ticket, get it from the transaction manager. The Updateupdate method returns an OrderResponse to signal the success or failure of the update request.

// Create a new order and save the order ticket
var tickets = ComboLegLimitOrder(legs, 1);

// Update the leg orders
foreach (var ticket in tickets)
{
    var direction = Math.Sign(ticket.Quantity);
    var response = ticket.Update(new UpdateOrderFields() 
    {
        Quantity = 2 * direction,
        LimitPrice = ticket.Get(OrderField.LimitPrice) + 0.01m * direction,
        Tag = $"Update #{ticket.UpdateRequests.Count + 1}"
    }); 

    // Check if the update was successful
    if (response.IsSuccess) 
    {
        Debug($"Order updated successfully for {ticket.Symbol}");
    }
}
# Create a new order and save the order tickets
tickets = self.combo_leg_limit_order(legs, 1)

# Update the leg orders
for ticket in tickets:
    direction = np.sign(ticket.quantity)
    update_settings = UpdateOrderFields()
    update_settings.quantity = 2 * direction
    update_settings.limit_price = ticket.get(OrderField.LIMIT_PRICE) + 0.01 * direction
    update_settings.tag = f"Update #{len(ticket.update_requests) + 1}"
    response = ticket.update(update_settings)

    # Check if the update was successful
    if response.is_success:
        self.debug(f"Order updated successfully for {ticket.symbol}")

To update individual fields of an order, call any of the following methods:

  • UpdateLimitPriceupdate_limit_price
  • UpdateQuantityupdate_quantity
  • UpdateTagupdate_tag
var limitResponse = ticket.UpdateLimitPrice(limitPrice, tag);

var quantityResponse = ticket.UpdateQuantity(quantity, tag);

var tagResponse = ticket.UpdateTag(tag);

response = ticket.update_limit_price(limit_price, tag)

response = ticket.update_quantity(quantity, tag)

response = ticket.update_tag(tag)

When you update an order, LEAN creates an UpdateOrderRequest object, which have the following attributes:

To get a list of UpdateOrderRequest objects for an order, call the UpdateRequestsupdate_requests method.

var updateRequests = ticket.UpdateRequests();
update_requests = ticket.update_requests()

Cancel Orders

To cancel a combo leg limit order, call the Cancelcancel method on the OrderTicket. If you don't have the order ticket, get it from the transaction manager. The Cancelcancel method returns an OrderResponse object to signal the success or failure of the cancel request.

var response = ticket.Cancel("Cancelled trade");
if (response.IsSuccess)
{
    Debug("Order successfully cancelled");
}
response = ticket.cancel("Cancelled Trade")
if response.is_success:
    self.debug("Order successfully cancelled")

When you cancel an order, LEAN creates a CancelOrderRequest, which have the following attributes:

To get the CancelOrderRequest for an order, call the CancelRequestcancel_order_request method on the order ticket. The method returns nullNone if the order hasn't been cancelled.

var request = ticket.cancel_order_request();
request = ticket.cancel_order_request()

Brokerage Support

Each brokerage has a set of assets and order types they support. To avoid issues with combo leg limit orders, set the brokerage model to a brokerage that supports them.

SetBrokerageModel(BrokerageName.QuantConnectBrokerage);
self.set_brokerage_model(BrokerageName.QuantConnectBrokerage)

To check if your brokerage has any special requirements for combo leg limit orders, see the Orders section of the brokerage model documentation.

Requirements

Combo leg limit orders can be submitted at any time for all security types.

If your algorithm subscribes to extended market hours, they can be filled outside regular trading hours.

Examples

The following backtest verifies the ComboLegLimitOrdercombo_leg_limit_order behavior. The algorithm buys one contract and sells one contract at the same time. The following table shows the two trades in the backtest:

TimeSymbolPriceQuantityTypeStatusValueTag
2015-12-24T09:31:00ZGOOG 16011SC0074500016.102BuyFilled32.20Update #72
2015-12-24T09:31:00ZGOOG 160115C0074750014.11515-2SellFilled-28.2303Update #72

On December 24, 2015 at 9:31 AM Eastern Time (ET), the algorithm places a combo leg limit order to buy one GOOG 16011SC00745000 contract and sell two GOOG 160115C00747500 contracts. The limit price of both orders is 99.9% of the respective contract price, which is $16.2837 for GOOG 16011SC00745000 and $14.83515 for GOOG 160115C00747500. The combo order doesn't fill immediately, so the algorithm updates the leg orders at each time step. During the first update, the algorithm sets the quantity of the GOOG 160115C00747500 leg to -2. During each update, the limit price moves $0.01 closer to the market. That is, the limit price of GOOG 16011SC00745000 increases by $0.01 and the limit price of GOOG 160115C00747500 decreases by $0.01. After the 72nd update, the ask low price is below the limit price of the leg to buy GOOG 16011SC00745000 and the bid high price is above the limit price of the leg to sell GOOG 160115C00747500, so the fill model fills the combo leg limit order at 10:44 AM ET.

To reproduce these results, backtest the following algorithm:

public class ComboLegLimitOrderAlgorithm : QCAlgorithm
{
    private List<OrderTicket> _tickets = new();

    public override void Initialize()
    {
        SetStartDate(2015, 12, 24);
        SetEndDate(2015, 12, 31);
        SetCash(100000);
        
        var option = AddOption("GOOG");
        option.SetFilter(minStrike: -2, maxStrike: 2, minExpiry: TimeSpan.FromDays(0), maxExpiry: TimeSpan.FromDays(180));
    }

    public override void OnData(Slice slice)
    {
        if (_tickets.IsNullOrEmpty())
        {
            foreach (var kvp in slice.OptionChains)
            {
                // Select contracts
                var contracts = kvp.Value.Where(contract => contract.Right == OptionRight.Call)
                    .GroupBy(x => x.Expiry)
                    .OrderBy(grouping => grouping.Key)
                    .First()
                    .OrderBy(x => x.Strike)
                    .ToList();

                if (contracts.Count < 2)
                {
                    return;
                }

                // Create order legs
                var legs = new List<Leg>()
                {
                    Leg.Create(contracts[0].Symbol, 1, slice[contracts[0].Symbol].Close * 0.999m),
                    Leg.Create(contracts[1].Symbol, -2, slice[contracts[1].Symbol].Close * 0.999m)
                };

                // Place order
                _tickets = ComboLegLimitOrder(legs, 1);
            }
        }
        else
        {
            if (Time.Hour == 10 && Time.Minute == 45)
            {
                Quit();
                return;
            }

            foreach (var ticket in _tickets)
            {
                var orderDirection = Math.Sign(ticket.Quantity);
                var limitPrice = ticket.Get(OrderField.LimitPrice);

                // Log the limit prices and contract prices
                var quoteBar = slice.QuoteBars[ticket.Symbol];
                var currentPrice = orderDirection == 1 ? quoteBar.Ask.Low : quoteBar.Bid.High;
                var readyToFill = orderDirection == 1 ? currentPrice < limitPrice : currentPrice > limitPrice;
                Log($"{Time} - {ticket.Symbol}. Current price: {currentPrice}; Limit price: {Math.Round(limitPrice, 2)}; Order direction: {orderDirection}; Ready to fill: {readyToFill}");

                // Update the leg orders
                var response = ticket.Update(new UpdateOrderFields() 
                {
                    Quantity = 2 * orderDirection,
                    LimitPrice = limitPrice + 0.01m * orderDirection,
                    Tag = $"Update #{ticket.UpdateRequests.Count + 1}"
                });
            }
        }
    }
    
    public override void OnOrderEvent(OrderEvent orderEvent)
    {
        if (orderEvent.Status == OrderStatus.Filled)
        {
            Log($"{Time} -- Order {orderEvent.OrderId} filled at {orderEvent.FillPrice}");
        }
    }
}
class ComboLegLimitOrderAlgorithm(QCAlgorithm):
    
    def initialize(self):
        self.set_start_date(2015, 12, 24) 
        self.set_end_date(2015, 12, 24)
        self.set_cash(100000) 
        option = self.add_option("GOOG")
        option.set_filter(min_strike=-2, max_strike=2, min_expiry=timedelta(days=0), max_expiry=timedelta(days=180))
        
        self.tickets = []

    def on_data(self, slice: Slice):
        if len(self.tickets) == 0:
            for canonical_symbol, chain in slice.option_chains.items():
                # Select contracts
                contracts = [contract for contract in chain if contract.right == OptionRight.CALL]
                contracts = [(key, list(group)) for key, group in itertools.groupby(contracts, key=lambda x: x.expiry)]
                contracts.sort(key=lambda x: x[0])
                contracts = contracts[0][1]
                contracts.sort(key=lambda x: x.strike)

                if len(contracts) < 2:
                    return

                # Create order legs   
                quantities = [1, -2]
                legs = []
                for i, contract in enumerate(contracts[:2]):
                    legs.append(Leg.create(contract.symbol, quantities[i], slice[contract.symbol].close * .999))

                # Place order
                self.tickets = self.combo_leg_limit_order(legs, 1)
        else:
            if self.time.hour == 10 and self.time.minute == 45:
                self.quit()
                return

            for ticket in self.tickets:
                order_direction = np.sign(ticket.quantity)
                limit_price = ticket.get(OrderField.LIMIT_PRICE)

                # Log the limit prices and contract prices
                quote_bar = slice.quote_bars[ticket.symbol]
                current_price = quote_bar.ask.low if order_direction == 1 else quote_bar.bid.high
                ready_to_fill = current_price < limit_price if order_direction == 1 else current_price > limit_price
                self.log(f"{self.time} - {ticket.symbol}. Current price: {current_price}; Limit price: {round(limit_price, 2)}; Direction: {order_direction}; Ready to fill: {ready_to_fill}")

                # Update the leg orders
                update_settings = UpdateOrderFields()
                update_settings.quantity = 2 * order_direction
                update_settings.limit_price = limit_price + 0.01 * order_direction
                update_settings.tag = f"Update #{len(ticket.update_requests) + 1}"
                response = ticket.update(update_settings)

    def on_order_event(self, orderEvent: OrderEvent) -> None:
        if orderEvent.status == OrderStatus.FILLED:
            self.log(f"{self.time} -- Order {orderEvent.order_id} filled at {orderEvent.fill_price}")

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: