Runbook: Delisting a Product

Last updated: 2026-09-02

Delisting is a coordinated wind-down: halt trading, settle open positions at the final settlement price, and terminate the instrument in EP3. The instrument's historical data (trades, transactions, funding) is preserved; the Postgres instruments row is never deleted — it is stamped delisted_at.

The final settlement price is simply the last settlement that printed for the symbol (the admin-cli delist default). Schedule the delist to run shortly after a settlement so that price is fresh.

How positions are settled (what clients see)

Residual positions are closed by final-settlement prints: one two-sided block trade per holder against the house settlement account, at the final settlement price, fee-free. These are real trades in every ledger — EP3, the ClickHouse mirror, and GET /fills (flagged is_final_settlement) — so a client's fills for the symbol sum to exactly zero after delisting. The prints do not appear on the public tape, candles, or volume; published open interest drops to zero automatically. Communicate this mechanism in the delisting notice.

Phase 1 — Last trading day

  1. Freeze trading (is_closing). At the announced last trading time, set the freeze from the admin GUI (Markets → row actions → Begin Delisting) or via the api-gateway admin route (POST /admin/instruments/{symbol}/closing). This is an AX-side-only freeze: it drives EP3 to Open so the closeout block trades will be accepted, while the order gateway rejects all new client orders and modifications as ExchangeClosed ("only cancels are accepted") and still allows cancels. Do not use set-instrument-state Closed/Halted — EP3 accepts block trades only in Open, so those states would block the closeout. Verify new order entry for the symbol fails cleanly and cancels still succeed. Resting orders need no manual sweep — the orchestrator cancels them at the start of the closeout.

  2. Wait for the final settlement. The final settlement price is the last settlement that printed, so run the delist shortly after a scheduled settlement. The settlement engine already pushed that price to EP3; there is nothing to publish manually.

  3. Confirm no funding run is pending. The orchestrator checks the settlement-pending:{symbol} guard, but do not start the closeout while a scheduled funding cycle for the symbol is mid-flight.

Phase 2 — Settle and terminate

  1. Dry-run the closeout.

    admin-cli delist --symbol <SYM> --dry-run

    The price defaults to the last settlement that printed for the symbol (from the funding_rates ledger); the command warns if that settlement is more than a day old. Pass --price to override.

    Review the booking plan: every holder, quantity, side, and notional at the settlement price. Long and short quantities must net to zero.

    The counterparty defaults to the canonical AX settlement account (AX_SETTLEMENT_ACCOUNT_ID under the environment's EP3 firms/prefix); it is provisioned automatically on the first real run. --settlement-account overrides it, but the override must already exist in EP3 and its user path must carry a ULID account id — the command rejects anything else, because trade-engine2 derives account ids from drop-copy participant paths and a non-conforming party would stall trade ingestion exchange-wide.

  2. Run the closeout.

    admin-cli delist --symbol <SYM> --actually

    The command prints the exact procedure it will follow before doing anything. With the instrument in Closing (EP3 still Open), the orchestrator takes the settlement-pending:{symbol} guard, sweeps resting orders, books one final-settlement block trade per holder (cross_id prefix F-, journaled in Postgres final_settlements), waits for the drop-copy mirror to catch up, verifies EP3 positions, ClickHouse current_positions, and sum-of-trades positions are all zero, then terminates the instrument in EP3 (Terminated) and finally stamps instruments.delisted_at and clears is_closing in one atomic update. A set delisted_at therefore always means the delist fully completed.

    Once complete, the orchestrator rewrites ticker:{symbol} as a terminal record — open interest 0, state Delisted, quotes cleared, mark and last settlement at the final settlement price — with a 7-day TTL matching the GUI's recently-delisted picker window. A failed rewrite warns without failing the delist, but leaves the last live ticker (stale open interest and state) being served until the key is rewritten or deleted.

    If the run fails partway, rerun the same command — every partial state resumes: holders are enumerated from live EP3 positions, so already-settled holders are skipped; in-flight journal rows are resolved against EP3 by cross-id; and a run interrupted between the EP3 terminate and the final stamp is recognized (EP3 Terminated, is_closing still set) and finishes the stamp. Two cases stop a rerun for an operator:

    Force mode (--force). If EP3 terminated the instrument out from under open AX-side positions (Terminated is one-way, and block trades cannot book in it), the normal path cannot reach the closeouts. --force books the same final-settlement prints directly into the AX books (ClickHouse trades and positions, balances via the transaction engine) under the same journal, with the holders enumerated from ClickHouse and cross-checked against the trade ledger first. It needs no is_closing freeze. Use it only for fixing accidents.

    EP3 can also purge a Terminated instrument's record entirely after some interval (observed on demo: TEST-PERP was gone from list-instruments --show-deleted and from EP3's instrument collections about six weeks after termination, while its trades remained). A force run then finds no EP3 record, which it treats as confirmation the instrument is dead, and resolves the inputs the record would have supplied AX-side:

    The dry run prints these values and their provenance in a banner under the plan. With --actually the run stops for a [y/N] confirmation showing the total dollars of closeouts it will book; pass --ep3-record-missing-ok to accept the values in an unattended run (an unattended run without it fails rather than hangs). Every journal row minted this way carries the provenance in final_settlements.reason. Without --force, an absent EP3 record is a hard error.

  3. Remove from the instrument spec. Remove (or comment out) the product from instruments.yml in each environment's config repository so a future instruments sync doesn't attempt to recreate it. Do not delete the Postgres instruments row.

  4. Funding stops automatically. delisted_at gates the settlement engine's symbol selection. Confirm on the next funding cycle that the symbol is skipped, and stop extending its cme_future_roll_schedule rows if applicable (existing rows remain).

  5. Restart catalog-bound services if required. The trade and risk engines build their instrument catalogs at startup and are not on the instruments publication; if any behavior keys off delisted_at in those services, schedule their restart. (The order gateway and api-gateway pick up the terminal state from EP3.)

  6. Update documentation. Remove the contract from the public contract-specs page, note the delisting in the public changelog, and keep the instrument ID reserved in ax_sdk_internal::constants::INSTRUMENT_IDS — IDs are never reused.

Phase 3 — Post-delist verification

  1. Run the automated invariant suite:

    admin-cli delist-verify --symbol <SYM>

    This re-asserts the terminal state across Postgres, EP3, and ClickHouse and prints a PASS/FAIL table: lifecycle stamped (delisted_at set, is_closing cleared), EP3 Terminated with an empty book, positions flat in EP3 / current_positions / sum-of-trades, published OI zero, every final_settlements journal row BOOKED at one price, every closeout print block-flagged and fee-free with the settlement account on exactly one side and matching its journal row on holder/quantity/price, and no funding transactions after delisted_at. If the delist used --settlement-account, pass the same identity's AX account id via --settlement-account-id. The command exits nonzero on any failure.

    An EP3 that has no record of the instrument at all passes the ep3-state check: EP3 purges Terminated records after some interval, so absence is the expected terminal state of an older delist, and the EP3 book and position checks report nothing to check.

The remaining items are manual:

  1. Order entry for the symbol is rejected on all gateways.
  2. Public surfaces are clean: no settlement prints on the tape or candles, 24h volume carries no closeout spike, and the EP3-derived ticker stats (last trade price/quantity, session high/low) show no closeout effect.
  3. GUI: for seven days the symbol stays findable in the market picker, tagged as recently delisted, and its row reads from the terminal ticker — confirm it shows state Delisted, zero open interest, and the final settlement price — and it is never chosen as the default market; historical positions, fills, and funding remain queryable with correct formatting. After the window the symbol drops out of the picker.
  4. Cash reconciles: each holder's balance change equals qty × multiplier × (final price − last mark) net of prior funding, and the settlement account nets to zero.

Known interactions