@tradejs/app 3.1.0 → 3.1.1
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/README.md +2 -0
- package/package.json +10 -7
- package/src/app/api/user/runtime-deployments/[deploymentId]/strategies/[strategyName]/control/route.ts +73 -0
- package/src/app/api/user/runtime-deployments/route.ts +55 -8
- package/src/app/api/user/runtime-strategy-releases/route.ts +60 -0
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +64 -5
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +45 -1
- package/src/app/components/Strategies/StrategyEvidencePopover.tsx +3 -1
- package/src/app/lib/runtimeStrategyReleaseService.ts +123 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +1 -18
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"app",
|
|
10
10
|
"dashboard",
|
|
11
11
|
"nextjs",
|
|
12
|
-
"signals"
|
|
12
|
+
"signals",
|
|
13
|
+
"ai",
|
|
14
|
+
"claude",
|
|
15
|
+
"codex"
|
|
13
16
|
],
|
|
14
17
|
"files": [
|
|
15
18
|
"README.md",
|
|
@@ -49,11 +52,11 @@
|
|
|
49
52
|
"@chakra-ui/charts": "3.36.1",
|
|
50
53
|
"@chakra-ui/react": "3.36.1",
|
|
51
54
|
"@emotion/react": "^11.14.0",
|
|
52
|
-
"@tradejs/core": "^3.1.
|
|
53
|
-
"@tradejs/indicators": "^3.1.
|
|
54
|
-
"@tradejs/infra": "^3.1.
|
|
55
|
-
"@tradejs/node": "^3.1.
|
|
56
|
-
"@tradejs/types": "^3.1.
|
|
55
|
+
"@tradejs/core": "^3.1.1",
|
|
56
|
+
"@tradejs/indicators": "^3.1.1",
|
|
57
|
+
"@tradejs/infra": "^3.1.1",
|
|
58
|
+
"@tradejs/node": "^3.1.1",
|
|
59
|
+
"@tradejs/types": "^3.1.1",
|
|
57
60
|
"@types/bcryptjs": "2.4.6",
|
|
58
61
|
"@types/lodash": "4.17.24",
|
|
59
62
|
"@types/node": "24.13.3",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
getRuntimeDeployment,
|
|
4
|
+
saveRuntimeDeployment,
|
|
5
|
+
} from '@tradejs/infra/runtimeDeployments';
|
|
6
|
+
import { recordRuntimeStrategyControlEvent } from '@tradejs/infra/runtimeStrategyReleases';
|
|
7
|
+
import type { RuntimeStrategyControlState } from '@tradejs/types';
|
|
8
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
9
|
+
|
|
10
|
+
export const PATCH = async (
|
|
11
|
+
request: NextRequest,
|
|
12
|
+
{
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: Promise<{ deploymentId: string; strategyName: string }>;
|
|
16
|
+
},
|
|
17
|
+
) => {
|
|
18
|
+
const userName = await getCurrentUserName();
|
|
19
|
+
if (!userName) {
|
|
20
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
const { deploymentId, strategyName: encodedStrategyName } = await params;
|
|
23
|
+
const strategyName = decodeURIComponent(encodedStrategyName);
|
|
24
|
+
const deployment = await getRuntimeDeployment(userName, deploymentId);
|
|
25
|
+
if (!deployment) {
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: 'Deployment not found' },
|
|
28
|
+
{ status: 404 },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const body = (await request.json()) as { controlState?: unknown };
|
|
32
|
+
if (
|
|
33
|
+
body.controlState !== 'active' &&
|
|
34
|
+
body.controlState !== 'entries_paused'
|
|
35
|
+
) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: 'controlState must be active or entries_paused' },
|
|
38
|
+
{ status: 400 },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const controlState: RuntimeStrategyControlState = body.controlState;
|
|
42
|
+
const reference = deployment.strategies.find(
|
|
43
|
+
(strategy) => strategy.strategyName === strategyName,
|
|
44
|
+
);
|
|
45
|
+
if (!reference?.releaseVersion) {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: 'Pause/resume requires a versioned strategy release' },
|
|
48
|
+
{ status: 400 },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const previousState = reference.controlState ?? 'active';
|
|
52
|
+
if (previousState === controlState) {
|
|
53
|
+
return NextResponse.json({ deployment, controlState });
|
|
54
|
+
}
|
|
55
|
+
const updated = await saveRuntimeDeployment(userName, {
|
|
56
|
+
...deployment,
|
|
57
|
+
strategies: deployment.strategies.map((strategy) =>
|
|
58
|
+
strategy.strategyName === strategyName
|
|
59
|
+
? { ...strategy, controlState }
|
|
60
|
+
: strategy,
|
|
61
|
+
),
|
|
62
|
+
});
|
|
63
|
+
const event = await recordRuntimeStrategyControlEvent({
|
|
64
|
+
userName,
|
|
65
|
+
deploymentId: deployment.id,
|
|
66
|
+
strategyName,
|
|
67
|
+
releaseVersion: reference.releaseVersion,
|
|
68
|
+
previousState,
|
|
69
|
+
nextState: controlState,
|
|
70
|
+
createdBy: userName,
|
|
71
|
+
});
|
|
72
|
+
return NextResponse.json({ deployment: updated, controlState, event });
|
|
73
|
+
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
saveRuntimeDeployment,
|
|
6
6
|
} from '@tradejs/infra/runtimeDeployments';
|
|
7
7
|
import { getTradingAccount } from '@tradejs/infra/tradingAccounts';
|
|
8
|
+
import { getRuntimeStrategyRelease } from '@tradejs/infra/runtimeStrategyReleases';
|
|
8
9
|
import { isMarketUniverse, type RuntimeDeployment } from '@tradejs/types';
|
|
9
10
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
10
11
|
|
|
@@ -32,21 +33,59 @@ export const POST = async (request: NextRequest) => {
|
|
|
32
33
|
}
|
|
33
34
|
try {
|
|
34
35
|
const body = (await request.json()) as Partial<RuntimeDeployment>;
|
|
35
|
-
const
|
|
36
|
+
const hasVersionedStrategies = Boolean(
|
|
37
|
+
body.strategies?.some((strategy) => strategy.releaseVersion != null),
|
|
38
|
+
);
|
|
39
|
+
if (
|
|
40
|
+
hasVersionedStrategies &&
|
|
41
|
+
body.strategies?.some((strategy) => strategy.releaseVersion == null)
|
|
42
|
+
) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: 'A deployment cannot mix legacy configs and releases' },
|
|
45
|
+
{ status: 400 },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const releases = hasVersionedStrategies
|
|
49
|
+
? await Promise.all(
|
|
50
|
+
(body.strategies ?? []).map(async (strategy) => {
|
|
51
|
+
if (
|
|
52
|
+
!Number.isSafeInteger(strategy.releaseVersion) ||
|
|
53
|
+
!strategy.releaseVersion ||
|
|
54
|
+
(strategy.config && Object.keys(strategy.config).length)
|
|
55
|
+
) {
|
|
56
|
+
throw new Error('Invalid versioned strategy reference');
|
|
57
|
+
}
|
|
58
|
+
const release = await getRuntimeStrategyRelease(
|
|
59
|
+
userName,
|
|
60
|
+
strategy.strategyName,
|
|
61
|
+
strategy.releaseVersion,
|
|
62
|
+
);
|
|
63
|
+
if (!release) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Release not found: ${strategy.strategyName} v${strategy.releaseVersion}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return release;
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
: [];
|
|
72
|
+
const universe = releases[0]?.config.UNIVERSE ?? body.universe;
|
|
73
|
+
const interval = releases[0]?.config.INTERVAL ?? body.interval;
|
|
36
74
|
if (
|
|
37
75
|
!body.id ||
|
|
38
76
|
!body.label ||
|
|
39
77
|
!body.connectorName ||
|
|
40
78
|
!body.provider ||
|
|
41
79
|
!body.accountId ||
|
|
42
|
-
!
|
|
80
|
+
!interval ||
|
|
43
81
|
!isMarketUniverse(universe) ||
|
|
44
82
|
!Array.isArray(body.strategies) ||
|
|
45
83
|
!body.strategies.length ||
|
|
46
|
-
body.strategies.some(
|
|
47
|
-
|
|
48
|
-
!String(strategy.strategyName ?? '').trim()
|
|
49
|
-
!String(strategy.
|
|
84
|
+
body.strategies.some((strategy) =>
|
|
85
|
+
hasVersionedStrategies
|
|
86
|
+
? !String(strategy.strategyName ?? '').trim()
|
|
87
|
+
: !String(strategy.strategyName ?? '').trim() ||
|
|
88
|
+
!String(strategy.policyProfileId ?? '').trim(),
|
|
50
89
|
)
|
|
51
90
|
) {
|
|
52
91
|
return NextResponse.json(
|
|
@@ -78,9 +117,17 @@ export const POST = async (request: NextRequest) => {
|
|
|
78
117
|
provider: body.provider,
|
|
79
118
|
accountId: body.accountId,
|
|
80
119
|
universe,
|
|
81
|
-
interval: String(
|
|
120
|
+
interval: String(interval),
|
|
82
121
|
enabled: body.enabled !== false,
|
|
83
|
-
strategies: body.strategies
|
|
122
|
+
strategies: body.strategies.map((strategy) =>
|
|
123
|
+
hasVersionedStrategies
|
|
124
|
+
? {
|
|
125
|
+
strategyName: strategy.strategyName,
|
|
126
|
+
releaseVersion: strategy.releaseVersion,
|
|
127
|
+
controlState: strategy.controlState ?? 'active',
|
|
128
|
+
}
|
|
129
|
+
: strategy,
|
|
130
|
+
),
|
|
84
131
|
assetClasses: body.assetClasses,
|
|
85
132
|
tickers: body.tickers,
|
|
86
133
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
};
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getColorByLevel,
|
|
27
27
|
RUNTIME_ORDER_ROW_HEIGHT,
|
|
28
28
|
} from './RuntimeStrategyCard.presenter';
|
|
29
|
+
import { toaster } from '#ui';
|
|
29
30
|
|
|
30
31
|
const StatItem = ({
|
|
31
32
|
stat,
|
|
@@ -64,11 +65,42 @@ export const RuntimeStrategyCard = ({
|
|
|
64
65
|
const [configOpen, setConfigOpen] = useState(false);
|
|
65
66
|
const [ordersOpen, setOrdersOpen] = useState(false);
|
|
66
67
|
const [statsOpen, setStatsOpen] = useState(false);
|
|
68
|
+
const [controlSaving, setControlSaving] = useState(false);
|
|
67
69
|
const viewModel = useMemo(
|
|
68
70
|
() => buildRuntimeStrategyCardViewModel(strategy),
|
|
69
71
|
[strategy],
|
|
70
72
|
);
|
|
71
73
|
const { lastTrade, runtimeOrders } = viewModel;
|
|
74
|
+
const setControlState = async (controlState: 'active' | 'entries_paused') => {
|
|
75
|
+
if (!strategy.deploymentId || !strategy.releaseVersion) return;
|
|
76
|
+
setControlSaving(true);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
`/api/user/runtime-deployments/${encodeURIComponent(strategy.deploymentId)}/strategies/${encodeURIComponent(strategy.strategyName)}/control`,
|
|
80
|
+
{
|
|
81
|
+
method: 'PATCH',
|
|
82
|
+
headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ controlState }),
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
const payload = (await response.json()) as { error?: string };
|
|
87
|
+
if (!response.ok) throw new Error(payload.error || 'Update failed');
|
|
88
|
+
toaster.success({
|
|
89
|
+
title:
|
|
90
|
+
controlState === 'entries_paused'
|
|
91
|
+
? 'New entries paused'
|
|
92
|
+
: 'Strategy resumed',
|
|
93
|
+
});
|
|
94
|
+
await onUpdated();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
toaster.error({
|
|
97
|
+
title: 'Could not update strategy state',
|
|
98
|
+
description: error instanceof Error ? error.message : String(error),
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
setControlSaving(false);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
72
104
|
|
|
73
105
|
return (
|
|
74
106
|
<Box
|
|
@@ -91,7 +123,11 @@ export const RuntimeStrategyCard = ({
|
|
|
91
123
|
fontFamily="mono"
|
|
92
124
|
letterSpacing="0"
|
|
93
125
|
>
|
|
94
|
-
{strategy.
|
|
126
|
+
{strategy.controlState === 'entries_paused'
|
|
127
|
+
? 'entries paused'
|
|
128
|
+
: strategy.enabled
|
|
129
|
+
? 'enabled'
|
|
130
|
+
: 'disabled'}
|
|
95
131
|
</Badge>
|
|
96
132
|
<Badge colorPalette="blue" variant="outline">
|
|
97
133
|
{strategy.universe}
|
|
@@ -99,9 +135,15 @@ export const RuntimeStrategyCard = ({
|
|
|
99
135
|
<Badge colorPalette="cyan" variant="outline">
|
|
100
136
|
TF: {strategy.interval}m
|
|
101
137
|
</Badge>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
{strategy.releaseVersion ? (
|
|
139
|
+
<Badge colorPalette="gray" variant="outline">
|
|
140
|
+
release: v{strategy.releaseVersion}
|
|
141
|
+
</Badge>
|
|
142
|
+
) : (
|
|
143
|
+
<Badge colorPalette="gray" variant="outline">
|
|
144
|
+
legacy config: {strategy.configId}
|
|
145
|
+
</Badge>
|
|
146
|
+
)}
|
|
105
147
|
{strategy.accountId ? (
|
|
106
148
|
<Badge colorPalette="purple" variant="outline">
|
|
107
149
|
account: {strategy.accountLabel ?? strategy.accountId}
|
|
@@ -171,7 +213,24 @@ export const RuntimeStrategyCard = ({
|
|
|
171
213
|
<Menu.Content minW="160px">
|
|
172
214
|
{strategy.connected ? (
|
|
173
215
|
<Menu.Item value="edit" onClick={() => setConfigOpen(true)}>
|
|
174
|
-
Edit
|
|
216
|
+
{strategy.releaseVersion ? 'View config' : 'Edit'}
|
|
217
|
+
</Menu.Item>
|
|
218
|
+
) : null}
|
|
219
|
+
{strategy.releaseVersion && strategy.deploymentId ? (
|
|
220
|
+
<Menu.Item
|
|
221
|
+
value="control"
|
|
222
|
+
disabled={controlSaving}
|
|
223
|
+
onClick={() =>
|
|
224
|
+
void setControlState(
|
|
225
|
+
strategy.controlState === 'entries_paused'
|
|
226
|
+
? 'active'
|
|
227
|
+
: 'entries_paused',
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
>
|
|
231
|
+
{strategy.controlState === 'entries_paused'
|
|
232
|
+
? 'Resume entries'
|
|
233
|
+
: 'Pause new entries'}
|
|
175
234
|
</Menu.Item>
|
|
176
235
|
) : null}
|
|
177
236
|
<Menu.Item value="orders" onClick={() => setOrdersOpen(true)}>
|
|
@@ -160,7 +160,7 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
160
160
|
setMlThreshold(String(managed.mlThreshold));
|
|
161
161
|
setParameters('{}');
|
|
162
162
|
}
|
|
163
|
-
void loadOptions();
|
|
163
|
+
if (!strategy?.releaseVersion) void loadOptions();
|
|
164
164
|
}, [loadOptions, open, strategy]);
|
|
165
165
|
|
|
166
166
|
const compatibleAccounts = useMemo(
|
|
@@ -251,6 +251,50 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
251
251
|
}
|
|
252
252
|
};
|
|
253
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
|
+
|
|
254
298
|
return (
|
|
255
299
|
<Drawer.Root
|
|
256
300
|
size="lg"
|
|
@@ -137,7 +137,9 @@ export const StrategyEvidencePopover = ({
|
|
|
137
137
|
)}.`
|
|
138
138
|
: timeline.status === 'invalid'
|
|
139
139
|
? 'Evidence failed checksum, identity, or structure verification. No markers are rendered.'
|
|
140
|
-
:
|
|
140
|
+
: timeline.status === 'not_attached'
|
|
141
|
+
? 'The runtime release is published, but no checksum-verified research artifact is attached yet.'
|
|
142
|
+
: 'No checksum-verified evidence artifact is available. No mutable fallback is used.'}
|
|
141
143
|
</Text>
|
|
142
144
|
</Box>
|
|
143
145
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRuntimeStrategyDraft,
|
|
3
|
+
listRuntimeStrategyReleases,
|
|
4
|
+
publishRuntimeStrategyRelease,
|
|
5
|
+
saveRuntimeStrategyDraft,
|
|
6
|
+
} from '@tradejs/infra/runtimeStrategyReleases';
|
|
7
|
+
import { getRuntimeStrategyPackageMetadata } from '@tradejs/node/runtimeStrategies';
|
|
8
|
+
import {
|
|
9
|
+
getAvailableStrategyNames,
|
|
10
|
+
getStrategyDefaults,
|
|
11
|
+
} from '@tradejs/node/strategies';
|
|
12
|
+
import type { StrategyConfig } from '@tradejs/types';
|
|
13
|
+
|
|
14
|
+
const OPERATIONAL_KEYS = [
|
|
15
|
+
'ACCOUNT_ID',
|
|
16
|
+
'AI_REPLAY_ANALYSES',
|
|
17
|
+
'DEPLOYMENT_ID',
|
|
18
|
+
'ENABLE',
|
|
19
|
+
'ENV',
|
|
20
|
+
'MAKE_ORDERS',
|
|
21
|
+
'RECORD_RUNTIME_TRADES',
|
|
22
|
+
'configId',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const assertConfig = (value: unknown): StrategyConfig => {
|
|
26
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
27
|
+
throw new Error('Strategy config must be a JSON object');
|
|
28
|
+
}
|
|
29
|
+
const config = value as StrategyConfig;
|
|
30
|
+
const invalidKey = OPERATIONAL_KEYS.find((key) => config[key] !== undefined);
|
|
31
|
+
if (invalidKey) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${invalidKey} is controlled by the deployment, not config`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assertKnownStrategy = async (
|
|
40
|
+
strategyName: string,
|
|
41
|
+
projectRoot: string,
|
|
42
|
+
) => {
|
|
43
|
+
if (!(await getAvailableStrategyNames(projectRoot)).includes(strategyName)) {
|
|
44
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getRuntimeStrategyReleaseOptions = async ({
|
|
49
|
+
userName,
|
|
50
|
+
projectRoot,
|
|
51
|
+
}: {
|
|
52
|
+
userName: string;
|
|
53
|
+
projectRoot: string;
|
|
54
|
+
}) => {
|
|
55
|
+
const strategyNames = await getAvailableStrategyNames(projectRoot);
|
|
56
|
+
const strategies = await Promise.all(
|
|
57
|
+
strategyNames.map(async (strategyName) => ({
|
|
58
|
+
strategyName,
|
|
59
|
+
draft: await getRuntimeStrategyDraft(userName, strategyName),
|
|
60
|
+
releases: await listRuntimeStrategyReleases(userName, strategyName),
|
|
61
|
+
})),
|
|
62
|
+
);
|
|
63
|
+
return { strategyNames, strategies };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const saveRuntimeStrategyReleaseDraftForUser = async ({
|
|
67
|
+
userName,
|
|
68
|
+
projectRoot,
|
|
69
|
+
input,
|
|
70
|
+
}: {
|
|
71
|
+
userName: string;
|
|
72
|
+
projectRoot: string;
|
|
73
|
+
input: Record<string, unknown>;
|
|
74
|
+
}) => {
|
|
75
|
+
const strategyName = String(input.strategyName ?? '').trim();
|
|
76
|
+
await assertKnownStrategy(strategyName, projectRoot);
|
|
77
|
+
return saveRuntimeStrategyDraft({
|
|
78
|
+
userName,
|
|
79
|
+
strategyName,
|
|
80
|
+
config: assertConfig(input.config),
|
|
81
|
+
baseReleaseVersion:
|
|
82
|
+
typeof input.baseReleaseVersion === 'number'
|
|
83
|
+
? input.baseReleaseVersion
|
|
84
|
+
: null,
|
|
85
|
+
updatedBy: userName,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const publishRuntimeStrategyReleaseForUser = async ({
|
|
90
|
+
userName,
|
|
91
|
+
projectRoot,
|
|
92
|
+
strategyName,
|
|
93
|
+
}: {
|
|
94
|
+
userName: string;
|
|
95
|
+
projectRoot: string;
|
|
96
|
+
strategyName: string;
|
|
97
|
+
}) => {
|
|
98
|
+
await assertKnownStrategy(strategyName, projectRoot);
|
|
99
|
+
const draft = await getRuntimeStrategyDraft(userName, strategyName);
|
|
100
|
+
if (!draft) throw new Error(`Draft not found: ${strategyName}`);
|
|
101
|
+
const defaults = (await getStrategyDefaults(strategyName, projectRoot)) ?? {};
|
|
102
|
+
const config: StrategyConfig = { ...defaults, ...draft.config };
|
|
103
|
+
for (const key of OPERATIONAL_KEYS) delete config[key];
|
|
104
|
+
const metadata = await getRuntimeStrategyPackageMetadata({
|
|
105
|
+
strategyName,
|
|
106
|
+
projectRoot,
|
|
107
|
+
});
|
|
108
|
+
const release = await publishRuntimeStrategyRelease({
|
|
109
|
+
userName,
|
|
110
|
+
strategyName,
|
|
111
|
+
config,
|
|
112
|
+
...metadata,
|
|
113
|
+
createdBy: userName,
|
|
114
|
+
});
|
|
115
|
+
await saveRuntimeStrategyDraft({
|
|
116
|
+
userName,
|
|
117
|
+
strategyName,
|
|
118
|
+
config: release.config,
|
|
119
|
+
baseReleaseVersion: release.releaseVersion,
|
|
120
|
+
updatedBy: userName,
|
|
121
|
+
});
|
|
122
|
+
return release;
|
|
123
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { Box,
|
|
4
|
+
import { Box, ClientOnly, Flex } from '@chakra-ui/react';
|
|
5
5
|
import { usePathname } from 'next/navigation';
|
|
6
6
|
import { FiFolder } from 'react-icons/fi';
|
|
7
7
|
import type {
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
} from '#actions/strategies';
|
|
17
17
|
import { RuntimeStrategyCard } from '#components/Strategies/RuntimeStrategyCard';
|
|
18
18
|
import { RuntimeStrategyCardSkeleton } from '#components/Strategies/RuntimeStrategyCardSkeleton';
|
|
19
|
-
import { RuntimeStrategyConfigDrawer } from '#components/Strategies/RuntimeStrategyConfigDrawer';
|
|
20
19
|
import { StrategySnapshotList } from '#components/Strategies/StrategySnapshotList';
|
|
21
20
|
import { BacktestResultsPageClient } from '#components/Backtest/ResultsPageClient';
|
|
22
21
|
import {
|
|
@@ -85,7 +84,6 @@ const RuntimeStrategiesContent = () => {
|
|
|
85
84
|
const [error, setError] = useState('');
|
|
86
85
|
const [runtimeData, setRuntimeData] =
|
|
87
86
|
useState<RuntimeStrategiesResponse | null>(null);
|
|
88
|
-
const [createRuntimeConfigOpen, setCreateRuntimeConfigOpen] = useState(false);
|
|
89
87
|
const [snapshotData, setSnapshotData] =
|
|
90
88
|
useState<StrategyChartsSnapshotResponse | null>(null);
|
|
91
89
|
const isSnapshotMode = mode === 'replay' || mode === 'ai';
|
|
@@ -519,23 +517,8 @@ const RuntimeStrategiesContent = () => {
|
|
|
519
517
|
/>
|
|
520
518
|
) : null}
|
|
521
519
|
</Flex>
|
|
522
|
-
{mode === 'runtime' ? (
|
|
523
|
-
<Button
|
|
524
|
-
ml="auto"
|
|
525
|
-
colorPalette="teal"
|
|
526
|
-
onClick={() => setCreateRuntimeConfigOpen(true)}
|
|
527
|
-
>
|
|
528
|
-
Create
|
|
529
|
-
</Button>
|
|
530
|
-
) : null}
|
|
531
520
|
</Flex>
|
|
532
521
|
|
|
533
|
-
<RuntimeStrategyConfigDrawer
|
|
534
|
-
open={createRuntimeConfigOpen}
|
|
535
|
-
onOpenChange={setCreateRuntimeConfigOpen}
|
|
536
|
-
onSaved={load}
|
|
537
|
-
/>
|
|
538
|
-
|
|
539
522
|
{isSnapshotMode ? (
|
|
540
523
|
<BulkDeleteToolbar
|
|
541
524
|
selectedCount={selectedFilteredCount}
|