@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
@@ -22,7 +22,7 @@ import {
22
22
  Textarea,
23
23
  } from '@chakra-ui/react';
24
24
  import type { MarketUniverse } from '@tradejs/types';
25
- import type { RuntimeStrategyView } from '#app/lib/runtimeStrategies';
25
+ import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
26
26
  import {
27
27
  DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS,
28
28
  mergeRuntimeStrategyManagedParameters,
@@ -0,0 +1,677 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Box,
5
+ CloseButton,
6
+ Drawer,
7
+ Flex,
8
+ Grid,
9
+ Portal,
10
+ SimpleGrid,
11
+ Text,
12
+ } from '@chakra-ui/react';
13
+ import type { ThresholdLevel } from '@tradejs/types';
14
+ import {
15
+ formatInteger,
16
+ formatPercent,
17
+ formatSignedNumber,
18
+ getPnlColor,
19
+ type OrdersDrawerSummaryItem,
20
+ } from '#components/Shared/OrdersDrawer';
21
+ import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
22
+ import { buildQuarterlyMonthlyStats } from '#app/lib/strategyPerformance';
23
+ import { AdvancedMetricsPanel } from './AdvancedMetricsPanel';
24
+ import {
25
+ ChartPanel,
26
+ DrawdownTimelineChart,
27
+ PnlDistributionChart,
28
+ RollingPerformanceChart,
29
+ TimeOfDaySessionChart,
30
+ WinLossStreakTimelineChart,
31
+ } from './StrategyPerformanceCharts';
32
+ import {
33
+ buildRuntimeStrategyCardViewModel,
34
+ getMetricColor,
35
+ getPnlBarColor,
36
+ type RuntimeSymbolPnlRank,
37
+ } from './RuntimeStrategyCard.presenter';
38
+
39
+ type RuntimeStrategyCardViewModel = ReturnType<
40
+ typeof buildRuntimeStrategyCardViewModel
41
+ >;
42
+
43
+ const RuntimeOrdersSummaryBlock = ({
44
+ items,
45
+ }: {
46
+ items: OrdersDrawerSummaryItem[];
47
+ }) => (
48
+ <Box
49
+ p={4}
50
+ borderWidth="1px"
51
+ borderColor="gray.800"
52
+ borderRadius="md"
53
+ bg="gray.900"
54
+ >
55
+ <SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
56
+ {items.map((item) => (
57
+ <Box key={item.title}>
58
+ <Text fontSize="sm" color="gray.500">
59
+ {item.title}
60
+ </Text>
61
+ <Text
62
+ mt={1}
63
+ fontSize="2xl"
64
+ color={item.color ?? 'gray.100'}
65
+ fontWeight="bold"
66
+ fontFamily="mono"
67
+ lineHeight="1.1"
68
+ >
69
+ {item.value}
70
+ </Text>
71
+ </Box>
72
+ ))}
73
+ </SimpleGrid>
74
+ </Box>
75
+ );
76
+
77
+ const renderPnlRanking = ({
78
+ title,
79
+ subtitle,
80
+ ranking,
81
+ maxAbsPnl,
82
+ }: {
83
+ title: string;
84
+ subtitle: string;
85
+ ranking: RuntimeSymbolPnlRank[];
86
+ maxAbsPnl: number;
87
+ }) => (
88
+ <Box
89
+ p={4}
90
+ borderWidth="1px"
91
+ borderColor="gray.800"
92
+ borderRadius="md"
93
+ bg="gray.900"
94
+ >
95
+ <Flex justify="space-between" align="baseline" gap={3} mb={4}>
96
+ <Text fontSize="sm" color="gray.300" fontWeight="semibold">
97
+ {title}
98
+ </Text>
99
+ <Text fontSize="xs" color="gray.500">
100
+ {subtitle}
101
+ </Text>
102
+ </Flex>
103
+
104
+ {ranking.length ? (
105
+ <Flex direction="column" gap={3}>
106
+ {ranking.map((rank) => {
107
+ const width = Math.max(4, (Math.abs(rank.pnl) / maxAbsPnl) * 100);
108
+
109
+ return (
110
+ <Grid
111
+ key={rank.symbol}
112
+ templateColumns="1.1fr 2fr 0.8fr"
113
+ alignItems="center"
114
+ gap={3}
115
+ >
116
+ <Box minW={0}>
117
+ <Text
118
+ fontSize="sm"
119
+ color="gray.200"
120
+ fontWeight="bold"
121
+ overflow="hidden"
122
+ textOverflow="ellipsis"
123
+ whiteSpace="nowrap"
124
+ >
125
+ {rank.symbol}
126
+ </Text>
127
+ <Text fontSize="xs" color="gray.500" fontFamily="mono">
128
+ {formatInteger(rank.orders)} orders ·{' '}
129
+ {formatPercent(rank.winRate)}
130
+ </Text>
131
+ <Text fontSize="xs" color="gray.600" fontFamily="mono">
132
+ avg {formatSignedNumber(rank.avgPnl)}
133
+ </Text>
134
+ </Box>
135
+ <Box h="10px" bg="gray.800">
136
+ <Box h="full" w={`${width}%`} bg={getPnlBarColor(rank.pnl)} />
137
+ </Box>
138
+ <Text
139
+ fontSize="sm"
140
+ color={getPnlColor(rank.pnl)}
141
+ fontWeight="bold"
142
+ fontFamily="mono"
143
+ textAlign="right"
144
+ >
145
+ {formatSignedNumber(rank.pnl)}
146
+ </Text>
147
+ </Grid>
148
+ );
149
+ })}
150
+ </Flex>
151
+ ) : (
152
+ <Text fontSize="sm" color="gray.500">
153
+ No symbol P&L data
154
+ </Text>
155
+ )}
156
+ </Box>
157
+ );
158
+
159
+ export const RuntimeStrategyStatsDrawer = ({
160
+ strategy,
161
+ provider,
162
+ open,
163
+ onOpenChange,
164
+ viewModel,
165
+ }: {
166
+ strategy: RuntimeStrategyView;
167
+ provider: string;
168
+ open: boolean;
169
+ onOpenChange: (open: boolean) => void;
170
+ viewModel: RuntimeStrategyCardViewModel;
171
+ }) => {
172
+ const {
173
+ runtimeOrderSummaryItems,
174
+ drawerMetrics,
175
+ symbolConcentration,
176
+ topSymbolPnlRanking,
177
+ worstSymbolPnlRanking,
178
+ symbolRankingMaxAbsPnl,
179
+ directionStats,
180
+ advancedMetrics,
181
+ } = viewModel;
182
+ const {
183
+ monthlyStats,
184
+ tradePoints: runtimeTradePoints,
185
+ drawdownPoints,
186
+ rollingPerformancePoints,
187
+ pnlDistributionBins,
188
+ sessionPnlStats,
189
+ hourlyPnlStats,
190
+ } = viewModel.performance;
191
+
192
+ return (
193
+ <Drawer.Root
194
+ size="xl"
195
+ open={open}
196
+ onOpenChange={(e) => onOpenChange(e.open)}
197
+ >
198
+ <Portal>
199
+ <Drawer.Backdrop />
200
+ <Drawer.Positioner>
201
+ <Drawer.Content
202
+ display="flex"
203
+ flexDirection="column"
204
+ w="50vw"
205
+ minW="640px"
206
+ maxW="50vw"
207
+ bg="gray.950"
208
+ >
209
+ <Drawer.Header>
210
+ <Drawer.Title>{strategy.strategyName}</Drawer.Title>
211
+ <Drawer.CloseTrigger asChild>
212
+ <CloseButton size="sm" />
213
+ </Drawer.CloseTrigger>
214
+ </Drawer.Header>
215
+
216
+ <Drawer.Body
217
+ display="flex"
218
+ flexDirection="column"
219
+ gap={4}
220
+ overflowY="auto"
221
+ flex="1"
222
+ minH="0"
223
+ w="full"
224
+ >
225
+ <RuntimeOrdersSummaryBlock items={runtimeOrderSummaryItems} />
226
+
227
+ <Box
228
+ p={4}
229
+ borderWidth="1px"
230
+ borderColor="gray.800"
231
+ borderRadius="md"
232
+ bg="gray.900"
233
+ >
234
+ <Text fontSize="sm" color="gray.500" mb={3}>
235
+ connector: {provider}
236
+ </Text>
237
+
238
+ <SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
239
+ {drawerMetrics.map((metric) => (
240
+ <Box
241
+ key={metric.id}
242
+ p={3}
243
+ borderWidth="1px"
244
+ borderColor="gray.800"
245
+ borderRadius="md"
246
+ bg="blackAlpha.300"
247
+ >
248
+ <Text
249
+ fontSize="xs"
250
+ color="gray.400"
251
+ fontWeight="semibold"
252
+ textTransform="uppercase"
253
+ >
254
+ {metric.label}
255
+ </Text>
256
+ <Text
257
+ mt={1}
258
+ fontSize="xl"
259
+ color={getMetricColor(metric.level)}
260
+ fontWeight="bold"
261
+ fontFamily="mono"
262
+ lineHeight="1.2"
263
+ >
264
+ {metric.value}
265
+ </Text>
266
+ </Box>
267
+ ))}
268
+ </SimpleGrid>
269
+ </Box>
270
+
271
+ <AdvancedMetricsPanel metrics={advancedMetrics} />
272
+
273
+ {monthlyStats.length ? (
274
+ <Box
275
+ p={4}
276
+ borderWidth="1px"
277
+ borderColor="gray.800"
278
+ borderRadius="md"
279
+ bg="gray.900"
280
+ >
281
+ <Text
282
+ fontSize="sm"
283
+ color="gray.300"
284
+ fontWeight="semibold"
285
+ mb={3}
286
+ >
287
+ Monthly Performance
288
+ </Text>
289
+
290
+ <Flex direction="column" gap={4}>
291
+ {monthlyStats.map((yearGroup) => (
292
+ <Box key={yearGroup.year}>
293
+ <Flex align="center" gap={3} mb={3}>
294
+ <Text
295
+ fontSize="lg"
296
+ color="gray.100"
297
+ fontWeight="bold"
298
+ fontFamily="mono"
299
+ >
300
+ {yearGroup.year}
301
+ </Text>
302
+ <Box flex="1" h="1px" bg="gray.800" />
303
+ </Flex>
304
+
305
+ <Flex direction="column" gap={3}>
306
+ {buildQuarterlyMonthlyStats(yearGroup.months).map(
307
+ (quarter) => (
308
+ <Flex
309
+ key={`${yearGroup.year}-${quarter.label}`}
310
+ align="stretch"
311
+ gap={3}
312
+ >
313
+ <Flex
314
+ w="34px"
315
+ flexShrink={0}
316
+ align="center"
317
+ justify="center"
318
+ >
319
+ <Text
320
+ fontSize="xs"
321
+ color="gray.500"
322
+ fontFamily="mono"
323
+ fontWeight="bold"
324
+ >
325
+ {quarter.label}
326
+ </Text>
327
+ </Flex>
328
+
329
+ <SimpleGrid columns={3} gap={3} flex="1">
330
+ {quarter.months.map((month, monthOffset) => {
331
+ const monthIndex =
332
+ quarter.monthIndexes[monthOffset] ?? 0;
333
+
334
+ if (!month) {
335
+ return (
336
+ <Box
337
+ key={`${quarter.label}-${monthIndex}-empty`}
338
+ minH="116px"
339
+ visibility="hidden"
340
+ />
341
+ );
342
+ }
343
+
344
+ const winRate =
345
+ month.orders > 0
346
+ ? (month.wins / month.orders) * 100
347
+ : null;
348
+
349
+ return (
350
+ <Box
351
+ key={month.id}
352
+ p={3}
353
+ minH="116px"
354
+ borderWidth="1px"
355
+ borderColor="gray.800"
356
+ borderLeftWidth="3px"
357
+ borderLeftColor={getPnlColor(month.pnl)}
358
+ borderRadius="md"
359
+ bg="blackAlpha.300"
360
+ >
361
+ <Flex
362
+ justify="space-between"
363
+ align="baseline"
364
+ gap={2}
365
+ >
366
+ <Text
367
+ fontSize="sm"
368
+ color="gray.200"
369
+ fontWeight="bold"
370
+ >
371
+ {month.monthLabel}
372
+ </Text>
373
+ <Text
374
+ fontSize="xs"
375
+ color="gray.500"
376
+ fontFamily="mono"
377
+ >
378
+ {String(month.monthIndex).padStart(
379
+ 2,
380
+ '0',
381
+ )}
382
+ </Text>
383
+ </Flex>
384
+ <Text
385
+ mt={3}
386
+ fontSize="xl"
387
+ color={getPnlColor(month.pnl)}
388
+ fontWeight="bold"
389
+ fontFamily="mono"
390
+ lineHeight="1.2"
391
+ >
392
+ {formatSignedNumber(month.pnl)}
393
+ </Text>
394
+ <Flex
395
+ mt={3}
396
+ justify="space-between"
397
+ gap={3}
398
+ >
399
+ <Box>
400
+ <Text
401
+ fontSize="xs"
402
+ color="gray.500"
403
+ >
404
+ Orders
405
+ </Text>
406
+ <Text
407
+ fontSize="sm"
408
+ color="gray.300"
409
+ fontFamily="mono"
410
+ fontWeight="semibold"
411
+ >
412
+ {formatInteger(month.orders)}
413
+ </Text>
414
+ </Box>
415
+ <Box textAlign="right">
416
+ <Text
417
+ fontSize="xs"
418
+ color="gray.500"
419
+ >
420
+ Win rate
421
+ </Text>
422
+ <Text
423
+ fontSize="sm"
424
+ color="gray.300"
425
+ fontFamily="mono"
426
+ fontWeight="semibold"
427
+ >
428
+ {formatPercent(winRate)}
429
+ </Text>
430
+ </Box>
431
+ </Flex>
432
+ </Box>
433
+ );
434
+ })}
435
+ </SimpleGrid>
436
+ </Flex>
437
+ ),
438
+ )}
439
+ </Flex>
440
+ </Box>
441
+ ))}
442
+ </Flex>
443
+ </Box>
444
+ ) : null}
445
+
446
+ <Flex direction="column" gap={4}>
447
+ <ChartPanel
448
+ title="Drawdown Timeline"
449
+ subtitle="equity peak to current equity"
450
+ >
451
+ <DrawdownTimelineChart points={drawdownPoints} />
452
+ </ChartPanel>
453
+
454
+ <ChartPanel
455
+ title="Rolling Performance"
456
+ subtitle="last 50 trades"
457
+ >
458
+ <RollingPerformanceChart points={rollingPerformancePoints} />
459
+ </ChartPanel>
460
+
461
+ <ChartPanel
462
+ title="Win / Loss Streak Timeline"
463
+ subtitle="trade sequence"
464
+ >
465
+ <WinLossStreakTimelineChart trades={runtimeTradePoints} />
466
+ </ChartPanel>
467
+
468
+ <ChartPanel
469
+ title="P&L Distribution"
470
+ subtitle="trade result buckets"
471
+ >
472
+ <PnlDistributionChart bins={pnlDistributionBins} />
473
+ </ChartPanel>
474
+
475
+ <ChartPanel title="P&L by Time of Day / Session" subtitle="UTC">
476
+ <TimeOfDaySessionChart
477
+ sessions={sessionPnlStats}
478
+ hours={hourlyPnlStats}
479
+ />
480
+ </ChartPanel>
481
+ </Flex>
482
+
483
+ <Flex direction="column" gap={4}>
484
+ {renderPnlRanking({
485
+ title: 'P&L Ranking',
486
+ subtitle: 'Top 10 contracts',
487
+ ranking: topSymbolPnlRanking,
488
+ maxAbsPnl: symbolRankingMaxAbsPnl,
489
+ })}
490
+ {renderPnlRanking({
491
+ title: 'Worst Contracts',
492
+ subtitle: 'Worst 10 contracts',
493
+ ranking: worstSymbolPnlRanking,
494
+ maxAbsPnl: symbolRankingMaxAbsPnl,
495
+ })}
496
+ </Flex>
497
+
498
+ {symbolConcentration.length ? (
499
+ <Box
500
+ p={4}
501
+ borderWidth="1px"
502
+ borderColor="gray.800"
503
+ borderRadius="md"
504
+ bg="gray.900"
505
+ >
506
+ <Flex justify="space-between" align="baseline" mb={3}>
507
+ <Text fontSize="sm" color="gray.300" fontWeight="semibold">
508
+ Symbol Concentration
509
+ </Text>
510
+ <Text fontSize="xs" color="gray.600" textAlign="right">
511
+ absolute P&L and order share
512
+ </Text>
513
+ </Flex>
514
+
515
+ <Flex direction="column" gap={2}>
516
+ {symbolConcentration.map((row) => (
517
+ <Box
518
+ key={row.symbol}
519
+ p={3}
520
+ borderWidth="1px"
521
+ borderColor="gray.800"
522
+ borderRadius="md"
523
+ bg="blackAlpha.300"
524
+ >
525
+ <Flex justify="space-between" align="center" gap={3}>
526
+ <Box minW="0">
527
+ <Text
528
+ color="gray.100"
529
+ fontSize="sm"
530
+ fontWeight="bold"
531
+ >
532
+ {row.symbol}
533
+ </Text>
534
+ <Text
535
+ mt={1}
536
+ color="gray.500"
537
+ fontSize="xs"
538
+ fontFamily="mono"
539
+ >
540
+ {formatInteger(row.orders)} trades / order share{' '}
541
+ {formatPercent(row.orderShare)}
542
+ </Text>
543
+ </Box>
544
+ <Box textAlign="right" flexShrink={0}>
545
+ <Text
546
+ color={getPnlColor(row.pnl)}
547
+ fontSize="sm"
548
+ fontWeight="bold"
549
+ fontFamily="mono"
550
+ >
551
+ {formatSignedNumber(row.pnl)}
552
+ </Text>
553
+ <Text
554
+ mt={1}
555
+ color="gray.400"
556
+ fontSize="xs"
557
+ fontFamily="mono"
558
+ >
559
+ abs {formatPercent(row.absPnlShare)}
560
+ </Text>
561
+ </Box>
562
+ </Flex>
563
+ </Box>
564
+ ))}
565
+ </Flex>
566
+ </Box>
567
+ ) : null}
568
+
569
+ <Box
570
+ p={4}
571
+ borderWidth="1px"
572
+ borderColor="gray.800"
573
+ borderRadius="md"
574
+ bg="gray.900"
575
+ >
576
+ <Text
577
+ fontSize="sm"
578
+ color="gray.300"
579
+ fontWeight="semibold"
580
+ mb={3}
581
+ >
582
+ LONG / SHORT
583
+ </Text>
584
+ <SimpleGrid columns={{ base: 1, md: 2 }} gap={3}>
585
+ {directionStats.map((group) => {
586
+ const ordersWithPnl = group.closed + group.active;
587
+ const winRate =
588
+ ordersWithPnl > 0
589
+ ? (group.wins / ordersWithPnl) * 100
590
+ : null;
591
+
592
+ return (
593
+ <Box
594
+ key={group.direction}
595
+ p={3}
596
+ borderWidth="1px"
597
+ borderColor="gray.800"
598
+ borderRadius="md"
599
+ bg="blackAlpha.300"
600
+ >
601
+ <Flex justify="space-between" align="center" mb={3}>
602
+ <Text
603
+ fontSize="sm"
604
+ color={
605
+ group.direction === 'LONG'
606
+ ? 'teal.400'
607
+ : 'pink.300'
608
+ }
609
+ fontWeight="bold"
610
+ >
611
+ {group.direction}
612
+ </Text>
613
+ {!group.orders ? (
614
+ <Text fontSize="xs" color="gray.500">
615
+ no data
616
+ </Text>
617
+ ) : null}
618
+ </Flex>
619
+
620
+ <SimpleGrid columns={1} gap={2}>
621
+ {[
622
+ ['Orders', formatInteger(group.orders), 'neutral'],
623
+ ['Active', formatInteger(group.active), 'warning'],
624
+ ['Closed', formatInteger(group.closed), 'neutral'],
625
+ ['Win rate', formatPercent(winRate), 'neutral'],
626
+ [
627
+ 'P&L',
628
+ formatSignedNumber(group.pnl),
629
+ group.pnl > 0
630
+ ? 'success'
631
+ : group.pnl < 0
632
+ ? 'error'
633
+ : 'neutral',
634
+ ],
635
+ [
636
+ 'Avg Profit',
637
+ formatSignedNumber(group.avgPnl),
638
+ (group.avgPnl ?? 0) > 0
639
+ ? 'success'
640
+ : (group.avgPnl ?? 0) < 0
641
+ ? 'error'
642
+ : 'neutral',
643
+ ],
644
+ ].map(([label, value, level]) => (
645
+ <Flex
646
+ key={label}
647
+ justify="space-between"
648
+ align="baseline"
649
+ gap={3}
650
+ >
651
+ <Text fontSize="xs" color="gray.500">
652
+ {label}
653
+ </Text>
654
+ <Text
655
+ fontSize="sm"
656
+ color={getMetricColor(level as ThresholdLevel)}
657
+ fontFamily="mono"
658
+ fontWeight="semibold"
659
+ textAlign="right"
660
+ >
661
+ {value}
662
+ </Text>
663
+ </Flex>
664
+ ))}
665
+ </SimpleGrid>
666
+ </Box>
667
+ );
668
+ })}
669
+ </SimpleGrid>
670
+ </Box>
671
+ </Drawer.Body>
672
+ </Drawer.Content>
673
+ </Drawer.Positioner>
674
+ </Portal>
675
+ </Drawer.Root>
676
+ );
677
+ };