@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.
Files changed (28) hide show
  1. package/README.md +2 -0
  2. package/package.json +10 -11
  3. package/src/app/actions/strategies.ts +4 -2
  4. package/src/app/api/ai/route.ts +21 -50
  5. package/src/app/api/strategies/runtime/route.ts +1 -1
  6. package/src/app/api/user/runtime-deployments/[deploymentId]/strategies/[strategyName]/control/route.ts +73 -0
  7. package/src/app/api/user/runtime-deployments/route.ts +55 -8
  8. package/src/app/api/user/runtime-strategy-releases/route.ts +60 -0
  9. package/src/app/components/Strategies/RuntimeStrategyCard.presenter.ts +5 -2
  10. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +65 -7
  11. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +46 -3
  12. package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +1 -2
  13. package/src/app/components/Strategies/StrategyEvidencePopover.tsx +3 -1
  14. package/src/app/lib/connectorCreator.ts +1 -10
  15. package/src/app/lib/runtimeStrategyReleaseService.ts +123 -0
  16. package/src/app/routes/derivatives/DerivativesDashboardView.tsx +900 -0
  17. package/src/app/routes/derivatives/derivativesDashboardConfig.ts +53 -0
  18. package/src/app/routes/derivatives/derivativesDashboardLoader.ts +74 -0
  19. package/src/app/routes/derivatives/page.tsx +18 -1087
  20. package/src/app/routes/derivatives/useDerivativesDashboard.ts +105 -0
  21. package/src/app/routes/strategies/StrategiesPageClient.tsx +5 -20
  22. package/src/app/lib/runtimeDashboard.ts +0 -700
  23. package/src/app/lib/runtimeStrategies.ts +0 -1107
  24. package/src/app/lib/runtimeStrategyContracts.ts +0 -85
  25. package/src/app/lib/runtimeStrategyLineage.ts +0 -264
  26. package/src/app/lib/runtimeTradeReconciliation.ts +0 -113
  27. package/src/app/lib/runtimeTradeSync.ts +0 -280
  28. package/src/app/lib/strategyEvidenceTimeline.ts +0 -298
package/README.md CHANGED
@@ -42,3 +42,5 @@ before starting or building the app with:
42
42
  ```bash
43
43
  NEXT_PUBLIC_TRADEJS_TELEMETRY_DISABLED=1
44
44
  ```
45
+
46
+ Keywords: ai, claude, codex.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "3.0.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,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
- "@langchain/core": "^1.2.3",
53
- "@langchain/openai": "^1.5.5",
54
- "@tradejs/connectors": "^3.0.1",
55
- "@tradejs/core": "^3.0.1",
56
- "@tradejs/indicators": "^3.0.1",
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 { StrategyChartsSnapshotResponse } from '@tradejs/types';
3
- import type { RuntimeStrategiesResponse } from '#app/lib/runtimeStrategyContracts';
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';
@@ -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 { normalizeAiEndpoint } from '@tradejs/core/aiEndpoints';
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 = new Array<BaseMessage>();
78
-
79
- messages.push(
80
- new SystemMessage(
81
- `You are a crypto trader assistant. Reply in ${getAiResponseLanguagePromptName(
82
- responseLanguage,
83
- )}.`,
84
- ),
85
- );
86
-
87
- messages.push(
88
- new SystemMessage(
89
- `Here is the market data for ${filters.symbol}: ${toJson(historyData)}`,
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(new HumanMessage(historyEntry.text));
84
+ messages.push({ role: 'user', content: historyEntry.text });
95
85
  }
96
86
 
97
87
  if (historyEntry.command === 'prompt') {
98
- messages.push(new HumanMessage(historyEntry.text));
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 invokeChatModel(chatMessages, userName);
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 universe = body.universe;
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
- !body.interval ||
80
+ !interval ||
43
81
  !isMarketUniverse(universe) ||
44
82
  !Array.isArray(body.strategies) ||
45
83
  !body.strategies.length ||
46
- body.strategies.some(
47
- (strategy) =>
48
- !String(strategy.strategyName ?? '').trim() ||
49
- !String(strategy.policyProfileId ?? '').trim(),
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(body.interval),
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 { TestThresholdsKey, ThresholdLevel } from '@tradejs/types';
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.enabled ? 'enabled' : 'disabled'}
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
- <Badge colorPalette="gray" variant="outline">
104
- config: {strategy.configId}
105
- </Badge>
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
- : 'No checksum-verified evidence artifact is available. No mutable fallback is used.'}
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
- if (registeredConnector) {
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
  };