@tradejs/app 3.0.1 → 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 -11
- package/src/app/actions/strategies.ts +4 -2
- package/src/app/api/ai/route.ts +21 -50
- package/src/app/api/strategies/runtime/route.ts +1 -1
- 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.presenter.ts +5 -2
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +65 -7
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +46 -3
- package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +1 -2
- package/src/app/components/Strategies/StrategyEvidencePopover.tsx +3 -1
- package/src/app/lib/connectorCreator.ts +1 -10
- package/src/app/lib/runtimeStrategyReleaseService.ts +123 -0
- package/src/app/routes/derivatives/DerivativesDashboardView.tsx +900 -0
- package/src/app/routes/derivatives/derivativesDashboardConfig.ts +53 -0
- package/src/app/routes/derivatives/derivativesDashboardLoader.ts +74 -0
- package/src/app/routes/derivatives/page.tsx +18 -1087
- package/src/app/routes/derivatives/useDerivativesDashboard.ts +105 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +5 -20
- package/src/app/lib/runtimeDashboard.ts +0 -700
- package/src/app/lib/runtimeStrategies.ts +0 -1107
- package/src/app/lib/runtimeStrategyContracts.ts +0 -85
- package/src/app/lib/runtimeStrategyLineage.ts +0 -264
- package/src/app/lib/runtimeTradeReconciliation.ts +0 -113
- package/src/app/lib/runtimeTradeSync.ts +0 -280
- package/src/app/lib/strategyEvidenceTimeline.ts +0 -298
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.
|
|
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,15 +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
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@tradejs/
|
|
55
|
-
"@tradejs/
|
|
56
|
-
"@tradejs/
|
|
57
|
-
"@tradejs/infra": "^3.0.1",
|
|
58
|
-
"@tradejs/node": "^3.0.1",
|
|
59
|
-
"@tradejs/strategies": "^3.0.1",
|
|
60
|
-
"@tradejs/types": "^3.0.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",
|
|
61
60
|
"@types/bcryptjs": "2.4.6",
|
|
62
61
|
"@types/lodash": "4.17.24",
|
|
63
62
|
"@types/node": "24.13.3",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { API } from '@tradejs/core/api';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
RuntimeStrategiesResponse,
|
|
4
|
+
StrategyChartsSnapshotResponse,
|
|
5
|
+
} from '@tradejs/types';
|
|
4
6
|
|
|
5
7
|
const API_PATH = '/api/strategies/runtime';
|
|
6
8
|
const REPLAY_API_PATH = '/api/strategies/replay';
|
package/src/app/api/ai/route.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { ChatOpenAI } from '@langchain/openai';
|
|
3
|
-
import {
|
|
4
|
-
BaseMessage,
|
|
5
|
-
HumanMessage,
|
|
6
|
-
SystemMessage,
|
|
7
|
-
} from '@langchain/core/messages';
|
|
8
2
|
import { TTL_1M } from '@tradejs/core/constants';
|
|
9
3
|
import { toJson } from '@tradejs/core/data';
|
|
10
4
|
import {
|
|
11
5
|
getAiResponseLanguagePromptName,
|
|
12
6
|
normalizeAiResponseLanguage,
|
|
13
7
|
} from '@tradejs/core/aiLanguages';
|
|
14
|
-
import {
|
|
15
|
-
import { normalizeAiModel } from '@tradejs/core/aiModels';
|
|
16
|
-
import { DEFAULT_AI_MODEL, getOpenRouterModelKwargs } from '@tradejs/node/ai';
|
|
8
|
+
import { invokeAiChat, type AiChatMessage } from '@tradejs/node/ai';
|
|
17
9
|
import {
|
|
18
10
|
AIChatHistory,
|
|
19
11
|
AIChatMessage,
|
|
@@ -74,56 +66,31 @@ const buildMessages = (
|
|
|
74
66
|
historyData: unknown,
|
|
75
67
|
responseLanguage: string,
|
|
76
68
|
) => {
|
|
77
|
-
const messages =
|
|
78
|
-
|
|
79
|
-
messages.push(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
),
|
|
91
|
-
);
|
|
69
|
+
const messages: AiChatMessage[] = [];
|
|
70
|
+
|
|
71
|
+
messages.push({
|
|
72
|
+
role: 'system',
|
|
73
|
+
content: `You are a crypto trader assistant. Reply in ${getAiResponseLanguagePromptName(
|
|
74
|
+
responseLanguage,
|
|
75
|
+
)}.`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
messages.push({
|
|
79
|
+
role: 'system',
|
|
80
|
+
content: `Here is the market data for ${filters.symbol}: ${toJson(historyData)}`,
|
|
81
|
+
});
|
|
92
82
|
|
|
93
83
|
if (historyEntry.command === '/line') {
|
|
94
|
-
messages.push(
|
|
84
|
+
messages.push({ role: 'user', content: historyEntry.text });
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
if (historyEntry.command === 'prompt') {
|
|
98
|
-
messages.push(
|
|
88
|
+
messages.push({ role: 'user', content: historyEntry.text });
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
return messages;
|
|
102
92
|
};
|
|
103
93
|
|
|
104
|
-
const invokeChatModel = async (messages: BaseMessage[], userName: string) => {
|
|
105
|
-
const settings = await getUserSettings(userName);
|
|
106
|
-
const endpoint = normalizeAiEndpoint(settings.AI_API_ENDPOINT);
|
|
107
|
-
if (!settings.AI_API_KEY || !endpoint) {
|
|
108
|
-
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const modelKwargs = getOpenRouterModelKwargs(endpoint);
|
|
112
|
-
|
|
113
|
-
const model = new ChatOpenAI({
|
|
114
|
-
temperature: 0.7,
|
|
115
|
-
modelName:
|
|
116
|
-
normalizeAiModel(settings.AI_MODEL, endpoint) || DEFAULT_AI_MODEL,
|
|
117
|
-
apiKey: settings.AI_API_KEY,
|
|
118
|
-
...(Object.keys(modelKwargs).length ? { modelKwargs } : {}),
|
|
119
|
-
configuration: {
|
|
120
|
-
baseURL: endpoint,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return model.invoke(messages);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
94
|
export const GET = async (request: NextRequest) => {
|
|
128
95
|
try {
|
|
129
96
|
const userName = await getCurrentUserName();
|
|
@@ -210,7 +177,11 @@ export const POST = async (request: NextRequest) => {
|
|
|
210
177
|
normalizeAiResponseLanguage(settings.AI_RESPONSE_LANGUAGE),
|
|
211
178
|
);
|
|
212
179
|
|
|
213
|
-
const response = await
|
|
180
|
+
const response = await invokeAiChat({
|
|
181
|
+
messages: chatMessages,
|
|
182
|
+
userName,
|
|
183
|
+
temperature: 0.7,
|
|
184
|
+
});
|
|
214
185
|
|
|
215
186
|
const responseMessage: AIChatMessage = {
|
|
216
187
|
from: 'ai',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { logger } from '@tradejs/infra/logger';
|
|
3
|
+
import { loadRuntimeDashboard } from '@tradejs/node/runtimeDashboard';
|
|
3
4
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
4
|
-
import { loadRuntimeDashboard } from '#app/lib/runtimeDashboard';
|
|
5
5
|
|
|
6
6
|
export const dynamic = 'force-dynamic';
|
|
7
7
|
|
|
@@ -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
|
+
};
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
getFormatted,
|
|
4
4
|
type AdvancedTradeInput,
|
|
5
5
|
} from '@tradejs/core/backtest';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
RuntimeStrategyView,
|
|
8
|
+
TestThresholdsKey,
|
|
9
|
+
ThresholdLevel,
|
|
10
|
+
} from '@tradejs/types';
|
|
7
11
|
import {
|
|
8
12
|
formatCompactNumber,
|
|
9
13
|
formatFee,
|
|
@@ -16,7 +20,6 @@ import {
|
|
|
16
20
|
type OrdersDrawerOrder,
|
|
17
21
|
type OrdersDrawerSummaryItem,
|
|
18
22
|
} from '#components/Shared/OrdersDrawer';
|
|
19
|
-
import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
|
|
20
23
|
import {
|
|
21
24
|
buildStrategyPerformanceViewModel,
|
|
22
25
|
calculateMaxLossStreak,
|
|
@@ -13,12 +13,11 @@ import {
|
|
|
13
13
|
Text,
|
|
14
14
|
} from '@chakra-ui/react';
|
|
15
15
|
import { getFormatted } from '@tradejs/core/backtest';
|
|
16
|
-
import type { TestThresholdsKey } from '@tradejs/types';
|
|
16
|
+
import type { RuntimeStrategyView, TestThresholdsKey } from '@tradejs/types';
|
|
17
17
|
import {
|
|
18
18
|
formatDateTime,
|
|
19
19
|
OrdersDrawerPanel,
|
|
20
20
|
} from '#components/Shared/OrdersDrawer';
|
|
21
|
-
import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
|
|
22
21
|
import { RuntimeStrategyChart } from './RuntimeStrategyChart';
|
|
23
22
|
import { RuntimeStrategyConfigDrawer } from './RuntimeStrategyConfigDrawer';
|
|
24
23
|
import { RuntimeStrategyStatsDrawer } from './RuntimeStrategyStatsDrawer';
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
getColorByLevel,
|
|
28
27
|
RUNTIME_ORDER_ROW_HEIGHT,
|
|
29
28
|
} from './RuntimeStrategyCard.presenter';
|
|
29
|
+
import { toaster } from '#ui';
|
|
30
30
|
|
|
31
31
|
const StatItem = ({
|
|
32
32
|
stat,
|
|
@@ -65,11 +65,42 @@ export const RuntimeStrategyCard = ({
|
|
|
65
65
|
const [configOpen, setConfigOpen] = useState(false);
|
|
66
66
|
const [ordersOpen, setOrdersOpen] = useState(false);
|
|
67
67
|
const [statsOpen, setStatsOpen] = useState(false);
|
|
68
|
+
const [controlSaving, setControlSaving] = useState(false);
|
|
68
69
|
const viewModel = useMemo(
|
|
69
70
|
() => buildRuntimeStrategyCardViewModel(strategy),
|
|
70
71
|
[strategy],
|
|
71
72
|
);
|
|
72
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
|
+
};
|
|
73
104
|
|
|
74
105
|
return (
|
|
75
106
|
<Box
|
|
@@ -92,7 +123,11 @@ export const RuntimeStrategyCard = ({
|
|
|
92
123
|
fontFamily="mono"
|
|
93
124
|
letterSpacing="0"
|
|
94
125
|
>
|
|
95
|
-
{strategy.
|
|
126
|
+
{strategy.controlState === 'entries_paused'
|
|
127
|
+
? 'entries paused'
|
|
128
|
+
: strategy.enabled
|
|
129
|
+
? 'enabled'
|
|
130
|
+
: 'disabled'}
|
|
96
131
|
</Badge>
|
|
97
132
|
<Badge colorPalette="blue" variant="outline">
|
|
98
133
|
{strategy.universe}
|
|
@@ -100,9 +135,15 @@ export const RuntimeStrategyCard = ({
|
|
|
100
135
|
<Badge colorPalette="cyan" variant="outline">
|
|
101
136
|
TF: {strategy.interval}m
|
|
102
137
|
</Badge>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
)}
|
|
106
147
|
{strategy.accountId ? (
|
|
107
148
|
<Badge colorPalette="purple" variant="outline">
|
|
108
149
|
account: {strategy.accountLabel ?? strategy.accountId}
|
|
@@ -172,7 +213,24 @@ export const RuntimeStrategyCard = ({
|
|
|
172
213
|
<Menu.Content minW="160px">
|
|
173
214
|
{strategy.connected ? (
|
|
174
215
|
<Menu.Item value="edit" onClick={() => setConfigOpen(true)}>
|
|
175
|
-
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'}
|
|
176
234
|
</Menu.Item>
|
|
177
235
|
) : null}
|
|
178
236
|
<Menu.Item value="orders" onClick={() => setOrdersOpen(true)}>
|
|
@@ -21,8 +21,7 @@ import {
|
|
|
21
21
|
Text,
|
|
22
22
|
Textarea,
|
|
23
23
|
} from '@chakra-ui/react';
|
|
24
|
-
import type { MarketUniverse } from '@tradejs/types';
|
|
25
|
-
import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
|
|
24
|
+
import type { MarketUniverse, RuntimeStrategyView } from '@tradejs/types';
|
|
26
25
|
import {
|
|
27
26
|
DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS,
|
|
28
27
|
mergeRuntimeStrategyManagedParameters,
|
|
@@ -161,7 +160,7 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
161
160
|
setMlThreshold(String(managed.mlThreshold));
|
|
162
161
|
setParameters('{}');
|
|
163
162
|
}
|
|
164
|
-
void loadOptions();
|
|
163
|
+
if (!strategy?.releaseVersion) void loadOptions();
|
|
165
164
|
}, [loadOptions, open, strategy]);
|
|
166
165
|
|
|
167
166
|
const compatibleAccounts = useMemo(
|
|
@@ -252,6 +251,50 @@ export const RuntimeStrategyConfigDrawer = ({
|
|
|
252
251
|
}
|
|
253
252
|
};
|
|
254
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
|
+
|
|
255
298
|
return (
|
|
256
299
|
<Drawer.Root
|
|
257
300
|
size="lg"
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SimpleGrid,
|
|
11
11
|
Text,
|
|
12
12
|
} from '@chakra-ui/react';
|
|
13
|
-
import type { ThresholdLevel } from '@tradejs/types';
|
|
13
|
+
import type { RuntimeStrategyView, ThresholdLevel } from '@tradejs/types';
|
|
14
14
|
import {
|
|
15
15
|
formatInteger,
|
|
16
16
|
formatPercent,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
getPnlColor,
|
|
19
19
|
type OrdersDrawerSummaryItem,
|
|
20
20
|
} from '#components/Shared/OrdersDrawer';
|
|
21
|
-
import type { RuntimeStrategyView } from '#app/lib/runtimeStrategyContracts';
|
|
22
21
|
import { buildQuarterlyMonthlyStats } from '#app/lib/strategyPerformance';
|
|
23
22
|
import { AdvancedMetricsPanel } from './AdvancedMetricsPanel';
|
|
24
23
|
import {
|
|
@@ -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
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getConnectorCreatorByProvider as getBuiltinConnectorCreatorByProvider } from '@tradejs/connectors';
|
|
2
1
|
import { resolveTradingAccount } from '@tradejs/infra/tradingAccounts';
|
|
3
2
|
import { getConnectorCreatorByProvider as getRegisteredConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
4
3
|
import { ConnectorCreator, MarketUniverse } from '@tradejs/types';
|
|
@@ -35,13 +34,5 @@ export const resolveConnectorCreatorByProvider = async (
|
|
|
35
34
|
projectRoot,
|
|
36
35
|
));
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
return registeredConnector as ConnectorCreator;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
getBuiltinConnectorCreatorByProvider(provider) ||
|
|
44
|
-
getBuiltinConnectorCreatorByProvider(fallbackProvider) ||
|
|
45
|
-
null
|
|
46
|
-
);
|
|
37
|
+
return (registeredConnector as ConnectorCreator | null) ?? null;
|
|
47
38
|
};
|