Contingency graphs (OCO / OUO / OTO)
Status: Phase 2 shipped (Alembic 0041). Manager:
alphaswarm/trading/execution/contingency.py.
The three relationships
| Type | Behaviour |
|---|---|
| OCO (one cancels other) | When any constituent fills (partial or full), the others are canceled. Canonical use: bracket a position with a take-profit limit + stop-loss stop. |
| OUO (one updates other) | When any constituent's quantity changes (partial fill or amend), every other constituent's quantity is updated to match the remaining size. Useful when the bracket has more than two legs. |
| OTO (one triggers other) | The parent is the trigger; children are emulated until the parent fills, then they're submitted. Canonical use: place an entry limit + parked TP/SL waiting for the entry to hit. |
Class layout
Manager behaviour
For each constituent, the manager tracks shadow remaining_quantity
and status:
- On fill (full or partial), OCO emits
CANCELfor every peer. - On partial fill, OUO emits
UPDATE_QUANTITYfor every peer with the newremaining_quantity. - On full fill, OUO emits
CANCELfor every peer (degenerates to OCO). - On parent fill, OTO emits
SUBMITfor every child. Subsequent child fills don't re-trigger anything.
Venue dispatch
Two routes:
-
Native atomic submission -- when the broker sets
supports_oco = True, the broker's :meth:IDomainBrokerage.submit_listsubmits the whole list in a single venue call (Alpaca bracket orders, IBKR OCA groups). The manager STILL registers the list so a partial-cancel still emits cleanup commands when the venue's atomicity is best-effort. -
Manager-simulated -- when the broker sets
supports_oco = False, the broker submits each constituent independently and the manager owns the cross-order cancels via :meth:ContingencyManager.on_execution_report.
Code example
from decimal import Decimal
from alphaswarm.core.domain.identifiers import (
ClientOrderId, InstrumentId, OrderListId, Symbol2, Venue,
)
from alphaswarm.core.domain.enums import ContingencyType, OrderSide, OrderType
from alphaswarm.core.domain.orders import LimitOrder, StopMarketOrder, OrderList
# Take-profit at 200, stop-loss at 180 -- OCO bracket
tp = LimitOrder(
client_order_id=ClientOrderId("tp-1"),
instrument_id=InstrumentId(Symbol2("AAPL"), Venue("NASDAQ")),
order_side=OrderSide.SELL,
quantity=Decimal("10"),
order_type=OrderType.LIMIT,
price=Decimal("200"),
)
sl = StopMarketOrder(
client_order_id=ClientOrderId("sl-1"),
instrument_id=InstrumentId(Symbol2("AAPL"), Venue("NASDAQ")),
order_side=OrderSide.SELL,
quantity=Decimal("10"),
order_type=OrderType.STOP_MARKET,
trigger_price=Decimal("180"),
)
order_list = OrderList(
order_list_id=OrderListId("oco-1"),
orders=[tp, sl],
contingency_type=ContingencyType.OCO,
)
# Submit the entire list atomically (or simulated)
await broker.submit_list(order_list)
Persistence
The Alembic 0041 migration adds:
order_lists-- one row per :class:OrderListdomain_orders.order_list_idFK -- ties constituents to parentexecution_reports-- audit trail; the manager reads this to recover state after a restart
Limitations
- OUO with three or more constituents updates ALL peers to the smallest remaining size. This matches the standard interpretation but may not be what every venue does -- check the venue's docs.
- OTO with multiple parents (a single child triggered by either of two parents) is NOT supported; that's a contingency-graph generalisation the manager can't currently express.
- Native broker OCO often comes with constraints (Alpaca brackets require the limit + stop to be on the same instrument; IBKR OCA groups allow cross-instrument). The contingency manager handles cross-instrument simulation.