When to Use
Use this skill when a strategy has been approved for retirement — alpha decay, a persistent risk-limit breach, a committee vote — and its open book must be flattened without dumping it into the market. Decommissioning is not one action but four that are easy to conflate: stop new entries, kill the orders already working, liquidate the inventory in participation-capped waves, and prove the book is flat before releasing the capital. Skipping any one of them leaves a live position attributed to a strategy nobody is watching.
StrategyDecommissioningEngine is the ledger and state machine for that sequence. It walks
ACTIVE → ORDER_ENTRY_BLOCKED → UNWIND_IN_PROGRESS → FULLY_UNWOUND →
DECOMMISSION_COMPLETE, and refuses the last transition while any position, unfilled slice,
unconfirmed cancellation, or unresolved reconciliation break remains.
The regulatory anchor for the orderly-withdrawal posture is MiFID II RTS 6
(Commission Delegated Regulation (EU) 2017/589) Art. 17(1) — remedial action on a triggered
post-trade control "may include adjusting or shutting down the relevant trading algorithm or
trading system or an orderly withdrawal from the market" — and Art. 12, which requires the
firm to be able to "cancel immediately, as an emergency measure, any or all of its unexecuted
orders". See references/standards.md for the exact scope of each.
When NOT to Use
- As a kill switch or emergency flatten. This engine is deliberately slow: it caps each
wave at a fraction of ADV. When the reason to exit is a runaway algorithm or a breached
loss limit, the correct control is an immediate halt-and-cancel — see
execution-algorithm-kill-switch-integrationandkill-switch-and-drawdown-circuit-breakers. Decommissioning is what happens after the bleeding stops, not instead of stopping it. - As an execution algorithm. It authorises a quantity; it does not schedule, price,
route, or send anything. There is no volume curve and no time grid. Each slice must be
handed to a real algo (
execution-algo-twap-vwap-slicing,participation-of-volume-pov-execution) — sending a whole wave as one market order reintroduces exactly the impact the cap exists to avoid. - As an entry block the strategy does not call.
assert_entry_allowed()is a gate, not an interceptor. The engine owns no order path; an entry path that never calls it is an unblocked entry path regardless of the engine's state. - On instruments where quantity is not the binding constraint. Options at expiry, futures
into delivery, and anything with early-assignment or physical-settlement mechanics need
expiry-aware handling first — see
options-pin-risk-management-at-expiryandphysical-vs-cash-settlement-handling. - Without an independent position source. The engine's inventory is whatever was loaded
into it. It detects overfills against its own book, not against the broker's; reconcile
against the broker (
multi-broker-consolidated-position-view) before and after.
Prerequisites
- Strategy identifier and an auditable decommissioning reason (required, non-empty — it is the audit record).
- Position inventory:
StrategyPosition(symbol, quantity, market_price, avg_daily_volume, max_adv_slice_pct=10.0, lot_size=1.0).quantityis signed (positive long, negative short);avg_daily_volumeandmarket_pricemust be positive. - The broker order ids of every order already working at the moment of the decision.
- A fill feed that carries the broker's execution/fill id — without it, duplicate-fill suppression is impossible.
Workflow
-
Hard-block entries and register working orders
initiate_decommissioning(reason, working_order_ids=[...])→ stateORDER_ENTRY_BLOCKED,new_entries_allowedbecomesFalse.- Decision point — blocking entries does nothing to orders already resting. A
retired strategy with 40 working limit orders will keep acquiring inventory while you
unwind it. Register those ids here; the engine will not let the strategy reach
FULLY_UNWOUNDuntil every one is confirmed cancelled. - Wire
assert_entry_allowed()into the strategy's entry path. The block is only real where it is called.
-
Confirm cancellations, not cancel requests
record_order_cancellation(order_id)— only on a broker-confirmed cancellation.- Decision point — a cancel request is not a cancellation. An order can fill in the
window between request and acknowledgement. Confirming on the request produces a book
that says flat while a fill is in flight; see
broker-api-idempotent-cancel-requests.
-
Release one participation-capped wave per symbol
generate_unwind_liquidation_slices()→ oneLiquidationSliceOrderper eligible symbol, capped atmax_adv_slice_pctof ADV, floored to a whole lot. Long →SELL, short →BUY.- Decision point — a symbol with an unfilled slice outstanding is skipped, not
re-sliced. Regenerating a wave for it would authorise the same shares twice; the
second fill flips the position. If the child order is dead, call
cancel_slice(slice_id)to release the symbol, then re-slice. - Decision point — check
report.unsliceable_symbolsbefore looping. A symbol whose participation cap is below one lot has no compliant wave; the engine reports it and leaves the position alone rather than rounding up through the cap. It will never shrink on its own — it needs a block trade, a broader cap, or a manual liquidation. - The last wave for a symbol carries
is_final_slice=Trueand deliberately includes the odd-lot residual: a residual that is never sent is a position that is never closed.
-
Record fills against the inventory
record_slice_execution(slice_id, symbol, executed_qty, executed_price, realized_pnl, execution_id=...).- Decision point — always pass
execution_id. It is the only thing making this call idempotent. A replayed webhook without one double-decrements the position and double-counts P&L; the engine warns but cannot protect you. - Decision point — an overfill is recorded, not clamped. Filling 1,500 against a
1,000 long leaves the position at −500 and raises a
ReconciliationBreak. That short is real and unhedged; the next wave will generate aBUYto close it, and treasury return stays blocked until the break is acknowledged. - Realized P&L is taken from the broker or the accounting system. The engine aggregates it and never recomputes it — tax-lot selection, fees and financing live there.
-
Return capital only against a proven-flat book
return_capital_to_treasury()→DECOMMISSION_COMPLETE. It raises unless every position is flat, no slice is outstanding, every registered working order is confirmed cancelled, and every reconciliation break is resolved (or explicitly acknowledged viaacknowledge_breaks=True, which records the human decision rather than hiding it).
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Blocking entries and calling it a decommissioning. New-entry blocking says nothing about resting orders. RTS 6 Art. 12 exists precisely because unexecuted orders are a separate exposure; register and confirm them or the "retired" strategy keeps trading.
- Re-generating a wave before the previous one is filled or dead. Two waves of 10% ADV released against the same 1,000-share position sell 1,000 shares of a position that is still 1,000 long on paper — the second fill opens an unintended short.
- Clamping an overfill to flat.
max(0, qty - executed)reports a closed book while an unhedged opposite-side position is live in the market. Record the flip and escalate. - Recording a fill without the broker's execution id. Fill webhooks retry. Without an idempotency key the same fill decrements the position twice, and the engine then reports the strategy flat while half the inventory is still open.
- Treating a generated slice as a liquidation. Authorising a wave changes no position. Reporting slice notional as liquidated notional tells the desk 45% of the book is gone before a single share has traded.
- Assuming slower is safer. Cutting the participation cap lengthens the unwind and keeps the retired strategy's market risk on the book for more days. That is the Almgren–Chriss impact-versus-timing-risk trade-off, not a free improvement; pick the cap deliberately.
- A zero or unknown ADV.
cap = pct × 0is a zero-quantity wave and an unwind loop that never terminates. The engine rejects a non-positive ADV at load time. - Ignoring what lot flooring does to the horizon. A 5% cap on 3,000 ADV is 150 shares,
but at
lot_size=100the wave is 100 — a third of the intended participation, turning a 27-wave unwind into 40. Readparticipation_pcton the emitted slice rather than assuming the configured cap was achieved. - Declaring victory on an empty position dictionary. A position that was loaded flat, or a residual below the lot size, must still be accounted for. Completion is tested on "every quantity is flat", never on "the container is empty".
- Unwinding a hedged pair leg by leg. Flattening the long side of a spread first leaves
the remaining leg naked for the rest of the unwind. Sequence the legs, or unwind on a
ratio — see
cross-asset-hedge-execution-synchronization.
Verification
- Load AAPL long 1,000 @ 150 (ADV 5,000) and MSFT short 500 @ 300 (ADV 2,000). Before
initiation,
new_entries_allowedisTrue; afterinitiate_decommissioning(reason)it isFalseandassert_entry_allowed()raisesEntryBlockedError. generate_unwind_liquidation_slices()yields AAPLSELL500 (10% of 5,000,remaining_after_slice_quantity+500) and MSFTBUY200 (10% of 2,000, −300), both withparticipation_pct10.0 andis_final_sliceFalse.- Accounting: with slices generated but nothing filled,
initial_total_notional_usdis 300,000,remaining_notional_usdis 300,000 andliquidated_notional_usdis 0. After filling 500 AAPL @ 151 and 200 MSFT @ 299,liquidated_notional_usdis 135,300 andinitial_total_notional_usdis still 300,000. - Negative checks that must each raise or flag:
avg_daily_volume=0,market_price<=0,max_adv_slice_pctoutside(0, 100], a duplicate symbol, slicing whileACTIVE, a fill before initiation, a fill for an unknown symbol, a repeatedexecution_id(suppressed), a 1,500 fill on a 1,000 long (position → −500 plus a reconciliation break), andreturn_capital_to_treasury()with a residual position, an unconfirmed working order, or an unacknowledged break. - Run
python -m unittest discover -s skills/strategy-decommissioning-and-position-unwind-procedure/scriptsand confirm a 100% pass rate.
Related Skills
strategy-lifecycle-retirement-criteriastrategy-committee-governance-for-capital-allocation-decisionsparticipation-of-volume-pov-executionexecution-algo-twap-vwap-slicingexecution-algorithm-kill-switch-integrationbroker-api-idempotent-cancel-requestsorder-placement-idempotencyminimum-fill-size-and-lot-rounding-logic