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>