@tradejs/app 2.0.18 → 2.0.20

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