@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.
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +131 -12
- package/src/adapters/binance/stream-protocol.ts +202 -0
- package/src/client/runtime.ts +1 -1
- package/src/internal/managed-websocket.ts +8 -0
- package/src/internal/subscription-multiplexer.ts +747 -0
- package/src/managers/market-manager.ts +63 -25
- package/src/adapters/binance/book-ticker.ts +0 -123
- package/src/adapters/binance/mark-price.ts +0 -126
|
@@ -110,25 +110,27 @@ export class MarketManagerImpl
|
|
|
110
110
|
readonly events: MarketEventStreams;
|
|
111
111
|
|
|
112
112
|
private readonly context: ClientContext;
|
|
113
|
-
private readonly
|
|
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
|
|
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
|
-
|
|
129
|
+
adapters: Map<Venue, MarketAdapter>,
|
|
128
130
|
options: MarketManagerOptions = {},
|
|
129
131
|
) {
|
|
130
132
|
this.context = context;
|
|
131
|
-
this.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
697
|
+
now: this.streamNow,
|
|
668
698
|
};
|
|
669
699
|
|
|
670
|
-
return this.
|
|
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:
|
|
758
|
+
now: this.streamNow,
|
|
725
759
|
};
|
|
726
760
|
|
|
727
|
-
return this.
|
|
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
|
-
}
|