@tradejs/app 1.0.9 → 1.0.11

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 (125) hide show
  1. package/README.md +16 -3
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/install/route.ts +51 -0
  20. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  21. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  22. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  23. package/src/app/api/scanner/[provider]/route.ts +30 -12
  24. package/src/app/api/scanner/route.ts +3 -3
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  26. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  27. package/src/app/api/strategies/ai/route.ts +139 -0
  28. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  29. package/src/app/api/strategies/replay/route.ts +44 -0
  30. package/src/app/api/strategies/runtime/route.ts +831 -0
  31. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  32. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  33. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  34. package/src/app/api/user/settings/route.ts +18 -1
  35. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  36. package/src/app/api/user/trading-accounts/route.ts +134 -0
  37. package/src/app/auth.ts +1 -19
  38. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  39. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  40. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  41. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  42. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  43. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  44. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  45. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  46. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  47. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  48. package/src/app/components/Backtest/TestCard/Root/index.tsx +18 -4
  49. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  50. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  51. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  52. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  53. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  56. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  63. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  64. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  65. package/src/app/components/Dashboard/KlineChart/index.tsx +21 -7
  66. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  67. package/src/app/components/Shared/AppShell.tsx +2 -2
  68. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  69. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  70. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  71. package/src/app/components/Shared/Filters/Backtest/index.tsx +51 -51
  72. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  73. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  74. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  75. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  76. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  77. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  78. package/src/app/components/Shared/Filters/index.ts +2 -0
  79. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  80. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  81. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  82. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  83. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  84. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  85. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  86. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  87. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  88. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  89. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  90. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  91. package/src/app/components/UI/Segment/index.tsx +8 -1
  92. package/src/app/components/UI/Select/index.tsx +24 -13
  93. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  94. package/src/app/layout.tsx +1 -1
  95. package/src/app/lib/backtestJobs.ts +925 -0
  96. package/src/app/lib/cardPageLayout.ts +1 -0
  97. package/src/app/lib/connectorCreator.ts +28 -0
  98. package/src/app/lib/currentUser.ts +1 -1
  99. package/src/app/lib/errorMessage.ts +34 -0
  100. package/src/app/lib/installation.ts +83 -0
  101. package/src/app/lib/marketDefaults.ts +12 -0
  102. package/src/app/lib/marketRoutes.ts +58 -0
  103. package/src/app/lib/runtimeStrategies.ts +1376 -0
  104. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  105. package/src/app/provider.tsx +1 -1
  106. package/src/app/routes/backtest/page.tsx +1100 -302
  107. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +76 -35
  108. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  109. package/src/app/routes/dashboard/page.tsx +15 -3
  110. package/src/app/routes/derivatives/page.tsx +1222 -165
  111. package/src/app/routes/install/page.tsx +175 -0
  112. package/src/app/routes/signin/page.tsx +7 -0
  113. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  114. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  115. package/src/app/routes/strategies/page.tsx +7 -0
  116. package/src/app/store/ai.ts +1 -1
  117. package/src/app/store/data.ts +73 -17
  118. package/src/app/store/filters.ts +1 -0
  119. package/src/app/store/marketKlineStream.ts +87 -0
  120. package/src/app/store/tests.ts +83 -45
  121. package/src/app/store/tickers.ts +59 -22
  122. package/src/proxy.ts +4 -0
  123. package/tsconfig.json +17 -12
  124. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  125. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -0,0 +1,593 @@
1
+ 'use client';
2
+
3
+ import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { Box, Button, ClientOnly, Flex } from '@chakra-ui/react';
5
+ import { usePathname } from 'next/navigation';
6
+ import { FiFolder } from 'react-icons/fi';
7
+ import type { StrategyChartsSnapshotResponse } from '@tradejs/types';
8
+ import {
9
+ deleteStrategyCard,
10
+ getAiStrategies,
11
+ getReplayStrategies,
12
+ getRuntimeStrategies,
13
+ } from '#actions/strategies';
14
+ import { RuntimeStrategyCard } from '#components/Strategies/RuntimeStrategyCard';
15
+ import { RuntimeStrategyCardSkeleton } from '#components/Strategies/RuntimeStrategyCardSkeleton';
16
+ import { RuntimeStrategyConfigDrawer } from '#components/Strategies/RuntimeStrategyConfigDrawer';
17
+ import { StrategySnapshotList } from '#components/Strategies/StrategySnapshotList';
18
+ import { BacktestResultsPageClient } from '#components/Backtest/ResultsPageClient';
19
+ import {
20
+ BulkDeleteToolbar,
21
+ useBulkSelection,
22
+ } from '#components/Shared/BulkSelection';
23
+ import type { RuntimeStrategiesResponse } from '#app/lib/runtimeStrategies';
24
+ import { CARD_PAGE_CONTENT_MAX_WIDTH } from '#app/lib/cardPageLayout';
25
+ import { EmptyState, Segment, Select, toaster } from '#ui';
26
+
27
+ const ALL_STRATEGIES = '__all__';
28
+ const ALL_RUNTIME_SCOPES = '__all_runtime_scopes__';
29
+ type StrategyMode = 'runtime' | 'replay' | 'ai' | 'backtest';
30
+ type RuntimeStatusFilter = 'all' | 'enabled' | 'disabled';
31
+
32
+ const MODE_ITEMS = [
33
+ { label: 'Runtime', value: 'runtime' },
34
+ { label: 'Replay', value: 'replay' },
35
+ { label: 'AI', value: 'ai' },
36
+ { label: 'Backtest', value: 'backtest' },
37
+ ];
38
+ const RUNTIME_STATUS_ITEMS = [
39
+ { label: 'All', value: 'all' },
40
+ { label: 'Enabled', value: 'enabled' },
41
+ { label: 'Disabled', value: 'disabled' },
42
+ ];
43
+ const HOURS_OPTIONS = [
44
+ { label: 'Last 24h', value: '24' },
45
+ { label: 'Last 7d', value: '168' },
46
+ { label: 'Last 30d', value: '720' },
47
+ { label: 'Last 60d', value: '1440' },
48
+ { label: 'Last 90d', value: '2160' },
49
+ ];
50
+
51
+ const normalizeMode = (value: string | null | undefined): StrategyMode =>
52
+ value === 'replay' || value === 'ai' || value === 'backtest'
53
+ ? value
54
+ : 'runtime';
55
+
56
+ interface PendingSnapshotDelete {
57
+ mode: 'replay' | 'ai';
58
+ label: string;
59
+ labelLower: string;
60
+ cardIds: string[];
61
+ }
62
+
63
+ const modeFromPathname = (pathname: string | null) => {
64
+ const segment = pathname?.split('/').filter(Boolean).at(-1);
65
+ return normalizeMode(segment);
66
+ };
67
+
68
+ const RuntimeStrategiesContent = () => {
69
+ const pathname = usePathname();
70
+ const routeMode = modeFromPathname(pathname);
71
+ const [mode, setMode] = useState<StrategyMode>(routeMode);
72
+ const [hours, setHours] = useState('168');
73
+ const [selectedStrategy, setSelectedStrategy] = useState(ALL_STRATEGIES);
74
+ const [runtimeStatusFilter, setRuntimeStatusFilter] =
75
+ useState<RuntimeStatusFilter>('all');
76
+ const [runtimeUniverse, setRuntimeUniverse] = useState(ALL_RUNTIME_SCOPES);
77
+ const [isDeleteSelectedOpen, setIsDeleteSelectedOpen] = useState(false);
78
+ const [isDeletingSelected, setIsDeletingSelected] = useState(false);
79
+ const [pendingSnapshotDelete, setPendingSnapshotDelete] =
80
+ useState<PendingSnapshotDelete | null>(null);
81
+ const [loading, setLoading] = useState(false);
82
+ const [fulfilled, setFulfilled] = useState(false);
83
+ const [error, setError] = useState('');
84
+ const [runtimeData, setRuntimeData] =
85
+ useState<RuntimeStrategiesResponse | null>(null);
86
+ const [createRuntimeConfigOpen, setCreateRuntimeConfigOpen] = useState(false);
87
+ const [snapshotData, setSnapshotData] =
88
+ useState<StrategyChartsSnapshotResponse | null>(null);
89
+ const isSnapshotMode = mode === 'replay' || mode === 'ai';
90
+ const snapshotModeLabel = mode === 'replay' ? 'Replay' : 'AI';
91
+ const snapshotModeLabelLower = snapshotModeLabel.toLowerCase();
92
+
93
+ useEffect(() => {
94
+ setMode(routeMode);
95
+ }, [routeMode]);
96
+
97
+ useEffect(() => {
98
+ const handlePopState = () => {
99
+ setMode(modeFromPathname(window.location.pathname));
100
+ setSelectedStrategy(ALL_STRATEGIES);
101
+ setRuntimeStatusFilter('all');
102
+ };
103
+
104
+ window.addEventListener('popstate', handlePopState);
105
+ return () => window.removeEventListener('popstate', handlePopState);
106
+ }, []);
107
+
108
+ const updateMode = useCallback((value: string | null) => {
109
+ const nextMode = normalizeMode(value);
110
+
111
+ setMode(nextMode);
112
+ setSelectedStrategy(ALL_STRATEGIES);
113
+ setRuntimeStatusFilter('all');
114
+
115
+ window.history.pushState(null, '', `/routes/strategies/${nextMode}`);
116
+ }, []);
117
+
118
+ const load = useCallback(async () => {
119
+ setLoading(true);
120
+ setError('');
121
+
122
+ try {
123
+ if (mode === 'runtime') {
124
+ const response = await getRuntimeStrategies({ hours: Number(hours) });
125
+ setRuntimeData(response);
126
+ setSnapshotData(null);
127
+
128
+ const exchangeErrors = response.dataSources?.exchangeErrors ?? [];
129
+ if (exchangeErrors.length > 0) {
130
+ toaster.error({
131
+ title: 'Exchange fallback unavailable',
132
+ description: exchangeErrors.slice(0, 2).join('; '),
133
+ });
134
+ }
135
+ } else if (mode === 'replay') {
136
+ const response = await getReplayStrategies();
137
+ setSnapshotData(response);
138
+ setRuntimeData(null);
139
+ } else if (mode === 'ai') {
140
+ const response = await getAiStrategies();
141
+ setSnapshotData(response);
142
+ setRuntimeData(null);
143
+ } else {
144
+ setRuntimeData(null);
145
+ setSnapshotData(null);
146
+ }
147
+ setFulfilled(true);
148
+ } catch (err) {
149
+ const message =
150
+ (err as Error)?.message ||
151
+ `Failed to load ${mode === 'runtime' ? 'runtime strategies' : `${mode} strategy charts`}`;
152
+ setError(message);
153
+ toaster.error({
154
+ title: 'Failed to load strategies',
155
+ description: message,
156
+ });
157
+ setFulfilled(true);
158
+ } finally {
159
+ setLoading(false);
160
+ }
161
+ }, [hours, mode]);
162
+
163
+ useEffect(() => {
164
+ void load();
165
+ }, [load]);
166
+
167
+ const strategyItems = useMemo(() => {
168
+ const names = [
169
+ ...new Set(
170
+ mode === 'runtime'
171
+ ? runtimeData?.strategies.map((strategy) => strategy.strategyName) ??
172
+ []
173
+ : snapshotData?.strategies.map((strategy) => strategy.strategyName) ??
174
+ [],
175
+ ),
176
+ ].sort((left, right) => left.localeCompare(right));
177
+
178
+ return [
179
+ { label: 'All strategies', value: ALL_STRATEGIES },
180
+ ...names.map((name) => ({ label: name, value: name })),
181
+ ];
182
+ }, [mode, runtimeData?.strategies, snapshotData?.strategies]);
183
+
184
+ useEffect(() => {
185
+ if (!strategyItems.some((item) => item.value === selectedStrategy)) {
186
+ setSelectedStrategy(ALL_STRATEGIES);
187
+ }
188
+ }, [selectedStrategy, strategyItems]);
189
+
190
+ const filteredRuntimeStrategies = useMemo(() => {
191
+ const strategies = runtimeData?.strategies ?? [];
192
+
193
+ return strategies.filter((strategy) => {
194
+ if (
195
+ selectedStrategy !== ALL_STRATEGIES &&
196
+ strategy.strategyName !== selectedStrategy
197
+ ) {
198
+ return false;
199
+ }
200
+ if (
201
+ runtimeUniverse !== ALL_RUNTIME_SCOPES &&
202
+ strategy.universe !== runtimeUniverse
203
+ ) {
204
+ return false;
205
+ }
206
+ if (runtimeStatusFilter === 'enabled') {
207
+ return strategy.enabled;
208
+ }
209
+
210
+ if (runtimeStatusFilter === 'disabled') {
211
+ return !strategy.enabled;
212
+ }
213
+
214
+ return true;
215
+ });
216
+ }, [
217
+ runtimeData?.strategies,
218
+ runtimeStatusFilter,
219
+ runtimeUniverse,
220
+ selectedStrategy,
221
+ ]);
222
+
223
+ const runtimeUniverseItems = useMemo(() => {
224
+ const strategies = runtimeData?.strategies ?? [];
225
+ return [
226
+ { label: 'All universes', value: ALL_RUNTIME_SCOPES },
227
+ ...[...new Set(strategies.map(({ universe }) => universe))]
228
+ .sort()
229
+ .map((value) => ({ label: value, value })),
230
+ ];
231
+ }, [runtimeData?.strategies]);
232
+
233
+ const filteredSnapshotStrategies = useMemo(() => {
234
+ const strategies = snapshotData?.strategies ?? [];
235
+
236
+ if (selectedStrategy === ALL_STRATEGIES) {
237
+ return strategies;
238
+ }
239
+
240
+ return strategies.filter(
241
+ (strategy) => strategy.strategyName === selectedStrategy,
242
+ );
243
+ }, [snapshotData?.strategies, selectedStrategy]);
244
+
245
+ const filteredSnapshotCardIds = useMemo(
246
+ () => filteredSnapshotStrategies.map((strategy) => strategy.cardId),
247
+ [filteredSnapshotStrategies],
248
+ );
249
+ const allSnapshotCardIds = useMemo(
250
+ () => snapshotData?.strategies.map((strategy) => strategy.cardId) ?? [],
251
+ [snapshotData?.strategies],
252
+ );
253
+ const {
254
+ selectedIdSet: selectedSnapshotCardIdSet,
255
+ selectedScopedIds: selectedFilteredSnapshotCardIds,
256
+ selectedScopedCount: selectedFilteredCount,
257
+ hasScopedSelection: hasSelectedInFilter,
258
+ checkboxState: selectionCheckboxState,
259
+ toggleSelection: handleToggleSnapshotSelection,
260
+ setScopeSelected: handleSelectAllFilteredSnapshots,
261
+ removeSelection: removeDeletedSnapshotSelection,
262
+ clearSelection: clearSnapshotSelection,
263
+ } = useBulkSelection({
264
+ scopeIds: filteredSnapshotCardIds,
265
+ validIds: allSnapshotCardIds,
266
+ enabled: isSnapshotMode,
267
+ });
268
+
269
+ useEffect(() => {
270
+ clearSnapshotSelection();
271
+ setPendingSnapshotDelete(null);
272
+ setIsDeleteSelectedOpen(false);
273
+ }, [clearSnapshotSelection, mode]);
274
+
275
+ const handleSnapshotDeleted = useCallback(
276
+ (cardId: string) => {
277
+ setSnapshotData((prev) => {
278
+ if (!prev) {
279
+ return prev;
280
+ }
281
+
282
+ return {
283
+ ...prev,
284
+ strategies: prev.strategies.filter(
285
+ (strategy) => strategy.cardId !== cardId,
286
+ ),
287
+ };
288
+ });
289
+ removeDeletedSnapshotSelection([cardId]);
290
+ },
291
+ [removeDeletedSnapshotSelection],
292
+ );
293
+
294
+ const handleOpenDeleteSelectedSnapshots = () => {
295
+ if (!isSnapshotMode || selectedFilteredSnapshotCardIds.length === 0) {
296
+ setIsDeleteSelectedOpen(false);
297
+ setPendingSnapshotDelete(null);
298
+ return;
299
+ }
300
+
301
+ setPendingSnapshotDelete({
302
+ mode,
303
+ label: snapshotModeLabel,
304
+ labelLower: snapshotModeLabelLower,
305
+ cardIds: selectedFilteredSnapshotCardIds,
306
+ });
307
+ setIsDeleteSelectedOpen(true);
308
+ };
309
+
310
+ const handleDeleteSelectedSnapshots = async () => {
311
+ const pendingDelete = pendingSnapshotDelete;
312
+
313
+ if (
314
+ !pendingDelete ||
315
+ pendingDelete.cardIds.length === 0 ||
316
+ isDeletingSelected
317
+ ) {
318
+ setIsDeleteSelectedOpen(false);
319
+ setPendingSnapshotDelete(null);
320
+ return;
321
+ }
322
+
323
+ setIsDeletingSelected(true);
324
+
325
+ try {
326
+ const results = await Promise.allSettled(
327
+ pendingDelete.cardIds.map(async (cardId) => {
328
+ const deleted = await deleteStrategyCard(pendingDelete.mode, cardId);
329
+ if (!deleted) {
330
+ throw new Error(`Delete failed for ${cardId}`);
331
+ }
332
+
333
+ return cardId;
334
+ }),
335
+ );
336
+
337
+ const deletedIds = results
338
+ .filter((item) => item.status === 'fulfilled')
339
+ .map((item) => item.value);
340
+ const successCount = deletedIds.length;
341
+ const failedCount = results.length - successCount;
342
+
343
+ if (successCount > 0) {
344
+ const deletedSet = new Set(deletedIds);
345
+ setSnapshotData((prev) =>
346
+ prev
347
+ ? {
348
+ ...prev,
349
+ strategies: prev.strategies.filter(
350
+ (strategy) => !deletedSet.has(strategy.cardId),
351
+ ),
352
+ }
353
+ : prev,
354
+ );
355
+ removeDeletedSnapshotSelection(deletedIds);
356
+ }
357
+
358
+ if (failedCount === 0) {
359
+ toaster.success({
360
+ title: `${pendingDelete.label} cards deleted`,
361
+ description: `Deleted: ${successCount}`,
362
+ });
363
+ } else {
364
+ toaster.error({
365
+ title: 'Bulk delete finished with errors',
366
+ description: `Deleted: ${successCount} of ${pendingDelete.cardIds.length}`,
367
+ });
368
+ }
369
+ } catch {
370
+ toaster.error({
371
+ title: 'Delete failed',
372
+ description: `Failed to delete selected ${pendingDelete.labelLower} cards.`,
373
+ });
374
+ } finally {
375
+ setIsDeletingSelected(false);
376
+ setIsDeleteSelectedOpen(false);
377
+ setPendingSnapshotDelete(null);
378
+ }
379
+ };
380
+
381
+ const handleBulkDeleteDialogOpenChange = (open: boolean) => {
382
+ setIsDeleteSelectedOpen(open);
383
+
384
+ if (!open && !isDeletingSelected) {
385
+ setPendingSnapshotDelete(null);
386
+ }
387
+ };
388
+
389
+ const noData =
390
+ fulfilled &&
391
+ !loading &&
392
+ !error &&
393
+ (mode === 'runtime'
394
+ ? filteredRuntimeStrategies.length === 0
395
+ : filteredSnapshotStrategies.length === 0);
396
+
397
+ const emptyTitle =
398
+ mode === 'runtime'
399
+ ? 'No runtime strategies found'
400
+ : mode === 'replay'
401
+ ? 'No replay charts found'
402
+ : 'No AI charts found';
403
+
404
+ const emptyDescription =
405
+ mode === 'runtime'
406
+ ? 'No connected strategies or runtime trades were found for the selected window.'
407
+ : mode === 'replay'
408
+ ? 'Run `yarn replay --chart` to save replay strategy cards for this page.'
409
+ : 'Run `yarn ai-train --chart` to save strategy-wide AI cards for this page.';
410
+
411
+ const deleteSelectedSnapshotCount =
412
+ pendingSnapshotDelete?.cardIds.length ?? selectedFilteredCount;
413
+ const deleteSnapshotLabel = pendingSnapshotDelete?.label ?? snapshotModeLabel;
414
+ const snapshotEmptyText =
415
+ mode === 'replay'
416
+ ? 'No replay trades for the selected run.'
417
+ : 'No approved trades for this quality bucket.';
418
+ const modeSegment = (
419
+ <Segment
420
+ defaultValue="runtime"
421
+ value={mode}
422
+ onChange={updateMode}
423
+ items={MODE_ITEMS}
424
+ />
425
+ );
426
+
427
+ if (mode === 'backtest') {
428
+ return <BacktestResultsPageClient toolbarPrefix={modeSegment} />;
429
+ }
430
+
431
+ return (
432
+ <ClientOnly>
433
+ <Box minH="100vh" bg="gray.900">
434
+ <Box
435
+ as="main"
436
+ minH="100vh"
437
+ minW="1200px"
438
+ pl={2}
439
+ bg="gray.900"
440
+ display="flex"
441
+ flexDirection="column"
442
+ alignItems="flex-start"
443
+ >
444
+ <Box
445
+ minH="100vh"
446
+ w="full"
447
+ maxW={CARD_PAGE_CONTENT_MAX_WIDTH}
448
+ display="flex"
449
+ flex="1"
450
+ flexDirection="column"
451
+ alignItems="flex-start"
452
+ >
453
+ <Flex
454
+ mb={2}
455
+ mt={2}
456
+ gap={8}
457
+ flexDirection="row"
458
+ alignItems="center"
459
+ width="full"
460
+ >
461
+ <Flex gap={3} alignItems="center">
462
+ {modeSegment}
463
+ <Select
464
+ placeholder="Strategy"
465
+ value={[selectedStrategy]}
466
+ defaultValue={[selectedStrategy]}
467
+ onChange={(value) =>
468
+ setSelectedStrategy(value[0] || ALL_STRATEGIES)
469
+ }
470
+ items={strategyItems}
471
+ width="220px"
472
+ />
473
+ {mode === 'runtime' ? (
474
+ <Select
475
+ placeholder="Window"
476
+ value={[hours]}
477
+ defaultValue={[hours]}
478
+ onChange={(value) => setHours(value[0] || '168')}
479
+ items={HOURS_OPTIONS}
480
+ width="180px"
481
+ />
482
+ ) : null}
483
+ {mode === 'runtime' ? (
484
+ <Segment
485
+ defaultValue="all"
486
+ value={runtimeStatusFilter}
487
+ onChange={(value) =>
488
+ setRuntimeStatusFilter(
489
+ value === 'enabled' || value === 'disabled'
490
+ ? value
491
+ : 'all',
492
+ )
493
+ }
494
+ items={RUNTIME_STATUS_ITEMS}
495
+ />
496
+ ) : null}
497
+ {mode === 'runtime' ? (
498
+ <Select
499
+ value={[runtimeUniverse]}
500
+ defaultValue={[runtimeUniverse]}
501
+ onChange={(value) =>
502
+ setRuntimeUniverse(value[0] || ALL_RUNTIME_SCOPES)
503
+ }
504
+ items={runtimeUniverseItems}
505
+ width="160px"
506
+ />
507
+ ) : null}
508
+ </Flex>
509
+ {mode === 'runtime' ? (
510
+ <Button
511
+ ml="auto"
512
+ colorPalette="teal"
513
+ onClick={() => setCreateRuntimeConfigOpen(true)}
514
+ >
515
+ Create
516
+ </Button>
517
+ ) : null}
518
+ </Flex>
519
+
520
+ <RuntimeStrategyConfigDrawer
521
+ open={createRuntimeConfigOpen}
522
+ onOpenChange={setCreateRuntimeConfigOpen}
523
+ onSaved={load}
524
+ />
525
+
526
+ {isSnapshotMode ? (
527
+ <BulkDeleteToolbar
528
+ selectedCount={selectedFilteredCount}
529
+ checkboxState={selectionCheckboxState}
530
+ hasSelection={hasSelectedInFilter}
531
+ isDeleting={isDeletingSelected}
532
+ dialogOpen={isDeleteSelectedOpen}
533
+ deleteTitle={`Delete selected ${deleteSnapshotLabel} cards`}
534
+ deleteDescription={`Delete selected ${deleteSnapshotLabel} cards (${deleteSelectedSnapshotCount})?`}
535
+ onDialogOpenChange={handleBulkDeleteDialogOpenChange}
536
+ onToggleAll={handleSelectAllFilteredSnapshots}
537
+ onRequestDelete={handleOpenDeleteSelectedSnapshots}
538
+ onConfirmDelete={handleDeleteSelectedSnapshots}
539
+ />
540
+ ) : null}
541
+
542
+ <Box flex="1" h="full" w="full">
543
+ {loading ? (
544
+ <>
545
+ <RuntimeStrategyCardSkeleton />
546
+ <RuntimeStrategyCardSkeleton />
547
+ </>
548
+ ) : null}
549
+
550
+ {noData ? (
551
+ <EmptyState
552
+ icon={FiFolder}
553
+ title={emptyTitle}
554
+ description={emptyDescription}
555
+ />
556
+ ) : null}
557
+
558
+ {!loading &&
559
+ mode === 'runtime' &&
560
+ filteredRuntimeStrategies.map((strategy) => (
561
+ <RuntimeStrategyCard
562
+ key={strategy.runtimeKey}
563
+ strategy={strategy}
564
+ provider={runtimeData?.provider || 'bybit'}
565
+ onUpdated={load}
566
+ />
567
+ ))}
568
+
569
+ {!loading && mode !== 'runtime' ? (
570
+ <StrategySnapshotList
571
+ strategies={filteredSnapshotStrategies}
572
+ mode={mode}
573
+ selectedCardIds={selectedSnapshotCardIdSet}
574
+ onDeleted={handleSnapshotDeleted}
575
+ onToggleSelection={handleToggleSnapshotSelection}
576
+ emptyText={snapshotEmptyText}
577
+ />
578
+ ) : null}
579
+ </Box>
580
+ </Box>
581
+ </Box>
582
+ </Box>
583
+ </ClientOnly>
584
+ );
585
+ };
586
+
587
+ const RuntimeStrategiesPage = () => (
588
+ <Suspense fallback={null}>
589
+ <RuntimeStrategiesContent />
590
+ </Suspense>
591
+ );
592
+
593
+ export default RuntimeStrategiesPage;
@@ -0,0 +1,20 @@
1
+ import { notFound } from 'next/navigation';
2
+ import StrategiesPageClient from '../StrategiesPageClient';
3
+
4
+ const MODES = new Set(['runtime', 'replay', 'ai', 'backtest']);
5
+
6
+ const StrategyModePage = async ({
7
+ params,
8
+ }: {
9
+ params: Promise<{ mode: string }>;
10
+ }) => {
11
+ const { mode } = await params;
12
+
13
+ if (!MODES.has(mode)) {
14
+ notFound();
15
+ }
16
+
17
+ return <StrategiesPageClient />;
18
+ };
19
+
20
+ export default StrategyModePage;
@@ -0,0 +1,7 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ const StrategiesPage = () => {
4
+ redirect('/routes/strategies/runtime');
5
+ };
6
+
7
+ export default StrategiesPage;
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import { getHistory, sendMessage } from '@actions/ai';
2
+ import { getHistory, sendMessage } from '#actions/ai';
3
3
  import { AIChatHistory, AIChatMessage, Filters } from '@tradejs/types';
4
4
 
5
5
  type AiChatEntry = {