@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,17 @@
|
|
|
1
|
+
import type { BinanceCoinmExchangeInfo, BinanceSpotExchangeInfo, BinanceUsdmExchangeInfo } from "./market-types.js";
|
|
2
|
+
export interface BinanceMarketRestClient {
|
|
3
|
+
fetchAllExchangeInfo(): Promise<{
|
|
4
|
+
spot: BinanceSpotExchangeInfo;
|
|
5
|
+
usdm: BinanceUsdmExchangeInfo;
|
|
6
|
+
coinm: BinanceCoinmExchangeInfo;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export interface CreateBinanceMarketRestClientInput {
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
baseUrls?: {
|
|
12
|
+
spot: string;
|
|
13
|
+
usdm: string;
|
|
14
|
+
coinm: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function createBinanceMarketRestClient(input?: CreateBinanceMarketRestClientInput): BinanceMarketRestClient;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createAcexError } from "../../errors/acex-error.js";
|
|
2
|
+
const DEFAULT_BASE_URLS = {
|
|
3
|
+
spot: "https://api.binance.com",
|
|
4
|
+
usdm: "https://fapi.binance.com",
|
|
5
|
+
coinm: "https://dapi.binance.com",
|
|
6
|
+
};
|
|
7
|
+
function createTransportError(message, cause) {
|
|
8
|
+
return createAcexError({
|
|
9
|
+
code: "TRANSPORT_UNAVAILABLE",
|
|
10
|
+
message,
|
|
11
|
+
retryable: true,
|
|
12
|
+
exchange: "binance",
|
|
13
|
+
...(cause === undefined ? {} : { cause }),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function createPayloadError(message, cause) {
|
|
17
|
+
return createAcexError({
|
|
18
|
+
code: "VALIDATION_ERROR",
|
|
19
|
+
message,
|
|
20
|
+
retryable: false,
|
|
21
|
+
exchange: "binance",
|
|
22
|
+
...(cause === undefined ? {} : { cause }),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function createBinanceMarketRestClient(input = {}) {
|
|
26
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
27
|
+
const baseUrls = input.baseUrls ?? DEFAULT_BASE_URLS;
|
|
28
|
+
async function getJson(url) {
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetchImpl(url);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw createTransportError(`binance market metadata request failed: ${url}`, error);
|
|
35
|
+
}
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw createTransportError(`binance market metadata request failed: ${url} (${response.status})`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return (await response.json());
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw createPayloadError(`invalid binance market metadata payload: ${url}`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function requireSymbolsArray(family, payload) {
|
|
47
|
+
if (!Array.isArray(payload.symbols)) {
|
|
48
|
+
throw createPayloadError(`invalid binance market metadata payload: ${family}`);
|
|
49
|
+
}
|
|
50
|
+
return payload;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async fetchAllExchangeInfo() {
|
|
54
|
+
const [spot, usdm, coinm] = await Promise.all([
|
|
55
|
+
getJson(`${baseUrls.spot}/api/v3/exchangeInfo`),
|
|
56
|
+
getJson(`${baseUrls.usdm}/fapi/v1/exchangeInfo`),
|
|
57
|
+
getJson(`${baseUrls.coinm}/dapi/v1/exchangeInfo`),
|
|
58
|
+
]);
|
|
59
|
+
return {
|
|
60
|
+
spot: requireSymbolsArray("spot", spot),
|
|
61
|
+
usdm: requireSymbolsArray("usdm", usdm),
|
|
62
|
+
coinm: requireSymbolsArray("coinm", coinm),
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BinanceCoinmExchangeInfo, BinanceMarketIndex, BinanceMarketRecord, BinanceSpotExchangeInfo, BinanceUsdmExchangeInfo } from "./market-types.js";
|
|
2
|
+
export declare function buildBinanceMarketIndex(input: {
|
|
3
|
+
spot: BinanceSpotExchangeInfo;
|
|
4
|
+
usdm: BinanceUsdmExchangeInfo;
|
|
5
|
+
coinm: BinanceCoinmExchangeInfo;
|
|
6
|
+
}): BinanceMarketIndex;
|
|
7
|
+
export declare function requireMarketRecord(index: BinanceMarketIndex, unifiedSymbol: string): BinanceMarketRecord;
|
|
8
|
+
export declare function requireFundingMarketRecord(index: BinanceMarketIndex, unifiedSymbol: string): BinanceMarketRecord;
|
|
9
|
+
export declare function getRecordByNativeSymbol(index: BinanceMarketIndex, family: BinanceMarketRecord["family"], nativeSymbol: string): BinanceMarketRecord | undefined;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createAcexError } from "../../errors/acex-error.js";
|
|
2
|
+
function keyFor(family, nativeSymbol) {
|
|
3
|
+
return `${family}:${nativeSymbol}`;
|
|
4
|
+
}
|
|
5
|
+
function createInvalidMetadataError(message) {
|
|
6
|
+
return createAcexError({
|
|
7
|
+
code: "VALIDATION_ERROR",
|
|
8
|
+
message: `invalid binance market metadata: ${message}`,
|
|
9
|
+
retryable: false,
|
|
10
|
+
exchange: "binance",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function createDuplicateSymbolError(message) {
|
|
14
|
+
return createAcexError({
|
|
15
|
+
code: "VALIDATION_ERROR",
|
|
16
|
+
message: `duplicate binance market symbol: ${message}`,
|
|
17
|
+
retryable: false,
|
|
18
|
+
exchange: "binance",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function getFilterValue(filters, filterType, key) {
|
|
22
|
+
return filters.find((filter) => filter.filterType === filterType)?.[key];
|
|
23
|
+
}
|
|
24
|
+
function parseTickOrStepSize(filters, nativeSymbol, filterType, key) {
|
|
25
|
+
const value = getFilterValue(filters, filterType, key);
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
throw createInvalidMetadataError(`missing ${filterType}.${key} for ${nativeSymbol}`);
|
|
28
|
+
}
|
|
29
|
+
const parsedSize = Number(value);
|
|
30
|
+
if (!Number.isFinite(parsedSize) || parsedSize <= 0) {
|
|
31
|
+
throw createInvalidMetadataError(`invalid ${filterType}.${key} for ${nativeSymbol}: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return parsedSize;
|
|
34
|
+
}
|
|
35
|
+
function parseDeliveryDateSuffix(nativeSymbol, deliveryDate) {
|
|
36
|
+
if (deliveryDate === undefined) {
|
|
37
|
+
throw createInvalidMetadataError(`missing delivery suffix for ${nativeSymbol}`);
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isInteger(deliveryDate) ||
|
|
40
|
+
deliveryDate < 1_000_000_000_000 ||
|
|
41
|
+
deliveryDate > 10_000_000_000_000) {
|
|
42
|
+
throw createInvalidMetadataError(`invalid deliveryDate for ${nativeSymbol}: ${deliveryDate}`);
|
|
43
|
+
}
|
|
44
|
+
const delivery = new Date(deliveryDate);
|
|
45
|
+
if (Number.isNaN(delivery.getTime())) {
|
|
46
|
+
throw createInvalidMetadataError(`invalid deliveryDate for ${nativeSymbol}: ${deliveryDate}`);
|
|
47
|
+
}
|
|
48
|
+
const year = String(delivery.getUTCFullYear()).slice(-2);
|
|
49
|
+
const month = String(delivery.getUTCMonth() + 1).padStart(2, "0");
|
|
50
|
+
const day = String(delivery.getUTCDate()).padStart(2, "0");
|
|
51
|
+
return `${year}${month}${day}`;
|
|
52
|
+
}
|
|
53
|
+
function toDeliverySuffix(nativeSymbol, deliveryDate) {
|
|
54
|
+
const nativeSuffix = nativeSymbol.match(/_(\d{6})$/)?.[1];
|
|
55
|
+
const deliveryDateSuffix = deliveryDate === undefined ? undefined : parseDeliveryDateSuffix(nativeSymbol, deliveryDate);
|
|
56
|
+
if (nativeSuffix !== undefined) {
|
|
57
|
+
if (deliveryDateSuffix !== undefined && deliveryDateSuffix !== nativeSuffix) {
|
|
58
|
+
throw createInvalidMetadataError(`deliveryDate does not match native suffix for ${nativeSymbol}: ${deliveryDateSuffix} !== ${nativeSuffix}`);
|
|
59
|
+
}
|
|
60
|
+
return `-${nativeSuffix}`;
|
|
61
|
+
}
|
|
62
|
+
if (deliveryDateSuffix !== undefined) {
|
|
63
|
+
return `-${deliveryDateSuffix}`;
|
|
64
|
+
}
|
|
65
|
+
throw createInvalidMetadataError(`missing delivery suffix for ${nativeSymbol}`);
|
|
66
|
+
}
|
|
67
|
+
function createMarketMaps(markets) {
|
|
68
|
+
const byUnifiedSymbol = new Map();
|
|
69
|
+
const byNativeSymbol = new Map();
|
|
70
|
+
for (const record of markets) {
|
|
71
|
+
if (byUnifiedSymbol.has(record.unifiedSymbol)) {
|
|
72
|
+
throw createDuplicateSymbolError(record.unifiedSymbol);
|
|
73
|
+
}
|
|
74
|
+
const nativeKey = keyFor(record.family, record.nativeSymbol);
|
|
75
|
+
if (byNativeSymbol.has(nativeKey)) {
|
|
76
|
+
throw createDuplicateSymbolError(nativeKey);
|
|
77
|
+
}
|
|
78
|
+
byUnifiedSymbol.set(record.unifiedSymbol, record);
|
|
79
|
+
byNativeSymbol.set(nativeKey, record);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
byUnifiedSymbol,
|
|
83
|
+
byNativeSymbol,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function buildSpotRecord(symbol) {
|
|
87
|
+
return {
|
|
88
|
+
exchange: "binance",
|
|
89
|
+
family: "spot",
|
|
90
|
+
nativeSymbol: symbol.symbol,
|
|
91
|
+
unifiedSymbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
|
|
92
|
+
baseAsset: symbol.baseAsset,
|
|
93
|
+
quoteAsset: symbol.quoteAsset,
|
|
94
|
+
fundingEligible: false,
|
|
95
|
+
active: symbol.status === "TRADING",
|
|
96
|
+
pricePrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "PRICE_FILTER", "tickSize"),
|
|
97
|
+
amountPrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "LOT_SIZE", "stepSize"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildUsdmRecord(symbol) {
|
|
101
|
+
return {
|
|
102
|
+
exchange: "binance",
|
|
103
|
+
family: "usdm",
|
|
104
|
+
nativeSymbol: symbol.symbol,
|
|
105
|
+
unifiedSymbol: `${symbol.baseAsset}/${symbol.quoteAsset}:${symbol.marginAsset}`,
|
|
106
|
+
baseAsset: symbol.baseAsset,
|
|
107
|
+
quoteAsset: symbol.quoteAsset,
|
|
108
|
+
settleAsset: symbol.marginAsset,
|
|
109
|
+
contractType: symbol.contractType,
|
|
110
|
+
fundingEligible: symbol.contractType === "PERPETUAL",
|
|
111
|
+
active: symbol.status === "TRADING",
|
|
112
|
+
pricePrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "PRICE_FILTER", "tickSize"),
|
|
113
|
+
amountPrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "LOT_SIZE", "stepSize"),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildCoinmRecord(symbol) {
|
|
117
|
+
const isPerpetual = symbol.contractType === "PERPETUAL";
|
|
118
|
+
const deliverySuffix = isPerpetual ? "" : toDeliverySuffix(symbol.symbol, symbol.deliveryDate);
|
|
119
|
+
const settleSegment = `${symbol.marginAsset}${deliverySuffix}`;
|
|
120
|
+
return {
|
|
121
|
+
exchange: "binance",
|
|
122
|
+
family: "coinm",
|
|
123
|
+
nativeSymbol: symbol.symbol,
|
|
124
|
+
unifiedSymbol: `${symbol.baseAsset}/${symbol.quoteAsset}:${settleSegment}`,
|
|
125
|
+
baseAsset: symbol.baseAsset,
|
|
126
|
+
quoteAsset: symbol.quoteAsset,
|
|
127
|
+
settleAsset: symbol.marginAsset,
|
|
128
|
+
contractType: symbol.contractType,
|
|
129
|
+
fundingEligible: symbol.contractType === "PERPETUAL",
|
|
130
|
+
active: symbol.contractStatus === "TRADING",
|
|
131
|
+
pricePrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "PRICE_FILTER", "tickSize"),
|
|
132
|
+
amountPrecision: parseTickOrStepSize(symbol.filters, symbol.symbol, "LOT_SIZE", "stepSize"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function buildBinanceMarketIndex(input) {
|
|
136
|
+
const markets = [
|
|
137
|
+
...input.spot.symbols.map(buildSpotRecord),
|
|
138
|
+
...input.usdm.symbols.map(buildUsdmRecord),
|
|
139
|
+
...input.coinm.symbols.map(buildCoinmRecord),
|
|
140
|
+
];
|
|
141
|
+
const maps = createMarketMaps(markets);
|
|
142
|
+
return {
|
|
143
|
+
markets,
|
|
144
|
+
byUnifiedSymbol: maps.byUnifiedSymbol,
|
|
145
|
+
byNativeSymbol: maps.byNativeSymbol,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function requireMarketRecord(index, unifiedSymbol) {
|
|
149
|
+
const record = index.byUnifiedSymbol.get(unifiedSymbol);
|
|
150
|
+
if (record === undefined) {
|
|
151
|
+
throw createAcexError({
|
|
152
|
+
code: "VALIDATION_ERROR",
|
|
153
|
+
message: `unsupported binance market symbol: ${unifiedSymbol}`,
|
|
154
|
+
retryable: false,
|
|
155
|
+
exchange: "binance",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return record;
|
|
159
|
+
}
|
|
160
|
+
export function requireFundingMarketRecord(index, unifiedSymbol) {
|
|
161
|
+
const record = requireMarketRecord(index, unifiedSymbol);
|
|
162
|
+
if (!record.fundingEligible) {
|
|
163
|
+
throw createAcexError({
|
|
164
|
+
code: "CAPABILITY_NOT_SUPPORTED",
|
|
165
|
+
message: "funding rate is not supported for spot or delivery markets",
|
|
166
|
+
retryable: false,
|
|
167
|
+
exchange: "binance",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return record;
|
|
171
|
+
}
|
|
172
|
+
export function getRecordByNativeSymbol(index, family, nativeSymbol) {
|
|
173
|
+
return index.byNativeSymbol.get(keyFor(family, nativeSymbol));
|
|
174
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type WsSocketLike } from "../../runtime/ws-connection-supervisor.js";
|
|
2
|
+
import type { BinanceMarketFamily } from "./market-types.js";
|
|
3
|
+
export interface BinanceWsSocketLike extends WsSocketLike {
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
}
|
|
6
|
+
export interface BinanceMarketWsTransport {
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
ensureSubscribed(stream: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface CreateBinanceMarketWsTransportInput {
|
|
12
|
+
family: BinanceMarketFamily;
|
|
13
|
+
createSocket: (url: string) => BinanceWsSocketLike;
|
|
14
|
+
url?: string;
|
|
15
|
+
onMessage?: (payload: unknown) => void;
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
onReconnect?: () => void | Promise<void>;
|
|
18
|
+
reconnectDelayMs?: number;
|
|
19
|
+
heartbeatIntervalMs?: number;
|
|
20
|
+
idleTimeoutMs?: number;
|
|
21
|
+
now?: () => number;
|
|
22
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createBinanceMarketWsTransport(input: CreateBinanceMarketWsTransportInput): BinanceMarketWsTransport;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createAcexError, isAcexError } from "../../errors/acex-error.js";
|
|
2
|
+
import { createWsConnectionSupervisor, } from "../../runtime/ws-connection-supervisor.js";
|
|
3
|
+
const DEFAULT_WS_URLS = {
|
|
4
|
+
spot: "wss://stream.binance.com:9443/ws",
|
|
5
|
+
usdm: "wss://fstream.binance.com/ws",
|
|
6
|
+
coinm: "wss://dstream.binance.com/ws",
|
|
7
|
+
};
|
|
8
|
+
function toMessageString(data) {
|
|
9
|
+
if (typeof data === "string") {
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
if (data instanceof ArrayBuffer) {
|
|
13
|
+
return Buffer.from(data).toString("utf8");
|
|
14
|
+
}
|
|
15
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
16
|
+
}
|
|
17
|
+
function createTransportError(message, cause) {
|
|
18
|
+
return createAcexError({
|
|
19
|
+
code: "TRANSPORT_UNAVAILABLE",
|
|
20
|
+
message,
|
|
21
|
+
retryable: true,
|
|
22
|
+
exchange: "binance",
|
|
23
|
+
...(cause === undefined ? {} : { cause }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function toTransportError(message, cause) {
|
|
27
|
+
if (isAcexError(cause) && cause.exchange === "binance") {
|
|
28
|
+
return cause;
|
|
29
|
+
}
|
|
30
|
+
return createTransportError(message, cause);
|
|
31
|
+
}
|
|
32
|
+
function isSocketNotOpenError(error) {
|
|
33
|
+
return (isAcexError(error) &&
|
|
34
|
+
error.code === "TRANSPORT_UNAVAILABLE" &&
|
|
35
|
+
error.exchange === undefined &&
|
|
36
|
+
error.message === "websocket is not open");
|
|
37
|
+
}
|
|
38
|
+
export function createBinanceMarketWsTransport(input) {
|
|
39
|
+
const url = input.url ?? DEFAULT_WS_URLS[input.family];
|
|
40
|
+
const reconnectDelayMs = input.reconnectDelayMs ?? 250;
|
|
41
|
+
const sleep = input.sleepImpl ??
|
|
42
|
+
((ms) => {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
});
|
|
45
|
+
const subscribed = new Set();
|
|
46
|
+
const pendingSubscriptions = new Set();
|
|
47
|
+
const inFlightSubscriptions = new Map();
|
|
48
|
+
const subscriptionErrors = new Map();
|
|
49
|
+
let requestId = 1;
|
|
50
|
+
let manualClose = false;
|
|
51
|
+
let awaitingReconnect = false;
|
|
52
|
+
let recoveryAttempts = 0;
|
|
53
|
+
let reconnectRecoveryTask;
|
|
54
|
+
const sendSubscribe = (stream) => {
|
|
55
|
+
try {
|
|
56
|
+
supervisor.send(JSON.stringify({
|
|
57
|
+
method: "SUBSCRIBE",
|
|
58
|
+
params: [stream],
|
|
59
|
+
id: requestId++,
|
|
60
|
+
}));
|
|
61
|
+
subscribed.add(stream);
|
|
62
|
+
pendingSubscriptions.delete(stream);
|
|
63
|
+
subscriptionErrors.delete(stream);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isSocketNotOpenError(error)) {
|
|
67
|
+
pendingSubscriptions.add(stream);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const transportError = createTransportError(`failed to subscribe binance ${input.family} stream: ${stream}`, error);
|
|
71
|
+
subscribed.delete(stream);
|
|
72
|
+
pendingSubscriptions.delete(stream);
|
|
73
|
+
subscriptionErrors.set(stream, transportError);
|
|
74
|
+
throw transportError;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const restoreDesiredSubscriptions = () => {
|
|
78
|
+
const desiredSubscriptions = new Set([...pendingSubscriptions, ...subscribed]);
|
|
79
|
+
pendingSubscriptions.clear();
|
|
80
|
+
for (const stream of desiredSubscriptions) {
|
|
81
|
+
pendingSubscriptions.add(stream);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const flushPendingSubscriptions = () => {
|
|
85
|
+
for (const stream of [...pendingSubscriptions]) {
|
|
86
|
+
try {
|
|
87
|
+
sendSubscribe(stream);
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const completeReconnect = async () => {
|
|
93
|
+
restoreDesiredSubscriptions();
|
|
94
|
+
flushPendingSubscriptions();
|
|
95
|
+
if (!awaitingReconnect) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (supervisor.getState() !== "open" || pendingSubscriptions.size > 0) {
|
|
99
|
+
scheduleReconnectRecovery();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
awaitingReconnect = false;
|
|
103
|
+
recoveryAttempts = 0;
|
|
104
|
+
await input.onReconnect?.();
|
|
105
|
+
};
|
|
106
|
+
const hasDesiredStreams = () => {
|
|
107
|
+
return subscribed.size > 0 || pendingSubscriptions.size > 0 || inFlightSubscriptions.size > 0;
|
|
108
|
+
};
|
|
109
|
+
const connectViaSupervisor = async () => {
|
|
110
|
+
manualClose = false;
|
|
111
|
+
try {
|
|
112
|
+
await supervisor.connect();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw toTransportError(`failed to connect binance ${input.family} websocket`, error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const recoveryDelayMs = () => {
|
|
119
|
+
const uncappedDelay = reconnectDelayMs * 2 ** recoveryAttempts;
|
|
120
|
+
const cappedDelay = Math.min(uncappedDelay, Math.max(reconnectDelayMs, 5_000));
|
|
121
|
+
const spread = Math.floor(cappedDelay * 0.2);
|
|
122
|
+
const jitterDelta = Math.floor(Math.random() * (spread * 2 + 1)) - spread;
|
|
123
|
+
return Math.max(0, cappedDelay + jitterDelta);
|
|
124
|
+
};
|
|
125
|
+
const scheduleReconnectRecovery = () => {
|
|
126
|
+
if (reconnectRecoveryTask !== undefined ||
|
|
127
|
+
manualClose ||
|
|
128
|
+
!awaitingReconnect ||
|
|
129
|
+
!hasDesiredStreams()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
reconnectRecoveryTask = (async () => {
|
|
133
|
+
try {
|
|
134
|
+
while (!manualClose && awaitingReconnect && hasDesiredStreams()) {
|
|
135
|
+
await sleep(recoveryDelayMs());
|
|
136
|
+
if (manualClose || !awaitingReconnect || !hasDesiredStreams()) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
recoveryAttempts += 1;
|
|
140
|
+
restoreDesiredSubscriptions();
|
|
141
|
+
try {
|
|
142
|
+
await supervisor.connect();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (manualClose || !awaitingReconnect) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await completeReconnect();
|
|
151
|
+
if (!awaitingReconnect) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
reconnectRecoveryTask = undefined;
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
};
|
|
161
|
+
const supervisor = createWsConnectionSupervisor({
|
|
162
|
+
createSocket: () => input.createSocket(url),
|
|
163
|
+
heartbeat: {
|
|
164
|
+
kind: "native_ping_pong",
|
|
165
|
+
heartbeatIntervalMs: input.heartbeatIntervalMs ?? 10_000,
|
|
166
|
+
idleTimeoutMs: input.idleTimeoutMs ?? 30_000,
|
|
167
|
+
},
|
|
168
|
+
backoff: {
|
|
169
|
+
baseDelayMs: input.reconnectDelayMs ?? 250,
|
|
170
|
+
maxDelayMs: Math.max(input.reconnectDelayMs ?? 250, 5_000),
|
|
171
|
+
jitter: true,
|
|
172
|
+
},
|
|
173
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
174
|
+
...(input.sleepImpl === undefined ? {} : { sleep: input.sleepImpl }),
|
|
175
|
+
onMessage(event) {
|
|
176
|
+
const raw = toMessageString(event.data);
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(raw);
|
|
179
|
+
const payload = typeof parsed === "object" && parsed !== null && "data" in parsed
|
|
180
|
+
? parsed.data
|
|
181
|
+
: parsed;
|
|
182
|
+
input.onMessage?.(payload);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
onDisconnect() {
|
|
189
|
+
awaitingReconnect = true;
|
|
190
|
+
input.onClose?.();
|
|
191
|
+
},
|
|
192
|
+
async onReconnect() {
|
|
193
|
+
await completeReconnect();
|
|
194
|
+
},
|
|
195
|
+
onError() {
|
|
196
|
+
queueMicrotask(() => {
|
|
197
|
+
if (supervisor.getState() === "idle") {
|
|
198
|
+
scheduleReconnectRecovery();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
async connect() {
|
|
205
|
+
if (supervisor.getState() === "open") {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
restoreDesiredSubscriptions();
|
|
209
|
+
await connectViaSupervisor();
|
|
210
|
+
await completeReconnect();
|
|
211
|
+
},
|
|
212
|
+
async close() {
|
|
213
|
+
manualClose = true;
|
|
214
|
+
awaitingReconnect = false;
|
|
215
|
+
try {
|
|
216
|
+
await supervisor.close();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
throw toTransportError(`failed to close binance ${input.family} websocket`, error);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
async ensureSubscribed(stream) {
|
|
223
|
+
if (subscribed.has(stream)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const existing = inFlightSubscriptions.get(stream);
|
|
227
|
+
if (existing !== undefined) {
|
|
228
|
+
await existing;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const task = (async () => {
|
|
232
|
+
pendingSubscriptions.add(stream);
|
|
233
|
+
if (awaitingReconnect) {
|
|
234
|
+
restoreDesiredSubscriptions();
|
|
235
|
+
}
|
|
236
|
+
await connectViaSupervisor();
|
|
237
|
+
if (!subscribed.has(stream)) {
|
|
238
|
+
if (awaitingReconnect) {
|
|
239
|
+
await completeReconnect();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (pendingSubscriptions.has(stream)) {
|
|
243
|
+
sendSubscribe(stream);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const error = subscriptionErrors.get(stream);
|
|
247
|
+
if (error !== undefined) {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
inFlightSubscriptions.set(stream, task);
|
|
253
|
+
try {
|
|
254
|
+
await task;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
inFlightSubscriptions.delete(stream);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -17,6 +17,7 @@ export class CcxtBinanceUsdMAdapter {
|
|
|
17
17
|
fetchOrderById: true,
|
|
18
18
|
};
|
|
19
19
|
#exchange;
|
|
20
|
+
#accountId;
|
|
20
21
|
#started = false;
|
|
21
22
|
#privateWatchTasks = new Set();
|
|
22
23
|
#orderSymbolsByClientOrderId = new Map();
|
|
@@ -30,6 +31,7 @@ export class CcxtBinanceUsdMAdapter {
|
|
|
30
31
|
#accountEventSink;
|
|
31
32
|
constructor(input) {
|
|
32
33
|
this.#exchange = input.exchange;
|
|
34
|
+
this.#accountId = input.accountId ?? "main";
|
|
33
35
|
}
|
|
34
36
|
async start() {
|
|
35
37
|
this.#started = true;
|
|
@@ -428,11 +430,11 @@ export class CcxtBinanceUsdMAdapter {
|
|
|
428
430
|
: "order.updated";
|
|
429
431
|
this.#emitOrderEvent({
|
|
430
432
|
type: eventType,
|
|
431
|
-
accountId:
|
|
433
|
+
accountId: this.#accountId,
|
|
432
434
|
exchange: this.exchange,
|
|
433
435
|
receivedAt: now,
|
|
434
436
|
snapshot: {
|
|
435
|
-
accountId:
|
|
437
|
+
accountId: this.#accountId,
|
|
436
438
|
exchange: this.exchange,
|
|
437
439
|
symbol,
|
|
438
440
|
side,
|
|
@@ -469,11 +471,11 @@ export class CcxtBinanceUsdMAdapter {
|
|
|
469
471
|
for (const asset of assets) {
|
|
470
472
|
this.#emitAccountEvent({
|
|
471
473
|
type: "balance.updated",
|
|
472
|
-
accountId:
|
|
474
|
+
accountId: this.#accountId,
|
|
473
475
|
exchange: this.exchange,
|
|
474
476
|
asset,
|
|
475
477
|
snapshot: {
|
|
476
|
-
accountId:
|
|
478
|
+
accountId: this.#accountId,
|
|
477
479
|
exchange: this.exchange,
|
|
478
480
|
asset,
|
|
479
481
|
free: valueToString(balance.free?.[asset]),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BinanceUsdmExchangeConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
secret: string;
|
|
4
|
+
enableRateLimit: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface BinanceUsdmExchangeLike {
|
|
7
|
+
options: {
|
|
8
|
+
defaultType?: string;
|
|
9
|
+
warnOnFetchOpenOrdersWithoutSymbol?: boolean;
|
|
10
|
+
};
|
|
11
|
+
enableDemoTrading?(value: boolean): void;
|
|
12
|
+
}
|
|
13
|
+
export type BinanceUsdmExchangeCtor = new (config: BinanceUsdmExchangeConfig) => BinanceUsdmExchangeLike;
|
|
14
|
+
export interface CreateBinanceUsdmExchangeInput {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
secret: string;
|
|
17
|
+
sandbox?: boolean;
|
|
18
|
+
enableRateLimit?: boolean;
|
|
19
|
+
exchangeCtor?: BinanceUsdmExchangeCtor;
|
|
20
|
+
}
|
|
21
|
+
export declare function getCcxtProBinanceUsdmCtor(): BinanceUsdmExchangeCtor;
|
|
22
|
+
export declare function createBinanceUsdmExchange(input: CreateBinanceUsdmExchangeInput): BinanceUsdmExchangeLike;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ccxt from "ccxt";
|
|
2
|
+
export function getCcxtProBinanceUsdmCtor() {
|
|
3
|
+
const proNamespace = ccxt
|
|
4
|
+
.pro;
|
|
5
|
+
if (proNamespace === undefined || proNamespace.binanceusdm === undefined) {
|
|
6
|
+
throw new Error("ccxt.pro.binanceusdm is not available");
|
|
7
|
+
}
|
|
8
|
+
return proNamespace.binanceusdm;
|
|
9
|
+
}
|
|
10
|
+
export function createBinanceUsdmExchange(input) {
|
|
11
|
+
const ExchangeCtor = input.exchangeCtor ?? getCcxtProBinanceUsdmCtor();
|
|
12
|
+
const exchange = new ExchangeCtor({
|
|
13
|
+
apiKey: input.apiKey,
|
|
14
|
+
secret: input.secret,
|
|
15
|
+
enableRateLimit: input.enableRateLimit ?? true,
|
|
16
|
+
});
|
|
17
|
+
exchange.options.defaultType = "future";
|
|
18
|
+
exchange.options.warnOnFetchOpenOrdersWithoutSymbol = false;
|
|
19
|
+
if (input.sandbox ?? true) {
|
|
20
|
+
exchange.enableDemoTrading?.(true);
|
|
21
|
+
}
|
|
22
|
+
return exchange;
|
|
23
|
+
}
|