110 lines
4.0 KiB
Markdown
110 lines
4.0 KiB
Markdown
|
|
# 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)
|
||
|
|
|
||
|
|
```json
|
||
|
|
{"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)
|