Standing steer (HJ, months): non-blocking behaviour + explicit execution priority.
Telemetry is not on the ladder at all, yet it sat ON the order path, inline and able to
raise — which is how it orphaned 6 live positions.
Exec path now does exactly one thing: pack primitives, append to a bounded ring, return.
No plane write, no snapshot construction, no I/O, no lock, no raise. Snapshot build +
plane publish move to a daemon drain lane. Ring is deque(maxlen=4096): drops OLDEST on
full and counts the drops — telemetry loss is always preferable to exec backpressure,
but it stays observable.
Measured: 1000 exec-path calls against a 2s-BLOCKING plane = 3.65 ms total (3.65 us/call).
Inline, that was 2000 s of stall. Wedged lane + 50k pushes -> ring pinned at 4096,
45903 counted drops, exec path never backpressured. Healthy plane still gets 5/5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pre-existing guard covered only publish(). The attribute/coercion block above it
(int()/str()/.metadata.get() on intent+order) was UNGUARDED, and kwarg binding happens
before the body runs at all — so a signature skew sailed straight past a defence that
existed precisely to prevent this, and orphaned 6 live positions.
Everything that can raise now lives inside one try. Failures log loud and are swallowed;
the order path is never affected. Proven against: exploding plane, poisoned intent
coercion (the formerly-unguarded region), and absent plane.
ARCHITECTURAL DEBT (raised by HJ, not fixed here): telemetry has no business being called
inline on the exec path at all. Correct shape = fire-and-forget enqueue drained by the M6
journal lane. This commit makes it harmless, not absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M1 re-vendor (89a1f1a) clobbered the T0-DEVIATION fix (20ad493): submit()/submit_async()
pass order_id=/client_order_id= post-ack, the signature dropped them. Every ENTER raised
TypeError AFTER the venue POST returned 200 OK -> rust_backend synthesised REJECTED ->
FSM rollback, while the venue kept the position. Flight-4: 8 bridged promotions, 6 orphan
SHORTs live on VST with a kernel that believes it is flat.
Two fixes:
1. signature accepts order_id/client_order_id again; snapshot prefers them (order is None
on the submit path, so the ack row is the only id source).
2. post-ack telemetry wrapped: observability can never again veto an accepted order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Architecture: DuckDB for persistence + full in-memory materialization for reads.
All reads served from Python dicts (sub-microsecond). DuckDB only hit on writes.
Performance evolution (get_asset benchmark):
V0 (raw DuckDB): 876µs per call
V1 (LRU cache): 2.3µs per call (380x)
V2 (in-memory): 0.2µs per call (4380x)
All reads now sub-microsecond:
get_asset: 0.2µs (was 876µs)
query(blockian): 6.6µs (was 2.2ms)
query(sector): 6.6µs (was 3.2ms)
exchange lookup: 12.5µs (was 1.5ms)
full scan: 5.9µs (was 1.8ms)
behavior: 0.4µs
Write path: sync_from_profiles batch-inserts all data, then materializes
into Python dicts. Resync: 76ms (was 210ms, 2.8x faster).
Data integrity: DuckDB WAL provides crash recovery. In-memory dicts are
reconstructed from DB on every sync/close-reopen cycle. Zero data loss.
Upstream-canonical commit of the ~366 lines that sat uncommitted in the working
tree (surveyed in DITAV2_SOA_SURVEY_20260702). Venue telemetry published at every
venue boundary (submit/cancel/reconcile) over the zinc venue_region; SOA per
UV DITAv2 adjudication 2026-07-03.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: open_positions()/open_orders() called _backend_snapshot() ->
_call_backend() -> _run() -> pool.submit(asyncio.run, coro) which spawned a
temporary event loop in a worker thread. httpx AsyncClient created inside that
temp loop, loop closed immediately. All subsequent HTTP calls raised Event loop
is closed or asyncio.locks.Event bound to different loop. Crash triggered WS
stream reconnects; each reconnect re-ran reconcile with N>1 BingX positions and
orphaned all but the largest.
Fix: open_positions()/open_orders() now read backend._state (populated by
await backend.connect() in the main loop). Fallback to _backend_snapshot()
for callers without a connected backend.
Fixes test_bingx_bugs::TestConnectNoDoubleRefresh: connect() is now async.
New test_orphan_prevention.py: 23 tests covering all 5 orphan mechanisms:
A. open_positions/open_orders use backend._state, never hit thread pool
B. connect() awaitable, backend.connect() runs in main event loop
C. Reconcile guard: >1 position logs ERROR and takes only largest
D. clientOrderId p-action-base36-rand4 on every order
E. EXIT sizing capped to kernel slot_size
391 passed, 2 skipped, 0 failed across all 14 test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
1. bingx_venue.py: add cancel_async() — async cancel that awaits backend.cancel()
directly in the main event loop. The sync cancel() path goes through _run()
→ thread-pool → asyncio.run() in a new thread, but aiohttp is bound to the
main loop → deadlock. Identical root cause as the old sync submit() → fixed
via submit_async. Remove dead cancel_order branch (BingxDirectExecutionAdapter
has cancel, not cancel_order).
2. rust_backend.py: process_intent_async CANCEL path now uses cancel_async when
available (matching the submit_async pattern for ENTER/EXIT). Sync cancel()
fallback kept for MockVenueAdapter compat.
3. bingx_direct.py: guard S2 background refresh task per symbol. Old code discarded
the task reference; rapid submits piled up concurrent _refresh_state_background
calls all writing self._state in arbitrary completion order (stale last-writer-
wins). Now: skip creating a new task if one is already pending for the symbol;
store reference and clear via done-callback.
Test additions (test_bingx_bugs.py, 29 tests):
- cancel_async: awaitable, calls backend.cancel directly, maps all statuses
- process_intent_async CANCEL: dispatches cancel_async / falls back to sync
- S2 guard: task stored, no duplicates while pending, new task after done
- _events_from_submit with None snapshots: FILLED/NEW/REJECTED/PARTIAL/RATE_LIMITED
- _filled_size_from_snapshots(None, None): safe 0.0 return
- _events_from_cancel: before/after completely ignored
- connect(): no double refresh_state, no-op if backend has no connect
- submit() sync with None snapshots: FULL_FILL still emitted
- cancel() branch audit: uses cancel not cancel_order, raises for no-cancel backend
Fix: test_exchange_event_seam_parity.py TestMockSubscribe — replace deprecated
asyncio.get_event_loop().run_until_complete() with asyncio.run() (Python 3.12
raises RuntimeError when event loop is closed by earlier suite tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: connect() — remove redundant _backend_snapshot(include_history=True).
backend.connect() already called refresh_state(); this was a second identical
network round-trip at startup (~400ms wasted).
Fix 2: cancel() — remove snapshot_before + snapshot_after. _events_from_cancel
never reads 'before' or 'after' — two gratuitous round-trips per cancel with
zero benefit.
Fix 3: submit() (sync/legacy path) — drop both _backend_snapshot calls, pass
None like submit_async already does. Receipt executedQty fields take precedence;
_filled_size_from_snapshots returning 0.0 is the correct safe fallback.
117/117 tests pass (2 pre-existing pytest-ordering failures in TestMockSubscribe
are unrelated — asyncio.get_event_loop() contamination from other suite files,
25/25 pass when the file runs alone).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
S1 — Leverage cache (bingx_direct.py):
_ensure_leverage(): per-symbol asyncio.Lock + cached value check; skips ~350ms
POST when exchange already has the requested leverage. Saves ~350ms/trade.
Cache updated ONLY on success; failed POST leaves cache stale → correct retry.
Persist: JSON sidecar /tmp/.bingx_leverage_cache_{env}.json; survives restarts.
connect(): _verify_leverage_drift() detects when another process changed leverage
at the exchange and updates cache to exchange truth (logs WARNING on drift).
Multi-runner contract: leverage is account-level on BingX; documented that
concurrent runners with different leverage desires for same symbol conflict.
20 mock tests: same-lev skip, change-triggers-POST, failure-no-cache-update,
concurrent-same-symbol (lock prevents race), drift-detect, persist/restore,
multi-runner known-limitation documentation test.
S2 — Background state refresh (bingx_direct.py):
MARKET fills: asyncio.create_task(_refresh_state_background) — does not block
submit path. WS FILL_SETTLED + ACCOUNT_UPDATE deliver capital truth anyway.
LIMIT fills: synchronous refresh retained (include_history=False, not True) —
needed to detect resting order state for next pump cycle.
Saves ~600–900ms/trade on MARKET exits. ENTER similarly improved.
Gap 1 — VenueEvent friction fields (contracts.py):
Added: fee, fee_asset, fee_source, is_maker, exchange_ts, slippage_bps,
mark_at_submit — all with defaults so existing callers are unaffected.
Detailed inline docs for sign conventions and provenance codes.
Gap 2 — Fee estimation + WS_SETTLED provenance (bingx_direct.py, pink_clickhouse.py):
submit_intent: estimates fee from fill_price × fill_qty × taker/maker rate;
annotates ack_row with _fee_estimated, _fee_source, _is_maker_est.
persist_fee_settled(): new method writes fee_settled_events row when WS
ORDER_TRADE_UPDATE delivers actual commission ("n" field); fee_source="WS_SETTLED".
pink_direct._run_account_stream: calls persist_fee_settled on FILL_SETTLED.
Gap 3 — Slippage measurement (bingx_direct.py, bingx_venue.py, pink_clickhouse.py):
Captures mark_at_submit before the order POST; computes slippage_bps signed
by side: positive = adverse (taker overpaid / maker undersold), negative =
price improvement. Measured for BOTH taker and maker fills for symmetry.
Flows through VenueEvent → trade_events.slippage_bps + trade_exit_legs.slippage_bps.
S3 / SOR — Maker order placement: comprehensive TODO block in submit_intent with:
SHORT/LONG-aware price offset design, OBF integration requirements,
TODO_ADD_PARAMSET_VIBRISS for spread_bps threshold, intelligent timeout_s
calibration requirements, price-impact awareness gap, SOR abstraction CRITICAL TODO.
REST/WS split: documented why BingX (and all retail venues) separate these
and why a unified VenueAdapter protocol is the long-term solution.
151/151 existing tests green + 20 new leverage cache tests = 171 total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Twenty-first pass: no ABI compatibility check on Rust .so load stale binary
corrupts silently (X1 Critical), real_zinc_plane _write_region zeroes entire
buffer before write visible all-zero window (X2 Critical), no requirements.txt
setup.py pyproject.toml zero Python dependency declarations (X3 Critical),
RealZincControlPlane.update() no thread lock concurrent calls corrupt seq and
shared memory (X4 High), libc declared in Cargo.toml never used dead dependency
(X5 High), 5 test files hardcoded sys.path.insert non-portable (X6 High),
_decode_packet no try/except on json.loads partial body read crashes reader (X7
High), ExchangeEvent not exported from __init__.py package API inconsistency (X8
High), RealZincPlane and RealZincControlPlane collide on {prefix}_control region
name (X10 Medium). 375 total flaws across 21 passes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
First commit of the previously-untracked PINK-on-DITAv2 migration system
(execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha
Engine algorithmic integrity is preserved). BLUE is untouched.
Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope):
- Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs.
- Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test
genuinely exercises its fix. Most important: Flaw 5 now asserts capital
moves by EXACTLY realized PnL (was entering/exiting at the same price).
- Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/
bridge + 96 runtime/persistence/multi-exit/restart/seams).
- GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green.
- Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore
(excludes Rust target/ build artifacts).
LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>