@tradejs/app 2.0.21 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +8 -8
  2. package/src/app/actions/backtest.ts +1 -1
  3. package/src/app/actions/strategies.ts +1 -1
  4. package/src/app/api/ai/route.ts +13 -6
  5. package/src/app/api/user/settings/route.ts +48 -28
  6. package/src/app/components/Shared/OrdersDrawer.tsx +4 -2
  7. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +3 -3
  8. package/src/app/components/Strategies/RuntimeStrategyCard.presenter.ts +536 -0
  9. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +16 -1207
  10. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +1 -1
  11. package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +677 -0
  12. package/src/app/components/Strategies/StrategySnapshotCard.details.presenter.ts +38 -0
  13. package/src/app/components/Strategies/StrategySnapshotCard.diagnostics.presenter.ts +263 -0
  14. package/src/app/components/Strategies/StrategySnapshotCard.orders.presenter.ts +680 -0
  15. package/src/app/components/Strategies/StrategySnapshotCard.presenter.ts +119 -0
  16. package/src/app/components/Strategies/StrategySnapshotCard.ranking.presenter.ts +76 -0
  17. package/src/app/components/Strategies/StrategySnapshotCard.tsx +13 -1793
  18. package/src/app/components/Strategies/StrategySnapshotCardDetailsDrawer.tsx +682 -0
  19. package/src/app/lib/runtimeStrategies.ts +7 -80
  20. package/src/app/lib/runtimeStrategyContracts.ts +85 -0
  21. package/src/app/routes/backtest/BacktestJobItem.tsx +275 -0
  22. package/src/app/routes/backtest/BacktestRunForm.tsx +502 -0
  23. package/src/app/routes/backtest/page.tsx +16 -1099
  24. package/src/app/routes/backtest/useBacktestRunsController.ts +441 -0
  25. package/src/app/routes/strategies/StrategiesPageClient.tsx +1 -1
@@ -0,0 +1,119 @@
1
+ import { calculateAdvancedTradeMetrics } from '@tradejs/core/backtest';
2
+ import type { StrategyChartSnapshot } from '@tradejs/types';
3
+ import { formatInteger } from '#components/Shared/OrdersDrawer';
4
+ import {
5
+ buildStrategyPerformanceViewModel,
6
+ calculateMaxLossStreak,
7
+ formatMaxDrawdownPercent as calculateMaxDrawdownPercent,
8
+ } from '#app/lib/strategyPerformance';
9
+ import {
10
+ buildSnapshotAdvancedTrades,
11
+ buildSnapshotOrders,
12
+ buildSnapshotSummaryMetrics,
13
+ } from './StrategySnapshotCard.orders.presenter';
14
+ import {
15
+ buildAiDiagnosticGroups,
16
+ buildDirectionStatGroups,
17
+ sortAiDrawerMetrics,
18
+ } from './StrategySnapshotCard.diagnostics.presenter';
19
+ import { buildSymbolPnlRanking } from './StrategySnapshotCard.ranking.presenter';
20
+
21
+ export { SNAPSHOT_ORDER_ROW_HEIGHT } from './StrategySnapshotCard.orders.presenter';
22
+ export {
23
+ getMetricColor,
24
+ type AiDiagnosticGroup,
25
+ type AiDiagnosticMetric,
26
+ type DirectionMetric,
27
+ type DirectionStatGroup,
28
+ } from './StrategySnapshotCard.diagnostics.presenter';
29
+ export {
30
+ getPnlBarColor,
31
+ type SymbolPnlRank,
32
+ } from './StrategySnapshotCard.ranking.presenter';
33
+
34
+ export const buildStrategySnapshotCardViewModel = (
35
+ snapshot: StrategyChartSnapshot,
36
+ mode: 'replay' | 'ai',
37
+ ) => {
38
+ const snapshotOrders = buildSnapshotOrders(snapshot, mode);
39
+ const symbolPnlRanking = buildSymbolPnlRanking(snapshot.details);
40
+ const performance = buildStrategyPerformanceViewModel(snapshot.orderLog);
41
+ const maxLossStreak = calculateMaxLossStreak(snapshot.orderLog);
42
+ const drawerBaseMetrics =
43
+ mode === 'ai'
44
+ ? snapshot.metrics
45
+ .filter((metric) => metric.id !== 'pnl')
46
+ .map((metric) =>
47
+ metric.id === 'quality' || metric.label === 'Quality'
48
+ ? {
49
+ id: 'maxDrawdown',
50
+ label: 'Max drawdown',
51
+ value:
52
+ calculateMaxDrawdownPercent(snapshot.orderLog) ?? 'n/a',
53
+ tone: 'warning' as const,
54
+ }
55
+ : metric,
56
+ )
57
+ : snapshot.metrics;
58
+ const firstPoint = snapshot.orderLog[0];
59
+ const lastPoint = snapshot.orderLog[snapshot.orderLog.length - 1];
60
+ const symbolsLabel =
61
+ snapshot.symbols.length > 3
62
+ ? `${snapshot.symbols.slice(0, 3).join(', ')} +${snapshot.symbols.length - 3}`
63
+ : snapshot.symbols.join(', ') || 'n/a';
64
+
65
+ return {
66
+ snapshotOrders,
67
+ aiDiagnosticGroups: buildAiDiagnosticGroups(snapshot.details),
68
+ directionStatGroups: buildDirectionStatGroups(snapshot.details),
69
+ symbolPnlRanking,
70
+ topSymbolPnlRanking: [...symbolPnlRanking]
71
+ .sort(
72
+ (left, right) =>
73
+ right.pnl - left.pnl || left.symbol.localeCompare(right.symbol),
74
+ )
75
+ .slice(0, 10),
76
+ worstSymbolPnlRanking: [...symbolPnlRanking]
77
+ .sort(
78
+ (left, right) =>
79
+ left.pnl - right.pnl || left.symbol.localeCompare(right.symbol),
80
+ )
81
+ .slice(0, 10),
82
+ symbolRankingMaxAbsPnl: Math.max(
83
+ ...symbolPnlRanking.map((rank) => Math.abs(rank.pnl)),
84
+ 1,
85
+ ),
86
+ performance,
87
+ symbolsLabel,
88
+ sourceLabel: mode === 'ai' && snapshot.datasetId ? 'dataset:' : 'symbols:',
89
+ sourceValue:
90
+ mode === 'ai' && snapshot.datasetId ? snapshot.datasetId : symbolsLabel,
91
+ tagsLabel: snapshot.tags?.join(' · ') ?? '',
92
+ displaySubtitle:
93
+ mode === 'ai'
94
+ ? snapshot.subtitle?.replace(/^q\d+\+\s*(?:·\s*)?/i, '').trim()
95
+ : snapshot.subtitle,
96
+ metrics: buildSnapshotSummaryMetrics(snapshot),
97
+ drawerMetrics:
98
+ mode === 'ai'
99
+ ? sortAiDrawerMetrics([
100
+ ...drawerBaseMetrics,
101
+ {
102
+ id: 'maxLossStreak',
103
+ label: 'Max loss streak',
104
+ value: formatInteger(maxLossStreak),
105
+ tone:
106
+ maxLossStreak > 0 ? ('warning' as const) : ('success' as const),
107
+ },
108
+ ])
109
+ : drawerBaseMetrics,
110
+ advancedMetrics: calculateAdvancedTradeMetrics({
111
+ trades: buildSnapshotAdvancedTrades(snapshot),
112
+ orderLog: snapshot.orderLog,
113
+ startTimestamp: firstPoint?.[0] ?? null,
114
+ endTimestamp: lastPoint?.[0] ?? null,
115
+ }),
116
+ hasOrdersDrawer: snapshotOrders.length > 0,
117
+ hasStatDrawer: mode === 'ai' || Boolean(snapshot.details?.length),
118
+ };
119
+ };
@@ -0,0 +1,76 @@
1
+ import type { StrategyChartDetail } from '@tradejs/types';
2
+ import {
3
+ isSymbolDetail,
4
+ parseFormattedNumber,
5
+ } from './StrategySnapshotCard.details.presenter';
6
+
7
+ export interface SymbolPnlRank {
8
+ symbol: string;
9
+ pnl: number;
10
+ orders: number | null;
11
+ winRate: number | null;
12
+ avgPnl: number | null;
13
+ }
14
+
15
+ export const getPnlBarColor = (value: number) => {
16
+ if (value > 0) return 'teal.500';
17
+ if (value < 0) return 'red.500';
18
+ return 'gray.500';
19
+ };
20
+
21
+ export const buildSymbolPnlRanking = (
22
+ details: StrategyChartDetail[] | undefined,
23
+ ): SymbolPnlRank[] => {
24
+ const grouped = new Map<string, Partial<SymbolPnlRank>>();
25
+
26
+ for (const detail of details ?? []) {
27
+ if (!isSymbolDetail(detail)) {
28
+ continue;
29
+ }
30
+
31
+ const [, symbol, metricId] = detail.id.split(':');
32
+ if (!symbol || !metricId) {
33
+ continue;
34
+ }
35
+
36
+ const current = grouped.get(symbol) ?? { symbol };
37
+ if (metricId === 'pnl') {
38
+ const pnl = parseFormattedNumber(detail.value);
39
+ if (pnl != null) {
40
+ current.pnl = pnl;
41
+ }
42
+ }
43
+ if (metricId === 'orders') {
44
+ current.orders = parseFormattedNumber(detail.value);
45
+ }
46
+ if (metricId === 'winRate') {
47
+ current.winRate = parseFormattedNumber(detail.value);
48
+ }
49
+
50
+ grouped.set(symbol, current);
51
+ }
52
+
53
+ return [...grouped.values()]
54
+ .filter(
55
+ (rank): rank is SymbolPnlRank =>
56
+ typeof rank.symbol === 'string' &&
57
+ typeof rank.pnl === 'number' &&
58
+ Number.isFinite(rank.pnl),
59
+ )
60
+ .map((rank) => ({
61
+ symbol: rank.symbol,
62
+ pnl: rank.pnl,
63
+ orders: rank.orders ?? null,
64
+ winRate: rank.winRate ?? null,
65
+ avgPnl:
66
+ typeof rank.orders === 'number' && rank.orders > 0
67
+ ? rank.pnl / rank.orders
68
+ : null,
69
+ }))
70
+ .sort(
71
+ (left, right) =>
72
+ Math.abs(right.pnl) - Math.abs(left.pnl) ||
73
+ right.pnl - left.pnl ||
74
+ left.symbol.localeCompare(right.symbol),
75
+ );
76
+ };