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/userDataStreamendpoint, 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.pymust usebase_url_ws_privatefrom config (already inBingxExecClientConfig)
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
executionReportschema: confirmed from BLUE observer.py analysis; verify against live VST fill when first Phase 2 order is placedACCOUNT_UPDATEbalance fields:wb(wallet balance),cw(cross wallet balance)- Funding fee
fs.fasign 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)