@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.10
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/README.md +4 -3
- package/docs/api.md +474 -1002
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +83 -12
- package/src/adapters/binance/private-adapter.ts +302 -59
- package/src/adapters/binance/rate-limit.ts +47 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/juplend/private-adapter.ts +97 -68
- package/src/adapters/types.ts +25 -1
- package/src/client/context.ts +26 -9
- package/src/client/private-subscription-coordinator.ts +898 -63
- package/src/client/runtime.ts +49 -11
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +156 -2
- package/src/index.ts +8 -1
- package/src/internal/http-client.ts +608 -0
- package/src/internal/rate-limiter.ts +181 -0
- package/src/internal/watermark.ts +83 -0
- package/src/managers/account-manager.ts +227 -23
- package/src/managers/market-manager.ts +224 -34
- package/src/managers/order-manager.ts +791 -76
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +25 -0
- package/src/types/order.ts +1 -0
- package/src/types/shared.ts +66 -0
|
@@ -14,7 +14,11 @@ import type {
|
|
|
14
14
|
HealthReporter,
|
|
15
15
|
ManagerLifecycle,
|
|
16
16
|
} from "../client/context.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
AcexError,
|
|
19
|
+
buildAcexErrorDetails,
|
|
20
|
+
formatAcexErrorMessage,
|
|
21
|
+
} from "../errors.ts";
|
|
18
22
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
19
23
|
import { toCanonical } from "../internal/decimal.ts";
|
|
20
24
|
import { matchesMarketFilter } from "../internal/filters.ts";
|
|
@@ -23,6 +27,7 @@ import type {
|
|
|
23
27
|
FundingRateUpdatedEvent,
|
|
24
28
|
L1Book,
|
|
25
29
|
L1BookUpdatedEvent,
|
|
30
|
+
MarketCatalogReloadSummary,
|
|
26
31
|
MarketDataStatus,
|
|
27
32
|
MarketDataStreamStatus,
|
|
28
33
|
MarketDefinition,
|
|
@@ -37,6 +42,7 @@ import type {
|
|
|
37
42
|
SubscribeL1BookInput,
|
|
38
43
|
SubscriptionActivity,
|
|
39
44
|
Venue,
|
|
45
|
+
VenueServerTime,
|
|
40
46
|
} from "../types/index.ts";
|
|
41
47
|
|
|
42
48
|
export interface MarketManagerOptions {
|
|
@@ -63,6 +69,13 @@ interface MarketRecord {
|
|
|
63
69
|
fundingRateStream?: StreamHandle;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
interface CatalogFetchResult {
|
|
73
|
+
venue: Venue;
|
|
74
|
+
added: string[];
|
|
75
|
+
removed: string[];
|
|
76
|
+
total: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
const DEFAULT_INITIAL_L1_TIMEOUT_MS = 15_000;
|
|
67
80
|
const DEFAULT_L1_STALE_AFTER_MS = 15_000;
|
|
68
81
|
const DEFAULT_L1_RECONNECT_DELAY_MS = 1_000;
|
|
@@ -114,7 +127,10 @@ export class MarketManagerImpl
|
|
|
114
127
|
private readonly definitions = new Map<string, MarketDefinition>();
|
|
115
128
|
private readonly records = new Map<string, MarketRecord>();
|
|
116
129
|
private readonly loadedCatalogVenues = new Set<Venue>();
|
|
117
|
-
private readonly catalogPromises = new Map<
|
|
130
|
+
private readonly catalogPromises = new Map<
|
|
131
|
+
Venue,
|
|
132
|
+
Promise<CatalogFetchResult>
|
|
133
|
+
>();
|
|
118
134
|
private readonly initialL1TimeoutMs: number;
|
|
119
135
|
private readonly l1StaleAfterMs: number;
|
|
120
136
|
private readonly l1ReconnectDelayMs: number;
|
|
@@ -166,6 +182,48 @@ export class MarketManagerImpl
|
|
|
166
182
|
);
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
async reloadMarkets(venue?: Venue): Promise<MarketCatalogReloadSummary[]> {
|
|
186
|
+
if (venue !== undefined) {
|
|
187
|
+
this.assertSupportedVenue(venue);
|
|
188
|
+
return [await this.reloadVenue(venue)];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const venues = [...this.adapters.keys()];
|
|
192
|
+
const settled = await Promise.allSettled(
|
|
193
|
+
venues.map((registeredVenue) => this.reloadVenue(registeredVenue)),
|
|
194
|
+
);
|
|
195
|
+
const summaries: MarketCatalogReloadSummary[] = [];
|
|
196
|
+
|
|
197
|
+
for (const result of settled) {
|
|
198
|
+
if (result.status === "fulfilled") {
|
|
199
|
+
summaries.push(result.value);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw result.reason;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return summaries;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async fetchServerTime(venue: Venue): Promise<VenueServerTime> {
|
|
210
|
+
const adapter = this.getMarketAdapter(venue);
|
|
211
|
+
if (!adapter.fetchServerTime) {
|
|
212
|
+
throw this.createError(
|
|
213
|
+
"VENUE_NOT_SUPPORTED",
|
|
214
|
+
`Venue is not supported yet: ${venue}`,
|
|
215
|
+
{ venue },
|
|
216
|
+
"client",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
return await adapter.fetchServerTime();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw this.createServerTimeFetchError(venue, error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
169
227
|
async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
|
|
170
228
|
this.context.assertStarted();
|
|
171
229
|
const market = await this.resolveMarketDefinition(input);
|
|
@@ -438,51 +496,163 @@ export class MarketManagerImpl
|
|
|
438
496
|
return;
|
|
439
497
|
}
|
|
440
498
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
catalogPromise = this.fetchAndStoreMarketCatalog(venue);
|
|
444
|
-
this.catalogPromises.set(venue, catalogPromise);
|
|
445
|
-
}
|
|
499
|
+
await this.fetchCatalogCoalesced(venue);
|
|
500
|
+
}
|
|
446
501
|
|
|
502
|
+
private async reloadVenue(venue: Venue): Promise<MarketCatalogReloadSummary> {
|
|
447
503
|
try {
|
|
448
|
-
await
|
|
504
|
+
const result = await this.fetchCatalogCoalesced(venue);
|
|
505
|
+
return { ...result, ok: true };
|
|
449
506
|
} catch (error) {
|
|
450
|
-
|
|
507
|
+
if (
|
|
508
|
+
error instanceof AcexError &&
|
|
509
|
+
error.code === "MARKET_CATALOG_LOAD_FAILED"
|
|
510
|
+
) {
|
|
511
|
+
return {
|
|
512
|
+
venue,
|
|
513
|
+
added: [],
|
|
514
|
+
removed: [],
|
|
515
|
+
total: this.countVenueMarkets(venue),
|
|
516
|
+
ok: false,
|
|
517
|
+
error,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
451
521
|
throw error;
|
|
452
522
|
}
|
|
453
523
|
}
|
|
454
524
|
|
|
455
|
-
private async
|
|
525
|
+
private async fetchCatalogCoalesced(
|
|
526
|
+
venue: Venue,
|
|
527
|
+
): Promise<CatalogFetchResult> {
|
|
528
|
+
let catalogPromise = this.catalogPromises.get(venue);
|
|
529
|
+
if (!catalogPromise) {
|
|
530
|
+
catalogPromise = this.fetchAndStoreMarketCatalog(venue).finally(() => {
|
|
531
|
+
this.catalogPromises.delete(venue);
|
|
532
|
+
});
|
|
533
|
+
this.catalogPromises.set(venue, catalogPromise);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return await catalogPromise;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private async fetchAndStoreMarketCatalog(
|
|
540
|
+
venue: Venue,
|
|
541
|
+
): Promise<CatalogFetchResult> {
|
|
456
542
|
const adapter = this.getMarketAdapter(venue);
|
|
543
|
+
let markets: MarketDefinition[];
|
|
457
544
|
|
|
458
545
|
try {
|
|
459
|
-
|
|
546
|
+
markets = await adapter.loadMarkets();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
throw this.createCatalogLoadError(venue, error);
|
|
549
|
+
}
|
|
460
550
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
551
|
+
const mismatchedMarket = markets.find((market) => market.venue !== venue);
|
|
552
|
+
if (mismatchedMarket) {
|
|
553
|
+
throw this.createCatalogLoadError(
|
|
554
|
+
venue,
|
|
555
|
+
new Error(
|
|
556
|
+
`Market catalog from ${venue} included ${mismatchedMarket.venue} market: ${mismatchedMarket.symbol}`,
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const previousKeys = this.getVenueMarketKeys(venue);
|
|
562
|
+
|
|
563
|
+
for (const [key, market] of this.definitions) {
|
|
564
|
+
if (market.venue === venue) {
|
|
565
|
+
this.definitions.delete(key);
|
|
465
566
|
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
for (const market of markets) {
|
|
570
|
+
this.definitions.set(marketKey(market), market);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.loadedCatalogVenues.add(venue);
|
|
466
574
|
|
|
467
|
-
|
|
468
|
-
|
|
575
|
+
const currentKeys = this.getVenueMarketKeys(venue);
|
|
576
|
+
return {
|
|
577
|
+
venue,
|
|
578
|
+
added: this.diffMarketSymbols(venue, currentKeys, previousKeys),
|
|
579
|
+
removed: this.diffMarketSymbols(venue, previousKeys, currentKeys),
|
|
580
|
+
total: currentKeys.size,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private getVenueMarketKeys(venue: Venue): Set<string> {
|
|
585
|
+
const keys = new Set<string>();
|
|
586
|
+
|
|
587
|
+
for (const [key, market] of this.definitions) {
|
|
588
|
+
if (market.venue === venue) {
|
|
589
|
+
keys.add(key);
|
|
469
590
|
}
|
|
591
|
+
}
|
|
470
592
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
593
|
+
return keys;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private countVenueMarkets(venue: Venue): number {
|
|
597
|
+
return this.getVenueMarketKeys(venue).size;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private diffMarketSymbols(
|
|
601
|
+
venue: Venue,
|
|
602
|
+
left: Set<string>,
|
|
603
|
+
right: Set<string>,
|
|
604
|
+
): string[] {
|
|
605
|
+
const prefix = `${venue}:`;
|
|
606
|
+
return [...left]
|
|
607
|
+
.filter((key) => !right.has(key))
|
|
608
|
+
.map((key) => key.slice(prefix.length))
|
|
609
|
+
.sort((leftSymbol, rightSymbol) => leftSymbol.localeCompare(rightSymbol));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private createCatalogLoadError(venue: Venue, error: unknown): AcexError {
|
|
613
|
+
const details = buildAcexErrorDetails({ venue }, error);
|
|
614
|
+
const wrapped = new AcexError(
|
|
615
|
+
"MARKET_CATALOG_LOAD_FAILED",
|
|
616
|
+
formatAcexErrorMessage(
|
|
475
617
|
`Failed to load market catalog from ${venue}`,
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
error
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
618
|
+
details,
|
|
619
|
+
),
|
|
620
|
+
{
|
|
621
|
+
cause: error,
|
|
622
|
+
details,
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
this.context.publishRuntimeError(
|
|
626
|
+
"adapter",
|
|
627
|
+
error instanceof Error
|
|
628
|
+
? error
|
|
629
|
+
: new Error("Unknown catalog load failure"),
|
|
630
|
+
{ venue },
|
|
631
|
+
);
|
|
632
|
+
return wrapped;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private createServerTimeFetchError(venue: Venue, error: unknown): AcexError {
|
|
636
|
+
const details = buildAcexErrorDetails({ venue }, error);
|
|
637
|
+
const wrapped = new AcexError(
|
|
638
|
+
"MARKET_SERVER_TIME_FETCH_FAILED",
|
|
639
|
+
formatAcexErrorMessage(
|
|
640
|
+
`Failed to fetch server time from ${venue}`,
|
|
641
|
+
details,
|
|
642
|
+
),
|
|
643
|
+
{
|
|
644
|
+
cause: error,
|
|
645
|
+
details,
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
this.context.publishRuntimeError(
|
|
649
|
+
"adapter",
|
|
650
|
+
error instanceof Error
|
|
651
|
+
? error
|
|
652
|
+
: new Error("Unknown server time fetch failure"),
|
|
653
|
+
{ venue },
|
|
654
|
+
);
|
|
655
|
+
return wrapped;
|
|
486
656
|
}
|
|
487
657
|
|
|
488
658
|
private async resolveMarketDefinition(input: {
|
|
@@ -608,11 +778,20 @@ export class MarketManagerImpl
|
|
|
608
778
|
|
|
609
779
|
try {
|
|
610
780
|
await record.l1BookStream.ready;
|
|
611
|
-
} catch {
|
|
781
|
+
} catch (error) {
|
|
782
|
+
record.l1BookStream.close();
|
|
612
783
|
record.l1BookStream = undefined;
|
|
784
|
+
const details = buildAcexErrorDetails(
|
|
785
|
+
{ venue: market.venue, symbol: market.symbol },
|
|
786
|
+
error,
|
|
787
|
+
);
|
|
613
788
|
const timeoutError = new AcexError(
|
|
614
789
|
"MARKET_STREAM_TIMEOUT",
|
|
615
790
|
`Timed out waiting for market data: ${market.symbol}`,
|
|
791
|
+
{
|
|
792
|
+
cause: error,
|
|
793
|
+
details,
|
|
794
|
+
},
|
|
616
795
|
);
|
|
617
796
|
this.context.publishRuntimeError("runtime", timeoutError, {
|
|
618
797
|
venue: market.venue,
|
|
@@ -636,11 +815,20 @@ export class MarketManagerImpl
|
|
|
636
815
|
|
|
637
816
|
try {
|
|
638
817
|
await record.fundingRateStream.ready;
|
|
639
|
-
} catch {
|
|
818
|
+
} catch (error) {
|
|
819
|
+
record.fundingRateStream.close();
|
|
640
820
|
record.fundingRateStream = undefined;
|
|
821
|
+
const details = buildAcexErrorDetails(
|
|
822
|
+
{ venue: market.venue, symbol: market.symbol },
|
|
823
|
+
error,
|
|
824
|
+
);
|
|
641
825
|
const timeoutError = new AcexError(
|
|
642
826
|
"MARKET_STREAM_TIMEOUT",
|
|
643
827
|
`Timed out waiting for market data: ${market.symbol}`,
|
|
828
|
+
{
|
|
829
|
+
cause: error,
|
|
830
|
+
details,
|
|
831
|
+
},
|
|
644
832
|
);
|
|
645
833
|
this.context.publishRuntimeError("runtime", timeoutError, {
|
|
646
834
|
venue: market.venue,
|
|
@@ -1039,7 +1227,9 @@ export class MarketManagerImpl
|
|
|
1039
1227
|
metadata?: { venue?: Venue; symbol?: string },
|
|
1040
1228
|
source: "market" | "client" = "market",
|
|
1041
1229
|
): AcexError {
|
|
1042
|
-
const error = new AcexError(code, message
|
|
1230
|
+
const error = new AcexError(code, message, {
|
|
1231
|
+
details: buildAcexErrorDetails(metadata),
|
|
1232
|
+
});
|
|
1043
1233
|
this.context.publishRuntimeError(source, error, metadata);
|
|
1044
1234
|
return error;
|
|
1045
1235
|
}
|