@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
@@ -9,12 +9,15 @@ import type {
9
9
  PositionPnlSnapshot,
10
10
  RuntimeTradeRecord,
11
11
  SimpleOrderLogData,
12
- StrategyConfig,
13
- StrategyEvidenceTimeline,
14
12
  TestStat,
15
- MarketUniverse,
16
- Interval,
17
13
  } from '@tradejs/types';
14
+ import type { RuntimeStrategyTradeView } from './runtimeStrategyContracts';
15
+ export type {
16
+ RuntimeStrategiesResponse,
17
+ RuntimeStrategyTradeSummary,
18
+ RuntimeStrategyTradeView,
19
+ RuntimeStrategyView,
20
+ } from './runtimeStrategyContracts';
18
21
  import {
19
22
  takeExactClosedPnlMatch,
20
23
  type ClosedPnlRecordWithOrderLinkId,
@@ -44,82 +47,6 @@ type RuntimeTradeWithResolvedPnl = RuntimeTradeRecord & {
44
47
  resolvedTimestamp: number;
45
48
  };
46
49
 
47
- export interface RuntimeStrategyTradeSummary {
48
- totalTrades: number;
49
- activeTrades: number;
50
- closedTrades: number;
51
- wins: number;
52
- losses: number;
53
- activePnl: number;
54
- closedPnl: number;
55
- totalPnl: number;
56
- symbolConcentrationTop1: number | null;
57
- symbolConcentrationTop5: number | null;
58
- }
59
-
60
- export interface RuntimeStrategyTradeView {
61
- orderId: string;
62
- symbol: string;
63
- direction: RuntimeTradeRecord['direction'];
64
- status: RuntimeTradeRecord['status'];
65
- qty: number;
66
- entryTimestamp: number;
67
- entryPrice: number;
68
- actualEntryPrice: number | null;
69
- exitTimestamp: number | null;
70
- exitPrice: number | null;
71
- actualExitPrice: number | null;
72
- currentPrice: number | null;
73
- pnl: number | null;
74
- durationHours: number | null;
75
- entrySlippagePercent: number | null;
76
- exitSlippagePercent: number | null;
77
- exitType: RuntimeTradeRecord['exitType'] | null;
78
- takeProfitPrice: number | null;
79
- stopLossPrice: number | null;
80
- takeProfitPercent: number | null;
81
- stopLossPercent: number | null;
82
- openFee: number | null;
83
- closeFee: number | null;
84
- fundingFee: number | null;
85
- totalFee: number | null;
86
- lastSyncedAt: number | null;
87
- }
88
-
89
- export interface RuntimeStrategyView {
90
- runtimeKey: string;
91
- strategyName: string;
92
- configId: string;
93
- interval: Interval;
94
- universe: MarketUniverse;
95
- accountId?: string;
96
- accountLabel?: string;
97
- deploymentId?: string;
98
- policyProfileId?: string;
99
- connected: boolean;
100
- enabled: boolean;
101
- config: StrategyConfig | null;
102
- symbols: string[];
103
- stat: TestStat;
104
- summary: RuntimeStrategyTradeSummary;
105
- orderLog: SimpleOrderLogData;
106
- evidenceTimeline: StrategyEvidenceTimeline;
107
- recentTrades: RuntimeStrategyTradeView[];
108
- orders: RuntimeStrategyTradeView[];
109
- }
110
-
111
- export interface RuntimeStrategiesResponse {
112
- provider: string;
113
- hours: number;
114
- generatedAt: number;
115
- dataSources?: {
116
- localTrades: number;
117
- exchangeFallbackTrades: number;
118
- exchangeErrors: string[];
119
- };
120
- strategies: RuntimeStrategyView[];
121
- }
122
-
123
50
  const roundValue = (value: number, digits = 2) => {
124
51
  if (!Number.isFinite(value)) {
125
52
  return 0;
@@ -0,0 +1,85 @@
1
+ import type {
2
+ Interval,
3
+ MarketUniverse,
4
+ RuntimeTradeRecord,
5
+ SimpleOrderLogData,
6
+ StrategyConfig,
7
+ StrategyEvidenceTimeline,
8
+ TestStat,
9
+ } from '@tradejs/types';
10
+
11
+ export interface RuntimeStrategyTradeSummary {
12
+ totalTrades: number;
13
+ activeTrades: number;
14
+ closedTrades: number;
15
+ wins: number;
16
+ losses: number;
17
+ activePnl: number;
18
+ closedPnl: number;
19
+ totalPnl: number;
20
+ symbolConcentrationTop1: number | null;
21
+ symbolConcentrationTop5: number | null;
22
+ }
23
+
24
+ export interface RuntimeStrategyTradeView {
25
+ orderId: string;
26
+ symbol: string;
27
+ direction: RuntimeTradeRecord['direction'];
28
+ status: RuntimeTradeRecord['status'];
29
+ qty: number;
30
+ entryTimestamp: number;
31
+ entryPrice: number;
32
+ actualEntryPrice: number | null;
33
+ exitTimestamp: number | null;
34
+ exitPrice: number | null;
35
+ actualExitPrice: number | null;
36
+ currentPrice: number | null;
37
+ pnl: number | null;
38
+ durationHours: number | null;
39
+ entrySlippagePercent: number | null;
40
+ exitSlippagePercent: number | null;
41
+ exitType: RuntimeTradeRecord['exitType'] | null;
42
+ takeProfitPrice: number | null;
43
+ stopLossPrice: number | null;
44
+ takeProfitPercent: number | null;
45
+ stopLossPercent: number | null;
46
+ openFee: number | null;
47
+ closeFee: number | null;
48
+ fundingFee: number | null;
49
+ totalFee: number | null;
50
+ lastSyncedAt: number | null;
51
+ }
52
+
53
+ export interface RuntimeStrategyView {
54
+ runtimeKey: string;
55
+ strategyName: string;
56
+ configId: string;
57
+ interval: Interval;
58
+ universe: MarketUniverse;
59
+ accountId?: string;
60
+ accountLabel?: string;
61
+ deploymentId?: string;
62
+ policyProfileId?: string;
63
+ connected: boolean;
64
+ enabled: boolean;
65
+ config: StrategyConfig | null;
66
+ symbols: string[];
67
+ stat: TestStat;
68
+ summary: RuntimeStrategyTradeSummary;
69
+ orderLog: SimpleOrderLogData;
70
+ evidenceTimeline: StrategyEvidenceTimeline;
71
+ recentTrades: RuntimeStrategyTradeView[];
72
+ orders: RuntimeStrategyTradeView[];
73
+ }
74
+
75
+ export interface RuntimeStrategiesResponse {
76
+ provider: string;
77
+ hours: number;
78
+ generatedAt: number;
79
+ dataSources?: {
80
+ localTrades: number;
81
+ exchangeFallbackTrades: number;
82
+ exchangeErrors: string[];
83
+ };
84
+ strategies: RuntimeStrategyView[];
85
+ }
@@ -0,0 +1,275 @@
1
+ 'use client';
2
+
3
+ import { Badge, Box, Button, Flex, Grid, Stack, Text } from '@chakra-ui/react';
4
+ import {
5
+ FiFolder,
6
+ FiPause,
7
+ FiPlay,
8
+ FiSquare,
9
+ FiTrash2,
10
+ FiX,
11
+ } from 'react-icons/fi';
12
+ import type {
13
+ BacktestJobRecord,
14
+ BacktestJobStatus,
15
+ } from '#app/lib/backtestJobContracts';
16
+ import type { JobAction } from './useBacktestRunsController';
17
+
18
+ const formatNumber = (value: number | null | undefined, fractionDigits = 1) =>
19
+ typeof value === 'number' && Number.isFinite(value)
20
+ ? value.toFixed(fractionDigits)
21
+ : '-';
22
+
23
+ const formatDateTime = (value: string | undefined) => {
24
+ if (!value) {
25
+ return '-';
26
+ }
27
+
28
+ const timestamp = Date.parse(value);
29
+ if (!Number.isFinite(timestamp)) {
30
+ return '-';
31
+ }
32
+
33
+ return new Intl.DateTimeFormat('en', {
34
+ month: 'short',
35
+ day: '2-digit',
36
+ hour: '2-digit',
37
+ minute: '2-digit',
38
+ }).format(timestamp);
39
+ };
40
+
41
+ const statusTone = (status: BacktestJobStatus) => {
42
+ if (status === 'running') {
43
+ return 'teal';
44
+ }
45
+ if (status === 'pausing' || status === 'paused') {
46
+ return 'yellow';
47
+ }
48
+ if (status === 'completed') {
49
+ return 'green';
50
+ }
51
+ if (status === 'cancelled') {
52
+ return 'gray';
53
+ }
54
+ return 'red';
55
+ };
56
+
57
+ const statusLabel = (status: BacktestJobStatus) =>
58
+ status.charAt(0).toUpperCase() + status.slice(1);
59
+
60
+ const getJobTitle = (job: BacktestJobRecord) =>
61
+ `${job.request.strategyName} / ${job.request.configId}`;
62
+
63
+ interface BacktestJobItemProps {
64
+ job: BacktestJobRecord;
65
+ busyAction: string;
66
+ onAction: (jobId: string, action: JobAction) => void;
67
+ onDelete: (jobId: string) => void;
68
+ onOpenResults: () => void;
69
+ }
70
+
71
+ export const BacktestJobItem = ({
72
+ job,
73
+ busyAction,
74
+ onAction,
75
+ onDelete,
76
+ onOpenResults,
77
+ }: BacktestJobItemProps) => {
78
+ const progress = job.progress;
79
+ const totalLabel = progress.total == null ? '?' : progress.total;
80
+ const logs = job.logs.slice(-10);
81
+ const canPause = job.status === 'running';
82
+ const canResume = job.status === 'paused';
83
+ const canCancel = !['completed', 'cancelled'].includes(job.status);
84
+ const canDelete = ['completed', 'cancelled', 'failed', 'paused'].includes(
85
+ job.status,
86
+ );
87
+
88
+ return (
89
+ <Box
90
+ borderWidth="1px"
91
+ borderColor="gray.700"
92
+ bg="gray.800"
93
+ p={4}
94
+ borderRadius="md"
95
+ >
96
+ <Flex alignItems="flex-start" justifyContent="space-between" gap={4}>
97
+ <Box minW={0}>
98
+ <Flex gap={2} alignItems="center" wrap="wrap">
99
+ <Text fontWeight="700" wordBreak="break-word">
100
+ {getJobTitle(job)}
101
+ </Text>
102
+ <Badge colorPalette={statusTone(job.status)}>
103
+ {statusLabel(job.status)}
104
+ </Badge>
105
+ {job.request.ai ? <Badge colorPalette="purple">AI</Badge> : null}
106
+ {job.request.fast ? <Badge colorPalette="blue">Fast</Badge> : null}
107
+ </Flex>
108
+ <Text fontSize="xs" color="gray.400" mt={1}>
109
+ Started {formatDateTime(job.startedAt)} · Updated{' '}
110
+ {formatDateTime(job.updatedAt)} · Run #{job.runCount}
111
+ </Text>
112
+ {job.pauseReason ? (
113
+ <Text fontSize="xs" color="yellow.300" mt={1}>
114
+ Pause reason: {job.pauseReason}
115
+ </Text>
116
+ ) : null}
117
+ {job.error ? (
118
+ <Text fontSize="xs" color="red.300" mt={1}>
119
+ {job.error}
120
+ </Text>
121
+ ) : null}
122
+ </Box>
123
+
124
+ <Flex gap={2} flexShrink={0} wrap="wrap" justifyContent="flex-end">
125
+ {canPause ? (
126
+ <>
127
+ <Button
128
+ type="button"
129
+ size="xs"
130
+ variant="outline"
131
+ loading={busyAction === `${job.id}:pause`}
132
+ onClick={() => onAction(job.id, 'pause')}
133
+ >
134
+ <FiPause />
135
+ Pause
136
+ </Button>
137
+ <Button
138
+ type="button"
139
+ size="xs"
140
+ variant="outline"
141
+ loading={busyAction === `${job.id}:stop`}
142
+ onClick={() => onAction(job.id, 'stop')}
143
+ >
144
+ <FiSquare />
145
+ Stop
146
+ </Button>
147
+ </>
148
+ ) : null}
149
+
150
+ {canResume ? (
151
+ <Button
152
+ type="button"
153
+ size="xs"
154
+ colorPalette="teal"
155
+ loading={busyAction === `${job.id}:resume`}
156
+ onClick={() => onAction(job.id, 'resume')}
157
+ >
158
+ <FiPlay />
159
+ Resume
160
+ </Button>
161
+ ) : null}
162
+
163
+ {job.status === 'completed' ? (
164
+ <Button
165
+ type="button"
166
+ size="xs"
167
+ variant="outline"
168
+ colorPalette="teal"
169
+ onClick={onOpenResults}
170
+ >
171
+ <FiFolder />
172
+ Results
173
+ </Button>
174
+ ) : null}
175
+
176
+ {canCancel ? (
177
+ <Button
178
+ type="button"
179
+ size="xs"
180
+ variant="ghost"
181
+ colorPalette="red"
182
+ loading={busyAction === `${job.id}:cancel`}
183
+ onClick={() => onAction(job.id, 'cancel')}
184
+ >
185
+ <FiX />
186
+ Cancel
187
+ </Button>
188
+ ) : null}
189
+
190
+ <Button
191
+ type="button"
192
+ size="xs"
193
+ variant="ghost"
194
+ colorPalette="red"
195
+ disabled={!canDelete}
196
+ loading={busyAction === `${job.id}:delete`}
197
+ onClick={() => onDelete(job.id)}
198
+ >
199
+ <FiTrash2 />
200
+ </Button>
201
+ </Flex>
202
+ </Flex>
203
+
204
+ <Box mt={4}>
205
+ <Flex alignItems="center" justifyContent="space-between" mb={2}>
206
+ <Text fontSize="sm" color="gray.300">
207
+ {progress.completed}/{totalLabel} tests
208
+ </Text>
209
+ <Text fontSize="sm" color="gray.300">
210
+ {formatNumber(progress.percent, 1)}%
211
+ </Text>
212
+ </Flex>
213
+ <Box h="8px" bg="gray.800" borderRadius="full" overflow="hidden">
214
+ <Box
215
+ h="full"
216
+ bg={job.status === 'failed' ? 'red.500' : 'teal.400'}
217
+ width={`${Math.max(0, Math.min(100, progress.percent))}%`}
218
+ transition="width 0.2s ease"
219
+ />
220
+ </Box>
221
+ </Box>
222
+
223
+ <Grid templateColumns="repeat(5, minmax(0, 1fr))" gap={3} mt={4}>
224
+ <Metric
225
+ label="Avg P&L"
226
+ value={`${formatNumber(progress.averageProfit, 2)}$`}
227
+ />
228
+ <Metric
229
+ label="Winrate"
230
+ value={`${formatNumber(progress.winRate, 1)}%`}
231
+ />
232
+ <Metric label="Success" value={String(progress.successTests ?? '-')} />
233
+ <Metric label="Errors" value={String(progress.errorTests ?? '-')} />
234
+ <Metric label="PID" value={String(job.pid ?? '-')} />
235
+ </Grid>
236
+
237
+ {logs.length ? (
238
+ <Box mt={4} bg="gray.950" borderRadius="md" p={3} overflow="hidden">
239
+ <Stack gap={1}>
240
+ {logs.map((line, index) => (
241
+ <Text
242
+ key={`${job.id}:log:${index}`}
243
+ fontFamily="mono"
244
+ fontSize="xs"
245
+ color="gray.300"
246
+ whiteSpace="pre-wrap"
247
+ wordBreak="break-word"
248
+ >
249
+ {line}
250
+ </Text>
251
+ ))}
252
+ </Stack>
253
+ </Box>
254
+ ) : null}
255
+ </Box>
256
+ );
257
+ };
258
+
259
+ const Metric = ({ label, value }: { label: string; value: string }) => (
260
+ <Box minW={0}>
261
+ <Text fontSize="xs" color="gray.500">
262
+ {label}
263
+ </Text>
264
+ <Text
265
+ fontSize="sm"
266
+ color="gray.100"
267
+ fontWeight="700"
268
+ overflow="hidden"
269
+ textOverflow="ellipsis"
270
+ whiteSpace="nowrap"
271
+ >
272
+ {value}
273
+ </Text>
274
+ </Box>
275
+ );