Contingent Orders
One Updates Other Orders
Introduction
One updates other (OUO) orders are sets of orders that work at the same time. When one order in the set partially fills, LEAN reduces the remaining quantity of the rest of the set by the same proportion. When one order completely fills, LEAN cancels the rest of the set. For example, to exit a position in parts without selling more than you hold, place the exit orders as OUO orders.
Place Orders
To place a set of OUO orders, create an order request for each order with the order factory. Pass a list of the requests to the OneUpdatesOtherOrderone_updates_other_order method. All the orders start working right away. When one of them partially fills, LEAN reduces the remaining quantity of the rest of the set by the same proportion. For example, if a take-profit order for -100 shares fills -40 shares, LEAN reduces the stop-loss order from -100 shares to -60 shares. When one of the orders completely fills, LEAN cancels the rest of the set. The method returns a list with the order ticket of each order.
var tickets = OneUpdatesOtherOrder(new List<SubmitOrderRequest>
{
OrderFactory.LimitOrder(_symbol, -100, price * 1.02m, tag: "Take profit"),
OrderFactory.StopMarketOrder(_symbol, -100, price * 0.99m, tag: "Stop loss")
}); tickets = self.one_updates_other_order([
self.order_factory.limit_order(self._symbol, -100, price * 1.02, tag="Take profit"),
self.order_factory.stop_market_order(self._symbol, -100, price * 0.99, tag="Stop loss")
])
LEAN rounds the new quantities to the lot size of the security. If a new quantity rounds to zero, LEAN cancels the order.
The following images show two outcomes of an OUO set that exits a long position of 100 shares. In both images, the take-profit order fills 40 shares, so LEAN reduces the stop-loss order to 60 shares. In the first image, the price falls to the stop price, so the stop-loss order fills 60 shares and LEAN cancels the take-profit order:
In the second image, the price returns to the take-profit price. The take-profit order fills the other 60 shares and LEAN cancels the stop-loss order:
Place Related Orders
Each order in an OUO set can trigger other orders. To trigger orders after an OUO order fills, call the Triggerstriggers method on its order request before you relate it with the other orders. The brokerage holds the triggered orders until their parent completely fills. If another order in the set completely fills, LEAN cancels the parent and the orders it triggers. This rule applies even if the parent partially filled. A triggered order can trigger other orders in turn, which are grandchildren of the OUO order.
// Reverse the long position when the stop loss fills.
var stopLoss = OrderFactory.StopMarketOrder(_symbol, -100, price * 0.99m).Triggers(OrderFactory.MarketOrder(_symbol, -100));
var tickets = OneUpdatesOtherOrder(new List<SubmitOrderRequest>
{
OrderFactory.LimitOrder(_symbol, -100, price * 1.02m),
stopLoss
}); # Reverse the long position when the stop loss fills.
stop_loss = self.order_factory.stop_market_order(self._symbol, -100, price * 0.99).triggers(
self.order_factory.market_order(self._symbol, -100))
tickets = self.one_updates_other_order([
self.order_factory.limit_order(self._symbol, -100, price * 1.02),
stop_loss
])
The quantity of a triggered order doesn't change when LEAN reduces the quantity of its parent.
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.
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. To update an order, pass an UpdateOrderFields object to the Updateupdate method on the OrderTicket, or call one of its update methods.
// Move the stop loss.
var response = tickets[1].UpdateStopPrice(price * 0.985m, "Updated stop loss");
if (response.IsSuccess)
{
Debug("Order updated successfully");
} # Move the stop loss.
response = tickets[1].update_stop_price(price * 0.985, "Updated stop loss")
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 member of an OCO or OUO set, LEAN cancels the rest of the set.
// Cancel the take profit, which also cancels the stop loss.
var response = tickets[0].Cancel("Cancel take profit, which also cancels the stop loss");
if (response.IsSuccess)
{
Debug("Order successfully cancelled");
} # Cancel the take profit, which also cancels the stop loss.
response = tickets[0].cancel("Cancel take profit, which also cancels 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:
- 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.
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. 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 one updates other orders.
Example 1: Exit Illiquid Positions in Parts
The following algorithm buys IWM each day at 10 AM Eastern Time when it has no position. After the market order fills, the algorithm places a take-profit order and a stop-loss order as OUO orders. If the take-profit order partially fills, LEAN reduces the stop-loss quantity by the same proportion, so the exit orders never sell more than the position.
public class OneUpdatesOtherExampleAlgorithm : QCAlgorithm
{
private Symbol _iwm;
public override void Initialize()
{
SetStartDate(2024, 1, 1);
SetEndDate(2024, 3, 31);
_iwm = AddEquity("IWM", Resolution.Minute).Symbol;
Schedule.On(DateRules.EveryDay(_iwm), TimeRules.At(10, 0), Enter);
}
private void Enter()
{
if (Portfolio.Invested || Transactions.GetOpenOrders(_iwm).Count > 0)
{
return;
}
var fillPrice = MarketOrder(_iwm, 500).AverageFillPrice;
OneUpdatesOtherOrder(new List<SubmitOrderRequest>
{
OrderFactory.LimitOrder(_iwm, -500, Math.Round(fillPrice * 1.01m, 2), tag: "Take profit"),
OrderFactory.StopMarketOrder(_iwm, -500, Math.Round(fillPrice * 0.99m, 2), tag: "Stop loss")
});
}
} class OneUpdatesOtherExampleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_end_date(2024, 3, 31)
self._iwm = self.add_equity("IWM", Resolution.MINUTE).symbol
self.schedule.on(self.date_rules.every_day(self._iwm), self.time_rules.at(10, 0), self._enter)
def _enter(self) -> None:
if self.portfolio.invested or self.transactions.get_open_orders(self._iwm):
return
fill_price = self.market_order(self._iwm, 500).average_fill_price
self.one_updates_other_order([
self.order_factory.limit_order(self._iwm, -500, round(fill_price * 1.01, 2), tag="Take profit"),
self.order_factory.stop_market_order(self._iwm, -500, round(fill_price * 0.99, 2), tag="Stop loss")
])
Example 2: Reverse the Position on the Stop Loss
The following algorithm buys IWM each day at 10 AM Eastern Time when it has no position, and then places OUO exit orders. The stop-loss order triggers a market order that sells 500 more shares to open a short position. The short entry is a bracket order, so its exit orders are grandchildren of the stop-loss order. If the take profit completely fills first, LEAN cancels the stop loss, the short entry, and its exit orders.
public class OneUpdatesOtherGrandchildExampleAlgorithm : QCAlgorithm
{
private Symbol _iwm;
public override void Initialize()
{
SetStartDate(2024, 1, 1);
SetEndDate(2024, 3, 31);
_iwm = AddEquity("IWM", Resolution.Minute).Symbol;
Schedule.On(DateRules.EveryDay(_iwm), TimeRules.At(10, 0), Enter);
}
private void Enter()
{
if (Portfolio.Invested || Transactions.GetOpenOrders(_iwm).Count > 0)
{
return;
}
var fillPrice = MarketOrder(_iwm, 500).AverageFillPrice;
var stopLossPrice = Math.Round(fillPrice * 0.99m, 2);
// The short entry and its exit orders wait for the stop loss to fill.
var shortEntry = OrderFactory.MarketOrder(_iwm, -500).Bracket(Math.Round(stopLossPrice * 0.98m, 2), Math.Round(fillPrice, 2));
OneUpdatesOtherOrder(new List<SubmitOrderRequest>
{
OrderFactory.LimitOrder(_iwm, -500, Math.Round(fillPrice * 1.01m, 2)),
OrderFactory.StopMarketOrder(_iwm, -500, stopLossPrice).Triggers(shortEntry)
});
}
} class OneUpdatesOtherGrandchildExampleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_end_date(2024, 3, 31)
self._iwm = self.add_equity("IWM", Resolution.MINUTE).symbol
self.schedule.on(self.date_rules.every_day(self._iwm), self.time_rules.at(10, 0), self._enter)
def _enter(self) -> None:
if self.portfolio.invested or self.transactions.get_open_orders(self._iwm):
return
fill_price = self.market_order(self._iwm, 500).average_fill_price
stop_loss_price = round(fill_price * 0.99, 2)
# The short entry and its exit orders wait for the stop loss to fill.
short_entry = self.order_factory.market_order(self._iwm, -500).bracket(round(stop_loss_price * 0.98, 2), round(fill_price, 2))
self.one_updates_other_order([
self.order_factory.limit_order(self._iwm, -500, round(fill_price * 1.01, 2)),
self.order_factory.stop_market_order(self._iwm, -500, stop_loss_price).triggers(short_entry)
])