@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 +6 -6
- package/src/app/api/strategies/ai/route.ts +49 -8
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +1 -17
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +160 -41
- package/src/app/components/Strategies/StrategySnapshotCard.presenter.ts +23 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +12 -0
- package/src/app/components/Strategies/StrategySnapshotList.tsx +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.
|
|
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.
|
|
56
|
-
"@tradejs/indicators": "^3.1.
|
|
57
|
-
"@tradejs/infra": "^3.1.
|
|
58
|
-
"@tradejs/node": "^3.1.
|
|
59
|
-
"@tradejs/types": "^3.1.
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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={
|
|
77
|
+
itemSize={getSnapshotCardRowHeight(mode)}
|
|
74
78
|
overscanCount={overscan}
|
|
75
79
|
itemKey={itemKey}
|
|
76
80
|
>
|