Skip to content

Commit 605d0fc

Browse files
committed
add test
1 parent 0253e96 commit 605d0fc

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

tests/margin-ws-api-live.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import Binance from '../src/node-binance-api';
2+
import { assert } from 'chai';
3+
4+
/**
5+
* Live test for margin websocket API (ws-api branch).
6+
*
7+
* This test:
8+
* 1. Connects to the margin user data stream via WebSocket API
9+
* 2. Creates a LIMIT BUY order at a low price (won't fill)
10+
* 3. Asserts an executionReport event is received via the websocket
11+
* 4. Cancels the order and asserts a second executionReport (CANCELED)
12+
* 5. Cleans up all connections
13+
*
14+
* Requirements:
15+
* - APIKEY / APISECRET env vars with margin-enabled Binance account
16+
* - Sufficient USDT balance in cross-margin account
17+
*
18+
* Run:
19+
* APIKEY=xxx APISECRET=xxx npx ts-mocha tests/margin-ws-api-live.test.ts --timeout 120000
20+
*/
21+
22+
const APIKEY = '';
23+
const APISECRET = '';
24+
25+
if (!APIKEY || !APISECRET) {
26+
console.error('APIKEY and APISECRET env vars are required');
27+
process.exit(1);
28+
}
29+
30+
// Use a cheap pair to minimize balance requirements.
31+
// XRPUSDT: ~$2.50, qty 5 => ~$10 notional at 80% price
32+
const SYMBOL = process.env.SYMBOL || 'XRPUSDT';
33+
const TIMEOUT = 90000;
34+
35+
const binance = new Binance().options({
36+
APIKEY,
37+
APISECRET,
38+
test: false,
39+
verbose: true,
40+
});
41+
42+
const stopWsApiConnections = function () {
43+
const connections = (binance as any).wsApiConnections;
44+
for (const connectionId in connections) {
45+
console.log('Terminated WebSocket API connection:', connectionId);
46+
(binance as any).terminateWsApi(connectionId);
47+
}
48+
};
49+
50+
describe('Margin WebSocket API – Live Order Test', function () {
51+
52+
after(function () {
53+
stopWsApiConnections();
54+
});
55+
56+
it('should receive executionReport events when placing and canceling a margin limit order', function (done) {
57+
this.timeout(TIMEOUT);
58+
59+
const events: any[] = [];
60+
let orderId: number | string | null = null;
61+
let newReceived = false;
62+
let canceledReceived = false;
63+
let finished = false;
64+
65+
const finish = (err?: Error) => {
66+
if (finished) return;
67+
finished = true;
68+
stopWsApiConnections();
69+
if (err) return done(err);
70+
done();
71+
};
72+
73+
binance.websockets.userMarginData(
74+
// all_updates_callback
75+
(data: any) => {
76+
console.log(' [all_updates]', data.e, data.s || '');
77+
},
78+
// balance_callback
79+
(balance: any) => {
80+
console.log(' [balance]', balance.e, balance.a || '');
81+
},
82+
// execution_callback
83+
(execution: any) => {
84+
console.log(' [execution]', execution.e, execution.x, execution.X, execution.s);
85+
events.push(execution);
86+
87+
assert(execution.e === 'executionReport', 'event type should be executionReport');
88+
assert(execution.s === SYMBOL, `symbol should be ${SYMBOL}`);
89+
assert(execution.S !== undefined, 'should have side (S)');
90+
assert(execution.o !== undefined, 'should have order type (o)');
91+
assert(execution.X !== undefined, 'should have order status (X)');
92+
assert(execution.x !== undefined, 'should have execution type (x)');
93+
94+
// Step 3: We got the NEW event – now cancel the order
95+
if (execution.x === 'NEW' && execution.X === 'NEW' && !newReceived) {
96+
newReceived = true;
97+
console.log('Received NEW executionReport – canceling order...');
98+
99+
// Use orderId from the event if not yet set
100+
const cancelId = orderId || execution.i;
101+
102+
binance.mgCancel(SYMBOL, cancelId).then((result: any) => {
103+
console.log('Cancel result:', result.status || result);
104+
}).catch((err: any) => {
105+
console.error('Cancel error:', err.message);
106+
finish(err);
107+
});
108+
}
109+
110+
// Step 4: We got the CANCELED event – assert and finish
111+
if (execution.x === 'CANCELED' && execution.X === 'CANCELED' && !canceledReceived) {
112+
canceledReceived = true;
113+
console.log('Received CANCELED executionReport');
114+
115+
assert(events.length >= 2, 'should have received at least 2 execution events');
116+
117+
const newEvent = events.find(e => e.x === 'NEW');
118+
const cancelEvent = events.find(e => e.x === 'CANCELED');
119+
120+
assert(newEvent, 'should have a NEW execution event');
121+
assert(cancelEvent, 'should have a CANCELED execution event');
122+
assert(newEvent.o === 'LIMIT', 'NEW event order type should be LIMIT');
123+
assert(cancelEvent.o === 'LIMIT', 'CANCELED event order type should be LIMIT');
124+
125+
console.log('All assertions passed!');
126+
finish();
127+
}
128+
},
129+
// subscribed_callback
130+
async (endpoint: string) => {
131+
console.log('Subscribed to margin data stream:', endpoint);
132+
assert(endpoint !== null, 'endpoint should not be null');
133+
134+
const subscriptionId = (binance as any).Options.marginDataSubscriptionId;
135+
assert(subscriptionId !== undefined, 'should have a subscription ID');
136+
console.log('Subscription ID:', subscriptionId);
137+
138+
// Wait for WebSocket to stabilize
139+
await new Promise(resolve => setTimeout(resolve, 2000));
140+
141+
try {
142+
// Step 2: Fetch current price and place a limit buy well below market
143+
const prices: any = await binance.prices(SYMBOL);
144+
const currentPrice = parseFloat(prices[SYMBOL]);
145+
// Set price 5% below market – passes PERCENT_PRICE_BY_SIDE but won't fill
146+
const limitPrice = (currentPrice * 0.95).toFixed(4);
147+
148+
// Use whole-number quantity that meets minimum notional
149+
const minNotional = 10;
150+
const quantity = Math.ceil(minNotional / parseFloat(limitPrice));
151+
const notional = quantity * parseFloat(limitPrice);
152+
153+
console.log(`Current ${SYMBOL} price: ${currentPrice}`);
154+
console.log(`Placing margin LIMIT BUY: ${quantity} @ ${limitPrice} (notional: ${notional.toFixed(2)} USDT)`);
155+
156+
const orderResult = await binance.mgBuy(SYMBOL, quantity, parseFloat(limitPrice), {
157+
sideEffectType: 'MARGIN_BUY'
158+
});
159+
console.log('Order placed:', orderResult.orderId, orderResult.status);
160+
161+
orderId = orderResult.orderId;
162+
163+
assert(orderResult.symbol === SYMBOL, 'order symbol should match');
164+
assert(orderResult.side === 'BUY', 'order side should be BUY');
165+
assert(orderResult.type === 'LIMIT', 'order type should be LIMIT');
166+
167+
console.log('Waiting for executionReport events via WebSocket...');
168+
} catch (err: any) {
169+
console.error('Error placing order:', err.body || err.message);
170+
finish(err);
171+
}
172+
},
173+
// list_status_callback
174+
(listStatus: any) => {
175+
console.log(' [listStatus]', listStatus);
176+
}
177+
);
178+
179+
// Safety timeout – fail if we don't get all events in time
180+
setTimeout(() => {
181+
if (!finished) {
182+
console.error('Timeout reached. Events received:', events.length);
183+
events.forEach((e, i) => console.error(` event[${i}]:`, e.x, e.X));
184+
185+
// Best-effort cancel if order was placed but never canceled
186+
if (orderId && !canceledReceived) {
187+
binance.mgCancel(SYMBOL, orderId).catch(() => {});
188+
}
189+
finish(new Error(`Timeout: newReceived=${newReceived}, canceledReceived=${canceledReceived}`));
190+
}
191+
}, TIMEOUT - 10000);
192+
});
193+
});

0 commit comments

Comments
 (0)