@imbingox/acex 0.1.0-beta.0 → 0.1.0-beta.1

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.
Files changed (64) hide show
  1. package/README.md +12 -54
  2. package/index.ts +1 -0
  3. package/package.json +15 -24
  4. package/src/adapters/binance/adapter.ts +53 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/market-catalog.ts +251 -0
  7. package/src/adapters/types.ts +43 -0
  8. package/src/client/context.ts +60 -0
  9. package/src/client/create-client.ts +6 -0
  10. package/src/client/runtime.ts +283 -0
  11. package/src/errors.ts +20 -0
  12. package/src/index.ts +4 -0
  13. package/src/internal/async-event-bus.ts +100 -0
  14. package/src/internal/filters.ts +119 -0
  15. package/src/internal/managed-websocket.ts +258 -0
  16. package/src/managers/account-manager.ts +315 -0
  17. package/src/managers/market-manager.ts +642 -0
  18. package/src/managers/order-manager.ts +304 -0
  19. package/src/types/account.ts +160 -0
  20. package/src/types/client.ts +79 -0
  21. package/src/types/index.ts +5 -0
  22. package/src/types/market.ts +136 -0
  23. package/src/types/order.ts +142 -0
  24. package/src/types/shared.ts +78 -0
  25. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  26. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  27. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -179
  28. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -537
  29. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  30. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  31. package/dist/adapters/types.d.ts +0 -210
  32. package/dist/adapters/types.js +0 -1
  33. package/dist/core/client.d.ts +0 -37
  34. package/dist/core/client.js +0 -45
  35. package/dist/core/recovery.d.ts +0 -22
  36. package/dist/core/recovery.js +0 -18
  37. package/dist/core/runtime.d.ts +0 -26
  38. package/dist/core/runtime.js +0 -150
  39. package/dist/errors/acex-error.d.ts +0 -25
  40. package/dist/errors/acex-error.js +0 -54
  41. package/dist/index.d.ts +0 -5
  42. package/dist/index.js +0 -3
  43. package/dist/managers/account-manager.d.ts +0 -41
  44. package/dist/managers/account-manager.js +0 -80
  45. package/dist/managers/market-manager.d.ts +0 -16
  46. package/dist/managers/market-manager.js +0 -28
  47. package/dist/managers/order-manager.d.ts +0 -87
  48. package/dist/managers/order-manager.js +0 -122
  49. package/dist/runtime/async-queue.d.ts +0 -8
  50. package/dist/runtime/async-queue.js +0 -88
  51. package/dist/runtime/request-id.d.ts +0 -1
  52. package/dist/runtime/request-id.js +0 -5
  53. package/dist/store/account-store.d.ts +0 -52
  54. package/dist/store/account-store.js +0 -18
  55. package/dist/store/health-store.d.ts +0 -16
  56. package/dist/store/health-store.js +0 -29
  57. package/dist/store/market-store.d.ts +0 -42
  58. package/dist/store/market-store.js +0 -51
  59. package/dist/store/order-store.d.ts +0 -38
  60. package/dist/store/order-store.js +0 -49
  61. package/dist/testing/create-fake-runtime.d.ts +0 -5
  62. package/dist/testing/create-fake-runtime.js +0 -7
  63. package/dist/types/public.d.ts +0 -11
  64. package/dist/types/public.js +0 -1
@@ -0,0 +1,642 @@
1
+ import type {
2
+ L1BookStreamCallbacks,
3
+ L1BookStreamOptions,
4
+ MarketAdapter,
5
+ RawL1BookUpdate,
6
+ StreamHandle,
7
+ } from "../adapters/types.ts";
8
+ import type {
9
+ ClientContext,
10
+ HealthReporter,
11
+ ManagerLifecycle,
12
+ } from "../client/context.ts";
13
+ import { AcexError } from "../errors.ts";
14
+ import { AsyncEventBus } from "../internal/async-event-bus.ts";
15
+ import { matchesMarketFilter } from "../internal/filters.ts";
16
+ import type {
17
+ Exchange,
18
+ FundingRateSnapshot,
19
+ FundingRateUpdatedEvent,
20
+ L1Book,
21
+ L1BookUpdatedEvent,
22
+ MarketDataStatus,
23
+ MarketDefinition,
24
+ MarketEvent,
25
+ MarketEventStreams,
26
+ MarketKeyInput,
27
+ MarketManager,
28
+ MarketStatusChangedEvent,
29
+ SubscribeFundingRateInput,
30
+ SubscribeL1BookInput,
31
+ } from "../types/index.ts";
32
+
33
+ export interface MarketManagerOptions {
34
+ initialL1TimeoutMs?: number;
35
+ l1StaleAfterMs?: number;
36
+ l1ReconnectDelayMs?: number;
37
+ l1ReconnectMaxDelayMs?: number;
38
+ }
39
+
40
+ interface MarketRecord {
41
+ exchange: Exchange;
42
+ symbol: string;
43
+ market?: MarketDefinition;
44
+ l1Book?: L1Book;
45
+ fundingRate?: FundingRateSnapshot;
46
+ l1BookSubscribed: boolean;
47
+ fundingRateSubscribed: boolean;
48
+ status: MarketDataStatus;
49
+ l1BookStream?: StreamHandle;
50
+ }
51
+
52
+ const DEFAULT_INITIAL_L1_TIMEOUT_MS = 15_000;
53
+ const DEFAULT_L1_STALE_AFTER_MS = 15_000;
54
+ const DEFAULT_L1_RECONNECT_DELAY_MS = 1_000;
55
+ const DEFAULT_L1_RECONNECT_MAX_DELAY_MS = 10_000;
56
+
57
+ function marketKey(input: MarketKeyInput): string {
58
+ return `${input.exchange}:${input.symbol}`;
59
+ }
60
+
61
+ function cloneMarketStatus(status: MarketDataStatus): MarketDataStatus {
62
+ return { ...status };
63
+ }
64
+
65
+ function cloneMarketDefinition(definition: MarketDefinition): MarketDefinition {
66
+ return { ...definition, raw: { ...definition.raw } };
67
+ }
68
+
69
+ export class MarketManagerImpl
70
+ implements MarketManager, ManagerLifecycle, HealthReporter<MarketDataStatus>
71
+ {
72
+ readonly events: MarketEventStreams;
73
+
74
+ private readonly context: ClientContext;
75
+ private readonly adapter: MarketAdapter;
76
+ private readonly marketBus = new AsyncEventBus<MarketEvent>();
77
+ private readonly marketStatusBus =
78
+ new AsyncEventBus<MarketStatusChangedEvent>();
79
+ private readonly definitions = new Map<string, MarketDefinition>();
80
+ private readonly records = new Map<string, MarketRecord>();
81
+ private catalogPromise: Promise<void> | undefined;
82
+ private readonly initialL1TimeoutMs: number;
83
+ private readonly l1StaleAfterMs: number;
84
+ private readonly l1ReconnectDelayMs: number;
85
+ private readonly l1ReconnectMaxDelayMs: number;
86
+
87
+ constructor(
88
+ context: ClientContext,
89
+ adapter: MarketAdapter,
90
+ options: MarketManagerOptions = {},
91
+ ) {
92
+ this.context = context;
93
+ this.adapter = adapter;
94
+ this.initialL1TimeoutMs =
95
+ options.initialL1TimeoutMs ?? DEFAULT_INITIAL_L1_TIMEOUT_MS;
96
+ this.l1StaleAfterMs = options.l1StaleAfterMs ?? DEFAULT_L1_STALE_AFTER_MS;
97
+ this.l1ReconnectDelayMs =
98
+ options.l1ReconnectDelayMs ?? DEFAULT_L1_RECONNECT_DELAY_MS;
99
+ this.l1ReconnectMaxDelayMs =
100
+ options.l1ReconnectMaxDelayMs ?? DEFAULT_L1_RECONNECT_MAX_DELAY_MS;
101
+
102
+ this.events = {
103
+ all: (filter) =>
104
+ this.marketBus.stream((event) => matchesMarketFilter(event, filter)),
105
+ fundingRateUpdates: (filter) =>
106
+ this.marketBus.stream(
107
+ (event): event is FundingRateUpdatedEvent =>
108
+ event.type === "funding_rate.updated" &&
109
+ matchesMarketFilter(event, filter),
110
+ ),
111
+ l1BookUpdates: (filter) =>
112
+ this.marketBus.stream(
113
+ (event): event is L1BookUpdatedEvent =>
114
+ event.type === "l1_book.updated" &&
115
+ matchesMarketFilter(event, filter),
116
+ ),
117
+ status: (filter) =>
118
+ this.marketStatusBus.stream((event) =>
119
+ matchesMarketFilter(event, filter),
120
+ ),
121
+ };
122
+ }
123
+
124
+ // --- MarketManager public API ---
125
+
126
+ async loadMarkets(): Promise<void> {
127
+ await this.loadMarketCatalog();
128
+ }
129
+
130
+ async subscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
131
+ this.context.assertStarted();
132
+ const market = await this.resolveMarketDefinition(input);
133
+ const record = this.getOrCreateRecord({
134
+ exchange: input.exchange,
135
+ symbol: market.symbol,
136
+ });
137
+
138
+ record.market = market;
139
+ record.l1BookSubscribed = true;
140
+ record.status = {
141
+ ...record.status,
142
+ activity: "active",
143
+ ready: Boolean(record.l1Book),
144
+ freshness: record.l1Book ? "stale" : undefined,
145
+ reason: undefined,
146
+ inactiveSince: undefined,
147
+ };
148
+ this.publishStatus(record);
149
+
150
+ await this.ensureL1BookStream(record, market);
151
+ }
152
+
153
+ async unsubscribeL1Book(input: SubscribeL1BookInput): Promise<void> {
154
+ const record = this.records.get(marketKey(input));
155
+ if (!record?.l1BookSubscribed) {
156
+ return;
157
+ }
158
+
159
+ record.l1BookStream?.close();
160
+ record.l1BookStream = undefined;
161
+ record.l1BookSubscribed = false;
162
+ this.updateActivity(record);
163
+ }
164
+
165
+ async subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void> {
166
+ this.context.assertStarted();
167
+ const record = this.getOrCreateRecord(input);
168
+ const fundingRate =
169
+ record.fundingRate ??
170
+ this.createFundingRate(input.exchange, input.symbol, record.fundingRate);
171
+
172
+ if (!record.fundingRateSubscribed) {
173
+ record.fundingRateSubscribed = true;
174
+ record.fundingRate = fundingRate;
175
+ }
176
+
177
+ record.status = {
178
+ ...record.status,
179
+ activity: "active",
180
+ ready: true,
181
+ freshness: "fresh",
182
+ lastReceivedAt: fundingRate.receivedAt,
183
+ lastReadyAt: fundingRate.updatedAt,
184
+ inactiveSince: undefined,
185
+ };
186
+
187
+ const event: FundingRateUpdatedEvent = {
188
+ type: "funding_rate.updated",
189
+ exchange: record.exchange,
190
+ symbol: record.symbol,
191
+ snapshot: fundingRate,
192
+ ts: this.context.now(),
193
+ };
194
+
195
+ this.publishMarketEvent(event);
196
+ this.publishStatus(record);
197
+ }
198
+
199
+ async unsubscribeFundingRate(
200
+ input: SubscribeFundingRateInput,
201
+ ): Promise<void> {
202
+ const record = this.records.get(marketKey(input));
203
+ if (!record?.fundingRateSubscribed) {
204
+ return;
205
+ }
206
+
207
+ record.fundingRateSubscribed = false;
208
+ this.updateActivity(record);
209
+ }
210
+
211
+ getMarket(symbol: string): MarketDefinition | undefined {
212
+ const market = this.definitions.get(symbol);
213
+ return market ? cloneMarketDefinition(market) : undefined;
214
+ }
215
+
216
+ listMarkets(): MarketDefinition[] {
217
+ return [...this.definitions.values()]
218
+ .sort((left, right) => left.symbol.localeCompare(right.symbol))
219
+ .map((market) => cloneMarketDefinition(market));
220
+ }
221
+
222
+ getL1Book(key: MarketKeyInput): L1Book | undefined {
223
+ return this.records.get(marketKey(key))?.l1Book;
224
+ }
225
+
226
+ getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined {
227
+ return this.records.get(marketKey(key))?.fundingRate;
228
+ }
229
+
230
+ getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined {
231
+ const status = this.records.get(marketKey(key))?.status;
232
+ return status ? cloneMarketStatus(status) : undefined;
233
+ }
234
+
235
+ // --- ManagerLifecycle ---
236
+
237
+ onClientStarted(): void {
238
+ const now = this.context.now();
239
+
240
+ for (const record of this.records.values()) {
241
+ if (!record.l1BookSubscribed && !record.fundingRateSubscribed) {
242
+ continue;
243
+ }
244
+
245
+ record.status = {
246
+ ...record.status,
247
+ activity: "active",
248
+ ready: Boolean(record.l1Book || record.fundingRate),
249
+ freshness: record.l1Book
250
+ ? "stale"
251
+ : record.status.ready
252
+ ? "fresh"
253
+ : undefined,
254
+ reason: undefined,
255
+ lastReadyAt: record.status.lastReadyAt ?? now,
256
+ lastReceivedAt: record.status.lastReceivedAt ?? now,
257
+ inactiveSince: undefined,
258
+ };
259
+ this.publishStatus(record);
260
+ }
261
+
262
+ void this.resumeStreams();
263
+ }
264
+
265
+ onClientStopping(now: number): void {
266
+ for (const record of this.records.values()) {
267
+ if (!record.l1BookSubscribed && !record.fundingRateSubscribed) {
268
+ continue;
269
+ }
270
+
271
+ record.l1BookStream?.close();
272
+ record.l1BookStream = undefined;
273
+
274
+ record.status = {
275
+ ...record.status,
276
+ activity: "inactive",
277
+ inactiveSince: now,
278
+ freshness: record.l1Book ? "stale" : undefined,
279
+ };
280
+ this.publishStatus(record);
281
+ }
282
+ }
283
+
284
+ // --- HealthReporter ---
285
+
286
+ getStatuses(): MarketDataStatus[] {
287
+ return [...this.records.values()]
288
+ .map((record) => cloneMarketStatus(record.status))
289
+ .sort((left, right) =>
290
+ `${left.exchange}:${left.symbol}`.localeCompare(
291
+ `${right.exchange}:${right.symbol}`,
292
+ ),
293
+ );
294
+ }
295
+
296
+ // --- Internal helpers ---
297
+
298
+ private async loadMarketCatalog(): Promise<void> {
299
+ if (this.definitions.size > 0) {
300
+ return;
301
+ }
302
+
303
+ if (!this.catalogPromise) {
304
+ this.catalogPromise = this.fetchAndStoreMarketCatalog();
305
+ }
306
+
307
+ try {
308
+ await this.catalogPromise;
309
+ } finally {
310
+ if (this.definitions.size === 0) {
311
+ this.catalogPromise = undefined;
312
+ }
313
+ }
314
+ }
315
+
316
+ private async fetchAndStoreMarketCatalog(): Promise<void> {
317
+ try {
318
+ const markets = await this.adapter.loadMarkets();
319
+ this.definitions.clear();
320
+
321
+ for (const market of markets) {
322
+ this.definitions.set(market.symbol, market);
323
+ }
324
+ } catch (error) {
325
+ const wrapped = new AcexError(
326
+ "MARKET_CATALOG_LOAD_FAILED",
327
+ "Failed to load market catalog from Binance",
328
+ );
329
+ this.context.publishRuntimeError(
330
+ "adapter",
331
+ error instanceof Error
332
+ ? error
333
+ : new Error("Unknown catalog load failure"),
334
+ { exchange: this.adapter.exchange },
335
+ );
336
+ throw wrapped;
337
+ }
338
+ }
339
+
340
+ private async resolveMarketDefinition(input: {
341
+ exchange: Exchange;
342
+ symbol: string;
343
+ }): Promise<MarketDefinition> {
344
+ this.assertSupportedExchange(input.exchange);
345
+ await this.loadMarketCatalog();
346
+
347
+ const market = this.definitions.get(input.symbol);
348
+ if (!market) {
349
+ throw this.createError(
350
+ "MARKET_NOT_FOUND",
351
+ `Unknown market symbol: ${input.symbol}`,
352
+ { exchange: input.exchange, symbol: input.symbol },
353
+ "market",
354
+ );
355
+ }
356
+
357
+ if (!market.active) {
358
+ throw this.createError(
359
+ "MARKET_INACTIVE",
360
+ `Inactive market symbol: ${input.symbol}`,
361
+ { exchange: input.exchange, symbol: input.symbol },
362
+ "market",
363
+ );
364
+ }
365
+
366
+ return market;
367
+ }
368
+
369
+ private assertSupportedExchange(exchange: Exchange): void {
370
+ if (exchange === this.adapter.exchange) {
371
+ return;
372
+ }
373
+
374
+ throw this.createError(
375
+ "EXCHANGE_NOT_SUPPORTED",
376
+ `Exchange is not supported yet: ${exchange}`,
377
+ { exchange },
378
+ "client",
379
+ );
380
+ }
381
+
382
+ private getOrCreateRecord(input: {
383
+ exchange: Exchange;
384
+ symbol: string;
385
+ }): MarketRecord {
386
+ const key = marketKey(input);
387
+ const existing = this.records.get(key);
388
+ if (existing) {
389
+ return existing;
390
+ }
391
+
392
+ const record: MarketRecord = {
393
+ exchange: input.exchange,
394
+ symbol: input.symbol,
395
+ l1BookSubscribed: false,
396
+ fundingRateSubscribed: false,
397
+ status: {
398
+ exchange: input.exchange,
399
+ symbol: input.symbol,
400
+ activity: "inactive",
401
+ ready: false,
402
+ },
403
+ };
404
+
405
+ this.records.set(key, record);
406
+ return record;
407
+ }
408
+
409
+ private async ensureL1BookStream(
410
+ record: MarketRecord,
411
+ market: MarketDefinition,
412
+ ): Promise<void> {
413
+ if (record.l1BookStream) {
414
+ await record.l1BookStream.ready;
415
+ return;
416
+ }
417
+
418
+ record.l1BookStream = this.createL1BookStream(record, market);
419
+
420
+ try {
421
+ await record.l1BookStream.ready;
422
+ } catch {
423
+ record.l1BookStream = undefined;
424
+ const timeoutError = new AcexError(
425
+ "MARKET_STREAM_TIMEOUT",
426
+ `Timed out waiting for market data: ${market.symbol}`,
427
+ );
428
+ this.context.publishRuntimeError("runtime", timeoutError, {
429
+ exchange: market.exchange,
430
+ symbol: market.symbol,
431
+ });
432
+ this.updateConnectionState(
433
+ record,
434
+ "stale",
435
+ "ws_disconnected",
436
+ Boolean(record.l1Book),
437
+ );
438
+ throw timeoutError;
439
+ }
440
+ }
441
+
442
+ private createL1BookStream(
443
+ record: MarketRecord,
444
+ market: MarketDefinition,
445
+ ): StreamHandle {
446
+ const callbacks: L1BookStreamCallbacks = {
447
+ onUpdate: (update: RawL1BookUpdate) => {
448
+ record.l1Book = this.createL1Book(
449
+ record.exchange,
450
+ record.symbol,
451
+ update,
452
+ record.l1Book,
453
+ );
454
+ record.status = {
455
+ ...record.status,
456
+ activity: "active",
457
+ ready: true,
458
+ freshness: "fresh",
459
+ reason: undefined,
460
+ lastReceivedAt: record.l1Book.receivedAt,
461
+ lastReadyAt: record.l1Book.updatedAt,
462
+ inactiveSince: undefined,
463
+ };
464
+
465
+ const event: L1BookUpdatedEvent = {
466
+ type: "l1_book.updated",
467
+ exchange: record.exchange,
468
+ symbol: record.symbol,
469
+ snapshot: record.l1Book,
470
+ ts: this.context.now(),
471
+ };
472
+
473
+ this.publishMarketEvent(event);
474
+ this.publishStatus(record);
475
+ },
476
+ onFreshnessChange: (freshness, reason) => {
477
+ this.updateConnectionState(
478
+ record,
479
+ freshness,
480
+ reason,
481
+ Boolean(record.l1Book),
482
+ );
483
+ },
484
+ onDisconnected: () => {
485
+ this.updateConnectionState(
486
+ record,
487
+ "stale",
488
+ "ws_disconnected",
489
+ Boolean(record.l1Book),
490
+ );
491
+ },
492
+ onError: (error) => {
493
+ this.context.publishRuntimeError("runtime", error, {
494
+ exchange: record.exchange,
495
+ symbol: record.symbol,
496
+ });
497
+ },
498
+ };
499
+
500
+ const options: L1BookStreamOptions = {
501
+ initialMessageTimeoutMs: this.initialL1TimeoutMs,
502
+ staleAfterMs: this.l1StaleAfterMs,
503
+ reconnectDelayMs: this.l1ReconnectDelayMs,
504
+ reconnectMaxDelayMs: this.l1ReconnectMaxDelayMs,
505
+ now: () => this.context.now(),
506
+ };
507
+
508
+ return this.adapter.createL1BookStream(market, callbacks, options);
509
+ }
510
+
511
+ private createL1Book(
512
+ exchange: Exchange,
513
+ symbol: string,
514
+ input: RawL1BookUpdate,
515
+ previous?: L1Book,
516
+ ): L1Book {
517
+ return {
518
+ exchange,
519
+ symbol,
520
+ bidPrice: input.bidPrice,
521
+ bidSize: input.bidSize,
522
+ askPrice: input.askPrice,
523
+ askSize: input.askSize,
524
+ exchangeTs: input.exchangeTs,
525
+ receivedAt: input.receivedAt,
526
+ updatedAt: input.receivedAt,
527
+ version: (previous?.version ?? 0) + 1,
528
+ };
529
+ }
530
+
531
+ private createFundingRate(
532
+ exchange: Exchange,
533
+ symbol: string,
534
+ previous?: FundingRateSnapshot,
535
+ ): FundingRateSnapshot {
536
+ const now = this.context.now();
537
+
538
+ return {
539
+ exchange,
540
+ symbol,
541
+ fundingRate: previous?.fundingRate ?? "0",
542
+ nextFundingTime: previous?.nextFundingTime,
543
+ markPrice: previous?.markPrice,
544
+ indexPrice: previous?.indexPrice,
545
+ exchangeTs: now,
546
+ receivedAt: now,
547
+ updatedAt: now,
548
+ version: (previous?.version ?? 0) + 1,
549
+ };
550
+ }
551
+
552
+ private updateConnectionState(
553
+ record: MarketRecord,
554
+ freshness: "fresh" | "stale",
555
+ reason: MarketDataStatus["reason"],
556
+ ready: boolean,
557
+ ): void {
558
+ record.status = {
559
+ ...record.status,
560
+ activity: "active",
561
+ ready,
562
+ freshness,
563
+ reason,
564
+ inactiveSince: undefined,
565
+ };
566
+ this.publishStatus(record);
567
+ }
568
+
569
+ private updateActivity(record: MarketRecord): void {
570
+ if (record.l1BookSubscribed || record.fundingRateSubscribed) {
571
+ record.status = {
572
+ ...record.status,
573
+ activity: "active",
574
+ inactiveSince: undefined,
575
+ };
576
+ } else {
577
+ record.status = {
578
+ ...record.status,
579
+ activity: "inactive",
580
+ inactiveSince: this.context.now(),
581
+ };
582
+ }
583
+
584
+ this.publishStatus(record);
585
+ }
586
+
587
+ private publishMarketEvent(event: MarketEvent): void {
588
+ this.marketBus.publish(event);
589
+ }
590
+
591
+ private publishStatus(record: MarketRecord): void {
592
+ const event: MarketStatusChangedEvent = {
593
+ type: "market.status_changed",
594
+ exchange: record.exchange,
595
+ symbol: record.symbol,
596
+ status: cloneMarketStatus(record.status),
597
+ ts: this.context.now(),
598
+ };
599
+
600
+ this.marketStatusBus.publish(event);
601
+ this.marketBus.publish(event);
602
+ this.context.publishHealthEvent(event);
603
+ }
604
+
605
+ private async resumeStreams(): Promise<void> {
606
+ for (const record of this.records.values()) {
607
+ if (!record.l1BookSubscribed || record.l1BookStream) {
608
+ continue;
609
+ }
610
+
611
+ const market = record.market;
612
+ if (!market) {
613
+ continue;
614
+ }
615
+
616
+ try {
617
+ record.status = {
618
+ ...record.status,
619
+ activity: "active",
620
+ freshness: record.l1Book ? "stale" : undefined,
621
+ reason: undefined,
622
+ inactiveSince: undefined,
623
+ };
624
+ this.publishStatus(record);
625
+ await this.ensureL1BookStream(record, market);
626
+ } catch {
627
+ // Errors are already published through the runtime error bus.
628
+ }
629
+ }
630
+ }
631
+
632
+ private createError(
633
+ code: "MARKET_NOT_FOUND" | "MARKET_INACTIVE" | "EXCHANGE_NOT_SUPPORTED",
634
+ message: string,
635
+ metadata?: { exchange?: Exchange; symbol?: string },
636
+ source: "market" | "client" = "market",
637
+ ): AcexError {
638
+ const error = new AcexError(code, message);
639
+ this.context.publishRuntimeError(source, error, metadata);
640
+ return error;
641
+ }
642
+ }