Repository navigation
Handle EXECUTION_COMPLETE place reports as terminal (#743) - #870
wippa-studios wants to merge 1 commit into
Conversation
Betfair returns orderStatus=EXECUTION_COMPLETE when a limit order crosses the book on placement. execute_place matched on PENDING and EXPIRED only, so EXECUTION_COMPLETE fell through to the else branch and published the order as EXECUTABLE. The order stream does eventually correct this, which makes it intermittent: the window is between the HTTP response and the next stream update. In that window the order reports a live status with nothing remaining, and any decision taken from it is made on a phantom position. The place report is already authoritative, so no stream round-trip is needed. Terminality is now derived from the report rather than from which branch executed. Regression tests use a real BetfairOrder; a mock cannot hold state, so a mock-based test cannot fail on this. Covers the resting-order, EXPIRED and PENDING paths as controls. Refs #743
|
High chance of a race condition if you do this |
|
You're right, and I missed the mechanism when I wrote that line. The change isn't status-only, and that's my error. I traced it after your comment:
My claim that "the place report is already authoritative" holds for the status. It does not hold for when the trade is considered complete. I asserted terminality where I should have been asserting it without the cascade. Where that leaves the options, as I see them
I lean toward (1) — the status is genuinely wrong but I can't demonstrate that fixing it here is safe, and you know the threading model better than I do. Tests in the PR stay useful either way: Happy to close it, or to rework to whichever option you prefer. Not going to push back on the race — I couldn't rule it out from the code and I've only now read |
|
i am not reading all of that |
|
Closing this one. You're right about the race — Closing rather than reworking since you've already told me where it lands. The analysis and the failing test are in #743 if it's useful later. Not pushing back. |
Fixes #743
The defect
execute_placebranches on the venue'sorderStatus:Betfair's
orderStatusenum isPENDING | EXECUTABLE | EXECUTION_COMPLETE | EXPIRED. A limit order that crosses the book on placement returnsEXECUTION_COMPLETE, which matches neither branch and falls through to theelse— publishing a fully-matched order asEXECUTABLE.Why it only shows up sometimes
The
elseis annotated "let process.py pick it up", and it does:process_current_orderhas anelif order.status == OrderStatus.EXECUTABLEarm that transitions toEXECUTION_COMPLETEonce the stream delivers aCurrentOrderreporting that status.So the order self-heals — after the HTTP response, when the next order-stream update lands. The window between the place response and that update is exactly when the order advertises a live status with
size_remaining == 0. Anything readingorder.statusin that window acts on a phantom position.That also matches the follow-up in #743: it reproduces in both sync and async mode, because this is about report handling rather than transport.
The fix
The place report is already authoritative — no stream round-trip is needed to know the order is terminal. This makes terminality a function of the venue's answer rather than of which branch happened to run.
Tests
New file
tests/test_execution_order_status.py, four tests:test_immediately_matched_is_execution_completetest_resting_order_is_executabletest_expired_is_execution_completetest_pending_leaves_status_aloneThe tests drive a real
BetfairOrderand mock only_execution_helper(the HTTP boundary). The defect is in the state the order ends up in; aMockholds no state, so a mock-based assertion cannot fail on this — and the existingtest_execute_place_successleavesorder_statusas an auto-Mock, which is why it passes today and effectively pins theelsebranch as correct.Verified in both directions:
Full suite: 1059 tests, all passing. No existing test is modified.
Scope
Only #743. The same
else-branch pattern exists inexecute_replace, wherereplacement_order.executable()atbetfairexecution.py:209is unconditional — but a replacement order's status has to come off theplaceInstructionReport, and I'd rather not bundle two behavioural changes into one fix. Happy to follow up if you want it.#509 (stale
size_remaining/size_cancelledafter a partial cancel) is diagnosed but not addressed here.execute_cancelreadsinstruction_report.size_cancelledto decide the status and then discards it, leaving the order publishing pre-cancel sizes until the stream catches up. A naive fix double-counts when the stream has already delivered the update, so it needs a deliberate idempotency decision rather than a patch. I'll put the failing test in that thread rather than land a red test here.Happy to rework either if the approach is wrong — the analysis is in #743 and #509.