@tradejs/app 3.1.29-beta.288 → 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.288",
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.288",
63
- "@tradejs/indicators": "^3.1.29-beta.288",
64
- "@tradejs/infra": "^3.1.29-beta.288",
65
- "@tradejs/node": "^3.1.29-beta.288",
66
- "@tradejs/types": "^3.1.29-beta.288",
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: 'Days', value: 'days' },
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 = 'rgba(255, 255, 255, 0.035)';
40
- const controlSurfaceHover = 'rgba(255, 255, 255, 0.05)';
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.700',
44
+ borderColor: 'gray.500',
45
45
  color: 'gray.100',
46
- _placeholder: { color: 'gray.500' },
46
+ _placeholder: { color: 'gray.300' },
47
47
  _hover: {
48
48
  bg: controlSurfaceHover,
49
- borderColor: 'gray.600',
49
+ borderColor: 'gray.400',
50
50
  },
51
51
  _focusVisible: {
52
- borderColor: 'gray.500',
53
- boxShadow: '0 0 0 1px var(--chakra-colors-gray-500)',
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-700)',
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-600)',
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-gray-500)',
91
- boxShadow: '0 0 0 1px var(--chakra-colors-gray-500)',
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-600)',
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
- color="gray.400"
113
- fontSize="xs"
115
+ as="h3"
116
+ color="gray.300"
117
+ fontSize="sm"
114
118
  fontWeight="700"
115
119
  letterSpacing="0"
116
- textTransform="uppercase"
120
+ textTransform="none"
117
121
  >
118
122
  {title}
119
123
  </Text>
120
- <Grid templateColumns={columns} gap={3} alignItems="end">
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 maxW="1200px" w="full">
168
- <Box
169
- borderWidth="1px"
170
- borderColor="gray.700"
171
- bg="gray.800"
172
- p={4}
173
- borderRadius="md"
174
- >
175
- <form onSubmit={handleStart}>
176
- <Stack gap={5}>
177
- <Flex alignItems="center" justifyContent="space-between" gap={4}>
178
- <Flex alignItems="center" gap={3} minW={0}>
179
- <Text fontWeight="700" flexShrink={0}>
180
- New run
181
- </Text>
182
- {onboardingMode ? (
183
- <Badge colorPalette="teal">First backtest preset</Badge>
184
- ) : null}
185
- {selectedConfig ? (
186
- <Flex gap={2} wrap="wrap">
187
- <Badge colorPalette="teal">
188
- {selectedConfig.combinationCount} combos
189
- </Badge>
190
- <Badge colorPalette="gray">
191
- {selectedConfig.paramCount} params
192
- </Badge>
193
- </Flex>
194
- ) : null}
195
- </Flex>
196
- </Flex>
197
-
198
- <FormSection title="Strategy" columns="repeat(2, minmax(0, 1fr))">
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="Strategy"
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>Config</Field.Label>
264
+ <Field.Label>Configuration</Field.Label>
219
265
  <SelectControl>
220
266
  <Select
221
- placeholder="Config"
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
- <Grid
237
- templateColumns="minmax(0, 1fr) 260px"
238
- gap={5}
239
- alignItems="start"
240
- >
241
- <FormSection
242
- title="Date window"
243
- columns={
244
- periodMode === 'days'
245
- ? '280px minmax(0, 1fr)'
246
- : '280px repeat(2, minmax(0, 1fr))'
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
- <Field.Root>
250
- <Field.Label>Mode</Field.Label>
251
- <Segment
252
- defaultValue="days"
253
- value={periodMode}
254
- items={PERIOD_ITEMS}
255
- onChange={(value) =>
256
- updateForm({
257
- periodMode: value === 'range' ? 'range' : 'days',
258
- })
259
- }
260
- />
261
- </Field.Root>
262
-
263
- {periodMode === 'days' ? (
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
- </Field.Root>
277
- ) : (
278
- <>
279
- <Field.Root>
280
- <Field.Label>Start</Field.Label>
281
- <Input
282
- value={startDate}
283
- type="date"
284
- {...inputControlProps}
285
- onChange={(event) =>
286
- updateForm({ startDate: event.target.value })
287
- }
288
- />
289
- </Field.Root>
290
- <Field.Root>
291
- <Field.Label>End</Field.Label>
292
- <Input
293
- value={endDate}
294
- type="date"
295
- {...inputControlProps}
296
- onChange={(event) =>
297
- updateForm({ endDate: event.target.value })
298
- }
299
- />
300
- </Field.Root>
301
- </>
302
- )}
303
- </FormSection>
304
-
305
- <FormSection title="Options" columns="1fr">
306
- <Flex gap={4} alignItems="center" minH="40px">
307
- <Checkbox.Root
308
- colorPalette="teal"
309
- checked={ai}
310
- onCheckedChange={(details) =>
311
- updateForm({ ai: details.checked === true })
312
- }
313
- >
314
- <Checkbox.HiddenInput />
315
- <Checkbox.Control
316
- bg={controlSurface}
317
- borderColor="gray.600"
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
- <Checkbox.Label>AI</Checkbox.Label>
320
- </Checkbox.Root>
321
- <Checkbox.Root
322
- colorPalette="teal"
323
- checked={fast}
324
- onCheckedChange={(details) =>
325
- updateForm({ fast: details.checked === true })
326
- }
327
- >
328
- <Checkbox.HiddenInput />
329
- <Checkbox.Control
330
- bg={controlSurface}
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
- <Checkbox.Label>Fast</Checkbox.Label>
334
- </Checkbox.Root>
335
- </Flex>
336
- </FormSection>
337
- </Grid>
338
-
339
- <FormSection title="Runtime" columns="220px 260px minmax(0, 1fr)">
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>Interval</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>Connector</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>Tickers</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="All tickers"
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
- <Box
409
- as="span"
410
- role="button"
411
- tabIndex={0}
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
- </Box>
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
- <FormSection title="Limits" columns="repeat(3, 1fr)">
445
- <Field.Root>
446
- <Field.Label>Tickers limit</Field.Label>
447
- <Input
448
- value={tickersLimit}
449
- type="number"
450
- min={1}
451
- {...inputControlProps}
452
- onChange={(event) =>
453
- updateForm({ tickersLimit: event.target.value })
454
- }
455
- />
456
- </Field.Root>
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
- <Field.Root>
459
- <Field.Label>Tests limit</Field.Label>
460
- <Input
461
- value={testsLimit}
462
- type="number"
463
- min={1}
464
- {...inputControlProps}
465
- onChange={(event) =>
466
- updateForm({ testsLimit: event.target.value })
467
- }
468
- />
469
- </Field.Root>
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
- <Field.Root>
472
- <Field.Label>Parallel</Field.Label>
473
- <Input
474
- value={parallel}
475
- type="number"
476
- min={1}
477
- {...inputControlProps}
478
- onChange={(event) =>
479
- updateForm({ parallel: event.target.value })
480
- }
481
- />
482
- </Field.Root>
483
- </FormSection>
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
- <Flex justifyContent="flex-end" pt={1}>
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={!selectedConfigId || starting}
695
+ disabled={!canStart}
490
696
  loading={starting}
491
- minW="160px"
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
- </Flex>
705
+ </Stack>
497
706
  </Stack>
498
- </form>
499
- </Box>
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
- as="main"
43
- minH="100vh"
44
- minW="1200px"
45
- pl={2}
46
- pr={4}
47
- py={3}
48
- bg="gray.900"
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 alignItems="center" justifyContent="space-between" mb={3}>
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>