ccp: read the resend markers and book replays on cumulative quantity (ibx#320, ibx#260) - #361
Closed
userFRM wants to merge 1 commit into
Closed
ccp: read the resend markers and book replays on cumulative quantity (ibx#320, ibx#260)#361userFRM wants to merge 1 commit into
userFRM wants to merge 1 commit into
Conversation
userFRM
force-pushed
the
fix/replay-restates-history
branch
from
July 30, 2026 14:56
307fdf9 to
aaa0c51
Compare
userFRM
force-pushed
the
fix/replay-restates-history
branch
from
July 30, 2026 18:43
aaa0c51 to
faaf8bf
Compare
At session start the gateway replays recent activity: a New/New report inserts the order through the cross-session recovery path, then the order's executions arrive behind it carrying their original ExecIDs and a resend marker. By then the order is tracked, so each execution books through the normal path — a fill event, a position move — for something that happened before the process started. The ExecID window cannot stop it, because a fresh process has never seen those IDs. Neither `97` (PossResend) nor `43` (PossDupFlag) was read anywhere in the handler. The recovery insert now seeds `filled` from the record's own CumQty instead of assuming zero, so an order arrives with the quantity it has actually filled rather than looking untouched. A marked report is then booked on what it adds to that baseline rather than on the increment it reports. Its CumQty is the order's cumulative filled quantity as of that execution, so the quantity that is news to this session is CumQty minus what the order already holds, and nothing when it restates. Reading the cumulative figure is what makes replay safe in general: the same report twice adds nothing the second time, copies arriving out of order settle on the true total instead of losing whichever lands behind a later one, and a long session that has rolled those executions out of the ExecID window is not overcounted. Live reports are unchanged — an increment is new quantity by definition, and the window is what stops a repeat. A marked report is not vetoed outright. A CCP reconnect keeps this state and the gateway replays recent executions on the new session, so a fill that executed during the outage arrives marked with an ID this session has never seen and is the first news of it. It carries cumulative quantity the order lacks, so it books. An execution with no ExecID skipped the window entirely and a replayed copy booked twice, which is the shape a replay takes and precisely when the window matters. It now falls back to a key built from the fields that identify an execution, including CumQty — the one field that separates two slices of the same size at the same price inside one timestamp tick, including across a replacement that raised the total and left the same quantity behind twice. The key is spent only where the fill can actually book, so an execution that arrives ahead of the recovery record for its order is not refused when the record finally lands. `update_order_filled` saturates rather than wrapping, so a quantity large enough to overflow the counter cannot take an order's filled quantity round to nothing. The dedup window stops being the guard for a replayed execution, which is what made its depth a cliff: a batch deeper than the window no longer held its own head, and for an order this session tracks the window was the only thing standing between the second copy and a second booking. A marked report now adds what it carries above the order's own quantity, whether or not its ExecID is still remembered. A marked report is recorded in the ExecID window even though the cumulative figure is what judged it: the same execution can arrive again without its marker, and the window is the only thing that can catch that copy. Where the reconciled quantity is not the increment the report states — a report arriving after one this client never saw — the fill carries the reconciled quantity at that report's price rather than one execution's own terms. The order total and the position are right; the execution detail is approximate, and the handler logs it as such. Making it exact needs a capture establishing whether replayed executions can arrive out of order at all. No new wire traffic: this reads tags the gateway already sends. Closes deepentropy#320. Closes deepentropy#260. Closes deepentropy#344.
userFRM
force-pushed
the
fix/replay-restates-history
branch
from
July 30, 2026 19:53
faaf8bf to
93bdc87
Compare
Author
|
Closing this. It's in #409 along with the rest of the fork, which is easier to take in one piece than sixty separate branches. |
userFRM
added a commit
to userFRM/ibx
that referenced
this pull request
Aug 30, 2026
…keep both test modules
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.
Problem
At session start the gateway replays recent activity. A New/New report inserts the order through the cross-session recovery path, then its executions arrive behind it carrying their original ExecIDs and a resend marker. By then the order is tracked, so each execution books through the normal path — a fill event, a position move — for something that happened before the process started. The ExecID window cannot stop it: a fresh process has never seen those IDs. Neither
97(PossResend) nor43(PossDupFlag) was read anywhere in the handler.What this changes
The recovery insert seeds
filledfrom the record's own CumQty instead of assuming zero, so a recovered order arrives with the quantity it has actually filled rather than looking untouched.A marked report books on what it adds to that baseline, not on the increment it reports. Its CumQty is the order's cumulative filled quantity as of that execution, so what is news to this session is CumQty minus what the order already holds — nothing when it restates. Reading the cumulative figure is what makes replay safe in general:
Live reports are unchanged: an increment is new quantity by definition, and the window is what stops a repeat.
A marked report is not vetoed outright. A CCP reconnect keeps this state and the gateway replays recent executions on the new session, so a fill that executed during the outage arrives marked with an ID this session has never seen and is the first news of it. It carries cumulative quantity the order lacks, so it books.
An execution with no ExecID is deduplicated on its content. It skipped the window entirely, so a replayed copy booked twice — the shape a replay takes, and precisely when the window matters. The fallback key includes CumQty, the one field that separates two slices of the same size at the same price inside one timestamp tick, including across a replacement that raised the total and left the same quantity behind twice. The key is spent only where the fill can actually book, so an execution arriving ahead of the recovery record for its order is not refused when that record finally lands.
update_order_filledsaturates rather than wrapping, so a quantity large enough to overflow the counter cannot take an order's filled quantity round to nothing.Tests
a_resent_execution_does_not_book_a_fill— both markers, with a live positive control.a_fresh_process_does_not_book_the_history_it_is_replayed— end to end: recovery record then its replayed execution.a_resent_execution_carrying_new_quantity_is_still_booked— the fill that ran during an outage.a_replay_of_booked_history_adds_nothing_to_the_order— unordered, ExecID-less replay of already-booked executions, and a genuinely new one in the same batch.a_marked_execution_delivered_twice_books_once.a_raised_total_does_not_collapse_two_slices_into_one.a_key_is_not_spent_before_the_order_exists.two_same_priced_slices_in_one_tick_are_not_one_execution,an_execution_without_an_exec_id_is_still_deduplicated.update_order_filled_saturates.Each fails by name against a compiling reversion of the production change it covers.
No new wire traffic: this reads tags the gateway already sends.
The dedup window stops being the guard for a replayed execution, which is what made its depth a cliff (#344): a batch deeper than the window no longer held its own head, and for an order this session tracks the window was the only thing between the second copy and a second booking.
Closes #320. Closes #260. Closes #344.
Test plan
a_resent_execution_does_not_book_a_fill,a_fresh_process_does_not_book_the_history_it_is_replayed,a_marked_execution_delivered_twice_books_onceanda_replay_of_booked_history_adds_nothing_to_the_orderby name.LeavesQtyinstead ofCumQtyfailsa_raised_total_does_not_collapse_two_slices_into_one.a_key_is_not_spent_before_the_order_exists.a_fresh_process_does_not_book_the_history_it_is_replayed.a_marked_execution_is_remembered_for_its_unmarked_twin.a_replay_deeper_than_the_exec_id_window_does_not_double_countcovers ccp: the ExecID window is 1024 entries, so a replay batch deeper than that double-counts its own head #344 — the window is no longer the guard for a replayed execution.cargo check --offlineclean on--lib,--lib --features python,--bins,--examples, and each integration target individually.tests/ib_paper_compatcompared against a clean checkout of the base commit — identical sorted diagnostic sets.cargo test --offline --lib— only the two knownconfig::expiry_testsfailures, 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).