Order Types
Combo Limit Orders
Introduction
Combo limit orders are individual orders that contain limit orders for muliple securities. Combo limit orders are different from combo leg limit orders because you must set the limit price of all the leg orders to be the same with combo limit orders. With combo leg limit orders, you can create the order without forcing each leg to have the same limit price. Combo limit orders currently only work for trading Option contracts and their underlying Equities.
Place Orders
To send a combo limit order, create multiple Leg objects to represent the legs of the combo order, then call the ComboLimitOrdercombo_limit_order method. At least one leg must have a positive quantity and a tleast one leg must have a negative quantity. The legs must each target a unique contract, but don't set the OrderPrice property for any of the legs. 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&lgt;Leg>()
{
Leg.Create(contracts[0].Symbol, 1),
Leg.Create(contracts[1].Symbol, -1)
};
// Calculate limit price
var limitPrice = Math.Round(legs.Select(leg => Securities[leg.Symbol].Close).Sum() * 0.95m, 2);
// Place order
ComboLimitOrder(legs, quantity: 1, limitPrice: limitPrice);
} 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]
for i, contract in enumerate(contracts):
legs.append(Leg.create(contract.symbol, quantities[i]))
# Calculate limit price
limit_price = round(sum([self.securities[leg.symbol].close for leg in legs]) * 0.95, 2)
# Place order
self.combo_limit_order(legs, 1, limit_price)
The quantity of the legs sets the ratio of the leg orders while the quantity argument of the ComboLimitOrdercombo_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 ComboLimitOrdercombo_limit_order method.
ComboLimitOrder(legs, quantity, limitPrice, tag: tag, orderProperties: orderProperties);
self.combo_limit_order(legs, quantity, limit_price, tag=tag, order_properties=order_properties)
Monitor Order Fills
Combo limit orders fill all the legs at the same time. Each leg can fill when the sum of the security prices of the legs pass the limit price of the combo order. To monitor the fills of your order, save a reference to the order tickets.
var tickets = ComboLimitOrder(legs, 1, limitPrice);
foreach (var ticket in tickets)
{
Debug($"Symbol: {ticket.Symbol}; Quantity filled: {ticket.QuantityFilled}; Fill price: {ticket.AverageFillPrice}");
} tickets = self.combo_limit_order(legs, 1, limit_price)
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 limit orders in each leg 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. To update the limit price of the combo order, you only need to update the limit price of one of the leg orders. 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 = ComboLimitOrder(legs, quantity: 1, limitPrice: limitPrice);
// Update the leg orders
foreach (var ticket in tickets)
{
var response = ticket.Update(new UpdateOrderFields()
{
Quantity = 2 * Math.Sign(ticket.Quantity),
LimitPrice = ticket.Get(OrderField.LimitPrice) + 0.01m,
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_limit_order(legs, 1, limit_price)
# Update the leg orders
for ticket in tickets:
update_settings = UpdateOrderFields()
update_settings.quantity = 2 * np.sign(ticket.quantity)
update_settings.limit_price = ticket.get(OrderField.LIMIT_PRICE) + 0.01
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_priceUpdateQuantityupdate_quantityUpdateTagupdate_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 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()
Asynchronous Orders
When you trade a large portfolio of assets, you may want to send orders in batches and not wait for the response of each one. To send asynchronous orders, set the asynchronous argument to trueTrue.
ComboLimitOrder(legs, 1, limitPrice, asynchronous: true);
self.combo_limit_order(legs, 1, limit_price, asynchronous=True);
Brokerage Support
Each brokerage has a set of assets and order types they support. To avoid issues with combo limit orders, set the brokerage model to a brokerage that supports them.
SetBrokerageModel(BrokerageName.QuantConnectBrokerage);
self.set_brokerage_model(BrokerageName.QUANTCONNECT_BROKERAGE)
To check if your brokerage has any special requirements for combo limit orders, see the Orders section of the brokerage model documentation.
Examples
The following backtest verifies the
ComboLimitOrder
combo_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:
| Time | Symbol | Price | Quantity | Type | Status | Value | Tag |
|---|---|---|---|---|---|---|---|
| 2015-12-24T09:31:00Z | GOOG 16011SC00745000 | 16.50 | 1 | Buy | Filled | 16.50 | |
| 2015-12-24T09:31:00Z | GOOG 160115C00747500 | 14.60 | -1 | Sell | Filled | -14.60 |
On December 24, 2015 at 9:31 AM Eastern Time (ET), the algorithm places a combo limit order to buy one GOOG 16011SC00745000 contract and sell one GOOG 160115C00747500 contracts. The limit price is 75% of the contract spread, which equals $2.02. The combo order doesn't fill immediately because the contract spread $2.40 > $2.02. By 9:36 AM, the spread drops to $1.90, which is below the limit price, so the fill model fills the combo limit order.
To reproduce these results, backtest the following algorithm:
public class ComboMarketOrderAlgorithm : QCAlgorithm
{
private List<OrderTicket> _tickets = new();
private decimal _limitPrice;
public override void Initialize()
{
SetStartDate(2024, 12, 24);
SetEndDate(2024, 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),
Leg.Create(contracts[1].Symbol, -1)
};
// Calculate limit price
_limitPrice = Math.Round((slice.QuoteBars[contracts[0].Symbol].Ask.Low - slice.QuoteBars[contracts[1].Symbol].Bid.Low) * 0.75m, 2);
// Place order
_tickets = ComboLimitOrder(legs, 1, _limitPrice);
}
}
else
{
if (Time.Hour == 9 && Time.Minute == 37)
{
Quit();
return;
}
// Log the limit price and aggregate contract price
var price = 0.0m;
foreach (var ticket in _tickets)
{
var quoteBar = slice.QuoteBars[ticket.Symbol];
price += (ticket.Quantity > 0 ? quoteBar.Ask : quoteBar.Bid).Low * Math.Sign(ticket.Quantity);
}
Log($"{Time}. Limit price: {_limitPrice}; Aggregate price: {Math.Round(price, 2)}; Ready to fill: {price < _limitPrice}");
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.Filled)
{
Log($"{Time} -- Order {orderEvent.OrderId} filled at {orderEvent.FillPrice}");
}
}
} import itertools
class ComboMarketOrderAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2024, 12, 24)
self.set_end_date(2024, 12, 31)
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, -1]
legs = []
for i, contract in enumerate(contracts[:2]):
legs.append(Leg.create(contract.symbol, quantities[i]))
# Calculate limit price
self.limit_price = round((slice.quote_bars[contracts[0].symbol].ask.low - slice.quote_bars[contracts[1].symbol].bid.low) * 0.75, 2)
# Place order
self.tickets = self.combo_limit_order(legs, 1, self.limit_price)
else:
if self.time.hour == 9 and self.time.minute == 37:
self.quit()
return
price = 0
for ticket in self.tickets:
quote_bar = slice.quote_bars[ticket.symbol]
price += ((quote_bar.ask if ticket.quantity > 0 else quote_bar.bid).low * np.sign(ticket.quantity))
self.log(f"{self.time}. Limit price: {self.limit_price}; Aggregate price: {round(price, 2)}; Ready to fill: {price < self.limit_price}")
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}")