@imbingox/acex 0.1.0-beta.3 → 0.1.0-beta.4

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.
@@ -1,8 +1,11 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import type {
3
+ FundingRateStreamCallbacks,
4
+ FundingRateStreamOptions,
3
5
  L1BookStreamCallbacks,
4
6
  L1BookStreamOptions,
5
7
  MarketAdapter,
8
+ RawFundingRateUpdate,
6
9
  RawL1BookUpdate,
7
10
  StreamHandle,
8
11
  } from "../adapters/types.ts";
@@ -21,6 +24,7 @@ import type {
21
24
  L1Book,
22
25
  L1BookUpdatedEvent,
23
26
  MarketDataStatus,
27
+ MarketDataStreamStatus,
24
28
  MarketDefinition,
25
29
  MarketEvent,
26
30
  MarketEventStreams,
@@ -29,6 +33,7 @@ import type {
29
33
  MarketStatusChangedEvent,
30
34
  SubscribeFundingRateInput,
31
35
  SubscribeL1BookInput,
36
+ SubscriptionActivity,
32
37
  } from "../types/index.ts";
33
38
 
34
39
  export interface MarketManagerOptions {
@@ -46,8 +51,13 @@ interface MarketRecord {
46
51
  fundingRate?: FundingRateSnapshot;
47
52
  l1BookSubscribed: boolean;
48
53
  fundingRateSubscribed: boolean;
54
+ l1Freshness?: "fresh" | "stale";
55
+ l1Reason?: MarketDataStatus["reason"];
56
+ fundingRateFreshness?: "fresh" | "stale";
57
+ fundingRateReason?: MarketDataStatus["reason"];
49
58
  status: MarketDataStatus;
50
59
  l1BookStream?: StreamHandle;
60
+ fundingRateStream?: StreamHandle;
51
61
  }
52
62
 
53
63
  const DEFAULT_INITIAL_L1_TIMEOUT_MS = 15_000;
@@ -63,6 +73,20 @@ function cloneMarketStatus(status: MarketDataStatus): MarketDataStatus {
63
73
  return { ...status };
64
74
  }
65
75
 
76
+ function cloneStreamStatus(
77
+ status: MarketDataStreamStatus,
78
+ ): MarketDataStreamStatus {
79
+ return { ...status };
80
+ }
81
+
82
+ function cloneL1Book(book: L1Book): L1Book {
83
+ return { ...book, status: cloneStreamStatus(book.status) };
84
+ }
85
+
86
+ function cloneFundingRate(snapshot: FundingRateSnapshot): FundingRateSnapshot {
87
+ return { ...snapshot, status: cloneStreamStatus(snapshot.status) };
88
+ }
89
+
66
90
  function cloneMarketDefinition(definition: MarketDefinition): MarketDefinition {
67
91
  return { ...definition, raw: { ...definition.raw } };
68
92
  }
@@ -138,15 +162,9 @@ export class MarketManagerImpl
138
162
 
139
163
  record.market = market;
140
164
  record.l1BookSubscribed = true;
141
- record.status = {
142
- ...record.status,
143
- activity: "active",
144
- ready: Boolean(record.l1Book),
145
- freshness: record.l1Book ? "stale" : undefined,
146
- reason: undefined,
147
- inactiveSince: undefined,
148
- };
149
- this.publishStatus(record);
165
+ record.l1Freshness = record.l1Book ? "stale" : undefined;
166
+ record.l1Reason = undefined;
167
+ this.recomputeAndPublishStatus(record);
150
168
 
151
169
  await this.ensureL1BookStream(record, market);
152
170
  }
@@ -160,41 +178,28 @@ export class MarketManagerImpl
160
178
  record.l1BookStream?.close();
161
179
  record.l1BookStream = undefined;
162
180
  record.l1BookSubscribed = false;
163
- this.updateActivity(record);
181
+ record.l1Freshness = undefined;
182
+ record.l1Reason = undefined;
183
+ this.syncL1BookStatus(record);
184
+ this.recomputeAndPublishStatus(record, this.context.now());
164
185
  }
165
186
 
166
187
  async subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void> {
167
188
  this.context.assertStarted();
168
- const record = this.getOrCreateRecord(input);
169
- const fundingRate =
170
- record.fundingRate ??
171
- this.createFundingRate(input.exchange, input.symbol, record.fundingRate);
172
-
173
- if (!record.fundingRateSubscribed) {
174
- record.fundingRateSubscribed = true;
175
- record.fundingRate = fundingRate;
176
- }
177
-
178
- record.status = {
179
- ...record.status,
180
- activity: "active",
181
- ready: true,
182
- freshness: "fresh",
183
- lastReceivedAt: fundingRate.receivedAt,
184
- lastReadyAt: fundingRate.updatedAt,
185
- inactiveSince: undefined,
186
- };
189
+ const market = await this.resolveMarketDefinition(input);
190
+ this.assertFundingRateSupported(market);
191
+ const record = this.getOrCreateRecord({
192
+ exchange: input.exchange,
193
+ symbol: market.symbol,
194
+ });
187
195
 
188
- const event: FundingRateUpdatedEvent = {
189
- type: "funding_rate.updated",
190
- exchange: record.exchange,
191
- symbol: record.symbol,
192
- snapshot: fundingRate,
193
- ts: this.context.now(),
194
- };
196
+ record.market = market;
197
+ record.fundingRateSubscribed = true;
198
+ record.fundingRateFreshness = record.fundingRate ? "stale" : undefined;
199
+ record.fundingRateReason = undefined;
200
+ this.recomputeAndPublishStatus(record);
195
201
 
196
- this.publishMarketEvent(event);
197
- this.publishStatus(record);
202
+ await this.ensureFundingRateStream(record, market);
198
203
  }
199
204
 
200
205
  async unsubscribeFundingRate(
@@ -205,8 +210,13 @@ export class MarketManagerImpl
205
210
  return;
206
211
  }
207
212
 
213
+ record.fundingRateStream?.close();
214
+ record.fundingRateStream = undefined;
208
215
  record.fundingRateSubscribed = false;
209
- this.updateActivity(record);
216
+ record.fundingRateFreshness = undefined;
217
+ record.fundingRateReason = undefined;
218
+ this.syncFundingRateStatus(record);
219
+ this.recomputeAndPublishStatus(record, this.context.now());
210
220
  }
211
221
 
212
222
  getMarket(exchange: Exchange, symbol: string): MarketDefinition | undefined {
@@ -231,11 +241,13 @@ export class MarketManagerImpl
231
241
  }
232
242
 
233
243
  getL1Book(key: MarketKeyInput): L1Book | undefined {
234
- return this.records.get(marketKey(key))?.l1Book;
244
+ const book = this.records.get(marketKey(key))?.l1Book;
245
+ return book ? cloneL1Book(book) : undefined;
235
246
  }
236
247
 
237
248
  getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined {
238
- return this.records.get(marketKey(key))?.fundingRate;
249
+ const fundingRate = this.records.get(marketKey(key))?.fundingRate;
250
+ return fundingRate ? cloneFundingRate(fundingRate) : undefined;
239
251
  }
240
252
 
241
253
  getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined {
@@ -253,21 +265,17 @@ export class MarketManagerImpl
253
265
  continue;
254
266
  }
255
267
 
256
- record.status = {
257
- ...record.status,
258
- activity: "active",
259
- ready: Boolean(record.l1Book || record.fundingRate),
260
- freshness: record.l1Book
261
- ? "stale"
262
- : record.status.ready
263
- ? "fresh"
264
- : undefined,
265
- reason: undefined,
266
- lastReadyAt: record.status.lastReadyAt ?? now,
267
- lastReceivedAt: record.status.lastReceivedAt ?? now,
268
- inactiveSince: undefined,
269
- };
270
- this.publishStatus(record);
268
+ if (record.l1BookSubscribed) {
269
+ record.l1Freshness = record.l1Book ? "stale" : undefined;
270
+ record.l1Reason = undefined;
271
+ this.syncL1BookStatus(record);
272
+ }
273
+ if (record.fundingRateSubscribed) {
274
+ record.fundingRateFreshness = record.fundingRate ? "stale" : undefined;
275
+ record.fundingRateReason = undefined;
276
+ this.syncFundingRateStatus(record);
277
+ }
278
+ this.recomputeAndPublishStatus(record, now);
271
279
  }
272
280
 
273
281
  void this.resumeStreams();
@@ -281,12 +289,18 @@ export class MarketManagerImpl
281
289
 
282
290
  record.l1BookStream?.close();
283
291
  record.l1BookStream = undefined;
292
+ record.fundingRateStream?.close();
293
+ record.fundingRateStream = undefined;
294
+ record.l1Freshness = record.l1Book ? "stale" : undefined;
295
+ record.fundingRateFreshness = record.fundingRate ? "stale" : undefined;
296
+ this.syncL1BookStatus(record, now, "inactive");
297
+ this.syncFundingRateStatus(record, now, "inactive");
284
298
 
285
299
  record.status = {
286
300
  ...record.status,
287
301
  activity: "inactive",
288
302
  inactiveSince: now,
289
- freshness: record.l1Book ? "stale" : undefined,
303
+ freshness: record.l1Book || record.fundingRate ? "stale" : undefined,
290
304
  };
291
305
  this.publishStatus(record);
292
306
  }
@@ -390,6 +404,19 @@ export class MarketManagerImpl
390
404
  );
391
405
  }
392
406
 
407
+ private assertFundingRateSupported(market: MarketDefinition): void {
408
+ if (market.contract && market.type === "swap") {
409
+ return;
410
+ }
411
+
412
+ throw this.createError(
413
+ "MARKET_FUNDING_RATE_UNSUPPORTED",
414
+ `Funding rate is not supported for market: ${market.symbol}`,
415
+ { exchange: market.exchange, symbol: market.symbol },
416
+ "market",
417
+ );
418
+ }
419
+
393
420
  private getOrCreateRecord(input: {
394
421
  exchange: Exchange;
395
422
  symbol: string;
@@ -440,11 +467,39 @@ export class MarketManagerImpl
440
467
  exchange: market.exchange,
441
468
  symbol: market.symbol,
442
469
  });
470
+ this.updateConnectionState(record, "l1Book", "stale", "ws_disconnected");
471
+ throw timeoutError;
472
+ }
473
+ }
474
+
475
+ private async ensureFundingRateStream(
476
+ record: MarketRecord,
477
+ market: MarketDefinition,
478
+ ): Promise<void> {
479
+ if (record.fundingRateStream) {
480
+ await record.fundingRateStream.ready;
481
+ return;
482
+ }
483
+
484
+ record.fundingRateStream = this.createFundingRateStream(record, market);
485
+
486
+ try {
487
+ await record.fundingRateStream.ready;
488
+ } catch {
489
+ record.fundingRateStream = undefined;
490
+ const timeoutError = new AcexError(
491
+ "MARKET_STREAM_TIMEOUT",
492
+ `Timed out waiting for market data: ${market.symbol}`,
493
+ );
494
+ this.context.publishRuntimeError("runtime", timeoutError, {
495
+ exchange: market.exchange,
496
+ symbol: market.symbol,
497
+ });
443
498
  this.updateConnectionState(
444
499
  record,
500
+ "fundingRate",
445
501
  "stale",
446
502
  "ws_disconnected",
447
- Boolean(record.l1Book),
448
503
  );
449
504
  throw timeoutError;
450
505
  }
@@ -462,42 +517,87 @@ export class MarketManagerImpl
462
517
  update,
463
518
  record.l1Book,
464
519
  );
465
- record.status = {
466
- ...record.status,
467
- activity: "active",
468
- ready: true,
469
- freshness: "fresh",
470
- reason: undefined,
471
- lastReceivedAt: record.l1Book.receivedAt,
472
- lastReadyAt: record.l1Book.updatedAt,
473
- inactiveSince: undefined,
474
- };
520
+ record.l1Freshness = "fresh";
521
+ record.l1Reason = undefined;
522
+ this.syncL1BookStatus(record);
475
523
 
476
524
  const event: L1BookUpdatedEvent = {
477
525
  type: "l1_book.updated",
478
526
  exchange: record.exchange,
479
527
  symbol: record.symbol,
480
- snapshot: record.l1Book,
528
+ snapshot: cloneL1Book(record.l1Book),
481
529
  ts: this.context.now(),
482
530
  };
483
531
 
484
532
  this.publishMarketEvent(event);
485
- this.publishStatus(record);
533
+ this.recomputeAndPublishStatus(record);
486
534
  },
487
535
  onFreshnessChange: (freshness, reason) => {
536
+ this.updateConnectionState(record, "l1Book", freshness, reason);
537
+ },
538
+ onDisconnected: () => {
488
539
  this.updateConnectionState(
489
540
  record,
490
- freshness,
491
- reason,
492
- Boolean(record.l1Book),
541
+ "l1Book",
542
+ "stale",
543
+ "ws_disconnected",
544
+ );
545
+ },
546
+ onError: (error) => {
547
+ this.context.publishRuntimeError("runtime", error, {
548
+ exchange: record.exchange,
549
+ symbol: record.symbol,
550
+ });
551
+ },
552
+ };
553
+
554
+ const options: L1BookStreamOptions = {
555
+ initialMessageTimeoutMs: this.initialL1TimeoutMs,
556
+ staleAfterMs: this.l1StaleAfterMs,
557
+ reconnectDelayMs: this.l1ReconnectDelayMs,
558
+ reconnectMaxDelayMs: this.l1ReconnectMaxDelayMs,
559
+ now: () => this.context.now(),
560
+ };
561
+
562
+ return this.adapter.createL1BookStream(market, callbacks, options);
563
+ }
564
+
565
+ private createFundingRateStream(
566
+ record: MarketRecord,
567
+ market: MarketDefinition,
568
+ ): StreamHandle {
569
+ const callbacks: FundingRateStreamCallbacks = {
570
+ onUpdate: (update: RawFundingRateUpdate) => {
571
+ record.fundingRate = this.createFundingRate(
572
+ record.exchange,
573
+ record.symbol,
574
+ update,
575
+ record.fundingRate,
493
576
  );
577
+ record.fundingRateFreshness = "fresh";
578
+ record.fundingRateReason = undefined;
579
+ this.syncFundingRateStatus(record);
580
+
581
+ const event: FundingRateUpdatedEvent = {
582
+ type: "funding_rate.updated",
583
+ exchange: record.exchange,
584
+ symbol: record.symbol,
585
+ snapshot: cloneFundingRate(record.fundingRate),
586
+ ts: this.context.now(),
587
+ };
588
+
589
+ this.publishMarketEvent(event);
590
+ this.recomputeAndPublishStatus(record);
591
+ },
592
+ onFreshnessChange: (freshness, reason) => {
593
+ this.updateConnectionState(record, "fundingRate", freshness, reason);
494
594
  },
495
595
  onDisconnected: () => {
496
596
  this.updateConnectionState(
497
597
  record,
598
+ "fundingRate",
498
599
  "stale",
499
600
  "ws_disconnected",
500
- Boolean(record.l1Book),
501
601
  );
502
602
  },
503
603
  onError: (error) => {
@@ -508,7 +608,7 @@ export class MarketManagerImpl
508
608
  },
509
609
  };
510
610
 
511
- const options: L1BookStreamOptions = {
611
+ const options: FundingRateStreamOptions = {
512
612
  initialMessageTimeoutMs: this.initialL1TimeoutMs,
513
613
  staleAfterMs: this.l1StaleAfterMs,
514
614
  reconnectDelayMs: this.l1ReconnectDelayMs,
@@ -516,7 +616,7 @@ export class MarketManagerImpl
516
616
  now: () => this.context.now(),
517
617
  };
518
618
 
519
- return this.adapter.createL1BookStream(market, callbacks, options);
619
+ return this.adapter.createFundingRateStream(market, callbacks, options);
520
620
  }
521
621
 
522
622
  private createL1Book(
@@ -536,63 +636,189 @@ export class MarketManagerImpl
536
636
  receivedAt: input.receivedAt,
537
637
  updatedAt: input.receivedAt,
538
638
  version: (previous?.version ?? 0) + 1,
639
+ status: previous?.status ?? {
640
+ activity: "active",
641
+ ready: true,
642
+ freshness: "fresh",
643
+ lastReceivedAt: input.receivedAt,
644
+ lastReadyAt: input.receivedAt,
645
+ },
539
646
  };
540
647
  }
541
648
 
542
649
  private createFundingRate(
543
650
  exchange: Exchange,
544
651
  symbol: string,
652
+ input: RawFundingRateUpdate,
545
653
  previous?: FundingRateSnapshot,
546
654
  ): FundingRateSnapshot {
547
- const now = this.context.now();
548
-
549
655
  return {
550
656
  exchange,
551
657
  symbol,
552
- fundingRate: previous?.fundingRate ?? new BigNumber(0),
553
- nextFundingTime: previous?.nextFundingTime,
554
- markPrice: previous?.markPrice,
555
- indexPrice: previous?.indexPrice,
556
- exchangeTs: now,
557
- receivedAt: now,
558
- updatedAt: now,
658
+ fundingRate: new BigNumber(input.fundingRate),
659
+ nextFundingTime: input.nextFundingTime,
660
+ markPrice: input.markPrice ? new BigNumber(input.markPrice) : undefined,
661
+ indexPrice: input.indexPrice
662
+ ? new BigNumber(input.indexPrice)
663
+ : undefined,
664
+ exchangeTs: input.exchangeTs,
665
+ receivedAt: input.receivedAt,
666
+ updatedAt: input.receivedAt,
559
667
  version: (previous?.version ?? 0) + 1,
668
+ status: previous?.status ?? {
669
+ activity: "active",
670
+ ready: true,
671
+ freshness: "fresh",
672
+ lastReceivedAt: input.receivedAt,
673
+ lastReadyAt: input.receivedAt,
674
+ },
560
675
  };
561
676
  }
562
677
 
563
678
  private updateConnectionState(
564
679
  record: MarketRecord,
680
+ stream: "l1Book" | "fundingRate",
565
681
  freshness: "fresh" | "stale",
566
682
  reason: MarketDataStatus["reason"],
567
- ready: boolean,
568
683
  ): void {
684
+ if (stream === "l1Book") {
685
+ record.l1Freshness = freshness;
686
+ record.l1Reason = reason;
687
+ this.syncL1BookStatus(record);
688
+ } else {
689
+ record.fundingRateFreshness = freshness;
690
+ record.fundingRateReason = reason;
691
+ this.syncFundingRateStatus(record);
692
+ }
693
+
694
+ this.recomputeAndPublishStatus(record);
695
+ }
696
+
697
+ private recomputeAndPublishStatus(
698
+ record: MarketRecord,
699
+ now = this.context.now(),
700
+ ): void {
701
+ const l1Ready = record.l1BookSubscribed && Boolean(record.l1Book);
702
+ const fundingRateReady =
703
+ record.fundingRateSubscribed && Boolean(record.fundingRate);
704
+ const active = record.l1BookSubscribed || record.fundingRateSubscribed;
705
+ const staleReason = record.l1Reason ?? record.fundingRateReason;
706
+ const freshness = this.resolveFreshness(record);
707
+
569
708
  record.status = {
570
709
  ...record.status,
571
- activity: "active",
572
- ready,
710
+ activity: active ? "active" : "inactive",
711
+ ready: l1Ready || fundingRateReady,
573
712
  freshness,
574
- reason,
575
- inactiveSince: undefined,
713
+ reason: freshness === "stale" ? staleReason : undefined,
714
+ inactiveSince: active ? undefined : now,
576
715
  };
716
+
717
+ if (record.status.ready) {
718
+ record.status.lastReceivedAt = this.resolveLastReceivedAt(record);
719
+ record.status.lastReadyAt = this.resolveLastReadyAt(record);
720
+ }
721
+
577
722
  this.publishStatus(record);
578
723
  }
579
724
 
580
- private updateActivity(record: MarketRecord): void {
581
- if (record.l1BookSubscribed || record.fundingRateSubscribed) {
582
- record.status = {
583
- ...record.status,
584
- activity: "active",
585
- inactiveSince: undefined,
586
- };
587
- } else {
588
- record.status = {
589
- ...record.status,
590
- activity: "inactive",
591
- inactiveSince: this.context.now(),
592
- };
725
+ private syncL1BookStatus(
726
+ record: MarketRecord,
727
+ now?: number,
728
+ activity?: SubscriptionActivity,
729
+ ): void {
730
+ if (!record.l1Book) {
731
+ return;
593
732
  }
594
733
 
595
- this.publishStatus(record);
734
+ record.l1Book.status = this.createStreamStatus(
735
+ activity ?? (record.l1BookSubscribed ? "active" : "inactive"),
736
+ true,
737
+ record.l1Freshness,
738
+ record.l1Reason,
739
+ record.l1Book.receivedAt,
740
+ record.l1Book.updatedAt,
741
+ now,
742
+ );
743
+ }
744
+
745
+ private syncFundingRateStatus(
746
+ record: MarketRecord,
747
+ now?: number,
748
+ activity?: SubscriptionActivity,
749
+ ): void {
750
+ if (!record.fundingRate) {
751
+ return;
752
+ }
753
+
754
+ record.fundingRate.status = this.createStreamStatus(
755
+ activity ?? (record.fundingRateSubscribed ? "active" : "inactive"),
756
+ true,
757
+ record.fundingRateFreshness,
758
+ record.fundingRateReason,
759
+ record.fundingRate.receivedAt,
760
+ record.fundingRate.updatedAt,
761
+ now,
762
+ );
763
+ }
764
+
765
+ private createStreamStatus(
766
+ activity: SubscriptionActivity,
767
+ ready: boolean,
768
+ freshness: MarketDataStreamStatus["freshness"],
769
+ reason: MarketDataStreamStatus["reason"],
770
+ lastReceivedAt?: number,
771
+ lastReadyAt?: number,
772
+ now = this.context.now(),
773
+ ): MarketDataStreamStatus {
774
+ return {
775
+ activity,
776
+ ready,
777
+ freshness,
778
+ reason: freshness === "stale" ? reason : undefined,
779
+ lastReceivedAt,
780
+ lastReadyAt,
781
+ inactiveSince: activity === "active" ? undefined : now,
782
+ };
783
+ }
784
+
785
+ private resolveFreshness(
786
+ record: MarketRecord,
787
+ ): MarketDataStatus["freshness"] | undefined {
788
+ if (record.l1BookSubscribed && record.l1Freshness === "stale") {
789
+ return "stale";
790
+ }
791
+ if (
792
+ record.fundingRateSubscribed &&
793
+ record.fundingRateFreshness === "stale"
794
+ ) {
795
+ return "stale";
796
+ }
797
+ if (record.l1BookSubscribed && record.l1Freshness === "fresh") {
798
+ return "fresh";
799
+ }
800
+ if (
801
+ record.fundingRateSubscribed &&
802
+ record.fundingRateFreshness === "fresh"
803
+ ) {
804
+ return "fresh";
805
+ }
806
+
807
+ return undefined;
808
+ }
809
+
810
+ private resolveLastReceivedAt(record: MarketRecord): number | undefined {
811
+ return Math.max(
812
+ record.l1Book?.receivedAt ?? 0,
813
+ record.fundingRate?.receivedAt ?? 0,
814
+ );
815
+ }
816
+
817
+ private resolveLastReadyAt(record: MarketRecord): number | undefined {
818
+ return Math.max(
819
+ record.l1Book?.updatedAt ?? 0,
820
+ record.fundingRate?.updatedAt ?? 0,
821
+ );
596
822
  }
597
823
 
598
824
  private publishMarketEvent(event: MarketEvent): void {
@@ -615,33 +841,43 @@ export class MarketManagerImpl
615
841
 
616
842
  private async resumeStreams(): Promise<void> {
617
843
  for (const record of this.records.values()) {
618
- if (!record.l1BookSubscribed || record.l1BookStream) {
619
- continue;
620
- }
621
-
622
844
  const market = record.market;
623
845
  if (!market) {
624
846
  continue;
625
847
  }
626
848
 
627
- try {
628
- record.status = {
629
- ...record.status,
630
- activity: "active",
631
- freshness: record.l1Book ? "stale" : undefined,
632
- reason: undefined,
633
- inactiveSince: undefined,
634
- };
635
- this.publishStatus(record);
636
- await this.ensureL1BookStream(record, market);
637
- } catch {
638
- // Errors are already published through the runtime error bus.
849
+ if (record.l1BookSubscribed && !record.l1BookStream) {
850
+ try {
851
+ record.l1Freshness = record.l1Book ? "stale" : undefined;
852
+ record.l1Reason = undefined;
853
+ this.recomputeAndPublishStatus(record);
854
+ await this.ensureL1BookStream(record, market);
855
+ } catch {
856
+ // Errors are already published through the runtime error bus.
857
+ }
858
+ }
859
+
860
+ if (record.fundingRateSubscribed && !record.fundingRateStream) {
861
+ try {
862
+ record.fundingRateFreshness = record.fundingRate
863
+ ? "stale"
864
+ : undefined;
865
+ record.fundingRateReason = undefined;
866
+ this.recomputeAndPublishStatus(record);
867
+ await this.ensureFundingRateStream(record, market);
868
+ } catch {
869
+ // Errors are already published through the runtime error bus.
870
+ }
639
871
  }
640
872
  }
641
873
  }
642
874
 
643
875
  private createError(
644
- code: "MARKET_NOT_FOUND" | "MARKET_INACTIVE" | "EXCHANGE_NOT_SUPPORTED",
876
+ code:
877
+ | "MARKET_NOT_FOUND"
878
+ | "MARKET_INACTIVE"
879
+ | "MARKET_FUNDING_RATE_UNSUPPORTED"
880
+ | "EXCHANGE_NOT_SUPPORTED",
645
881
  message: string,
646
882
  metadata?: { exchange?: Exchange; symbol?: string },
647
883
  source: "market" | "client" = "market",