@tradejs/app 3.0.0 → 3.1.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.
@@ -1,700 +0,0 @@
1
- import { getRuntimeStorageDayKeys } from '@tradejs/core/time';
2
- import { logger } from '@tradejs/infra/logger';
3
- import { strategyLogicConfigFingerprint } from '@tradejs/infra/strategyReleaseEvidence';
4
- import {
5
- listTradingAccounts,
6
- resolveTradingAccount,
7
- } from '@tradejs/infra/tradingAccounts';
8
- import { listRuntimeDeployments } from '@tradejs/infra/runtimeDeployments';
9
- import { strategyEntries } from '@tradejs/strategies';
10
- import { loadRuntimeStrategyConfigs as loadStoredRuntimeStrategyConfigs } from '@tradejs/infra/runtimeStrategyConfigs';
11
- import {
12
- getData,
13
- getHashJsonValues,
14
- getKeys,
15
- redisKeys,
16
- } from '@tradejs/infra/redis';
17
- import type {
18
- Connector,
19
- RuntimeTradeRecord,
20
- Interval,
21
- StrategyConfig,
22
- } from '@tradejs/types';
23
- import { getAvailableStrategyNames } from '@tradejs/node/strategies';
24
- import {
25
- DEFAULT_CONNECTOR_PROVIDER,
26
- resolveConnectorAccountId,
27
- resolveConnectorCreatorByProvider,
28
- } from '#app/lib/connectorCreator';
29
- import {
30
- buildRuntimeStrategyAnalytics,
31
- buildExchangeFallbackRuntimeTrades,
32
- isRuntimeTradeRecord,
33
- RuntimeStrategiesResponse,
34
- selectTradesForWindow,
35
- toRuntimeTradeView,
36
- } from '#app/lib/runtimeStrategies';
37
- import {
38
- assignLegacyRuntimeTradeAccountScopes,
39
- buildRuntimeStrategyIdentityKey,
40
- } from '#app/lib/runtimeStrategyLineage';
41
- import {
42
- loadStrategyEvidenceTimelines,
43
- strategyEvidenceTimelineSelectorKey,
44
- } from '#app/lib/strategyEvidenceTimeline';
45
- import {
46
- isRuntimeTradeInConnectorScope,
47
- syncRuntimeTrades,
48
- } from '#app/lib/runtimeTradeSync';
49
-
50
- const DEFAULT_PROVIDER = DEFAULT_CONNECTOR_PROVIDER;
51
- const DEFAULT_HOURS = 168;
52
- const MIN_HOURS = 6;
53
- const MAX_HOURS = 24 * 90;
54
- const BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1000 - 1_000;
55
- const EXCHANGE_REQUEST_TIMEOUT_MS = 15_000;
56
-
57
- const coerceHours = (value: string | number | null | undefined) => {
58
- const parsed = Number(value ?? Number.NaN);
59
- if (!Number.isFinite(parsed)) {
60
- return DEFAULT_HOURS;
61
- }
62
-
63
- return Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.trunc(parsed)));
64
- };
65
-
66
- const isRuntimeStrategyConfigEnabled = (config: StrategyConfig | null) => {
67
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
68
- return false;
69
- }
70
-
71
- return (config as Record<string, unknown>).ENABLE !== false;
72
- };
73
-
74
- const loadRuntimeStrategyConfigs = async (userName: string) => {
75
- return (await loadStoredRuntimeStrategyConfigs(userName)).map(
76
- ({ strategyConfig, ...record }) => ({
77
- ...record,
78
- config: strategyConfig,
79
- }),
80
- );
81
- };
82
-
83
- const loadConfiguredStrategyNames = async (projectRoot: string) => {
84
- try {
85
- const names = await getAvailableStrategyNames(projectRoot);
86
- const builtInNames = strategyEntries
87
- .map((entry) => entry.manifest?.name)
88
- .filter((value): value is string => Boolean(value));
89
-
90
- return [...new Set([...names, ...builtInNames])].sort((left, right) =>
91
- left.localeCompare(right),
92
- );
93
- } catch (error) {
94
- logger.warn(
95
- 'strategies runtime: failed to load configured strategies: %s',
96
- (error as Error)?.message || String(error),
97
- );
98
- return strategyEntries
99
- .map((entry) => entry.manifest?.name)
100
- .filter((value): value is string => Boolean(value))
101
- .sort((left, right) => left.localeCompare(right));
102
- }
103
- };
104
-
105
- const loadRuntimeTrades = async (
106
- userName: string,
107
- {
108
- startTime,
109
- endTime,
110
- }: {
111
- startTime: number;
112
- endTime: number;
113
- },
114
- ): Promise<RuntimeTradeRecord[]> => {
115
- const filterByWindow = (trade: RuntimeTradeRecord) =>
116
- trade.entryTimestamp >= startTime ||
117
- (typeof trade.exitTimestamp === 'number' &&
118
- trade.exitTimestamp >= startTime);
119
- const dayKeys = getRuntimeStorageDayKeys(startTime, endTime);
120
- const bucketTrades = (
121
- await Promise.all(
122
- dayKeys.map((dayKey) =>
123
- getHashJsonValues<RuntimeTradeRecord>(
124
- redisKeys.runtimeTradeBucket(userName, dayKey),
125
- ),
126
- ),
127
- )
128
- ).flat();
129
- const dedupedBucketTrades = new Map<string, RuntimeTradeRecord>();
130
-
131
- for (const trade of bucketTrades) {
132
- if (!isRuntimeTradeRecord(trade)) {
133
- continue;
134
- }
135
- dedupedBucketTrades.set(trade.orderId, trade);
136
- }
137
-
138
- if (dedupedBucketTrades.size > 0 || dayKeys.length === 0) {
139
- return [...dedupedBucketTrades.values()]
140
- .filter(filterByWindow)
141
- .sort((left, right) => left.entryTimestamp - right.entryTimestamp);
142
- }
143
-
144
- const keys = await getKeys(redisKeys.runtimeTrades(userName));
145
- const trades = await Promise.all(keys.map((key) => getData(key, null)));
146
-
147
- return trades
148
- .filter(isRuntimeTradeRecord)
149
- .filter(filterByWindow)
150
- .sort((left, right) => left.entryTimestamp - right.entryTimestamp);
151
- };
152
-
153
- const buildExchangeTimeRanges = (startTime: number, endTime: number) => {
154
- const ranges: Array<{ startTime: number; endTime: number }> = [];
155
- let cursor = startTime;
156
-
157
- while (cursor < endTime) {
158
- const rangeEnd = Math.min(endTime, cursor + BYBIT_MAX_TIME_RANGE_MS);
159
- ranges.push({ startTime: cursor, endTime: rangeEnd });
160
- cursor = rangeEnd + 1;
161
- }
162
-
163
- return ranges;
164
- };
165
-
166
- const loadExchangeRange = async <T>({
167
- label,
168
- startTime,
169
- endTime,
170
- load,
171
- errors,
172
- }: {
173
- label: string;
174
- startTime: number;
175
- endTime: number;
176
- load: () => Promise<T[]>;
177
- errors?: string[];
178
- }) => {
179
- try {
180
- return await Promise.race([
181
- load(),
182
- new Promise<T[]>((_, reject) => {
183
- setTimeout(
184
- () =>
185
- reject(
186
- new Error(
187
- `${label} timed out for ${new Date(startTime).toISOString()} - ${new Date(endTime).toISOString()}`,
188
- ),
189
- ),
190
- EXCHANGE_REQUEST_TIMEOUT_MS,
191
- );
192
- }),
193
- ]);
194
- } catch (error) {
195
- const message = (error as Error)?.message || String(error);
196
- errors?.push(`${label}: ${message}`);
197
- logger.warn('strategies runtime: %s failed: %s', label, message);
198
- return [];
199
- }
200
- };
201
-
202
- const loadActiveRuntimeOrderIds = async (userName: string) => {
203
- const keys = await getKeys(redisKeys.runtimeActiveTrades(userName));
204
- const refs = await Promise.all(keys.map((key) => getData(key, null)));
205
-
206
- return new Set(
207
- refs
208
- .map((ref) =>
209
- typeof ref?.orderId === 'string' && ref.orderId.trim()
210
- ? ref.orderId.trim()
211
- : null,
212
- )
213
- .filter((value): value is string => Boolean(value)),
214
- );
215
- };
216
-
217
- const loadClosedPnlRows = async ({
218
- connector,
219
- startTime,
220
- endTime,
221
- errors,
222
- }: {
223
- connector: Connector;
224
- startTime: number;
225
- endTime: number;
226
- errors?: string[];
227
- }) => {
228
- if (typeof connector.getClosedPnl !== 'function') {
229
- return [];
230
- }
231
-
232
- try {
233
- const rows = (
234
- await Promise.all(
235
- buildExchangeTimeRanges(startTime, endTime).map((range) =>
236
- loadExchangeRange({
237
- label: 'getClosedPnl',
238
- ...range,
239
- errors,
240
- load: () =>
241
- connector.getClosedPnl?.({
242
- ...range,
243
- limit: 100,
244
- }) ?? Promise.resolve([]),
245
- }),
246
- ),
247
- )
248
- ).flatMap((items) => items ?? []);
249
-
250
- return rows.sort((left, right) => left.closedAt - right.closedAt);
251
- } catch (error) {
252
- const message = (error as Error)?.message || String(error);
253
- errors?.push(`getClosedPnl: ${message}`);
254
- logger.warn('strategies runtime: getClosedPnl failed: %s', message);
255
- return [];
256
- }
257
- };
258
-
259
- const loadExchangeEntryRows = async ({
260
- connector,
261
- startTime,
262
- endTime,
263
- errors,
264
- }: {
265
- connector: Connector;
266
- startTime: number;
267
- endTime: number;
268
- errors?: string[];
269
- }) => {
270
- if (typeof connector.getEntryExecutions !== 'function') {
271
- return [];
272
- }
273
-
274
- try {
275
- const rows = (
276
- await Promise.all(
277
- buildExchangeTimeRanges(startTime, endTime).map((range) =>
278
- loadExchangeRange({
279
- label: 'getEntryExecutions',
280
- ...range,
281
- errors,
282
- load: () =>
283
- connector.getEntryExecutions?.({
284
- ...range,
285
- limit: 100,
286
- }) ?? Promise.resolve([]),
287
- }),
288
- ),
289
- )
290
- ).flatMap((items) => items ?? []);
291
-
292
- return rows.sort(
293
- (left, right) => left.entryTimestamp - right.entryTimestamp,
294
- );
295
- } catch (error) {
296
- const message = (error as Error)?.message || String(error);
297
- errors?.push(`getEntryExecutions: ${message}`);
298
- logger.warn('strategies runtime: getEntryExecutions failed: %s', message);
299
- return [];
300
- }
301
- };
302
-
303
- const loadOpenPositions = async (
304
- connector: Connector,
305
- errors?: string[],
306
- ): Promise<{
307
- positions: Awaited<ReturnType<NonNullable<Connector['getOpenPositionPnl']>>>;
308
- reliable: boolean;
309
- }> => {
310
- if (typeof connector.getOpenPositionPnl !== 'function') {
311
- return { positions: [], reliable: false };
312
- }
313
-
314
- try {
315
- return {
316
- positions: await connector.getOpenPositionPnl(),
317
- reliable: true,
318
- };
319
- } catch (error) {
320
- const message = (error as Error)?.message || String(error);
321
- errors?.push(`getOpenPositionPnl: ${message}`);
322
- logger.warn('strategies runtime: getOpenPositionPnl failed: %s', message);
323
- return { positions: [], reliable: false };
324
- }
325
- };
326
-
327
- export interface RuntimeDashboardQuery {
328
- userName: string;
329
- provider?: string | null;
330
- hours?: string | number | null;
331
- now?: number;
332
- projectRoot?: string;
333
- }
334
-
335
- export const loadRuntimeDashboard = async ({
336
- userName,
337
- provider: requestedProvider,
338
- hours: requestedHours,
339
- now,
340
- projectRoot: requestedProjectRoot,
341
- }: RuntimeDashboardQuery): Promise<RuntimeStrategiesResponse> => {
342
- const provider = requestedProvider?.trim() || DEFAULT_PROVIDER;
343
- const hours = coerceHours(requestedHours);
344
- const endTime = now ?? Date.now();
345
- const startTime = endTime - hours * 60 * 60 * 1000;
346
- const projectRoot =
347
- requestedProjectRoot?.trim() ||
348
- String(process.env.PROJECT_CWD || process.cwd()).trim() ||
349
- process.cwd();
350
- const exchangeErrors: string[] = [];
351
- const connectorCreator = await resolveConnectorCreatorByProvider(
352
- provider,
353
- projectRoot,
354
- DEFAULT_PROVIDER,
355
- );
356
-
357
- if (!connectorCreator) {
358
- throw new Error(`No connector available for provider "${provider}"`);
359
- }
360
-
361
- const connectorAccountId = await resolveConnectorAccountId({
362
- userName,
363
- provider,
364
- universe: 'crypto',
365
- });
366
- const connector = await connectorCreator({
367
- userName,
368
- accountId: connectorAccountId,
369
- universe: 'crypto',
370
- });
371
-
372
- const [
373
- runtimeStrategyConfigs,
374
- configuredStrategyNames,
375
- runtimeTrades,
376
- activeOrderIds,
377
- closedPnlRows,
378
- entryRows,
379
- openPositionsSnapshot,
380
- runtimeDeployments,
381
- tradingAccounts,
382
- ] = await Promise.all([
383
- loadRuntimeStrategyConfigs(userName),
384
- loadConfiguredStrategyNames(projectRoot),
385
- loadRuntimeTrades(userName, { startTime, endTime }),
386
- loadActiveRuntimeOrderIds(userName),
387
- loadClosedPnlRows({
388
- connector,
389
- startTime,
390
- endTime,
391
- errors: exchangeErrors,
392
- }),
393
- loadExchangeEntryRows({
394
- connector,
395
- startTime,
396
- endTime,
397
- errors: exchangeErrors,
398
- }),
399
- loadOpenPositions(connector, exchangeErrors),
400
- listRuntimeDeployments(userName),
401
- listTradingAccounts(userName),
402
- ]);
403
- const relevantTrades = selectTradesForWindow(
404
- runtimeTrades,
405
- startTime,
406
- activeOrderIds,
407
- );
408
- const syncableTrades = relevantTrades.filter((trade) =>
409
- isRuntimeTradeInConnectorScope(trade, connector),
410
- );
411
- const unsyncedTrades = relevantTrades.filter(
412
- (trade) => !isRuntimeTradeInConnectorScope(trade, connector),
413
- );
414
- const syncedConnectorTrades = await syncRuntimeTrades({
415
- userName,
416
- connector,
417
- trades: syncableTrades,
418
- endTime,
419
- openPositions: openPositionsSnapshot.positions,
420
- openPositionsReliable: openPositionsSnapshot.reliable,
421
- closedPnlRows,
422
- });
423
- const syncedTrades = [...unsyncedTrades, ...syncedConnectorTrades];
424
- const fallbackStrategyNames = [
425
- ...new Set([
426
- ...runtimeStrategyConfigs.map(({ strategyName }) => strategyName),
427
- ...configuredStrategyNames,
428
- ]),
429
- ];
430
- const fallbackTrades = buildExchangeFallbackRuntimeTrades({
431
- entryRows,
432
- closedPnlRows,
433
- openPositions: openPositionsSnapshot.positions,
434
- strategyNames: fallbackStrategyNames,
435
- existingTrades: syncedTrades,
436
- endTime,
437
- });
438
- const allTrades = [...syncedTrades, ...fallbackTrades].filter(
439
- isRuntimeTradeRecord,
440
- );
441
- const connectedSet = new Set(
442
- runtimeStrategyConfigs.map(
443
- ({ strategyName, configId }) => `${strategyName}:${configId}`,
444
- ),
445
- );
446
- const accountsById = new Map(
447
- tradingAccounts.map((account) => [account.id, account]),
448
- );
449
- const runtimeIdentityKey = (trade: RuntimeTradeRecord) =>
450
- buildRuntimeStrategyIdentityKey({
451
- strategyName: trade.strategy,
452
- configId: trade.runtimeConfigId,
453
- universe: trade.universe,
454
- accountId: trade.accountId,
455
- deploymentId: trade.deploymentId,
456
- policyProfileId: trade.policyProfileId,
457
- });
458
- const identityByKey = new Map<
459
- string,
460
- {
461
- strategyName: string;
462
- configId: string;
463
- interval: Interval;
464
- universe: 'crypto' | 'tradfi';
465
- accountId?: string;
466
- accountLabel?: string;
467
- deploymentId?: string;
468
- policyProfileId?: string;
469
- releaseCompositionId?: string;
470
- enabled?: boolean;
471
- config?: Record<string, unknown>;
472
- connected?: boolean;
473
- gitSha?: string;
474
- configFingerprint?: string;
475
- gateFingerprint?: string;
476
- contextFingerprint?: string;
477
- maxLossValue?: number;
478
- }
479
- >();
480
- const runtimeConfigAccountScopes = new Array<{
481
- strategyName: string;
482
- configId: string;
483
- universe: 'crypto' | 'tradfi';
484
- accountId?: string;
485
- }>();
486
- for (const deployment of runtimeDeployments) {
487
- for (const deploymentStrategy of deployment.strategies) {
488
- const runtimeKey = buildRuntimeStrategyIdentityKey({
489
- strategyName: deploymentStrategy.strategyName,
490
- configId: `deployment-${deployment.id}`,
491
- universe: deployment.universe,
492
- accountId: deployment.accountId,
493
- deploymentId: deployment.id,
494
- policyProfileId: deploymentStrategy.policyProfileId,
495
- });
496
- identityByKey.set(runtimeKey, {
497
- strategyName: deploymentStrategy.strategyName,
498
- configId: `deployment-${deployment.id}`,
499
- interval: String(deployment.interval) as Interval,
500
- universe: deployment.universe,
501
- accountId: deployment.accountId,
502
- accountLabel: accountsById.get(deployment.accountId)?.label,
503
- deploymentId: deployment.id,
504
- policyProfileId: deploymentStrategy.policyProfileId,
505
- releaseCompositionId: deploymentStrategy.releaseCompositionId,
506
- enabled: deployment.enabled && deploymentStrategy.enabled !== false,
507
- config: deploymentStrategy.config,
508
- connected: false,
509
- configFingerprint: strategyLogicConfigFingerprint(
510
- deploymentStrategy.config,
511
- ),
512
- });
513
- }
514
- }
515
- for (const runtimeConfig of runtimeStrategyConfigs) {
516
- const universe =
517
- runtimeConfig.config.UNIVERSE === 'tradfi' ? 'tradfi' : 'crypto';
518
- const configuredAccountId =
519
- typeof runtimeConfig.config.ACCOUNT_ID === 'string' &&
520
- runtimeConfig.config.ACCOUNT_ID.trim()
521
- ? runtimeConfig.config.ACCOUNT_ID.trim()
522
- : undefined;
523
- const resolvedAccount = await resolveTradingAccount({
524
- userName,
525
- accountId: configuredAccountId,
526
- provider,
527
- universe,
528
- }).catch(() => null);
529
- const accountId = resolvedAccount?.id ?? configuredAccountId;
530
- runtimeConfigAccountScopes.push({
531
- strategyName: runtimeConfig.strategyName,
532
- configId: runtimeConfig.configId,
533
- universe,
534
- accountId,
535
- });
536
- const runtimeKey = buildRuntimeStrategyIdentityKey({
537
- strategyName: runtimeConfig.strategyName,
538
- configId: runtimeConfig.configId,
539
- universe,
540
- accountId,
541
- });
542
- identityByKey.set(runtimeKey, {
543
- strategyName: runtimeConfig.strategyName,
544
- configId: runtimeConfig.configId,
545
- interval: String(runtimeConfig.config.INTERVAL ?? '15') as Interval,
546
- universe,
547
- accountId,
548
- accountLabel: accountId ? accountsById.get(accountId)?.label : undefined,
549
- enabled: isRuntimeStrategyConfigEnabled(runtimeConfig.config),
550
- config: runtimeConfig.config,
551
- connected: true,
552
- configFingerprint: strategyLogicConfigFingerprint(runtimeConfig.config),
553
- });
554
- }
555
- const accountScopedTrades = assignLegacyRuntimeTradeAccountScopes(
556
- allTrades,
557
- runtimeConfigAccountScopes,
558
- );
559
- for (const trade of accountScopedTrades) {
560
- const key = runtimeIdentityKey(trade);
561
- const configuredCompositionId =
562
- identityByKey.get(key)?.releaseCompositionId;
563
- const observedCompositionId = trade.runtimeLineage?.compositionId;
564
- const releaseCompositionId =
565
- configuredCompositionId &&
566
- observedCompositionId &&
567
- configuredCompositionId !== observedCompositionId
568
- ? undefined
569
- : observedCompositionId ?? configuredCompositionId;
570
- identityByKey.set(key, {
571
- ...identityByKey.get(key),
572
- strategyName: trade.strategy,
573
- configId: trade.runtimeConfigId ?? 'config',
574
- interval: String(trade.interval ?? '15') as Interval,
575
- universe: trade.universe ?? 'crypto',
576
- accountId: trade.accountId,
577
- accountLabel: trade.accountId
578
- ? accountsById.get(trade.accountId)?.label
579
- : undefined,
580
- deploymentId: trade.deploymentId,
581
- policyProfileId: trade.policyProfileId,
582
- releaseCompositionId,
583
- configFingerprint: trade.runtimeLineage?.configFingerprint,
584
- gateFingerprint: trade.runtimeLineage?.gateFingerprint,
585
- contextFingerprint: trade.runtimeLineage?.contextFingerprint,
586
- gitSha: trade.runtimeLineage?.gitSha ?? undefined,
587
- maxLossValue: trade.runtimeLineage?.maxLossValue ?? undefined,
588
- });
589
- }
590
-
591
- const evidenceTimelines = await loadStrategyEvidenceTimelines({
592
- projectRoot,
593
- markerDir: process.env.STRATEGY_RELEASE_MARKER_DIR,
594
- selectors: [...identityByKey.values()].map((identity) => ({
595
- strategy: identity.strategyName,
596
- compositionId: identity.releaseCompositionId,
597
- configFingerprint: identity.configFingerprint,
598
- gateFingerprint: identity.gateFingerprint,
599
- contextFingerprint: identity.contextFingerprint,
600
- gitSha: identity.gitSha,
601
- maxLossValue: identity.maxLossValue,
602
- requireCompleteLineage: true,
603
- })),
604
- startTime,
605
- endTime,
606
- });
607
-
608
- const strategies = await Promise.all(
609
- [...identityByKey.entries()].map(async ([runtimeKey, identity]) => {
610
- const { strategyName } = identity;
611
- const strategyTrades = accountScopedTrades
612
- .filter((trade) => runtimeIdentityKey(trade) === runtimeKey)
613
- .sort((left, right) => right.entryTimestamp - left.entryTimestamp);
614
- const orders = strategyTrades
615
- .sort((left, right) => {
616
- const leftDate = left.exitTimestamp ?? left.entryTimestamp;
617
- const rightDate = right.exitTimestamp ?? right.entryTimestamp;
618
-
619
- return rightDate - leftDate;
620
- })
621
- .map((trade) => toRuntimeTradeView(trade, endTime));
622
- const analytics = buildRuntimeStrategyAnalytics({
623
- trades: strategyTrades,
624
- startTime,
625
- endTime,
626
- });
627
- const effectiveStrategyConfig = identity.config ?? null;
628
-
629
- return {
630
- runtimeKey,
631
- strategyName,
632
- configId: identity.configId,
633
- interval: identity.interval,
634
- universe: identity.universe,
635
- accountId: identity.accountId,
636
- accountLabel: identity.accountLabel,
637
- deploymentId: identity.deploymentId,
638
- policyProfileId: identity.policyProfileId,
639
- connected:
640
- identity.connected ??
641
- connectedSet.has(`${strategyName}:${identity.configId}`),
642
- enabled:
643
- identity.enabled ??
644
- isRuntimeStrategyConfigEnabled(effectiveStrategyConfig),
645
- config: effectiveStrategyConfig,
646
- symbols: [...new Set(strategyTrades.map((trade) => trade.symbol))],
647
- stat: analytics.stat,
648
- summary: analytics.summary,
649
- orderLog: analytics.orderLog,
650
- evidenceTimeline: evidenceTimelines.get(
651
- strategyEvidenceTimelineSelectorKey({
652
- strategy: strategyName,
653
- compositionId: identity.releaseCompositionId,
654
- configFingerprint: identity.configFingerprint,
655
- gateFingerprint: identity.gateFingerprint,
656
- contextFingerprint: identity.contextFingerprint,
657
- gitSha: identity.gitSha,
658
- maxLossValue: identity.maxLossValue,
659
- requireCompleteLineage: true,
660
- }),
661
- ) ?? {
662
- status: 'missing',
663
- observedFrom: null,
664
- markers: [],
665
- },
666
- recentTrades: strategyTrades
667
- .slice(0, 8)
668
- .map((trade) => toRuntimeTradeView(trade, endTime)),
669
- orders,
670
- };
671
- }),
672
- );
673
-
674
- strategies.sort((left, right) => {
675
- if (left.stat.netProfit !== right.stat.netProfit) {
676
- return right.stat.netProfit - left.stat.netProfit;
677
- }
678
- if (left.summary.totalPnl !== right.summary.totalPnl) {
679
- return right.summary.totalPnl - left.summary.totalPnl;
680
- }
681
- if (left.connected !== right.connected) {
682
- return left.connected ? -1 : 1;
683
- }
684
- return left.strategyName.localeCompare(right.strategyName);
685
- });
686
-
687
- const response: RuntimeStrategiesResponse = {
688
- provider,
689
- hours,
690
- generatedAt: endTime,
691
- dataSources: {
692
- localTrades: syncedTrades.length,
693
- exchangeFallbackTrades: fallbackTrades.length,
694
- exchangeErrors: [...new Set(exchangeErrors)].sort(),
695
- },
696
- strategies,
697
- };
698
-
699
- return response;
700
- };