@tradejs/app 3.1.8 → 3.1.9

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.8",
3
+ "version": "3.1.9",
4
4
  "description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -52,11 +52,11 @@
52
52
  "@chakra-ui/charts": "3.36.1",
53
53
  "@chakra-ui/react": "3.36.1",
54
54
  "@emotion/react": "^11.14.0",
55
- "@tradejs/core": "^3.1.8",
56
- "@tradejs/indicators": "^3.1.8",
57
- "@tradejs/infra": "^3.1.8",
58
- "@tradejs/node": "^3.1.8",
59
- "@tradejs/types": "^3.1.8",
55
+ "@tradejs/core": "^3.1.9",
56
+ "@tradejs/indicators": "^3.1.9",
57
+ "@tradejs/infra": "^3.1.9",
58
+ "@tradejs/node": "^3.1.9",
59
+ "@tradejs/types": "^3.1.9",
60
60
  "@types/bcryptjs": "2.4.6",
61
61
  "@types/lodash": "4.17.24",
62
62
  "@types/node": "24.13.3",
@@ -7,6 +7,7 @@ import type {
7
7
  } from '@tradejs/types';
8
8
  import { toFileToken } from '@tradejs/infra/ai';
9
9
  import { getData, getKeys, redisKeys } from '@tradejs/infra/redis';
10
+ import { getAvailableStrategyNames } from '@tradejs/node/registry';
10
11
  import { getCurrentUserName } from '#app/lib/currentUser';
11
12
 
12
13
  export const dynamic = 'force-dynamic';
@@ -104,6 +105,46 @@ const normalizeStrategyChartCard = (
104
105
  orders: Array.isArray(card.orders) ? card.orders : [],
105
106
  });
106
107
 
108
+ const normalizeLegacyStrategyNames = async (cards: StrategyChartSnapshot[]) => {
109
+ let availableStrategyNames: string[] = [];
110
+
111
+ try {
112
+ availableStrategyNames = await getAvailableStrategyNames(getProjectRoot());
113
+ } catch {
114
+ return cards;
115
+ }
116
+
117
+ const canonicalNameByToken = new Map(
118
+ availableStrategyNames.map((strategyName) => [
119
+ toFileToken(strategyName),
120
+ strategyName,
121
+ ]),
122
+ );
123
+
124
+ return cards.map((card) => {
125
+ const canonicalName = canonicalNameByToken.get(
126
+ toFileToken(card.strategyName),
127
+ );
128
+ if (!canonicalName || canonicalName === card.strategyName) {
129
+ return card;
130
+ }
131
+
132
+ const titlePrefix = `${card.strategyName} · `;
133
+ const title =
134
+ card.title === card.strategyName
135
+ ? canonicalName
136
+ : card.title.startsWith(titlePrefix)
137
+ ? `${canonicalName}${card.title.slice(card.strategyName.length)}`
138
+ : card.title;
139
+
140
+ return {
141
+ ...card,
142
+ strategyName: canonicalName,
143
+ title,
144
+ };
145
+ });
146
+ };
147
+
107
148
  export async function GET() {
108
149
  const userName = await getCurrentUserName();
109
150
  if (!userName) {
@@ -111,7 +152,7 @@ export async function GET() {
111
152
  }
112
153
 
113
154
  const keys = await getKeys(redisKeys.strategyChartCards(userName, 'ai'));
114
- const cards = (
155
+ const storedCards = (
115
156
  await Promise.all(
116
157
  keys.map(
117
158
  (key) => getData(key, null) as Promise<StrategyChartSnapshot | null>,
@@ -119,13 +160,13 @@ export async function GET() {
119
160
  )
120
161
  )
121
162
  .filter((card): card is StrategyChartSnapshot => Boolean(card))
122
- .map(normalizeStrategyChartCard)
123
- .sort(
124
- (left, right) =>
125
- resolveTotalPnl(right) - resolveTotalPnl(left) ||
126
- right.generatedAt - left.generatedAt ||
127
- left.title.localeCompare(right.title),
128
- );
163
+ .map(normalizeStrategyChartCard);
164
+ const cards = (await normalizeLegacyStrategyNames(storedCards)).sort(
165
+ (left, right) =>
166
+ resolveTotalPnl(right) - resolveTotalPnl(left) ||
167
+ right.generatedAt - left.generatedAt ||
168
+ left.title.localeCompare(right.title),
169
+ );
129
170
  const strategies = await attachLegacyDatasetIds(cards);
130
171
 
131
172
  const data: StrategyChartsSnapshotResponse = {
@@ -134,23 +134,6 @@ export const RuntimeStrategyCard = ({
134
134
  <Badge colorPalette="cyan" variant="outline">
135
135
  TF: {strategy.interval}m
136
136
  </Badge>
137
- <Badge colorPalette="gray" variant="outline">
138
- version: v{strategy.version}
139
- </Badge>
140
- {strategy.accountId ? (
141
- <Badge colorPalette="purple" variant="outline">
142
- account: {strategy.accountLabel ?? strategy.accountId}
143
- </Badge>
144
- ) : null}
145
- <Badge colorPalette="orange" variant="outline">
146
- deployment: {strategy.deploymentId}
147
- </Badge>
148
- {strategy.policyProfileId ? (
149
- <Badge colorPalette="cyan" variant="outline">
150
- policy: {strategy.policyProfileId}
151
- </Badge>
152
- ) : null}
153
-
154
137
  <Flex gap="1">
155
138
  <Text fontSize="sm" fontWeight="bold" color="gray.400" mt={1}>
156
139
  connector:
@@ -244,6 +227,7 @@ export const RuntimeStrategyCard = ({
244
227
  <RuntimeStrategyConfigDrawer
245
228
  open={configOpen}
246
229
  strategy={strategy}
230
+ provider={provider}
247
231
  onOpenChange={setConfigOpen}
248
232
  />
249
233
 
@@ -1,10 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ Box,
4
5
  Button,
6
+ Clipboard,
5
7
  CloseButton,
6
8
  Drawer,
9
+ Flex,
10
+ IconButton,
7
11
  Portal,
12
+ SimpleGrid,
8
13
  Text,
9
14
  Textarea,
10
15
  } from '@chakra-ui/react';
@@ -13,49 +18,163 @@ import type { RuntimeStrategyView } from '@tradejs/types';
13
18
  export const RuntimeStrategyConfigDrawer = ({
14
19
  open,
15
20
  strategy,
21
+ provider,
16
22
  onOpenChange,
17
23
  }: {
18
24
  open: boolean;
19
25
  strategy: RuntimeStrategyView;
26
+ provider: string;
20
27
  onOpenChange: (open: boolean) => void;
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} version v{strategy.version}
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
- This configuration is read-only here. Change it in
42
- tradejs.config.ts and deploy a new Project image.
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
- );
28
+ }) => {
29
+ const config = JSON.stringify(strategy.config, null, 2);
30
+ const serviceInfo = [
31
+ { label: 'Version', value: `v${strategy.version}` },
32
+ { label: 'Config ID', value: strategy.configId },
33
+ { label: 'Runtime key', value: strategy.runtimeKey },
34
+ { label: 'Deployment', value: strategy.deploymentId },
35
+ { label: 'Account label', value: strategy.accountLabel ?? 'Not set' },
36
+ { label: 'Account ID', value: strategy.accountId ?? 'Not assigned' },
37
+ {
38
+ label: 'Policy profile',
39
+ value: strategy.policyProfileId ?? 'Not assigned',
40
+ },
41
+ { label: 'Connector', value: provider },
42
+ { label: 'Universe', value: strategy.universe },
43
+ { label: 'Timeframe', value: `${strategy.interval}m` },
44
+ {
45
+ label: 'Strategy state',
46
+ value: strategy.enabled ? 'Enabled' : 'Disabled',
47
+ },
48
+ { label: 'Control state', value: strategy.controlState },
49
+ {
50
+ label: 'Runtime connection',
51
+ value: strategy.connected ? 'Connected' : 'Disconnected',
52
+ },
53
+ { label: 'Total trades', value: strategy.summary.totalTrades },
54
+ { label: 'Active trades', value: strategy.summary.activeTrades },
55
+ { label: 'Closed trades', value: strategy.summary.closedTrades },
56
+ { label: 'Tracked symbols', value: strategy.symbols.length },
57
+ ];
58
+
59
+ return (
60
+ <Drawer.Root
61
+ size="lg"
62
+ open={open}
63
+ onOpenChange={(event) => onOpenChange(event.open)}
64
+ >
65
+ <Portal>
66
+ <Drawer.Backdrop />
67
+ <Drawer.Positioner>
68
+ <Drawer.Content bg="gray.950">
69
+ <Drawer.Header>
70
+ <Drawer.Title>{strategy.strategyName} configuration</Drawer.Title>
71
+ <Drawer.CloseTrigger asChild>
72
+ <CloseButton size="sm" />
73
+ </Drawer.CloseTrigger>
74
+ </Drawer.Header>
75
+ <Drawer.Body
76
+ display="flex"
77
+ flexDirection="column"
78
+ gap={4}
79
+ overflowY="auto"
80
+ >
81
+ <Text color="gray.400">
82
+ This configuration is read-only here. Change it in
83
+ tradejs.config.ts and deploy a new Project image.
84
+ </Text>
85
+
86
+ <Box
87
+ p={4}
88
+ borderWidth="1px"
89
+ borderColor="gray.800"
90
+ borderRadius="md"
91
+ bg="gray.900"
92
+ >
93
+ <Text
94
+ mb={3}
95
+ fontSize="xs"
96
+ fontWeight="semibold"
97
+ color="gray.400"
98
+ textTransform="uppercase"
99
+ letterSpacing="wide"
100
+ >
101
+ Runtime details
102
+ </Text>
103
+ <SimpleGrid columns={{ base: 1, sm: 2 }} gapX={6} gapY={4}>
104
+ {serviceInfo.map((item) => (
105
+ <Box key={item.label} minW={0}>
106
+ <Text fontSize="xs" color="gray.500">
107
+ {item.label}
108
+ </Text>
109
+ <Text
110
+ mt={1}
111
+ fontFamily="mono"
112
+ fontSize="sm"
113
+ color="gray.200"
114
+ overflowWrap="anywhere"
115
+ >
116
+ {item.value}
117
+ </Text>
118
+ </Box>
119
+ ))}
120
+ </SimpleGrid>
121
+ <Box mt={4} pt={4} borderTopWidth="1px" borderColor="gray.800">
122
+ <Text fontSize="xs" color="gray.500">
123
+ Symbols
124
+ </Text>
125
+ <Text
126
+ mt={1}
127
+ fontFamily="mono"
128
+ fontSize="sm"
129
+ color="gray.200"
130
+ overflowWrap="anywhere"
131
+ >
132
+ {strategy.symbols.length
133
+ ? strategy.symbols.join(', ')
134
+ : 'No symbols'}
135
+ </Text>
136
+ </Box>
137
+ </Box>
138
+
139
+ <Clipboard.Root value={config} display="block" w="full">
140
+ <Flex mb={2} alignItems="center" justifyContent="space-between">
141
+ <Text
142
+ fontSize="xs"
143
+ fontWeight="semibold"
144
+ color="gray.400"
145
+ textTransform="uppercase"
146
+ letterSpacing="wide"
147
+ >
148
+ Strategy config
149
+ </Text>
150
+ <Clipboard.Trigger asChild>
151
+ <IconButton
152
+ aria-label="Copy strategy config"
153
+ title="Copy strategy config"
154
+ size="xs"
155
+ variant="ghost"
156
+ >
157
+ <Clipboard.Indicator />
158
+ </IconButton>
159
+ </Clipboard.Trigger>
160
+ </Flex>
161
+ <Textarea
162
+ value={config}
163
+ readOnly
164
+ minH="45vh"
165
+ fontFamily="mono"
166
+ fontSize="sm"
167
+ />
168
+ </Clipboard.Root>
169
+ </Drawer.Body>
170
+ <Drawer.Footer>
171
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
172
+ Close
173
+ </Button>
174
+ </Drawer.Footer>
175
+ </Drawer.Content>
176
+ </Drawer.Positioner>
177
+ </Portal>
178
+ </Drawer.Root>
179
+ );
180
+ };
@@ -31,6 +31,27 @@ export {
31
31
  type SymbolPnlRank,
32
32
  } from './StrategySnapshotCard.ranking.presenter';
33
33
 
34
+ const formatDatasetCreatedAt = (datasetId?: string) => {
35
+ if (!datasetId || !/^\d{12,}$/.test(datasetId)) {
36
+ return '';
37
+ }
38
+
39
+ const timestamp = Number(datasetId);
40
+ if (!Number.isSafeInteger(timestamp)) {
41
+ return '';
42
+ }
43
+
44
+ const date = new Date(timestamp);
45
+ if (!Number.isFinite(date.getTime())) {
46
+ return '';
47
+ }
48
+
49
+ return new Intl.DateTimeFormat('en-GB', {
50
+ dateStyle: 'medium',
51
+ timeStyle: 'short',
52
+ }).format(date);
53
+ };
54
+
34
55
  export const buildStrategySnapshotCardViewModel = (
35
56
  snapshot: StrategyChartSnapshot,
36
57
  mode: 'replay' | 'ai',
@@ -88,6 +109,8 @@ export const buildStrategySnapshotCardViewModel = (
88
109
  sourceLabel: mode === 'ai' && snapshot.datasetId ? 'dataset:' : 'symbols:',
89
110
  sourceValue:
90
111
  mode === 'ai' && snapshot.datasetId ? snapshot.datasetId : symbolsLabel,
112
+ datasetCreatedAtLabel:
113
+ mode === 'ai' ? formatDatasetCreatedAt(snapshot.datasetId) : '',
91
114
  tagsLabel: snapshot.tags?.join(' · ') ?? '',
92
115
  displaySubtitle:
93
116
  mode === 'ai'
@@ -53,6 +53,7 @@ export const StrategySnapshotCard = ({
53
53
  snapshotOrders,
54
54
  sourceLabel,
55
55
  sourceValue,
56
+ datasetCreatedAtLabel,
56
57
  tagsLabel,
57
58
  displaySubtitle,
58
59
  metrics,
@@ -131,6 +132,17 @@ export const StrategySnapshotCard = ({
131
132
  </Text>
132
133
  </Flex>
133
134
 
135
+ {datasetCreatedAtLabel ? (
136
+ <Flex gap="1">
137
+ <Text fontSize="sm" fontWeight="bold" color="gray.400" mt={1}>
138
+ exported:
139
+ </Text>
140
+ <Text fontSize="sm" color="gray.300" mt={1}>
141
+ {datasetCreatedAtLabel}
142
+ </Text>
143
+ </Flex>
144
+ ) : null}
145
+
134
146
  {tagsLabel ? (
135
147
  <Box
136
148
  px={2}
@@ -7,7 +7,11 @@ import { Box } from '@chakra-ui/react';
7
7
  import type { StrategyChartSnapshot } from '@tradejs/types';
8
8
  import { StrategySnapshotCard } from './StrategySnapshotCard';
9
9
 
10
- const SNAPSHOT_CARD_ROW_HEIGHT = 620;
10
+ const REPLAY_SNAPSHOT_CARD_ROW_HEIGHT = 620;
11
+ const AI_SNAPSHOT_CARD_ROW_HEIGHT = 548;
12
+
13
+ export const getSnapshotCardRowHeight = (mode: 'replay' | 'ai') =>
14
+ mode === 'ai' ? AI_SNAPSHOT_CARD_ROW_HEIGHT : REPLAY_SNAPSHOT_CARD_ROW_HEIGHT;
11
15
 
12
16
  interface StrategySnapshotListProps {
13
17
  strategies: StrategyChartSnapshot[];
@@ -70,7 +74,7 @@ export const StrategySnapshotList = ({
70
74
  height={height}
71
75
  width={width}
72
76
  itemCount={strategies.length}
73
- itemSize={SNAPSHOT_CARD_ROW_HEIGHT}
77
+ itemSize={getSnapshotCardRowHeight(mode)}
74
78
  overscanCount={overscan}
75
79
  itemKey={itemKey}
76
80
  >