@tradejs/app 1.0.9 → 1.0.11

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 (125) hide show
  1. package/README.md +16 -3
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/install/route.ts +51 -0
  20. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  21. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  22. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  23. package/src/app/api/scanner/[provider]/route.ts +30 -12
  24. package/src/app/api/scanner/route.ts +3 -3
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  26. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  27. package/src/app/api/strategies/ai/route.ts +139 -0
  28. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  29. package/src/app/api/strategies/replay/route.ts +44 -0
  30. package/src/app/api/strategies/runtime/route.ts +831 -0
  31. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  32. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  33. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  34. package/src/app/api/user/settings/route.ts +18 -1
  35. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  36. package/src/app/api/user/trading-accounts/route.ts +134 -0
  37. package/src/app/auth.ts +1 -19
  38. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  39. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  40. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  41. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  42. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  43. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  44. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  45. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  46. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  47. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  48. package/src/app/components/Backtest/TestCard/Root/index.tsx +18 -4
  49. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  50. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  51. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  52. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  53. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  56. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  63. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  64. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  65. package/src/app/components/Dashboard/KlineChart/index.tsx +21 -7
  66. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  67. package/src/app/components/Shared/AppShell.tsx +2 -2
  68. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  69. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  70. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  71. package/src/app/components/Shared/Filters/Backtest/index.tsx +51 -51
  72. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  73. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  74. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  75. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  76. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  77. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  78. package/src/app/components/Shared/Filters/index.ts +2 -0
  79. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  80. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  81. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  82. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  83. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  84. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  85. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  86. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  87. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  88. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  89. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  90. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  91. package/src/app/components/UI/Segment/index.tsx +8 -1
  92. package/src/app/components/UI/Select/index.tsx +24 -13
  93. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  94. package/src/app/layout.tsx +1 -1
  95. package/src/app/lib/backtestJobs.ts +925 -0
  96. package/src/app/lib/cardPageLayout.ts +1 -0
  97. package/src/app/lib/connectorCreator.ts +28 -0
  98. package/src/app/lib/currentUser.ts +1 -1
  99. package/src/app/lib/errorMessage.ts +34 -0
  100. package/src/app/lib/installation.ts +83 -0
  101. package/src/app/lib/marketDefaults.ts +12 -0
  102. package/src/app/lib/marketRoutes.ts +58 -0
  103. package/src/app/lib/runtimeStrategies.ts +1376 -0
  104. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  105. package/src/app/provider.tsx +1 -1
  106. package/src/app/routes/backtest/page.tsx +1100 -302
  107. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +76 -35
  108. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  109. package/src/app/routes/dashboard/page.tsx +15 -3
  110. package/src/app/routes/derivatives/page.tsx +1222 -165
  111. package/src/app/routes/install/page.tsx +175 -0
  112. package/src/app/routes/signin/page.tsx +7 -0
  113. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  114. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  115. package/src/app/routes/strategies/page.tsx +7 -0
  116. package/src/app/store/ai.ts +1 -1
  117. package/src/app/store/data.ts +73 -17
  118. package/src/app/store/filters.ts +1 -0
  119. package/src/app/store/marketKlineStream.ts +87 -0
  120. package/src/app/store/tests.ts +83 -45
  121. package/src/app/store/tickers.ts +59 -22
  122. package/src/proxy.ts +4 -0
  123. package/tsconfig.json +17 -12
  124. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  125. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -0,0 +1,40 @@
1
+ import {
2
+ Box,
3
+ Flex,
4
+ Skeleton,
5
+ SkeletonText,
6
+ SimpleGrid,
7
+ } from '@chakra-ui/react';
8
+
9
+ export const RuntimeStrategyCardSkeleton = () => (
10
+ <Box
11
+ p={2}
12
+ mb={4}
13
+ maxW="1400px"
14
+ borderRadius="md"
15
+ shadow="sm"
16
+ borderWidth="1px"
17
+ borderColor="gray.800"
18
+ overflowX="auto"
19
+ >
20
+ <Flex p={4} mb={3} alignItems="center" gap={4} wrap="wrap">
21
+ <Skeleton height="28px" width="160px" />
22
+ <Skeleton height="28px" width="90px" />
23
+ <Skeleton height="28px" width="110px" />
24
+ <Skeleton height="28px" width="320px" />
25
+ <Skeleton height="28px" width="90px" />
26
+ <Skeleton height="28px" width="90px" />
27
+ <Skeleton height="20px" width="240px" ml="auto" />
28
+ </Flex>
29
+
30
+ <Box w="100%" minW="600px" h="350px" pr={2}>
31
+ <Skeleton height="100%" />
32
+ </Box>
33
+
34
+ <SimpleGrid columns={{ base: 4, md: 8 }} p={4} gap={4}>
35
+ {Array.from({ length: 8 }).map((_, index) => (
36
+ <SkeletonText key={index} noOfLines={2} gap="3" />
37
+ ))}
38
+ </SimpleGrid>
39
+ </Box>
40
+ );
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { Box, Text } from '@chakra-ui/react';
5
+ import { Chart, useChart } from '@chakra-ui/charts';
6
+ import {
7
+ CartesianGrid,
8
+ Line,
9
+ LineChart,
10
+ ReferenceLine,
11
+ ResponsiveContainer,
12
+ Tooltip,
13
+ XAxis,
14
+ YAxis,
15
+ } from 'recharts';
16
+ import { format } from 'date-fns';
17
+ import { getFormatted } from '@tradejs/core/backtest';
18
+ import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
19
+
20
+ interface RuntimeStrategyChartProps {
21
+ orderLog: SimpleOrderLogData;
22
+ stat: TestStat;
23
+ height?: string | number;
24
+ }
25
+
26
+ export const RuntimeStrategyChart = ({
27
+ orderLog,
28
+ stat,
29
+ height = '350px',
30
+ }: RuntimeStrategyChartProps) => {
31
+ const chartData = useMemo(
32
+ () => ({
33
+ data: orderLog.map(([timestamp, amount]) => ({
34
+ timestamp: format(timestamp, 'dd.MM'),
35
+ equity: amount,
36
+ })),
37
+ series: [
38
+ {
39
+ name: 'equity',
40
+ color: 'teal.solid',
41
+ },
42
+ ],
43
+ }),
44
+ [orderLog],
45
+ );
46
+
47
+ const chart = useChart(chartData as any);
48
+ const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
49
+ const { formatted: minAmount } = getFormatted(stat, 'minAmount');
50
+
51
+ if (!orderLog.length) {
52
+ return (
53
+ <Box
54
+ w="100%"
55
+ minW="600px"
56
+ h={height}
57
+ display="flex"
58
+ alignItems="center"
59
+ justifyContent="center"
60
+ >
61
+ <Text color="gray.500">No runtime trades for the selected window.</Text>
62
+ </Box>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <Box w="100%" minW="600px" h={height} pr={2}>
68
+ <ResponsiveContainer width="100%" height="100%">
69
+ <Chart.Root maxH="md" chart={chart}>
70
+ <LineChart data={chart.data}>
71
+ <CartesianGrid stroke={chart.color('border')} vertical={false} />
72
+ <ReferenceLine
73
+ stroke={chart.color('gray.600')}
74
+ strokeDasharray="5 5"
75
+ y={stat.maxAmount}
76
+ label={{
77
+ value: `Max: ${maxAmount}`,
78
+ offset: 10,
79
+ fill: chart.color('gray.600'),
80
+ position: 'top',
81
+ }}
82
+ />
83
+ <ReferenceLine
84
+ stroke={chart.color('gray.600')}
85
+ strokeDasharray="8 8"
86
+ y={100}
87
+ />
88
+ <ReferenceLine
89
+ stroke={chart.color('gray.600')}
90
+ strokeDasharray="5 5"
91
+ y={stat.minAmount}
92
+ label={{
93
+ value: `Min: ${minAmount}`,
94
+ offset: 10,
95
+ fill: chart.color('gray.600'),
96
+ position: 'bottom',
97
+ }}
98
+ />
99
+ <XAxis dataKey="timestamp" />
100
+ <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
101
+ <Tooltip
102
+ animationDuration={100}
103
+ cursor={false}
104
+ content={<Chart.Tooltip />}
105
+ />
106
+ {chart.series.map((item) => (
107
+ <Line
108
+ key={item.name as string}
109
+ isAnimationActive={false}
110
+ dataKey={chart.key(item.name) as string}
111
+ stroke={chart.color(item.color)}
112
+ strokeWidth={2}
113
+ dot={false}
114
+ />
115
+ ))}
116
+ </LineChart>
117
+ </Chart.Root>
118
+ </ResponsiveContainer>
119
+ </Box>
120
+ );
121
+ };
@@ -0,0 +1,458 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from 'react';
10
+ import {
11
+ Box,
12
+ Button,
13
+ Checkbox,
14
+ CloseButton,
15
+ Drawer,
16
+ Field,
17
+ Flex,
18
+ Input,
19
+ NativeSelect,
20
+ Portal,
21
+ Text,
22
+ Textarea,
23
+ } from '@chakra-ui/react';
24
+ import type { MarketUniverse } from '@tradejs/types';
25
+ import type { RuntimeStrategyView } from '#app/lib/runtimeStrategies';
26
+ import {
27
+ DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS,
28
+ mergeRuntimeStrategyManagedParameters,
29
+ splitRuntimeStrategyConfig,
30
+ } from '#app/lib/runtimeStrategyConfigForm';
31
+ import { toaster } from '#ui';
32
+
33
+ type AccountOption = {
34
+ id: string;
35
+ label: string;
36
+ provider: string;
37
+ enabled: boolean;
38
+ isDefault?: boolean;
39
+ universes: MarketUniverse[];
40
+ };
41
+
42
+ type OptionsResponse = {
43
+ strategyNames: string[];
44
+ accounts: AccountOption[];
45
+ intervals: string[];
46
+ error?: string;
47
+ };
48
+
49
+ const SelectField = ({
50
+ label,
51
+ value,
52
+ onChange,
53
+ children,
54
+ disabled,
55
+ }: {
56
+ label: string;
57
+ value: string;
58
+ onChange: (value: string) => void;
59
+ children: ReactNode;
60
+ disabled?: boolean;
61
+ }) => (
62
+ <Field.Root>
63
+ <Field.Label>{label}</Field.Label>
64
+ <NativeSelect.Root disabled={disabled}>
65
+ <NativeSelect.Field
66
+ value={value}
67
+ onChange={(event) => onChange(event.currentTarget.value)}
68
+ >
69
+ {children}
70
+ </NativeSelect.Field>
71
+ <NativeSelect.Indicator />
72
+ </NativeSelect.Root>
73
+ </Field.Root>
74
+ );
75
+
76
+ export const RuntimeStrategyConfigDrawer = ({
77
+ open,
78
+ strategy,
79
+ onOpenChange,
80
+ onSaved,
81
+ }: {
82
+ open: boolean;
83
+ strategy?: RuntimeStrategyView | null;
84
+ onOpenChange: (open: boolean) => void;
85
+ onSaved: () => Promise<void> | void;
86
+ }) => {
87
+ const editing = Boolean(strategy);
88
+ const [options, setOptions] = useState<OptionsResponse>({
89
+ strategyNames: [],
90
+ accounts: [],
91
+ intervals: ['15'],
92
+ });
93
+ const [strategyName, setStrategyName] = useState('');
94
+ const [configId, setConfigId] = useState('config');
95
+ const [interval, setInterval] = useState('15');
96
+ const [universe, setUniverse] = useState<MarketUniverse>('crypto');
97
+ const [accountId, setAccountId] = useState('');
98
+ const [enabled, setEnabled] = useState(true);
99
+ const [maxLossValue, setMaxLossValue] = useState('1');
100
+ const [aiEnabled, setAiEnabled] = useState(true);
101
+ const [aiMode, setAiMode] = useState<'gate' | 'llm'>('gate');
102
+ const [minAiQuality, setMinAiQuality] = useState('4');
103
+ const [mlEnabled, setMlEnabled] = useState(false);
104
+ const [mlThreshold, setMlThreshold] = useState('0.1');
105
+ const [parameters, setParameters] = useState('{}');
106
+ const [loading, setLoading] = useState(false);
107
+ const [saving, setSaving] = useState(false);
108
+
109
+ const loadOptions = useCallback(async () => {
110
+ setLoading(true);
111
+ try {
112
+ const response = await fetch('/api/user/runtime-strategy-configs');
113
+ const payload = (await response.json()) as OptionsResponse;
114
+ if (!response.ok)
115
+ throw new Error(
116
+ payload.error || 'Failed to load configuration options',
117
+ );
118
+ setOptions(payload);
119
+ if (!editing && !strategyName)
120
+ setStrategyName(payload.strategyNames[0] ?? '');
121
+ } catch (error) {
122
+ toaster.error({
123
+ title: 'Failed to load strategy settings',
124
+ description: error instanceof Error ? error.message : String(error),
125
+ });
126
+ } finally {
127
+ setLoading(false);
128
+ }
129
+ }, [editing, strategyName]);
130
+
131
+ useEffect(() => {
132
+ if (!open) return;
133
+ if (strategy) {
134
+ const { managed, parameters: strategyParameters } =
135
+ splitRuntimeStrategyConfig(strategy.config);
136
+ setStrategyName(strategy.strategyName);
137
+ setConfigId(strategy.configId);
138
+ setInterval(String(strategy.interval ?? '15'));
139
+ setUniverse(strategy.universe ?? 'crypto');
140
+ setAccountId(String(strategy.config?.ACCOUNT_ID ?? ''));
141
+ setEnabled(strategy.enabled);
142
+ setMaxLossValue(String(managed.maxLossValue));
143
+ setAiEnabled(managed.aiEnabled);
144
+ setAiMode(managed.aiMode);
145
+ setMinAiQuality(String(managed.minAiQuality));
146
+ setMlEnabled(managed.mlEnabled);
147
+ setMlThreshold(String(managed.mlThreshold));
148
+ setParameters(JSON.stringify(strategyParameters, null, 2));
149
+ } else {
150
+ const managed = DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS;
151
+ setConfigId('config');
152
+ setInterval('15');
153
+ setUniverse('crypto');
154
+ setAccountId('');
155
+ setEnabled(true);
156
+ setMaxLossValue(String(managed.maxLossValue));
157
+ setAiEnabled(managed.aiEnabled);
158
+ setAiMode(managed.aiMode);
159
+ setMinAiQuality(String(managed.minAiQuality));
160
+ setMlEnabled(managed.mlEnabled);
161
+ setMlThreshold(String(managed.mlThreshold));
162
+ setParameters('{}');
163
+ }
164
+ void loadOptions();
165
+ }, [loadOptions, open, strategy]);
166
+
167
+ const compatibleAccounts = useMemo(
168
+ () =>
169
+ options.accounts.filter(
170
+ (account) =>
171
+ account.enabled &&
172
+ account.provider === 'bybit' &&
173
+ account.universes.includes(universe),
174
+ ),
175
+ [options.accounts, universe],
176
+ );
177
+
178
+ useEffect(() => {
179
+ if (
180
+ accountId &&
181
+ !compatibleAccounts.some((account) => account.id === accountId)
182
+ ) {
183
+ setAccountId('');
184
+ }
185
+ }, [accountId, compatibleAccounts]);
186
+
187
+ const save = async () => {
188
+ setSaving(true);
189
+ try {
190
+ const parsed = JSON.parse(parameters) as unknown;
191
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
192
+ throw new Error('Strategy parameters must be a JSON object');
193
+ }
194
+ const parsedMaxLossValue = Number(maxLossValue);
195
+ if (!Number.isFinite(parsedMaxLossValue) || parsedMaxLossValue < 0) {
196
+ throw new Error('Max loss value must be a non-negative number');
197
+ }
198
+ const parsedMinAiQuality = Number(minAiQuality);
199
+ if (
200
+ !Number.isInteger(parsedMinAiQuality) ||
201
+ parsedMinAiQuality < 0 ||
202
+ parsedMinAiQuality > 5
203
+ ) {
204
+ throw new Error('Minimum AI quality must be an integer from 0 to 5');
205
+ }
206
+ const parsedMlThreshold = Number(mlThreshold);
207
+ if (
208
+ !Number.isFinite(parsedMlThreshold) ||
209
+ parsedMlThreshold < 0 ||
210
+ parsedMlThreshold > 1
211
+ ) {
212
+ throw new Error('ML threshold must be a number from 0 to 1');
213
+ }
214
+ const managedParameters = mergeRuntimeStrategyManagedParameters(
215
+ parsed as Record<string, unknown>,
216
+ {
217
+ maxLossValue: parsedMaxLossValue,
218
+ aiEnabled,
219
+ aiMode,
220
+ minAiQuality: parsedMinAiQuality,
221
+ mlEnabled,
222
+ mlThreshold: parsedMlThreshold,
223
+ },
224
+ );
225
+ const response = await fetch('/api/user/runtime-strategy-configs', {
226
+ method: editing ? 'PATCH' : 'POST',
227
+ headers: { 'content-type': 'application/json' },
228
+ body: JSON.stringify({
229
+ strategyName,
230
+ configId,
231
+ interval,
232
+ universe,
233
+ accountId: accountId || null,
234
+ enabled,
235
+ parameters: managedParameters,
236
+ }),
237
+ });
238
+ const payload = (await response.json()) as { error?: string };
239
+ if (!response.ok) throw new Error(payload.error || 'Save failed');
240
+ toaster.success({
241
+ title: editing ? 'Strategy config updated' : 'Strategy config created',
242
+ });
243
+ onOpenChange(false);
244
+ await onSaved();
245
+ } catch (error) {
246
+ toaster.error({
247
+ title: 'Could not save strategy config',
248
+ description: error instanceof Error ? error.message : String(error),
249
+ });
250
+ } finally {
251
+ setSaving(false);
252
+ }
253
+ };
254
+
255
+ return (
256
+ <Drawer.Root
257
+ size="lg"
258
+ open={open}
259
+ onOpenChange={(event) => onOpenChange(event.open)}
260
+ >
261
+ <Portal>
262
+ <Drawer.Backdrop />
263
+ <Drawer.Positioner>
264
+ <Drawer.Content bg="gray.950">
265
+ <Drawer.Header>
266
+ <Drawer.Title>
267
+ {editing
268
+ ? 'Edit strategy configuration'
269
+ : 'Create strategy configuration'}
270
+ </Drawer.Title>
271
+ <Drawer.CloseTrigger asChild>
272
+ <CloseButton size="sm" />
273
+ </Drawer.CloseTrigger>
274
+ </Drawer.Header>
275
+ <Drawer.Body display="flex" flexDirection="column" gap={5}>
276
+ {editing ? (
277
+ <Field.Root>
278
+ <Field.Label>Strategy</Field.Label>
279
+ <Input value={strategyName} disabled />
280
+ </Field.Root>
281
+ ) : (
282
+ <SelectField
283
+ label="Strategy"
284
+ value={strategyName}
285
+ onChange={setStrategyName}
286
+ disabled={loading}
287
+ >
288
+ {options.strategyNames.map((name) => (
289
+ <option key={name} value={name}>
290
+ {name}
291
+ </option>
292
+ ))}
293
+ </SelectField>
294
+ )}
295
+ <Field.Root>
296
+ <Field.Label>Configuration id</Field.Label>
297
+ <Input
298
+ value={configId}
299
+ disabled={editing}
300
+ onChange={(event) => setConfigId(event.target.value)}
301
+ />
302
+ </Field.Root>
303
+ <Flex gap={4} align="start">
304
+ <Box flex="1">
305
+ <SelectField
306
+ label="Timeframe"
307
+ value={interval}
308
+ onChange={setInterval}
309
+ >
310
+ {options.intervals.map((value) => (
311
+ <option key={value} value={value}>
312
+ {value}m
313
+ </option>
314
+ ))}
315
+ </SelectField>
316
+ </Box>
317
+ <Box flex="1">
318
+ <SelectField
319
+ label="Universe"
320
+ value={universe}
321
+ onChange={(value) => setUniverse(value as MarketUniverse)}
322
+ >
323
+ <option value="crypto">Crypto</option>
324
+ <option value="tradfi">TradFi</option>
325
+ </SelectField>
326
+ </Box>
327
+ </Flex>
328
+ <SelectField
329
+ label="Trading account"
330
+ value={accountId}
331
+ onChange={setAccountId}
332
+ >
333
+ <option value="">Default account for {universe}</option>
334
+ {compatibleAccounts.map((account) => (
335
+ <option key={account.id} value={account.id}>
336
+ {account.label}
337
+ {account.isDefault ? ' (default)' : ''}
338
+ </option>
339
+ ))}
340
+ </SelectField>
341
+ <Checkbox.Root
342
+ checked={enabled}
343
+ onCheckedChange={(event) => setEnabled(event.checked === true)}
344
+ >
345
+ <Checkbox.HiddenInput />
346
+ <Checkbox.Control>
347
+ <Checkbox.Indicator />
348
+ </Checkbox.Control>
349
+ <Checkbox.Label>Enabled</Checkbox.Label>
350
+ </Checkbox.Root>
351
+ <Field.Root>
352
+ <Field.Label>Max loss value</Field.Label>
353
+ <Input
354
+ type="number"
355
+ min={0}
356
+ step="0.1"
357
+ value={maxLossValue}
358
+ onChange={(event) => setMaxLossValue(event.target.value)}
359
+ />
360
+ </Field.Root>
361
+ <Text color="gray.300" fontWeight="semibold">
362
+ AI
363
+ </Text>
364
+ <Flex gap={4} align="start">
365
+ <Box flex="1">
366
+ <SelectField
367
+ label="AI status"
368
+ value={aiEnabled ? 'enabled' : 'disabled'}
369
+ onChange={(value) => setAiEnabled(value === 'enabled')}
370
+ >
371
+ <option value="enabled">Enabled</option>
372
+ <option value="disabled">Disabled</option>
373
+ </SelectField>
374
+ </Box>
375
+ <Box flex="1">
376
+ <SelectField
377
+ label="AI mode"
378
+ value={aiMode}
379
+ onChange={(value) =>
380
+ setAiMode(value === 'llm' ? 'llm' : 'gate')
381
+ }
382
+ >
383
+ <option value="gate">Gate</option>
384
+ <option value="llm">LLM</option>
385
+ </SelectField>
386
+ </Box>
387
+ <Box flex="1">
388
+ <Field.Root>
389
+ <Field.Label>Minimum quality</Field.Label>
390
+ <Input
391
+ type="number"
392
+ min={0}
393
+ max={5}
394
+ step={1}
395
+ value={minAiQuality}
396
+ onChange={(event) => setMinAiQuality(event.target.value)}
397
+ />
398
+ </Field.Root>
399
+ </Box>
400
+ </Flex>
401
+ <Text color="gray.300" fontWeight="semibold">
402
+ ML
403
+ </Text>
404
+ <Flex gap={4} align="start">
405
+ <Box flex="1">
406
+ <SelectField
407
+ label="ML status"
408
+ value={mlEnabled ? 'enabled' : 'disabled'}
409
+ onChange={(value) => setMlEnabled(value === 'enabled')}
410
+ >
411
+ <option value="enabled">Enabled</option>
412
+ <option value="disabled">Disabled</option>
413
+ </SelectField>
414
+ </Box>
415
+ <Box flex="1">
416
+ <Field.Root>
417
+ <Field.Label>ML threshold</Field.Label>
418
+ <Input
419
+ type="number"
420
+ min={0}
421
+ max={1}
422
+ step="0.01"
423
+ value={mlThreshold}
424
+ onChange={(event) => setMlThreshold(event.target.value)}
425
+ />
426
+ </Field.Root>
427
+ </Box>
428
+ </Flex>
429
+ <Field.Root flex="1">
430
+ <Field.Label>Strategy parameters (JSON)</Field.Label>
431
+ <Textarea
432
+ value={parameters}
433
+ onChange={(event) => setParameters(event.target.value)}
434
+ minH="300px"
435
+ fontFamily="mono"
436
+ fontSize="sm"
437
+ />
438
+ </Field.Root>
439
+ </Drawer.Body>
440
+ <Drawer.Footer>
441
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
442
+ Cancel
443
+ </Button>
444
+ <Button
445
+ colorPalette="teal"
446
+ loading={saving}
447
+ disabled={loading || !strategyName || !configId.trim()}
448
+ onClick={() => void save()}
449
+ >
450
+ {editing ? 'Save changes' : 'Create'}
451
+ </Button>
452
+ </Drawer.Footer>
453
+ </Drawer.Content>
454
+ </Drawer.Positioner>
455
+ </Portal>
456
+ </Drawer.Root>
457
+ );
458
+ };