Managing Contingent Orders, Child Sizing, and Bracket Risks
Summary
The document explains how order lists group orders and how OTO, OCO, and OUO relationships coordinate child activation, cancellation, or quantity changes. The behavior depends on the matching engine, emulator, adapter, venue, or strategy logic, so metadata alone does not ensure that a live venue honors a relationship. Mixed-instrument lists also carry operational risks: some checks and cache queries use the first order’s instrument, and adapter batching may route other legs incorrectly.
For OTO orders, the text details partial-fill and full-fill release models and how strategy-managed children are sized as parent fills accumulate. Sizing can be limited by a linked position, rounded to the child instrument’s size increment, or reduced to zero; missing or conflicting state can leave a child unchanged. The document warns that full-fill release delays protection during partial fills, while partial release does not guarantee proportionate coverage. It also describes OCO cancellation as best effort and presents brackets as an entry with linked take-profit and stop-loss exits. Venue margin rules, independent leg failures, and delayed cancellations remain important caveats.
Key ideas
- Order lists can include multiple instruments at one venue, but risk checks, queries, and adapter routing may rely on the first order’s instrument.
- OTO releases child orders based on parent fills, while OCO requests sibling cancellation and OUO requests quantity updates.
- Strategy-managed OTO sizing depends on cached instruments, position state, child size increments, and parent fill events.
- A full-fill trigger can leave partial positions without exits until completion, while partial triggering does not ensure proportional child sizing.
- OCO cancellation may arrive too late to prevent another sibling from filling, and leg failures can leave exposure unprotected.
- Bracket factories link an entry with reduce-only take-profit and stop-loss orders, but venues may impose margin requirements or reject children.
Tags
Full text
# Advanced orders
# Advanced orders
Order lists group related orders, while contingency metadata describes how fills, cancellations, or
updates should affect linked orders. The component that handles the list determines the behavior:
the backtest matching engine, local order emulator, live adapter and venue, or strategy code.
:::warning
An `OrderList` or `ContingencyType` does not guarantee that every live adapter or venue implements
the relationship. Check the target integration before relying on native contingency behavior.
:::
## Order lists
An order list groups contingent orders or a larger batch under one `order_list_id`. Orders in the
list do not need a contingency relationship; their own metadata defines any relationship.
Production constructors require every order in a list to use the same venue. Orders may target
different instruments at that venue, such as pairs, calendar spreads, or multi-leg strategies. The
list takes its representative `instrument_id` from the first order; consumers that need the actual
instrument must resolve each order individually.
Caveats for mixed-instrument lists:
- **Pre-trade checks**: Price precision, quantity precision, and GTD expiry use each order's own
instrument.
- **Cumulative risk check**: Free balance, notional bounds, position-reducing exposure, and market
data use the list's representative instrument. For a mixed list, this produces a single-instrument
bound rather than per-instrument accuracy.
- **Cache lookups**: `cache.order_lists(instrument_id=...)` filters against the representative
`instrument_id`; lists containing other instruments will not match queries for those other
instruments.
- **Position IDs**: The execution engine denies mixed-instrument lists when a `position_id` is
supplied (a position belongs to a single instrument, regardless of OMS).
- **Adapter batching**: `submit_order_list` implementations vary. Some iterate orders per leg and
resolve each order's own `instrument_id` against the venue API; others still build the batch
request around the list's representative `instrument_id` and will misroute non-first
orders. Treat mixed-instrument lists as adapter-specific; verify the target adapter's
behavior before relying on it. Backtesting and strategy-managed routing avoid relying on an
adapter's mixed-instrument batch behavior.
## Contingency types
- **OTO (One-Triggers-Other)**: A parent order releases one or more child orders after a configured
fill condition.
- **OCO (One-Cancels-Other)**: A fill in one linked order requests cancellation of the others.
- **OUO (One-Updates-Other)**: A fill in one linked order requests a quantity update for the others.
:::info
These types correspond to FIX
[`ContingencyType <1385>`](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html).
:::
### Strategy-managed contingencies
Enable `StrategyConfig.manage_contingent_orders` to manage open OTO, OCO, and OUO relationships for
orders that are not active local. The strategy sends the resulting cancel and quantity-update
commands through the normal execution path before it calls the specific and aggregate user
order-event handlers.
The `OrderEmulator` always owns active-local orders. Enabling strategy management therefore does
not make the strategy and emulator manage the same order. The option does not add native venue
support or submit a non-active-local OTO child: it manages non-active-local orders that are already
open.
### One-Triggers-Other (OTO)
An OTO relationship has two parts:
1. The parent order enters its execution path.
1. One or more child orders reference the parent and wait for the configured release condition.
The handler determines where the children wait. The backtest engine can hold them locally, while a
live adapter may send native venue instructions, submit all legs, reject the list, or require the
strategy to manage the relationship.
#### Child sizing
Before the parent's first fill, strategy management propagates parent quantity updates to open,
non-active-local OTO children.
After filling starts, each parent event starts the child target at the parent's cumulative filled
quantity. For an execution spawn, this quantity includes fills from every order in the spawn.
For a parent linked to a position, the manager then adjusts the target in order:
1. For a non-spread parent with a reduce-only child, cap the total target at the child's filled
quantity plus the current [commission-adjusted](../positions.md#base-currency-commissions)
position quantity. This keeps the child's remaining quantity within the open position.
1. Round the total target down to a multiple of the child instrument's size increment.
1. When configured, treat a rounded target below the child instrument's minimum quantity as zero.
The calculation does not round position or account state. A remaining position too small to meet
the child instrument's size increment and optional minimum quantity stays open without reduce-only
child coverage. Spread parents skip the position cap because the execution engine does not create
positions for them. Non-reduce-only children also skip the cap. Both still use the child
instrument's size rules.
#### Required sizing state
When a fill event, cached parent, or filled execution-spawn sibling identifies a position, sizing
requires:
- The parent and child instruments in the cache.
- A positive size increment for the child instrument.
- The linked position in the cache for a non-spread parent with a reduce-only child.
- Matching fill-event and cached position IDs when both are present.
A fill-event position ID that conflicts with cached ownership stops processing for that parent
event. Other missing sizing state leaves the affected child unchanged, and processing continues
with the remaining linked children.
#### Child lifecycle
Parent events apply the validated target according to the child and parent state:
| Condition | Action |
| -------------------------------------------------------- | ---------------------------------------------------------- |
| Managed child has a different positive target | Update its total quantity. |
| Target is zero; parent or execution spawn remains active | Keep the child unchanged and wait for executable quantity. |
| Target is zero; parent or execution spawn closes | Cancel the child. |
| Child fills meet or exceed the positive target | Cancel any remaining quantity. |
| Active-local child reaches an executable positive target | The active-local emulator submits it once. |
A child fill or update does not recalculate the target immediately. The next parent event refreshes
it.
#### Trigger models
| Trigger model | Backtest release condition |
| ------------- | ------------------------------------------------------------------------- |
| **Partial** | Release children after the parent's first partial fill. |
| **Full** | Release children after the parent's cumulative fill reaches its quantity. |
:::info
The default `BacktestVenueConfig` mode is `OtoTriggerMode.PARTIAL`. Set `oto_trigger_mode` to
`OtoTriggerMode.FULL` to wait for a complete fill. This setting controls release timing; it does not
promise pro rata child sizing. Verify child quantities when the parent fills partially.
:::
With `support_contingent_orders` enabled, the backtest venue sizes each child as if it covers the
parent's whole quantity. When the parent is modified or closes, each child's remaining quantity
covers what the parent can still hold, less what the children have filled between them: the
parent's quantity while it works, and its filled quantity once it is canceled, expires, or is
reduced to its filled quantity. A modify grows children only when it raises the parent's quantity,
and closing the parent only shrinks them:
- Released children keep working for the parent's filled quantity. A child with nothing left to
cover is canceled.
- Reducing the parent to its filled quantity completes it, so `OtoTriggerMode.FULL` releases held
children at that quantity, and a child that fills on release leaves the others only what it did
not fill.
- Canceling or expiring the parent cancels held children, so a partially filled parent that closes
this way under `OtoTriggerMode.FULL` leaves its fills without children.
The venue compares quantities in the parent's units:
- A child that still holds a quote quantity, under a parent whose quantity is in base, is compared
as the base quantity at its price, else its trigger price, else the best bid for a sell or the
best ask for a buy in the parent's book. It holds that base quantity once the venue resizes it,
and whenever the parent closes as above, so a later conversion cannot take it past the parent's
fills. Without such a price it is not resized.
- A parent that still holds a quote quantity, such as a stop that converts when it fills, resizes
only the children that hold one too.
- Inverse instruments have no quote conversion, so their quantities compare as they are.
- A child on another instrument covers the parent's quantity one to one. Once its own
instrument's engine matches it, what remains for it to cover is rounded down to that
instrument's size precision, and a quote quantity converts by that instrument, at its own price
or trigger price only.
Reduce-only children also track the position between these events, as described in
[Backtest reduce-only resizing](#backtest-reduce-only-resizing).
Reduce-only OTO children resize when a parent fill releases them, and again on later parent fills.
Fills awaiting cache delivery count toward the available position quantity. A released exit can fill
in the same bar when a later step in the [OHLC price simulation](../backtesting/bar-execution.md#ohlc-price-simulation)
reaches its matching price.
#### Enforcing a full-fill trigger in strategy code
If the execution context does not provide the required full-fill behavior:
1. Submit the parent order without contingent children.
1. Handle `OrderFilled` events for the parent.
1. Confirm the parent has reached `FILLED` status.
1. Submit the stop-loss, take-profit, or other child orders.
:::warning
Full-fill release leaves a partially filled position without its contingent exits until the parent
finishes. Partial release reduces that delay. In backtests, reduce-only children with enforcement
enabled track the available position quantity, subject to parent caps. Other children do not
automatically track each partial fill. Check quantities and adapter behavior before treating a child
as complete protection.
:::
### One-Cancels-Other (OCO)
In backtest local matching, a full or partial fill in one OCO order causes a best-effort request to
cancel its open siblings. The local order manager applies this behavior only while a sibling remains
active local. With strategy management enabled, the strategy requests cancellation for open,
non-active-local siblings. Otherwise, the adapter or venue determines cancellation behavior.
Another sibling can fill before cancellation completes.
### One-Updates-Other (OUO)
#### Updates after a sibling fill
In backtest local matching, a fill in one OUO order uses that order's remaining quantity as the
target for each open sibling:
- If the target is zero or the sibling's filled quantity already meets the target, cancel the sibling.
- Otherwise, update the sibling's quantity when needed.
This behavior suits equal-sized peers and does not preserve a ratio between unequal starting
quantities. With strategy management enabled, the strategy applies the same update or cancellation
behavior to open, non-active-local siblings. Otherwise, live behavior depends on adapter and venue
support.
#### Backtest reduce-only resizing
With reduce-only enforcement enabled, a fill can resize resting reduce-only orders to the available
position quantity, subject to parent caps. When contingent-order support is also enabled, a resized
OUO order propagates its remaining quantity to siblings that are:
- Open and not active local.
- Passive orders resting on the same instrument's book.
Siblings do not need to be `reduce_only`. Each sibling's quantity update follows these rules:
- Add the sibling's prior fills to the propagated remaining quantity to obtain its total quantity.
- Apply the sibling's own cached parent's filled-quantity cap, when available.
- Never reduce the total below the sibling's prior fills.
The order already being filled retains its active fill loop's quantity rules. This propagation does
not trigger matching itself.
Resized quantities are in base. An order that still holds a quote quantity, such as an untriggered
stop, takes its resized quantity in base, so its trigger does not convert it again. Inverse
instruments have no quote conversion, so the order keeps its quantity denomination.
#### Backtest cancellation at zero capacity
With reduce-only enforcement and contingent-order support enabled:
- When the reduce-only order has no remaining capacity, cancel it and its eligible siblings without
resizing the siblings. This also covers siblings whose acceptance event is still awaiting delivery.
- When a sibling exhausts only its own parent allowance, resize it to its filled quantity, then cancel it.
## Constructing contingent orders
Use `OrderFactory.bracket` to construct a bracket's contingency metadata. In Rust,
`self.order().create_list(...)` assigns a fresh `order_list_id` to an existing group of orders.
Python code instead passes a plain list to `self.submit_order_list(...)`, which creates an
`OrderList` when needed. These grouping paths do not create parent or linked-order relationships.
The current model enforces only part of the remaining consistency:
- A contingent order must have at least one `linked_order_id`.
- A child identifies its parent through `parent_order_id`.
- Rust `create_list` requires a non-empty list whose orders use one venue.
- `OrderList.validate` checks for non-empty, unique client order IDs when a strategy submits the
list.
- `OrderList.validate` does not verify shared `order_list_id` values, parent references, or other
cross-field relationships.
Modification, cancellation, and rejection behavior depends on the component managing the
contingency. Do not assume a parent update or cancellation cascades in every live integration.
:::warning
Handle `OrderDenied` and `OrderRejected` events for every leg. Adapter or venue failures can affect
legs independently and leave a position without its intended protection.
:::
## Bracket orders
Bracket orders combine an entry with take-profit and stop-loss children. By default,
`OrderFactory.bracket` creates a `MARKET` entry, a `LIMIT` take-profit, and a `STOP_MARKET`
stop-loss. It marks the entry with an `OTO` contingency, marks both exits `reduce_only`, and links
the exits with an `OUO` contingency. The default `LIMIT` take-profit is also `post_only`.
The factory creates the orders and their relationship metadata. The execution context determines
whether children wait locally, use a native venue instruction, enter the venue with the parent, or
require manual strategy handling.
Create brackets with
[`OrderFactory`](/docs/python-api-latest/common.html#nautilus_trader.common.OrderFactory), which
also supports different entry and exit types, trigger settings, and execution instructions.
In the following example we bracket a *Market* entry to BUY 10 ETHUSDT-PERP contracts with a
take-profit *Limit* at 3,300 USDT and a stop-loss *Stop-Market* triggering at 2,800 USDT. The entry
defaults to `MARKET`, the take-profit to `LIMIT`, and the stop-loss to `STOP_MARKET`; the take-profit
and stop-loss legs are `reduce_only` and linked with the `OUO` contingency:
```rust tab="Rust"
use nautilus_model::{
enums::OrderSide,
identifiers::InstrumentId,
types::{Price, Quantity},
};
// `bracket()` returns a `bon` builder; finalize with `.call()`.
// The result is a `Vec<OrderAny>` ordered as [entry, stop-loss, take-profit].
let orders = self
.order()
.bracket()
.instrument_id(InstrumentId::from("ETHUSDT-PERP.BINANCE"))
.order_side(OrderSide::Buy)
.quantity(Quantity::from(10))
.tp_price(Price::from("3300.00")) // take-profit LIMIT (default)
.sl_trigger_price(Price::from("2800.00")) // stop-loss STOP_MARKET (default)
.call();
```
```python tab="Python"
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import OrderSide
from nautilus_trader.model import Price
from nautilus_trader.model import Quantity
orders = self.order_factory.bracket(
instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"),
order_side=OrderSide.BUY,
quantity=Quantity.from_int(10),
tp_price=Price.from_str("3300.00"), # <-- take-profit LIMIT (default)
sl_trigger_price=Price.from_str("2800.00"), # <-- stop-loss STOP_MARKET (default)
)
```
:::warning
Some venues reserve margin for bracket legs. Check the venue's margin rules and handle a child
rejection after the entry fills.
:::
## Related guides
- [Orders](index.md) - Order concepts, execution instructions, and the order factory.
- [Emulated orders](emulated.md) - Emulating order types on venues without native support.
- [Execution](../execution/) - Order execution and fill handling.Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.