Skip to content

A definite TradeScopeDenied on a bracket leg is booked state-unknown, parking exits on a phantom pending row #945

Description

@eaitbrahim

Found in the end-to-end review of the sell-side build (e97141e..origin/main, #889–#939): reviewer suggestion S-6, one of the rulings the reviewer would overturn (R5's unknown-state bucket), accepted by the operator.

Problem

executor.place_bracket runs after the entry has already filled, so it must never raise (#799, plan R5). Its recovery books the failure by STAGE:

  • raise before the bracket's orders row exists → unbracketed: retry record, the sweep retries next cycle;
  • raise after it exists (place_order threw) → the pending row stays, the retry record is CLEARED, and a CRITICAL executor.bracket_state_unknown parks the product's exits until a human reconciles the row -- because the venue MIGHT be holding the order, and a retry could double-commit the base.

TradeScopeDenied lands in the second bucket today (executor.py, the broad except Exception; the docstring even says it is included on purpose). But a permissions refusal is a DEFINITE answer: the venue did not take the order. Nothing rests at the exchange. The position is left unprotected behind a phantom pending row that only a human can clear, when the honest record is "rejected, retry when you can".

The row status already exists: _run_order writes rejected for a broker-rejected placement (PlaceResult(success=False)), and reconcile's own docstring says "a broker rejection writes rejected" -- a permissions refusal is the same fact.

Expected

  • TradeScopeDenied on the bracket leg: the written row is marked rejected, the unbracketed: retry record is ARMED (not cleared), a CRITICAL names the refusal and says the position is unprotected -- and the next cycle's reconcile_unbracketed_positions sweep retries instead of waiting for a human.
  • Any other raise after the row exists (timeout, network) keeps today's state-unknown handling unchanged -- there the venue really may be holding the order.
  • The refutation write itself stays where it is (_run_order, Venue visibility must be capability-based, not key-presence-based — a read-only key looks identical to a working one #233): a placement-path refusal is a credential fact; only its row bookkeeping changes.

Fix plan

  • executor.place_bracket: dedicated except TradeScopeDenied ahead of the broad handler; new executor.bracket_refused CRITICAL; docstring bullet.
  • tests/execution/test_bracket_downgrade.py: split the parametrized place-raise test (timeout keeps state-unknown); new test pins rejected-row + armed retry + no unknown event + the sweep healing with a working broker.
  • Plan doc: R80.

Activity

  1. added a commit that references this issue on Sep 30, 2026
  2. eaitbrahim commented on Sep 30, 2026

    @eaitbrahim
    ContributorAuthor

    Fixed in PR #948 (merged to main): R80 a TradeScopeDenied on a bracket leg marks the row rejected, arms the unbracketed: retry, and the sweep heals; ambiguous raises keep R5's state-unknown. Ships in 0.21.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions