@tradejs/app 2.0.21 → 3.0.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.
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,38 @@
1
+ import type { StrategyChartDetail } from '@tradejs/types';
2
+
3
+ const DIRECTION_DETAIL_PREFIX = 'direction:';
4
+ const SYMBOL_DETAIL_PREFIX = 'symbol:';
5
+
6
+ export const isDirectionDetail = (detail: StrategyChartDetail) =>
7
+ detail.id.startsWith(DIRECTION_DETAIL_PREFIX);
8
+
9
+ export const isSymbolDetail = (detail: StrategyChartDetail) =>
10
+ detail.id.startsWith(SYMBOL_DETAIL_PREFIX);
11
+
12
+ export const isStructuredDetail = (detail: StrategyChartDetail) =>
13
+ isDirectionDetail(detail) || isSymbolDetail(detail);
14
+
15
+ export const getDetailById = (
16
+ details: StrategyChartDetail[] | undefined,
17
+ id: string,
18
+ ) => details?.find((detail) => detail.id === id) ?? null;
19
+
20
+ export const parseFormattedNumber = (value: string) => {
21
+ const normalized = value
22
+ .replace(/\s/g, '')
23
+ .replace(',', '.')
24
+ .replace(/[^\d.+-]/g, '');
25
+ const parsed = Number(normalized);
26
+ return Number.isFinite(parsed) ? parsed : null;
27
+ };
28
+
29
+ export const parseConfusionDetail = (
30
+ detail: StrategyChartDetail | null,
31
+ ): number[] | null => {
32
+ if (!detail) return null;
33
+ const values = detail.value
34
+ .split('/')
35
+ .map((part) => parseFormattedNumber(part))
36
+ .filter((value): value is number => value !== null);
37
+ return values.length === 4 ? values : null;
38
+ };
@@ -0,0 +1,263 @@
1
+ import type { StrategyChartDetail, StrategyChartMetric } from '@tradejs/types';
2
+ import { formatInteger } from '#components/Shared/OrdersDrawer';
3
+ import {
4
+ getDetailById,
5
+ isStructuredDetail,
6
+ parseConfusionDetail,
7
+ } from './StrategySnapshotCard.details.presenter';
8
+
9
+ const AI_STAT_DIRECTIONS = ['LONG', 'SHORT'] as const;
10
+ type AiStatDirection = (typeof AI_STAT_DIRECTIONS)[number];
11
+
12
+ export interface DirectionMetric {
13
+ id: string;
14
+ label: string;
15
+ value: string;
16
+ tone?: StrategyChartMetric['tone'];
17
+ }
18
+
19
+ export interface DirectionStatGroup {
20
+ direction: AiStatDirection;
21
+ metrics: DirectionMetric[];
22
+ hasData: boolean;
23
+ }
24
+
25
+ export interface AiDiagnosticMetric {
26
+ id: string;
27
+ label: string;
28
+ value: string;
29
+ detail?: string;
30
+ tone?: StrategyChartMetric['tone'];
31
+ }
32
+
33
+ export interface AiDiagnosticGroup {
34
+ id: string;
35
+ title: string;
36
+ description: string;
37
+ columns: 1 | 2 | 4;
38
+ metrics: AiDiagnosticMetric[];
39
+ }
40
+
41
+ export const getMetricColor = (tone: StrategyChartMetric['tone']) => {
42
+ switch (tone) {
43
+ case 'success':
44
+ return 'teal.500';
45
+ case 'warning':
46
+ return 'fg.warning';
47
+ case 'neutral':
48
+ return 'gray.300';
49
+ case 'error':
50
+ return 'fg.error';
51
+ default:
52
+ return 'gray.200';
53
+ }
54
+ };
55
+
56
+ const directionMetricLabels: Record<string, string> = {
57
+ approved: 'Approved',
58
+ precision: 'Precision',
59
+ monthlyPnl: 'Monthly P&L',
60
+ pnl: 'P&L',
61
+ avgProfit: 'Avg Profit',
62
+ };
63
+
64
+ const directionMetricOrder = [
65
+ 'approved',
66
+ 'precision',
67
+ 'monthlyPnl',
68
+ 'pnl',
69
+ 'avgProfit',
70
+ ] as const;
71
+
72
+ const aiDrawerMetricOrder = [
73
+ 'monthlyPnl',
74
+ 'avgProfit',
75
+ 'maxDrawdown',
76
+ 'maxLossStreak',
77
+ 'approved',
78
+ 'approvedPerDay',
79
+ 'accuracy',
80
+ 'precision',
81
+ ] as const;
82
+
83
+ const aiDrawerMetricOrderIndex = new Map<string, number>(
84
+ aiDrawerMetricOrder.map((metricId, index) => [metricId, index]),
85
+ );
86
+
87
+ export const sortAiDrawerMetrics = (metrics: StrategyChartMetric[]) =>
88
+ metrics
89
+ .filter((metric) => metric.id !== 'recall')
90
+ .sort((left, right) => {
91
+ const leftIndex = aiDrawerMetricOrderIndex.get(left.id) ?? 100;
92
+ const rightIndex = aiDrawerMetricOrderIndex.get(right.id) ?? 100;
93
+
94
+ return leftIndex - rightIndex || left.label.localeCompare(right.label);
95
+ });
96
+
97
+ export const getPnlBarColor = (value: number) => {
98
+ if (value > 0) {
99
+ return 'teal.500';
100
+ }
101
+ if (value < 0) {
102
+ return 'red.500';
103
+ }
104
+ return 'gray.500';
105
+ };
106
+
107
+ export const buildDirectionStatGroups = (
108
+ details: StrategyChartDetail[] | undefined,
109
+ ): DirectionStatGroup[] => {
110
+ const grouped = new Map<AiStatDirection, Map<string, DirectionMetric>>();
111
+
112
+ for (const direction of AI_STAT_DIRECTIONS) {
113
+ grouped.set(direction, new Map());
114
+ }
115
+
116
+ for (const detail of details ?? []) {
117
+ const [, direction, metricId] = detail.id.split(':');
118
+ if (metricId == null) {
119
+ continue;
120
+ }
121
+
122
+ if (direction !== 'LONG' && direction !== 'SHORT') {
123
+ continue;
124
+ }
125
+
126
+ const metric: DirectionMetric = {
127
+ id: metricId,
128
+ label: directionMetricLabels[metricId] ?? detail.label,
129
+ value: detail.value,
130
+ };
131
+ if (detail.tone) {
132
+ metric.tone = detail.tone;
133
+ }
134
+
135
+ grouped.get(direction)?.set(metricId, metric);
136
+ }
137
+
138
+ return AI_STAT_DIRECTIONS.map((direction) => {
139
+ const values = grouped.get(direction) ?? new Map<string, DirectionMetric>();
140
+ const metrics = directionMetricOrder.map(
141
+ (metricId): DirectionMetric =>
142
+ values.get(metricId) ?? {
143
+ id: metricId,
144
+ label: directionMetricLabels[metricId],
145
+ value: 'n/a',
146
+ tone: 'default',
147
+ },
148
+ );
149
+
150
+ return {
151
+ direction,
152
+ metrics,
153
+ hasData: values.size > 0,
154
+ };
155
+ });
156
+ };
157
+
158
+ export const buildAiDiagnosticGroups = (
159
+ details: StrategyChartDetail[] | undefined,
160
+ ): AiDiagnosticGroup[] => {
161
+ const plainDetails = details?.filter((detail) => !isStructuredDetail(detail));
162
+ const groups: AiDiagnosticGroup[] = [];
163
+ const windowDetail = getDetailById(plainDetails, 'window');
164
+ const confusion = parseConfusionDetail(
165
+ getDetailById(plainDetails, 'confusion'),
166
+ );
167
+ const avgProfitAll = getDetailById(plainDetails, 'avgProfitAll');
168
+ const expectancyDelta = getDetailById(plainDetails, 'expectancyDelta');
169
+
170
+ if (windowDetail) {
171
+ groups.push({
172
+ id: 'window',
173
+ title: 'Evaluation window',
174
+ description: 'source rows used for this AI snapshot',
175
+ columns: 1,
176
+ metrics: [
177
+ {
178
+ id: windowDetail.id,
179
+ label: 'Window',
180
+ value: windowDetail.value,
181
+ detail: 'UTC range',
182
+ tone: windowDetail.tone,
183
+ },
184
+ ],
185
+ });
186
+ }
187
+
188
+ if (confusion) {
189
+ const [truePositive, falsePositive, trueNegative, falseNegative] =
190
+ confusion;
191
+
192
+ groups.push({
193
+ id: 'confusion',
194
+ title: 'Decision matrix',
195
+ description: 'approved vs blocked outcomes',
196
+ columns: 4,
197
+ metrics: [
198
+ {
199
+ id: 'truePositive',
200
+ label: 'TP',
201
+ value: formatInteger(truePositive),
202
+ detail: 'winner approved',
203
+ tone: 'success',
204
+ },
205
+ {
206
+ id: 'falsePositive',
207
+ label: 'FP',
208
+ value: formatInteger(falsePositive),
209
+ detail: 'loser approved',
210
+ tone: falsePositive > 0 ? 'warning' : 'neutral',
211
+ },
212
+ {
213
+ id: 'trueNegative',
214
+ label: 'TN',
215
+ value: formatInteger(trueNegative),
216
+ detail: 'loser blocked',
217
+ tone: 'success',
218
+ },
219
+ {
220
+ id: 'falseNegative',
221
+ label: 'FN',
222
+ value: formatInteger(falseNegative),
223
+ detail: 'winner blocked',
224
+ tone: falseNegative > 0 ? 'warning' : 'neutral',
225
+ },
226
+ ],
227
+ });
228
+ }
229
+
230
+ const liftMetrics: AiDiagnosticMetric[] = [];
231
+
232
+ if (avgProfitAll) {
233
+ liftMetrics.push({
234
+ id: avgProfitAll.id,
235
+ label: 'Avg all candidates',
236
+ value: avgProfitAll.value,
237
+ detail: 'before AI approval',
238
+ tone: avgProfitAll.tone,
239
+ });
240
+ }
241
+
242
+ if (expectancyDelta) {
243
+ liftMetrics.push({
244
+ id: expectancyDelta.id,
245
+ label: 'Expectancy lift',
246
+ value: expectancyDelta.value,
247
+ detail: 'approved avg minus all avg',
248
+ tone: expectancyDelta.tone,
249
+ });
250
+ }
251
+
252
+ if (liftMetrics.length) {
253
+ groups.push({
254
+ id: 'lift',
255
+ title: 'Gate lift',
256
+ description: 'what approval changes',
257
+ columns: 2,
258
+ metrics: liftMetrics,
259
+ });
260
+ }
261
+
262
+ return groups;
263
+ };