@tradejs/app 3.1.3 → 3.1.4

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.
@@ -1,501 +1,61 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- type ReactNode,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useState,
9
- } from 'react';
10
- import {
11
- Box,
12
4
  Button,
13
- Checkbox,
14
5
  CloseButton,
15
6
  Drawer,
16
- Field,
17
- Flex,
18
- Input,
19
- NativeSelect,
20
7
  Portal,
21
8
  Text,
22
9
  Textarea,
23
10
  } from '@chakra-ui/react';
24
- import type { MarketUniverse, RuntimeStrategyView } from '@tradejs/types';
25
- import {
26
- DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS,
27
- mergeRuntimeStrategyManagedParameters,
28
- splitRuntimeStrategyConfig,
29
- } from '#app/lib/runtimeStrategyConfigForm';
30
- import { toaster } from '#ui';
31
-
32
- type AccountOption = {
33
- id: string;
34
- label: string;
35
- provider: string;
36
- enabled: boolean;
37
- isDefault?: boolean;
38
- universes: MarketUniverse[];
39
- };
40
-
41
- type OptionsResponse = {
42
- strategyNames: string[];
43
- accounts: AccountOption[];
44
- intervals: string[];
45
- error?: string;
46
- };
47
-
48
- const SelectField = ({
49
- label,
50
- value,
51
- onChange,
52
- children,
53
- disabled,
54
- }: {
55
- label: string;
56
- value: string;
57
- onChange: (value: string) => void;
58
- children: ReactNode;
59
- disabled?: boolean;
60
- }) => (
61
- <Field.Root>
62
- <Field.Label>{label}</Field.Label>
63
- <NativeSelect.Root disabled={disabled}>
64
- <NativeSelect.Field
65
- value={value}
66
- onChange={(event) => onChange(event.currentTarget.value)}
67
- >
68
- {children}
69
- </NativeSelect.Field>
70
- <NativeSelect.Indicator />
71
- </NativeSelect.Root>
72
- </Field.Root>
73
- );
11
+ import type { RuntimeStrategyView } from '@tradejs/types';
74
12
 
75
13
  export const RuntimeStrategyConfigDrawer = ({
76
14
  open,
77
15
  strategy,
78
16
  onOpenChange,
79
- onSaved,
80
17
  }: {
81
18
  open: boolean;
82
- strategy?: RuntimeStrategyView | null;
19
+ strategy: RuntimeStrategyView;
83
20
  onOpenChange: (open: boolean) => void;
84
- onSaved: () => Promise<void> | void;
85
- }) => {
86
- const editing = Boolean(strategy);
87
- const [options, setOptions] = useState<OptionsResponse>({
88
- strategyNames: [],
89
- accounts: [],
90
- intervals: ['15'],
91
- });
92
- const [strategyName, setStrategyName] = useState('');
93
- const [configId, setConfigId] = useState('config');
94
- const [interval, setInterval] = useState('15');
95
- const [universe, setUniverse] = useState<MarketUniverse>('crypto');
96
- const [accountId, setAccountId] = useState('');
97
- const [enabled, setEnabled] = useState(true);
98
- const [maxLossValue, setMaxLossValue] = useState('1');
99
- const [aiEnabled, setAiEnabled] = useState(true);
100
- const [aiMode, setAiMode] = useState<'gate' | 'llm'>('gate');
101
- const [minAiQuality, setMinAiQuality] = useState('4');
102
- const [mlEnabled, setMlEnabled] = useState(false);
103
- const [mlThreshold, setMlThreshold] = useState('0.1');
104
- const [parameters, setParameters] = useState('{}');
105
- const [loading, setLoading] = useState(false);
106
- const [saving, setSaving] = useState(false);
107
-
108
- const loadOptions = useCallback(async () => {
109
- setLoading(true);
110
- try {
111
- const response = await fetch('/api/user/runtime-strategy-configs');
112
- const payload = (await response.json()) as OptionsResponse;
113
- if (!response.ok)
114
- throw new Error(
115
- payload.error || 'Failed to load configuration options',
116
- );
117
- setOptions(payload);
118
- if (!editing && !strategyName)
119
- setStrategyName(payload.strategyNames[0] ?? '');
120
- } catch (error) {
121
- toaster.error({
122
- title: 'Failed to load strategy settings',
123
- description: error instanceof Error ? error.message : String(error),
124
- });
125
- } finally {
126
- setLoading(false);
127
- }
128
- }, [editing, strategyName]);
129
-
130
- useEffect(() => {
131
- if (!open) return;
132
- if (strategy) {
133
- const { managed, parameters: strategyParameters } =
134
- splitRuntimeStrategyConfig(strategy.config);
135
- setStrategyName(strategy.strategyName);
136
- setConfigId(strategy.configId);
137
- setInterval(String(strategy.interval ?? '15'));
138
- setUniverse(strategy.universe ?? 'crypto');
139
- setAccountId(String(strategy.config?.ACCOUNT_ID ?? ''));
140
- setEnabled(strategy.enabled);
141
- setMaxLossValue(String(managed.maxLossValue));
142
- setAiEnabled(managed.aiEnabled);
143
- setAiMode(managed.aiMode);
144
- setMinAiQuality(String(managed.minAiQuality));
145
- setMlEnabled(managed.mlEnabled);
146
- setMlThreshold(String(managed.mlThreshold));
147
- setParameters(JSON.stringify(strategyParameters, null, 2));
148
- } else {
149
- const managed = DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS;
150
- setConfigId('config');
151
- setInterval('15');
152
- setUniverse('crypto');
153
- setAccountId('');
154
- setEnabled(true);
155
- setMaxLossValue(String(managed.maxLossValue));
156
- setAiEnabled(managed.aiEnabled);
157
- setAiMode(managed.aiMode);
158
- setMinAiQuality(String(managed.minAiQuality));
159
- setMlEnabled(managed.mlEnabled);
160
- setMlThreshold(String(managed.mlThreshold));
161
- setParameters('{}');
162
- }
163
- if (!strategy?.releaseVersion) void loadOptions();
164
- }, [loadOptions, open, strategy]);
165
-
166
- const compatibleAccounts = useMemo(
167
- () =>
168
- options.accounts.filter(
169
- (account) =>
170
- account.enabled &&
171
- account.provider === 'bybit' &&
172
- account.universes.includes(universe),
173
- ),
174
- [options.accounts, universe],
175
- );
176
-
177
- useEffect(() => {
178
- if (
179
- accountId &&
180
- !compatibleAccounts.some((account) => account.id === accountId)
181
- ) {
182
- setAccountId('');
183
- }
184
- }, [accountId, compatibleAccounts]);
185
-
186
- const save = async () => {
187
- setSaving(true);
188
- try {
189
- const parsed = JSON.parse(parameters) as unknown;
190
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
191
- throw new Error('Strategy parameters must be a JSON object');
192
- }
193
- const parsedMaxLossValue = Number(maxLossValue);
194
- if (!Number.isFinite(parsedMaxLossValue) || parsedMaxLossValue < 0) {
195
- throw new Error('Max loss value must be a non-negative number');
196
- }
197
- const parsedMinAiQuality = Number(minAiQuality);
198
- if (
199
- !Number.isInteger(parsedMinAiQuality) ||
200
- parsedMinAiQuality < 0 ||
201
- parsedMinAiQuality > 5
202
- ) {
203
- throw new Error('Minimum AI quality must be an integer from 0 to 5');
204
- }
205
- const parsedMlThreshold = Number(mlThreshold);
206
- if (
207
- !Number.isFinite(parsedMlThreshold) ||
208
- parsedMlThreshold < 0 ||
209
- parsedMlThreshold > 1
210
- ) {
211
- throw new Error('ML threshold must be a number from 0 to 1');
212
- }
213
- const managedParameters = mergeRuntimeStrategyManagedParameters(
214
- parsed as Record<string, unknown>,
215
- {
216
- maxLossValue: parsedMaxLossValue,
217
- aiEnabled,
218
- aiMode,
219
- minAiQuality: parsedMinAiQuality,
220
- mlEnabled,
221
- mlThreshold: parsedMlThreshold,
222
- },
223
- );
224
- const response = await fetch('/api/user/runtime-strategy-configs', {
225
- method: editing ? 'PATCH' : 'POST',
226
- headers: { 'content-type': 'application/json' },
227
- body: JSON.stringify({
228
- strategyName,
229
- configId,
230
- interval,
231
- universe,
232
- accountId: accountId || null,
233
- enabled,
234
- parameters: managedParameters,
235
- }),
236
- });
237
- const payload = (await response.json()) as { error?: string };
238
- if (!response.ok) throw new Error(payload.error || 'Save failed');
239
- toaster.success({
240
- title: editing ? 'Strategy config updated' : 'Strategy config created',
241
- });
242
- onOpenChange(false);
243
- await onSaved();
244
- } catch (error) {
245
- toaster.error({
246
- title: 'Could not save strategy config',
247
- description: error instanceof Error ? error.message : String(error),
248
- });
249
- } finally {
250
- setSaving(false);
251
- }
252
- };
253
-
254
- if (strategy?.releaseVersion) {
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
- {strategy.strategyName} release v{strategy.releaseVersion}
268
- </Drawer.Title>
269
- <Drawer.CloseTrigger asChild>
270
- <CloseButton size="sm" />
271
- </Drawer.CloseTrigger>
272
- </Drawer.Header>
273
- <Drawer.Body display="flex" flexDirection="column" gap={4}>
274
- <Text color="gray.400">
275
- Published releases are immutable. Create and publish a new
276
- release to change this configuration.
277
- </Text>
278
- <Textarea
279
- value={JSON.stringify(strategy.config ?? {}, null, 2)}
280
- readOnly
281
- minH="70vh"
282
- fontFamily="mono"
283
- fontSize="sm"
284
- />
285
- </Drawer.Body>
286
- <Drawer.Footer>
287
- <Button variant="outline" onClick={() => onOpenChange(false)}>
288
- Close
289
- </Button>
290
- </Drawer.Footer>
291
- </Drawer.Content>
292
- </Drawer.Positioner>
293
- </Portal>
294
- </Drawer.Root>
295
- );
296
- }
297
-
298
- return (
299
- <Drawer.Root
300
- size="lg"
301
- open={open}
302
- onOpenChange={(event) => onOpenChange(event.open)}
303
- >
304
- <Portal>
305
- <Drawer.Backdrop />
306
- <Drawer.Positioner>
307
- <Drawer.Content bg="gray.950">
308
- <Drawer.Header>
309
- <Drawer.Title>
310
- {editing
311
- ? 'Edit strategy configuration'
312
- : 'Create strategy configuration'}
313
- </Drawer.Title>
314
- <Drawer.CloseTrigger asChild>
315
- <CloseButton size="sm" />
316
- </Drawer.CloseTrigger>
317
- </Drawer.Header>
318
- <Drawer.Body display="flex" flexDirection="column" gap={5}>
319
- {editing ? (
320
- <Field.Root>
321
- <Field.Label>Strategy</Field.Label>
322
- <Input value={strategyName} disabled />
323
- </Field.Root>
324
- ) : (
325
- <SelectField
326
- label="Strategy"
327
- value={strategyName}
328
- onChange={setStrategyName}
329
- disabled={loading}
330
- >
331
- {options.strategyNames.map((name) => (
332
- <option key={name} value={name}>
333
- {name}
334
- </option>
335
- ))}
336
- </SelectField>
337
- )}
338
- <Field.Root>
339
- <Field.Label>Configuration id</Field.Label>
340
- <Input
341
- value={configId}
342
- disabled={editing}
343
- onChange={(event) => setConfigId(event.target.value)}
344
- />
345
- </Field.Root>
346
- <Flex gap={4} align="start">
347
- <Box flex="1">
348
- <SelectField
349
- label="Timeframe"
350
- value={interval}
351
- onChange={setInterval}
352
- >
353
- {options.intervals.map((value) => (
354
- <option key={value} value={value}>
355
- {value}m
356
- </option>
357
- ))}
358
- </SelectField>
359
- </Box>
360
- <Box flex="1">
361
- <SelectField
362
- label="Universe"
363
- value={universe}
364
- onChange={(value) => setUniverse(value as MarketUniverse)}
365
- >
366
- <option value="crypto">Crypto</option>
367
- <option value="tradfi">TradFi</option>
368
- </SelectField>
369
- </Box>
370
- </Flex>
371
- <SelectField
372
- label="Trading account"
373
- value={accountId}
374
- onChange={setAccountId}
375
- >
376
- <option value="">Default account for {universe}</option>
377
- {compatibleAccounts.map((account) => (
378
- <option key={account.id} value={account.id}>
379
- {account.label}
380
- {account.isDefault ? ' (default)' : ''}
381
- </option>
382
- ))}
383
- </SelectField>
384
- <Checkbox.Root
385
- checked={enabled}
386
- onCheckedChange={(event) => setEnabled(event.checked === true)}
387
- >
388
- <Checkbox.HiddenInput />
389
- <Checkbox.Control>
390
- <Checkbox.Indicator />
391
- </Checkbox.Control>
392
- <Checkbox.Label>Enabled</Checkbox.Label>
393
- </Checkbox.Root>
394
- <Field.Root>
395
- <Field.Label>Max loss value</Field.Label>
396
- <Input
397
- type="number"
398
- min={0}
399
- step="0.1"
400
- value={maxLossValue}
401
- onChange={(event) => setMaxLossValue(event.target.value)}
402
- />
403
- </Field.Root>
404
- <Text color="gray.300" fontWeight="semibold">
405
- AI
406
- </Text>
407
- <Flex gap={4} align="start">
408
- <Box flex="1">
409
- <SelectField
410
- label="AI status"
411
- value={aiEnabled ? 'enabled' : 'disabled'}
412
- onChange={(value) => setAiEnabled(value === 'enabled')}
413
- >
414
- <option value="enabled">Enabled</option>
415
- <option value="disabled">Disabled</option>
416
- </SelectField>
417
- </Box>
418
- <Box flex="1">
419
- <SelectField
420
- label="AI mode"
421
- value={aiMode}
422
- onChange={(value) =>
423
- setAiMode(value === 'llm' ? 'llm' : 'gate')
424
- }
425
- >
426
- <option value="gate">Gate</option>
427
- <option value="llm">LLM</option>
428
- </SelectField>
429
- </Box>
430
- <Box flex="1">
431
- <Field.Root>
432
- <Field.Label>Minimum quality</Field.Label>
433
- <Input
434
- type="number"
435
- min={0}
436
- max={5}
437
- step={1}
438
- value={minAiQuality}
439
- onChange={(event) => setMinAiQuality(event.target.value)}
440
- />
441
- </Field.Root>
442
- </Box>
443
- </Flex>
444
- <Text color="gray.300" fontWeight="semibold">
445
- ML
446
- </Text>
447
- <Flex gap={4} align="start">
448
- <Box flex="1">
449
- <SelectField
450
- label="ML status"
451
- value={mlEnabled ? 'enabled' : 'disabled'}
452
- onChange={(value) => setMlEnabled(value === 'enabled')}
453
- >
454
- <option value="enabled">Enabled</option>
455
- <option value="disabled">Disabled</option>
456
- </SelectField>
457
- </Box>
458
- <Box flex="1">
459
- <Field.Root>
460
- <Field.Label>ML threshold</Field.Label>
461
- <Input
462
- type="number"
463
- min={0}
464
- max={1}
465
- step="0.01"
466
- value={mlThreshold}
467
- onChange={(event) => setMlThreshold(event.target.value)}
468
- />
469
- </Field.Root>
470
- </Box>
471
- </Flex>
472
- <Field.Root flex="1">
473
- <Field.Label>Strategy parameters (JSON)</Field.Label>
474
- <Textarea
475
- value={parameters}
476
- onChange={(event) => setParameters(event.target.value)}
477
- minH="300px"
478
- fontFamily="mono"
479
- fontSize="sm"
480
- />
481
- </Field.Root>
482
- </Drawer.Body>
483
- <Drawer.Footer>
484
- <Button variant="outline" onClick={() => onOpenChange(false)}>
485
- Cancel
486
- </Button>
487
- <Button
488
- colorPalette="teal"
489
- loading={saving}
490
- disabled={loading || !strategyName || !configId.trim()}
491
- onClick={() => void save()}
492
- >
493
- {editing ? 'Save changes' : 'Create'}
494
- </Button>
495
- </Drawer.Footer>
496
- </Drawer.Content>
497
- </Drawer.Positioner>
498
- </Portal>
499
- </Drawer.Root>
500
- );
501
- };
21
+ }) => (
22
+ <Drawer.Root
23
+ size="lg"
24
+ open={open}
25
+ onOpenChange={(event) => onOpenChange(event.open)}
26
+ >
27
+ <Portal>
28
+ <Drawer.Backdrop />
29
+ <Drawer.Positioner>
30
+ <Drawer.Content bg="gray.950">
31
+ <Drawer.Header>
32
+ <Drawer.Title>
33
+ {strategy.strategyName} release v{strategy.releaseVersion}
34
+ </Drawer.Title>
35
+ <Drawer.CloseTrigger asChild>
36
+ <CloseButton size="sm" />
37
+ </Drawer.CloseTrigger>
38
+ </Drawer.Header>
39
+ <Drawer.Body display="flex" flexDirection="column" gap={4}>
40
+ <Text color="gray.400">
41
+ Published releases are immutable. Roll out a new release to change
42
+ this configuration.
43
+ </Text>
44
+ <Textarea
45
+ value={JSON.stringify(strategy.config, null, 2)}
46
+ readOnly
47
+ minH="70vh"
48
+ fontFamily="mono"
49
+ fontSize="sm"
50
+ />
51
+ </Drawer.Body>
52
+ <Drawer.Footer>
53
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
54
+ Close
55
+ </Button>
56
+ </Drawer.Footer>
57
+ </Drawer.Content>
58
+ </Drawer.Positioner>
59
+ </Portal>
60
+ </Drawer.Root>
61
+ );
@@ -1,51 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import {
3
- getRuntimeStrategyConfigOptions,
4
- RuntimeStrategyConfigServiceError,
5
- saveRuntimeStrategyConfigForUser,
6
- } from '#app/lib/runtimeStrategyConfigService';
7
- import { getCurrentUserName } from '#app/lib/currentUser';
8
-
9
- export const dynamic = 'force-dynamic';
10
-
11
- const projectRoot =
12
- String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
13
-
14
- export const GET = async () => {
15
- const userName = await getCurrentUserName();
16
- if (!userName)
17
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
18
- return NextResponse.json(
19
- await getRuntimeStrategyConfigOptions({ userName, projectRoot }),
20
- );
21
- };
22
-
23
- const save = async (request: NextRequest, editing: boolean) => {
24
- const userName = await getCurrentUserName();
25
- if (!userName)
26
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
27
- try {
28
- return NextResponse.json(
29
- await saveRuntimeStrategyConfigForUser({
30
- userName,
31
- projectRoot,
32
- input: (await request.json()) as Record<string, unknown>,
33
- editing,
34
- }),
35
- );
36
- } catch (error) {
37
- const message = error instanceof Error ? error.message : String(error);
38
- const status =
39
- error instanceof RuntimeStrategyConfigServiceError &&
40
- error.code === 'conflict'
41
- ? 409
42
- : error instanceof RuntimeStrategyConfigServiceError &&
43
- error.code === 'not_found'
44
- ? 404
45
- : 400;
46
- return NextResponse.json({ error: message }, { status });
47
- }
48
- };
49
-
50
- export const POST = (request: NextRequest) => save(request, false);
51
- export const PATCH = (request: NextRequest) => save(request, true);
@@ -1,60 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import {
3
- getRuntimeStrategyReleaseOptions,
4
- publishRuntimeStrategyReleaseForUser,
5
- saveRuntimeStrategyReleaseDraftForUser,
6
- } from '#app/lib/runtimeStrategyReleaseService';
7
- import { getCurrentUserName } from '#app/lib/currentUser';
8
-
9
- export const dynamic = 'force-dynamic';
10
-
11
- const projectRoot =
12
- String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
13
-
14
- export const GET = async () => {
15
- const userName = await getCurrentUserName();
16
- if (!userName)
17
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
18
- return NextResponse.json(
19
- await getRuntimeStrategyReleaseOptions({ userName, projectRoot }),
20
- );
21
- };
22
-
23
- export const PATCH = async (request: NextRequest) => {
24
- const userName = await getCurrentUserName();
25
- if (!userName)
26
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
27
- try {
28
- const draft = await saveRuntimeStrategyReleaseDraftForUser({
29
- userName,
30
- projectRoot,
31
- input: (await request.json()) as Record<string, unknown>,
32
- });
33
- return NextResponse.json({ draft });
34
- } catch (error) {
35
- return NextResponse.json(
36
- { error: error instanceof Error ? error.message : String(error) },
37
- { status: 400 },
38
- );
39
- }
40
- };
41
-
42
- export const POST = async (request: NextRequest) => {
43
- const userName = await getCurrentUserName();
44
- if (!userName)
45
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
46
- try {
47
- const body = (await request.json()) as { strategyName?: unknown };
48
- const release = await publishRuntimeStrategyReleaseForUser({
49
- userName,
50
- projectRoot,
51
- strategyName: String(body.strategyName ?? '').trim(),
52
- });
53
- return NextResponse.json({ release });
54
- } catch (error) {
55
- return NextResponse.json(
56
- { error: error instanceof Error ? error.message : String(error) },
57
- { status: 400 },
58
- );
59
- }
60
- };