Skip to content

Commit 7d36369

Browse files
authored
feat: updgrade spot and margin websocket (#699)
* upgrade spot ws * user and magin ws * add examples * lint * update websocket * fix order price
1 parent 8551c72 commit 7d36369

13 files changed

Lines changed: 894 additions & 203 deletions

File tree

examples/ws-margin-user-stream.mjs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* WebSocket Margin User Data Stream Example
3+
*
4+
* Connects to the cross-margin user data stream via WebSocket API
5+
* (listenToken method), places a margin limit order, and logs
6+
* the execution reports received.
7+
*
8+
* Usage:
9+
* export BINANCE_APIKEY="..."
10+
* export BINANCE_SECRET="..."
11+
* node examples/ws-margin-user-stream.mjs
12+
*
13+
* For isolated margin, use client.ws.isolatedMarginUser() instead:
14+
* client.ws.isolatedMarginUser({ symbol: 'BTCUSDT' }, msg => { ... })
15+
*/
16+
17+
import BinanceModule from '../dist/index.js'
18+
const Binance = BinanceModule.default
19+
20+
const client = Binance({
21+
apiKey: process.env.BINANCE_APIKEY,
22+
apiSecret: process.env.BINANCE_SECRET,
23+
})
24+
25+
const CONNECT_TIMEOUT_MS = 15000
26+
27+
async function main() {
28+
// 1. Connect to margin user data stream with a timeout
29+
console.log('Connecting to margin user data stream...')
30+
const clean = await Promise.race([
31+
client.ws.marginUser(msg => {
32+
console.log('\n--- Margin User Event ---')
33+
console.log('Type:', msg.eventType || msg.type)
34+
console.log(JSON.stringify(msg, null, 2))
35+
}),
36+
new Promise((_, reject) =>
37+
setTimeout(
38+
() => reject(new Error('Connection timed out after ' + CONNECT_TIMEOUT_MS + 'ms')),
39+
CONNECT_TIMEOUT_MS,
40+
),
41+
),
42+
])
43+
console.log('Connected.\n')
44+
45+
// 2. Check margin balances and pick a viable order
46+
// Tries to sell an asset we hold; amount is kept small to avoid fills.
47+
console.log('Checking margin account balances...')
48+
const account = await client.marginAccountInfo()
49+
const nonZero = account.userAssets.filter(a => parseFloat(a.free) > 0)
50+
nonZero.forEach(a => console.log(` ${a.asset}: free ${a.free}`))
51+
52+
// Candidate pairs: prefer selling a non-USDT asset we hold against USDT.
53+
// Quantity must clear the $5 min notional filter.
54+
const MIN_NOTIONAL = 5.5
55+
56+
let symbol, side, limitPrice, quantity
57+
const allPrices = await client.prices()
58+
const freeUsdt = parseFloat(nonZero.find(a => a.asset === 'USDT')?.free || '0')
59+
60+
// Assets to try (order of preference)
61+
const assets = ['ETH', 'BTC', 'SOL', 'BNB']
62+
63+
for (const asset of assets) {
64+
const pair = `${asset}USDT`
65+
const price = parseFloat(allPrices[pair] || '0')
66+
if (!price) continue
67+
68+
const bal = nonZero.find(a => a.asset === asset)
69+
const free = parseFloat(bal?.free || '0')
70+
// Compute the smallest qty that clears min notional, rounded up to 4 decimals
71+
const minQty = Math.ceil((MIN_NOTIONAL / price) * 10000) / 10000
72+
73+
if (free >= minQty) {
74+
symbol = pair
75+
side = 'SELL'
76+
quantity = minQty.toFixed(4)
77+
limitPrice = (price * 1.05).toFixed(2)
78+
console.log(`\n ${asset} free: ${free} >= ${minQty} — SELL ${pair} @ ${limitPrice}`)
79+
break
80+
}
81+
82+
if (freeUsdt >= price * 0.95 * minQty) {
83+
symbol = pair
84+
side = 'BUY'
85+
quantity = minQty.toFixed(4)
86+
limitPrice = (price * 0.95).toFixed(2)
87+
console.log(`\n USDT free: ${freeUsdt} — BUY ${pair} @ ${limitPrice}`)
88+
break
89+
}
90+
}
91+
92+
if (!symbol) {
93+
console.log('\nNo sufficient balance found for any candidate pair.')
94+
console.log('Stream connection was successful. Transfer funds to cross-margin and retry.')
95+
clean()
96+
process.exit(0)
97+
}
98+
99+
console.log(`Placing margin limit ${side} ${quantity} ${symbol} @ ${limitPrice}...`)
100+
101+
// 3. Place a margin limit order
102+
const order = await client.marginOrder({
103+
symbol,
104+
side,
105+
type: 'LIMIT',
106+
quantity,
107+
price: limitPrice,
108+
})
109+
console.log('Margin order placed:', {
110+
orderId: order.orderId,
111+
symbol: order.symbol,
112+
side: order.side,
113+
type: order.type,
114+
price: order.price,
115+
status: order.status,
116+
})
117+
118+
// 4. Wait for events to come through, then cancel and clean up
119+
console.log('\nWaiting 5s for WebSocket events...')
120+
await new Promise(r => setTimeout(r, 5000))
121+
122+
console.log('\nCancelling order...')
123+
try {
124+
const cancelled = await client.marginCancelOrder({
125+
symbol,
126+
orderId: order.orderId,
127+
})
128+
console.log('Cancelled:', cancelled.status)
129+
} catch (e) {
130+
console.log('Cancel error (order may have already been filled):', e.message)
131+
}
132+
133+
// 5. Wait a bit more for the cancel event
134+
await new Promise(r => setTimeout(r, 2000))
135+
136+
console.log('\nClosing WebSocket...')
137+
clean()
138+
console.log('Done.')
139+
process.exit(0)
140+
}
141+
142+
main().catch(err => {
143+
console.error('Error:', err.message || err)
144+
process.exit(1)
145+
})

examples/ws-user-stream.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* WebSocket User Data Stream Example
3+
*
4+
* Connects to the spot user data stream via WebSocket API,
5+
* places a limit order, and logs the execution reports received.
6+
*
7+
* Usage:
8+
* export BINANCE_APIKEY="..."
9+
* export BINANCE_SECRET="..."
10+
* node examples/ws-user-stream.mjs
11+
*/
12+
13+
import BinanceModule from '../dist/index.js'
14+
const Binance = BinanceModule.default
15+
16+
const client = Binance({
17+
apiKey: process.env.BINANCE_APIKEY,
18+
apiSecret: process.env.BINANCE_SECRET,
19+
httpBase: 'https://demo-api.binance.com',
20+
wsApi: 'wss://demo-ws-api.binance.com/ws-api/v3',
21+
})
22+
23+
const CONNECT_TIMEOUT_MS = 15000
24+
25+
async function main() {
26+
// 1. Connect to user data stream with a timeout
27+
console.log('Connecting to user data stream...')
28+
const clean = await Promise.race([
29+
client.ws.user(msg => {
30+
console.log('\n--- User Event ---')
31+
console.log('Type:', msg.eventType || msg.type)
32+
console.log(JSON.stringify(msg, null, 2))
33+
}),
34+
new Promise((_, reject) =>
35+
setTimeout(
36+
() => reject(new Error('Connection timed out after ' + CONNECT_TIMEOUT_MS + 'ms')),
37+
CONNECT_TIMEOUT_MS,
38+
),
39+
),
40+
])
41+
console.log('Connected.\n')
42+
43+
// 2. Get current price for BTCUSDT to set a limit price far from market
44+
const prices = await client.prices({ symbol: 'BTCUSDT' })
45+
const currentPrice = parseFloat(prices.BTCUSDT)
46+
// Set limit buy 5% below market so it won't fill immediately
47+
const limitPrice = (currentPrice * 0.95).toFixed(2)
48+
49+
console.log(`BTCUSDT current price: ${currentPrice}`)
50+
console.log(`Placing limit BUY at ${limitPrice}...\n`)
51+
52+
// 3. Place a limit order
53+
const order = await client.order({
54+
symbol: 'BTCUSDT',
55+
side: 'BUY',
56+
type: 'LIMIT',
57+
quantity: '0.001',
58+
price: limitPrice,
59+
})
60+
console.log('Order placed:', {
61+
orderId: order.orderId,
62+
symbol: order.symbol,
63+
side: order.side,
64+
type: order.type,
65+
price: order.price,
66+
status: order.status,
67+
})
68+
69+
// 4. Wait for events to come through, then cancel and clean up
70+
console.log('\nWaiting 5s for WebSocket events...')
71+
await new Promise(r => setTimeout(r, 5000))
72+
73+
console.log('\nCancelling order...')
74+
try {
75+
const cancelled = await client.cancelOrder({
76+
symbol: 'BTCUSDT',
77+
orderId: order.orderId,
78+
})
79+
console.log('Cancelled:', cancelled.status)
80+
} catch (e) {
81+
console.log('Cancel error (order may have already been filled):', e.message)
82+
}
83+
84+
// 5. Wait a bit more for the cancel event
85+
await new Promise(r => setTimeout(r, 2000))
86+
87+
console.log('\nClosing WebSocket...')
88+
clean()
89+
console.log('Done.')
90+
process.exit(0)
91+
}
92+
93+
main().catch(err => {
94+
console.error('Error:', err.message || err)
95+
process.exit(1)
96+
})

src/http-client.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,11 +543,7 @@ export default opts => {
543543
keepDataStream: payload => privCall('/api/v3/userDataStream', payload, 'PUT', false, true),
544544
closeDataStream: payload =>
545545
privCall('/api/v3/userDataStream', payload, 'DELETE', false, true),
546-
marginGetDataStream: () => privCall('/sapi/v1/userDataStream', null, 'POST', true),
547-
marginKeepDataStream: payload =>
548-
privCall('/sapi/v1/userDataStream', payload, 'PUT', false, true),
549-
marginCloseDataStream: payload =>
550-
privCall('/sapi/v1/userDataStream', payload, 'DELETE', false, true),
546+
marginGetListenToken: payload => privCall('/sapi/v1/userListenToken', payload, 'POST'),
551547
futuresGetDataStream: () => privCall('/fapi/v1/listenKey', null, 'POST', true),
552548
futuresKeepDataStream: payload =>
553549
privCall('/fapi/v1/listenKey', payload, 'PUT', false, true),

0 commit comments

Comments
 (0)