@tradejs/app 2.0.18 → 2.0.19

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 (48) 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/spread/[symbol]/[interval]/route.ts +1 -1
  9. package/src/app/api/spread/summary/route.ts +1 -1
  10. package/src/app/api/strategies/runtime/route.ts +4 -674
  11. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
  12. package/src/app/api/user/runtime-deployments/route.ts +2 -2
  13. package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
  14. package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
  15. package/src/app/components/Backtest/TestList/index.tsx +1 -1
  16. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
  17. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
  18. package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
  19. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
  20. package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
  21. package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
  22. package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
  23. package/src/app/components/Shared/Filters/context.ts +1 -1
  24. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +81 -858
  25. package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
  26. package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
  27. package/src/app/components/UI/Segment/index.tsx +1 -1
  28. package/src/app/components/UI/Select/index.tsx +1 -1
  29. package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
  30. package/src/app/lib/backtestJobContracts.ts +67 -0
  31. package/src/app/lib/backtestJobProgress.ts +28 -0
  32. package/src/app/lib/backtestJobRequest.ts +107 -0
  33. package/src/app/lib/backtestJobs.ts +25 -257
  34. package/src/app/lib/runtimeDashboard.ts +684 -0
  35. package/src/app/lib/runtimeStrategies.ts +24 -454
  36. package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
  37. package/src/app/lib/runtimeStrategyLineage.ts +264 -0
  38. package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
  39. package/src/app/lib/runtimeTradeSync.ts +1 -1
  40. package/src/app/lib/strategyPerformance.ts +387 -0
  41. package/src/app/routes/dashboard/Dashboard.tsx +2 -6
  42. package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
  43. package/src/app/routes/derivatives/page.tsx +70 -262
  44. package/src/app/store/filters.ts +2 -1
  45. package/src/app/store/indicators.ts +2 -1
  46. package/src/app/store/tests.ts +1 -1
  47. package/src/app/store/tickers.ts +2 -1
  48. package/src/app/types/ui.ts +20 -0
@@ -0,0 +1,419 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { Box, Flex, SimpleGrid, Text } from '@chakra-ui/react';
5
+ import {
6
+ formatInteger,
7
+ formatPercent,
8
+ formatSignedNumber,
9
+ getPnlColor,
10
+ } from '#components/Shared/OrdersDrawer';
11
+ import type {
12
+ DistributionBin,
13
+ DrawdownPoint,
14
+ HourlyPnlStat,
15
+ RollingPerformancePoint,
16
+ SessionPnlStat,
17
+ StrategyTradePoint,
18
+ } from '#app/lib/strategyPerformance';
19
+
20
+ const CHART_WIDTH = 640;
21
+ const CHART_HEIGHT = 120;
22
+ const CHART_PADDING = 10;
23
+ const POSITIVE_CHART_COLOR = '#5eead4';
24
+ const NEGATIVE_CHART_COLOR = '#f87171';
25
+ const NEUTRAL_CHART_COLOR = '#6b7280';
26
+
27
+ const getChartPnlColor = (value: number) =>
28
+ value > 0
29
+ ? POSITIVE_CHART_COLOR
30
+ : value < 0
31
+ ? NEGATIVE_CHART_COLOR
32
+ : NEUTRAL_CHART_COLOR;
33
+
34
+ const buildPolylinePoints = (values: number[]) => {
35
+ if (!values.length) {
36
+ return '';
37
+ }
38
+
39
+ const min = Math.min(...values);
40
+ const max = Math.max(...values);
41
+ const range = max - min || 1;
42
+ const drawableWidth = CHART_WIDTH - CHART_PADDING * 2;
43
+ const drawableHeight = CHART_HEIGHT - CHART_PADDING * 2;
44
+
45
+ return values
46
+ .map((value, index) => {
47
+ const x =
48
+ CHART_PADDING +
49
+ (values.length === 1
50
+ ? 0
51
+ : (index / (values.length - 1)) * drawableWidth);
52
+ const y = CHART_PADDING + ((max - value) / range) * drawableHeight;
53
+
54
+ return `${x.toFixed(2)},${y.toFixed(2)}`;
55
+ })
56
+ .join(' ');
57
+ };
58
+
59
+ export const ChartPanel = ({
60
+ title,
61
+ subtitle,
62
+ children,
63
+ }: {
64
+ title: string;
65
+ subtitle?: string;
66
+ children: ReactNode;
67
+ }) => (
68
+ <Box
69
+ p={4}
70
+ borderWidth="1px"
71
+ borderColor="gray.800"
72
+ borderRadius="md"
73
+ bg="gray.900"
74
+ minH="210px"
75
+ >
76
+ <Flex justify="space-between" align="baseline" gap={3} mb={3}>
77
+ <Text fontSize="sm" color="gray.300" fontWeight="semibold">
78
+ {title}
79
+ </Text>
80
+ {subtitle ? (
81
+ <Text fontSize="xs" color="gray.500" textAlign="right">
82
+ {subtitle}
83
+ </Text>
84
+ ) : null}
85
+ </Flex>
86
+ {children}
87
+ </Box>
88
+ );
89
+
90
+ const EmptyChart = () => (
91
+ <Flex h="140px" align="center" justify="center">
92
+ <Text fontSize="sm" color="gray.500">
93
+ No trade data
94
+ </Text>
95
+ </Flex>
96
+ );
97
+
98
+ export const DrawdownTimelineChart = ({
99
+ points,
100
+ }: {
101
+ points: DrawdownPoint[];
102
+ }) => {
103
+ if (points.length < 2) {
104
+ return <EmptyChart />;
105
+ }
106
+
107
+ const values = points.map((point) => -point.drawdownPercent);
108
+ const maxDrawdown = Math.max(...points.map((point) => point.drawdownPercent));
109
+ const linePoints = buildPolylinePoints(values);
110
+
111
+ return (
112
+ <Box>
113
+ <svg
114
+ width="100%"
115
+ viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
116
+ role="img"
117
+ aria-label="Drawdown timeline"
118
+ >
119
+ <line
120
+ x1={CHART_PADDING}
121
+ x2={CHART_WIDTH - CHART_PADDING}
122
+ y1={CHART_PADDING}
123
+ y2={CHART_PADDING}
124
+ stroke="#374151"
125
+ strokeDasharray="4 4"
126
+ />
127
+ <polyline
128
+ points={linePoints}
129
+ fill="none"
130
+ stroke={NEGATIVE_CHART_COLOR}
131
+ strokeWidth="2"
132
+ />
133
+ </svg>
134
+ <Flex justify="space-between" align="center" mt={2}>
135
+ <Text fontSize="xs" color="gray.500">
136
+ max drawdown
137
+ </Text>
138
+ <Text
139
+ fontSize="sm"
140
+ color="orange.300"
141
+ fontFamily="mono"
142
+ fontWeight="bold"
143
+ >
144
+ {formatPercent(maxDrawdown)}
145
+ </Text>
146
+ </Flex>
147
+ </Box>
148
+ );
149
+ };
150
+
151
+ export const WinLossStreakTimelineChart = ({
152
+ trades,
153
+ }: {
154
+ trades: StrategyTradePoint[];
155
+ }) => {
156
+ if (!trades.length) {
157
+ return <EmptyChart />;
158
+ }
159
+
160
+ const maxAbsPnl = Math.max(...trades.map((trade) => Math.abs(trade.pnl)), 1);
161
+ const centerY = CHART_HEIGHT / 2;
162
+ const barWidth = CHART_WIDTH / trades.length;
163
+
164
+ return (
165
+ <Box>
166
+ <svg
167
+ width="100%"
168
+ viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
169
+ role="img"
170
+ aria-label="Win loss streak timeline"
171
+ >
172
+ <line
173
+ x1="0"
174
+ x2={CHART_WIDTH}
175
+ y1={centerY}
176
+ y2={centerY}
177
+ stroke="#374151"
178
+ />
179
+ {trades.map((trade, index) => {
180
+ const height = Math.max(2, (Math.abs(trade.pnl) / maxAbsPnl) * 48);
181
+ const isWin = trade.pnl > 0;
182
+ const y = isWin ? centerY - height : centerY;
183
+
184
+ return (
185
+ <rect
186
+ key={`${trade.timestamp}-${index}`}
187
+ x={index * barWidth}
188
+ y={y}
189
+ width={Math.max(1, barWidth - 0.4)}
190
+ height={height}
191
+ fill={getChartPnlColor(trade.pnl)}
192
+ opacity="0.9"
193
+ />
194
+ );
195
+ })}
196
+ </svg>
197
+ <Flex justify="space-between" align="center" mt={2}>
198
+ <Text fontSize="xs" color="gray.500">
199
+ wins above line, losses below
200
+ </Text>
201
+ <Text
202
+ fontSize="sm"
203
+ color="gray.300"
204
+ fontFamily="mono"
205
+ fontWeight="bold"
206
+ >
207
+ {formatInteger(trades.length)} trades
208
+ </Text>
209
+ </Flex>
210
+ </Box>
211
+ );
212
+ };
213
+
214
+ export const PnlDistributionChart = ({ bins }: { bins: DistributionBin[] }) => {
215
+ if (!bins.length) {
216
+ return <EmptyChart />;
217
+ }
218
+
219
+ const maxCount = Math.max(...bins.map((bin) => bin.count), 1);
220
+ const barWidth = CHART_WIDTH / bins.length;
221
+
222
+ return (
223
+ <Box>
224
+ <svg
225
+ width="100%"
226
+ viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
227
+ role="img"
228
+ aria-label="P and L distribution"
229
+ >
230
+ {bins.map((bin, index) => {
231
+ const height = Math.max(2, (bin.count / maxCount) * 92);
232
+ const x = index * barWidth + 2;
233
+ const y = CHART_HEIGHT - height - CHART_PADDING;
234
+ const midpoint = (bin.min + bin.max) / 2;
235
+
236
+ return (
237
+ <rect
238
+ key={bin.id}
239
+ x={x}
240
+ y={y}
241
+ width={Math.max(2, barWidth - 4)}
242
+ height={height}
243
+ fill={getChartPnlColor(midpoint)}
244
+ />
245
+ );
246
+ })}
247
+ </svg>
248
+ <Flex justify="space-between" align="center" mt={2}>
249
+ <Text fontSize="xs" color="gray.500">
250
+ trade P&L buckets
251
+ </Text>
252
+ <Text
253
+ fontSize="sm"
254
+ color="gray.300"
255
+ fontFamily="mono"
256
+ fontWeight="bold"
257
+ >
258
+ {formatInteger(bins.reduce((sum, bin) => sum + bin.count, 0))}
259
+ </Text>
260
+ </Flex>
261
+ </Box>
262
+ );
263
+ };
264
+
265
+ export const RollingPerformanceChart = ({
266
+ points,
267
+ }: {
268
+ points: RollingPerformancePoint[];
269
+ }) => {
270
+ if (points.length < 2) {
271
+ return <EmptyChart />;
272
+ }
273
+
274
+ const winRateLine = buildPolylinePoints(points.map((point) => point.winRate));
275
+ const pnlLine = buildPolylinePoints(points.map((point) => point.pnl));
276
+ const latest = points[points.length - 1];
277
+
278
+ return (
279
+ <Box>
280
+ <svg
281
+ width="100%"
282
+ viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
283
+ role="img"
284
+ aria-label="Rolling win rate and rolling P and L"
285
+ >
286
+ <line
287
+ x1={CHART_PADDING}
288
+ x2={CHART_WIDTH - CHART_PADDING}
289
+ y1={CHART_HEIGHT / 2}
290
+ y2={CHART_HEIGHT / 2}
291
+ stroke="#374151"
292
+ strokeDasharray="4 4"
293
+ />
294
+ <polyline
295
+ points={pnlLine}
296
+ fill="none"
297
+ stroke={POSITIVE_CHART_COLOR}
298
+ strokeWidth="2"
299
+ />
300
+ <polyline
301
+ points={winRateLine}
302
+ fill="none"
303
+ stroke="#fbbf24"
304
+ strokeWidth="2"
305
+ opacity="0.9"
306
+ />
307
+ </svg>
308
+ <Flex justify="space-between" align="center" mt={2}>
309
+ <Text fontSize="xs" color="gray.500">
310
+ teal P&L, yellow win rate
311
+ </Text>
312
+ <Text
313
+ fontSize="sm"
314
+ color="gray.300"
315
+ fontFamily="mono"
316
+ fontWeight="bold"
317
+ >
318
+ {formatPercent(latest?.winRate)} /{' '}
319
+ {formatSignedNumber(latest?.pnl ?? null)}
320
+ </Text>
321
+ </Flex>
322
+ </Box>
323
+ );
324
+ };
325
+
326
+ export const TimeOfDaySessionChart = ({
327
+ sessions,
328
+ hours,
329
+ }: {
330
+ sessions: SessionPnlStat[];
331
+ hours: HourlyPnlStat[];
332
+ }) => {
333
+ if (!sessions.some((session) => session.orders > 0)) {
334
+ return <EmptyChart />;
335
+ }
336
+
337
+ const maxSessionAbsPnl = Math.max(
338
+ ...sessions.map((session) => Math.abs(session.pnl)),
339
+ 1,
340
+ );
341
+ const maxHourAbsPnl = Math.max(...hours.map((hour) => Math.abs(hour.pnl)), 1);
342
+
343
+ return (
344
+ <Flex direction="column" gap={4}>
345
+ <SimpleGrid columns={3} gap={3}>
346
+ {sessions.map((session) => {
347
+ const width = Math.max(
348
+ 4,
349
+ (Math.abs(session.pnl) / maxSessionAbsPnl) * 100,
350
+ );
351
+
352
+ return (
353
+ <Box key={session.session}>
354
+ <Flex justify="space-between" align="center" mb={1}>
355
+ <Text fontSize="xs" color="gray.400" fontWeight="semibold">
356
+ {session.session}
357
+ </Text>
358
+ <Text
359
+ fontSize="xs"
360
+ color={getPnlColor(session.pnl)}
361
+ fontFamily="mono"
362
+ fontWeight="bold"
363
+ >
364
+ {formatSignedNumber(session.pnl)}
365
+ </Text>
366
+ </Flex>
367
+ <Box h="8px" bg="gray.800">
368
+ <Box
369
+ h="full"
370
+ w={`${width}%`}
371
+ bg={getChartPnlColor(session.pnl)}
372
+ />
373
+ </Box>
374
+ <Text mt={1} fontSize="xs" color="gray.500" fontFamily="mono">
375
+ {formatInteger(session.orders)} orders
376
+ </Text>
377
+ </Box>
378
+ );
379
+ })}
380
+ </SimpleGrid>
381
+
382
+ <svg
383
+ width="100%"
384
+ viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
385
+ role="img"
386
+ aria-label="P and L by UTC hour"
387
+ >
388
+ <line
389
+ x1="0"
390
+ x2={CHART_WIDTH}
391
+ y1={CHART_HEIGHT / 2}
392
+ y2={CHART_HEIGHT / 2}
393
+ stroke="#374151"
394
+ />
395
+ {hours.map((hour) => {
396
+ const barWidth = CHART_WIDTH / 24;
397
+ const height = Math.max(1, (Math.abs(hour.pnl) / maxHourAbsPnl) * 46);
398
+ const isPositive = hour.pnl >= 0;
399
+ const y = isPositive ? CHART_HEIGHT / 2 - height : CHART_HEIGHT / 2;
400
+
401
+ return (
402
+ <rect
403
+ key={hour.hour}
404
+ x={hour.hour * barWidth + 2}
405
+ y={y}
406
+ width={Math.max(2, barWidth - 4)}
407
+ height={height}
408
+ fill={getChartPnlColor(hour.pnl)}
409
+ opacity={hour.orders > 0 ? 0.95 : 0.2}
410
+ />
411
+ );
412
+ })}
413
+ </svg>
414
+ <Text fontSize="xs" color="gray.500">
415
+ UTC hours, sessions: Asia 00-07, Europe 08-15, US 16-23
416
+ </Text>
417
+ </Flex>
418
+ );
419
+ };