@imbingox/acex 0.4.0-beta.5 → 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/docs/api.md +21 -0
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +9 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/types.ts +2 -0
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/managers/market-manager.ts +181 -31
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +25 -0
package/docs/api.md
CHANGED
|
@@ -345,6 +345,7 @@ interface MarketManager {
|
|
|
345
345
|
readonly events: MarketEventStreams;
|
|
346
346
|
|
|
347
347
|
loadMarkets(): Promise<void>;
|
|
348
|
+
reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]>;
|
|
348
349
|
listMarkets(venue?: Venue): MarketDefinition[];
|
|
349
350
|
getMarket(venue: Venue, symbol: string): MarketDefinition | undefined;
|
|
350
351
|
getMarkets(symbol: string): MarketDefinition[];
|
|
@@ -368,6 +369,7 @@ interface MarketManager {
|
|
|
368
369
|
|
|
369
370
|
```ts
|
|
370
371
|
await client.market.loadMarkets();
|
|
372
|
+
const reloadSummaries = await client.market.reloadMarkets("binance");
|
|
371
373
|
|
|
372
374
|
const all = client.market.listMarkets();
|
|
373
375
|
const binanceOnly = client.market.listMarkets("binance");
|
|
@@ -378,6 +380,25 @@ const allBtcPerp = client.market.getMarkets("BTC/USDT:USDT");
|
|
|
378
380
|
|
|
379
381
|
`getMarkets(symbol)` 严格按完整统一 symbol 匹配。
|
|
380
382
|
|
|
383
|
+
`loadMarkets()` 会懒加载并缓存当前已注册 venue 的市场目录;已加载过的 venue 不会重复拉取。`reloadMarkets(venue?)` 用于主动刷新市场目录:传入 `venue` 时只刷新该 venue,省略时刷新所有已注册 market adapter。它和 `loadMarkets()` 一样不要求 client 已 `start()`,因此可在 `start()` 前或 `stop()` 后调用。
|
|
384
|
+
|
|
385
|
+
`reloadMarkets()` 返回每个 venue 的刷新摘要:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
type MarketCatalogReloadSummary = {
|
|
389
|
+
venue: Venue;
|
|
390
|
+
added: string[];
|
|
391
|
+
removed: string[];
|
|
392
|
+
total: number;
|
|
393
|
+
ok: boolean;
|
|
394
|
+
error?: AcexError;
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`added` / `removed` 是本次刷新相对旧目录变化的 symbol 列表,`total` 是刷新后该 venue 的目录数量。catalog 拉取失败时,对应 summary 为 `ok: false`,`error.code = "MARKET_CATALOG_LOAD_FAILED"`,旧目录会保留,方法不会因为该 venue 的 catalog 失败而 reject;未注册 runtime adapter 的合法 venue(例如当前 market adapter 未接入的 `bybit`)仍会抛 `VENUE_NOT_SUPPORTED`。
|
|
399
|
+
|
|
400
|
+
如果刷新会新增 symbol,调用方应先 `await client.market.reloadMarkets(venue)`,再按 summary 订阅新增 symbol。已加载 venue 上的后台 reload 不会阻塞并发 `subscribe*()`;reload 完成前订阅新增 symbol 仍可能按旧目录返回 `MARKET_NOT_FOUND`。
|
|
401
|
+
|
|
381
402
|
`MarketDefinition` 见 [§9](#9-数据类型参考)。价格/数量相关字段(`priceStep`、`amountStep`、`contractSize`、`minAmount`、`minNotional`)都是 canonical decimal string;需要运算时用 `new BigNumber(field)` 自行解析。
|
|
382
403
|
|
|
383
404
|
归一化下单价格和数量:
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -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,
|
package/src/errors.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
FundingRateUpdatedEvent,
|
|
24
24
|
L1Book,
|
|
25
25
|
L1BookUpdatedEvent,
|
|
26
|
+
MarketCatalogReloadSummary,
|
|
26
27
|
MarketDataStatus,
|
|
27
28
|
MarketDataStreamStatus,
|
|
28
29
|
MarketDefinition,
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
37
38
|
SubscribeL1BookInput,
|
|
38
39
|
SubscriptionActivity,
|
|
39
40
|
Venue,
|
|
41
|
+
VenueServerTime,
|
|
40
42
|
} from "../types/index.ts";
|
|
41
43
|
|
|
42
44
|
export interface MarketManagerOptions {
|
|
@@ -63,6 +65,13 @@ interface MarketRecord {
|
|
|
63
65
|
fundingRateStream?: StreamHandle;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
interface CatalogFetchResult {
|
|
69
|
+
venue: Venue;
|
|
70
|
+
added: string[];
|
|
71
|
+
removed: string[];
|
|
72
|
+
total: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
const DEFAULT_INITIAL_L1_TIMEOUT_MS = 15_000;
|
|
67
76
|
const DEFAULT_L1_STALE_AFTER_MS = 15_000;
|
|
68
77
|
const DEFAULT_L1_RECONNECT_DELAY_MS = 1_000;
|
|
@@ -114,7 +123,10 @@ export class MarketManagerImpl
|
|
|
114
123
|
private readonly definitions = new Map<string, MarketDefinition>();
|
|
115
124
|
private readonly records = new Map<string, MarketRecord>();
|
|
116
125
|
private readonly loadedCatalogVenues = new Set<Venue>();
|
|
117
|
-
private readonly catalogPromises = new Map<
|
|
126
|
+
private readonly catalogPromises = new Map<
|
|
127
|
+
Venue,
|
|
128
|
+
Promise<CatalogFetchResult>
|
|
129
|
+
>();
|
|
118
130
|
private readonly initialL1TimeoutMs: number;
|
|
119
131
|
private readonly l1StaleAfterMs: number;
|
|
120
132
|
private readonly l1ReconnectDelayMs: number;
|
|
@@ -166,6 +178,48 @@ export class MarketManagerImpl
|
|
|
166
178
|
);
|
|
167
179
|
}
|
|
168
180
|
|
|
181
|
+
async reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]> {
|
|
182
|
+
if (venue !== undefined) {
|
|
183
|
+
this.assertSupportedVenue(venue);
|
|
184
|
+
return [await this.reloadVenue(venue)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const venues = [...this.adapters.keys()];
|
|
188
|
+
const settled = await Promise.allSettled(
|
|
189
|
+
venues.map((registeredVenue) => this.reloadVenue(registeredVenue)),
|
|
190
|
+
);
|
|
191
|
+
const summaries: MarketCatalogReloadSummary[] = [];
|
|
192
|
+
|
|
193
|
+
for (const result of settled) {
|
|
194
|
+
if (result.status === "fulfilled") {
|
|
195
|
+
summaries.push(result.value);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw result.reason;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return summaries;
|
|
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
|
+
|
|
169
223
|
async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
|
|
170
224
|
this.context.assertStarted();
|
|
171
225
|
const market = await this.resolveMarketDefinition(input);
|
|
@@ -438,51 +492,147 @@ export class MarketManagerImpl
|
|
|
438
492
|
return;
|
|
439
493
|
}
|
|
440
494
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
catalogPromise = this.fetchAndStoreMarketCatalog(venue);
|
|
444
|
-
this.catalogPromises.set(venue, catalogPromise);
|
|
445
|
-
}
|
|
495
|
+
await this.fetchCatalogCoalesced(venue);
|
|
496
|
+
}
|
|
446
497
|
|
|
498
|
+
private async reloadVenue(venue: Venue): Promise<MarketCatalogReloadSummary> {
|
|
447
499
|
try {
|
|
448
|
-
await
|
|
500
|
+
const result = await this.fetchCatalogCoalesced(venue);
|
|
501
|
+
return { ...result, ok: true };
|
|
449
502
|
} catch (error) {
|
|
450
|
-
|
|
503
|
+
if (
|
|
504
|
+
error instanceof AcexError &&
|
|
505
|
+
error.code === "MARKET_CATALOG_LOAD_FAILED"
|
|
506
|
+
) {
|
|
507
|
+
return {
|
|
508
|
+
venue,
|
|
509
|
+
added: [],
|
|
510
|
+
removed: [],
|
|
511
|
+
total: this.countVenueMarkets(venue),
|
|
512
|
+
ok: false,
|
|
513
|
+
error,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
451
517
|
throw error;
|
|
452
518
|
}
|
|
453
519
|
}
|
|
454
520
|
|
|
455
|
-
private async
|
|
521
|
+
private async fetchCatalogCoalesced(
|
|
522
|
+
venue: Venue,
|
|
523
|
+
): Promise<CatalogFetchResult> {
|
|
524
|
+
let catalogPromise = this.catalogPromises.get(venue);
|
|
525
|
+
if (!catalogPromise) {
|
|
526
|
+
catalogPromise = this.fetchAndStoreMarketCatalog(venue).finally(() => {
|
|
527
|
+
this.catalogPromises.delete(venue);
|
|
528
|
+
});
|
|
529
|
+
this.catalogPromises.set(venue, catalogPromise);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return await catalogPromise;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private async fetchAndStoreMarketCatalog(
|
|
536
|
+
venue: Venue,
|
|
537
|
+
): Promise<CatalogFetchResult> {
|
|
456
538
|
const adapter = this.getMarketAdapter(venue);
|
|
539
|
+
let markets: MarketDefinition[];
|
|
457
540
|
|
|
458
541
|
try {
|
|
459
|
-
|
|
542
|
+
markets = await adapter.loadMarkets();
|
|
543
|
+
} catch (error) {
|
|
544
|
+
throw this.createCatalogLoadError(venue, error);
|
|
545
|
+
}
|
|
460
546
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
547
|
+
const mismatchedMarket = markets.find((market) => market.venue !== venue);
|
|
548
|
+
if (mismatchedMarket) {
|
|
549
|
+
throw this.createCatalogLoadError(
|
|
550
|
+
venue,
|
|
551
|
+
new Error(
|
|
552
|
+
`Market catalog from ${venue} included ${mismatchedMarket.venue} market: ${mismatchedMarket.symbol}`,
|
|
553
|
+
),
|
|
554
|
+
);
|
|
555
|
+
}
|
|
466
556
|
|
|
467
|
-
|
|
468
|
-
|
|
557
|
+
const previousKeys = this.getVenueMarketKeys(venue);
|
|
558
|
+
|
|
559
|
+
for (const [key, market] of this.definitions) {
|
|
560
|
+
if (market.venue === venue) {
|
|
561
|
+
this.definitions.delete(key);
|
|
469
562
|
}
|
|
563
|
+
}
|
|
470
564
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const wrapped = new AcexError(
|
|
474
|
-
"MARKET_CATALOG_LOAD_FAILED",
|
|
475
|
-
`Failed to load market catalog from ${venue}`,
|
|
476
|
-
);
|
|
477
|
-
this.context.publishRuntimeError(
|
|
478
|
-
"adapter",
|
|
479
|
-
error instanceof Error
|
|
480
|
-
? error
|
|
481
|
-
: new Error("Unknown catalog load failure"),
|
|
482
|
-
{ venue },
|
|
483
|
-
);
|
|
484
|
-
throw wrapped;
|
|
565
|
+
for (const market of markets) {
|
|
566
|
+
this.definitions.set(marketKey(market), market);
|
|
485
567
|
}
|
|
568
|
+
|
|
569
|
+
this.loadedCatalogVenues.add(venue);
|
|
570
|
+
|
|
571
|
+
const currentKeys = this.getVenueMarketKeys(venue);
|
|
572
|
+
return {
|
|
573
|
+
venue,
|
|
574
|
+
added: this.diffMarketSymbols(venue, currentKeys, previousKeys),
|
|
575
|
+
removed: this.diffMarketSymbols(venue, previousKeys, currentKeys),
|
|
576
|
+
total: currentKeys.size,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private getVenueMarketKeys(venue: Venue): Set<string> {
|
|
581
|
+
const keys = new Set<string>();
|
|
582
|
+
|
|
583
|
+
for (const [key, market] of this.definitions) {
|
|
584
|
+
if (market.venue === venue) {
|
|
585
|
+
keys.add(key);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return keys;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private countVenueMarkets(venue: Venue): number {
|
|
593
|
+
return this.getVenueMarketKeys(venue).size;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private diffMarketSymbols(
|
|
597
|
+
venue: Venue,
|
|
598
|
+
left: Set<string>,
|
|
599
|
+
right: Set<string>,
|
|
600
|
+
): string[] {
|
|
601
|
+
const prefix = `${venue}:`;
|
|
602
|
+
return [...left]
|
|
603
|
+
.filter((key) => !right.has(key))
|
|
604
|
+
.map((key) => key.slice(prefix.length))
|
|
605
|
+
.sort((leftSymbol, rightSymbol) => leftSymbol.localeCompare(rightSymbol));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private createCatalogLoadError(venue: Venue, error: unknown): AcexError {
|
|
609
|
+
const wrapped = new AcexError(
|
|
610
|
+
"MARKET_CATALOG_LOAD_FAILED",
|
|
611
|
+
`Failed to load market catalog from ${venue}`,
|
|
612
|
+
);
|
|
613
|
+
this.context.publishRuntimeError(
|
|
614
|
+
"adapter",
|
|
615
|
+
error instanceof Error
|
|
616
|
+
? error
|
|
617
|
+
: new Error("Unknown catalog load failure"),
|
|
618
|
+
{ venue },
|
|
619
|
+
);
|
|
620
|
+
return wrapped;
|
|
621
|
+
}
|
|
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;
|
|
486
636
|
}
|
|
487
637
|
|
|
488
638
|
private async resolveMarketDefinition(input: {
|
package/src/types/client.ts
CHANGED
|
@@ -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[];
|
package/src/types/market.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type BigNumber from "bignumber.js";
|
|
2
|
+
import type { AcexError } from "../errors.ts";
|
|
2
3
|
import type { MarketFreshness, SubscriptionActivity, Venue } from "./shared.ts";
|
|
3
4
|
|
|
4
5
|
export type MarketType = "spot" | "swap" | "future";
|
|
@@ -26,6 +27,28 @@ export interface MarketDefinition {
|
|
|
26
27
|
raw: Record<string, unknown>;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export interface MarketCatalogReloadSummary {
|
|
31
|
+
venue: Venue;
|
|
32
|
+
added: string[];
|
|
33
|
+
removed: string[];
|
|
34
|
+
total: number;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
error?: AcexError;
|
|
37
|
+
}
|
|
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
|
+
|
|
29
52
|
export interface MarketDataStatus {
|
|
30
53
|
venue: Venue;
|
|
31
54
|
symbol: string;
|
|
@@ -159,6 +182,8 @@ export interface MarketManager {
|
|
|
159
182
|
readonly events: MarketEventStreams;
|
|
160
183
|
|
|
161
184
|
loadMarkets(): Promise<void>;
|
|
185
|
+
reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]>;
|
|
186
|
+
fetchServerTime(venue: Venue): Promise<VenueServerTime>;
|
|
162
187
|
subscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
|
|
163
188
|
unsubscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
|
|
164
189
|
subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
|