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

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.4.0-beta.4",
3
+ "version": "0.4.0-beta.6",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -699,6 +699,7 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
699
699
  private readonly options: {
700
700
  readonly fetchFn?: FetchLike;
701
701
  readonly httpTimeoutMs?: number;
702
+ readonly pollIntervalMs?: number;
702
703
  } = {},
703
704
  ) {}
704
705
 
@@ -762,13 +763,13 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
762
763
  createPrivateStream(
763
764
  credentials: AccountCredentials,
764
765
  callbacks: PrivateStreamCallbacks,
765
- options: PrivateStreamOptions,
766
+ _options: PrivateStreamOptions,
766
767
  accountOptions?: Record<string, unknown>,
767
768
  ): StreamHandle {
768
769
  let closed = false;
769
770
  let timer: ReturnType<typeof setTimeout> | undefined;
770
771
  const pollIntervalMs =
771
- options.juplendPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
772
+ this.options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
772
773
 
773
774
  const poll = async (): Promise<void> => {
774
775
  try {
@@ -210,7 +210,6 @@ export interface PrivateStreamOptions {
210
210
  reconnectDelayMs: number;
211
211
  reconnectMaxDelayMs: number;
212
212
  listenKeyKeepAliveMs: number;
213
- juplendPollIntervalMs?: number;
214
213
  now?: () => number;
215
214
  }
216
215
 
@@ -13,6 +13,7 @@ import type {
13
13
  PrivateRuntimeReason,
14
14
  PrivateRuntimeStatus,
15
15
  Venue,
16
+ VenueOrderCapabilities,
16
17
  } from "../types/index.ts";
17
18
 
18
19
  export interface RegisteredAccountRecord {
@@ -26,6 +27,7 @@ export interface ClientContext {
26
27
  now(): number;
27
28
  assertStarted(): void;
28
29
  getRegisteredAccount(accountId: string): RegisteredAccountRecord;
30
+ getPrivateOrderCapabilities(venue: Venue): VenueOrderCapabilities | undefined;
29
31
  ensurePrivateCredentials(accountId: string): void;
30
32
  subscribePrivateAccountFeed(accountId: string): Promise<void>;
31
33
  unsubscribePrivateAccountFeed(accountId: string): void;
@@ -105,13 +107,11 @@ export interface PrivateOrderDataConsumer {
105
107
 
106
108
  export function hasPrivateCredentials(
107
109
  credentials?: AccountCredentials,
108
- venue?: Venue,
110
+ credentialsRequired = true,
109
111
  ): boolean {
110
- if (venue === "juplend") {
111
- return true;
112
- }
113
-
114
- return Boolean(credentials?.apiKey && credentials.secret);
112
+ return credentialsRequired
113
+ ? Boolean(credentials?.apiKey && credentials.secret)
114
+ : true;
115
115
  }
116
116
 
117
117
  export function mergeCredentials(
@@ -73,7 +73,6 @@ export class PrivateSubscriptionCoordinator {
73
73
  private readonly streamReconnectMaxDelayMs: number;
74
74
  private readonly listenKeyKeepAliveMs: number;
75
75
  private readonly binanceRiskPollIntervalMs: number;
76
- private readonly juplendPollIntervalMs?: number;
77
76
  private readonly records = new Map<string, PrivateSubscriptionRecord>();
78
77
 
79
78
  constructor(
@@ -102,7 +101,6 @@ export class PrivateSubscriptionCoordinator {
102
101
  options.binance?.riskPollIntervalMs,
103
102
  DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS,
104
103
  );
105
- this.juplendPollIntervalMs = options.juplend?.pollIntervalMs;
106
104
  }
107
105
 
108
106
  async subscribeAccountFeed(accountId: string): Promise<void> {
@@ -115,7 +113,8 @@ export class PrivateSubscriptionCoordinator {
115
113
  }
116
114
 
117
115
  try {
118
- if (record.venue === "juplend") {
116
+ const adapter = this.getAdapter(record.venue);
117
+ if (adapter.accountCapabilities.updates === "polling") {
119
118
  await this.bootstrapAccount(record, account);
120
119
  await this.ensureStream(record, account);
121
120
  } else {
@@ -233,7 +232,11 @@ export class PrivateSubscriptionCoordinator {
233
232
  this.stopAccountRefreshPolling(record);
234
233
 
235
234
  try {
236
- if (record.venue === "juplend" && record.accountSubscribed) {
235
+ const adapter = this.getAdapter(record.venue);
236
+ if (
237
+ adapter.accountCapabilities.updates === "polling" &&
238
+ record.accountSubscribed
239
+ ) {
237
240
  await this.bootstrapAccount(record, account);
238
241
  await this.ensureStream(record, account);
239
242
  } else {
@@ -318,7 +321,7 @@ export class PrivateSubscriptionCoordinator {
318
321
 
319
322
  private ensureAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
320
323
  if (
321
- record.venue !== "binance" ||
324
+ typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
322
325
  !record.accountSubscribed ||
323
326
  record.accountRefreshTimer ||
324
327
  record.accountRefreshInFlight
@@ -339,7 +342,10 @@ export class PrivateSubscriptionCoordinator {
339
342
  }
340
343
 
341
344
  private scheduleAccountRefreshPoll(record: PrivateSubscriptionRecord): void {
342
- if (record.venue !== "binance" || !record.accountSubscribed) {
345
+ if (
346
+ typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
347
+ !record.accountSubscribed
348
+ ) {
343
349
  return;
344
350
  }
345
351
 
@@ -348,7 +354,7 @@ export class PrivateSubscriptionCoordinator {
348
354
  record.accountRefreshTimer = undefined;
349
355
  if (
350
356
  generation !== record.accountRefreshGeneration ||
351
- record.venue !== "binance" ||
357
+ typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
352
358
  !record.accountSubscribed
353
359
  ) {
354
360
  return;
@@ -374,7 +380,10 @@ export class PrivateSubscriptionCoordinator {
374
380
  }
375
381
 
376
382
  record.accountRefreshInFlight = undefined;
377
- if (record.accountSubscribed && record.venue === "binance") {
383
+ if (
384
+ record.accountSubscribed &&
385
+ typeof this.getAdapter(record.venue).refreshAccount === "function"
386
+ ) {
378
387
  this.scheduleAccountRefreshPoll(record);
379
388
  }
380
389
  });
@@ -488,15 +497,15 @@ export class PrivateSubscriptionCoordinator {
488
497
  record: PrivateSubscriptionRecord,
489
498
  account: RegisteredAccountRecord,
490
499
  ): Promise<void> {
500
+ const adapter = this.getAdapter(record.venue);
491
501
  const credentials = account.credentials;
492
- if (!credentials && record.venue !== "juplend") {
502
+ if (adapter.accountCapabilities.credentialsRequired && !credentials) {
493
503
  throw new AcexError(
494
504
  "CREDENTIALS_MISSING",
495
505
  `Account credentials are required for private subscriptions: ${account.accountId}`,
496
506
  );
497
507
  }
498
508
 
499
- const adapter = this.getAdapter(record.venue);
500
509
  const stream = adapter.createPrivateStream(
501
510
  credentials ?? {},
502
511
  {
@@ -592,7 +601,6 @@ export class PrivateSubscriptionCoordinator {
592
601
  reconnectDelayMs: this.streamReconnectDelayMs,
593
602
  reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
594
603
  listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
595
- juplendPollIntervalMs: this.juplendPollIntervalMs,
596
604
  now: () => this.context.now(),
597
605
  },
598
606
  { ...account.options, accountId: account.accountId },
@@ -664,7 +672,8 @@ export class PrivateSubscriptionCoordinator {
664
672
  account: RegisteredAccountRecord,
665
673
  ): Promise<void> {
666
674
  try {
667
- const bootstrap = await this.getAdapter(record.venue).bootstrapAccount(
675
+ const adapter = this.getAdapter(record.venue);
676
+ const bootstrap = await adapter.bootstrapAccount(
668
677
  account.credentials ?? {},
669
678
  { ...account.options, accountId: account.accountId },
670
679
  );
@@ -700,7 +709,10 @@ export class PrivateSubscriptionCoordinator {
700
709
  ready: false,
701
710
  reason: transportReason(
702
711
  error,
703
- record.venue === "juplend" ? "http_failed" : "auth_failed",
712
+ this.getAdapter(record.venue).accountCapabilities
713
+ .credentialsRequired
714
+ ? "auth_failed"
715
+ : "http_failed",
704
716
  ),
705
717
  },
706
718
  );
@@ -38,6 +38,7 @@ import type {
38
38
  StopOptions,
39
39
  Venue,
40
40
  VenueCapabilities,
41
+ VenueOrderCapabilities,
41
42
  } from "../types/index.ts";
42
43
  import {
43
44
  type ClientContext,
@@ -114,6 +115,9 @@ export class AcexClientImpl implements AcexClient, ClientContext {
114
115
  new JuplendPrivateAdapter(
115
116
  options.account?.juplend?.rpcUrl,
116
117
  options.account?.juplend?.jupApiKey,
118
+ {
119
+ pollIntervalMs: options.account?.juplend?.pollIntervalMs,
120
+ },
117
121
  ),
118
122
  ];
119
123
  this.privateAdapters = new Map(
@@ -298,9 +302,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
298
302
  return account;
299
303
  }
300
304
 
305
+ getPrivateOrderCapabilities(
306
+ venue: Venue,
307
+ ): VenueOrderCapabilities | undefined {
308
+ return this.privateAdapters.get(venue)?.orderCapabilities;
309
+ }
310
+
301
311
  ensurePrivateCredentials(accountId: string): void {
302
312
  const account = this.getRegisteredAccount(accountId);
303
- if (hasPrivateCredentials(account.credentials, account.venue)) {
313
+ if (
314
+ hasPrivateCredentials(
315
+ account.credentials,
316
+ this.getPrivateCredentialsRequired(account.venue),
317
+ )
318
+ ) {
304
319
  return;
305
320
  }
306
321
 
@@ -429,7 +444,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
429
444
  private getPrivateCommandAccount(accountId: string): RegisteredAccountRecord {
430
445
  const account = this.getRegisteredAccount(accountId);
431
446
  const adapter = this.getPrivateAdapter(account.venue);
432
- if (adapter.venue === "juplend") {
447
+ if (!adapter.orderCapabilities.supported) {
433
448
  throw this.createError(
434
449
  "VENUE_NOT_SUPPORTED",
435
450
  `Venue does not support private order commands: ${account.venue}`,
@@ -437,7 +452,12 @@ export class AcexClientImpl implements AcexClient, ClientContext {
437
452
  );
438
453
  }
439
454
 
440
- if (!hasPrivateCredentials(account.credentials, account.venue)) {
455
+ if (
456
+ !hasPrivateCredentials(
457
+ account.credentials,
458
+ adapter.accountCapabilities.credentialsRequired,
459
+ )
460
+ ) {
441
461
  throw this.createError(
442
462
  "CREDENTIALS_MISSING",
443
463
  `Account credentials are required for private order commands: ${accountId}`,
@@ -448,6 +468,13 @@ export class AcexClientImpl implements AcexClient, ClientContext {
448
468
  return account;
449
469
  }
450
470
 
471
+ private getPrivateCredentialsRequired(venue: Venue): boolean {
472
+ return (
473
+ this.privateAdapters.get(venue)?.accountCapabilities
474
+ .credentialsRequired ?? true
475
+ );
476
+ }
477
+
451
478
  private getPrivateAdapter(venue: Venue): PrivateUserDataAdapter {
452
479
  const adapter = this.privateAdapters.get(venue);
453
480
  if (!adapter) {
@@ -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,
@@ -63,6 +64,13 @@ interface MarketRecord {
63
64
  fundingRateStream?: StreamHandle;
64
65
  }
65
66
 
67
+ interface CatalogFetchResult {
68
+ venue: Venue;
69
+ added: string[];
70
+ removed: string[];
71
+ total: number;
72
+ }
73
+
66
74
  const DEFAULT_INITIAL_L1_TIMEOUT_MS = 15_000;
67
75
  const DEFAULT_L1_STALE_AFTER_MS = 15_000;
68
76
  const DEFAULT_L1_RECONNECT_DELAY_MS = 1_000;
@@ -114,7 +122,10 @@ export class MarketManagerImpl
114
122
  private readonly definitions = new Map<string, MarketDefinition>();
115
123
  private readonly records = new Map<string, MarketRecord>();
116
124
  private readonly loadedCatalogVenues = new Set<Venue>();
117
- private readonly catalogPromises = new Map<Venue, Promise<void>>();
125
+ private readonly catalogPromises = new Map<
126
+ Venue,
127
+ Promise<CatalogFetchResult>
128
+ >();
118
129
  private readonly initialL1TimeoutMs: number;
119
130
  private readonly l1StaleAfterMs: number;
120
131
  private readonly l1ReconnectDelayMs: number;
@@ -166,6 +177,30 @@ export class MarketManagerImpl
166
177
  );
167
178
  }
168
179
 
180
+ async reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]> {
181
+ if (venue !== undefined) {
182
+ this.assertSupportedVenue(venue);
183
+ return [await this.reloadVenue(venue)];
184
+ }
185
+
186
+ const venues = [...this.adapters.keys()];
187
+ const settled = await Promise.allSettled(
188
+ venues.map((registeredVenue) => this.reloadVenue(registeredVenue)),
189
+ );
190
+ const summaries: MarketCatalogReloadSummary[] = [];
191
+
192
+ for (const result of settled) {
193
+ if (result.status === "fulfilled") {
194
+ summaries.push(result.value);
195
+ continue;
196
+ }
197
+
198
+ throw result.reason;
199
+ }
200
+
201
+ return summaries;
202
+ }
203
+
169
204
  async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
170
205
  this.context.assertStarted();
171
206
  const market = await this.resolveMarketDefinition(input);
@@ -438,51 +473,132 @@ export class MarketManagerImpl
438
473
  return;
439
474
  }
440
475
 
441
- let catalogPromise = this.catalogPromises.get(venue);
442
- if (!catalogPromise) {
443
- catalogPromise = this.fetchAndStoreMarketCatalog(venue);
444
- this.catalogPromises.set(venue, catalogPromise);
445
- }
476
+ await this.fetchCatalogCoalesced(venue);
477
+ }
446
478
 
479
+ private async reloadVenue(venue: Venue): Promise<MarketCatalogReloadSummary> {
447
480
  try {
448
- await catalogPromise;
481
+ const result = await this.fetchCatalogCoalesced(venue);
482
+ return { ...result, ok: true };
449
483
  } catch (error) {
450
- this.catalogPromises.delete(venue);
484
+ if (
485
+ error instanceof AcexError &&
486
+ error.code === "MARKET_CATALOG_LOAD_FAILED"
487
+ ) {
488
+ return {
489
+ venue,
490
+ added: [],
491
+ removed: [],
492
+ total: this.countVenueMarkets(venue),
493
+ ok: false,
494
+ error,
495
+ };
496
+ }
497
+
451
498
  throw error;
452
499
  }
453
500
  }
454
501
 
455
- private async fetchAndStoreMarketCatalog(venue: Venue): Promise<void> {
502
+ private async fetchCatalogCoalesced(
503
+ venue: Venue,
504
+ ): Promise<CatalogFetchResult> {
505
+ let catalogPromise = this.catalogPromises.get(venue);
506
+ if (!catalogPromise) {
507
+ catalogPromise = this.fetchAndStoreMarketCatalog(venue).finally(() => {
508
+ this.catalogPromises.delete(venue);
509
+ });
510
+ this.catalogPromises.set(venue, catalogPromise);
511
+ }
512
+
513
+ return await catalogPromise;
514
+ }
515
+
516
+ private async fetchAndStoreMarketCatalog(
517
+ venue: Venue,
518
+ ): Promise<CatalogFetchResult> {
456
519
  const adapter = this.getMarketAdapter(venue);
520
+ let markets: MarketDefinition[];
457
521
 
458
522
  try {
459
- const markets = await adapter.loadMarkets();
523
+ markets = await adapter.loadMarkets();
524
+ } catch (error) {
525
+ throw this.createCatalogLoadError(venue, error);
526
+ }
460
527
 
461
- for (const [key, market] of this.definitions) {
462
- if (market.venue === venue) {
463
- this.definitions.delete(key);
464
- }
465
- }
528
+ const mismatchedMarket = markets.find((market) => market.venue !== venue);
529
+ if (mismatchedMarket) {
530
+ throw this.createCatalogLoadError(
531
+ venue,
532
+ new Error(
533
+ `Market catalog from ${venue} included ${mismatchedMarket.venue} market: ${mismatchedMarket.symbol}`,
534
+ ),
535
+ );
536
+ }
537
+
538
+ const previousKeys = this.getVenueMarketKeys(venue);
466
539
 
467
- for (const market of markets) {
468
- this.definitions.set(marketKey(market), market);
540
+ for (const [key, market] of this.definitions) {
541
+ if (market.venue === venue) {
542
+ this.definitions.delete(key);
469
543
  }
544
+ }
470
545
 
471
- this.loadedCatalogVenues.add(venue);
472
- } catch (error) {
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;
546
+ for (const market of markets) {
547
+ this.definitions.set(marketKey(market), market);
485
548
  }
549
+
550
+ this.loadedCatalogVenues.add(venue);
551
+
552
+ const currentKeys = this.getVenueMarketKeys(venue);
553
+ return {
554
+ venue,
555
+ added: this.diffMarketSymbols(venue, currentKeys, previousKeys),
556
+ removed: this.diffMarketSymbols(venue, previousKeys, currentKeys),
557
+ total: currentKeys.size,
558
+ };
559
+ }
560
+
561
+ private getVenueMarketKeys(venue: Venue): Set<string> {
562
+ const keys = new Set<string>();
563
+
564
+ for (const [key, market] of this.definitions) {
565
+ if (market.venue === venue) {
566
+ keys.add(key);
567
+ }
568
+ }
569
+
570
+ return keys;
571
+ }
572
+
573
+ private countVenueMarkets(venue: Venue): number {
574
+ return this.getVenueMarketKeys(venue).size;
575
+ }
576
+
577
+ private diffMarketSymbols(
578
+ venue: Venue,
579
+ left: Set<string>,
580
+ right: Set<string>,
581
+ ): string[] {
582
+ const prefix = `${venue}:`;
583
+ return [...left]
584
+ .filter((key) => !right.has(key))
585
+ .map((key) => key.slice(prefix.length))
586
+ .sort((leftSymbol, rightSymbol) => leftSymbol.localeCompare(rightSymbol));
587
+ }
588
+
589
+ private createCatalogLoadError(venue: Venue, error: unknown): AcexError {
590
+ const wrapped = new AcexError(
591
+ "MARKET_CATALOG_LOAD_FAILED",
592
+ `Failed to load market catalog from ${venue}`,
593
+ );
594
+ this.context.publishRuntimeError(
595
+ "adapter",
596
+ error instanceof Error
597
+ ? error
598
+ : new Error("Unknown catalog load failure"),
599
+ { venue },
600
+ );
601
+ return wrapped;
486
602
  }
487
603
 
488
604
  private async resolveMarketDefinition(input: {
@@ -102,7 +102,10 @@ export class OrderManagerImpl
102
102
  async subscribeOrders(input: SubscribeOrdersInput): Promise<void> {
103
103
  this.context.assertStarted();
104
104
  const account = this.context.getRegisteredAccount(input.accountId);
105
- if (account.venue === "juplend") {
105
+ if (
106
+ this.context.getPrivateOrderCapabilities(account.venue)?.updates ===
107
+ "unsupported"
108
+ ) {
106
109
  throw this.createError(
107
110
  "VENUE_NOT_SUPPORTED",
108
111
  `Venue does not support private order subscriptions: ${account.venue}`,
@@ -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,15 @@ 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
+
29
39
  export interface MarketDataStatus {
30
40
  venue: Venue;
31
41
  symbol: string;
@@ -159,6 +169,7 @@ export interface MarketManager {
159
169
  readonly events: MarketEventStreams;
160
170
 
161
171
  loadMarkets(): Promise<void>;
172
+ reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]>;
162
173
  subscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
163
174
  unsubscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
164
175
  subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;