Skip to content

bridge: a completed order does not return to the open book (ibx#262) - #360

Open
userFRM wants to merge 1 commit into
deepentropy:mainfrom
userFRM:fix/terminal-order-not-resurrected
Open

bridge: a completed order does not return to the open book (ibx#262)#360
userFRM wants to merge 1 commit into
deepentropy:mainfrom
userFRM:fix/terminal-order-not-resurrected

Conversation

@userFRM

@userFRM userFRM commented Jul 30, 2026

Copy link
Copy Markdown

Problem

Nothing remembered that an order had reached a terminal state, so a replayed frame put it back into the open book.

push_order_info was a bare insert with no ordering rule, and drain_open_orders filters on the string it writes. The reconnect open-order burst racing a fill — or any message the gateway resends — made req_open_orders report a completed order as live. A strategy then re-manages a position it already has, or cancels an order that no longer exists, with the open-order snapshot corroborating the wrong picture.

Why the cached status cannot carry it

Two ways a status-only guard is bypassed:

  • Completing an order evicts its cache row. The replayed frame finds nothing to refuse and inserts itself. That is the ordinary path, not an edge case.
  • A cached string is overwritten by the next terminal report. A report landing between the completion and the replay erases the evidence, and the replay then passes.

What this changes

A bounded set of completed order ids carries the knowledge instead, recorded where the completed-order record is already written and surviving the eviction. It is capped and oldest-first, because a session completes orders indefinitely; the width only has to outlive a reconnect's replay of recent activity, which is what resurrects an order.

The rule covers every status the engine treats as ending an order's life — Filled, Cancelled and Rejected are the three it acts on by removing the order from its book. Inactive is not among them, because it returns to working when the condition holding the order clears, and neither is Uncertain, which states the opposite of a conclusion.

A trade cancel (150=H) or trade correction (150=G) restates an execution the gateway has already reported and can legitimately return a filled order to a working quantity. That is the gateway's own statement, not a replay of an older one, so those two execution types write through a separate entry point that supersedes the completion rather than being refused by it.

Tests

  • a_completed_order_is_not_returned_to_the_open_book — all three terminal statuses, all four open ones.
  • a_completion_outlives_the_cache_row_it_evicts — the ordinary completion path.
  • an_intervening_report_does_not_erase_the_completionFilled → Cancelled → Submitted.
  • a_trade_correction_can_reopen_a_completed_order — and the order stops being remembered afterwards.
  • a_fill_still_writes_over_a_working_status — the ordinary direction, so the guards are not passing against a cache that refuses everything.
  • the_completed_memory_is_bounded — capped, oldest evicted first.

Each fails by name against a compiling reversion of the production change it covers.

Closes #262.

Test plan

  • Mutation: removing the completed-id memory fails a_completion_outlives_the_cache_row_it_evicts by name.
  • Mutation: narrowing the terminal set to Filled fails a_completed_order_is_not_returned_to_the_open_book.
  • Mutation: leaving the id remembered after a correction fails a_trade_correction_can_reopen_a_completed_order.
  • Mutation: removing the cap fails the_completed_memory_is_bounded.
  • Positive control: a_fill_still_writes_over_a_working_status, so the guards are not passing against a cache that refuses everything.
  • cargo check --offline clean on --lib, --lib --features python, --bins, --examples, and each integration target individually.
  • tests/ib_paper_compat compared against a clean checkout of the base commit — identical sorted diagnostic sets.
  • cargo test --offline --lib — only the two known config::expiry_tests failures, which fail on the base commit for missing legacy tzdata (fixed separately in config: resolve the legacy timezone names IB states its times in (ibx#335) #336).

@userFRM
userFRM force-pushed the fix/terminal-order-not-resurrected branch from c7661be to d1380a3 Compare July 30, 2026 15:44
@userFRM userFRM changed the title bridge: a filled order does not return to the open book (ibx#262) bridge: a completed order does not return to the open book (ibx#262) Jul 30, 2026
@userFRM
userFRM force-pushed the fix/terminal-order-not-resurrected branch from d1380a3 to 749b71b Compare July 31, 2026 11:31
Nothing remembered that an order had reached a terminal state, so a replayed frame put it back into the open book. `push_order_info` was a bare insert with no ordering rule, and `drain_open_orders` filters on the string it writes — so the reconnect open-order burst racing a fill, or any message the gateway resends, made `req_open_orders` report a completed order as live. A strategy then re-manages a position it already has, or cancels an order that no longer exists, with the open-order snapshot corroborating the wrong picture.

The cached status cannot be what carries that knowledge. Completing an order evicts its cache row, so the replayed frame finds nothing to refuse and inserts itself — the ordinary path, not an edge case. A cached string is also overwritten by the next terminal report, so a report arriving between the completion and the replay would erase the evidence.

A set of completed order ids carries it instead, recorded where the completed-order record is already written and surviving the eviction. Entries are held by age rather than by count: what the memory has to outlive is the window in which a stale frame for the order can still arrive, and a reconnect replays recent activity within seconds — that is a duration, not a number of orders. Counting instead meant a busy session's unrelated completions pushed a still-relevant entry out, and the replay it was there to refuse got back in. A generous cap remains as a backstop against a session completing orders faster than they expire: expired entries are pruned first, and because a session fast enough to need the backstop can still be over the cap with nothing yet expired, the oldest survivors are evicted next until the set is back under it — the cap bounds the set itself, not just the entries old enough to prune.

The rule covers every status the engine treats as ending an order's life — filled, cancelled and rejected are the three it acts on by removing the order from its book. `Inactive` is not among them, because it returns to working when the condition holding the order clears, and neither is `Uncertain`, which states the opposite of a conclusion.

A trade cancel or trade correction restates an execution the gateway has already reported and can legitimately return a filled order to a working quantity. That is the gateway's own statement rather than a replay of an older one, so those two execution types write through a separate entry point that supersedes the completion rather than being refused by it.

Closes deepentropy#262.
@userFRM
userFRM force-pushed the fix/terminal-order-not-resurrected branch from 749b71b to c46907b Compare July 31, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orders: a replayed frame resurrects a filled order into the open book, in both the engine and the API cache

1 participant