Commit Graph

3 Commits

Author SHA1 Message Date
Codex
7e83a5c5c5 PINK: fix event-loop corruption in open_positions/open_orders + 23-test orphan suite
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>
2026-06-06 20:53:41 +02:00
Codex
33d8e855c8 PINK: fix EXIT position not closing — 3 root causes, 368/368 tests green
Root cause 1 (http.py): duplicate signature= in POST body — canonical_query
included signature key after build_signed_params injected it, then body
appended &signature= again. Fix: exclude 'signature' from canonical.

Root cause 2 (bingx_direct + http.py): HTTP retry sent same MARKET order to
backup URL (bingx.pro), which hits the same VST account. Without clientOrderId,
each retry opened a new SHORT position; EXIT BUY 10 only closed one. Fix:
restore clientOrderId in hyphen format p-{e/x}-{base36_ts}-{rand4} (pure
alphanumeric rejected by VST; hyphen format accepted). Adds max_retries_override
+ urls_to_try to _request_json for non-idempotent override path.

Root cause 3 (flat_and_start_pink): k.venue.connect() ran backend.connect()
inside asyncio.run() in a thread-pool. httpx session created there references
a dead event loop; order POSTs raise RuntimeError("Event loop is closed").
Fix: await adapter.connect() directly from main event loop.

Also: enter_wall_ms + tight _is_our_position createTime filter to separate
PINK's position from concurrent strategies on shared VST account. 1.5s
settle sleep before flat check.

New test suite test_bingx_http_safety.py: 20 tests covering idempotency,
retry correctness, backup-URL dedup, event-loop hygiene, signing correctness.

Live result: ENTER 290ms, EXIT 260ms — both sub-second. Position flat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 01:39:35 +02:00
Codex
535eea855d PINK: cancel_async, S2 task guard, 29 new regression tests — 346/346 green
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>
2026-06-05 16:02:13 +02:00