@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
@@ -0,0 +1,502 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import {
5
+ Badge,
6
+ Box,
7
+ Button,
8
+ Checkbox,
9
+ Field,
10
+ Flex,
11
+ Grid,
12
+ Input,
13
+ Stack,
14
+ Text,
15
+ } from '@chakra-ui/react';
16
+ import { FiPlay, FiX } from 'react-icons/fi';
17
+ import { Segment, Select, SelectWithSearch } from '#ui';
18
+ import { useBacktestRunsController } from './useBacktestRunsController';
19
+
20
+ type BacktestRunsController = ReturnType<typeof useBacktestRunsController>;
21
+
22
+ const PERIOD_ITEMS = [
23
+ { label: 'Days', value: 'days' },
24
+ { label: 'Date range', value: 'range' },
25
+ ];
26
+ const INTERVAL_ITEMS = [
27
+ { label: '5m', value: '5' },
28
+ { label: '15m', value: '15' },
29
+ { label: '30m', value: '30' },
30
+ { label: '1h', value: '60' },
31
+ { label: '4h', value: '240' },
32
+ ];
33
+ const CONNECTOR_ITEMS = [
34
+ { label: 'Bybit', value: 'bybit' },
35
+ { label: 'Binance', value: 'binance' },
36
+ { label: 'Coinbase', value: 'coinbase' },
37
+ ];
38
+
39
+ const controlSurface = 'rgba(255, 255, 255, 0.035)';
40
+ const controlSurfaceHover = 'rgba(255, 255, 255, 0.05)';
41
+
42
+ const inputControlProps = {
43
+ bg: controlSurface,
44
+ borderColor: 'gray.700',
45
+ color: 'gray.100',
46
+ _placeholder: { color: 'gray.500' },
47
+ _hover: {
48
+ bg: controlSurfaceHover,
49
+ borderColor: 'gray.600',
50
+ },
51
+ _focusVisible: {
52
+ borderColor: 'gray.500',
53
+ boxShadow: '0 0 0 1px var(--chakra-colors-gray-500)',
54
+ },
55
+ };
56
+
57
+ const SelectControl = ({ children }: { children: React.ReactNode }) => (
58
+ <Box
59
+ w="full"
60
+ minW={0}
61
+ css={{
62
+ '& [data-part="control"]': {
63
+ width: '100%',
64
+ },
65
+ '& [data-part="trigger"], & [data-part="input"]': {
66
+ background: controlSurface,
67
+ borderColor: 'var(--chakra-colors-gray-700)',
68
+ color: 'var(--chakra-colors-gray-100)',
69
+ minWidth: '0',
70
+ width: '100%',
71
+ },
72
+ '& [data-part="trigger"]': {
73
+ flex: '1',
74
+ minHeight: '40px',
75
+ },
76
+ '& [data-part="input"]': {
77
+ flex: '1',
78
+ minHeight: '40px',
79
+ },
80
+ '& [data-part="indicator-group"]': {
81
+ flexShrink: '0',
82
+ },
83
+ '& [data-part="trigger"]:is(:hover, [data-hover]), & [data-part="input"]:is(:hover, [data-hover])':
84
+ {
85
+ background: controlSurfaceHover,
86
+ borderColor: 'var(--chakra-colors-gray-600)',
87
+ },
88
+ '& [data-part="trigger"]:is(:focus-visible, [data-focus-visible]), & [data-part="input"]:is(:focus-visible, [data-focus-visible])':
89
+ {
90
+ borderColor: 'var(--chakra-colors-gray-500)',
91
+ boxShadow: '0 0 0 1px var(--chakra-colors-gray-500)',
92
+ },
93
+ '& [data-part="control"]:is(:hover, [data-hover]) [data-part="input"]': {
94
+ background: controlSurfaceHover,
95
+ borderColor: 'var(--chakra-colors-gray-600)',
96
+ },
97
+ }}
98
+ >
99
+ {children}
100
+ </Box>
101
+ );
102
+
103
+ interface FormSectionProps {
104
+ title: string;
105
+ columns: string;
106
+ children: React.ReactNode;
107
+ }
108
+
109
+ const FormSection = ({ title, columns, children }: FormSectionProps) => (
110
+ <Stack gap={3} minW={0}>
111
+ <Text
112
+ color="gray.400"
113
+ fontSize="xs"
114
+ fontWeight="700"
115
+ letterSpacing="0"
116
+ textTransform="uppercase"
117
+ >
118
+ {title}
119
+ </Text>
120
+ <Grid templateColumns={columns} gap={3} alignItems="end">
121
+ {children}
122
+ </Grid>
123
+ </Stack>
124
+ );
125
+
126
+ export const BacktestRunForm = ({
127
+ controller,
128
+ }: {
129
+ controller: BacktestRunsController;
130
+ }) => {
131
+ const {
132
+ state,
133
+ strategyItems,
134
+ configItems,
135
+ selectedConfig,
136
+ tickerItems,
137
+ ensureTickersLoaded,
138
+ updateForm,
139
+ setSelectedTickers,
140
+ start,
141
+ } = controller;
142
+ const { configs, starting, onboardingMode, form } = state;
143
+ const {
144
+ selectedStrategy,
145
+ selectedConfigId,
146
+ periodMode,
147
+ days,
148
+ startDate,
149
+ endDate,
150
+ ai,
151
+ fast,
152
+ interval,
153
+ connector,
154
+ selectedTickers,
155
+ tickersLimit,
156
+ testsLimit,
157
+ parallel,
158
+ } = form;
159
+ const hasConfigs = configs.length > 0;
160
+
161
+ const handleStart = (event: React.FormEvent<HTMLFormElement>) => {
162
+ event.preventDefault();
163
+ void start();
164
+ };
165
+
166
+ 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))">
199
+ <Field.Root>
200
+ <Field.Label>Strategy</Field.Label>
201
+ <SelectControl>
202
+ <Select
203
+ placeholder="Strategy"
204
+ value={selectedStrategy ? [selectedStrategy] : []}
205
+ defaultValue={selectedStrategy ? [selectedStrategy] : []}
206
+ onChange={(value) =>
207
+ updateForm({ selectedStrategy: value[0] || '' })
208
+ }
209
+ items={strategyItems}
210
+ emptyState="No backtest configs"
211
+ width="100%"
212
+ disabled={!hasConfigs}
213
+ />
214
+ </SelectControl>
215
+ </Field.Root>
216
+
217
+ <Field.Root>
218
+ <Field.Label>Config</Field.Label>
219
+ <SelectControl>
220
+ <Select
221
+ placeholder="Config"
222
+ value={selectedConfigId ? [selectedConfigId] : []}
223
+ defaultValue={selectedConfigId ? [selectedConfigId] : []}
224
+ onChange={(value) =>
225
+ updateForm({ selectedConfigId: value[0] || '' })
226
+ }
227
+ items={configItems}
228
+ emptyState="No configs for strategy"
229
+ width="100%"
230
+ disabled={!selectedStrategy}
231
+ />
232
+ </SelectControl>
233
+ </Field.Root>
234
+ </FormSection>
235
+
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
+ }
248
+ >
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>
266
+ <Input
267
+ value={days}
268
+ type="number"
269
+ min={1}
270
+ step={1}
271
+ {...inputControlProps}
272
+ onChange={(event) =>
273
+ updateForm({ days: event.target.value })
274
+ }
275
+ />
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"
318
+ />
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"
332
+ />
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)">
340
+ <Field.Root>
341
+ <Field.Label>Interval</Field.Label>
342
+ <SelectControl>
343
+ <Select
344
+ placeholder="Interval"
345
+ value={[interval]}
346
+ defaultValue={[interval]}
347
+ onChange={(value) =>
348
+ updateForm({ interval: value[0] || '15' })
349
+ }
350
+ items={INTERVAL_ITEMS}
351
+ width="100%"
352
+ />
353
+ </SelectControl>
354
+ </Field.Root>
355
+
356
+ <Field.Root>
357
+ <Field.Label>Connector</Field.Label>
358
+ <SelectControl>
359
+ <Select
360
+ placeholder="Connector"
361
+ value={[connector]}
362
+ defaultValue={[connector]}
363
+ onChange={(value) => {
364
+ updateForm({
365
+ connector: value[0] || 'binance',
366
+ selectedTickers: [],
367
+ });
368
+ }}
369
+ items={CONNECTOR_ITEMS}
370
+ width="100%"
371
+ />
372
+ </SelectControl>
373
+ </Field.Root>
374
+
375
+ <Field.Root>
376
+ <Field.Label>Tickers</Field.Label>
377
+ <Stack gap={2} w="full" minW={0}>
378
+ <SelectControl>
379
+ <SelectWithSearch
380
+ key={connector}
381
+ multiple
382
+ placeholder="All tickers"
383
+ emptyState="No tickers"
384
+ defaultValue={[]}
385
+ value={selectedTickers}
386
+ items={tickerItems}
387
+ width="100%"
388
+ onChange={setSelectedTickers}
389
+ onOpenChange={(open) => {
390
+ if (open) {
391
+ void ensureTickersLoaded().catch(() => undefined);
392
+ }
393
+ }}
394
+ />
395
+ </SelectControl>
396
+ {selectedTickers.length ? (
397
+ <Flex gap={2} wrap="wrap">
398
+ {selectedTickers.map((ticker) => (
399
+ <Badge
400
+ key={ticker}
401
+ colorPalette="teal"
402
+ display="inline-flex"
403
+ alignItems="center"
404
+ gap={1}
405
+ py="1"
406
+ >
407
+ {ticker}
408
+ <Box
409
+ as="span"
410
+ role="button"
411
+ tabIndex={0}
412
+ aria-label={`Remove ${ticker}`}
413
+ color="gray.300"
414
+ cursor="pointer"
415
+ _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
+ onClick={() =>
427
+ setSelectedTickers((currentTickers) =>
428
+ currentTickers.filter(
429
+ (currentTicker) => currentTicker !== ticker,
430
+ ),
431
+ )
432
+ }
433
+ >
434
+ <FiX size={12} />
435
+ </Box>
436
+ </Badge>
437
+ ))}
438
+ </Flex>
439
+ ) : null}
440
+ </Stack>
441
+ </Field.Root>
442
+ </FormSection>
443
+
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>
457
+
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>
470
+
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>
484
+
485
+ <Flex justifyContent="flex-end" pt={1}>
486
+ <Button
487
+ type="submit"
488
+ colorPalette="teal"
489
+ disabled={!selectedConfigId || starting}
490
+ loading={starting}
491
+ minW="160px"
492
+ >
493
+ <FiPlay />
494
+ Start
495
+ </Button>
496
+ </Flex>
497
+ </Stack>
498
+ </form>
499
+ </Box>
500
+ </Box>
501
+ );
502
+ };