@imbingox/acex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +92 -285
  2. package/index.ts +1 -0
  3. package/package.json +40 -23
  4. package/src/adapters/binance/adapter.ts +80 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/mark-price.ts +126 -0
  7. package/src/adapters/binance/market-catalog.ts +258 -0
  8. package/src/adapters/binance/private-adapter.ts +833 -0
  9. package/src/adapters/types.ts +219 -0
  10. package/src/client/context.ts +123 -0
  11. package/src/client/create-client.ts +6 -0
  12. package/src/client/private-subscription-coordinator.ts +512 -0
  13. package/src/client/runtime.ts +410 -0
  14. package/src/errors.ts +27 -0
  15. package/src/index.ts +5 -0
  16. package/src/internal/async-event-bus.ts +100 -0
  17. package/src/internal/filters.ts +117 -0
  18. package/src/internal/managed-websocket.ts +280 -0
  19. package/src/managers/account-manager.ts +609 -0
  20. package/src/managers/market-manager.ts +889 -0
  21. package/src/managers/order-manager.ts +685 -0
  22. package/src/types/account.ts +157 -0
  23. package/src/types/client.ts +79 -0
  24. package/src/types/index.ts +5 -0
  25. package/src/types/market.ts +150 -0
  26. package/src/types/order.ts +177 -0
  27. package/src/types/shared.ts +93 -0
  28. package/dist/adapters/binance/composite-adapter.d.ts +0 -116
  29. package/dist/adapters/binance/composite-adapter.js +0 -121
  30. package/dist/adapters/binance/market-types.d.ts +0 -63
  31. package/dist/adapters/binance/market-types.js +0 -1
  32. package/dist/adapters/binance/native-market-adapter.d.ts +0 -102
  33. package/dist/adapters/binance/native-market-adapter.js +0 -455
  34. package/dist/adapters/binance/normalizers.d.ts +0 -8
  35. package/dist/adapters/binance/normalizers.js +0 -123
  36. package/dist/adapters/binance/rest-client.d.ts +0 -17
  37. package/dist/adapters/binance/rest-client.js +0 -66
  38. package/dist/adapters/binance/symbol-router.d.ts +0 -9
  39. package/dist/adapters/binance/symbol-router.js +0 -174
  40. package/dist/adapters/binance/ws-client.d.ts +0 -24
  41. package/dist/adapters/binance/ws-client.js +0 -261
  42. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  43. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  44. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -180
  45. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -539
  46. package/dist/adapters/ccxt/binance-usdm-exchange.d.ts +0 -22
  47. package/dist/adapters/ccxt/binance-usdm-exchange.js +0 -23
  48. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  49. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  50. package/dist/adapters/types.d.ts +0 -210
  51. package/dist/adapters/types.js +0 -1
  52. package/dist/core/client.d.ts +0 -50
  53. package/dist/core/client.js +0 -403
  54. package/dist/core/recovery.d.ts +0 -22
  55. package/dist/core/recovery.js +0 -18
  56. package/dist/core/runtime.d.ts +0 -26
  57. package/dist/core/runtime.js +0 -150
  58. package/dist/errors/acex-error.d.ts +0 -25
  59. package/dist/errors/acex-error.js +0 -54
  60. package/dist/index.d.ts +0 -6
  61. package/dist/index.js +0 -3
  62. package/dist/managers/account-manager.d.ts +0 -41
  63. package/dist/managers/account-manager.js +0 -80
  64. package/dist/managers/market-manager.d.ts +0 -16
  65. package/dist/managers/market-manager.js +0 -28
  66. package/dist/managers/order-manager.d.ts +0 -87
  67. package/dist/managers/order-manager.js +0 -122
  68. package/dist/runtime/async-queue.d.ts +0 -8
  69. package/dist/runtime/async-queue.js +0 -88
  70. package/dist/runtime/request-id.d.ts +0 -1
  71. package/dist/runtime/request-id.js +0 -5
  72. package/dist/runtime/ws-connection-supervisor.d.ts +0 -76
  73. package/dist/runtime/ws-connection-supervisor.js +0 -522
  74. package/dist/store/account-store.d.ts +0 -52
  75. package/dist/store/account-store.js +0 -18
  76. package/dist/store/health-store.d.ts +0 -16
  77. package/dist/store/health-store.js +0 -29
  78. package/dist/store/market-store.d.ts +0 -42
  79. package/dist/store/market-store.js +0 -51
  80. package/dist/store/order-store.d.ts +0 -38
  81. package/dist/store/order-store.js +0 -49
  82. package/dist/testing/create-fake-runtime.d.ts +0 -5
  83. package/dist/testing/create-fake-runtime.js +0 -7
  84. package/dist/types/public.d.ts +0 -5
  85. package/dist/types/public.js +0 -1
@@ -0,0 +1,833 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
3
+ import type { AccountCredentials, PositionSide } from "../../types/index.ts";
4
+ import type {
5
+ CancelAllOrdersRequest,
6
+ CancelOrderRequest,
7
+ CreateOrderRequest,
8
+ PrivateStreamCallbacks,
9
+ PrivateStreamOptions,
10
+ PrivateUserDataAdapter,
11
+ RawAccountBootstrap,
12
+ RawAccountUpdate,
13
+ RawBalanceUpdate,
14
+ RawOrderUpdate,
15
+ RawPositionUpdate,
16
+ RawRiskUpdate,
17
+ StreamHandle,
18
+ } from "../types.ts";
19
+
20
+ type TimerHandle = ReturnType<typeof setInterval>;
21
+ type SignedRequestMethod = "GET" | "POST" | "DELETE";
22
+
23
+ interface BinancePapiBalance {
24
+ asset?: string;
25
+ totalWalletBalance?: string;
26
+ crossMarginFree?: string;
27
+ crossMarginLocked?: string;
28
+ availableBalance?: string;
29
+ maxWithdrawAmount?: string;
30
+ balance?: string;
31
+ }
32
+
33
+ interface BinancePapiAccount {
34
+ accountEquity?: string;
35
+ totalEquity?: string;
36
+ accountInitialMargin?: string;
37
+ totalInitialMargin?: string;
38
+ accountMaintMargin?: string;
39
+ totalMaintMargin?: string;
40
+ uniMMR?: string;
41
+ updateTime?: number;
42
+ }
43
+
44
+ interface BinancePapiUmPosition {
45
+ symbol?: string;
46
+ positionAmt?: string;
47
+ entryPrice?: string;
48
+ markPrice?: string;
49
+ unRealizedProfit?: string;
50
+ unrealizedProfit?: string;
51
+ liquidationPrice?: string;
52
+ leverage?: string;
53
+ positionSide?: string;
54
+ updateTime?: number;
55
+ }
56
+
57
+ interface BinancePapiOpenOrder {
58
+ symbol?: string;
59
+ orderId?: number | string;
60
+ clientOrderId?: string;
61
+ side?: string;
62
+ type?: string;
63
+ status?: string;
64
+ price?: string;
65
+ stopPrice?: string;
66
+ origQty?: string;
67
+ executedQty?: string;
68
+ avgPrice?: string;
69
+ reduceOnly?: boolean;
70
+ positionSide?: string;
71
+ updateTime?: number;
72
+ time?: number;
73
+ }
74
+
75
+ interface BinanceListenKeyResponse {
76
+ listenKey?: string;
77
+ }
78
+
79
+ interface BinanceAccountUpdateBalance {
80
+ a?: string;
81
+ wb?: string;
82
+ cw?: string;
83
+ bc?: string;
84
+ }
85
+
86
+ interface BinanceAccountUpdatePosition {
87
+ s?: string;
88
+ pa?: string;
89
+ ep?: string;
90
+ cr?: string;
91
+ up?: string;
92
+ mt?: string;
93
+ iw?: string;
94
+ ps?: string;
95
+ ma?: string;
96
+ }
97
+
98
+ interface BinanceAccountUpdateMessage {
99
+ e?: string;
100
+ E?: number;
101
+ T?: number;
102
+ a?: {
103
+ B?: BinanceAccountUpdateBalance[];
104
+ P?: BinanceAccountUpdatePosition[];
105
+ };
106
+ }
107
+
108
+ interface BinanceOrderTradeUpdatePayload {
109
+ s?: string;
110
+ i?: number | string;
111
+ c?: string;
112
+ S?: string;
113
+ o?: string;
114
+ X?: string;
115
+ p?: string;
116
+ sp?: string;
117
+ q?: string;
118
+ z?: string;
119
+ ap?: string;
120
+ R?: boolean;
121
+ ps?: string;
122
+ T?: number;
123
+ }
124
+
125
+ interface BinanceOrderTradeUpdateMessage {
126
+ e?: string;
127
+ E?: number;
128
+ T?: number;
129
+ o?: BinanceOrderTradeUpdatePayload;
130
+ }
131
+
132
+ type BinancePrivateMessage =
133
+ | BinanceAccountUpdateMessage
134
+ | BinanceOrderTradeUpdateMessage;
135
+
136
+ const BINANCE_PAPI_REST_BASE_URL = "https://papi.binance.com";
137
+ const BINANCE_PAPI_WS_BASE_URL = "wss://fstream.binance.com/pm/ws";
138
+ const DEFAULT_RECV_WINDOW = 5_000;
139
+ const USDM_QUOTE_ASSETS = ["FDUSD", "USDC", "BUSD", "USDT"];
140
+
141
+ function requirePrivateCredentials(credentials: AccountCredentials): {
142
+ apiKey: string;
143
+ secret: string;
144
+ } {
145
+ if (!credentials.apiKey || !credentials.secret) {
146
+ throw new Error("Binance PAPI credentials require apiKey and secret");
147
+ }
148
+
149
+ return {
150
+ apiKey: credentials.apiKey,
151
+ secret: credentials.secret,
152
+ };
153
+ }
154
+
155
+ function firstString(...values: Array<string | undefined>): string | undefined {
156
+ return values.find((value) => value !== undefined && value !== "");
157
+ }
158
+
159
+ function getNumberOption(
160
+ options: Record<string, unknown> | undefined,
161
+ key: string,
162
+ ): number | undefined {
163
+ const value = options?.[key];
164
+ return typeof value === "number" && Number.isFinite(value)
165
+ ? value
166
+ : undefined;
167
+ }
168
+
169
+ function signQuery(query: string, secret: string): string {
170
+ return createHmac("sha256", secret).update(query).digest("hex");
171
+ }
172
+
173
+ function normalizeUmSymbol(symbol: string): string {
174
+ for (const quote of USDM_QUOTE_ASSETS) {
175
+ if (symbol.endsWith(quote) && symbol.length > quote.length) {
176
+ return `${symbol.slice(0, -quote.length)}/${quote}:${quote}`;
177
+ }
178
+ }
179
+
180
+ return symbol;
181
+ }
182
+
183
+ function encodeUmSymbol(symbol: string): string {
184
+ const matched = /^([^/]+)\/([^:]+):([^:]+)$/.exec(symbol);
185
+ if (matched && matched[2] === matched[3]) {
186
+ return `${matched[1]}${matched[2]}`;
187
+ }
188
+
189
+ return symbol;
190
+ }
191
+
192
+ function normalizePositionSide(value?: string): PositionSide {
193
+ switch (value) {
194
+ case "LONG":
195
+ return "long";
196
+ case "SHORT":
197
+ return "short";
198
+ default:
199
+ return "net";
200
+ }
201
+ }
202
+
203
+ function normalizeOrderSide(value?: string): "buy" | "sell" {
204
+ return value === "SELL" ? "sell" : "buy";
205
+ }
206
+
207
+ function encodeOrderSide(value: CreateOrderRequest["side"]): "BUY" | "SELL" {
208
+ return value === "sell" ? "SELL" : "BUY";
209
+ }
210
+
211
+ function encodeOrderType(
212
+ value: CreateOrderRequest["type"],
213
+ ): "LIMIT" | "MARKET" {
214
+ return value === "market" ? "MARKET" : "LIMIT";
215
+ }
216
+
217
+ function encodePositionSide(
218
+ value?: PositionSide,
219
+ ): "BOTH" | "LONG" | "SHORT" | undefined {
220
+ switch (value) {
221
+ case "long":
222
+ return "LONG";
223
+ case "short":
224
+ return "SHORT";
225
+ case "net":
226
+ return "BOTH";
227
+ default:
228
+ return undefined;
229
+ }
230
+ }
231
+
232
+ function normalizeOrderStatus(
233
+ value?: string,
234
+ ): RawOrderUpdate["status"] | undefined {
235
+ switch (value) {
236
+ case "PARTIALLY_FILLED":
237
+ return "partially_filled";
238
+ case "FILLED":
239
+ return "filled";
240
+ case "CANCELED":
241
+ case "CANCELLED":
242
+ return "canceled";
243
+ case "REJECTED":
244
+ return "rejected";
245
+ case "EXPIRED":
246
+ case "EXPIRED_IN_MATCH":
247
+ return "expired";
248
+ default:
249
+ return value ? "open" : undefined;
250
+ }
251
+ }
252
+
253
+ function mapBalance(
254
+ input: BinancePapiBalance,
255
+ receivedAt: number,
256
+ ): RawBalanceUpdate | undefined {
257
+ if (!input.asset) {
258
+ return undefined;
259
+ }
260
+
261
+ const total = firstString(input.totalWalletBalance, input.balance) ?? "0";
262
+ const free =
263
+ firstString(
264
+ input.crossMarginFree,
265
+ input.availableBalance,
266
+ input.maxWithdrawAmount,
267
+ total,
268
+ ) ?? "0";
269
+
270
+ return {
271
+ asset: input.asset,
272
+ free,
273
+ used: input.crossMarginLocked,
274
+ total,
275
+ receivedAt,
276
+ };
277
+ }
278
+
279
+ function mapAccountRisk(
280
+ input: BinancePapiAccount,
281
+ receivedAt: number,
282
+ ): RawRiskUpdate | undefined {
283
+ const risk: RawRiskUpdate = {
284
+ equity: firstString(input.accountEquity, input.totalEquity),
285
+ marginRatio: input.uniMMR,
286
+ initialMargin: firstString(
287
+ input.accountInitialMargin,
288
+ input.totalInitialMargin,
289
+ ),
290
+ maintenanceMargin: firstString(
291
+ input.accountMaintMargin,
292
+ input.totalMaintMargin,
293
+ ),
294
+ exchangeTs: input.updateTime,
295
+ receivedAt,
296
+ };
297
+
298
+ if (
299
+ !risk.equity &&
300
+ !risk.marginRatio &&
301
+ !risk.initialMargin &&
302
+ !risk.maintenanceMargin
303
+ ) {
304
+ return undefined;
305
+ }
306
+
307
+ return risk;
308
+ }
309
+
310
+ function mapUmPosition(
311
+ input: BinancePapiUmPosition,
312
+ receivedAt: number,
313
+ ): RawPositionUpdate | undefined {
314
+ if (!input.symbol) {
315
+ return undefined;
316
+ }
317
+
318
+ return {
319
+ symbol: normalizeUmSymbol(input.symbol),
320
+ side: normalizePositionSide(input.positionSide),
321
+ size: input.positionAmt ?? "0",
322
+ entryPrice: input.entryPrice,
323
+ markPrice: input.markPrice,
324
+ unrealizedPnl: firstString(input.unRealizedProfit, input.unrealizedProfit),
325
+ leverage: input.leverage,
326
+ liquidationPrice: input.liquidationPrice,
327
+ exchangeTs: input.updateTime,
328
+ receivedAt,
329
+ };
330
+ }
331
+
332
+ function mapOpenOrder(
333
+ input: BinancePapiOpenOrder,
334
+ receivedAt: number,
335
+ ): RawOrderUpdate | undefined {
336
+ const status = normalizeOrderStatus(input.status);
337
+ if (!input.symbol || !status) {
338
+ return undefined;
339
+ }
340
+
341
+ return {
342
+ orderId: input.orderId === undefined ? undefined : `${input.orderId}`,
343
+ clientOrderId: input.clientOrderId,
344
+ symbol: normalizeUmSymbol(input.symbol),
345
+ side: normalizeOrderSide(input.side),
346
+ type: input.type ?? "unknown",
347
+ status,
348
+ price: input.price,
349
+ triggerPrice: input.stopPrice,
350
+ amount: input.origQty ?? "0",
351
+ filled: input.executedQty ?? "0",
352
+ avgFillPrice: input.avgPrice,
353
+ reduceOnly: input.reduceOnly,
354
+ positionSide: normalizePositionSide(input.positionSide),
355
+ exchangeTs: input.updateTime ?? input.time,
356
+ receivedAt,
357
+ };
358
+ }
359
+
360
+ function mapAccountUpdateBalance(
361
+ input: BinanceAccountUpdateBalance,
362
+ exchangeTs: number | undefined,
363
+ receivedAt: number,
364
+ ): RawBalanceUpdate | undefined {
365
+ if (!input.a) {
366
+ return undefined;
367
+ }
368
+
369
+ const total = input.wb ?? "0";
370
+ return {
371
+ asset: input.a,
372
+ free: input.cw ?? total,
373
+ total,
374
+ exchangeTs,
375
+ receivedAt,
376
+ };
377
+ }
378
+
379
+ function mapAccountUpdatePosition(
380
+ input: BinanceAccountUpdatePosition,
381
+ exchangeTs: number | undefined,
382
+ receivedAt: number,
383
+ ): RawPositionUpdate | undefined {
384
+ if (!input.s) {
385
+ return undefined;
386
+ }
387
+
388
+ return {
389
+ symbol: normalizeUmSymbol(input.s),
390
+ side: normalizePositionSide(input.ps),
391
+ size: input.pa ?? "0",
392
+ entryPrice: input.ep,
393
+ unrealizedPnl: input.up,
394
+ exchangeTs,
395
+ receivedAt,
396
+ };
397
+ }
398
+
399
+ function parsePrivateMessage(data: string): BinancePrivateMessage | undefined {
400
+ const parsed = JSON.parse(data) as BinancePrivateMessage;
401
+ return parsed.e === "ACCOUNT_UPDATE" || parsed.e === "ORDER_TRADE_UPDATE"
402
+ ? parsed
403
+ : undefined;
404
+ }
405
+
406
+ function isAccountUpdateMessage(
407
+ message: BinancePrivateMessage,
408
+ ): message is BinanceAccountUpdateMessage {
409
+ return message.e === "ACCOUNT_UPDATE";
410
+ }
411
+
412
+ function mapAccountUpdate(
413
+ message: BinanceAccountUpdateMessage,
414
+ receivedAt: number,
415
+ ): RawAccountUpdate {
416
+ const exchangeTs = message.T ?? message.E;
417
+ return {
418
+ balances: message.a?.B?.flatMap((balance) => {
419
+ const mapped = mapAccountUpdateBalance(balance, exchangeTs, receivedAt);
420
+ return mapped ? [mapped] : [];
421
+ }),
422
+ positions: message.a?.P?.flatMap((position) => {
423
+ const mapped = mapAccountUpdatePosition(position, exchangeTs, receivedAt);
424
+ return mapped ? [mapped] : [];
425
+ }),
426
+ exchangeTs,
427
+ receivedAt,
428
+ };
429
+ }
430
+
431
+ function mapOrderUpdate(
432
+ message: BinanceOrderTradeUpdateMessage,
433
+ receivedAt: number,
434
+ ): RawOrderUpdate | undefined {
435
+ const payload = message.o;
436
+ const status = normalizeOrderStatus(payload?.X);
437
+ if (!payload?.s || !status) {
438
+ return undefined;
439
+ }
440
+
441
+ return {
442
+ orderId: payload.i === undefined ? undefined : `${payload.i}`,
443
+ clientOrderId: payload.c,
444
+ symbol: normalizeUmSymbol(payload.s),
445
+ side: normalizeOrderSide(payload.S),
446
+ type: payload.o ?? "unknown",
447
+ status,
448
+ price: payload.p,
449
+ triggerPrice: payload.sp,
450
+ amount: payload.q ?? "0",
451
+ filled: payload.z ?? "0",
452
+ avgFillPrice: payload.ap,
453
+ reduceOnly: payload.R,
454
+ positionSide: normalizePositionSide(payload.ps),
455
+ exchangeTs: payload.T ?? message.T ?? message.E,
456
+ receivedAt,
457
+ };
458
+ }
459
+
460
+ async function readJson<T>(response: Response, url: string): Promise<T> {
461
+ const text = await response.text();
462
+ if (!response.ok) {
463
+ throw new Error(
464
+ `Binance PAPI request failed: ${response.status} ${response.statusText} ${url} ${text}`,
465
+ );
466
+ }
467
+
468
+ if (!text) {
469
+ return {} as T;
470
+ }
471
+
472
+ return JSON.parse(text) as T;
473
+ }
474
+
475
+ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
476
+ readonly exchange = "binance" as const;
477
+
478
+ async bootstrapAccount(
479
+ credentials: AccountCredentials,
480
+ accountOptions?: Record<string, unknown>,
481
+ ): Promise<RawAccountBootstrap> {
482
+ const receivedAt = Date.now();
483
+ const [balances, account, positions] = await Promise.all([
484
+ this.signedRequest<BinancePapiBalance[]>(
485
+ "GET",
486
+ "/papi/v1/balance",
487
+ credentials,
488
+ accountOptions,
489
+ ),
490
+ this.signedRequest<BinancePapiAccount>(
491
+ "GET",
492
+ "/papi/v1/account",
493
+ credentials,
494
+ accountOptions,
495
+ ),
496
+ this.signedRequest<BinancePapiUmPosition[]>(
497
+ "GET",
498
+ "/papi/v1/um/positionRisk",
499
+ credentials,
500
+ accountOptions,
501
+ ),
502
+ ]);
503
+
504
+ return {
505
+ balances: balances.flatMap((balance) => {
506
+ const mapped = mapBalance(balance, receivedAt);
507
+ return mapped ? [mapped] : [];
508
+ }),
509
+ positions: positions.flatMap((position) => {
510
+ const mapped = mapUmPosition(position, receivedAt);
511
+ return mapped ? [mapped] : [];
512
+ }),
513
+ risk: mapAccountRisk(account, receivedAt),
514
+ exchangeTs: account.updateTime,
515
+ receivedAt,
516
+ };
517
+ }
518
+
519
+ async bootstrapOpenOrders(
520
+ credentials: AccountCredentials,
521
+ accountOptions?: Record<string, unknown>,
522
+ ): Promise<RawOrderUpdate[]> {
523
+ const receivedAt = Date.now();
524
+ const orders = await this.signedRequest<BinancePapiOpenOrder[]>(
525
+ "GET",
526
+ "/papi/v1/um/openOrders",
527
+ credentials,
528
+ accountOptions,
529
+ );
530
+
531
+ return orders.flatMap((order) => {
532
+ const mapped = mapOpenOrder(order, receivedAt);
533
+ return mapped ? [mapped] : [];
534
+ });
535
+ }
536
+
537
+ async createOrder(
538
+ credentials: AccountCredentials,
539
+ request: CreateOrderRequest,
540
+ accountOptions?: Record<string, unknown>,
541
+ ): Promise<RawOrderUpdate> {
542
+ const receivedAt = Date.now();
543
+ const response = await this.signedRequest<BinancePapiOpenOrder>(
544
+ "POST",
545
+ "/papi/v1/um/order",
546
+ credentials,
547
+ accountOptions,
548
+ {
549
+ symbol: encodeUmSymbol(request.symbol),
550
+ side: encodeOrderSide(request.side),
551
+ type: encodeOrderType(request.type),
552
+ quantity: request.amount,
553
+ price: request.price,
554
+ timeInForce: request.type === "limit" ? "GTC" : undefined,
555
+ newClientOrderId: request.clientOrderId,
556
+ reduceOnly:
557
+ request.reduceOnly === undefined
558
+ ? undefined
559
+ : `${request.reduceOnly}`,
560
+ positionSide: encodePositionSide(request.positionSide),
561
+ },
562
+ );
563
+
564
+ const mapped = mapOpenOrder(response, receivedAt);
565
+ if (!mapped) {
566
+ throw new Error(
567
+ "Binance PAPI createOrder response did not contain an order",
568
+ );
569
+ }
570
+
571
+ return mapped;
572
+ }
573
+
574
+ async cancelOrder(
575
+ credentials: AccountCredentials,
576
+ request: CancelOrderRequest,
577
+ accountOptions?: Record<string, unknown>,
578
+ ): Promise<RawOrderUpdate> {
579
+ const receivedAt = Date.now();
580
+ const response = await this.signedRequest<BinancePapiOpenOrder>(
581
+ "DELETE",
582
+ "/papi/v1/um/order",
583
+ credentials,
584
+ accountOptions,
585
+ {
586
+ symbol: encodeUmSymbol(request.symbol),
587
+ orderId: request.orderId,
588
+ origClientOrderId: request.clientOrderId,
589
+ },
590
+ );
591
+
592
+ const mapped = mapOpenOrder(response, receivedAt);
593
+ if (!mapped) {
594
+ throw new Error(
595
+ "Binance PAPI cancelOrder response did not contain an order",
596
+ );
597
+ }
598
+
599
+ return mapped;
600
+ }
601
+
602
+ async cancelAllOrders(
603
+ credentials: AccountCredentials,
604
+ request: CancelAllOrdersRequest,
605
+ accountOptions?: Record<string, unknown>,
606
+ ): Promise<RawOrderUpdate[]> {
607
+ const receivedAt = Date.now();
608
+ const responses = await this.signedRequest<BinancePapiOpenOrder[]>(
609
+ "DELETE",
610
+ "/papi/v1/um/allOpenOrders",
611
+ credentials,
612
+ accountOptions,
613
+ {
614
+ symbol: encodeUmSymbol(request.symbol),
615
+ },
616
+ );
617
+
618
+ return responses.flatMap((response) => {
619
+ const mapped = mapOpenOrder(response, receivedAt);
620
+ return mapped ? [mapped] : [];
621
+ });
622
+ }
623
+
624
+ createPrivateStream(
625
+ credentials: AccountCredentials,
626
+ callbacks: PrivateStreamCallbacks,
627
+ options: PrivateStreamOptions,
628
+ _accountOptions?: Record<string, unknown>,
629
+ ): StreamHandle {
630
+ let closed = false;
631
+ let listenKey: string | undefined;
632
+ let keepAliveTimer: TimerHandle | undefined;
633
+ let websocket: StreamHandle | undefined;
634
+ let openedOnce = false;
635
+
636
+ const clearKeepAlive = () => {
637
+ if (keepAliveTimer) {
638
+ clearInterval(keepAliveTimer);
639
+ keepAliveTimer = undefined;
640
+ }
641
+ };
642
+
643
+ const closeListenKey = () => {
644
+ if (!listenKey) {
645
+ return;
646
+ }
647
+
648
+ const key = listenKey;
649
+ listenKey = undefined;
650
+ void this.closeUserDataStream(credentials, key).catch((error) => {
651
+ callbacks.onError(
652
+ error instanceof Error
653
+ ? error
654
+ : new Error("Failed to close Binance PAPI listenKey"),
655
+ );
656
+ });
657
+ };
658
+
659
+ const ready = (async () => {
660
+ listenKey = await this.startUserDataStream(credentials);
661
+ if (closed) {
662
+ closeListenKey();
663
+ return;
664
+ }
665
+
666
+ keepAliveTimer = setInterval(() => {
667
+ if (!listenKey) {
668
+ return;
669
+ }
670
+
671
+ void this.keepAliveUserDataStream(credentials, listenKey).catch(
672
+ (error) => {
673
+ callbacks.onError(
674
+ error instanceof Error
675
+ ? error
676
+ : new Error("Failed to keep Binance PAPI listenKey alive"),
677
+ );
678
+ },
679
+ );
680
+ }, options.listenKeyKeepAliveMs);
681
+
682
+ websocket = createManagedWebSocket<BinancePrivateMessage>({
683
+ url: `${BINANCE_PAPI_WS_BASE_URL}/${listenKey}`,
684
+ initialMessageTimeoutMs: options.openTimeoutMs,
685
+ readyWhen: "open",
686
+ now: options.now,
687
+ parseMessage: parsePrivateMessage,
688
+ onOpen() {
689
+ if (openedOnce) {
690
+ callbacks.onReconnected();
691
+ }
692
+ openedOnce = true;
693
+ },
694
+ onMessage(message, receivedAt) {
695
+ if (isAccountUpdateMessage(message)) {
696
+ callbacks.onAccountUpdate(mapAccountUpdate(message, receivedAt));
697
+ return;
698
+ }
699
+
700
+ const orderUpdate = mapOrderUpdate(message, receivedAt);
701
+ if (orderUpdate) {
702
+ callbacks.onOrderUpdate(orderUpdate);
703
+ }
704
+ },
705
+ onUnexpectedClose() {
706
+ callbacks.onDisconnected();
707
+ },
708
+ onError() {
709
+ callbacks.onError(
710
+ new Error("WebSocket error for Binance PAPI private stream"),
711
+ );
712
+ },
713
+ reconnect: {
714
+ initialDelayMs: options.reconnectDelayMs,
715
+ maxDelayMs: options.reconnectMaxDelayMs,
716
+ reconnectWithoutMessages: true,
717
+ },
718
+ });
719
+
720
+ await websocket.ready;
721
+ })();
722
+
723
+ return {
724
+ ready,
725
+ close() {
726
+ if (closed) {
727
+ return;
728
+ }
729
+
730
+ closed = true;
731
+ clearKeepAlive();
732
+ websocket?.close();
733
+ closeListenKey();
734
+ },
735
+ };
736
+ }
737
+
738
+ private async signedRequest<T>(
739
+ method: SignedRequestMethod,
740
+ path: string,
741
+ credentials: AccountCredentials,
742
+ accountOptions?: Record<string, unknown>,
743
+ queryParams?: Record<string, string | undefined>,
744
+ ): Promise<T> {
745
+ const { apiKey, secret } = requirePrivateCredentials(credentials);
746
+ const params = new URLSearchParams();
747
+ for (const [key, value] of Object.entries(queryParams ?? {})) {
748
+ if (value !== undefined) {
749
+ params.set(key, value);
750
+ }
751
+ }
752
+ params.set(
753
+ "timestamp",
754
+ `${getNumberOption(accountOptions, "timestamp") ?? Date.now()}`,
755
+ );
756
+ params.set(
757
+ "recvWindow",
758
+ `${getNumberOption(accountOptions, "recvWindow") ?? DEFAULT_RECV_WINDOW}`,
759
+ );
760
+ params.set("signature", signQuery(params.toString(), secret));
761
+
762
+ const url = `${BINANCE_PAPI_REST_BASE_URL}${path}?${params.toString()}`;
763
+ const response = await fetch(url, {
764
+ method,
765
+ headers: {
766
+ "X-MBX-APIKEY": apiKey,
767
+ },
768
+ });
769
+
770
+ return readJson<T>(response, url);
771
+ }
772
+
773
+ private async startUserDataStream(
774
+ credentials: AccountCredentials,
775
+ ): Promise<string> {
776
+ const response = await this.userStreamRequest<BinanceListenKeyResponse>(
777
+ "POST",
778
+ credentials,
779
+ );
780
+ if (!response.listenKey) {
781
+ throw new Error("Binance PAPI did not return a listenKey");
782
+ }
783
+
784
+ return response.listenKey;
785
+ }
786
+
787
+ private async keepAliveUserDataStream(
788
+ credentials: AccountCredentials,
789
+ listenKey: string,
790
+ ): Promise<void> {
791
+ await this.userStreamRequest<Record<string, never>>(
792
+ "PUT",
793
+ credentials,
794
+ listenKey,
795
+ );
796
+ }
797
+
798
+ private async closeUserDataStream(
799
+ credentials: AccountCredentials,
800
+ listenKey: string,
801
+ ): Promise<void> {
802
+ await this.userStreamRequest<Record<string, never>>(
803
+ "DELETE",
804
+ credentials,
805
+ listenKey,
806
+ );
807
+ }
808
+
809
+ private async userStreamRequest<T>(
810
+ method: "POST" | "PUT" | "DELETE",
811
+ credentials: AccountCredentials,
812
+ listenKey?: string,
813
+ ): Promise<T> {
814
+ const { apiKey } = requirePrivateCredentials(credentials);
815
+ const params = new URLSearchParams();
816
+ if (listenKey) {
817
+ params.set("listenKey", listenKey);
818
+ }
819
+
820
+ const query = params.toString();
821
+ const url = `${BINANCE_PAPI_REST_BASE_URL}/papi/v1/listenKey${
822
+ query ? `?${query}` : ""
823
+ }`;
824
+ const response = await fetch(url, {
825
+ method,
826
+ headers: {
827
+ "X-MBX-APIKEY": apiKey,
828
+ },
829
+ });
830
+
831
+ return readJson<T>(response, url);
832
+ }
833
+ }