@imbingox/acex 0.1.0-beta.0 → 0.1.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.
- package/README.md +334 -40
- package/dist/adapters/binance/composite-adapter.d.ts +116 -0
- package/dist/adapters/binance/composite-adapter.js +121 -0
- package/dist/adapters/binance/market-types.d.ts +63 -0
- package/dist/adapters/binance/market-types.js +1 -0
- package/dist/adapters/binance/native-market-adapter.d.ts +102 -0
- package/dist/adapters/binance/native-market-adapter.js +455 -0
- package/dist/adapters/binance/normalizers.d.ts +8 -0
- package/dist/adapters/binance/normalizers.js +123 -0
- package/dist/adapters/binance/rest-client.d.ts +17 -0
- package/dist/adapters/binance/rest-client.js +66 -0
- package/dist/adapters/binance/symbol-router.d.ts +9 -0
- package/dist/adapters/binance/symbol-router.js +174 -0
- package/dist/adapters/binance/ws-client.d.ts +24 -0
- package/dist/adapters/binance/ws-client.js +261 -0
- package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +1 -0
- package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +6 -4
- package/dist/adapters/ccxt/binance-usdm-exchange.d.ts +22 -0
- package/dist/adapters/ccxt/binance-usdm-exchange.js +23 -0
- package/dist/core/client.d.ts +18 -5
- package/dist/core/client.js +360 -2
- package/dist/index.d.ts +2 -1
- package/dist/runtime/ws-connection-supervisor.d.ts +76 -0
- package/dist/runtime/ws-connection-supervisor.js +522 -0
- package/dist/testing/create-fake-runtime.d.ts +1 -1
- package/dist/types/public.d.ts +0 -6
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AdapterAccountBaseline, ExchangeAdapter, ExchangeCapabilities, NormalizedAccountEvent, NormalizedMarketEvent, NormalizedOrderEvent } from "../types.js";
|
|
2
|
+
import type { BinanceMarketFamily } from "./market-types.js";
|
|
3
|
+
import { type BinanceMarketRestClient } from "./rest-client.js";
|
|
4
|
+
import { type BinanceMarketWsTransport } from "./ws-client.js";
|
|
5
|
+
export interface CreateBinanceNativeMarketAdapterInput {
|
|
6
|
+
restClient?: BinanceMarketRestClient;
|
|
7
|
+
createTransport?: (input: {
|
|
8
|
+
family: BinanceMarketFamily;
|
|
9
|
+
}) => BinanceMarketWsTransport;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
export declare class BinanceNativeMarketAdapter implements ExchangeAdapter {
|
|
13
|
+
#private;
|
|
14
|
+
readonly exchange = "binance";
|
|
15
|
+
readonly capabilities: ExchangeCapabilities;
|
|
16
|
+
constructor(input?: CreateBinanceNativeMarketAdapterInput);
|
|
17
|
+
start(): Promise<void>;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
fetchMarketInfo(): Promise<{
|
|
20
|
+
exchange: string;
|
|
21
|
+
symbol: string;
|
|
22
|
+
baseAsset: string;
|
|
23
|
+
quoteAsset: string;
|
|
24
|
+
marketType: "spot" | "swap" | "future";
|
|
25
|
+
pricePrecision: number;
|
|
26
|
+
amountPrecision: number;
|
|
27
|
+
active: boolean;
|
|
28
|
+
}[]>;
|
|
29
|
+
subscribeL1Book(input: {
|
|
30
|
+
exchange: string;
|
|
31
|
+
symbol: string;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
subscribeFundingRate(input: {
|
|
34
|
+
exchange: string;
|
|
35
|
+
symbol: string;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
watchMarketEvents(): import("../../runtime/async-queue.js").AsyncQueue<NormalizedMarketEvent>;
|
|
38
|
+
setMarketEventSink(sink: ((event: NormalizedMarketEvent) => void) | undefined): void;
|
|
39
|
+
watchInternalErrors(): import("../../runtime/async-queue.js").AsyncQueue<Error>;
|
|
40
|
+
subscribeOrders(_input: {
|
|
41
|
+
accountId: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
fetchOpenOrdersBaseline(_accountId: string): Promise<NormalizedOrderEvent["snapshot"][]>;
|
|
44
|
+
watchOrderEvents(): import("../../runtime/async-queue.js").AsyncQueue<NormalizedOrderEvent>;
|
|
45
|
+
setOrderEventSink(sink: ((event: NormalizedOrderEvent) => void) | undefined): void;
|
|
46
|
+
fetchAccountBaseline(_accountId: string): Promise<AdapterAccountBaseline>;
|
|
47
|
+
watchAccountEvents(): import("../../runtime/async-queue.js").AsyncQueue<NormalizedAccountEvent>;
|
|
48
|
+
setAccountEventSink(sink: ((event: NormalizedAccountEvent) => void) | undefined): void;
|
|
49
|
+
placeOrder(_input: {
|
|
50
|
+
accountId: string;
|
|
51
|
+
exchange: string;
|
|
52
|
+
symbol: string;
|
|
53
|
+
side: "buy" | "sell";
|
|
54
|
+
amount: string;
|
|
55
|
+
clientOrderId: string;
|
|
56
|
+
type: string;
|
|
57
|
+
price?: string;
|
|
58
|
+
reduceOnly?: boolean;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
clientOrderId?: string;
|
|
61
|
+
orderId?: string;
|
|
62
|
+
receivedAt: number;
|
|
63
|
+
}>;
|
|
64
|
+
amendOrder(_input: {
|
|
65
|
+
accountId: string;
|
|
66
|
+
exchange: string;
|
|
67
|
+
clientOrderId?: string;
|
|
68
|
+
orderId?: string;
|
|
69
|
+
symbol?: string;
|
|
70
|
+
newPrice?: string;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
clientOrderId?: string;
|
|
73
|
+
orderId?: string;
|
|
74
|
+
receivedAt: number;
|
|
75
|
+
}>;
|
|
76
|
+
cancelOrder(_input: {
|
|
77
|
+
accountId: string;
|
|
78
|
+
exchange: string;
|
|
79
|
+
clientOrderId?: string;
|
|
80
|
+
orderId?: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
clientOrderId?: string;
|
|
83
|
+
orderId?: string;
|
|
84
|
+
receivedAt: number;
|
|
85
|
+
}>;
|
|
86
|
+
cancelAllOrders(_input: {
|
|
87
|
+
accountId: string;
|
|
88
|
+
exchange: string;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
accountId: string;
|
|
91
|
+
exchange: string;
|
|
92
|
+
canceledCount: number;
|
|
93
|
+
}>;
|
|
94
|
+
getHealth(): {
|
|
95
|
+
exchange: string;
|
|
96
|
+
status: string;
|
|
97
|
+
wsConnected: boolean;
|
|
98
|
+
};
|
|
99
|
+
handleTransportMessage(family: BinanceMarketFamily, payload: unknown): void;
|
|
100
|
+
handleTransportClose(family: BinanceMarketFamily): void;
|
|
101
|
+
handleTransportReconnect(family: BinanceMarketFamily): Promise<void>;
|
|
102
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { createAcexError } from "../../errors/acex-error.js";
|
|
2
|
+
import { createAsyncQueue } from "../../runtime/async-queue.js";
|
|
3
|
+
import { normalizeBookTickerEvent, normalizeMarkPriceEvent } from "./normalizers.js";
|
|
4
|
+
import { createBinanceMarketRestClient } from "./rest-client.js";
|
|
5
|
+
import { buildBinanceMarketIndex, getRecordByNativeSymbol, requireFundingMarketRecord, requireMarketRecord, } from "./symbol-router.js";
|
|
6
|
+
import { createBinanceMarketWsTransport, } from "./ws-client.js";
|
|
7
|
+
function createUnsupportedError(message) {
|
|
8
|
+
return createAcexError({
|
|
9
|
+
code: "CAPABILITY_NOT_SUPPORTED",
|
|
10
|
+
message,
|
|
11
|
+
retryable: false,
|
|
12
|
+
exchange: "binance",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function createAdapterLifecycleError(message) {
|
|
16
|
+
return createAcexError({
|
|
17
|
+
code: "VALIDATION_ERROR",
|
|
18
|
+
message,
|
|
19
|
+
retryable: false,
|
|
20
|
+
exchange: "binance",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function toError(error) {
|
|
24
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
25
|
+
}
|
|
26
|
+
function subscriptionKey(subscription) {
|
|
27
|
+
return `${subscription.family}:${subscription.nativeSymbol}:${subscription.streamKind}`;
|
|
28
|
+
}
|
|
29
|
+
function marketTypeFor(record) {
|
|
30
|
+
if (record.family === "spot") {
|
|
31
|
+
return "spot";
|
|
32
|
+
}
|
|
33
|
+
return record.fundingEligible ? "swap" : "future";
|
|
34
|
+
}
|
|
35
|
+
function createSymbolEventKey(event) {
|
|
36
|
+
return `${event.family}:${event.nativeSymbol}`;
|
|
37
|
+
}
|
|
38
|
+
export class BinanceNativeMarketAdapter {
|
|
39
|
+
exchange = "binance";
|
|
40
|
+
capabilities = {
|
|
41
|
+
publicWs: true,
|
|
42
|
+
privateWs: false,
|
|
43
|
+
l1BookStream: true,
|
|
44
|
+
fundingRateStream: true,
|
|
45
|
+
accountStream: false,
|
|
46
|
+
orderStream: false,
|
|
47
|
+
fetchMarketInfo: true,
|
|
48
|
+
fetchBalances: false,
|
|
49
|
+
fetchPositions: false,
|
|
50
|
+
fetchRisk: false,
|
|
51
|
+
fetchOpenOrders: false,
|
|
52
|
+
fetchMyTrades: false,
|
|
53
|
+
fetchOrderById: false,
|
|
54
|
+
};
|
|
55
|
+
#restClient;
|
|
56
|
+
#createTransport;
|
|
57
|
+
#now;
|
|
58
|
+
#started = false;
|
|
59
|
+
#stopping = false;
|
|
60
|
+
#stopTask;
|
|
61
|
+
#marketIndex;
|
|
62
|
+
#marketInfo = [];
|
|
63
|
+
#transports = new Map();
|
|
64
|
+
#transportConnections = new Map();
|
|
65
|
+
#transportTasks = new Map();
|
|
66
|
+
#reconnectTasks = new Map();
|
|
67
|
+
#subscriptions = new Map();
|
|
68
|
+
#subscriptionTasks = new Map();
|
|
69
|
+
#marketQueue = createAsyncQueue({ maxBufferSize: 100 });
|
|
70
|
+
#marketEventSink;
|
|
71
|
+
#internalErrorQueue = createAsyncQueue({ maxBufferSize: 20 });
|
|
72
|
+
#orderQueue = createAsyncQueue({ maxBufferSize: 10 });
|
|
73
|
+
#orderEventSink;
|
|
74
|
+
#accountQueue = createAsyncQueue({ maxBufferSize: 10 });
|
|
75
|
+
#accountEventSink;
|
|
76
|
+
constructor(input = {}) {
|
|
77
|
+
this.#restClient = input.restClient ?? createBinanceMarketRestClient();
|
|
78
|
+
this.#createTransport =
|
|
79
|
+
input.createTransport ??
|
|
80
|
+
((transportInput) => createBinanceMarketWsTransport({
|
|
81
|
+
family: transportInput.family,
|
|
82
|
+
createSocket: (url) => new WebSocket(url),
|
|
83
|
+
onMessage: (payload) => {
|
|
84
|
+
this.handleTransportMessage(transportInput.family, payload);
|
|
85
|
+
},
|
|
86
|
+
onClose: () => {
|
|
87
|
+
this.handleTransportClose(transportInput.family);
|
|
88
|
+
},
|
|
89
|
+
onReconnect: async () => {
|
|
90
|
+
await this.handleTransportReconnect(transportInput.family);
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
this.#now = input.now ?? (() => Date.now());
|
|
94
|
+
}
|
|
95
|
+
async start() {
|
|
96
|
+
if (this.#stopping || this.#stopTask !== undefined) {
|
|
97
|
+
throw createAdapterLifecycleError("market adapter cannot be started after stop begins");
|
|
98
|
+
}
|
|
99
|
+
if (this.#started) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.#marketIndex = buildBinanceMarketIndex(await this.#restClient.fetchAllExchangeInfo());
|
|
103
|
+
this.#marketInfo = this.#marketIndex.markets.map((record) => ({
|
|
104
|
+
exchange: "binance",
|
|
105
|
+
symbol: record.unifiedSymbol,
|
|
106
|
+
baseAsset: record.baseAsset,
|
|
107
|
+
quoteAsset: record.quoteAsset,
|
|
108
|
+
marketType: marketTypeFor(record),
|
|
109
|
+
pricePrecision: record.pricePrecision,
|
|
110
|
+
amountPrecision: record.amountPrecision,
|
|
111
|
+
active: record.active,
|
|
112
|
+
}));
|
|
113
|
+
this.#started = true;
|
|
114
|
+
}
|
|
115
|
+
async stop() {
|
|
116
|
+
if (this.#stopTask !== undefined) {
|
|
117
|
+
await this.#stopTask;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.#stopTask = (async () => {
|
|
121
|
+
this.#stopping = true;
|
|
122
|
+
this.#started = false;
|
|
123
|
+
await Promise.allSettled([...this.#transports.values()].map((transport) => transport.close()));
|
|
124
|
+
await Promise.allSettled([
|
|
125
|
+
...this.#transportTasks.values(),
|
|
126
|
+
...this.#subscriptionTasks.values(),
|
|
127
|
+
...this.#reconnectTasks.values(),
|
|
128
|
+
]);
|
|
129
|
+
this.#transports.clear();
|
|
130
|
+
this.#transportConnections.clear();
|
|
131
|
+
this.#transportTasks.clear();
|
|
132
|
+
this.#reconnectTasks.clear();
|
|
133
|
+
this.#subscriptions.clear();
|
|
134
|
+
this.#subscriptionTasks.clear();
|
|
135
|
+
this.#marketIndex = undefined;
|
|
136
|
+
this.#marketInfo = [];
|
|
137
|
+
this.#marketQueue.close();
|
|
138
|
+
this.#internalErrorQueue.close();
|
|
139
|
+
this.#orderQueue.close();
|
|
140
|
+
this.#accountQueue.close();
|
|
141
|
+
})();
|
|
142
|
+
try {
|
|
143
|
+
await this.#stopTask;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
this.#stopTask = undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async fetchMarketInfo() {
|
|
150
|
+
this.#requireMarketIndex();
|
|
151
|
+
return this.#marketInfo.map((market) => ({ ...market }));
|
|
152
|
+
}
|
|
153
|
+
async subscribeL1Book(input) {
|
|
154
|
+
const marketIndex = this.#requireMarketIndex();
|
|
155
|
+
const record = requireMarketRecord(marketIndex, input.symbol);
|
|
156
|
+
await this.#ensureSubscribed(record, "l1Book", `${record.nativeSymbol.toLowerCase()}@bookTicker`);
|
|
157
|
+
}
|
|
158
|
+
async subscribeFundingRate(input) {
|
|
159
|
+
const marketIndex = this.#requireMarketIndex();
|
|
160
|
+
const record = requireFundingMarketRecord(marketIndex, input.symbol);
|
|
161
|
+
await this.#ensureSubscribed(record, "fundingRate", `${record.nativeSymbol.toLowerCase()}@markPrice@1s`);
|
|
162
|
+
}
|
|
163
|
+
watchMarketEvents() {
|
|
164
|
+
return this.#marketQueue;
|
|
165
|
+
}
|
|
166
|
+
setMarketEventSink(sink) {
|
|
167
|
+
this.#marketEventSink = sink;
|
|
168
|
+
}
|
|
169
|
+
watchInternalErrors() {
|
|
170
|
+
return this.#internalErrorQueue;
|
|
171
|
+
}
|
|
172
|
+
async subscribeOrders(_input) {
|
|
173
|
+
throw createUnsupportedError("private orders stay on ccxt adapter");
|
|
174
|
+
}
|
|
175
|
+
async fetchOpenOrdersBaseline(_accountId) {
|
|
176
|
+
throw createUnsupportedError("private orders stay on ccxt adapter");
|
|
177
|
+
}
|
|
178
|
+
watchOrderEvents() {
|
|
179
|
+
return this.#orderQueue;
|
|
180
|
+
}
|
|
181
|
+
setOrderEventSink(sink) {
|
|
182
|
+
this.#orderEventSink = sink;
|
|
183
|
+
}
|
|
184
|
+
async fetchAccountBaseline(_accountId) {
|
|
185
|
+
return {
|
|
186
|
+
balances: {},
|
|
187
|
+
positions: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
watchAccountEvents() {
|
|
191
|
+
return this.#accountQueue;
|
|
192
|
+
}
|
|
193
|
+
setAccountEventSink(sink) {
|
|
194
|
+
this.#accountEventSink = sink;
|
|
195
|
+
}
|
|
196
|
+
async placeOrder(_input) {
|
|
197
|
+
throw createUnsupportedError("market adapter does not place orders");
|
|
198
|
+
}
|
|
199
|
+
async amendOrder(_input) {
|
|
200
|
+
throw createUnsupportedError("market adapter does not amend orders");
|
|
201
|
+
}
|
|
202
|
+
async cancelOrder(_input) {
|
|
203
|
+
throw createUnsupportedError("market adapter does not cancel orders");
|
|
204
|
+
}
|
|
205
|
+
async cancelAllOrders(_input) {
|
|
206
|
+
throw createUnsupportedError("market adapter does not cancel all orders");
|
|
207
|
+
}
|
|
208
|
+
getHealth() {
|
|
209
|
+
if (!this.#started) {
|
|
210
|
+
return {
|
|
211
|
+
exchange: this.exchange,
|
|
212
|
+
status: "idle",
|
|
213
|
+
wsConnected: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const subscribedFamilies = new Set();
|
|
217
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
218
|
+
subscribedFamilies.add(subscription.family);
|
|
219
|
+
}
|
|
220
|
+
if (subscribedFamilies.size === 0) {
|
|
221
|
+
return {
|
|
222
|
+
exchange: this.exchange,
|
|
223
|
+
status: "healthy",
|
|
224
|
+
wsConnected: true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const allFamiliesConnected = [...subscribedFamilies].every((family) => this.#transportConnections.get(family) === true);
|
|
228
|
+
return {
|
|
229
|
+
exchange: this.exchange,
|
|
230
|
+
status: allFamiliesConnected ? "healthy" : "degraded",
|
|
231
|
+
wsConnected: allFamiliesConnected,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
handleTransportMessage(family, payload) {
|
|
235
|
+
if (this.#stopping || this.#marketIndex === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const nativeSymbol = this.#readPayloadSymbol(payload);
|
|
239
|
+
if (nativeSymbol === undefined) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const record = getRecordByNativeSymbol(this.#marketIndex, family, nativeSymbol);
|
|
243
|
+
if (record === undefined) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const receivedAt = this.#now();
|
|
248
|
+
const event = this.#isFundingPayload(payload)
|
|
249
|
+
? normalizeMarkPriceEvent(record, payload, receivedAt)
|
|
250
|
+
: normalizeBookTickerEvent(record, payload, receivedAt);
|
|
251
|
+
this.#emitMarketEvent(event);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
this.#emitInternalError(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
handleTransportClose(family) {
|
|
258
|
+
if (this.#stopping || this.#marketIndex === undefined) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.#transportConnections.set(family, false);
|
|
262
|
+
const receivedAt = this.#now();
|
|
263
|
+
const emittedSymbols = new Set();
|
|
264
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
265
|
+
if (subscription.family !== family) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const symbolEventKey = createSymbolEventKey(subscription);
|
|
269
|
+
if (emittedSymbols.has(symbolEventKey)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const record = getRecordByNativeSymbol(this.#marketIndex, family, subscription.nativeSymbol);
|
|
273
|
+
if (record === undefined) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
emittedSymbols.add(symbolEventKey);
|
|
277
|
+
this.#emitMarketEvent({
|
|
278
|
+
type: "market.disconnected",
|
|
279
|
+
exchange: "binance",
|
|
280
|
+
symbol: record.unifiedSymbol,
|
|
281
|
+
receivedAt,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async handleTransportReconnect(family) {
|
|
286
|
+
if (this.#stopping || !this.#started) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const existingTask = this.#reconnectTasks.get(family);
|
|
290
|
+
if (existingTask !== undefined) {
|
|
291
|
+
await existingTask;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const task = this.#runTransportReconnect(family);
|
|
295
|
+
this.#reconnectTasks.set(family, task);
|
|
296
|
+
try {
|
|
297
|
+
await task;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.#reconnectTasks.delete(family);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async #runTransportReconnect(family) {
|
|
304
|
+
if (this.#stopping || this.#marketIndex === undefined) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const transport = this.#transports.get(family);
|
|
308
|
+
if (transport === undefined) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const receivedAt = this.#now();
|
|
312
|
+
const emittedSymbols = new Set();
|
|
313
|
+
let reconnectSucceeded = true;
|
|
314
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
315
|
+
if (this.#stopping) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (subscription.family !== family) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const record = getRecordByNativeSymbol(this.#marketIndex, family, subscription.nativeSymbol);
|
|
322
|
+
if (record !== undefined) {
|
|
323
|
+
const symbolEventKey = createSymbolEventKey(subscription);
|
|
324
|
+
if (!emittedSymbols.has(symbolEventKey)) {
|
|
325
|
+
emittedSymbols.add(symbolEventKey);
|
|
326
|
+
this.#emitMarketEvent({
|
|
327
|
+
type: "market.reconnecting",
|
|
328
|
+
exchange: "binance",
|
|
329
|
+
symbol: record.unifiedSymbol,
|
|
330
|
+
receivedAt,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await transport.ensureSubscribed(subscription.streamName);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
reconnectSucceeded = false;
|
|
339
|
+
this.#emitInternalError(error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.#transportConnections.set(family, reconnectSucceeded);
|
|
343
|
+
}
|
|
344
|
+
async #ensureSubscribed(record, streamKind, streamName) {
|
|
345
|
+
if (this.#stopping) {
|
|
346
|
+
throw createAdapterLifecycleError("market adapter must be started before using market methods");
|
|
347
|
+
}
|
|
348
|
+
const subscription = {
|
|
349
|
+
family: record.family,
|
|
350
|
+
nativeSymbol: record.nativeSymbol,
|
|
351
|
+
streamKind,
|
|
352
|
+
streamName,
|
|
353
|
+
};
|
|
354
|
+
const key = subscriptionKey(subscription);
|
|
355
|
+
if (this.#subscriptions.has(key)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const existingTask = this.#subscriptionTasks.get(key);
|
|
359
|
+
if (existingTask !== undefined) {
|
|
360
|
+
await existingTask;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const task = (async () => {
|
|
364
|
+
this.#subscriptions.set(key, subscription);
|
|
365
|
+
try {
|
|
366
|
+
const transport = await this.#getOrCreateTransport(record.family);
|
|
367
|
+
if (this.#stopping) {
|
|
368
|
+
throw createAdapterLifecycleError("market adapter must be started before using market methods");
|
|
369
|
+
}
|
|
370
|
+
await transport.ensureSubscribed(streamName);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.#subscriptions.delete(key);
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
this.#subscriptionTasks.set(key, task);
|
|
378
|
+
try {
|
|
379
|
+
await task;
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
this.#subscriptionTasks.delete(key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async #getOrCreateTransport(family) {
|
|
386
|
+
if (this.#stopping) {
|
|
387
|
+
throw createAdapterLifecycleError("market adapter must be started before using market methods");
|
|
388
|
+
}
|
|
389
|
+
const existing = this.#transports.get(family);
|
|
390
|
+
if (existing !== undefined) {
|
|
391
|
+
return existing;
|
|
392
|
+
}
|
|
393
|
+
const existingTask = this.#transportTasks.get(family);
|
|
394
|
+
if (existingTask !== undefined) {
|
|
395
|
+
return await existingTask;
|
|
396
|
+
}
|
|
397
|
+
const task = (async () => {
|
|
398
|
+
const transport = this.#createTransport({ family });
|
|
399
|
+
this.#transports.set(family, transport);
|
|
400
|
+
try {
|
|
401
|
+
await transport.connect();
|
|
402
|
+
if (this.#stopping) {
|
|
403
|
+
await transport.close();
|
|
404
|
+
throw createAdapterLifecycleError("market adapter must be started before using market methods");
|
|
405
|
+
}
|
|
406
|
+
this.#transportConnections.set(family, true);
|
|
407
|
+
return transport;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
this.#transports.delete(family);
|
|
411
|
+
this.#transportConnections.delete(family);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
this.#transportTasks.set(family, task);
|
|
416
|
+
try {
|
|
417
|
+
return await task;
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
this.#transportTasks.delete(family);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
#requireMarketIndex() {
|
|
424
|
+
if (!this.#started || this.#marketIndex === undefined) {
|
|
425
|
+
throw createAdapterLifecycleError("market adapter must be started before using market methods");
|
|
426
|
+
}
|
|
427
|
+
return this.#marketIndex;
|
|
428
|
+
}
|
|
429
|
+
#readPayloadSymbol(payload) {
|
|
430
|
+
if (typeof payload !== "object" || payload === null || !("s" in payload)) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
const symbol = payload.s;
|
|
434
|
+
return typeof symbol === "string" ? symbol : undefined;
|
|
435
|
+
}
|
|
436
|
+
#isFundingPayload(payload) {
|
|
437
|
+
return typeof payload === "object" && payload !== null && "r" in payload;
|
|
438
|
+
}
|
|
439
|
+
#emitMarketEvent(event) {
|
|
440
|
+
if (this.#stopping) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (this.#marketEventSink !== undefined) {
|
|
444
|
+
this.#marketEventSink(event);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.#marketQueue.push(event);
|
|
448
|
+
}
|
|
449
|
+
#emitInternalError(error) {
|
|
450
|
+
if (this.#stopping) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.#internalErrorQueue.push(toError(error));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NormalizedMarketEvent } from "../types.js";
|
|
2
|
+
import type { BinanceMarketRecord } from "./market-types.js";
|
|
3
|
+
export declare function normalizeBookTickerEvent(record: BinanceMarketRecord, payload: unknown, receivedAt: number): Extract<NormalizedMarketEvent, {
|
|
4
|
+
type: "l1_book.updated";
|
|
5
|
+
}>;
|
|
6
|
+
export declare function normalizeMarkPriceEvent(record: BinanceMarketRecord, payload: unknown, receivedAt: number): Extract<NormalizedMarketEvent, {
|
|
7
|
+
type: "funding_rate.updated";
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createAcexError } from "../../errors/acex-error.js";
|
|
2
|
+
function createPayloadShapeError(stream, message) {
|
|
3
|
+
return createAcexError({
|
|
4
|
+
code: "VALIDATION_ERROR",
|
|
5
|
+
message: `invalid binance ${stream} payload: ${message}`,
|
|
6
|
+
retryable: false,
|
|
7
|
+
exchange: "binance",
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function createPayloadValidationError(stream, record, payloadSymbol) {
|
|
11
|
+
return createAcexError({
|
|
12
|
+
code: "VALIDATION_ERROR",
|
|
13
|
+
message: `invalid binance ${stream} payload symbol: expected ${record.nativeSymbol}, received ${payloadSymbol}`,
|
|
14
|
+
retryable: false,
|
|
15
|
+
exchange: "binance",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function assertNativeSymbolMatches(stream, record, payloadSymbol) {
|
|
19
|
+
if (payloadSymbol !== record.nativeSymbol) {
|
|
20
|
+
throw createPayloadValidationError(stream, record, payloadSymbol);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function assertFundingEligible(record) {
|
|
24
|
+
if (!record.fundingEligible) {
|
|
25
|
+
throw createAcexError({
|
|
26
|
+
code: "CAPABILITY_NOT_SUPPORTED",
|
|
27
|
+
message: "funding rate is not supported for spot or delivery markets",
|
|
28
|
+
retryable: false,
|
|
29
|
+
exchange: "binance",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function asPayloadRecord(stream, payload) {
|
|
34
|
+
if (typeof payload !== "object" || payload === null) {
|
|
35
|
+
throw createPayloadShapeError(stream, "payload must be an object");
|
|
36
|
+
}
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
function readRequiredStringField(stream, payload, field) {
|
|
40
|
+
const value = payload[field];
|
|
41
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
42
|
+
throw createPayloadShapeError(stream, `missing or invalid ${field}`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function readRequiredFiniteNumberField(stream, payload, field) {
|
|
47
|
+
const value = payload[field];
|
|
48
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
49
|
+
throw createPayloadShapeError(stream, `missing or invalid ${field}`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function readOptionalFiniteNumberField(stream, payload, field) {
|
|
54
|
+
const value = payload[field];
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
59
|
+
throw createPayloadShapeError(stream, `missing or invalid ${field}`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function parseBookTickerPayload(record, payload) {
|
|
64
|
+
const parsed = asPayloadRecord("bookTicker", payload);
|
|
65
|
+
const symbol = readRequiredStringField("bookTicker", parsed, "s");
|
|
66
|
+
const bidPrice = readRequiredStringField("bookTicker", parsed, "b");
|
|
67
|
+
const bidSize = readRequiredStringField("bookTicker", parsed, "B");
|
|
68
|
+
const askPrice = readRequiredStringField("bookTicker", parsed, "a");
|
|
69
|
+
const askSize = readRequiredStringField("bookTicker", parsed, "A");
|
|
70
|
+
const exchangeTs = readOptionalFiniteNumberField("bookTicker", parsed, "E");
|
|
71
|
+
if (record.family === "spot") {
|
|
72
|
+
if (exchangeTs !== undefined) {
|
|
73
|
+
throw createPayloadShapeError("bookTicker", "spot bookTicker payload must not include E");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (exchangeTs === undefined) {
|
|
77
|
+
throw createPayloadShapeError("bookTicker", `${record.family} bookTicker payload must include E`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
s: symbol,
|
|
81
|
+
b: bidPrice,
|
|
82
|
+
B: bidSize,
|
|
83
|
+
a: askPrice,
|
|
84
|
+
A: askSize,
|
|
85
|
+
...(exchangeTs === undefined ? {} : { E: exchangeTs }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseMarkPricePayload(payload) {
|
|
89
|
+
const parsed = asPayloadRecord("markPrice", payload);
|
|
90
|
+
return {
|
|
91
|
+
s: readRequiredStringField("markPrice", parsed, "s"),
|
|
92
|
+
r: readRequiredStringField("markPrice", parsed, "r"),
|
|
93
|
+
E: readRequiredFiniteNumberField("markPrice", parsed, "E"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function normalizeBookTickerEvent(record, payload, receivedAt) {
|
|
97
|
+
const parsed = parseBookTickerPayload(record, payload);
|
|
98
|
+
assertNativeSymbolMatches("bookTicker", record, parsed.s);
|
|
99
|
+
return {
|
|
100
|
+
type: "l1_book.updated",
|
|
101
|
+
exchange: "binance",
|
|
102
|
+
symbol: record.unifiedSymbol,
|
|
103
|
+
bidPrice: parsed.b,
|
|
104
|
+
bidSize: parsed.B,
|
|
105
|
+
askPrice: parsed.a,
|
|
106
|
+
askSize: parsed.A,
|
|
107
|
+
...(parsed.E === undefined ? {} : { exchangeTs: parsed.E }),
|
|
108
|
+
receivedAt,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function normalizeMarkPriceEvent(record, payload, receivedAt) {
|
|
112
|
+
assertFundingEligible(record);
|
|
113
|
+
const parsed = parseMarkPricePayload(payload);
|
|
114
|
+
assertNativeSymbolMatches("markPrice", record, parsed.s);
|
|
115
|
+
return {
|
|
116
|
+
type: "funding_rate.updated",
|
|
117
|
+
exchange: "binance",
|
|
118
|
+
symbol: record.unifiedSymbol,
|
|
119
|
+
fundingRate: parsed.r,
|
|
120
|
+
exchangeTs: parsed.E,
|
|
121
|
+
receivedAt,
|
|
122
|
+
};
|
|
123
|
+
}
|