@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 +21 -0
- package/package.json +1 -1
- package/src/adapters/juplend/private-adapter.ts +3 -2
- package/src/adapters/types.ts +0 -1
- package/src/client/context.ts +6 -6
- package/src/client/private-subscription-coordinator.ts +25 -13
- package/src/client/runtime.ts +30 -3
- package/src/managers/market-manager.ts +147 -31
- package/src/managers/order-manager.ts +4 -1
- package/src/types/market.ts +11 -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
|
@@ -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
|
-
|
|
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.
|
|
772
|
+
this.options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
772
773
|
|
|
773
774
|
const poll = async (): Promise<void> => {
|
|
774
775
|
try {
|
package/src/adapters/types.ts
CHANGED
package/src/client/context.ts
CHANGED
|
@@ -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
|
-
|
|
110
|
+
credentialsRequired = true,
|
|
109
111
|
): boolean {
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 !== "
|
|
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 (
|
|
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 !== "
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
712
|
+
this.getAdapter(record.venue).accountCapabilities
|
|
713
|
+
.credentialsRequired
|
|
714
|
+
? "auth_failed"
|
|
715
|
+
: "http_failed",
|
|
704
716
|
),
|
|
705
717
|
},
|
|
706
718
|
);
|
package/src/client/runtime.ts
CHANGED
|
@@ -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 (
|
|
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.
|
|
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 (
|
|
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<
|
|
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
|
-
|
|
442
|
-
|
|
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
|
|
481
|
+
const result = await this.fetchCatalogCoalesced(venue);
|
|
482
|
+
return { ...result, ok: true };
|
|
449
483
|
} catch (error) {
|
|
450
|
-
|
|
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
|
|
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
|
-
|
|
523
|
+
markets = await adapter.loadMarkets();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw this.createCatalogLoadError(venue, error);
|
|
526
|
+
}
|
|
460
527
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
468
|
-
|
|
540
|
+
for (const [key, market] of this.definitions) {
|
|
541
|
+
if (market.venue === venue) {
|
|
542
|
+
this.definitions.delete(key);
|
|
469
543
|
}
|
|
544
|
+
}
|
|
470
545
|
|
|
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;
|
|
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 (
|
|
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}`,
|
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,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>;
|