@imbingox/acex 0.1.0-beta.2 → 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.
- package/README.md +84 -516
- package/package.json +24 -2
- package/src/adapters/binance/adapter.ts +27 -0
- package/src/adapters/binance/mark-price.ts +126 -0
- package/src/adapters/binance/private-adapter.ts +833 -0
- package/src/adapters/types.ts +177 -1
- package/src/client/context.ts +63 -0
- package/src/client/private-subscription-coordinator.ts +512 -0
- package/src/client/runtime.ts +129 -2
- package/src/errors.ts +8 -1
- package/src/internal/filters.ts +12 -14
- package/src/internal/managed-websocket.ts +24 -2
- package/src/managers/account-manager.ts +346 -52
- package/src/managers/market-manager.ts +357 -121
- package/src/managers/order-manager.ts +427 -46
- package/src/types/account.ts +2 -6
- package/src/types/market.ts +12 -0
- package/src/types/order.ts +40 -6
- package/src/types/shared.ts +15 -0
|
@@ -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.
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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.
|
|
466
|
-
|
|
467
|
-
|
|
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.
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
553
|
-
nextFundingTime:
|
|
554
|
-
markPrice:
|
|
555
|
-
indexPrice:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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:
|
|
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",
|