@imbingox/acex 0.3.0 → 0.4.0-beta.1

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.
@@ -0,0 +1,202 @@
1
+ import type { VenueStreamProtocol } from "../../internal/subscription-multiplexer.ts";
2
+ import type { BinanceMarketDefinition } from "./market-catalog.ts";
3
+
4
+ export type BinanceStreamChannel = "l1book" | "fundingRate";
5
+
6
+ export interface BinanceStreamDescriptor {
7
+ readonly channel: BinanceStreamChannel;
8
+ readonly market: BinanceMarketDefinition;
9
+ }
10
+
11
+ export interface BinanceStreamMessage {
12
+ readonly result?: unknown;
13
+ readonly id?: number | string;
14
+ readonly e?: string;
15
+ readonly E?: number;
16
+ readonly s?: string;
17
+ readonly b?: string;
18
+ readonly B?: string;
19
+ readonly a?: string;
20
+ readonly A?: string;
21
+ readonly p?: string;
22
+ readonly i?: string;
23
+ readonly r?: string;
24
+ readonly T?: number;
25
+ }
26
+
27
+ export type BinanceStreamPayload =
28
+ | {
29
+ readonly channel: "l1book";
30
+ readonly bidPrice: string;
31
+ readonly bidSize: string;
32
+ readonly askPrice: string;
33
+ readonly askSize: string;
34
+ readonly exchangeTs?: number;
35
+ }
36
+ | {
37
+ readonly channel: "fundingRate";
38
+ readonly fundingRate: string;
39
+ readonly nextFundingTime?: number;
40
+ readonly markPrice?: string;
41
+ readonly indexPrice?: string;
42
+ readonly exchangeTs?: number;
43
+ };
44
+
45
+ const BINANCE_SPOT_WS_BASE_URL = "wss://stream.binance.com:9443/ws";
46
+ const BINANCE_USDM_WS_BASE_URL = "wss://fstream.binance.com/ws";
47
+ const BINANCE_USDM_MARKET_WS_BASE_URL = "wss://fstream.binance.com/market/ws";
48
+ const BINANCE_COINM_WS_BASE_URL = "wss://dstream.binance.com/ws";
49
+
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
52
+ }
53
+
54
+ function hasField(value: object, field: keyof BinanceStreamMessage): boolean {
55
+ return Object.hasOwn(value, field);
56
+ }
57
+
58
+ function streamName(descriptor: BinanceStreamDescriptor): string {
59
+ const channel = descriptor.channel === "l1book" ? "bookTicker" : "markPrice";
60
+ return `${descriptor.market.id.toLowerCase()}@${channel}`;
61
+ }
62
+
63
+ function connectionKeyFor(descriptor: BinanceStreamDescriptor): string {
64
+ switch (descriptor.market.family) {
65
+ case "spot":
66
+ if (descriptor.channel === "fundingRate") {
67
+ throw new Error(
68
+ `Funding rate is not supported for spot market: ${descriptor.market.symbol}`,
69
+ );
70
+ }
71
+ return BINANCE_SPOT_WS_BASE_URL;
72
+ case "usdm":
73
+ return descriptor.channel === "l1book"
74
+ ? BINANCE_USDM_WS_BASE_URL
75
+ : BINANCE_USDM_MARKET_WS_BASE_URL;
76
+ case "coinm":
77
+ return BINANCE_COINM_WS_BASE_URL;
78
+ }
79
+ }
80
+
81
+ export class BinanceStreamProtocol
82
+ implements
83
+ VenueStreamProtocol<
84
+ BinanceStreamMessage,
85
+ BinanceStreamDescriptor,
86
+ BinanceStreamPayload
87
+ >
88
+ {
89
+ private nextControlFrameId = 1;
90
+
91
+ subscriptionKey(descriptor: BinanceStreamDescriptor): string {
92
+ return `${descriptor.channel}:${descriptor.market.id}`;
93
+ }
94
+
95
+ connectionKey(descriptor: BinanceStreamDescriptor): string {
96
+ return connectionKeyFor(descriptor);
97
+ }
98
+
99
+ connectionUrl(connectionKey: string): string {
100
+ return connectionKey;
101
+ }
102
+
103
+ parseMessage(data: string): BinanceStreamMessage | undefined {
104
+ try {
105
+ const parsed = JSON.parse(data) as unknown;
106
+ if (!isRecord(parsed)) {
107
+ return undefined;
108
+ }
109
+
110
+ return parsed;
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
116
+ encodeSubscribe(descriptors: BinanceStreamDescriptor[]): string {
117
+ return this.encodeControlFrame("SUBSCRIBE", descriptors);
118
+ }
119
+
120
+ encodeUnsubscribe(descriptors: BinanceStreamDescriptor[]): string {
121
+ return this.encodeControlFrame("UNSUBSCRIBE", descriptors);
122
+ }
123
+
124
+ routeMessage(message: BinanceStreamMessage):
125
+ | {
126
+ kind: "data";
127
+ subscriptionKey: string;
128
+ payload: BinanceStreamPayload;
129
+ }
130
+ | { kind: "ack" }
131
+ | { kind: "ignore" } {
132
+ if (hasField(message, "result") || this.isIdOnlyAck(message)) {
133
+ return { kind: "ack" };
134
+ }
135
+
136
+ if (
137
+ typeof message.s === "string" &&
138
+ typeof message.r === "string" &&
139
+ message.e === "markPriceUpdate"
140
+ ) {
141
+ return {
142
+ kind: "data",
143
+ subscriptionKey: `fundingRate:${message.s}`,
144
+ payload: {
145
+ channel: "fundingRate",
146
+ fundingRate: message.r,
147
+ nextFundingTime: message.T,
148
+ markPrice: message.p,
149
+ indexPrice: message.i,
150
+ exchangeTs: message.E,
151
+ },
152
+ };
153
+ }
154
+
155
+ if (
156
+ typeof message.s === "string" &&
157
+ typeof message.b === "string" &&
158
+ typeof message.B === "string" &&
159
+ typeof message.a === "string" &&
160
+ typeof message.A === "string"
161
+ ) {
162
+ return {
163
+ kind: "data",
164
+ subscriptionKey: `l1book:${message.s}`,
165
+ payload: {
166
+ channel: "l1book",
167
+ bidPrice: message.b,
168
+ bidSize: message.B,
169
+ askPrice: message.a,
170
+ askSize: message.A,
171
+ exchangeTs: message.T,
172
+ },
173
+ };
174
+ }
175
+
176
+ return { kind: "ignore" };
177
+ }
178
+
179
+ private encodeControlFrame(
180
+ method: "SUBSCRIBE" | "UNSUBSCRIBE",
181
+ descriptors: BinanceStreamDescriptor[],
182
+ ): string {
183
+ const frame = {
184
+ method,
185
+ params: descriptors.map(streamName),
186
+ id: this.nextControlFrameId,
187
+ };
188
+ this.nextControlFrameId += 1;
189
+ return JSON.stringify(frame);
190
+ }
191
+
192
+ private isIdOnlyAck(message: BinanceStreamMessage): boolean {
193
+ return (
194
+ message.id !== undefined &&
195
+ message.e === undefined &&
196
+ message.s === undefined &&
197
+ message.b === undefined &&
198
+ message.a === undefined &&
199
+ message.r === undefined
200
+ );
201
+ }
202
+ }
@@ -115,7 +115,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
115
115
  privateAdapters.map((adapter) => [adapter.venue, adapter]),
116
116
  );
117
117
 
118
- this.marketManager = new MarketManagerImpl(this, marketAdapter, {
118
+ this.marketManager = new MarketManagerImpl(this, this.marketAdapters, {
119
119
  initialL1TimeoutMs: options.market?.l1InitialMessageTimeoutMs,
120
120
  l1StaleAfterMs: options.market?.l1StaleAfterMs,
121
121
  l1ReconnectDelayMs: options.market?.l1ReconnectDelayMs,
@@ -0,0 +1,19 @@
1
+ import BigNumber from "bignumber.js";
2
+ import type { DecimalInput } from "../types/index.ts";
3
+
4
+ /**
5
+ * Convert a decimal value to its canonical string form: full precision, no
6
+ * scientific notation, no trailing zeros.
7
+ *
8
+ * Throws on non-finite input (NaN / Infinity) so producers can never leak
9
+ * sentinel strings into public output fields. Call sites that legitimately
10
+ * accept non-finite input (e.g. order-input validation) must guard before
11
+ * calling this.
12
+ */
13
+ export function toCanonical(value: DecimalInput): string {
14
+ const bn = new BigNumber(value);
15
+ if (!bn.isFinite()) {
16
+ throw new RangeError(`invalid non-finite DecimalInput: ${bn.toString()}`);
17
+ }
18
+ return bn.toFixed();
19
+ }
@@ -33,6 +33,7 @@ export interface ManagedWebSocketOptions<TMessage> {
33
33
 
34
34
  export interface ManagedWebSocketSession {
35
35
  readonly ready: Promise<void>;
36
+ send(data: string): void;
36
37
  close(): void;
37
38
  }
38
39
 
@@ -265,6 +266,13 @@ export function createManagedWebSocket<TMessage>(
265
266
 
266
267
  return {
267
268
  ready,
269
+ send(data: string): void {
270
+ if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {
271
+ return;
272
+ }
273
+
274
+ activeSocket.send(data);
275
+ },
268
276
  close() {
269
277
  if (closed) {
270
278
  return;