@imbingox/acex 0.4.0-beta.6 → 0.4.0-beta.7

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.4.0-beta.6",
3
+ "version": "0.4.0-beta.7",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,6 +3,7 @@ import type {
3
3
  MarketDefinition,
4
4
  RateLimiter,
5
5
  VenueMarketCapabilities,
6
+ VenueServerTime,
6
7
  } from "../../types/index.ts";
7
8
  import type {
8
9
  FundingRateStreamCallbacks,
@@ -16,6 +17,7 @@ import {
16
17
  type BinanceMarketDefinition,
17
18
  loadBinanceMarkets,
18
19
  } from "./market-catalog.ts";
20
+ import { fetchBinanceServerTime } from "./server-time.ts";
19
21
  import {
20
22
  type BinanceStreamDescriptor,
21
23
  type BinanceStreamMessage,
@@ -45,6 +47,7 @@ export class BinanceMarketAdapter implements MarketAdapter {
45
47
  readonly venue = "binance" as const;
46
48
  readonly marketCapabilities: VenueMarketCapabilities = {
47
49
  catalog: "supported",
50
+ serverTime: "supported",
48
51
  l1Book: "supported",
49
52
  fundingRate: "market_dependent",
50
53
  marketTypes: ["spot", "swap", "future"],
@@ -73,6 +76,12 @@ export class BinanceMarketAdapter implements MarketAdapter {
73
76
  return markets;
74
77
  }
75
78
 
79
+ async fetchServerTime(): Promise<VenueServerTime> {
80
+ return await fetchBinanceServerTime({
81
+ rateLimiter: this.options.rateLimiter,
82
+ });
83
+ }
84
+
76
85
  createL1BookStream(
77
86
  market: MarketDefinition,
78
87
  callbacks: L1BookStreamCallbacks,
@@ -0,0 +1,106 @@
1
+ import {
2
+ type HttpClientMessages,
3
+ httpRequest,
4
+ isTransportError,
5
+ } from "../../internal/http-client.ts";
6
+ import type {
7
+ RateLimiter,
8
+ RateLimitScope,
9
+ VenueServerTime,
10
+ } from "../../types/index.ts";
11
+ import { parseBinanceRateLimitUsage } from "./rate-limit.ts";
12
+
13
+ type FetchLike = (
14
+ input: string | URL | Request,
15
+ init?: RequestInit,
16
+ ) => Promise<Response>;
17
+
18
+ interface BinanceServerTimeResponse {
19
+ serverTime?: unknown;
20
+ }
21
+
22
+ export interface FetchBinanceServerTimeOptions {
23
+ readonly rateLimiter?: RateLimiter;
24
+ readonly fetchFn?: FetchLike;
25
+ readonly now?: () => number;
26
+ readonly monotonicNow?: () => number;
27
+ }
28
+
29
+ const BINANCE_USDM_SERVER_TIME_URL = "https://fapi.binance.com/fapi/v1/time";
30
+ const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
31
+ const BINANCE_SERVER_TIME_HTTP_MESSAGES: HttpClientMessages = {
32
+ http: ({ status, statusText }) =>
33
+ `Binance server time request failed: ${status} ${statusText ?? ""}`,
34
+ };
35
+
36
+ export async function fetchBinanceServerTime(
37
+ options: FetchBinanceServerTimeOptions = {},
38
+ ): Promise<VenueServerTime> {
39
+ const fetchFn = options.fetchFn ?? fetch;
40
+ const now = options.now ?? Date.now;
41
+ const monotonicNow = options.monotonicNow ?? (() => performance.now());
42
+ const scope: RateLimitScope = {
43
+ venue: "binance",
44
+ endpointKey: "GET /fapi/v1/time",
45
+ };
46
+
47
+ await options.rateLimiter?.beforeRequest({ scope });
48
+
49
+ const requestSentAt = now();
50
+ const startMono = monotonicNow();
51
+
52
+ try {
53
+ const response = await httpRequest<BinanceServerTimeResponse>({
54
+ fetchFn,
55
+ url: BINANCE_USDM_SERVER_TIME_URL,
56
+ timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
57
+ parseAs: "json",
58
+ jsonParseMode: "response",
59
+ retryPolicy: {
60
+ idempotent: true,
61
+ maxAttempts: 1,
62
+ },
63
+ messages: BINANCE_SERVER_TIME_HTTP_MESSAGES,
64
+ });
65
+ const responseReceivedAt = now();
66
+ const endMono = monotonicNow();
67
+
68
+ await options.rateLimiter?.afterResponse(
69
+ { scope },
70
+ {
71
+ status: response.status,
72
+ headers: response.headers,
73
+ usage: parseBinanceRateLimitUsage(response.headers),
74
+ },
75
+ );
76
+
77
+ const { serverTime } = response.body;
78
+ if (typeof serverTime !== "number" || !Number.isFinite(serverTime)) {
79
+ throw new Error(
80
+ "Binance server time response missing numeric serverTime",
81
+ );
82
+ }
83
+
84
+ return {
85
+ serverTime,
86
+ requestSentAt,
87
+ responseReceivedAt,
88
+ roundTripMs: endMono - startMono,
89
+ estimatedOffsetMs: serverTime - (requestSentAt + responseReceivedAt) / 2,
90
+ };
91
+ } catch (error) {
92
+ if (isTransportError(error)) {
93
+ await options.rateLimiter?.onTransportError(
94
+ { scope },
95
+ {
96
+ status: error.status,
97
+ headers: error.headers,
98
+ retryAfterMs: error.retryAfterMs,
99
+ usage: parseBinanceRateLimitUsage(error.headers),
100
+ },
101
+ );
102
+ }
103
+
104
+ throw error;
105
+ }
106
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  VenueAccountCapabilities,
10
10
  VenueMarketCapabilities,
11
11
  VenueOrderCapabilities,
12
+ VenueServerTime,
12
13
  } from "../types/index.ts";
13
14
 
14
15
  export interface StreamHandle {
@@ -74,6 +75,7 @@ export interface MarketAdapter {
74
75
  readonly venue: Venue;
75
76
  readonly marketCapabilities: VenueMarketCapabilities;
76
77
  loadMarkets(): Promise<MarketDefinition[]>;
78
+ fetchServerTime?(): Promise<VenueServerTime>;
77
79
  createL1BookStream(
78
80
  market: MarketDefinition,
79
81
  callbacks: L1BookStreamCallbacks,
@@ -42,6 +42,7 @@ const typeOnlyNotes = [
42
42
 
43
43
  const unsupportedMarket: VenueMarketCapabilities = {
44
44
  catalog: "unsupported",
45
+ serverTime: "unsupported",
45
46
  l1Book: "unsupported",
46
47
  fundingRate: "unsupported",
47
48
  marketTypes: [],
package/src/errors.ts CHANGED
@@ -6,6 +6,7 @@ export type AcexErrorCode =
6
6
  | "CREDENTIALS_MISSING"
7
7
  | "VENUE_NOT_SUPPORTED"
8
8
  | "MARKET_CATALOG_LOAD_FAILED"
9
+ | "MARKET_SERVER_TIME_FETCH_FAILED"
9
10
  | "MARKET_INACTIVE"
10
11
  | "MARKET_FUNDING_RATE_UNSUPPORTED"
11
12
  | "MARKET_NOT_FOUND"
@@ -38,6 +38,7 @@ import type {
38
38
  SubscribeL1BookInput,
39
39
  SubscriptionActivity,
40
40
  Venue,
41
+ VenueServerTime,
41
42
  } from "../types/index.ts";
42
43
 
43
44
  export interface MarketManagerOptions {
@@ -201,6 +202,24 @@ export class MarketManagerImpl
201
202
  return summaries;
202
203
  }
203
204
 
205
+ async fetchServerTime(venue: Venue): Promise<VenueServerTime> {
206
+ const adapter = this.getMarketAdapter(venue);
207
+ if (!adapter.fetchServerTime) {
208
+ throw this.createError(
209
+ "VENUE_NOT_SUPPORTED",
210
+ `Venue is not supported yet: ${venue}`,
211
+ { venue },
212
+ "client",
213
+ );
214
+ }
215
+
216
+ try {
217
+ return await adapter.fetchServerTime();
218
+ } catch (error) {
219
+ throw this.createServerTimeFetchError(venue, error);
220
+ }
221
+ }
222
+
204
223
  async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
205
224
  this.context.assertStarted();
206
225
  const market = await this.resolveMarketDefinition(input);
@@ -601,6 +620,21 @@ export class MarketManagerImpl
601
620
  return wrapped;
602
621
  }
603
622
 
623
+ private createServerTimeFetchError(venue: Venue, error: unknown): AcexError {
624
+ const wrapped = new AcexError(
625
+ "MARKET_SERVER_TIME_FETCH_FAILED",
626
+ `Failed to fetch server time from ${venue}`,
627
+ );
628
+ this.context.publishRuntimeError(
629
+ "adapter",
630
+ error instanceof Error
631
+ ? error
632
+ : new Error("Unknown server time fetch failure"),
633
+ { venue },
634
+ );
635
+ return wrapped;
636
+ }
637
+
604
638
  private async resolveMarketDefinition(input: {
605
639
  venue: Venue;
606
640
  symbol: string;
@@ -83,6 +83,7 @@ export type OrderTimeInForceCapability = "gtc" | "post_only";
83
83
 
84
84
  export interface VenueMarketCapabilities {
85
85
  catalog: VenueCapabilitySupport;
86
+ serverTime: VenueCapabilitySupport;
86
87
  l1Book: VenueCapabilitySupport;
87
88
  fundingRate: FundingRateCapability;
88
89
  marketTypes: MarketType[];
@@ -36,6 +36,19 @@ export interface MarketCatalogReloadSummary {
36
36
  error?: AcexError;
37
37
  }
38
38
 
39
+ export interface VenueServerTime {
40
+ /** Exchange server time in epoch milliseconds. Binance currently measures the USDM cluster. */
41
+ serverTime: number;
42
+ /** Local wall-clock timestamp captured immediately before the HTTP request is sent. */
43
+ requestSentAt: number;
44
+ /** Local wall-clock timestamp captured immediately after the HTTP response is received. */
45
+ responseReceivedAt: number;
46
+ /** Round trip duration measured with a monotonic clock, in milliseconds. */
47
+ roundTripMs: number;
48
+ /** NTP-style offset estimate: serverTime - midpoint(requestSentAt, responseReceivedAt). */
49
+ estimatedOffsetMs: number;
50
+ }
51
+
39
52
  export interface MarketDataStatus {
40
53
  venue: Venue;
41
54
  symbol: string;
@@ -170,6 +183,7 @@ export interface MarketManager {
170
183
 
171
184
  loadMarkets(): Promise<void>;
172
185
  reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]>;
186
+ fetchServerTime(venue: Venue): Promise<VenueServerTime>;
173
187
  subscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
174
188
  unsubscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
175
189
  subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;