@imbingox/acex 0.3.0-beta.6 → 0.3.1-beta.0

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.
@@ -110,25 +110,27 @@ export class MarketManagerImpl
110
110
  readonly events: MarketEventStreams;
111
111
 
112
112
  private readonly context: ClientContext;
113
- private readonly adapter: MarketAdapter;
113
+ private readonly adapters: Map<Venue, MarketAdapter>;
114
114
  private readonly marketBus = new AsyncEventBus<MarketEvent>();
115
115
  private readonly marketStatusBus =
116
116
  new AsyncEventBus<MarketStatusChangedEvent>();
117
117
  private readonly definitions = new Map<string, MarketDefinition>();
118
118
  private readonly records = new Map<string, MarketRecord>();
119
- private catalogPromise: Promise<void> | undefined;
119
+ private readonly loadedCatalogVenues = new Set<Venue>();
120
+ private readonly catalogPromises = new Map<Venue, Promise<void>>();
120
121
  private readonly initialL1TimeoutMs: number;
121
122
  private readonly l1StaleAfterMs: number;
122
123
  private readonly l1ReconnectDelayMs: number;
123
124
  private readonly l1ReconnectMaxDelayMs: number;
125
+ private readonly streamNow = (): number => this.context.now();
124
126
 
125
127
  constructor(
126
128
  context: ClientContext,
127
- adapter: MarketAdapter,
129
+ adapters: Map<Venue, MarketAdapter>,
128
130
  options: MarketManagerOptions = {},
129
131
  ) {
130
132
  this.context = context;
131
- this.adapter = adapter;
133
+ this.adapters = new Map(adapters);
132
134
  this.initialL1TimeoutMs =
133
135
  options.initialL1TimeoutMs ?? DEFAULT_INITIAL_L1_TIMEOUT_MS;
134
136
  this.l1StaleAfterMs = options.l1StaleAfterMs ?? DEFAULT_L1_STALE_AFTER_MS;
@@ -162,7 +164,9 @@ export class MarketManagerImpl
162
164
  // --- MarketManager public API ---
163
165
 
164
166
  async loadMarkets(): Promise<void> {
165
- await this.loadMarketCatalog();
167
+ await Promise.all(
168
+ [...this.adapters.keys()].map((venue) => this.loadMarketCatalog(venue)),
169
+ );
166
170
  }
167
171
 
168
172
  async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
@@ -414,43 +418,55 @@ export class MarketManagerImpl
414
418
 
415
419
  // --- Internal helpers ---
416
420
 
417
- private async loadMarketCatalog(): Promise<void> {
418
- if (this.definitions.size > 0) {
421
+ private async loadMarketCatalog(venue: Venue): Promise<void> {
422
+ this.assertSupportedVenue(venue);
423
+
424
+ if (this.loadedCatalogVenues.has(venue)) {
419
425
  return;
420
426
  }
421
427
 
422
- if (!this.catalogPromise) {
423
- this.catalogPromise = this.fetchAndStoreMarketCatalog();
428
+ let catalogPromise = this.catalogPromises.get(venue);
429
+ if (!catalogPromise) {
430
+ catalogPromise = this.fetchAndStoreMarketCatalog(venue);
431
+ this.catalogPromises.set(venue, catalogPromise);
424
432
  }
425
433
 
426
434
  try {
427
- await this.catalogPromise;
428
- } finally {
429
- if (this.definitions.size === 0) {
430
- this.catalogPromise = undefined;
431
- }
435
+ await catalogPromise;
436
+ } catch (error) {
437
+ this.catalogPromises.delete(venue);
438
+ throw error;
432
439
  }
433
440
  }
434
441
 
435
- private async fetchAndStoreMarketCatalog(): Promise<void> {
442
+ private async fetchAndStoreMarketCatalog(venue: Venue): Promise<void> {
443
+ const adapter = this.getMarketAdapter(venue);
444
+
436
445
  try {
437
- const markets = await this.adapter.loadMarkets();
438
- this.definitions.clear();
446
+ const markets = await adapter.loadMarkets();
447
+
448
+ for (const [key, market] of this.definitions) {
449
+ if (market.venue === venue) {
450
+ this.definitions.delete(key);
451
+ }
452
+ }
439
453
 
440
454
  for (const market of markets) {
441
455
  this.definitions.set(marketKey(market), market);
442
456
  }
457
+
458
+ this.loadedCatalogVenues.add(venue);
443
459
  } catch (error) {
444
460
  const wrapped = new AcexError(
445
461
  "MARKET_CATALOG_LOAD_FAILED",
446
- "Failed to load market catalog from Binance",
462
+ `Failed to load market catalog from ${venue}`,
447
463
  );
448
464
  this.context.publishRuntimeError(
449
465
  "adapter",
450
466
  error instanceof Error
451
467
  ? error
452
468
  : new Error("Unknown catalog load failure"),
453
- { venue: this.adapter.venue },
469
+ { venue },
454
470
  );
455
471
  throw wrapped;
456
472
  }
@@ -461,7 +477,7 @@ export class MarketManagerImpl
461
477
  symbol: string;
462
478
  }): Promise<MarketDefinition> {
463
479
  this.assertSupportedVenue(input.venue);
464
- await this.loadMarketCatalog();
480
+ await this.loadMarketCatalog(input.venue);
465
481
 
466
482
  const market = this.definitions.get(marketKey(input));
467
483
  if (!market) {
@@ -500,7 +516,7 @@ export class MarketManagerImpl
500
516
  }
501
517
 
502
518
  private assertSupportedVenue(venue: Venue): void {
503
- if (venue === this.adapter.venue) {
519
+ if (this.adapters.has(venue)) {
504
520
  return;
505
521
  }
506
522
 
@@ -512,6 +528,20 @@ export class MarketManagerImpl
512
528
  );
513
529
  }
514
530
 
531
+ private getMarketAdapter(venue: Venue): MarketAdapter {
532
+ const adapter = this.adapters.get(venue);
533
+ if (!adapter) {
534
+ throw this.createError(
535
+ "VENUE_NOT_SUPPORTED",
536
+ `Venue is not supported yet: ${venue}`,
537
+ { venue },
538
+ "client",
539
+ );
540
+ }
541
+
542
+ return adapter;
543
+ }
544
+
515
545
  private assertFundingRateSupported(market: MarketDefinition): void {
516
546
  if (market.contract && market.type === "swap") {
517
547
  return;
@@ -664,10 +694,14 @@ export class MarketManagerImpl
664
694
  staleAfterMs: this.l1StaleAfterMs,
665
695
  reconnectDelayMs: this.l1ReconnectDelayMs,
666
696
  reconnectMaxDelayMs: this.l1ReconnectMaxDelayMs,
667
- now: () => this.context.now(),
697
+ now: this.streamNow,
668
698
  };
669
699
 
670
- return this.adapter.createL1BookStream(market, callbacks, options);
700
+ return this.getMarketAdapter(market.venue).createL1BookStream(
701
+ market,
702
+ callbacks,
703
+ options,
704
+ );
671
705
  }
672
706
 
673
707
  private createFundingRateStream(
@@ -721,10 +755,14 @@ export class MarketManagerImpl
721
755
  staleAfterMs: this.l1StaleAfterMs,
722
756
  reconnectDelayMs: this.l1ReconnectDelayMs,
723
757
  reconnectMaxDelayMs: this.l1ReconnectMaxDelayMs,
724
- now: () => this.context.now(),
758
+ now: this.streamNow,
725
759
  };
726
760
 
727
- return this.adapter.createFundingRateStream(market, callbacks, options);
761
+ return this.getMarketAdapter(market.venue).createFundingRateStream(
762
+ market,
763
+ callbacks,
764
+ options,
765
+ );
728
766
  }
729
767
 
730
768
  private createL1Book(
@@ -1,123 +0,0 @@
1
- import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
2
- import type { BinanceMarketDefinition } from "./market-catalog.ts";
3
-
4
- export interface BinanceL1BookUpdate {
5
- bidPrice: string;
6
- bidSize: string;
7
- askPrice: string;
8
- askSize: string;
9
- exchangeTs?: number;
10
- receivedAt: number;
11
- }
12
-
13
- export interface BinanceBookTickerSubscription {
14
- readonly ready: Promise<void>;
15
- close(): void;
16
- }
17
-
18
- export interface BinanceBookTickerCallbacks {
19
- onBookTicker(update: BinanceL1BookUpdate): void;
20
- onFreshnessChange(
21
- freshness: "fresh" | "stale",
22
- reason?: "heartbeat_timeout",
23
- ): void;
24
- onDisconnected(): void;
25
- onError?(error: Error): void;
26
- }
27
-
28
- export interface BinanceBookTickerOptions {
29
- initialMessageTimeoutMs: number;
30
- staleAfterMs: number;
31
- reconnectDelayMs: number;
32
- reconnectMaxDelayMs: number;
33
- now?: () => number;
34
- }
35
-
36
- interface BinanceBookTickerMessage {
37
- b?: string;
38
- B?: string;
39
- a?: string;
40
- A?: string;
41
- T?: number;
42
- }
43
-
44
- const BINANCE_SPOT_WS_BASE_URL = "wss://stream.binance.com:9443/ws";
45
- const BINANCE_USDM_WS_BASE_URL = "wss://fstream.binance.com/ws";
46
- const BINANCE_COINM_WS_BASE_URL = "wss://dstream.binance.com/ws";
47
-
48
- function getWsBaseUrl(market: BinanceMarketDefinition): string {
49
- switch (market.family) {
50
- case "spot":
51
- return BINANCE_SPOT_WS_BASE_URL;
52
- case "usdm":
53
- return BINANCE_USDM_WS_BASE_URL;
54
- case "coinm":
55
- return BINANCE_COINM_WS_BASE_URL;
56
- }
57
- }
58
-
59
- function buildBookTickerUrl(market: BinanceMarketDefinition): string {
60
- return `${getWsBaseUrl(market)}/${market.id.toLowerCase()}@bookTicker`;
61
- }
62
-
63
- function parseBookTickerMessage(
64
- data: string,
65
- ): BinanceBookTickerMessage | undefined {
66
- const parsed = JSON.parse(data) as BinanceBookTickerMessage;
67
- if (!parsed.b || !parsed.B || !parsed.a || !parsed.A) {
68
- return undefined;
69
- }
70
-
71
- return parsed;
72
- }
73
-
74
- export function subscribeBinanceBookTicker(
75
- market: BinanceMarketDefinition,
76
- callbacks: BinanceBookTickerCallbacks,
77
- options: BinanceBookTickerOptions,
78
- ): BinanceBookTickerSubscription {
79
- const session = createManagedWebSocket<BinanceBookTickerMessage>({
80
- url: buildBookTickerUrl(market),
81
- initialMessageTimeoutMs: options.initialMessageTimeoutMs,
82
- now: options.now,
83
- messageWatchdog: {
84
- staleAfterMs: options.staleAfterMs,
85
- onStale() {
86
- callbacks.onFreshnessChange("stale", "heartbeat_timeout");
87
- },
88
- },
89
- reconnect: {
90
- initialDelayMs: options.reconnectDelayMs,
91
- maxDelayMs: options.reconnectMaxDelayMs,
92
- },
93
- parseMessage: parseBookTickerMessage,
94
- onMessage(message, receivedAt) {
95
- if (!message.b || !message.B || !message.a || !message.A) {
96
- return;
97
- }
98
-
99
- callbacks.onBookTicker({
100
- bidPrice: message.b,
101
- bidSize: message.B,
102
- askPrice: message.a,
103
- askSize: message.A,
104
- exchangeTs: message.T,
105
- receivedAt,
106
- });
107
- callbacks.onFreshnessChange("fresh");
108
- },
109
- onUnexpectedClose() {
110
- callbacks.onDisconnected();
111
- },
112
- onError() {
113
- callbacks.onError?.(new Error(`WebSocket error for ${market.symbol}`));
114
- },
115
- });
116
-
117
- return {
118
- ready: session.ready,
119
- close() {
120
- session.close();
121
- },
122
- };
123
- }
@@ -1,126 +0,0 @@
1
- import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
2
- import type { BinanceMarketDefinition } from "./market-catalog.ts";
3
-
4
- export interface BinanceFundingRateUpdate {
5
- fundingRate: string;
6
- nextFundingTime?: number;
7
- markPrice?: string;
8
- indexPrice?: string;
9
- exchangeTs?: number;
10
- receivedAt: number;
11
- }
12
-
13
- export interface BinanceMarkPriceSubscription {
14
- readonly ready: Promise<void>;
15
- close(): void;
16
- }
17
-
18
- export interface BinanceMarkPriceCallbacks {
19
- onFundingRate(update: BinanceFundingRateUpdate): void;
20
- onFreshnessChange(
21
- freshness: "fresh" | "stale",
22
- reason?: "heartbeat_timeout",
23
- ): void;
24
- onDisconnected(): void;
25
- onError?(error: Error): void;
26
- }
27
-
28
- export interface BinanceMarkPriceOptions {
29
- initialMessageTimeoutMs: number;
30
- staleAfterMs: number;
31
- reconnectDelayMs: number;
32
- reconnectMaxDelayMs: number;
33
- now?: () => number;
34
- }
35
-
36
- interface BinanceMarkPriceMessage {
37
- e?: string;
38
- E?: number;
39
- s?: string;
40
- p?: string;
41
- i?: string;
42
- r?: string;
43
- T?: number;
44
- }
45
-
46
- const BINANCE_USDM_MARKET_WS_BASE_URL = "wss://fstream.binance.com/market/ws";
47
- const BINANCE_COINM_WS_BASE_URL = "wss://dstream.binance.com/ws";
48
-
49
- function getWsBaseUrl(market: BinanceMarketDefinition): string {
50
- switch (market.family) {
51
- case "usdm":
52
- return BINANCE_USDM_MARKET_WS_BASE_URL;
53
- case "coinm":
54
- return BINANCE_COINM_WS_BASE_URL;
55
- case "spot":
56
- throw new Error(
57
- `Funding rate is not supported for spot market: ${market.symbol}`,
58
- );
59
- }
60
- }
61
-
62
- function buildMarkPriceUrl(market: BinanceMarketDefinition): string {
63
- return `${getWsBaseUrl(market)}/${market.id.toLowerCase()}@markPrice`;
64
- }
65
-
66
- function parseMarkPriceMessage(
67
- data: string,
68
- ): BinanceMarkPriceMessage | undefined {
69
- const parsed = JSON.parse(data) as BinanceMarkPriceMessage;
70
- if (!parsed.r) {
71
- return undefined;
72
- }
73
-
74
- return parsed;
75
- }
76
-
77
- export function subscribeBinanceMarkPrice(
78
- market: BinanceMarketDefinition,
79
- callbacks: BinanceMarkPriceCallbacks,
80
- options: BinanceMarkPriceOptions,
81
- ): BinanceMarkPriceSubscription {
82
- const session = createManagedWebSocket<BinanceMarkPriceMessage>({
83
- url: buildMarkPriceUrl(market),
84
- initialMessageTimeoutMs: options.initialMessageTimeoutMs,
85
- now: options.now,
86
- messageWatchdog: {
87
- staleAfterMs: options.staleAfterMs,
88
- onStale() {
89
- callbacks.onFreshnessChange("stale", "heartbeat_timeout");
90
- },
91
- },
92
- reconnect: {
93
- initialDelayMs: options.reconnectDelayMs,
94
- maxDelayMs: options.reconnectMaxDelayMs,
95
- },
96
- parseMessage: parseMarkPriceMessage,
97
- onMessage(message, receivedAt) {
98
- if (!message.r) {
99
- return;
100
- }
101
-
102
- callbacks.onFundingRate({
103
- fundingRate: message.r,
104
- nextFundingTime: message.T,
105
- markPrice: message.p,
106
- indexPrice: message.i,
107
- exchangeTs: message.E,
108
- receivedAt,
109
- });
110
- callbacks.onFreshnessChange("fresh");
111
- },
112
- onUnexpectedClose() {
113
- callbacks.onDisconnected();
114
- },
115
- onError() {
116
- callbacks.onError?.(new Error(`WebSocket error for ${market.symbol}`));
117
- },
118
- });
119
-
120
- return {
121
- ready: session.ready,
122
- close() {
123
- session.close();
124
- },
125
- };
126
- }