From f601cae4e11e460994f7115a6e6bda6fa06cbce7 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Thu, 24 Sep 2026 21:28:33 +0100 Subject: [PATCH] Add Contingent Orders page for LEAN #9828 Documents bracket, OCO, OTO, and OUO orders and the order factory. Replaces the manual OCO workaround on Other Order Types. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../01 Key Concepts/03 Order Types.html | 1 + .../03 Order Types/00.json | 1 + .../13 Contingent Orders/01 Introduction.html | 9 + .../02 Bracket Orders.html | 26 +++ .../03 One Cancels Other Orders.html | 23 ++ .../04 One Triggers Other Orders.html | 28 +++ .../05 One Updates Other Orders.html | 15 ++ .../06 Compose Contingent Orders.html | 52 +++++ .../13 Contingent Orders/07 Combo Orders.html | 23 ++ .../08 Monitor Orders.html | 18 ++ .../09 Update Orders.html | 16 ++ .../10 Cancel Orders.html | 16 ++ .../11 Backtest Fills.html | 10 + .../13 Contingent Orders/12 Buying Power.html | 1 + .../13 Brokerage Support.php | 6 + .../13 Contingent Orders/99 Examples.html | 51 ++++ .../13 Contingent Orders/metadata.json | 12 + .../99 Other Order Types/01 Introduction.html | 2 +- .../02 One Cancels the Other Orders.html | 111 --------- .../99 Other Order Types/99 Examples.html | 218 ------------------ .../99 Other Order Types/metadata.json | 6 +- 21 files changed, 312 insertions(+), 333 deletions(-) create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/01 Introduction.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/02 Bracket Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/03 One Cancels Other Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/04 One Triggers Other Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/05 One Updates Other Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/06 Compose Contingent Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/07 Combo Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/08 Monitor Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/09 Update Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/10 Cancel Orders.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/11 Backtest Fills.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/12 Buying Power.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/13 Brokerage Support.php create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/99 Examples.html create mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/metadata.json delete mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/02 One Cancels the Other Orders.html delete mode 100644 03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/99 Examples.html diff --git a/03 Writing Algorithms/22 Trading and Orders/01 Key Concepts/03 Order Types.html b/03 Writing Algorithms/22 Trading and Orders/01 Key Concepts/03 Order Types.html index 8cd3ac8fa5..ccb3e64c80 100644 --- a/03 Writing Algorithms/22 Trading and Orders/01 Key Concepts/03 Order Types.html +++ b/03 Writing Algorithms/22 Trading and Orders/01 Key Concepts/03 Order Types.html @@ -12,5 +12,6 @@
  • Combo market
  • Combo limit
  • Combo leg limit
  • +
  • Contingent
  • Option exercise
  • \ No newline at end of file diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/00.json b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/00.json index a1fdaa6eb7..a920563dd0 100644 --- a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/00.json +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/00.json @@ -17,6 +17,7 @@ "10" : "", "11" : "", "12" : "", + "13" : "", "20" : "", "99" : "" } diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/01 Introduction.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/01 Introduction.html new file mode 100644 index 0000000000..511e9c7b13 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/01 Introduction.html @@ -0,0 +1,9 @@ +

    Contingent orders are sets of orders with related lifecycles. When an order in the set fills or you cancel it, the brokerage triggers, cancels, or resizes the other orders in the set. The brokerage manages the whole set, so a fill and a cancel request can't race each other and leave a position without its exit orders. LEAN supports the following contingency types:

    + + + +

    You can compose these types. For example, a bracket order is an entry order that triggers a take-profit order and a stop-loss order, which cancel each other. The members of a set can be combo orders.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/02 Bracket Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/02 Bracket Orders.html new file mode 100644 index 0000000000..bffe24ba97 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/02 Bracket Orders.html @@ -0,0 +1,26 @@ +

    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)
    +
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/03 One Cancels Other Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/03 One Cancels Other Orders.html new file mode 100644 index 0000000000..c42ac97901 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/03 One Cancels Other Orders.html @@ -0,0 +1,23 @@ +

    To place a set of OCO orders, create an order request for each order with the order factory. Pass a list of the requests to the OneCancelsOtherOrderone_cancels_other_order method. All the orders start working right away. When one of them fills, even partially, LEAN cancels the rest of the set. The method returns a list with the order ticket of each order.

    + +
    +
    // Exit an existing long position at the first price level the market reaches.
    +var price = Securities[_symbol].Price;
    +var tickets = OneCancelsOtherOrder(new List<SubmitOrderRequest>
    +{
    +    OrderFactory.LimitOrder(_symbol, -100, price * 1.02m, tag: "Take profit"),
    +    OrderFactory.StopMarketOrder(_symbol, -100, price * 0.99m, tag: "Stop loss")
    +});
    +
    # Exit an existing long position at the first price level the market reaches.
    +price = self.securities[self._symbol].price
    +tickets = self.one_cancels_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")
    +])
    +
    + +

    The following images show the two outcomes of an OCO set that exits a long position. In the first image, the price rises to the take-profit price, so the take-profit order fills and LEAN cancels the stop-loss order:

    +An OCO set has a take-profit sell limit order at 60,000 and a stop-loss sell stop order at 40,000. The price rises to 60,000, the take-profit order fills, and LEAN cancels the stop-loss order. + +

    In the second image, the price falls to the stop price, so the stop-loss order fills and LEAN cancels the take-profit order:

    +An OCO set has a take-profit sell limit order at 60,000 and a stop-loss sell stop order at 40,000. The price falls to 40,000, the stop-loss order fills, and LEAN cancels the take-profit order. diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/04 One Triggers Other Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/04 One Triggers Other Orders.html new file mode 100644 index 0000000000..f6b728f45a --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/04 One Triggers Other Orders.html @@ -0,0 +1,28 @@ +

    To place a set of OTO orders, create the parent and child order requests with the order factory and pass them to the OneTriggersOtherOrderone_triggers_other_order method. The brokerage holds the child orders until the parent order completely fills. If the parent order is canceled or invalid, LEAN cancels the child orders, even if the parent partially filled. The method returns a list with the order ticket of each order, starting with the parent.

    + +
    +
    // Buy IBM and BAC after the SPY entry fills.
    +var parent = OrderFactory.LimitOrder(_spy, 100, limitPrice);
    +var tickets = OneTriggersOtherOrder(parent, new List<SubmitOrderRequest>
    +{
    +    OrderFactory.MarketOrder(_ibm, 10),
    +    OrderFactory.MarketOrder(_bac, 20)
    +});
    +
    # Buy IBM and BAC after the SPY entry fills.
    +parent = self.order_factory.limit_order(self._spy, 100, limit_price)
    +tickets = self.one_triggers_other_order(parent, [
    +    self.order_factory.market_order(self._ibm, 10),
    +    self.order_factory.market_order(self._bac, 20)
    +])
    +
    + +

    The child orders are independent of each other and can be for different securities. To relate the child orders, pass them through the OrderFactory.OneCancelsOtherorder_factory.one_cancels_other or OrderFactory.OneUpdatesOtherorder_factory.one_updates_other method first. A child order can trigger other orders in turn. To create a chain, call the Triggerstriggers method on the child order request.

    + +
    +
    // The IBM order triggers the BAC order once it fills.
    +var child = OrderFactory.MarketOrder(_ibm, 10).Triggers(OrderFactory.MarketOrder(_bac, 20));
    +var tickets = OneTriggersOtherOrder(parent, new List<SubmitOrderRequest> { child });
    +
    # The IBM order triggers the BAC order once it fills.
    +child = self.order_factory.market_order(self._ibm, 10).triggers(self.order_factory.market_order(self._bac, 20))
    +tickets = self.one_triggers_other_order(parent, [child])
    +
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/05 One Updates Other Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/05 One Updates Other Orders.html new file mode 100644 index 0000000000..6e0eaac040 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/05 One Updates Other Orders.html @@ -0,0 +1,15 @@ +

    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.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/06 Compose Contingent Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/06 Compose Contingent Orders.html new file mode 100644 index 0000000000..43031aa897 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/06 Compose Contingent Orders.html @@ -0,0 +1,52 @@ +

    The order factory creates SubmitOrderRequest objects without submitting them, so you can relate them before you place them. The order factory has a method for each order type, with the same arguments as the method that places the order. For example, OrderFactory.LimitOrderorder_factory.limit_order accepts the same arguments as LimitOrderlimit_order. To relate the order requests, use the following methods:

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MethodDescription
    request.Triggers(children)request.triggers(children)The request triggers the child orders when it completely fills (OTO).
    request.Bracket(takeProfitPrice, stopLossPrice)request.bracket(take_profit_price, stop_loss_price)The request triggers a take-profit order and a stop-loss order that cancel each other.
    OrderFactory.OneCancelsOther(requests)order_factory.one_cancels_other(requests)The first fill of a request cancels the others (OCO).
    OrderFactory.OneUpdatesOther(requests)order_factory.one_updates_other(requests)A partial fill of a request reduces the quantity of the others (OUO).
    + +

    The Triggerstriggers and Bracketbracket methods return the same request. The OneCancelsOtherone_cancels_other and OneUpdatesOtherone_updates_other methods return the list of requests. You can pass the result of one method to another to build any composition. To place the orders, pass any request of the set to the Orderorder method. The method places every order in the set and returns their order tickets, with each parent before the orders it triggers.

    + +
    +
    // A limit entry that triggers a take profit and a stop loss that cancel each other.
    +var entry = OrderFactory.LimitOrder(_symbol, 100, price * 0.999m);
    +var takeProfit = OrderFactory.LimitOrder(_symbol, -100, price * 1.02m);
    +var stopLoss = OrderFactory.StopMarketOrder(_symbol, -100, price * 0.99m);
    +entry.Triggers(OrderFactory.OneCancelsOther(takeProfit, stopLoss));
    +var tickets = Order(entry);
    +
    # A limit entry that triggers a take profit and a stop loss that cancel each other.
    +entry = self.order_factory.limit_order(self._symbol, 100, price * 0.999)
    +take_profit = self.order_factory.limit_order(self._symbol, -100, price * 1.02)
    +stop_loss = self.order_factory.stop_market_order(self._symbol, -100, price * 0.99)
    +entry.triggers(self.order_factory.one_cancels_other(take_profit, stop_loss))
    +tickets = self.order(entry)
    +
    + +

    You can submit each order request once. After you submit the request, its OrderIdorder_id property holds the order Id. To get the order ticket of a request, get it from the transaction manager.

    + +
    +
    var stopLossTicket = Transactions.GetOrderTicket(stopLoss.OrderId);
    +
    stop_loss_ticket = self.transactions.get_order_ticket(stop_loss.order_id)
    +
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/07 Combo Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/07 Combo Orders.html new file mode 100644 index 0000000000..4fdadc4cce --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/07 Combo Orders.html @@ -0,0 +1,23 @@ +

    The combo methods of the order factory return a list with one order request for each leg. LEAN treats the legs of a combo order as a single member of the set. Pass all the legs whenever you relate or submit a combo order. When a combo order is the parent of an OTO set, LEAN triggers the child orders when all the legs completely fill. To use a combo order as the parent, pass the list of legs to the OneTriggersOtherOrderone_triggers_other_order method. The Bracketbracket method doesn't support combo orders.

    + +
    +
    // Buy a butterfly spread and sell it when its net price reaches the take-profit level.
    +var legs = new List<Leg>
    +{
    +    Leg.Create(calls[0].Symbol, 1),
    +    Leg.Create(calls[1].Symbol, -2),
    +    Leg.Create(calls[2].Symbol, 1)
    +};
    +var entry = OrderFactory.ComboMarketOrder(legs, 2);
    +var takeProfit = OrderFactory.ComboLimitOrder(legs, -2, takeProfitPrice);
    +var tickets = OneTriggersOtherOrder(entry, takeProfit);
    +
    # Buy a butterfly spread and sell it when its net price reaches the take-profit level.
    +legs = [
    +    Leg.create(calls[0].symbol, 1),
    +    Leg.create(calls[1].symbol, -2),
    +    Leg.create(calls[2].symbol, 1)
    +]
    +entry = self.order_factory.combo_market_order(legs, 2)
    +take_profit = self.order_factory.combo_limit_order(legs, -2, take_profit_price)
    +tickets = self.one_triggers_other_order(entry, take_profit)
    +
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/08 Monitor Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/08 Monitor Orders.html new file mode 100644 index 0000000000..16d63e2a83 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/08 Monitor Orders.html @@ -0,0 +1,18 @@ +

    Each contingent order has its own order ticket, which fills and closes independently. 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 Contingencycontingency on 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.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/09 Update Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/09 Update Orders.html new file mode 100644 index 0000000000..e3145bcb40 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/09 Update Orders.html @@ -0,0 +1,16 @@ +

    You can update any open contingent order, 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 of a bracket order while the entry waits to fill.
    +var response = tickets[1].UpdateLimitPrice(price * 1.03m, "Updated take profit");
    +if (response.IsSuccess)
    +{
    +    Debug("Take profit updated successfully");
    +}
    +
    # Move the take profit of a bracket order while the entry waits to fill.
    +response = tickets[1].update_limit_price(price * 1.03, "Updated take profit")
    +if response.is_success:
    +    self.debug("Take profit 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.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/10 Cancel Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/10 Cancel Orders.html new file mode 100644 index 0000000000..1f45fc9d97 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/10 Cancel Orders.html @@ -0,0 +1,16 @@ +

    To cancel a contingent order, 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 of a bracket order, which also cancels the take profit and the stop loss.
    +var response = tickets[0].Cancel("Cancel bracket");
    +if (response.IsSuccess)
    +{
    +    Debug("Bracket order successfully cancelled");
    +}
    +
    # Cancel the entry of a bracket order, which also cancels the take profit and the stop loss.
    +response = tickets[0].cancel("Cancel bracket")
    +if response.is_success:
    +    self.debug("Bracket order successfully cancelled")
    +
    + +

    The message of the cancel order event of each dependent order names the order that caused the cancellation.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/11 Backtest Fills.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/11 Backtest Fills.html new file mode 100644 index 0000000000..bfec85afc6 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/11 Backtest Fills.html @@ -0,0 +1,10 @@ +

    In backtests, LEAN simulates how the brokerage manages the set with the following rules:

    + + + +

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

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/12 Buying Power.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/12 Buying Power.html new file mode 100644 index 0000000000..cf9d17957e --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/12 Buying Power.html @@ -0,0 +1 @@ +

    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.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/13 Brokerage Support.php b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/13 Brokerage Support.php new file mode 100644 index 0000000000..43a56180ed --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/13 Brokerage Support.php @@ -0,0 +1,6 @@ + +

    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.

    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/99 Examples.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/99 Examples.html new file mode 100644 index 0000000000..6f99d1b1df --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/99 Examples.html @@ -0,0 +1,51 @@ +

    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))
    +
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/metadata.json b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/metadata.json new file mode 100644 index 0000000000..0430ac6846 --- /dev/null +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/13 Contingent Orders/metadata.json @@ -0,0 +1,12 @@ +{ + "type": "metadata", + "values": { + "description": "Contingent orders are sets of orders with related lifecycles, such as bracket, one cancels other, one triggers other, and one updates other orders.", + "keywords": "contingent orders, bracket orders, one cancels other, oco orders, one triggers other, oto orders, one updates other, ouo orders, otoco, take profit, stop loss, order factory", + "og:description": "Contingent orders are sets of orders with related lifecycles, such as bracket, one cancels other, one triggers other, and one updates other orders.", + "og:title": "Contingent Orders - Documentation QuantConnect.com", + "og:type": "website", + "og:site_name": "Contingent Orders - QuantConnect.com", + "og:image": "https://cdn.quantconnect.com/docs/i/writing-algorithms/trading-and-orders/order-types/contingent-orders.png" + } +} diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/01 Introduction.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/01 Introduction.html index 35effc9686..dd536ec56e 100644 --- a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/01 Introduction.html +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/01 Introduction.html @@ -1 +1 @@ -

    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.

    \ No newline at end of file +

    You can build other order types from the built-in order types. To place one cancels the other (OCO) orders, bracket orders, and other sets of related orders, see Contingent Orders.

    \ No newline at end of file diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/02 One Cancels the Other Orders.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/02 One Cancels the Other Orders.html deleted file mode 100644 index 383b9479f1..0000000000 --- a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/02 One Cancels the Other Orders.html +++ /dev/null @@ -1,111 +0,0 @@ -

    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 OnOrderEventson_order_events 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
    -_market = MarketOrder("SPY", 1);
    -var fillPrice = _market.AverageFillPrice;
    -
    -// Get the fill price from the OnOrderEvent event handler
    -public override void OnOrderEvent(OrderEvent orderEvent)
    -{
    -    if (orderEvent.Status == OrderStatus.Filled && orderEvent.Ticket.OrderType == OrderType.Market)
    -    {
    -        var fillPrice = orderEvent.FillPrice;
    -    }
    -}
    -
    # Get the fill price from the order ticket of a sync market order
    -self._market = self.market_order("SPY", 1)
    -fill_price = self._market.average_fill_price
    -
    -# Get the fill price from the OnOrderEvent event handler
    -def on_order_event(self, order_event: OrderEvent) -> None:
    -    if order_event.status == OrderStatus.FILLED and order_event.ticket.order_type == OrderType.MARKET:
    -        fill_price = order_event.fill_price
    -
    - -

    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 OnOrderEventon_order_event event handler, cancel the other orders in the OCO order set.

    - -
    -
    private OrderTicket _stopLoss;
    -private OrderTicket _takeProfit;
    -
    -public override void OnOrderEvent(OrderEvent orderEvent)
    -{
    -    if (orderEvent.Status != OrderStatus.Filled) return;
    -    
    -    switch (orderEvent.Ticket.OrderType)
    -    {
    -        case OrderType.Market:
    -            _stopLoss = StopMarketOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*0.95m);
    -            _takeProfit = LimitOrder(orderEvent.Symbol, -orderEvent.FillQuantity, orderEvent.FillPrice*1.10m);
    -            return;
    -        case OrderType.StopMarket:
    -            _takeProfit?.Cancel();
    -            return;
    -        case OrderType.Limit:
    -            _stopLoss?.Cancel();
    -            return;
    -    }
    -}
    -
    _stop_loss = None
    -_take_profit = None
    -
    -def on_order_event(self, order_event: OrderEvent) -> None:
    -    if order_event.status != OrderStatus.FILLED:
    -        return
    -
    -    match order_event.ticket.order_type:
    -        case OrderType.MARKET:
    -            self._stop_loss = self.stop_market_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*0.95)
    -            self._take_profit = self.limit_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*1.10)
    -        case OrderType.STOP_MARKET:
    -            self._take_profit.cancel()
    -        case OrderType.LIMIT:
    -            self._stop_loss.cancel()
    -
    - -

    Simulate Orders

    -

    To simulate OCO orders, track the asset price in the OnDataon_data 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(symbol: "SPY", -1, tag: "take profit");
    -    }
    -    else if (slice.Bars["SPY"].Price <= _entryPrice * 0.95m)
    -    {
    -        Liquidate(symbol: "SPY", -1, tag: "stop loss");
    -    }
    -}
    -
    -
    def on_data(self, slice: Slice) -> None:
    -    if not self.portfolio.invested:
    -        ticket = self.market_order("SPY", 1)
    -        self.entry_price = ticket.average_fill_price
    -
    -    bar = slice.get("SPY")
    -    if bar:
    -        if bar.price >= self.entry_price * 1.10:
    -            self.liquidate(symbol="SPY", -1, tag="take profit")
    -
    -        elif bar.price <= self.entry_price * 0.95:
    -            self.liquidate(symbol="SPY", -1, tag="stop loss")
    -
    \ No newline at end of file diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/99 Examples.html b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/99 Examples.html deleted file mode 100644 index 1c872f1852..0000000000 --- a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/99 Examples.html +++ /dev/null @@ -1,218 +0,0 @@ -

    - The following example demonstrates a helper class for OCO orders. - This is not an officially supported OCO order type as it's possible for both the take profit and stop loss orders to fill in the same bar. - Before you use this approach, verify that your brokerage model supports stop market orders and limit orders. -

    -
    -
    public class OCOTrade
    -{
    -    private readonly QCAlgorithm _algorithm;
    -    private readonly Symbol _symbol;
    -    private readonly decimal _takeProfitPrice, _stopLossPrice;
    -    public OrderTicket Entry, TakeProfit, StopLoss;
    -
    -    public OCOTrade(QCAlgorithm algorithm, Symbol symbol, decimal quantity, decimal entryStopPrice,
    -                    decimal takeProfitPrice, decimal stopLossPrice)
    -    {
    -        _algorithm = algorithm;
    -        _symbol = symbol;
    -        _takeProfitPrice = takeProfitPrice;
    -        _stopLossPrice = stopLossPrice;
    -        Entry = _algorithm.StopMarketOrder(_symbol, quantity, entryStopPrice);
    -        TakeProfit = null;
    -        StopLoss = null;
    -    }
    -
    -    public void OnOrderEvent(OrderEvent orderEvent)
    -    {
    -        if (orderEvent.Status != OrderStatus.Filled)
    -            return;
    -
    -        if (Entry != null && Entry.OrderId == orderEvent.OrderId)
    -        {
    -            var quantity = orderEvent.FillQuantity;
    -            TakeProfit = _algorithm.LimitOrder(_symbol, -quantity, _takeProfitPrice);
    -            StopLoss = _algorithm.StopMarketOrder(_symbol, -quantity, _stopLossPrice);
    -            Entry = null;
    -        }
    -        else if (TakeProfit != null && TakeProfit.OrderId == orderEvent.OrderId)
    -        {
    -            StopLoss.Cancel();
    -            TakeProfit = null;
    -            StopLoss = null;
    -        }
    -        else if (StopLoss != null && StopLoss.OrderId == orderEvent.OrderId)
    -        {
    -            TakeProfit.Cancel();
    -            TakeProfit = null;
    -            StopLoss = null;
    -        }
    -    }
    -}
    -
    -public class InsideDayBreakout : QCAlgorithm
    -{
    -    private Security _forex;
    -    private int _riskPct;
    -    private OCOTrade _longTrade;
    -    private OCOTrade _shortTrade;
    -
    -    public override void Initialize()
    -    {
    -        SetStartDate(2024, 9, 1);
    -        SetEndDate(2024, 12, 31);
    -        SetCash(1000);
    -        SetTimeZone(TimeZones.Utc);
    -        SetBrokerageModel(BrokerageName.OandaBrokerage, AccountType.Margin);
    -        _forex = AddForex("USDJPY");
    -        _forex.Session.Size = 3;
    -        foreach (var bar in History<QuoteBar>(_forex.Symbol, 3))
    -            _forex.Session.Update(bar);
    -        _riskPct = 7;
    -        SetWarmUp(TimeSpan.FromDays(4));
    -    }
    -
    -    public override void OnWarmupFinished()
    -    {
    -        // Add a Scheduled event to rebalance the portfolio every weekday.
    -        Schedule.On(
    -            DateRules.Every(DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday),
    -            TimeRules.AfterMarketOpen(_forex.Symbol, 1),
    -            Rebalance
    -        );
    -    }
    -
    -    private void Rebalance()
    -    {
    -        if (!_forex.Session.IsReady || Portfolio.Invested)
    -            return;
    -        var session = _forex.Session[1];
    -        var high = session.High;
    -        var low = session.Low;
    -        var prevHigh = _forex.Session[2].High;
    -        var prevLow = _forex.Session[2].Low;
    -
    -        if (!(high < prevHigh && low > prevLow))
    -            return;
    -        // Replace any pending entry orders from a previous inside day.
    -        Liquidate();
    -        var insideHigh = Math.Round(high, 3, MidpointRounding.AwayFromZero);
    -        var insideLow = Math.Round(low, 3, MidpointRounding.AwayFromZero);
    -        var dayRange = insideHigh - insideLow;
    -        // Risk amount in USD.
    -        var riskUsd = Portfolio.Cash * (_riskPct / 100m);
    -        // Risk per unit in USD terms (dayRange is in JPY, convert to USD via close price).
    -        var riskPerUnit = dayRange / session.Close;
    -        // OANDA max margin 50:1.
    -        var riskQuantity = (int)(riskUsd / riskPerUnit);
    -        var maxQuantity = (int)((Portfolio.Cash * 48) / session.Close);
    -        var quantity = Math.Min(riskQuantity, maxQuantity);
    -        if (quantity == 0)
    -            return;
    -        _longTrade = new OCOTrade(this, _forex.Symbol, quantity, insideHigh,
    -            takeProfitPrice: Math.Round(insideHigh + dayRange, 3),
    -            stopLossPrice: insideLow);
    -        _shortTrade = new OCOTrade(this, _forex.Symbol, -quantity, insideLow,
    -            takeProfitPrice: Math.Round(insideLow - dayRange, 3),
    -            stopLossPrice: insideHigh);
    -    }
    -
    -    public override void OnOrderEvent(OrderEvent orderEvent)
    -    {
    -        foreach (var trade in new[] { _longTrade, _shortTrade })
    -            if (trade != null)
    -                trade.OnOrderEvent(orderEvent);
    -    }
    -}
    -
    class OCOTrade:
    - 
    -    def __init__(self, algorithm, symbol, quantity, entry_stop_price, take_profit_price, stop_loss_price):
    -        self._algorithm = algorithm
    -        self._symbol = symbol
    -        self._take_profit_price = take_profit_price
    -        self._stop_loss_price = stop_loss_price
    -        self._entry = algorithm.stop_market_order(symbol, quantity, entry_stop_price)
    -        self._take_profit = None
    -        self._stop_loss = None
    -
    -    def on_order_event(self, order_event):
    -        if order_event.status != OrderStatus.FILLED:
    -            return
    -        _id = order_event.order_id
    -        if self._entry and self._entry.order_id == _id:
    -            quantity = order_event.fill_quantity
    -            self._take_profit = self._algorithm.limit_order(self._symbol, -quantity, self._take_profit_price)
    -            self._stop_loss = self._algorithm.stop_market_order(self._symbol, -quantity, self._stop_loss_price)
    -            self._entry = None
    -        elif self._take_profit and self._take_profit.order_id == _id:
    -            self._stop_loss.cancel()
    -            self._take_profit = self._stop_loss = None
    -        elif self._stop_loss and self._stop_loss.order_id == _id:
    -            self._take_profit.cancel()
    -            self._take_profit = self._stop_loss = None
    -
    -
    -class InsideDayBreakout(QCAlgorithm):
    -
    -    def initialize(self):
    -        self.set_start_date(2024, 9, 1)
    -        self.set_end_date(2024, 12, 31)
    -        self.set_cash(1000)
    -        self.set_time_zone(TimeZones.UTC)
    -        self.set_brokerage_model(BrokerageName.OANDA_BROKERAGE, AccountType.MARGIN)
    -        self._forex = self.add_forex("USDJPY")
    -        self._forex.session.size = 3
    -        for bar in self.history[QuoteBar](self._forex, 3):
    -            self._forex.session.update(bar)
    -        self._risk_pct = 7
    -        self._long_trade = None
    -        self._short_trade = None
    -        self.set_warm_up(timedelta(4))
    -
    -    def on_warmup_finished(self):
    -        # Add a Scheduled event to rebalance the portfolio monthly.
    -        self.schedule.on(
    -            self.date_rules.every([DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY]),
    -            self.time_rules.after_market_open(self._forex, 1),
    -            self._rebalance
    -        )
    -
    -    def _rebalance(self):
    -        if not self._forex.session.is_ready or self.portfolio.invested:
    -            return
    -        session = self._forex.session[1]
    -        high, low = session.high, session.low
    -        prev_high, prev_low = self._forex.session[2].high, self._forex.session[2].low
    -        if not (high < prev_high and low > prev_low):
    -            return
    -        # Replace any pending entry orders from a previous inside day.
    -        self.liquidate()
    -        inside_high = round(high, 3)
    -        inside_low = round(low, 3)
    -        day_range = inside_high - inside_low
    -        # Risk amount in USD.
    -        risk_usd = self.portfolio.cash * (self._risk_pct / 100)
    -        # Risk per unit in USD terms (day_range is in JPY, convert to USD via close price).
    -        risk_per_unit = day_range / session.close
    -        # OANDA max margin 50:1.
    -        risk_quantity = int(risk_usd / risk_per_unit)
    -        max_quantity = int((self.portfolio.cash * 48) / session.close)
    -        quantity = min(risk_quantity, max_quantity)
    -        if not quantity:
    -            return
    -        self._long_trade = OCOTrade(
    -            self, self._forex, quantity, inside_high,
    -            take_profit_price=round(inside_high + day_range, 3),
    -            stop_loss_price=inside_low
    -        )
    -        self._short_trade = OCOTrade(
    -            self, self._forex, -quantity, inside_low,
    -            take_profit_price=round(inside_low - day_range, 3),
    -            stop_loss_price=inside_high
    -        )
    -
    -    def on_order_event(self, order_event):
    -        for trade in [self._long_trade, self._short_trade]:
    -            if trade is not None:
    -                trade.on_order_event(order_event)
    -
    diff --git a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/metadata.json b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/metadata.json index b2110249c7..39e422c1bb 100644 --- a/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/metadata.json +++ b/03 Writing Algorithms/22 Trading and Orders/03 Order Types/99 Other Order Types/metadata.json @@ -1,9 +1,9 @@ { "type": "metadata", "values": { - "description": "We are often asked to support other order types like one cancels the other, trailing stop, and multi-leg orders. We will add them over time.", - "keywords": "one cancels the other orders, oco orders, trailing stop orders, multi-leg orders, Option strategies, spreads, straddles, strangles", - "og:description": "We are often asked to support other order types like one cancels the other, trailing stop, and multi-leg orders. We will add them over time.", + "description": "You can build other order types, like multi-leg orders, from the built-in order types.", + "keywords": "multi-leg orders, Option strategies, spreads, straddles, strangles", + "og:description": "You can build other order types, like multi-leg orders, from the built-in order types.", "og:title": "Other Order Types - Documentation QuantConnect.com", "og:type": "website", "og:site_name": "Other Order Types - QuantConnect.com",