Skip to content

v0.8.4 — Propagate per-item sCode into exchange_code on batch failures

Choose a tag to compare

@ChronoCoders ChronoCoders released this 17 May 00:33
· 34 commits to main since this release

Fixes a latent bug that made the v0.7.2 idempotent-cancel handler a no-op against OKX.

The bug

OKX returns batch-failure responses as:

{"code":"1","msg":"All operations failed","data":[{"sCode":"51400","sMsg":"Order cancellation failed as the order has been filled, canceled or does not exist."}]}

The top-level code="1" is the generic "at least one operation failed" envelope. The per-item data[0].sCode is the actual error.

process_status (in src/execution/okx_rest_client.cpp) was writing the top-level 1 into exchange_code_out and returning false. The per-item sCode was extracted via parse_data_first but only used in the log line — never written back.

Downstream, OrderManager::cancel_slot (v0.7.2) checks:

if (c.binance_code == 51400 || c.binance_code == -2011) {
    spdlog::info("cancel_idempotent ... reason=already_gone");
    transition(slot_idx, OrderState::CANCELED);
    s.active = false;
    return true;
}

With c.binance_code = 1 (not 51400), the idempotent branch never fired. The slot stayed active=true in state ACKNOWLEDGED, and every subsequent quote tick retried the cancel → another 51400 → another retry, forever. Sides got stuck after the first silent fill.

Caught during the v0.8.3 OKX demo validation:

  • 22 ACK cycles ✓
  • 21 CANCELED transitions ✓
  • 29 okx_rest_fail item_code=51400 errors
  • 0 cancel_idempotent log lines — the smoking gun

The fix

In process_status, after parse_data_first(body), if pr.s_code != 0, overwrite exchange_code_out with the per-item code before returning false. The per-item code is the actionable one for callers.

Three lines of real change inside the existing block, plus a WHY comment explaining why per-item takes precedence over top-level on batch failures.

const auto pr = parse_data_first(body);
if (pr.s_code != 0) {
    exchange_code_out = pr.s_code;
}
spdlog::warn(...);
return false;

Effect across callers

This change benefits every OKX REST caller that inspects binance_code on failure:

  • cancel_order → OrderManager::cancel_slot idempotent path (51400) now fires.
  • place_order / place_market_order → callers see specific reject reasons (51008 insufficient margin, 51010 account-mode mismatch, 51000 clOrdId format error, etc.) instead of generic 1. Useful for any future code that wants to react to specific OKX error codes.
  • amend_order → same surface.

Tests

8/8 C++ tests pass under -O3 -march=native -Wall -Wextra -Werror, zero warnings. Existing test_execution::AmendHalfStateReportsCritical and test_execution::GeoblockHaltTrigger cover the geoblock (50114) and amend-half-state paths; both still pass because the change is additive (only overwrites when per-item code is present and non-zero).

Known follow-up: Phase 9 user-data stream

The validation run also surfaced 1 silent fill on the OKX demo book — our local PositionTracker showed inv=0 while OKX held pos=1 at 78145.30. Root cause is the absence of a private user-data WebSocket subscription (/v5/private with signed auth handshake for the orders / fills channels). With this fix, the cancel path handles the silent-fill aftermath gracefully (slot transitions to CANCELED idempotently), but the fill itself still goes unnotified to the local position tracker. The next startup catches the divergence via reconcile_now() and either trips the circuit breaker (if > position_divergence_tolerance) or accepts it as new state.

Full fix is the Phase 9 private-WS wiring; tools/okx_demo_flatten.sh remains the workaround for demo testing.