@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.
- package/README.md +7 -7
- package/docs/api.md +66 -66
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +131 -12
- package/src/adapters/binance/market-catalog.ts +10 -10
- package/src/adapters/binance/stream-protocol.ts +202 -0
- package/src/client/runtime.ts +1 -1
- package/src/internal/decimal.ts +19 -0
- package/src/internal/managed-websocket.ts +8 -0
- package/src/internal/subscription-multiplexer.ts +747 -0
- package/src/managers/account-manager.ts +40 -32
- package/src/managers/market-manager.ts +100 -51
- package/src/managers/order-manager.ts +7 -8
- package/src/types/account.ts +27 -28
- package/src/types/market.ts +12 -12
- package/src/types/order.ts +6 -7
- package/src/adapters/binance/book-ticker.ts +0 -123
- package/src/adapters/binance/mark-price.ts +0 -126
|
@@ -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
|
+
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -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,
|
|
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;
|