fix(execution): cancel a resting SELL whose position has left the account (#668) - #671
Merged
Conversation
…ount (#668) `_clear_resting_bracket` fails closed before an exit. `reconcile_unbracketed_ positions` heals a position that has no bracket. Both are POSITION-DRIVEN: they start from a tranche and ask what protects it. A protective order whose position has left the account has no tranche to be walked from, so neither can reach it, and nothing else in the engine looked. That is the structural reason the gap existed rather than an oversight in either sweep. `sweep_orphan_brackets` runs the other way -- from keel's own resting SELLs to the account -- and cancels any whose product the venue no longer holds. Three ways one appears: the operator sells or transfers the asset at the venue and keel's bracket keeps resting against inventory that left without telling it; `scale_out` and `_roll_stop` both cancel before they place, and a process dying inside that window can leave a replacement resting over a resolved position; and a venue without a native trigger-bracket needs two legs, where a filled target leaves a live stop behind -- not reachable on the venue that trades live today, where `BracketGTC` is one order, and reachable the moment one is not. Left alone the market reverses through the stop and the venue is asked to sell an asset that is not there. On a cash account that is a rejection; on a margin-enabled one (#666) it is a short produced entirely by a missing cancel. THE VENUE DECIDES, NEVER THE LEDGER, and that asymmetry is a safety property rather than a preference. Cancelling a protective order over a position that really exists strips a live holding of its only stop, so the trigger has to be the account's own statement about itself. keel's ledger can be stale in exactly the dangerous direction -- saying zero while the venue holds the position -- and a sweep driven from it would cancel precisely the brackets that were working. An unreadable balance therefore cancels NOTHING. `Balance.total`, not `Balance.available`, for the same reason as #667's clamp: a resting SELL holds the base it commits, so `available` reads ~0 for exactly the products this sweep looks at. Reading it would cancel every protective order keel has ever placed, on the first cycle, and call the result a fix. Dust strictly BELOW the venue's `base_increment` counts as nothing held -- a residue the venue cannot express as a size is not a position. Exactly one increment is the smallest real position there is and keeps its bracket. With no increment known there is no threshold and none is invented: `_base_increment_for` returns None for a venue error as well as an unknown product, and a guessed floor would cancel protective orders over a number nobody supplied. Runs LAST of the three reconciliation passes, and the order is load-bearing: `reconcile_unbracketed_positions` will PLACE a bracket for a tranche whose position left out of band, so sweeping afterwards makes the cancel the cycle's final word and nothing re-creates within one cycle what it just removed. #667's rail 21 stops that re-place at source, which makes the churn rare rather than the ordering optional -- the rail refuses to PLACE into an empty holding, this cancels what already rests, and neither subsumes the other. The tranche is deliberately left open. Closing it would book a realized outcome at a price nobody observed; `doctor`'s new `bracket.orphan` and the existing `ledger.unbooked_exit` both surface it for a human instead. 11 mutants, 11 killed -- including the wiring (a sweep nothing calls reports nothing) and its POSITION in the cycle, which a presence-only test would miss. Stacked on #667: the sweep reads `executor._held_base`, and a second reader of the same venue fact would be two things to keep in agreement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #668. Stacked on #670 (targets
fix-667-sell-clamp) — the sweep readsexecutor._held_base, and a second reader of the same venue fact would be two things to keep in agreement.Why nothing caught this
_clear_resting_bracketfails closed before an exit.reconcile_unbracketed_positionsheals a position that has no bracket. Both are position-driven — they start from a tranche and ask what protects it. A protective order whose position has left the account has no tranche to be walked from, so neither can reach it. That is the structural reason the gap existed, not an oversight in either sweep.sweep_orphan_bracketsruns the other way: from keel's own resting SELLs to the account.How an orphan appears
The shape is not obvious on a venue with native brackets, so it is worth being concrete:
scale_outand_roll_stopboth cancel before they place — a process dying inside that window can leave a replacement resting over a resolved position;BracketGTCis one order — and reachable the moment one is not.Left alone, the market reverses through the stop and the venue is asked to sell an asset that is not there. Cash account: a rejection. Margin-enabled (#666): a short, produced entirely by a missing cancel.
The venue decides, never the ledger
This is a safety property, not a preference. Cancelling a protective order over a position that really exists strips a live holding of its only stop, so the trigger has to be the account's own statement about itself. keel's ledger can be stale in exactly the dangerous direction — saying zero while the venue holds the position — and a sweep driven from it would cancel precisely the brackets that were working.
An unreadable balance therefore cancels nothing. A venue that omits empty accounts is saying nothing, not saying zero.
Balance.total, notBalance.available, for the same reason as #667's clamp: a resting SELL holds the base it commits, soavailablereads ~0 for exactly the products this sweep looks at. Reading it would cancel every protective order keel has ever placed, on the first cycle, and call the result a fix. There is a test for that specific inversion.Dust
Strictly below
base_incrementcounts as nothing held — a residue the venue cannot express as a size is not a position. Exactly one increment is the smallest real position there is and keeps its bracket. With no increment known there is no threshold and none is invented:_base_increment_forreturnsNonefor a venue error as much as for an unknown product, and a guessed floor would cancel protective orders over a number nobody supplied.Where it runs, and why there
Last of the three reconciliation passes.
reconcile_unbracketed_positionswill place a bracket for a tranche whose position left out of band, so sweeping afterwards makes the cancel the cycle's final word and nothing re-creates within one cycle what it just removed. Running it first would leave a fresh orphan resting for a full day — this deployment cycles once per UTC day.#667's rail 21 stops that re-place at source, which makes the churn rare rather than the ordering optional: the rail refuses to place into an empty holding, this cancels what already rests, and neither subsumes the other.
What it deliberately does not do
The tranche is left open. Closing it would book a realized outcome at a price nobody observed — what the position was worth when it left the account is a question only an operator can answer.
doctor's newbracket.orphanfinding and the existingledger.unbooked_exitboth surface it.It also never raises into the cycle: per-row isolation, an accepted-but-unsettled cancel is not recorded as done, and a refused cancel leaves the local row untouched (a venue that refuses may have already filled it).
Verification
11 mutants, 11 killed — including the wiring (a sweep nothing calls reports nothing) and its position in the cycle, which a presence-only test would miss.
Full suite: 5105 passed, 3 skipped. ruff and mypy clean.