ccp: a cancel rejected as unknown retires the order (ibx#252) - #379
Open
userFRM wants to merge 2 commits into
Open
ccp: a cancel rejected as unknown retires the order (ibx#252)#379userFRM wants to merge 2 commits into
userFRM wants to merge 2 commits into
Conversation
The fill block recorded the ExecID and then booked the fill only if the order was already in `context`. There was no other branch, so a fill for an order the session does not track was dropped in full: no `Fill`, no execution detail, no position update, and nothing in the log to say a fill had been seen at all. The ExecID had already been consumed by then, which made the loss permanent. The replay the gateway pushes after a reconnect — the thing that would have recovered it — is rejected as a duplicate. An order is missing from `context` in ordinary ways. It reached a terminal status and was removed, which is the cancel/fill race: the cancel ack is processed first and the fill arrives to find nothing. Or it was placed from another client, or in an earlier session; the recovery insert does not pick those up, since it only fires for a New/New report carrying a contract id and a quantity. What is left is a position the account holds and the engine does not, disagreeing with the gateway's own position feed, with no error raised either way. The execution report already carries what booking needs, so the fill is now placed from the report when the order is unknown. Both the contract and the side must be on it: a guessed side moves the position the wrong way, which is worse than reporting that the fill could not be placed, so a report without tag 54 is refused rather than defaulted. Registration goes through the fallible path, because a full instrument table is a condition to report and not one to abort an inbound message on. A replayed execution does not open a new position on this path. On a fresh process the gateway resends prior executions carrying `97=Y` and their original ExecIDs, for orders no session tracks, and booking those would build a position out of history on top of the one the position feed already reports. Within a process the ExecID window covers the reconnect burst, so the marker is consulted only where the order is unknown. It does not reach a replay whose order the recovery insert has already tracked — that path books as it did before, and is worth separating out (ibx#320). The ExecID is recorded once the fill can be booked rather than before. An execution that could not be placed stays replayable. Each reason for refusing to book is logged where it is decided; one message for all of them misdiagnoses two of the three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FIX `CxlRejReason 1` is UnknownOrder: the gateway is stating that the order does not exist on its side. The handler restored it to working anyway — the restore ran first and unconditionally, and only afterwards did anything branch on the reason — and then removed the shared-cache row. Engine and cache were left asserting opposite things. The engine's own view is the one that governs subsequent cancels, modifies and reconnect bookkeeping, so a phantom order persisted there with no cache row to make it visible. It compounds after a reconnect, where a cancel aimed at an order that no longer exists is the expected shape. The reason is read before the status is chosen, and read as a positive statement rather than an absence: a missing or unparseable tag 102 is synthesized as -1 and says nothing, so it takes the same path as the reasons that do mean the order is still working and the cancel arrived at the wrong moment. An unknown order is retired — terminal status, removed from the book, cache row dropped. Holding the record in a non-working status instead is not an option: those are excluded from the open-order count that guards instrument reclamation, so the slot could be handed to another contract while a retained order still pointed at it, and a late fill would move the wrong position. A fill that races the rejection is recoverable on the terms the untracked-fill path sets: the execution has to carry its contract id, because nothing else says which instrument moved, and it must not be resend-marked, because a replayed execution for an order this session does not track is history rather than news. One carrying neither is dropped — as it already was for any order removed from the book ahead of a late fill. No synthetic status update is queued. The cancel-reject is the report, and both dispatchers drain fills ahead of order updates — an update queued here would reach a caller after the fill that raced it, stating the order was gone when they had just been told it filled. Both dispatchers retire the client's own record when they deliver that rejection. The open-order snapshot unions the client's records with the cached view, so retiring only the engine's left the order being reported by the surface the issue is about. Closes deepentropy#252.
userFRM
force-pushed
the
fix/unknown-order-cancel-reject-v2
branch
from
July 30, 2026 17:16
d5adcb5 to
25dbaac
Compare
userFRM
added a commit
to userFRM/ibx
that referenced
this pull request
Aug 3, 2026
…k_order and the typed status signature
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
FIX
CxlRejReason 1is UnknownOrder: the gateway is stating that the order does not exist on its side.handle_cancel_rejectrestored it to working anyway. The restore ran first and unconditionally, and only afterwards did anything branch on the reason — then it removed the shared-cache row. Engine and cache were left asserting opposite things.The engine's own view governs subsequent cancels, modifies and reconnect bookkeeping, so a phantom order persisted there with no cache row to make it visible — the inverse of the usual stale-cache problem and harder to notice. It compounds after a reconnect, where a cancel aimed at an order that no longer exists is the expected shape.
What this changes
The reason is read before the status is chosen, and read as a positive statement rather than an absence. A missing or unparseable tag 102 is synthesized as
-1and says nothing, so it takes the same path as the reasons that do mean the order is still working and the cancel arrived at the wrong moment.An unknown order is retired — terminal status, removed from the book, cache row dropped.
Holding the record in a non-working status instead is not an option: those are excluded from the open-order count that guards instrument reclamation, so the slot could be handed to another contract while a retained order still pointed at it, and a late fill would move the wrong position.
A fill that races the rejection is recoverable, on the terms the untracked-fill path from #322 sets — hence the stacking. The execution has to carry its contract id, because nothing else says which instrument moved, and it must not be resend-marked, because a replayed execution for an order this session does not track is history rather than news. One carrying neither is dropped, as it already was for any order removed from the book ahead of a late fill.
No synthetic status update is queued. The cancel-reject is the report, and both dispatchers drain fills ahead of order updates; an update queued here would reach a caller after the fill that raced it, stating the order was gone when they had just been told it filled.
Both dispatchers retire the client's own record when they deliver that rejection.
collect_open_ordersunions the client's records with the cached view, so retiring only the engine's left the order being reported by the very surface this issue is about.Tests
an_unknown_order_rejection_retires_the_order— order and cache row gone, cancel-reject delivered, no synthetic update.an_execution_racing_an_unknown_order_rejection_still_books— the fill books and the position moves.any_other_rejection_leaves_the_order_in_place— stated reasons that mean it is working, and the absent/unparseable case.Each fails by name against a compiling reversion of the production change it covers.
Closes #252.
Test plan
an_unknown_order_rejection_retires_the_orderby name.any_other_rejection_leaves_the_order_in_place.an_execution_racing_an_unknown_order_rejection_still_bookscovers the case retiring the record could have cost.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).