@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.3.1-beta.0",
3
+ "version": "0.4.0-beta.10",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,7 +1,9 @@
1
1
  import { SubscriptionMultiplexer } from "../../internal/subscription-multiplexer.ts";
2
2
  import type {
3
3
  MarketDefinition,
4
+ RateLimiter,
4
5
  VenueMarketCapabilities,
6
+ VenueServerTime,
5
7
  } from "../../types/index.ts";
6
8
  import type {
7
9
  FundingRateStreamCallbacks,
@@ -15,6 +17,7 @@ import {
15
17
  type BinanceMarketDefinition,
16
18
  loadBinanceMarkets,
17
19
  } from "./market-catalog.ts";
20
+ import { fetchBinanceServerTime } from "./server-time.ts";
18
21
  import {
19
22
  type BinanceStreamDescriptor,
20
23
  type BinanceStreamMessage,
@@ -44,6 +47,7 @@ export class BinanceMarketAdapter implements MarketAdapter {
44
47
  readonly venue = "binance" as const;
45
48
  readonly marketCapabilities: VenueMarketCapabilities = {
46
49
  catalog: "supported",
50
+ serverTime: "supported",
47
51
  l1Book: "supported",
48
52
  fundingRate: "market_dependent",
49
53
  marketTypes: ["spot", "swap", "future"],
@@ -53,8 +57,16 @@ export class BinanceMarketAdapter implements MarketAdapter {
53
57
  private multiplexer: BinanceMarketMultiplexer | undefined;
54
58
  private multiplexerConfig: BinanceMultiplexerConfig | undefined;
55
59
 
60
+ constructor(
61
+ private readonly options: {
62
+ readonly rateLimiter?: RateLimiter;
63
+ } = {},
64
+ ) {}
65
+
56
66
  async loadMarkets(): Promise<MarketDefinition[]> {
57
- const markets = await loadBinanceMarkets();
67
+ const markets = await loadBinanceMarkets(fetch, {
68
+ rateLimiter: this.options.rateLimiter,
69
+ });
58
70
  this.definitions.clear();
59
71
 
60
72
  for (const market of markets) {
@@ -64,6 +76,12 @@ export class BinanceMarketAdapter implements MarketAdapter {
64
76
  return markets;
65
77
  }
66
78
 
79
+ async fetchServerTime(): Promise<VenueServerTime> {
80
+ return await fetchBinanceServerTime({
81
+ rateLimiter: this.options.rateLimiter,
82
+ });
83
+ }
84
+
67
85
  createL1BookStream(
68
86
  market: MarketDefinition,
69
87
  callbacks: L1BookStreamCallbacks,
@@ -1,5 +1,16 @@
1
- import BigNumber from "bignumber.js";
2
- import type { MarketDefinition, MarketType } from "../../types/index.ts";
1
+ import { toCanonical } from "../../internal/decimal.ts";
2
+ import {
3
+ type HttpClientMessages,
4
+ httpRequest,
5
+ isTransportError,
6
+ } from "../../internal/http-client.ts";
7
+ import type {
8
+ MarketDefinition,
9
+ MarketType,
10
+ RateLimiter,
11
+ RateLimitScope,
12
+ } from "../../types/index.ts";
13
+ import { parseBinanceRateLimitUsage } from "./rate-limit.ts";
3
14
 
4
15
  type FetchLike = typeof fetch;
5
16
 
@@ -54,6 +65,11 @@ const BINANCE_USDM_EXCHANGE_INFO_URL =
54
65
  "https://fapi.binance.com/fapi/v1/exchangeInfo";
55
66
  const BINANCE_COINM_EXCHANGE_INFO_URL =
56
67
  "https://dapi.binance.com/dapi/v1/exchangeInfo";
68
+ const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
69
+ const BINANCE_CATALOG_HTTP_MESSAGES: HttpClientMessages = {
70
+ http: ({ status, statusText }) =>
71
+ `Binance request failed: ${status} ${statusText ?? ""}`,
72
+ };
57
73
 
58
74
  function toRecord(value: unknown): Record<string, unknown> {
59
75
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -96,7 +112,9 @@ function inferContractType(
96
112
  contractType: string | undefined,
97
113
  deliveryDate: number | undefined,
98
114
  ): MarketType {
99
- if (contractType === "PERPETUAL") {
115
+ // Binance TradFi perpetuals expose a far-future deliveryDate, so the
116
+ // contractType is authoritative for perpetual classification.
117
+ if (contractType === "PERPETUAL" || contractType === "TRADIFI_PERPETUAL") {
100
118
  return "swap";
101
119
  }
102
120
 
@@ -146,12 +164,12 @@ function normalizeSpotSymbol(
146
164
  contract: false,
147
165
  pricePrecision: precisionFromStep(priceStep),
148
166
  amountPrecision: precisionFromStep(amountStep),
149
- priceStep: new BigNumber(priceStep),
150
- amountStep: new BigNumber(amountStep),
167
+ priceStep: toCanonical(priceStep),
168
+ amountStep: toCanonical(amountStep),
151
169
  minAmount: lotSizeFilter?.minQty
152
- ? new BigNumber(lotSizeFilter.minQty)
170
+ ? toCanonical(lotSizeFilter.minQty)
153
171
  : undefined,
154
- minNotional: notionalValue ? new BigNumber(notionalValue) : undefined,
172
+ minNotional: notionalValue ? toCanonical(notionalValue) : undefined,
155
173
  raw: toRecord(symbol),
156
174
  };
157
175
  }
@@ -198,29 +216,72 @@ function normalizeDerivativesSymbol(
198
216
  contract: true,
199
217
  linear: family === "usdm",
200
218
  inverse: family === "coinm",
201
- contractSize: contractSize ? new BigNumber(contractSize) : undefined,
219
+ contractSize: contractSize ? toCanonical(contractSize) : undefined,
202
220
  pricePrecision: precisionFromStep(priceStep),
203
221
  amountPrecision: precisionFromStep(amountStep),
204
- priceStep: new BigNumber(priceStep),
205
- amountStep: new BigNumber(amountStep),
222
+ priceStep: toCanonical(priceStep),
223
+ amountStep: toCanonical(amountStep),
206
224
  minAmount: lotSizeFilter?.minQty
207
- ? new BigNumber(lotSizeFilter.minQty)
225
+ ? toCanonical(lotSizeFilter.minQty)
208
226
  : undefined,
209
- minNotional: notionalValue ? new BigNumber(notionalValue) : undefined,
227
+ minNotional: notionalValue ? toCanonical(notionalValue) : undefined,
210
228
  expiry: type === "future" ? symbol.deliveryDate : undefined,
211
229
  raw: toRecord(symbol),
212
230
  };
213
231
  }
214
232
 
215
- async function fetchJson<T>(fetchFn: FetchLike, url: string): Promise<T> {
216
- const response = await fetchFn(url);
217
- if (!response.ok) {
218
- throw new Error(
219
- `Binance request failed: ${response.status} ${response.statusText}`,
233
+ async function requestCatalogJson<T>(
234
+ fetchFn: FetchLike,
235
+ url: string,
236
+ rateLimiter: RateLimiter | undefined,
237
+ endpointKey: string,
238
+ ): Promise<T> {
239
+ const scope: RateLimitScope = {
240
+ venue: "binance",
241
+ endpointKey,
242
+ };
243
+
244
+ await rateLimiter?.beforeRequest({ scope });
245
+
246
+ try {
247
+ const response = await httpRequest<T>({
248
+ fetchFn,
249
+ url,
250
+ timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
251
+ parseAs: "json",
252
+ jsonParseMode: "response",
253
+ retryPolicy: {
254
+ idempotent: true,
255
+ maxAttempts: 3,
256
+ },
257
+ messages: BINANCE_CATALOG_HTTP_MESSAGES,
258
+ });
259
+
260
+ await rateLimiter?.afterResponse(
261
+ { scope },
262
+ {
263
+ status: response.status,
264
+ headers: response.headers,
265
+ usage: parseBinanceRateLimitUsage(response.headers),
266
+ },
220
267
  );
221
- }
222
268
 
223
- return (await response.json()) as T;
269
+ return response.body;
270
+ } catch (error) {
271
+ if (isTransportError(error)) {
272
+ await rateLimiter?.onTransportError(
273
+ { scope },
274
+ {
275
+ status: error.status,
276
+ headers: error.headers,
277
+ retryAfterMs: error.retryAfterMs,
278
+ usage: parseBinanceRateLimitUsage(error.headers),
279
+ },
280
+ );
281
+ }
282
+
283
+ throw error;
284
+ }
224
285
  }
225
286
 
226
287
  function sortMarkets(
@@ -233,16 +294,26 @@ function sortMarkets(
233
294
 
234
295
  export async function loadBinanceMarkets(
235
296
  fetchFn: FetchLike = fetch,
297
+ options: { readonly rateLimiter?: RateLimiter } = {},
236
298
  ): Promise<BinanceMarketDefinition[]> {
237
299
  const [spot, usdm, coinm] = await Promise.all([
238
- fetchJson<BinanceSpotExchangeInfo>(fetchFn, BINANCE_SPOT_EXCHANGE_INFO_URL),
239
- fetchJson<BinanceDerivativesExchangeInfo>(
300
+ requestCatalogJson<BinanceSpotExchangeInfo>(
301
+ fetchFn,
302
+ BINANCE_SPOT_EXCHANGE_INFO_URL,
303
+ options.rateLimiter,
304
+ "GET /api/v3/exchangeInfo",
305
+ ),
306
+ requestCatalogJson<BinanceDerivativesExchangeInfo>(
240
307
  fetchFn,
241
308
  BINANCE_USDM_EXCHANGE_INFO_URL,
309
+ options.rateLimiter,
310
+ "GET /fapi/v1/exchangeInfo",
242
311
  ),
243
- fetchJson<BinanceDerivativesExchangeInfo>(
312
+ requestCatalogJson<BinanceDerivativesExchangeInfo>(
244
313
  fetchFn,
245
314
  BINANCE_COINM_EXCHANGE_INFO_URL,
315
+ options.rateLimiter,
316
+ "GET /dapi/v1/exchangeInfo",
246
317
  ),
247
318
  ]);
248
319