Files
siloqy/prod/clean_arch/dita_v2/BINGX_USERSTREAM_NOTES.md

4.0 KiB

BingX User Stream — VST Probe Notes (Phase 0)

Date: 2026-06-01
Scope: VST only (no LIVE touch).
Result: Outcome A — VST has WebSocket. Full WS-on-both symmetry is achievable.


Gate G0 resolution

Check Result
listenKey endpoint (POST /openApi/user/auth/userDataStream) Returns listenKey (signed request, signed_post_raw)
Signing method Standard HMAC-SHA256 signed POST works — "header-only/unsigned" concern was unfounded
WS URL wss://vst-open-api-ws.bingx.com/swap-market?listenKey=<key>
Frames delivered 667 SNAPSHOT frames in 20 s (idle session, no active orders)
Gzip Binary frames are gzip-compressed — gzip.decompress(bytes(msg.data))
Ping/Pong Server sends text "Ping" → client must respond with "Pong"
listenKey keepalive PUT /openApi/user/auth/userDataStream {"listenKey": ...}
listenKey delete DELETE /openApi/user/auth/userDataStream {"listenKey": ...}

Event schemas

SNAPSHOT — position/leverage state (received continuously)

{"e":"SNAPSHOT","E":1780336019559,"ac":{"s":"MTL-USDT","l":1,"S":1,"mt":"isolated"}}
Field Meaning
e "SNAPSHOT"
E Server timestamp ms
ac.s Symbol
ac.l Long leverage
ac.S Short leverage
ac.mt Margin type ("isolated")

ORDER_TRADE_UPDATE — fill/order status (arrives on trade activity)

Top-level envelope: {"e":"ORDER_TRADE_UPDATE","E":<ts>,"o":{...}}

Inner o object:

Field Meaning
s Symbol
c clientOrderId
i orderId (venue)
X Order status (NEW, PARTIALLY_FILLED, FILLED, CANCELED)
x Execution type
p Order price
ap Average fill price
z Cumulative filled qty (total filled so far)
l lastFilledQty — incremental fill for this event
L Last fill price
n Commission amount
N Commission asset

Critical: z is cumulative; l is incremental per-event. bingx_venue.py:582 reads lastFilledQty = l. The Rust kernel's apply_fill now accumulates (slot.size += l).

ACCOUNT_UPDATE — balance/position push (arrives on trade activity)

Top-level: {"e":"ACCOUNT_UPDATE","E":<ts>,...}

Balance array (B): [{"a":"USDT","wb":<wallet_balance>,"cw":<cross_wallet_balance>}] Position array (P): [{"s":<symbol>,"pa":<positionAmt>,"ep":<entryPrice>,"up":<unrealizedPnL>,"mt":<marginType>,"ps":<positionSide>}]

FUNDING_FEE — funding charge (arrives on funding interval)

Envelope: {"e":"FUNDING_FEE","E":<ts>,"fs":{"s":<symbol>,"fa":<fundingAmount>,"a":<asset>}}

Identified by m == "FUNDING_FEE" in some variants, or e == "FUNDING_FEE".


VST ↔ LIVE symmetry notes

  • Same POST /openApi/user/auth/userDataStream endpoint, same signing method
  • VST WS base: wss://vst-open-api-ws.bingx.com/swap-market
  • LIVE WS base: wss://open-api-swap.bingx.com/swap-market
  • Only difference: base hostname — all frame schemas are identical
  • bingx_user_stream.py must use base_url_ws_private from config (already in BingxExecClientConfig)

listenKey lifecycle

POST  /openApi/user/auth/userDataStream {}               → {"listenKey": "..."}
PUT   /openApi/user/auth/userDataStream {"listenKey":..} → {} (keepalive, every 1800s)
DELETE /openApi/user/auth/userDataStream {"listenKey":..} → {} (on close)

listenKey TTL: ~60 min. Keepalive extends it. Server signals expiry via {"e":"listenKeyExpired"}.


Open items for Phase 2

  • executionReport schema: confirmed from BLUE observer.py analysis; verify against live VST fill when first Phase 2 order is placed
  • ACCOUNT_UPDATE balance fields: wb (wallet balance), cw (cross wallet balance)
  • Funding fee fs.fa sign convention (positive = received, negative = paid) — to verify
  • 24h connection cap: BingX closes the socket after ~24h regardless of keepalive; overlap-rotation strategy required (open new connection before closing old)