@tradejs/app 1.0.9 → 1.0.10

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 (119) hide show
  1. package/README.md +7 -2
  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/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -0,0 +1,376 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import { Box, ClientOnly, Flex } from '@chakra-ui/react';
5
+ import { deleteBacktest } from '#actions/backtest';
6
+ import { useBacktestMutations, useTestList } from '#store';
7
+ import { Select, toaster } from '#ui';
8
+ import { CompareList } from '#components/Backtest/CompareList';
9
+ import { TestList } from '#components/Backtest/TestList';
10
+ import {
11
+ BulkDeleteToolbar,
12
+ useBulkSelection,
13
+ } from '#components/Shared/BulkSelection';
14
+ import { parseTestName } from '@tradejs/core/backtest';
15
+ import { CARD_PAGE_CONTENT_MAX_WIDTH } from '#app/lib/cardPageLayout';
16
+
17
+ const ALL_STRATEGIES = '__all__';
18
+ const ALL_SUITES = '__all__';
19
+ const ALL_CONFIGS = '__all__';
20
+
21
+ interface BacktestResultsPageClientProps {
22
+ toolbarPrefix?: React.ReactNode;
23
+ }
24
+
25
+ interface PendingBacktestDelete {
26
+ value: string;
27
+ strategyName?: string;
28
+ }
29
+
30
+ export const BacktestResultsPageClient = ({
31
+ toolbarPrefix,
32
+ }: BacktestResultsPageClientProps) => {
33
+ const { tests, loadding, fulFilled } = useTestList();
34
+ const { removeBacktestTest } = useBacktestMutations();
35
+ const [isDeleteSelectedOpen, setIsDeleteSelectedOpen] = useState(false);
36
+ const [isDeletingSelected, setIsDeletingSelected] = useState(false);
37
+ const [pendingDeleteTests, setPendingDeleteTests] = useState<
38
+ PendingBacktestDelete[]
39
+ >([]);
40
+ const strategyItems = useMemo(() => {
41
+ const names = new Set<string>();
42
+ for (const test of tests) {
43
+ const strategyName = test.data?.strategyName;
44
+ if (typeof strategyName === 'string' && strategyName) {
45
+ names.add(strategyName);
46
+ }
47
+ }
48
+
49
+ return [
50
+ { label: 'All strategies', value: ALL_STRATEGIES },
51
+ ...Array.from(names)
52
+ .sort()
53
+ .map((strategyName) => ({
54
+ label: strategyName,
55
+ value: strategyName,
56
+ })),
57
+ ];
58
+ }, [tests]);
59
+ const [selectedStrategy, setSelectedStrategy] = useState(ALL_STRATEGIES);
60
+ const suiteItems = useMemo(() => {
61
+ const names = new Set<string>();
62
+ for (const test of tests) {
63
+ if (
64
+ selectedStrategy !== ALL_STRATEGIES &&
65
+ test.data?.strategyName !== selectedStrategy
66
+ ) {
67
+ continue;
68
+ }
69
+
70
+ const { testSuiteId } = parseTestName(test.value);
71
+ if (testSuiteId) {
72
+ names.add(testSuiteId);
73
+ }
74
+ }
75
+
76
+ return [
77
+ { label: 'All suites', value: ALL_SUITES },
78
+ ...Array.from(names)
79
+ .sort()
80
+ .map((testSuiteId) => ({
81
+ label: testSuiteId,
82
+ value: testSuiteId,
83
+ })),
84
+ ];
85
+ }, [tests, selectedStrategy]);
86
+ const [selectedSuite, setSelectedSuite] = useState(ALL_SUITES);
87
+ const configItems = useMemo(() => {
88
+ const names = new Set<string>();
89
+ for (const test of tests) {
90
+ if (
91
+ selectedStrategy !== ALL_STRATEGIES &&
92
+ test.data?.strategyName !== selectedStrategy
93
+ ) {
94
+ continue;
95
+ }
96
+
97
+ if (selectedSuite !== ALL_SUITES) {
98
+ const { testSuiteId } = parseTestName(test.value);
99
+ if (testSuiteId !== selectedSuite) {
100
+ continue;
101
+ }
102
+ }
103
+
104
+ const configId =
105
+ typeof test.data?.configId === 'string' ? test.data.configId : '';
106
+ if (configId) {
107
+ names.add(configId);
108
+ }
109
+ }
110
+
111
+ return [
112
+ { label: 'All configs', value: ALL_CONFIGS },
113
+ ...Array.from(names)
114
+ .sort()
115
+ .map((configId) => ({
116
+ label: configId,
117
+ value: configId,
118
+ })),
119
+ ];
120
+ }, [tests, selectedStrategy, selectedSuite]);
121
+ const [selectedConfigId, setSelectedConfigId] = useState(ALL_CONFIGS);
122
+
123
+ useEffect(() => {
124
+ if (!strategyItems.some((item) => item.value === selectedStrategy)) {
125
+ setSelectedStrategy(strategyItems[0]?.value || ALL_STRATEGIES);
126
+ }
127
+ }, [strategyItems, selectedStrategy]);
128
+
129
+ useEffect(() => {
130
+ if (!suiteItems.some((item) => item.value === selectedSuite)) {
131
+ setSelectedSuite(suiteItems[0]?.value || ALL_SUITES);
132
+ }
133
+ }, [suiteItems, selectedSuite]);
134
+
135
+ useEffect(() => {
136
+ if (!configItems.some((item) => item.value === selectedConfigId)) {
137
+ setSelectedConfigId(configItems[0]?.value || ALL_CONFIGS);
138
+ }
139
+ }, [configItems, selectedConfigId]);
140
+
141
+ const filteredTests = useMemo(() => {
142
+ return tests.filter((test) => {
143
+ if (
144
+ selectedStrategy !== ALL_STRATEGIES &&
145
+ test.data?.strategyName !== selectedStrategy
146
+ ) {
147
+ return false;
148
+ }
149
+
150
+ if (selectedSuite !== ALL_SUITES) {
151
+ const { testSuiteId } = parseTestName(test.value);
152
+ if (testSuiteId !== selectedSuite) {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ if (selectedConfigId !== ALL_CONFIGS) {
158
+ return test.data?.configId === selectedConfigId;
159
+ }
160
+
161
+ return true;
162
+ });
163
+ }, [tests, selectedStrategy, selectedSuite, selectedConfigId]);
164
+
165
+ const filteredTestNames = useMemo(
166
+ () => filteredTests.map((test) => test.value),
167
+ [filteredTests],
168
+ );
169
+ const allTestNames = useMemo(() => tests.map((test) => test.value), [tests]);
170
+ const {
171
+ selectedIds: selectedTestNames,
172
+ selectedScopedCount: selectedFilteredCount,
173
+ hasScopedSelection: hasSelectedInFilter,
174
+ checkboxState: selectionCheckboxState,
175
+ toggleSelection: handleToggleSelection,
176
+ setScopeSelected: handleSelectAllFiltered,
177
+ removeSelection: removeDeletedSelection,
178
+ } = useBulkSelection({
179
+ scopeIds: filteredTestNames,
180
+ validIds: allTestNames,
181
+ });
182
+
183
+ const noData = fulFilled && !loadding && filteredTests.length === 0;
184
+
185
+ const handleOpenDeleteSelected = () => {
186
+ const selectedSet = new Set(selectedTestNames);
187
+ const targets = filteredTests.filter((test) => selectedSet.has(test.value));
188
+
189
+ if (targets.length === 0) {
190
+ setIsDeleteSelectedOpen(false);
191
+ setPendingDeleteTests([]);
192
+ return;
193
+ }
194
+
195
+ setPendingDeleteTests(
196
+ targets.map((test) => ({
197
+ value: test.value,
198
+ strategyName:
199
+ typeof test.data?.strategyName === 'string'
200
+ ? test.data.strategyName
201
+ : undefined,
202
+ })),
203
+ );
204
+ setIsDeleteSelectedOpen(true);
205
+ };
206
+
207
+ const handleDeleteSelected = async () => {
208
+ const targets = pendingDeleteTests;
209
+
210
+ if (targets.length === 0 || isDeletingSelected) {
211
+ setIsDeleteSelectedOpen(false);
212
+ setPendingDeleteTests([]);
213
+ return;
214
+ }
215
+
216
+ setIsDeletingSelected(true);
217
+
218
+ try {
219
+ const results = await Promise.allSettled(
220
+ targets.map(async (test) => {
221
+ const strategyName = test.strategyName;
222
+ if (!strategyName) {
223
+ throw new Error(`Missing strategy for ${test.value}`);
224
+ }
225
+
226
+ const deleted = await deleteBacktest(test.value, strategyName);
227
+ if (!deleted) {
228
+ throw new Error(`Delete failed for ${test.value}`);
229
+ }
230
+
231
+ await removeBacktestTest(test.value);
232
+ return test.value;
233
+ }),
234
+ );
235
+
236
+ const successCount = results.filter(
237
+ (item) => item.status === 'fulfilled',
238
+ ).length;
239
+ const failedCount = results.length - successCount;
240
+
241
+ if (successCount > 0) {
242
+ const deletedIds = results
243
+ .filter((item) => item.status === 'fulfilled')
244
+ .map((item) => item.value);
245
+ removeDeletedSelection(deletedIds);
246
+ }
247
+
248
+ if (failedCount === 0) {
249
+ toaster.success({
250
+ title: 'Tests deleted',
251
+ description: `Deleted: ${successCount}`,
252
+ });
253
+ } else {
254
+ toaster.error({
255
+ title: 'Bulk delete finished with errors',
256
+ description: `Deleted: ${successCount} of ${targets.length}`,
257
+ });
258
+ }
259
+ } catch {
260
+ toaster.error({
261
+ title: 'Delete failed',
262
+ description: 'Failed to delete selected tests.',
263
+ });
264
+ } finally {
265
+ setIsDeletingSelected(false);
266
+ setIsDeleteSelectedOpen(false);
267
+ setPendingDeleteTests([]);
268
+ }
269
+ };
270
+
271
+ const handleBulkDeleteDialogOpenChange = (open: boolean) => {
272
+ setIsDeleteSelectedOpen(open);
273
+
274
+ if (!open && !isDeletingSelected) {
275
+ setPendingDeleteTests([]);
276
+ }
277
+ };
278
+
279
+ const deleteSelectedTestCount = isDeleteSelectedOpen
280
+ ? pendingDeleteTests.length
281
+ : selectedFilteredCount;
282
+
283
+ return (
284
+ <ClientOnly>
285
+ <Box minH="100vh" bg="gray.900">
286
+ <Box
287
+ as="main"
288
+ minH="100vh"
289
+ minW="1200px"
290
+ pl={2}
291
+ bg="gray.900"
292
+ display="flex"
293
+ flexDirection="column"
294
+ alignItems="flex-start"
295
+ >
296
+ <Box
297
+ minH="100vh"
298
+ w="full"
299
+ maxW={CARD_PAGE_CONTENT_MAX_WIDTH}
300
+ display="flex"
301
+ flex="1"
302
+ flexDirection="column"
303
+ alignItems="flex-start"
304
+ >
305
+ <Flex
306
+ mb={2}
307
+ mt={2}
308
+ gap={8}
309
+ flexDirection="row"
310
+ alignItems="center"
311
+ w="full"
312
+ >
313
+ <Flex gap={3} alignItems="center">
314
+ {toolbarPrefix}
315
+ <Select
316
+ placeholder="Strategy"
317
+ value={[selectedStrategy]}
318
+ defaultValue={[selectedStrategy]}
319
+ onChange={(value) =>
320
+ setSelectedStrategy(value[0] || ALL_STRATEGIES)
321
+ }
322
+ items={strategyItems}
323
+ width="220px"
324
+ />
325
+ <Select
326
+ placeholder="TestSuite"
327
+ value={[selectedSuite]}
328
+ defaultValue={[selectedSuite]}
329
+ onChange={(value) => setSelectedSuite(value[0] || ALL_SUITES)}
330
+ items={suiteItems}
331
+ width="180px"
332
+ />
333
+ <Select
334
+ placeholder="ConfigId"
335
+ value={[selectedConfigId]}
336
+ defaultValue={[selectedConfigId]}
337
+ onChange={(value) =>
338
+ setSelectedConfigId(value[0] || ALL_CONFIGS)
339
+ }
340
+ items={configItems}
341
+ width="180px"
342
+ />
343
+ </Flex>
344
+ <CompareList />
345
+ </Flex>
346
+ <BulkDeleteToolbar
347
+ selectedCount={selectedFilteredCount}
348
+ checkboxState={selectionCheckboxState}
349
+ hasSelection={hasSelectedInFilter}
350
+ isDeleting={isDeletingSelected}
351
+ dialogOpen={isDeleteSelectedOpen}
352
+ deleteTitle="Delete selected tests"
353
+ deleteDescription={`Delete selected tests (${deleteSelectedTestCount})?`}
354
+ onDialogOpenChange={handleBulkDeleteDialogOpenChange}
355
+ onToggleAll={handleSelectAllFiltered}
356
+ onRequestDelete={handleOpenDeleteSelected}
357
+ onConfirmDelete={handleDeleteSelected}
358
+ />
359
+ <Box flex="1" h="full" w="full">
360
+ <TestList
361
+ tests={filteredTests}
362
+ loadding={loadding}
363
+ fulFilled={fulFilled}
364
+ noData={noData}
365
+ selectedTestNames={selectedTestNames}
366
+ onToggleSelection={handleToggleSelection}
367
+ />
368
+ </Box>
369
+ </Box>
370
+ </Box>
371
+ </Box>
372
+ </ClientOnly>
373
+ );
374
+ };
375
+
376
+ export default BacktestResultsPageClient;
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button, Menu, Portal } from '@chakra-ui/react';
5
+ import { useFavoriteTests, useTestsCompare } from '#store';
6
+ import { buildDashboardPath } from '#app/lib/marketRoutes';
7
+ import { TestCardConfigDrawerPanel } from '../ConfigDrawer';
8
+ import { TestCardDeleteDialog } from '../DeleteButton';
9
+ import { TestCardOrdersDrawerPanel } from '../OrdersDrawer';
10
+ import { TestCardStatDrawerPanel } from '../StatDrawer';
11
+ import { useTestContext } from '../context';
12
+
13
+ const connectorNameToProvider: Record<
14
+ string,
15
+ 'bybit' | 'binance' | 'coinbase'
16
+ > = {
17
+ bybit: 'bybit',
18
+ binance: 'binance',
19
+ coinbase: 'coinbase',
20
+ };
21
+
22
+ export const TestCardActionsMenu = () => {
23
+ const [configOpen, setConfigOpen] = useState(false);
24
+ const [ordersOpen, setOrdersOpen] = useState(false);
25
+ const [statOpen, setStatOpen] = useState(false);
26
+ const [deleteOpen, setDeleteOpen] = useState(false);
27
+ const { testResult } = useTestContext();
28
+ const { checkIsCompared, onChangeCompare } = useTestsCompare();
29
+ const { checkIsFavorite, toggleFavorite } = useFavoriteTests();
30
+ const isCompared = checkIsCompared(testResult.test.name);
31
+ const isFavorite = checkIsFavorite(testResult.test.name);
32
+
33
+ const openDashboard = () => {
34
+ const provider =
35
+ connectorNameToProvider[testResult.test.connectorName.toLowerCase()] ||
36
+ 'bybit';
37
+ const params = new URLSearchParams();
38
+
39
+ params.set('backtestId', testResult.test.name);
40
+ params.set('backtestStrategy', testResult.test.strategyName);
41
+
42
+ window.open(
43
+ buildDashboardPath(
44
+ {
45
+ provider,
46
+ universe: testResult.test.universe ?? 'crypto',
47
+ symbol: testResult.test.symbol,
48
+ interval: testResult.test.interval ?? '15',
49
+ },
50
+ params,
51
+ ),
52
+ '_blank',
53
+ 'noopener,noreferrer',
54
+ );
55
+ };
56
+
57
+ return (
58
+ <>
59
+ <Menu.Root positioning={{ placement: 'bottom-end' }}>
60
+ <Menu.Trigger asChild>
61
+ <Button size="sm" variant="outline">
62
+ Actions
63
+ </Button>
64
+ </Menu.Trigger>
65
+ <Portal>
66
+ <Menu.Positioner>
67
+ <Menu.Content minW="180px">
68
+ <Menu.Item
69
+ value="compare"
70
+ onClick={() => onChangeCompare(testResult.test.name)}
71
+ >
72
+ {isCompared ? 'Remove compare' : 'Compare'}
73
+ </Menu.Item>
74
+ <Menu.Item
75
+ value="favorite"
76
+ onClick={() =>
77
+ toggleFavorite(
78
+ testResult.test.name,
79
+ testResult.stat.netProfit,
80
+ )
81
+ }
82
+ >
83
+ {isFavorite ? 'Unfavorite' : 'Favorite'}
84
+ </Menu.Item>
85
+ <Menu.Separator />
86
+ <Menu.Item value="config" onClick={() => setConfigOpen(true)}>
87
+ Config
88
+ </Menu.Item>
89
+ <Menu.Item value="orders" onClick={() => setOrdersOpen(true)}>
90
+ Orders
91
+ </Menu.Item>
92
+ <Menu.Item value="stat" onClick={() => setStatOpen(true)}>
93
+ Stat
94
+ </Menu.Item>
95
+ <Menu.Item value="dashboard" onClick={openDashboard}>
96
+ Dashboard
97
+ </Menu.Item>
98
+ <Menu.Separator />
99
+ <Menu.Item
100
+ value="delete"
101
+ color="fg.error"
102
+ onClick={() => setDeleteOpen(true)}
103
+ >
104
+ Delete
105
+ </Menu.Item>
106
+ </Menu.Content>
107
+ </Menu.Positioner>
108
+ </Portal>
109
+ </Menu.Root>
110
+
111
+ <TestCardConfigDrawerPanel
112
+ open={configOpen}
113
+ onOpenChange={setConfigOpen}
114
+ />
115
+ <TestCardOrdersDrawerPanel
116
+ open={ordersOpen}
117
+ onOpenChange={setOrdersOpen}
118
+ />
119
+ <TestCardStatDrawerPanel open={statOpen} onOpenChange={setStatOpen} />
120
+ <TestCardDeleteDialog open={deleteOpen} onOpenChange={setDeleteOpen} />
121
+ </>
122
+ );
123
+ };
@@ -13,7 +13,7 @@ import {
13
13
  ReferenceLine,
14
14
  ResponsiveContainer,
15
15
  } from 'recharts';
16
- import { useTestsCompare } from '@store';
16
+ import { useTestsCompare } from '#store';
17
17
  import { useTestContext } from '../context';
18
18
  import { TestCompareList } from '@tradejs/types';
19
19
  import { mapOrderLogToChartData, getChartData } from './utils';
@@ -1,6 +1,6 @@
1
1
  import { IconButton } from '@chakra-ui/react';
2
2
  import { TbArrowsLeftRight } from 'react-icons/tb';
3
- import { useTestsCompare } from '@store';
3
+ import { useTestsCompare } from '#store';
4
4
  import { useTestContext } from '../context';
5
5
 
6
6
  export const TestCardCompareButton = () => {
@@ -17,8 +17,15 @@ const JsonCodeBlock = dynamic(() => import('./JsonCodeBlock'), { ssr: false });
17
17
 
18
18
  type TabType = 'test' | 'bot';
19
19
 
20
- export const TestCardConfigDrawer = () => {
21
- const [open, setOpen] = useState(false);
20
+ interface TestCardConfigDrawerPanelProps {
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ }
24
+
25
+ export const TestCardConfigDrawerPanel = ({
26
+ open,
27
+ onOpenChange,
28
+ }: TestCardConfigDrawerPanelProps) => {
22
29
  const [tab, setTab] = useState<TabType>('test');
23
30
  const { testResult } = useTestContext();
24
31
 
@@ -43,18 +50,9 @@ export const TestCardConfigDrawer = () => {
43
50
  >
44
51
  <Drawer.Root
45
52
  open={open}
46
- onOpenChange={(e) => setOpen(e.open)}
53
+ onOpenChange={(e) => onOpenChange(e.open)}
47
54
  size={'lg'}
48
55
  >
49
- <Drawer.Trigger asChild>
50
- <IconButton
51
- colorPalette="teal"
52
- size="xs"
53
- variant={open ? 'surface' : 'outline'}
54
- >
55
- <FiSettings />
56
- </IconButton>
57
- </Drawer.Trigger>
58
56
  <Portal>
59
57
  <Drawer.Backdrop />
60
58
  <Drawer.Positioner>
@@ -92,3 +90,21 @@ export const TestCardConfigDrawer = () => {
92
90
  </Tabs.Root>
93
91
  );
94
92
  };
93
+
94
+ export const TestCardConfigDrawer = () => {
95
+ const [open, setOpen] = useState(false);
96
+
97
+ return (
98
+ <>
99
+ <IconButton
100
+ colorPalette="teal"
101
+ size="xs"
102
+ variant={open ? 'surface' : 'outline'}
103
+ onClick={() => setOpen(true)}
104
+ >
105
+ <FiSettings />
106
+ </IconButton>
107
+ <TestCardConfigDrawerPanel open={open} onOpenChange={setOpen} />
108
+ </>
109
+ );
110
+ };
@@ -10,13 +10,20 @@ import {
10
10
  Text,
11
11
  } from '@chakra-ui/react';
12
12
  import { FiTrash2 } from 'react-icons/fi';
13
- import { deleteBacktest } from '@actions/backtest';
14
- import { useBacktestMutations } from '@store';
15
- import { toaster } from '@UI';
13
+ import { deleteBacktest } from '#actions/backtest';
14
+ import { useBacktestMutations } from '#store';
15
+ import { toaster } from '#ui';
16
16
  import { useTestContext } from '../context';
17
17
 
18
- export const TestCardDeleteButton = () => {
19
- const [open, setOpen] = useState(false);
18
+ interface TestCardDeleteDialogProps {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ }
22
+
23
+ export const TestCardDeleteDialog = ({
24
+ open,
25
+ onOpenChange,
26
+ }: TestCardDeleteDialogProps) => {
20
27
  const [isDeleting, setIsDeleting] = useState(false);
21
28
  const [error, setError] = useState<string | null>(null);
22
29
  const { removeBacktestTest } = useBacktestMutations();
@@ -46,7 +53,7 @@ export const TestCardDeleteButton = () => {
46
53
  }
47
54
 
48
55
  await removeBacktestTest(testResult.test.name);
49
- setOpen(false);
56
+ onOpenChange(false);
50
57
  toaster.success({
51
58
  title: 'Backtest deleted',
52
59
  description: testResult.test.name,
@@ -66,22 +73,12 @@ export const TestCardDeleteButton = () => {
66
73
  <Dialog.Root
67
74
  open={open}
68
75
  onOpenChange={(e) => {
69
- setOpen(e.open);
76
+ onOpenChange(e.open);
70
77
  if (!e.open) {
71
78
  setError(null);
72
79
  }
73
80
  }}
74
81
  >
75
- <Dialog.Trigger asChild>
76
- <IconButton
77
- size="xs"
78
- colorPalette="red"
79
- variant="outline"
80
- aria-label="Delete backtest"
81
- >
82
- <FiTrash2 />
83
- </IconButton>
84
- </Dialog.Trigger>
85
82
  <Portal>
86
83
  <Dialog.Backdrop />
87
84
  <Dialog.Positioner>
@@ -126,3 +123,22 @@ export const TestCardDeleteButton = () => {
126
123
  </Dialog.Root>
127
124
  );
128
125
  };
126
+
127
+ export const TestCardDeleteButton = () => {
128
+ const [open, setOpen] = useState(false);
129
+
130
+ return (
131
+ <>
132
+ <IconButton
133
+ size="xs"
134
+ colorPalette="red"
135
+ variant="outline"
136
+ aria-label="Delete backtest"
137
+ onClick={() => setOpen(true)}
138
+ >
139
+ <FiTrash2 />
140
+ </IconButton>
141
+ <TestCardDeleteDialog open={open} onOpenChange={setOpen} />
142
+ </>
143
+ );
144
+ };
@@ -1,5 +1,5 @@
1
- import { FavoriteButton } from '@shared/FavoriteButton';
2
- import { useFavoriteTests } from '@store';
1
+ import { FavoriteButton } from '#shared/FavoriteButton';
2
+ import { useFavoriteTests } from '#store';
3
3
  import { useTestContext } from '../context';
4
4
 
5
5
  export const TestCardFavoriteIndicator = () => {
@@ -1,5 +1,6 @@
1
1
  import { IconButton } from '@chakra-ui/react';
2
2
  import { FiBarChart2 } from 'react-icons/fi';
3
+ import { buildDashboardPath } from '#app/lib/marketRoutes';
3
4
  import { useTestContext } from '../context';
4
5
 
5
6
  const connectorNameToProvider: Record<
@@ -28,7 +29,15 @@ export const TestCardOpenDashboardButton = () => {
28
29
  variant="outline"
29
30
  onClick={() =>
30
31
  window.open(
31
- `/routes/dashboard/${provider}/${testResult.test.symbol}/15?${params.toString()}`,
32
+ buildDashboardPath(
33
+ {
34
+ provider,
35
+ universe: testResult.test.universe ?? 'crypto',
36
+ symbol: testResult.test.symbol,
37
+ interval: testResult.test.interval ?? '15',
38
+ },
39
+ params,
40
+ ),
32
41
  '_blank',
33
42
  'noopener,noreferrer',
34
43
  )