@imbingox/acex 0.3.0-beta.2 → 0.3.0-beta.4

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.0-beta.2",
3
+ "version": "0.3.0-beta.4",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "files": [
18
18
  "index.ts",
19
- "src/"
19
+ "src/",
20
+ "docs/api.md"
20
21
  ],
21
22
  "scripts": {
22
23
  "changeset": "changeset",
@@ -47,11 +48,11 @@
47
48
  "devDependencies": {
48
49
  "@biomejs/biome": "^2.4.10",
49
50
  "@changesets/cli": "^2.31.0",
51
+ "@mindfoldhq/trellis": "^0.4.0",
50
52
  "@types/bun": "latest",
51
53
  "typescript": "^6.0.2"
52
54
  },
53
55
  "dependencies": {
54
- "@mindfoldhq/trellis": "^0.4.0",
55
56
  "bignumber.js": "^11.0.0"
56
57
  }
57
58
  }
@@ -1,4 +1,7 @@
1
- import type { MarketDefinition } from "../../types/index.ts";
1
+ import type {
2
+ MarketDefinition,
3
+ VenueMarketCapabilities,
4
+ } from "../../types/index.ts";
2
5
  import type {
3
6
  FundingRateStreamCallbacks,
4
7
  FundingRateStreamOptions,
@@ -16,6 +19,12 @@ import {
16
19
 
17
20
  export class BinanceMarketAdapter implements MarketAdapter {
18
21
  readonly venue = "binance" as const;
22
+ readonly marketCapabilities: VenueMarketCapabilities = {
23
+ catalog: "supported",
24
+ l1Book: "supported",
25
+ fundingRate: "market_dependent",
26
+ marketTypes: ["spot", "swap", "future"],
27
+ };
19
28
 
20
29
  private readonly definitions = new Map<string, BinanceMarketDefinition>();
21
30
 
@@ -1,7 +1,12 @@
1
1
  import { createHmac } from "node:crypto";
2
2
  import BigNumber from "bignumber.js";
3
3
  import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
4
- import type { AccountCredentials, PositionSide } from "../../types/index.ts";
4
+ import type {
5
+ AccountCredentials,
6
+ PositionSide,
7
+ VenueAccountCapabilities,
8
+ VenueOrderCapabilities,
9
+ } from "../../types/index.ts";
5
10
  import type {
6
11
  CancelAllOrdersRequest,
7
12
  CancelOrderRequest,
@@ -51,6 +56,7 @@ interface BinancePapiUmPosition {
51
56
  unrealizedProfit?: string;
52
57
  liquidationPrice?: string;
53
58
  leverage?: string;
59
+ notional?: string;
54
60
  positionSide?: string;
55
61
  updateTime?: number;
56
62
  }
@@ -280,14 +286,18 @@ function mapBalance(
280
286
  function mapAccountRisk(
281
287
  input: BinancePapiAccount,
282
288
  receivedAt: number,
289
+ positions: BinancePapiUmPosition[] = [],
283
290
  ): RawRiskUpdate | undefined {
284
291
  const uniMmr = firstString(input.uniMMR);
285
292
  const riskRatio = uniMmr
286
293
  ? new BigNumber(1).dividedBy(uniMmr).toString(10)
287
294
  : undefined;
295
+ const equity = firstString(input.accountEquity, input.totalEquity);
296
+ const actualLeverage = calculateActualLeverage(equity, positions);
288
297
  const risk: RawRiskUpdate = {
289
- equity: firstString(input.accountEquity, input.totalEquity),
298
+ equity,
290
299
  riskRatio,
300
+ actualLeverage,
291
301
  initialMargin: firstString(
292
302
  input.accountInitialMargin,
293
303
  input.totalInitialMargin,
@@ -303,6 +313,7 @@ function mapAccountRisk(
303
313
  if (
304
314
  !risk.equity &&
305
315
  !risk.riskRatio &&
316
+ !risk.actualLeverage &&
306
317
  !risk.initialMargin &&
307
318
  !risk.maintenanceMargin
308
319
  ) {
@@ -312,6 +323,34 @@ function mapAccountRisk(
312
323
  return risk;
313
324
  }
314
325
 
326
+ function calculateActualLeverage(
327
+ equity: string | undefined,
328
+ positions: BinancePapiUmPosition[],
329
+ ): string | undefined {
330
+ if (!equity) {
331
+ return undefined;
332
+ }
333
+
334
+ const equityValue = new BigNumber(equity);
335
+ if (!equityValue.isFinite() || equityValue.isZero()) {
336
+ return undefined;
337
+ }
338
+
339
+ const grossExposure = positions.reduce((total, position) => {
340
+ const notional = firstString(position.notional);
341
+ if (!notional) {
342
+ return total;
343
+ }
344
+
345
+ const value = new BigNumber(notional);
346
+ return value.isFinite() ? total.plus(value.absoluteValue()) : total;
347
+ }, new BigNumber(0));
348
+
349
+ return grossExposure.isZero()
350
+ ? undefined
351
+ : grossExposure.dividedBy(equityValue).toString(10);
352
+ }
353
+
315
354
  function mapUmPosition(
316
355
  input: BinancePapiUmPosition,
317
356
  receivedAt: number,
@@ -334,6 +373,22 @@ function mapUmPosition(
334
373
  };
335
374
  }
336
375
 
376
+ function mapAccountRefresh(
377
+ account: BinancePapiAccount,
378
+ positions: BinancePapiUmPosition[],
379
+ receivedAt: number,
380
+ ): RawAccountUpdate {
381
+ return {
382
+ positions: positions.flatMap((position) => {
383
+ const mapped = mapUmPosition(position, receivedAt);
384
+ return mapped ? [mapped] : [];
385
+ }),
386
+ risk: mapAccountRisk(account, receivedAt, positions),
387
+ exchangeTs: account.updateTime,
388
+ receivedAt,
389
+ };
390
+ }
391
+
337
392
  function mapOpenOrder(
338
393
  input: BinancePapiOpenOrder,
339
394
  receivedAt: number,
@@ -479,6 +534,36 @@ async function readJson<T>(response: Response, url: string): Promise<T> {
479
534
 
480
535
  export class BinancePrivateAdapter implements PrivateUserDataAdapter {
481
536
  readonly venue = "binance" as const;
537
+ readonly readOnly = false;
538
+ readonly notes = [
539
+ "Capabilities describe the current SDK runtime, not Binance's full exchange API surface.",
540
+ "Funding rate support depends on the market type.",
541
+ "Order commands currently target Binance PAPI UM USD-M symbols; venue-level order.supported does not mean every Binance market type is orderable.",
542
+ ];
543
+ readonly accountCapabilities: VenueAccountCapabilities = {
544
+ register: "supported",
545
+ snapshot: "supported",
546
+ updates: "websocket",
547
+ balances: "supported",
548
+ positions: "supported",
549
+ risk: "supported",
550
+ lending: "unsupported",
551
+ credentialsRequired: true,
552
+ };
553
+ readonly orderCapabilities: VenueOrderCapabilities = {
554
+ supported: true,
555
+ openOrders: "supported",
556
+ updates: "websocket",
557
+ create: "supported",
558
+ cancel: "supported",
559
+ cancelAll: "symbol",
560
+ orderTypes: ["limit", "market"],
561
+ timeInForce: ["gtc", "post_only"],
562
+ postOnly: true,
563
+ reduceOnly: true,
564
+ positionSide: "required_for_hedge",
565
+ clientOrderId: true,
566
+ };
482
567
 
483
568
  async bootstrapAccount(
484
569
  credentials: AccountCredentials,
@@ -515,12 +600,35 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
515
600
  const mapped = mapUmPosition(position, receivedAt);
516
601
  return mapped ? [mapped] : [];
517
602
  }),
518
- risk: mapAccountRisk(account, receivedAt),
603
+ risk: mapAccountRisk(account, receivedAt, positions),
519
604
  exchangeTs: account.updateTime,
520
605
  receivedAt,
521
606
  };
522
607
  }
523
608
 
609
+ async refreshAccount(
610
+ credentials: AccountCredentials,
611
+ accountOptions?: Record<string, unknown>,
612
+ ): Promise<RawAccountUpdate> {
613
+ const receivedAt = Date.now();
614
+ const [account, positions] = await Promise.all([
615
+ this.signedRequest<BinancePapiAccount>(
616
+ "GET",
617
+ "/papi/v1/account",
618
+ credentials,
619
+ accountOptions,
620
+ ),
621
+ this.signedRequest<BinancePapiUmPosition[]>(
622
+ "GET",
623
+ "/papi/v1/um/positionRisk",
624
+ credentials,
625
+ accountOptions,
626
+ ),
627
+ ]);
628
+
629
+ return mapAccountRefresh(account, positions, receivedAt);
630
+ }
631
+
524
632
  async bootstrapOpenOrders(
525
633
  credentials: AccountCredentials,
526
634
  accountOptions?: Record<string, unknown>,
@@ -1,6 +1,10 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import { AcexError } from "../../errors.ts";
3
- import type { AccountCredentials } from "../../types/index.ts";
3
+ import type {
4
+ AccountCredentials,
5
+ VenueAccountCapabilities,
6
+ VenueOrderCapabilities,
7
+ } from "../../types/index.ts";
4
8
  import type {
5
9
  CancelAllOrdersRequest,
6
10
  CancelOrderRequest,
@@ -361,6 +365,36 @@ function mapAccount(
361
365
 
362
366
  export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
363
367
  readonly venue = "juplend" as const;
368
+ readonly readOnly = true;
369
+ readonly notes = [
370
+ "Juplend support is limited to read-only lending account views.",
371
+ "Order and market data managers are not supported for this venue.",
372
+ ];
373
+ readonly accountCapabilities: VenueAccountCapabilities = {
374
+ register: "supported",
375
+ snapshot: "supported",
376
+ updates: "polling",
377
+ balances: "supported",
378
+ positions: "unsupported",
379
+ risk: "supported",
380
+ lending: "supported",
381
+ credentialsRequired: true,
382
+ };
383
+ readonly orderCapabilities: VenueOrderCapabilities = {
384
+ supported: false,
385
+ openOrders: "unsupported",
386
+ updates: "unsupported",
387
+ create: "unsupported",
388
+ cancel: "unsupported",
389
+ cancelAll: "unsupported",
390
+ orderTypes: [],
391
+ timeInForce: [],
392
+ postOnly: false,
393
+ reduceOnly: false,
394
+ positionSide: "unsupported",
395
+ clientOrderId: false,
396
+ reason: "read_only",
397
+ };
364
398
 
365
399
  async bootstrapAccount(
366
400
  credentials: AccountCredentials,
@@ -6,6 +6,9 @@ import type {
6
6
  OrderStatus,
7
7
  PositionSide,
8
8
  Venue,
9
+ VenueAccountCapabilities,
10
+ VenueMarketCapabilities,
11
+ VenueOrderCapabilities,
9
12
  } from "../types/index.ts";
10
13
 
11
14
  export interface StreamHandle {
@@ -69,6 +72,7 @@ export interface FundingRateStreamOptions {
69
72
 
70
73
  export interface MarketAdapter {
71
74
  readonly venue: Venue;
75
+ readonly marketCapabilities: VenueMarketCapabilities;
72
76
  loadMarkets(): Promise<MarketDefinition[]>;
73
77
  createL1BookStream(
74
78
  market: MarketDefinition,
@@ -117,6 +121,7 @@ export interface RawPositionUpdate {
117
121
  export interface RawRiskUpdate {
118
122
  equity?: string;
119
123
  riskRatio?: string;
124
+ actualLeverage?: string;
120
125
  initialMargin?: string;
121
126
  maintenanceMargin?: string;
122
127
  exchangeTs?: number;
@@ -210,10 +215,18 @@ export interface PrivateStreamOptions {
210
215
 
211
216
  export interface PrivateUserDataAdapter {
212
217
  readonly venue: Venue;
218
+ readonly readOnly: boolean;
219
+ readonly notes: string[];
220
+ readonly accountCapabilities: VenueAccountCapabilities;
221
+ readonly orderCapabilities: VenueOrderCapabilities;
213
222
  bootstrapAccount(
214
223
  credentials: AccountCredentials,
215
224
  accountOptions?: Record<string, unknown>,
216
225
  ): Promise<RawAccountBootstrap>;
226
+ refreshAccount?(
227
+ credentials: AccountCredentials,
228
+ accountOptions?: Record<string, unknown>,
229
+ ): Promise<RawAccountUpdate>;
217
230
  bootstrapOpenOrders(
218
231
  credentials: AccountCredentials,
219
232
  accountOptions?: Record<string, unknown>,
@@ -75,6 +75,7 @@ export interface PrivateAccountDataConsumer {
75
75
  accountId: string,
76
76
  venue: Venue,
77
77
  update: RawAccountUpdate,
78
+ options?: { preserveStatus?: boolean },
78
79
  ): void;
79
80
  onPrivateAccountStreamState(
80
81
  accountId: string,
@@ -19,6 +19,9 @@ interface PrivateSubscriptionRecord {
19
19
  accountReady: boolean;
20
20
  orderReady: boolean;
21
21
  stream?: StreamHandle;
22
+ accountRefreshTimer?: ReturnType<typeof setTimeout>;
23
+ accountRefreshInFlight?: Promise<void>;
24
+ accountRefreshGeneration: number;
22
25
  startPromise?: Promise<void>;
23
26
  reconcilePromise?: Promise<void>;
24
27
  }
@@ -27,6 +30,16 @@ const DEFAULT_STREAM_OPEN_TIMEOUT_MS = 15_000;
27
30
  const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
28
31
  const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
29
32
  const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
33
+ const DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS = 5_000;
34
+
35
+ function normalizePositiveInterval(
36
+ value: number | undefined,
37
+ fallback: number,
38
+ ): number {
39
+ return value !== undefined && Number.isFinite(value) && value > 0
40
+ ? value
41
+ : fallback;
42
+ }
30
43
 
31
44
  export class PrivateSubscriptionCoordinator {
32
45
  private readonly context: ClientContext;
@@ -37,6 +50,7 @@ export class PrivateSubscriptionCoordinator {
37
50
  private readonly streamReconnectDelayMs: number;
38
51
  private readonly streamReconnectMaxDelayMs: number;
39
52
  private readonly listenKeyKeepAliveMs: number;
53
+ private readonly binanceRiskPollIntervalMs: number;
40
54
  private readonly juplendPollIntervalMs?: number;
41
55
  private readonly records = new Map<string, PrivateSubscriptionRecord>();
42
56
 
@@ -62,6 +76,10 @@ export class PrivateSubscriptionCoordinator {
62
76
  DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
63
77
  this.listenKeyKeepAliveMs =
64
78
  options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
79
+ this.binanceRiskPollIntervalMs = normalizePositiveInterval(
80
+ options.binance?.riskPollIntervalMs,
81
+ DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS,
82
+ );
65
83
  this.juplendPollIntervalMs = options.juplend?.pollIntervalMs;
66
84
  }
67
85
 
@@ -81,6 +99,7 @@ export class PrivateSubscriptionCoordinator {
81
99
  } else {
82
100
  await this.ensureStream(record, account);
83
101
  await this.bootstrapAccount(record, account);
102
+ this.ensureAccountRefreshPolling(record);
84
103
  }
85
104
  } catch (error) {
86
105
  record.accountSubscribed = false;
@@ -96,6 +115,7 @@ export class PrivateSubscriptionCoordinator {
96
115
  }
97
116
 
98
117
  record.accountSubscribed = false;
118
+ this.stopAccountRefreshPolling(record);
99
119
  this.closeIfUnused(record);
100
120
  }
101
121
 
@@ -153,6 +173,7 @@ export class PrivateSubscriptionCoordinator {
153
173
 
154
174
  onClientStopping(): void {
155
175
  for (const record of this.records.values()) {
176
+ this.stopAccountRefreshPolling(record);
156
177
  this.closeStream(record);
157
178
  }
158
179
  }
@@ -164,6 +185,7 @@ export class PrivateSubscriptionCoordinator {
164
185
  }
165
186
 
166
187
  this.closeStream(record);
188
+ this.stopAccountRefreshPolling(record);
167
189
  this.records.delete(accountId);
168
190
  }
169
191
 
@@ -186,6 +208,7 @@ export class PrivateSubscriptionCoordinator {
186
208
  private async resumeRecord(record: PrivateSubscriptionRecord): Promise<void> {
187
209
  const account = this.getAccount(record.accountId);
188
210
  this.closeStream(record);
211
+ this.stopAccountRefreshPolling(record);
189
212
 
190
213
  try {
191
214
  if (record.venue === "juplend" && record.accountSubscribed) {
@@ -195,6 +218,7 @@ export class PrivateSubscriptionCoordinator {
195
218
  await this.ensureStream(record, account);
196
219
  if (record.accountSubscribed) {
197
220
  await this.bootstrapAccount(record, account);
221
+ this.ensureAccountRefreshPolling(record);
198
222
  }
199
223
  }
200
224
  if (record.ordersSubscribed) {
@@ -244,6 +268,7 @@ export class PrivateSubscriptionCoordinator {
244
268
  ordersSubscribed: false,
245
269
  accountReady: false,
246
270
  orderReady: false,
271
+ accountRefreshGeneration: 0,
247
272
  };
248
273
 
249
274
  this.records.set(account.accountId, record);
@@ -259,6 +284,7 @@ export class PrivateSubscriptionCoordinator {
259
284
  return;
260
285
  }
261
286
 
287
+ this.stopAccountRefreshPolling(record);
262
288
  this.closeStream(record);
263
289
  this.records.delete(record.accountId);
264
290
  }
@@ -268,6 +294,153 @@ export class PrivateSubscriptionCoordinator {
268
294
  record.stream = undefined;
269
295
  }
270
296
 
297
+ private ensureAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
298
+ if (
299
+ record.venue !== "binance" ||
300
+ !record.accountSubscribed ||
301
+ record.accountRefreshTimer ||
302
+ record.accountRefreshInFlight
303
+ ) {
304
+ return;
305
+ }
306
+
307
+ this.scheduleAccountRefreshPoll(record);
308
+ }
309
+
310
+ private stopAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
311
+ record.accountRefreshGeneration += 1;
312
+ if (record.accountRefreshTimer) {
313
+ clearTimeout(record.accountRefreshTimer);
314
+ record.accountRefreshTimer = undefined;
315
+ }
316
+ record.accountRefreshInFlight = undefined;
317
+ }
318
+
319
+ private scheduleAccountRefreshPoll(record: PrivateSubscriptionRecord): void {
320
+ if (record.venue !== "binance" || !record.accountSubscribed) {
321
+ return;
322
+ }
323
+
324
+ const generation = record.accountRefreshGeneration;
325
+ record.accountRefreshTimer = setTimeout(() => {
326
+ record.accountRefreshTimer = undefined;
327
+ if (
328
+ generation !== record.accountRefreshGeneration ||
329
+ record.venue !== "binance" ||
330
+ !record.accountSubscribed
331
+ ) {
332
+ return;
333
+ }
334
+
335
+ let latestAccount: RegisteredAccountRecord;
336
+ try {
337
+ latestAccount = this.getAccount(record.accountId);
338
+ } catch (error) {
339
+ this.handleAccountRefreshLookupError(record, error);
340
+ return;
341
+ }
342
+
343
+ record.accountRefreshInFlight = this.refreshAccount(
344
+ record,
345
+ latestAccount,
346
+ generation,
347
+ )
348
+ .catch(() => {})
349
+ .finally(() => {
350
+ if (generation !== record.accountRefreshGeneration) {
351
+ return;
352
+ }
353
+
354
+ record.accountRefreshInFlight = undefined;
355
+ if (record.accountSubscribed && record.venue === "binance") {
356
+ this.scheduleAccountRefreshPoll(record);
357
+ }
358
+ });
359
+ }, this.binanceRiskPollIntervalMs);
360
+ }
361
+
362
+ private handleAccountRefreshLookupError(
363
+ record: PrivateSubscriptionRecord,
364
+ error: unknown,
365
+ ): void {
366
+ this.stopAccountRefreshPolling(record);
367
+ if (error instanceof AcexError && error.code === "ACCOUNT_NOT_FOUND") {
368
+ return;
369
+ }
370
+
371
+ this.context.publishRuntimeError(
372
+ "adapter",
373
+ error instanceof Error
374
+ ? error
375
+ : new Error(`Failed to load ${record.venue} account for risk refresh`),
376
+ {
377
+ accountId: record.accountId,
378
+ venue: record.venue,
379
+ },
380
+ );
381
+ }
382
+
383
+ private async refreshAccount(
384
+ record: PrivateSubscriptionRecord,
385
+ account: RegisteredAccountRecord,
386
+ generation: number,
387
+ ): Promise<void> {
388
+ const adapter = this.getAdapter(record.venue);
389
+ if (!adapter.refreshAccount) {
390
+ return;
391
+ }
392
+
393
+ try {
394
+ const update = await adapter.refreshAccount(account.credentials ?? {}, {
395
+ ...account.options,
396
+ accountId: account.accountId,
397
+ });
398
+ if (
399
+ !record.accountSubscribed ||
400
+ generation !== record.accountRefreshGeneration
401
+ ) {
402
+ return;
403
+ }
404
+
405
+ record.accountReady = true;
406
+ this.accountConsumer.onPrivateAccountUpdate(
407
+ record.accountId,
408
+ record.venue,
409
+ update,
410
+ { preserveStatus: true },
411
+ );
412
+ } catch (error) {
413
+ if (
414
+ !record.accountSubscribed ||
415
+ generation !== record.accountRefreshGeneration
416
+ ) {
417
+ return;
418
+ }
419
+
420
+ this.context.publishRuntimeError(
421
+ "adapter",
422
+ error instanceof Error
423
+ ? error
424
+ : new Error(
425
+ `Failed to refresh ${record.venue} private account state`,
426
+ ),
427
+ {
428
+ accountId: record.accountId,
429
+ venue: record.venue,
430
+ },
431
+ );
432
+ this.accountConsumer.onPrivateAccountStreamState(
433
+ record.accountId,
434
+ record.venue,
435
+ {
436
+ runtimeStatus: "degraded",
437
+ ready: record.accountReady,
438
+ reason: "http_failed",
439
+ },
440
+ );
441
+ }
442
+ }
443
+
271
444
  private async ensureStream(
272
445
  record: PrivateSubscriptionRecord,
273
446
  account: RegisteredAccountRecord,
@@ -5,6 +5,7 @@ import type {
5
5
  CancelAllOrdersRequest,
6
6
  CancelOrderRequest,
7
7
  CreateOrderRequest,
8
+ MarketAdapter,
8
9
  PrivateUserDataAdapter,
9
10
  RawOrderUpdate,
10
11
  } from "../adapters/types.ts";
@@ -35,6 +36,7 @@ import type {
35
36
  RegisterAccountResult,
36
37
  StopOptions,
37
38
  Venue,
39
+ VenueCapabilities,
38
40
  } from "../types/index.ts";
39
41
  import {
40
42
  type ClientContext,
@@ -45,6 +47,10 @@ import {
45
47
  type RegisteredAccountRecord,
46
48
  } from "./context.ts";
47
49
  import { PrivateSubscriptionCoordinator } from "./private-subscription-coordinator.ts";
50
+ import {
51
+ getVenueCapabilitiesSnapshot,
52
+ listVenueCapabilitiesSnapshots,
53
+ } from "./venue-capabilities.ts";
48
54
 
49
55
  const activeClients = new Set<AcexClientImpl>();
50
56
 
@@ -89,13 +95,15 @@ export class AcexClientImpl implements AcexClient, ClientContext {
89
95
  private readonly marketManager: MarketManagerImpl;
90
96
  private readonly accountManager: AccountManagerImpl;
91
97
  private readonly orderManager: OrderManagerImpl;
92
- private readonly privateAdapters: Map<string, PrivateUserDataAdapter>;
98
+ private readonly marketAdapters: Map<Venue, MarketAdapter>;
99
+ private readonly privateAdapters: Map<Venue, PrivateUserDataAdapter>;
93
100
  private readonly privateCoordinator: PrivateSubscriptionCoordinator;
94
101
 
95
102
  constructor(options: CreateClientOptions = {}) {
96
103
  activeClients.add(this);
97
104
 
98
105
  const marketAdapter = new BinanceMarketAdapter();
106
+ this.marketAdapters = new Map([[marketAdapter.venue, marketAdapter]]);
99
107
  const privateAdapters = [
100
108
  new BinancePrivateAdapter(),
101
109
  new JuplendPrivateAdapter(),
@@ -142,6 +150,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
142
150
  };
143
151
  }
144
152
 
153
+ getVenueCapabilities(venue: Venue): VenueCapabilities {
154
+ return getVenueCapabilitiesSnapshot(venue, {
155
+ marketAdapters: this.marketAdapters,
156
+ privateAdapters: this.privateAdapters,
157
+ });
158
+ }
159
+
160
+ listVenueCapabilities(): VenueCapabilities[] {
161
+ return listVenueCapabilitiesSnapshots({
162
+ marketAdapters: this.marketAdapters,
163
+ privateAdapters: this.privateAdapters,
164
+ });
165
+ }
166
+
145
167
  async registerAccount(
146
168
  input: RegisterAccountInput,
147
169
  ): Promise<RegisterAccountResult> {