Contingent Orders
Bracket Orders
Introduction
A bracket order is an entry order that triggers a take-profit order and a stop-loss order when it completely fills. The take-profit order and the stop-loss order are one cancels other orders, so when one of them fills, LEAN cancels the other one. Bracket orders set an upper and a lower bound on the exit price of a trade.
Place Orders
To place a bracket order, call the BracketOrderbracket_order method with the Symbol, the entry quantity, the take-profit price, and the stop-loss price. When the entry order completely fills, LEAN triggers a take-profit limit order and a stop-loss stop market order with the opposite quantity. When one of these exit orders fills, LEAN cancels the other one. The method returns a list with the order tickets of the entry order, the take-profit order, and the stop-loss order.
var price = Securities[_symbol].Price; var tickets = BracketOrder(_symbol, 100, takeProfitPrice: price * 1.02m, stopLossPrice: price * 0.99m);
price = self.securities[self._symbol].price tickets = self.bracket_order(self._symbol, 100, take_profit_price=price * 1.02, stop_loss_price=price * 0.99)
By default, the entry is a market order. To use a limit order for the entry, pass a limitPricelimit_price argument. You can also provide a tag, order properties, and an asynchronous flag.
BracketOrder(_symbol, 100, takeProfitPrice, stopLossPrice, limitPrice: limitPrice, tag: tag, orderProperties: orderProperties);
self.bracket_order(self._symbol, 100, take_profit_price, stop_loss_price, limit_price=limit_price, tag=tag, order_properties=order_properties)
To bracket other entry order types, create the entry order request with the order factory. Call its Bracketbracket method and pass the request to the Orderorder method. The Bracketbracket method accepts a stopLossLimitPricestop_loss_limit_price argument to use a stop limit order for the stop loss. To reduce the stop-loss quantity when the take-profit order partially fills, pass ContingencyType.OneUpdatesOtherContingencyType.ONE_UPDATES_OTHER as the contingencyTypecontingency_type argument.
var entry = OrderFactory.StopMarketOrder(_symbol, 100, price * 1.01m)
.Bracket(price * 1.03m, price * 0.99m, stopLossLimitPrice: price * 0.985m, contingencyType: ContingencyType.OneUpdatesOther);
var tickets = Order(entry); entry = self.order_factory.stop_market_order(self._symbol, 100, price * 1.01).bracket(
price * 1.03, price * 0.99, stop_loss_limit_price=price * 0.985, contingency_type=ContingencyType.ONE_UPDATES_OTHER)
tickets = self.order(entry)
The following images show the two outcomes of a bracket order. In the first image, the entry order fills and triggers the exit orders. Then the price rises to the take-profit price, so the take-profit order fills and LEAN cancels the stop-loss order:
In the second image, the entry order fills and then the price falls to the stop price. The stop-loss order fills and LEAN cancels the take-profit order:
Place Related Orders
The exit orders that the BracketOrderbracket_order and Bracketbracket methods create can't trigger other orders. To make an exit trigger another order, build the bracket from its parts with the order factory. For more information, see Custom Contingent Orders.
- Call the
Triggerstriggersmethod on the exit order request with the order to trigger. - Relate the exit order requests with the
OrderFactory.OneCancelsOtherorder_factory.one_cancels_othermethod. - Pass the exit order requests to the
Triggerstriggersmethod of the entry order request. - Pass the entry order request to the
Orderordermethod.
// The take profit triggers a new entry at the entry price.
var takeProfit = OrderFactory.LimitOrder(_symbol, -100, price * 1.02m)
.Triggers(OrderFactory.LimitOrder(_symbol, 100, price * 0.999m));
var stopLoss = OrderFactory.StopMarketOrder(_symbol, -100, price * 0.99m);
var entry = OrderFactory.LimitOrder(_symbol, 100, price * 0.999m)
.Triggers(OrderFactory.OneCancelsOther(takeProfit, stopLoss));
var tickets = Order(entry); # The take profit triggers a new entry at the entry price.
take_profit = self.order_factory.limit_order(self._symbol, -100, price * 1.02).triggers(
self.order_factory.limit_order(self._symbol, 100, price * 0.999))
stop_loss = self.order_factory.stop_market_order(self._symbol, -100, price * 0.99)
entry = self.order_factory.limit_order(self._symbol, 100, price * 0.999).triggers(
self.order_factory.one_cancels_other(take_profit, stop_loss))
tickets = self.order(entry)
The order that the exit triggers is a grandchild of the entry. The brokerage holds it until the exit completely fills. If the other exit fills first, LEAN cancels the exit and its grandchild. The Orderorder method returns the order tickets of the entry, the take profit, the order it triggers, and the stop loss, in that order.
Monitor Orders
Each order in the set has its own order ticket, which fills and closes independently. To get the relationships of an order, use the Contingencycontingency property of the order ticket. A child order that waits for its parent to fill has the OrderStatus.SubmittedOrderStatus.SUBMITTED status. To check if the brokerage still holds the order, use the IsWaitingForTriggeris_waiting_for_trigger property of the contingency.
foreach (var ticket in tickets)
{
Debug($"{ticket.OrderId} held: {ticket.Contingency.IsWaitingForTrigger}; Quantity filled: {ticket.QuantityFilled}");
} for ticket in tickets:
self.debug(f"{ticket.order_id} held: {ticket.contingency.is_waiting_for_trigger}; Quantity filled: {ticket.quantity_filled}")
The Contingencycontingency property is an OrderContingency object, which has the following attributes:
The Linkslinks property holds a ContingencyLink object for each relationship of the order, which has the following attributes:
The GetOpenOrdersRemainingQuantityget_open_orders_remaining_quantity method of the transaction manager excludes the orders that wait for their parent to fill. At most one order of an OCO or OUO set fills, so the method includes only the largest remaining quantity of each set.
Update Orders
You can update any open order in the set, including a child order that waits for its parent to fill. To update an order, pass an UpdateOrderFields object to the Updateupdate method on the OrderTicket, or call one of its update methods. An update doesn't release a held order. If you give a held limit order a marketable price, the order fills only after its parent fills.
// Move the take profit.
var response = tickets[1].UpdateLimitPrice(price * 1.03m, "Updated take profit");
if (response.IsSuccess)
{
Debug("Order updated successfully");
} # Move the take profit.
response = tickets[1].update_limit_price(price * 1.03, "Updated take profit")
if response.is_success:
self.debug("Order updated successfully")
Some brokerages don't support updates to contingent orders or to their quantity. To change these orders, cancel the set and place it again.
Cancel Orders
To cancel an order in the set, call the Cancelcancel method on the OrderTicket. LEAN also cancels the orders that depend on it. If you cancel a parent order, LEAN cancels the orders it triggers and the orders they trigger in turn. If you cancel a member of an OCO or OUO set, LEAN cancels the rest of the set. This rule applies to working orders and to orders that wait for their parent to fill. The parent of the set keeps working.
// Cancel the entry, which also cancels the take profit and the stop loss.
var response = tickets[0].Cancel("Cancel entry, which also cancels the take profit and the stop loss");
if (response.IsSuccess)
{
Debug("Order successfully cancelled");
} # Cancel the entry, which also cancels the take profit and the stop loss.
response = tickets[0].cancel("Cancel entry, which also cancels the take profit and the stop loss")
if response.is_success:
self.debug("Order successfully cancelled")
The message of the cancel order event of each dependent order names the order that caused the cancellation.
Backtest Fills
In backtests, LEAN simulates how the brokerage manages the set with the following rules:
- When a parent order fills, LEAN triggers the child orders at the fill time. Triggered market orders fill right away. Other triggered orders require new data before they can fill, so they don't fill with prices from before the trigger.
- If an OCO or OUO order and its siblings can all fill with the same data, LEAN fills the stop orders first. For a bracket order, the stop loss fills and LEAN cancels the take profit.
- A triggered trailing stop order sets its initial stop price from the market price at the trigger time.
- LEAN measures the day time in force of a child order from the time its parent fills.
For more information about how LEAN models order fills in backtests, see Trade Fills.
Buying Power
When you place a set of contingent orders, LEAN checks the buying power of the orders that start working right away. LEAN checks the buying power of a child order when its parent fills, so you can place child orders larger than your current buying power. In cash accounts, LEAN reserves the buying power of only the largest order in each set of OCO or OUO orders.
Brokerage Support
Each brokerage has a set of assets and order types they support. To avoid issues with contingent 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 contingent orders, see the Orders section of the brokerage model documentation.
The following brokerages support contingent orders:
Brokerages that support contingent orders can limit the contingency types, the number of orders in a set, the securities in a set, combo order members, and chains. If the brokerage model doesn't support a set of contingent orders, LEAN invalidates every order in the set.
Examples
The following examples demonstrate common practices for using contingent orders.
Example 1: Bracket Orders on New Highs
The following algorithm buys SPY when its price closes above the highest high of the previous 20 days. A bracket order places the take profit 2% above the close price and the stop loss 1% below the close price. The brokerage manages the exit orders, so the algorithm only places a new bracket order after the previous one closes the position.
public class ContingentOrdersExampleAlgorithm : QCAlgorithm
{
private Symbol _spy;
private Maximum _max;
public override void Initialize()
{
SetStartDate(2024, 1, 1);
SetEndDate(2024, 12, 31);
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
_max = MAX(_spy, 20, Resolution.Daily, Field.High);
WarmUpIndicator(_spy, _max, Resolution.Daily);
}
public override void OnData(Slice slice)
{
// Wait until the previous bracket order closes the position.
if (Portfolio.Invested || Transactions.GetOpenOrders(_spy).Count > 0 || !slice.Bars.TryGetValue(_spy, out var bar))
{
return;
}
if (bar.Close > _max.Previous.Value)
{
var quantity = CalculateOrderQuantity(_spy, 0.5m);
BracketOrder(_spy, quantity, takeProfitPrice: Math.Round(bar.Close * 1.02m, 2), stopLossPrice: Math.Round(bar.Close * 0.99m, 2));
}
}
} class ContingentOrdersExampleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_end_date(2024, 12, 31)
self._spy = self.add_equity("SPY", Resolution.DAILY).symbol
self._max = self.max(self._spy, 20, Resolution.DAILY, Field.HIGH)
self.warm_up_indicator(self._spy, self._max, Resolution.DAILY)
def on_data(self, slice: Slice) -> None:
# Wait until the previous bracket order closes the position.
bar = slice.bars.get(self._spy)
if self.portfolio.invested or self.transactions.get_open_orders(self._spy) or not bar:
return
if bar.close > self._max.previous.value:
quantity = self.calculate_order_quantity(self._spy, 0.5)
self.bracket_order(self._spy, quantity, take_profit_price=round(bar.close * 1.02, 2), stop_loss_price=round(bar.close * 0.99, 2))
Example 2: Re-Enter After the Take Profit
The following algorithm buys SPY with a bracket order when it has no position. The entry is 1% below the close price. The take profit is 2% above the close price, and the stop loss is 2% below it. When the take profit fills, it triggers a new entry at the entry price. When the new entry fills, the algorithm places new exit orders as one cancels other orders.
public class BracketReEntryExampleAlgorithm : QCAlgorithm
{
private Symbol _spy;
public override void Initialize()
{
SetStartDate(2024, 1, 1);
SetEndDate(2024, 12, 31);
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
}
public override void OnData(Slice slice)
{
if (Transactions.GetOpenOrders(_spy).Count > 0 || !slice.Bars.TryGetValue(_spy, out var bar))
{
return;
}
var entryPrice = Math.Round(bar.Close * 0.99m, 2);
var takeProfitPrice = Math.Round(bar.Close * 1.02m, 2);
var stopLossPrice = Math.Round(bar.Close * 0.98m, 2);
if (Portfolio[_spy].Invested)
{
// Exit the position of the new entry.
OneCancelsOtherOrder(new List<SubmitOrderRequest>
{
OrderFactory.LimitOrder(_spy, -100, takeProfitPrice),
OrderFactory.StopMarketOrder(_spy, -100, stopLossPrice)
});
return;
}
var takeProfit = OrderFactory.LimitOrder(_spy, -100, takeProfitPrice)
.Triggers(OrderFactory.LimitOrder(_spy, 100, entryPrice));
var stopLoss = OrderFactory.StopMarketOrder(_spy, -100, stopLossPrice);
Order(OrderFactory.LimitOrder(_spy, 100, entryPrice).Triggers(OrderFactory.OneCancelsOther(takeProfit, stopLoss)));
}
} class BracketReEntryExampleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_end_date(2024, 12, 31)
self._spy = self.add_equity("SPY", Resolution.DAILY).symbol
def on_data(self, slice: Slice) -> None:
bar = slice.bars.get(self._spy)
if self.transactions.get_open_orders(self._spy) or not bar:
return
entry_price = round(bar.close * 0.99, 2)
take_profit_price = round(bar.close * 1.02, 2)
stop_loss_price = round(bar.close * 0.98, 2)
if self.portfolio[self._spy].invested:
# Exit the position of the new entry.
self.one_cancels_other_order([
self.order_factory.limit_order(self._spy, -100, take_profit_price),
self.order_factory.stop_market_order(self._spy, -100, stop_loss_price)
])
return
take_profit = self.order_factory.limit_order(self._spy, -100, take_profit_price).triggers(
self.order_factory.limit_order(self._spy, 100, entry_price))
stop_loss = self.order_factory.stop_market_order(self._spy, -100, stop_loss_price)
self.order(self.order_factory.limit_order(self._spy, 100, entry_price).triggers(
self.order_factory.one_cancels_other(take_profit, stop_loss)))