@tradejs/app 3.1.29-beta.289 → 3.1.29-beta.290
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.29-beta.
|
|
3
|
+
"version": "3.1.29-beta.290",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@emotion/react": "^11.14.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
61
61
|
"@node-oauth/oauth2-server": "^5.3.0",
|
|
62
|
-
"@tradejs/core": "^3.1.29-beta.
|
|
63
|
-
"@tradejs/indicators": "^3.1.29-beta.
|
|
64
|
-
"@tradejs/infra": "^3.1.29-beta.
|
|
65
|
-
"@tradejs/node": "^3.1.29-beta.
|
|
66
|
-
"@tradejs/types": "^3.1.29-beta.
|
|
62
|
+
"@tradejs/core": "^3.1.29-beta.290",
|
|
63
|
+
"@tradejs/indicators": "^3.1.29-beta.290",
|
|
64
|
+
"@tradejs/infra": "^3.1.29-beta.290",
|
|
65
|
+
"@tradejs/node": "^3.1.29-beta.290",
|
|
66
|
+
"@tradejs/types": "^3.1.29-beta.290",
|
|
67
67
|
"@types/bcryptjs": "2.4.6",
|
|
68
68
|
"@types/lodash": "4.17.24",
|
|
69
69
|
"@types/node": "24.13.3",
|
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
} from '@chakra-ui/react';
|
|
16
16
|
import { FiPlay, FiX } from 'react-icons/fi';
|
|
17
17
|
import { Segment, Select, SelectWithSearch } from '#ui';
|
|
18
|
-
import { useBacktestRunsController } from './useBacktestRunsController';
|
|
18
|
+
import type { useBacktestRunsController } from './useBacktestRunsController';
|
|
19
19
|
|
|
20
20
|
type BacktestRunsController = ReturnType<typeof useBacktestRunsController>;
|
|
21
21
|
|
|
22
22
|
const PERIOD_ITEMS = [
|
|
23
|
-
{ label: '
|
|
23
|
+
{ label: 'Last N days', value: 'days' },
|
|
24
24
|
{ label: 'Date range', value: 'range' },
|
|
25
25
|
];
|
|
26
26
|
const INTERVAL_ITEMS = [
|
|
@@ -36,21 +36,21 @@ const CONNECTOR_ITEMS = [
|
|
|
36
36
|
{ label: 'Coinbase', value: 'coinbase' },
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
const controlSurface = '
|
|
40
|
-
const controlSurfaceHover = '
|
|
39
|
+
const controlSurface = 'var(--chakra-colors-gray-700)';
|
|
40
|
+
const controlSurfaceHover = 'var(--chakra-colors-gray-600)';
|
|
41
41
|
|
|
42
42
|
const inputControlProps = {
|
|
43
43
|
bg: controlSurface,
|
|
44
|
-
borderColor: 'gray.
|
|
44
|
+
borderColor: 'gray.500',
|
|
45
45
|
color: 'gray.100',
|
|
46
|
-
_placeholder: { color: 'gray.
|
|
46
|
+
_placeholder: { color: 'gray.300' },
|
|
47
47
|
_hover: {
|
|
48
48
|
bg: controlSurfaceHover,
|
|
49
|
-
borderColor: 'gray.
|
|
49
|
+
borderColor: 'gray.400',
|
|
50
50
|
},
|
|
51
51
|
_focusVisible: {
|
|
52
|
-
borderColor: '
|
|
53
|
-
boxShadow: '0 0 0
|
|
52
|
+
borderColor: 'teal.300',
|
|
53
|
+
boxShadow: '0 0 0 2px var(--chakra-colors-teal-300)',
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
|
|
@@ -64,7 +64,7 @@ const SelectControl = ({ children }: { children: React.ReactNode }) => (
|
|
|
64
64
|
},
|
|
65
65
|
'& [data-part="trigger"], & [data-part="input"]': {
|
|
66
66
|
background: controlSurface,
|
|
67
|
-
borderColor: 'var(--chakra-colors-gray-
|
|
67
|
+
borderColor: 'var(--chakra-colors-gray-500)',
|
|
68
68
|
color: 'var(--chakra-colors-gray-100)',
|
|
69
69
|
minWidth: '0',
|
|
70
70
|
width: '100%',
|
|
@@ -77,22 +77,25 @@ const SelectControl = ({ children }: { children: React.ReactNode }) => (
|
|
|
77
77
|
flex: '1',
|
|
78
78
|
minHeight: '40px',
|
|
79
79
|
},
|
|
80
|
+
'& [data-part="input"]::placeholder': {
|
|
81
|
+
color: 'var(--chakra-colors-gray-300)',
|
|
82
|
+
},
|
|
80
83
|
'& [data-part="indicator-group"]': {
|
|
81
84
|
flexShrink: '0',
|
|
82
85
|
},
|
|
83
86
|
'& [data-part="trigger"]:is(:hover, [data-hover]), & [data-part="input"]:is(:hover, [data-hover])':
|
|
84
87
|
{
|
|
85
88
|
background: controlSurfaceHover,
|
|
86
|
-
borderColor: 'var(--chakra-colors-gray-
|
|
89
|
+
borderColor: 'var(--chakra-colors-gray-400)',
|
|
87
90
|
},
|
|
88
91
|
'& [data-part="trigger"]:is(:focus-visible, [data-focus-visible]), & [data-part="input"]:is(:focus-visible, [data-focus-visible])':
|
|
89
92
|
{
|
|
90
|
-
borderColor: 'var(--chakra-colors-
|
|
91
|
-
boxShadow: '0 0 0
|
|
93
|
+
borderColor: 'var(--chakra-colors-teal-300)',
|
|
94
|
+
boxShadow: '0 0 0 2px var(--chakra-colors-teal-300)',
|
|
92
95
|
},
|
|
93
96
|
'& [data-part="control"]:is(:hover, [data-hover]) [data-part="input"]': {
|
|
94
97
|
background: controlSurfaceHover,
|
|
95
|
-
borderColor: 'var(--chakra-colors-gray-
|
|
98
|
+
borderColor: 'var(--chakra-colors-gray-400)',
|
|
96
99
|
},
|
|
97
100
|
}}
|
|
98
101
|
>
|
|
@@ -109,15 +112,20 @@ interface FormSectionProps {
|
|
|
109
112
|
const FormSection = ({ title, columns, children }: FormSectionProps) => (
|
|
110
113
|
<Stack gap={3} minW={0}>
|
|
111
114
|
<Text
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
as="h3"
|
|
116
|
+
color="gray.300"
|
|
117
|
+
fontSize="sm"
|
|
114
118
|
fontWeight="700"
|
|
115
119
|
letterSpacing="0"
|
|
116
|
-
textTransform="
|
|
120
|
+
textTransform="none"
|
|
117
121
|
>
|
|
118
122
|
{title}
|
|
119
123
|
</Text>
|
|
120
|
-
<Grid
|
|
124
|
+
<Grid
|
|
125
|
+
templateColumns={{ base: 'minmax(0, 1fr)', md: columns }}
|
|
126
|
+
gap={4}
|
|
127
|
+
alignItems="start"
|
|
128
|
+
>
|
|
121
129
|
{children}
|
|
122
130
|
</Grid>
|
|
123
131
|
</Stack>
|
|
@@ -139,7 +147,7 @@ export const BacktestRunForm = ({
|
|
|
139
147
|
setSelectedTickers,
|
|
140
148
|
start,
|
|
141
149
|
} = controller;
|
|
142
|
-
const { configs, starting, onboardingMode, form } = state;
|
|
150
|
+
const { configs, loadingConfigs, starting, onboardingMode, form } = state;
|
|
143
151
|
const {
|
|
144
152
|
selectedStrategy,
|
|
145
153
|
selectedConfigId,
|
|
@@ -157,50 +165,88 @@ export const BacktestRunForm = ({
|
|
|
157
165
|
parallel,
|
|
158
166
|
} = form;
|
|
159
167
|
const hasConfigs = configs.length > 0;
|
|
168
|
+
const advancedCount = [tickersLimit, testsLimit, parallel].filter(
|
|
169
|
+
Boolean,
|
|
170
|
+
).length;
|
|
171
|
+
const periodValid =
|
|
172
|
+
periodMode === 'days'
|
|
173
|
+
? Number.isInteger(Number(days)) && Number(days) > 0
|
|
174
|
+
: Boolean(startDate && endDate && startDate <= endDate);
|
|
175
|
+
const canStart = Boolean(
|
|
176
|
+
selectedConfig &&
|
|
177
|
+
selectedConfig.strategyName === selectedStrategy &&
|
|
178
|
+
periodValid &&
|
|
179
|
+
!loadingConfigs &&
|
|
180
|
+
!starting,
|
|
181
|
+
);
|
|
182
|
+
const launchHint = loadingConfigs
|
|
183
|
+
? 'Loading configurations…'
|
|
184
|
+
: !hasConfigs
|
|
185
|
+
? 'Create a backtest configuration to get started.'
|
|
186
|
+
: !selectedConfig
|
|
187
|
+
? 'Choose a strategy and configuration.'
|
|
188
|
+
: !periodValid
|
|
189
|
+
? 'Enter a valid period to continue.'
|
|
190
|
+
: 'Review your settings, then launch the backtest.';
|
|
191
|
+
const periodLabel =
|
|
192
|
+
periodMode === 'days'
|
|
193
|
+
? `Last ${days || '—'} days`
|
|
194
|
+
: `${startDate || '…'} → ${endDate || '…'} (UTC)`;
|
|
160
195
|
|
|
161
196
|
const handleStart = (event: React.FormEvent<HTMLFormElement>) => {
|
|
162
197
|
event.preventDefault();
|
|
163
|
-
void start();
|
|
198
|
+
if (canStart) void start();
|
|
164
199
|
};
|
|
165
200
|
|
|
166
201
|
return (
|
|
167
|
-
<Box
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
<Box
|
|
203
|
+
maxW="1200px"
|
|
204
|
+
w="full"
|
|
205
|
+
borderWidth="1px"
|
|
206
|
+
borderColor="gray.700"
|
|
207
|
+
bg="gray.800"
|
|
208
|
+
borderRadius="xl"
|
|
209
|
+
overflow="hidden"
|
|
210
|
+
>
|
|
211
|
+
<form onSubmit={handleStart}>
|
|
212
|
+
<Flex
|
|
213
|
+
px={{ base: 4, md: 6 }}
|
|
214
|
+
py={5}
|
|
215
|
+
gap={3}
|
|
216
|
+
align="center"
|
|
217
|
+
justify="space-between"
|
|
218
|
+
wrap="wrap"
|
|
219
|
+
borderBottomWidth="1px"
|
|
220
|
+
borderColor="gray.700"
|
|
221
|
+
>
|
|
222
|
+
<Stack gap={1}>
|
|
223
|
+
<Text as="h2" fontSize="lg" fontWeight="700">
|
|
224
|
+
New backtest
|
|
225
|
+
</Text>
|
|
226
|
+
<Text color="gray.400" fontSize="sm">
|
|
227
|
+
Choose what to test and how far to look back.
|
|
228
|
+
</Text>
|
|
229
|
+
</Stack>
|
|
230
|
+
{onboardingMode ? (
|
|
231
|
+
<Badge colorPalette="teal">First backtest preset</Badge>
|
|
232
|
+
) : null}
|
|
233
|
+
</Flex>
|
|
234
|
+
<Grid
|
|
235
|
+
templateColumns={{
|
|
236
|
+
base: 'minmax(0, 1fr)',
|
|
237
|
+
lg: 'minmax(0, 1fr) 300px',
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<Stack gap={6} p={{ base: 4, md: 6 }} minW={0}>
|
|
241
|
+
<FormSection
|
|
242
|
+
title="Strategy & configuration"
|
|
243
|
+
columns="repeat(2, minmax(0, 1fr))"
|
|
244
|
+
>
|
|
199
245
|
<Field.Root>
|
|
200
246
|
<Field.Label>Strategy</Field.Label>
|
|
201
247
|
<SelectControl>
|
|
202
248
|
<Select
|
|
203
|
-
placeholder="
|
|
249
|
+
placeholder="Select a strategy"
|
|
204
250
|
value={selectedStrategy ? [selectedStrategy] : []}
|
|
205
251
|
defaultValue={selectedStrategy ? [selectedStrategy] : []}
|
|
206
252
|
onChange={(value) =>
|
|
@@ -215,10 +261,10 @@ export const BacktestRunForm = ({
|
|
|
215
261
|
</Field.Root>
|
|
216
262
|
|
|
217
263
|
<Field.Root>
|
|
218
|
-
<Field.Label>
|
|
264
|
+
<Field.Label>Configuration</Field.Label>
|
|
219
265
|
<SelectControl>
|
|
220
266
|
<Select
|
|
221
|
-
placeholder="
|
|
267
|
+
placeholder="Select a configuration"
|
|
222
268
|
value={selectedConfigId ? [selectedConfigId] : []}
|
|
223
269
|
defaultValue={selectedConfigId ? [selectedConfigId] : []}
|
|
224
270
|
onChange={(value) =>
|
|
@@ -233,112 +279,122 @@ export const BacktestRunForm = ({
|
|
|
233
279
|
</Field.Root>
|
|
234
280
|
</FormSection>
|
|
235
281
|
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
282
|
+
<FormSection title="Test period" columns="minmax(0, 1fr)">
|
|
283
|
+
<Box
|
|
284
|
+
css={{
|
|
285
|
+
'& [data-scope="segment-group"][data-part="root"]': {
|
|
286
|
+
background: 'var(--chakra-colors-gray-900)',
|
|
287
|
+
border: '1px solid var(--chakra-colors-gray-500)',
|
|
288
|
+
},
|
|
289
|
+
'& [data-part="indicator"]': {
|
|
290
|
+
background: 'var(--chakra-colors-gray-600)',
|
|
291
|
+
border: '1px solid var(--chakra-colors-gray-400)',
|
|
292
|
+
},
|
|
293
|
+
}}
|
|
248
294
|
>
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<Field.Root>
|
|
265
|
-
<Field.Label>Days</Field.Label>
|
|
295
|
+
<Segment
|
|
296
|
+
defaultValue="days"
|
|
297
|
+
value={periodMode}
|
|
298
|
+
items={PERIOD_ITEMS}
|
|
299
|
+
onChange={(value) =>
|
|
300
|
+
updateForm({
|
|
301
|
+
periodMode: value === 'range' ? 'range' : 'days',
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
/>
|
|
305
|
+
</Box>
|
|
306
|
+
{periodMode === 'days' ? (
|
|
307
|
+
<Field.Root invalid={!periodValid}>
|
|
308
|
+
<Field.Label>Number of days</Field.Label>
|
|
309
|
+
<Flex gap={2} wrap="wrap" align="center">
|
|
266
310
|
<Input
|
|
311
|
+
w="100px"
|
|
267
312
|
value={days}
|
|
268
313
|
type="number"
|
|
269
314
|
min={1}
|
|
270
315
|
step={1}
|
|
316
|
+
required
|
|
271
317
|
{...inputControlProps}
|
|
272
318
|
onChange={(event) =>
|
|
273
319
|
updateForm({ days: event.target.value })
|
|
274
320
|
}
|
|
275
321
|
/>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
<Flex
|
|
323
|
+
gap={1}
|
|
324
|
+
wrap="wrap"
|
|
325
|
+
role="group"
|
|
326
|
+
aria-label="Common periods"
|
|
327
|
+
>
|
|
328
|
+
{[7, 30, 90, 180].map((preset) => (
|
|
329
|
+
<Button
|
|
330
|
+
key={preset}
|
|
331
|
+
type="button"
|
|
332
|
+
size="sm"
|
|
333
|
+
variant={days === String(preset) ? 'subtle' : 'ghost'}
|
|
334
|
+
colorPalette={
|
|
335
|
+
days === String(preset) ? 'teal' : 'gray'
|
|
336
|
+
}
|
|
337
|
+
aria-pressed={days === String(preset)}
|
|
338
|
+
onClick={() => updateForm({ days: String(preset) })}
|
|
339
|
+
>
|
|
340
|
+
{preset}d
|
|
341
|
+
</Button>
|
|
342
|
+
))}
|
|
343
|
+
</Flex>
|
|
344
|
+
</Flex>
|
|
345
|
+
{periodValid ? (
|
|
346
|
+
<Field.HelperText>
|
|
347
|
+
Rolling window ending at the time of launch.
|
|
348
|
+
</Field.HelperText>
|
|
349
|
+
) : (
|
|
350
|
+
<Field.ErrorText>
|
|
351
|
+
Enter a whole number of days greater than zero.
|
|
352
|
+
</Field.ErrorText>
|
|
353
|
+
)}
|
|
354
|
+
</Field.Root>
|
|
355
|
+
) : (
|
|
356
|
+
<Grid
|
|
357
|
+
templateColumns={{
|
|
358
|
+
base: 'minmax(0, 1fr)',
|
|
359
|
+
sm: 'repeat(2, minmax(0, 1fr))',
|
|
360
|
+
}}
|
|
361
|
+
gap={4}
|
|
362
|
+
>
|
|
363
|
+
<Field.Root>
|
|
364
|
+
<Field.Label>Start date (UTC)</Field.Label>
|
|
365
|
+
<Input
|
|
366
|
+
value={startDate}
|
|
367
|
+
type="date"
|
|
368
|
+
max={endDate || undefined}
|
|
369
|
+
required
|
|
370
|
+
{...inputControlProps}
|
|
371
|
+
onChange={(event) =>
|
|
372
|
+
updateForm({ startDate: event.target.value })
|
|
373
|
+
}
|
|
318
374
|
/>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
borderColor="gray.600"
|
|
375
|
+
</Field.Root>
|
|
376
|
+
<Field.Root>
|
|
377
|
+
<Field.Label>End date (UTC)</Field.Label>
|
|
378
|
+
<Input
|
|
379
|
+
value={endDate}
|
|
380
|
+
type="date"
|
|
381
|
+
min={startDate || undefined}
|
|
382
|
+
required
|
|
383
|
+
{...inputControlProps}
|
|
384
|
+
onChange={(event) =>
|
|
385
|
+
updateForm({ endDate: event.target.value })
|
|
386
|
+
}
|
|
332
387
|
/>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
388
|
+
</Field.Root>
|
|
389
|
+
</Grid>
|
|
390
|
+
)}
|
|
391
|
+
</FormSection>
|
|
392
|
+
<FormSection
|
|
393
|
+
title="Market & instruments"
|
|
394
|
+
columns="minmax(0, 1fr) minmax(0, 1fr)"
|
|
395
|
+
>
|
|
340
396
|
<Field.Root>
|
|
341
|
-
<Field.Label>
|
|
397
|
+
<Field.Label>Candle interval</Field.Label>
|
|
342
398
|
<SelectControl>
|
|
343
399
|
<Select
|
|
344
400
|
placeholder="Interval"
|
|
@@ -354,7 +410,7 @@ export const BacktestRunForm = ({
|
|
|
354
410
|
</Field.Root>
|
|
355
411
|
|
|
356
412
|
<Field.Root>
|
|
357
|
-
<Field.Label>
|
|
413
|
+
<Field.Label>Exchange</Field.Label>
|
|
358
414
|
<SelectControl>
|
|
359
415
|
<Select
|
|
360
416
|
placeholder="Connector"
|
|
@@ -372,14 +428,18 @@ export const BacktestRunForm = ({
|
|
|
372
428
|
</SelectControl>
|
|
373
429
|
</Field.Root>
|
|
374
430
|
|
|
375
|
-
<Field.Root>
|
|
376
|
-
<Field.Label>
|
|
431
|
+
<Field.Root gridColumn="1 / -1">
|
|
432
|
+
<Field.Label>Instruments</Field.Label>
|
|
377
433
|
<Stack gap={2} w="full" minW={0}>
|
|
378
434
|
<SelectControl>
|
|
379
435
|
<SelectWithSearch
|
|
380
436
|
key={connector}
|
|
381
437
|
multiple
|
|
382
|
-
placeholder=
|
|
438
|
+
placeholder={
|
|
439
|
+
selectedTickers.length
|
|
440
|
+
? 'Search instruments…'
|
|
441
|
+
: 'All available instruments'
|
|
442
|
+
}
|
|
383
443
|
emptyState="No tickers"
|
|
384
444
|
defaultValue={[]}
|
|
385
445
|
value={selectedTickers}
|
|
@@ -405,24 +465,17 @@ export const BacktestRunForm = ({
|
|
|
405
465
|
py="1"
|
|
406
466
|
>
|
|
407
467
|
{ticker}
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
468
|
+
<Button
|
|
469
|
+
type="button"
|
|
470
|
+
variant="plain"
|
|
471
|
+
size="xs"
|
|
472
|
+
minW="24px"
|
|
473
|
+
h="24px"
|
|
474
|
+
p={0}
|
|
412
475
|
aria-label={`Remove ${ticker}`}
|
|
413
476
|
color="gray.300"
|
|
414
477
|
cursor="pointer"
|
|
415
478
|
_hover={{ color: 'white' }}
|
|
416
|
-
onKeyDown={(event) => {
|
|
417
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
418
|
-
event.preventDefault();
|
|
419
|
-
setSelectedTickers((currentTickers) =>
|
|
420
|
-
currentTickers.filter(
|
|
421
|
-
(currentTicker) => currentTicker !== ticker,
|
|
422
|
-
),
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
}}
|
|
426
479
|
onClick={() =>
|
|
427
480
|
setSelectedTickers((currentTickers) =>
|
|
428
481
|
currentTickers.filter(
|
|
@@ -432,7 +485,7 @@ export const BacktestRunForm = ({
|
|
|
432
485
|
}
|
|
433
486
|
>
|
|
434
487
|
<FiX size={12} />
|
|
435
|
-
</
|
|
488
|
+
</Button>
|
|
436
489
|
</Badge>
|
|
437
490
|
))}
|
|
438
491
|
</Flex>
|
|
@@ -441,62 +494,218 @@ export const BacktestRunForm = ({
|
|
|
441
494
|
</Field.Root>
|
|
442
495
|
</FormSection>
|
|
443
496
|
|
|
444
|
-
<
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
497
|
+
<Box
|
|
498
|
+
as="details"
|
|
499
|
+
borderTopWidth="1px"
|
|
500
|
+
borderColor="gray.700"
|
|
501
|
+
pt={4}
|
|
502
|
+
css={{
|
|
503
|
+
'& > summary': { cursor: 'pointer', borderRadius: '4px' },
|
|
504
|
+
'& > summary:focus-visible': {
|
|
505
|
+
outline: '2px solid var(--chakra-colors-teal-400)',
|
|
506
|
+
outlineOffset: '4px',
|
|
507
|
+
},
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
<Box as="summary" color="gray.300" fontSize="sm" fontWeight="600">
|
|
511
|
+
Advanced settings
|
|
512
|
+
{advancedCount ? ` · ${advancedCount} custom` : ''}
|
|
513
|
+
</Box>
|
|
514
|
+
<Box pt={4}>
|
|
515
|
+
<FormSection title="Execution limits" columns="repeat(3, 1fr)">
|
|
516
|
+
<Field.Root>
|
|
517
|
+
<Field.Label>Max instruments</Field.Label>
|
|
518
|
+
<Input
|
|
519
|
+
placeholder="No limit"
|
|
520
|
+
value={tickersLimit}
|
|
521
|
+
type="number"
|
|
522
|
+
min={1}
|
|
523
|
+
{...inputControlProps}
|
|
524
|
+
onChange={(event) =>
|
|
525
|
+
updateForm({ tickersLimit: event.target.value })
|
|
526
|
+
}
|
|
527
|
+
/>
|
|
528
|
+
</Field.Root>
|
|
457
529
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
530
|
+
<Field.Root>
|
|
531
|
+
<Field.Label>Max tests</Field.Label>
|
|
532
|
+
<Input
|
|
533
|
+
placeholder="No limit"
|
|
534
|
+
value={testsLimit}
|
|
535
|
+
type="number"
|
|
536
|
+
min={1}
|
|
537
|
+
{...inputControlProps}
|
|
538
|
+
onChange={(event) =>
|
|
539
|
+
updateForm({ testsLimit: event.target.value })
|
|
540
|
+
}
|
|
541
|
+
/>
|
|
542
|
+
</Field.Root>
|
|
470
543
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
544
|
+
<Field.Root>
|
|
545
|
+
<Field.Label>Parallel workers</Field.Label>
|
|
546
|
+
<Input
|
|
547
|
+
placeholder="Automatic"
|
|
548
|
+
value={parallel}
|
|
549
|
+
type="number"
|
|
550
|
+
min={1}
|
|
551
|
+
{...inputControlProps}
|
|
552
|
+
onChange={(event) =>
|
|
553
|
+
updateForm({ parallel: event.target.value })
|
|
554
|
+
}
|
|
555
|
+
/>
|
|
556
|
+
</Field.Root>
|
|
557
|
+
</FormSection>
|
|
484
558
|
|
|
485
|
-
|
|
559
|
+
<Text fontSize="xs" color="gray.400" mt={3}>
|
|
560
|
+
Leave limits empty to run all tests. Worker count is chosen
|
|
561
|
+
automatically.
|
|
562
|
+
</Text>
|
|
563
|
+
</Box>
|
|
564
|
+
</Box>
|
|
565
|
+
</Stack>
|
|
566
|
+
<Stack
|
|
567
|
+
as="aside"
|
|
568
|
+
aria-label="Run summary"
|
|
569
|
+
minW={0}
|
|
570
|
+
gap={5}
|
|
571
|
+
p={{ base: 4, md: 6 }}
|
|
572
|
+
bg="rgba(0, 0, 0, 0.14)"
|
|
573
|
+
borderLeftWidth={{ base: '0', lg: '1px' }}
|
|
574
|
+
borderTopWidth={{ base: '1px', lg: '0' }}
|
|
575
|
+
borderColor="gray.700"
|
|
576
|
+
>
|
|
577
|
+
<Stack gap={1}>
|
|
578
|
+
<Text fontWeight="600" color="teal.300" fontSize="sm">
|
|
579
|
+
Run summary
|
|
580
|
+
</Text>
|
|
581
|
+
<Text fontSize="lg" fontWeight="700" overflowWrap="anywhere">
|
|
582
|
+
{selectedStrategy || 'Choose a strategy'}
|
|
583
|
+
</Text>
|
|
584
|
+
<Text fontSize="sm" color="gray.400" overflowWrap="anywhere">
|
|
585
|
+
{selectedConfigId || 'No configuration selected'}
|
|
586
|
+
</Text>
|
|
587
|
+
</Stack>
|
|
588
|
+
<Stack as="dl" gap={3} fontSize="sm" m={0}>
|
|
589
|
+
{[
|
|
590
|
+
['Period', periodLabel],
|
|
591
|
+
[
|
|
592
|
+
'Exchange',
|
|
593
|
+
CONNECTOR_ITEMS.find((item) => item.value === connector)
|
|
594
|
+
?.label || connector,
|
|
595
|
+
],
|
|
596
|
+
[
|
|
597
|
+
'Candle interval',
|
|
598
|
+
INTERVAL_ITEMS.find((item) => item.value === interval)
|
|
599
|
+
?.label || interval,
|
|
600
|
+
],
|
|
601
|
+
[
|
|
602
|
+
'Instruments',
|
|
603
|
+
selectedTickers.length
|
|
604
|
+
? `${selectedTickers.length} selected`
|
|
605
|
+
: 'All available',
|
|
606
|
+
],
|
|
607
|
+
[
|
|
608
|
+
'Combinations',
|
|
609
|
+
selectedConfig
|
|
610
|
+
? selectedConfig.combinationCount.toLocaleString()
|
|
611
|
+
: '—',
|
|
612
|
+
],
|
|
613
|
+
...(tickersLimit ? [['Max instruments', tickersLimit]] : []),
|
|
614
|
+
...(testsLimit ? [['Max tests', testsLimit]] : []),
|
|
615
|
+
...(parallel ? [['Parallel workers', parallel]] : []),
|
|
616
|
+
].map(([label, value]) => (
|
|
617
|
+
<Flex key={label} justify="space-between" gap={3}>
|
|
618
|
+
<Text as="dt" color="gray.400" flexShrink={0}>
|
|
619
|
+
{label}
|
|
620
|
+
</Text>
|
|
621
|
+
<Text as="dd" m={0} textAlign="right" overflowWrap="anywhere">
|
|
622
|
+
{value}
|
|
623
|
+
</Text>
|
|
624
|
+
</Flex>
|
|
625
|
+
))}
|
|
626
|
+
</Stack>
|
|
627
|
+
<Stack gap={4} borderTopWidth="1px" borderColor="gray.700" pt={5}>
|
|
628
|
+
<Checkbox.Root
|
|
629
|
+
colorPalette="teal"
|
|
630
|
+
alignItems="start"
|
|
631
|
+
checked={ai}
|
|
632
|
+
onCheckedChange={(details) =>
|
|
633
|
+
updateForm({ ai: details.checked === true })
|
|
634
|
+
}
|
|
635
|
+
>
|
|
636
|
+
<Checkbox.HiddenInput />
|
|
637
|
+
<Checkbox.Control
|
|
638
|
+
flexShrink={0}
|
|
639
|
+
mt="2px"
|
|
640
|
+
bg="gray.700"
|
|
641
|
+
borderColor="gray.500"
|
|
642
|
+
_checked={{ bg: 'teal.500', borderColor: 'teal.300' }}
|
|
643
|
+
>
|
|
644
|
+
<Checkbox.Indicator />
|
|
645
|
+
</Checkbox.Control>
|
|
646
|
+
<Checkbox.Label>
|
|
647
|
+
<Text fontSize="sm" fontWeight="500">
|
|
648
|
+
Export AI dataset
|
|
649
|
+
</Text>
|
|
650
|
+
<Text fontSize="xs" color="gray.400" mt={1}>
|
|
651
|
+
Save prompt rows for later AI research.
|
|
652
|
+
</Text>
|
|
653
|
+
</Checkbox.Label>
|
|
654
|
+
</Checkbox.Root>
|
|
655
|
+
<Checkbox.Root
|
|
656
|
+
colorPalette="teal"
|
|
657
|
+
alignItems="start"
|
|
658
|
+
checked={fast}
|
|
659
|
+
onCheckedChange={(details) =>
|
|
660
|
+
updateForm({ fast: details.checked === true })
|
|
661
|
+
}
|
|
662
|
+
>
|
|
663
|
+
<Checkbox.HiddenInput />
|
|
664
|
+
<Checkbox.Control
|
|
665
|
+
flexShrink={0}
|
|
666
|
+
mt="2px"
|
|
667
|
+
bg="gray.700"
|
|
668
|
+
borderColor="gray.500"
|
|
669
|
+
_checked={{ bg: 'teal.500', borderColor: 'teal.300' }}
|
|
670
|
+
>
|
|
671
|
+
<Checkbox.Indicator />
|
|
672
|
+
</Checkbox.Control>
|
|
673
|
+
<Checkbox.Label>
|
|
674
|
+
<Text fontSize="sm" fontWeight="500">
|
|
675
|
+
Fast mode
|
|
676
|
+
</Text>
|
|
677
|
+
<Text fontSize="xs" color="gray.400" mt={1}>
|
|
678
|
+
Skip per-test artifacts to reduce disk writes.
|
|
679
|
+
</Text>
|
|
680
|
+
</Checkbox.Label>
|
|
681
|
+
</Checkbox.Root>
|
|
682
|
+
</Stack>
|
|
683
|
+
<Stack gap={3} mt="auto" pt={3}>
|
|
684
|
+
<Text
|
|
685
|
+
fontSize="xs"
|
|
686
|
+
color="gray.400"
|
|
687
|
+
id="backtest-launch-hint"
|
|
688
|
+
role="status"
|
|
689
|
+
>
|
|
690
|
+
{launchHint}
|
|
691
|
+
</Text>
|
|
486
692
|
<Button
|
|
487
693
|
type="submit"
|
|
488
694
|
colorPalette="teal"
|
|
489
|
-
disabled={!
|
|
695
|
+
disabled={!canStart}
|
|
490
696
|
loading={starting}
|
|
491
|
-
|
|
697
|
+
loadingText="Starting…"
|
|
698
|
+
w="full"
|
|
699
|
+
size="lg"
|
|
700
|
+
aria-describedby="backtest-launch-hint"
|
|
492
701
|
>
|
|
493
702
|
<FiPlay />
|
|
494
|
-
Start
|
|
703
|
+
Start backtest
|
|
495
704
|
</Button>
|
|
496
|
-
</
|
|
705
|
+
</Stack>
|
|
497
706
|
</Stack>
|
|
498
|
-
</
|
|
499
|
-
</
|
|
707
|
+
</Grid>
|
|
708
|
+
</form>
|
|
500
709
|
</Box>
|
|
501
710
|
);
|
|
502
711
|
};
|
|
@@ -38,16 +38,14 @@ const BacktestRunPage = () => {
|
|
|
38
38
|
return (
|
|
39
39
|
<ClientOnly>
|
|
40
40
|
<Box minH="100vh" bg="gray.900" color="gray.100">
|
|
41
|
-
<Box
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>
|
|
50
|
-
<Flex alignItems="center" justifyContent="space-between" mb={3}>
|
|
41
|
+
<Box as="main" minH="100vh" minW={0} pl={2} pr={4} py={3} bg="gray.900">
|
|
42
|
+
<Flex
|
|
43
|
+
alignItems="center"
|
|
44
|
+
justifyContent="space-between"
|
|
45
|
+
wrap="wrap"
|
|
46
|
+
gap={3}
|
|
47
|
+
mb={3}
|
|
48
|
+
>
|
|
51
49
|
<Box pl={2}>
|
|
52
50
|
<Text fontSize="lg" fontWeight="700" lineHeight="1.2">
|
|
53
51
|
Backtest runs
|
|
@@ -82,7 +80,13 @@ const BacktestRunPage = () => {
|
|
|
82
80
|
<BacktestRunForm controller={controller} />
|
|
83
81
|
|
|
84
82
|
<Box maxW="1200px" w="full" mt={5}>
|
|
85
|
-
<Flex
|
|
83
|
+
<Flex
|
|
84
|
+
alignItems="center"
|
|
85
|
+
justifyContent="space-between"
|
|
86
|
+
wrap="wrap"
|
|
87
|
+
gap={3}
|
|
88
|
+
mb={3}
|
|
89
|
+
>
|
|
86
90
|
<Flex alignItems="center" gap={3}>
|
|
87
91
|
<Text fontWeight="700">Active runs</Text>
|
|
88
92
|
<Badge colorPalette="gray">{jobs.length} jobs</Badge>
|