Skip to content

orders: report the order's cumulative fill on order_status, not the last print (ibx#315) - #323

Open
userFRM wants to merge 2 commits into
deepentropy:mainfrom
userFRM:fix/order-status-reports-last-fill
Open

orders: report the order's cumulative fill on order_status, not the last print (ibx#315)#323
userFRM wants to merge 2 commits into
deepentropy:mainfrom
userFRM:fix/order-status-reports-last-fill

Conversation

@userFRM

@userFRM userFRM commented Jul 29, 2026

Copy link
Copy Markdown

Stacked on #322. The first commit is that PR; review this one from its second commit.

Summary

  • order_status was given the size and price of the print that triggered it, where the callback's contract asks for the order as a whole. Argument 3 (filled) received the print's quantity and argument 5 (avgFillPrice) its price. Argument 8, lastFillPrice, received the same price — the one place it belonged.
  • IB's contract is filled cumulative, avgFillPrice volume-weighted across every print, lastFillPrice this print.
  • A 300-share order filling in three 100-share prints should report filled=100 avg=P1 → filled=200 avg=(P1+P2)/2 → filled=300 avg=VWAP. It reported filled=100 three times, at three different prices each presented as the average.
  • Any average-cost or P&L calculation reading avgFillPrice is wrong whenever an order fills in more than one print, which above a round lot is most of the time. filled never reaches the order quantity, so completion detection on that field never fires.

Closes #315.

Change

The gateway sends both numbers on every execution report — CumQty (14) and AvgPx (6). They were already parsed, but only into the execution record, never into the status callback.

They now travel on the fill itself. Reading them from the order cache at dispatch time would have been a smaller change, but a second print arriving between the push and the drain would report its cumulative against the earlier fill; carrying them with the event keeps each one self-consistent. A report that omits either field falls back to the print, which is what the callback reported before.

Every consumer, not just the one the issue named

  • Python compat client — dispatches the same drained fill through its own order_status and had the identical pair.
  • Open-order snapshot — both clients stored the print as the order's filled quantity, so after a partial fill the push callback said filled=200 while reqOpenOrders said filled=100 for the same order.

Tests

One end-to-end assertion pins the whole callback string, so it covers filled and avgFillPrice together:

order_status:42:PartiallyFilled:200:100:150.5

Reverting the dispatch produces ...:100:100:151, which is the reported bug exactly. Two more tests drive an execution report through the handler and assert the drained fill's cumulative pair comes off tags 14 and 6 rather than 32 and 31, and that it falls back to the print when neither is present.

Test plan

🤖 Generated with Claude Code

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 is excluded. On a fresh process the gateway resends prior executions carrying `97=Y` and their original ExecIDs, for orders that no session tracks — 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 only consulted on the untracked path.

The ExecID is recorded once the fill can be booked rather than before. An execution that could not be placed stays replayable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ast print (ibx#315)

`order_status` was given the size and price of the print that triggered it where the callback's contract asks for the order as a whole. Argument 3, `filled`, received this print's quantity; argument 5, `avgFillPrice`, received this print's price. Argument 8 is `lastFillPrice` and received the same price, which is the one place it belonged.

IB's contract is that `filled` is cumulative and `avgFillPrice` is volume-weighted across every print, with `lastFillPrice` describing the print. A 300-share order filling in three 100-share prints therefore reported `filled=100` three times, at three different prices each presented as the average. Average-cost and P&L calculations reading that field are wrong for any order that fills in more than one print, and `filled` never reaches the order quantity, so completion detection on it never fires.

The gateway sends both numbers on every execution report — CumQty (14) and AvgPx (6). They were already parsed, but only into the execution record, never into the status callback.

They now travel on the fill itself. Reading them from the order cache at dispatch time would have been smaller, but a second print arriving between the push and the drain would report its cumulative against the earlier fill; carrying them with the event keeps each one self-consistent. A report that omits either field falls back to the print, which is what the callback reported before.

Every consumer of the drained fill is corrected, not only the one the report named. The Python compat client dispatches the same fill through its own `order_status` and had the identical pair; the open-order snapshot both clients poll stored the print as the order's filled quantity, so a partial fill would have had the push callback and `reqOpenOrders` disagreeing about the same order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@userFRM
userFRM force-pushed the fix/order-status-reports-last-fill branch from 4d509d0 to 109cb48 Compare July 29, 2026 16:53
@userFRM

userFRM commented Jul 29, 2026

Copy link
Copy Markdown
Author

Four corrections in <this branch>.

Two integration targets stopped compiling. Adding fields to Fill broke twenty struct literals across tests/scenarios.rs and tests/hot_loop_lifecycle.rs, which cargo test --lib never builds. Both compile now, and the gate for this branch covers every target rather than the library alone.

A debug hook removed. An IBX_WIRE_CAPTURE println had been left in the fill path from wire-capture work; it is out, and the branch is clean of it throughout its history.

Scope trimmed to the message. The commit had also carried an unrelated server-tag redesign, byte-identical to the one in #286, under a title about filled quantity. Stripped to what the message claims.

A misindented field corrected to rustfmt.

The functional change is unaltered: order_status reports cumulative filled quantity rather than the unfilled remainder, and LeavesQty remains correct at both of its production uses.

userFRM added a commit to userFRM/ibx that referenced this pull request Aug 3, 2026
…totals on the fill, booked quantity preserved
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: order_status reports the last print's size and price as filled and avgFillPrice, so a multi-print order never reports its true average

1 participant