@tradejs/app 1.0.0

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 (126) hide show
  1. package/README.md +12 -0
  2. package/bin/tradejs-app.mjs +54 -0
  3. package/next-env.d.ts +6 -0
  4. package/next.config.mjs +31 -0
  5. package/package.json +60 -0
  6. package/src/app/actions/ai.ts +33 -0
  7. package/src/app/actions/backtest.ts +55 -0
  8. package/src/app/actions/kline.ts +18 -0
  9. package/src/app/actions/scanner.ts +10 -0
  10. package/src/app/actions/signal.ts +19 -0
  11. package/src/app/api/ai/route.ts +151 -0
  12. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  13. package/src/app/api/backtest/files/route.ts +60 -0
  14. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
  15. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
  16. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
  17. package/src/app/api/cron/route.ts +4 -0
  18. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
  19. package/src/app/api/derivatives/summary/route.ts +20 -0
  20. package/src/app/api/files/screenshot/[name]/route.ts +42 -0
  21. package/src/app/api/indicators/route.ts +24 -0
  22. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
  23. package/src/app/api/scanner/[provider]/route.ts +41 -0
  24. package/src/app/api/scanner/route.ts +31 -0
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
  26. package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
  27. package/src/app/api/spread/summary/route.ts +20 -0
  28. package/src/app/auth.ts +76 -0
  29. package/src/app/components/Backtest/CompareList/index.tsx +34 -0
  30. package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
  31. package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
  32. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
  33. package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
  34. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
  35. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
  36. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
  37. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
  38. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
  39. package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
  40. package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
  41. package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
  42. package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
  43. package/src/app/components/Backtest/TestCard/context.ts +14 -0
  44. package/src/app/components/Backtest/TestCard/index.ts +28 -0
  45. package/src/app/components/Backtest/TestList/index.tsx +124 -0
  46. package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
  47. package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
  48. package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
  49. package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
  50. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
  51. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
  52. package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
  53. package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
  54. package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
  55. package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
  56. package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
  57. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
  58. package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
  59. package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
  60. package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
  61. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
  62. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
  63. package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
  64. package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
  65. package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
  66. package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
  67. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
  68. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
  69. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
  70. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
  71. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
  72. package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
  73. package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
  74. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
  75. package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
  76. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
  77. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
  78. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
  79. package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
  80. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
  81. package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
  82. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
  83. package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
  84. package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
  85. package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
  86. package/src/app/components/Shared/AppShell.tsx +28 -0
  87. package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
  88. package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
  89. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
  90. package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
  91. package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
  92. package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
  93. package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
  94. package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
  95. package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
  96. package/src/app/components/Shared/Filters/context.ts +17 -0
  97. package/src/app/components/Shared/Filters/index.ts +17 -0
  98. package/src/app/components/Shared/Sidebar/index.tsx +72 -0
  99. package/src/app/components/UI/ColorMode/index.tsx +112 -0
  100. package/src/app/components/UI/EmptyState/index.tsx +28 -0
  101. package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
  102. package/src/app/components/UI/Segment/index.tsx +23 -0
  103. package/src/app/components/UI/Select/index.tsx +81 -0
  104. package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
  105. package/src/app/components/UI/Switcher/index.tsx +24 -0
  106. package/src/app/components/UI/Toaster/index.tsx +45 -0
  107. package/src/app/components/UI/index.ts +8 -0
  108. package/src/app/favicon.ico +0 -0
  109. package/src/app/globals.css +5 -0
  110. package/src/app/layout.tsx +31 -0
  111. package/src/app/page.tsx +14 -0
  112. package/src/app/provider.tsx +39 -0
  113. package/src/app/routes/backtest/[test]/page.tsx +33 -0
  114. package/src/app/routes/backtest/page.tsx +374 -0
  115. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
  116. package/src/app/routes/dashboard/page.tsx +20 -0
  117. package/src/app/routes/derivatives/page.tsx +202 -0
  118. package/src/app/routes/signin/page.tsx +155 -0
  119. package/src/app/store/data.ts +144 -0
  120. package/src/app/store/filters.ts +29 -0
  121. package/src/app/store/index.ts +13 -0
  122. package/src/app/store/indicators.ts +229 -0
  123. package/src/app/store/tests.ts +464 -0
  124. package/src/app/store/tickers.ts +89 -0
  125. package/src/proxy.ts +142 -0
  126. package/tsconfig.json +40 -0
@@ -0,0 +1,374 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import {
5
+ Box,
6
+ Button,
7
+ Checkbox,
8
+ ClientOnly,
9
+ CloseButton,
10
+ Dialog,
11
+ Flex,
12
+ Portal,
13
+ Text,
14
+ } from '@chakra-ui/react';
15
+ import { deleteBacktest } from '@actions/backtest';
16
+ import { useBacktestMutations, useTestList } from '@store';
17
+ import { Select, toaster } from '@UI';
18
+ import { CompareList } from '@components/Backtest/CompareList';
19
+ import { TestList } from '@components/Backtest/TestList';
20
+ import { parseTestName } from '@tradejs/core/backtest';
21
+
22
+ const ALL_STRATEGIES = '__all__';
23
+ const ALL_SUITES = '__all__';
24
+
25
+ const Backtest = () => {
26
+ const { tests, loadding, fulFilled } = useTestList();
27
+ const { removeBacktestTest } = useBacktestMutations();
28
+ const [selectedTestNames, setSelectedTestNames] = useState<string[]>([]);
29
+ const [isDeleteSelectedOpen, setIsDeleteSelectedOpen] = useState(false);
30
+ const [isDeletingSelected, setIsDeletingSelected] = useState(false);
31
+ const strategyItems = useMemo(() => {
32
+ const names = new Set<string>();
33
+ for (const test of tests) {
34
+ const strategyName = test.data?.strategyName;
35
+ if (typeof strategyName === 'string' && strategyName) {
36
+ names.add(strategyName);
37
+ }
38
+ }
39
+
40
+ return [
41
+ { label: 'All strategies', value: ALL_STRATEGIES },
42
+ ...Array.from(names)
43
+ .sort()
44
+ .map((strategyName) => ({
45
+ label: strategyName,
46
+ value: strategyName,
47
+ })),
48
+ ];
49
+ }, [tests]);
50
+ const [selectedStrategy, setSelectedStrategy] = useState(ALL_STRATEGIES);
51
+ const suiteItems = useMemo(() => {
52
+ const names = new Set<string>();
53
+ for (const test of tests) {
54
+ if (
55
+ selectedStrategy !== ALL_STRATEGIES &&
56
+ test.data?.strategyName !== selectedStrategy
57
+ ) {
58
+ continue;
59
+ }
60
+
61
+ const { testSuiteId } = parseTestName(test.value);
62
+ if (testSuiteId) {
63
+ names.add(testSuiteId);
64
+ }
65
+ }
66
+
67
+ return [
68
+ { label: 'All suites', value: ALL_SUITES },
69
+ ...Array.from(names)
70
+ .sort()
71
+ .map((testSuiteId) => ({
72
+ label: testSuiteId,
73
+ value: testSuiteId,
74
+ })),
75
+ ];
76
+ }, [tests, selectedStrategy]);
77
+ const [selectedSuite, setSelectedSuite] = useState(ALL_SUITES);
78
+
79
+ useEffect(() => {
80
+ if (!strategyItems.some((item) => item.value === selectedStrategy)) {
81
+ setSelectedStrategy(strategyItems[0]?.value || ALL_STRATEGIES);
82
+ }
83
+ }, [strategyItems, selectedStrategy]);
84
+
85
+ useEffect(() => {
86
+ if (!suiteItems.some((item) => item.value === selectedSuite)) {
87
+ setSelectedSuite(suiteItems[0]?.value || ALL_SUITES);
88
+ }
89
+ }, [suiteItems, selectedSuite]);
90
+
91
+ const filteredTests = useMemo(() => {
92
+ return tests.filter((test) => {
93
+ if (
94
+ selectedStrategy !== ALL_STRATEGIES &&
95
+ test.data?.strategyName !== selectedStrategy
96
+ ) {
97
+ return false;
98
+ }
99
+
100
+ if (selectedSuite !== ALL_SUITES) {
101
+ const { testSuiteId } = parseTestName(test.value);
102
+ return testSuiteId === selectedSuite;
103
+ }
104
+
105
+ return true;
106
+ });
107
+ }, [tests, selectedStrategy, selectedSuite]);
108
+
109
+ const filteredTestNames = useMemo(
110
+ () => filteredTests.map((test) => test.value),
111
+ [filteredTests],
112
+ );
113
+
114
+ const selectedFilteredCount = useMemo(() => {
115
+ const filteredSet = new Set(filteredTestNames);
116
+ return selectedTestNames.filter((testName) => filteredSet.has(testName))
117
+ .length;
118
+ }, [filteredTestNames, selectedTestNames]);
119
+
120
+ const allFilteredSelected =
121
+ filteredTests.length > 0 && selectedFilteredCount === filteredTests.length;
122
+ const hasSelectedInFilter = selectedFilteredCount > 0;
123
+
124
+ const noData = fulFilled && !loadding && filteredTests.length === 0;
125
+
126
+ useEffect(() => {
127
+ const actual = new Set(tests.map((test) => test.value));
128
+ setSelectedTestNames((prev) => {
129
+ const next = prev.filter((testName) => actual.has(testName));
130
+
131
+ if (
132
+ next.length === prev.length &&
133
+ next.every((name, i) => name === prev[i])
134
+ ) {
135
+ return prev;
136
+ }
137
+
138
+ return next;
139
+ });
140
+ }, [tests]);
141
+
142
+ const handleToggleSelection = (testName: string, checked: boolean) => {
143
+ setSelectedTestNames((prev) => {
144
+ if (checked) {
145
+ if (prev.includes(testName)) {
146
+ return prev;
147
+ }
148
+ return [...prev, testName];
149
+ }
150
+
151
+ return prev.filter((name) => name !== testName);
152
+ });
153
+ };
154
+
155
+ const handleSelectAllFiltered = (checked: boolean) => {
156
+ setSelectedTestNames((prev) => {
157
+ if (!checked) {
158
+ const filteredSet = new Set(filteredTestNames);
159
+ return prev.filter((name) => !filteredSet.has(name));
160
+ }
161
+
162
+ const next = new Set(prev);
163
+ for (const name of filteredTestNames) {
164
+ next.add(name);
165
+ }
166
+ return Array.from(next);
167
+ });
168
+ };
169
+
170
+ const handleDeleteSelected = async () => {
171
+ const selectedSet = new Set(selectedTestNames);
172
+ const targets = filteredTests.filter((test) => selectedSet.has(test.value));
173
+
174
+ if (targets.length === 0 || isDeletingSelected) {
175
+ setIsDeleteSelectedOpen(false);
176
+ return;
177
+ }
178
+
179
+ setIsDeletingSelected(true);
180
+
181
+ try {
182
+ const results = await Promise.allSettled(
183
+ targets.map(async (test) => {
184
+ const strategyName = test.data?.strategyName as string | undefined;
185
+ if (!strategyName) {
186
+ throw new Error(`Missing strategy for ${test.value}`);
187
+ }
188
+
189
+ const deleted = await deleteBacktest(test.value, strategyName);
190
+ if (!deleted) {
191
+ throw new Error(`Delete failed for ${test.value}`);
192
+ }
193
+
194
+ await removeBacktestTest(test.value);
195
+ return test.value;
196
+ }),
197
+ );
198
+
199
+ const successCount = results.filter(
200
+ (item) => item.status === 'fulfilled',
201
+ ).length;
202
+ const failedCount = results.length - successCount;
203
+
204
+ if (successCount > 0) {
205
+ const deletedSet = new Set(
206
+ results
207
+ .filter((item) => item.status === 'fulfilled')
208
+ .map((item) => item.value),
209
+ );
210
+ setSelectedTestNames((prev) =>
211
+ prev.filter((testName) => !deletedSet.has(testName)),
212
+ );
213
+ }
214
+
215
+ if (failedCount === 0) {
216
+ toaster.success({
217
+ title: 'Tests deleted',
218
+ description: `Deleted: ${successCount}`,
219
+ });
220
+ } else {
221
+ toaster.error({
222
+ title: 'Bulk delete finished with errors',
223
+ description: `Deleted: ${successCount} of ${targets.length}`,
224
+ });
225
+ }
226
+ } catch {
227
+ toaster.error({
228
+ title: 'Delete failed',
229
+ description: 'Failed to delete selected tests.',
230
+ });
231
+ } finally {
232
+ setIsDeletingSelected(false);
233
+ setIsDeleteSelectedOpen(false);
234
+ }
235
+ };
236
+
237
+ return (
238
+ <ClientOnly>
239
+ <Box minH="100vh" bg="gray.900">
240
+ <Box
241
+ as="main"
242
+ minH="100vh"
243
+ minW="1200px"
244
+ pl={2}
245
+ bg="gray.900"
246
+ display="flex"
247
+ flexDirection="column"
248
+ alignItems="flex-start"
249
+ >
250
+ <Flex
251
+ mb={2}
252
+ mt={2}
253
+ pl={2}
254
+ gap={8}
255
+ flexDirection="row"
256
+ alignItems="center"
257
+ >
258
+ <Flex gap={3} alignItems="center">
259
+ <Select
260
+ placeholder="Strategy"
261
+ value={[selectedStrategy]}
262
+ defaultValue={[selectedStrategy]}
263
+ onChange={(value) =>
264
+ setSelectedStrategy(value[0] || ALL_STRATEGIES)
265
+ }
266
+ items={strategyItems}
267
+ width="220px"
268
+ />
269
+ <Select
270
+ placeholder="TestSuite"
271
+ value={[selectedSuite]}
272
+ defaultValue={[selectedSuite]}
273
+ onChange={(value) => setSelectedSuite(value[0] || ALL_SUITES)}
274
+ items={suiteItems}
275
+ width="180px"
276
+ />
277
+ </Flex>
278
+ <CompareList />
279
+ </Flex>
280
+ <Flex mb={4} pl={2} gap={4} alignItems="center" w="full" minH="32px">
281
+ <Checkbox.Root
282
+ size="sm"
283
+ colorPalette="teal"
284
+ checked={
285
+ allFilteredSelected
286
+ ? true
287
+ : hasSelectedInFilter
288
+ ? 'indeterminate'
289
+ : false
290
+ }
291
+ onCheckedChange={(details) =>
292
+ handleSelectAllFiltered(details.checked === true)
293
+ }
294
+ >
295
+ <Checkbox.HiddenInput />
296
+ <Checkbox.Control />
297
+ </Checkbox.Root>
298
+ <Text color="gray.200" fontWeight="semibold">
299
+ Selected: {selectedFilteredCount}
300
+ </Text>
301
+
302
+ <Dialog.Root
303
+ open={isDeleteSelectedOpen}
304
+ onOpenChange={(e) => setIsDeleteSelectedOpen(e.open)}
305
+ >
306
+ <Dialog.Trigger asChild>
307
+ <Button
308
+ size="sm"
309
+ colorPalette="red"
310
+ variant="outline"
311
+ disabled={!hasSelectedInFilter || isDeletingSelected}
312
+ >
313
+ Delete
314
+ </Button>
315
+ </Dialog.Trigger>
316
+ <Portal>
317
+ <Dialog.Backdrop />
318
+ <Dialog.Positioner>
319
+ <Dialog.Content>
320
+ <Dialog.Header>
321
+ <Dialog.Title>Delete selected tests</Dialog.Title>
322
+ <Dialog.CloseTrigger asChild>
323
+ <CloseButton position="absolute" right="3" top="3" />
324
+ </Dialog.CloseTrigger>
325
+ </Dialog.Header>
326
+ <Dialog.Body>
327
+ <Text fontSize="sm" color="gray.200">
328
+ Delete selected tests ({selectedFilteredCount})?
329
+ </Text>
330
+ <Text fontSize="sm" color="gray.400" mt={2}>
331
+ This action cannot be undone.
332
+ </Text>
333
+ </Dialog.Body>
334
+ <Dialog.Footer>
335
+ <Dialog.ActionTrigger asChild>
336
+ <Button
337
+ variant="outline"
338
+ size="sm"
339
+ disabled={isDeletingSelected}
340
+ >
341
+ Cancel
342
+ </Button>
343
+ </Dialog.ActionTrigger>
344
+ <Button
345
+ colorPalette="red"
346
+ size="sm"
347
+ onClick={handleDeleteSelected}
348
+ loading={isDeletingSelected}
349
+ >
350
+ Delete
351
+ </Button>
352
+ </Dialog.Footer>
353
+ </Dialog.Content>
354
+ </Dialog.Positioner>
355
+ </Portal>
356
+ </Dialog.Root>
357
+ </Flex>
358
+ <Box flex="1" h="full" w="full">
359
+ <TestList
360
+ tests={filteredTests}
361
+ loadding={loadding}
362
+ fulFilled={fulFilled}
363
+ noData={noData}
364
+ selectedTestNames={selectedTestNames}
365
+ onToggleSelection={handleToggleSelection}
366
+ />
367
+ </Box>
368
+ </Box>
369
+ </Box>
370
+ </ClientOnly>
371
+ );
372
+ };
373
+
374
+ export default Backtest;
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Box, Flex, ClientOnly } from '@chakra-ui/react';
6
+ import { useFilters, useTickers, useTestList } from '@store';
7
+ import { Filters } from '@shared/Filters';
8
+ import { MainChart } from '@app/components/Dashboard/MainChart';
9
+ import { Interval, OnChangeFilters, Provider } from '@tradejs/types';
10
+
11
+ const Dashboard = () => {
12
+ const searchParams = useSearchParams();
13
+ const { filters, setFilters } = useFilters();
14
+ const { tickers } = useTickers(filters.provider || 'bybit');
15
+ const { tests } = useTestList({ symbol: filters.symbol });
16
+ const hasBacktestId = searchParams.has('backtestId');
17
+ const hasBacktestStrategy = searchParams.has('backtestStrategy');
18
+ const backtestId = searchParams.get('backtestId');
19
+ const backtestStrategy = searchParams.get('backtestStrategy');
20
+
21
+ const parseDashboardPath = useCallback(() => {
22
+ const parts = window.location.pathname.split('/').filter(Boolean);
23
+ return {
24
+ provider: (parts[2] || filters.provider || 'bybit') as Provider,
25
+ symbol: (parts[3] || filters.symbol) as string,
26
+ interval: (parts[4] || filters.interval) as Interval,
27
+ };
28
+ }, [filters.interval, filters.provider, filters.symbol]);
29
+
30
+ useEffect(() => {
31
+ const parsed = parseDashboardPath();
32
+ setFilters({
33
+ provider: parsed.provider,
34
+ symbol: parsed.symbol,
35
+ interval: parsed.interval,
36
+ ...(hasBacktestId ? { backtestId } : {}),
37
+ ...(hasBacktestStrategy ? { backtestStrategy } : {}),
38
+ });
39
+ }, [
40
+ backtestId,
41
+ backtestStrategy,
42
+ hasBacktestId,
43
+ hasBacktestStrategy,
44
+ parseDashboardPath,
45
+ setFilters,
46
+ ]);
47
+
48
+ const onChangeFilters: OnChangeFilters = useCallback(
49
+ (newFilters) => {
50
+ const parsed = parseDashboardPath();
51
+ const nextFilters = {
52
+ ...filters,
53
+ ...newFilters,
54
+ provider: (newFilters.provider || parsed.provider) as Provider,
55
+ symbol: (newFilters.symbol || parsed.symbol) as string,
56
+ interval: (newFilters.interval || parsed.interval) as Interval,
57
+ };
58
+ setFilters(nextFilters);
59
+ const nextProvider = nextFilters.provider || 'bybit';
60
+ const nextSymbol = nextFilters.symbol;
61
+ const nextInterval = nextFilters.interval;
62
+ const params = new URLSearchParams(window.location.search);
63
+
64
+ if (nextFilters.backtestId) {
65
+ params.set('backtestId', nextFilters.backtestId);
66
+ } else {
67
+ params.delete('backtestId');
68
+ }
69
+
70
+ if (nextFilters.backtestStrategy) {
71
+ params.set('backtestStrategy', nextFilters.backtestStrategy);
72
+ } else {
73
+ params.delete('backtestStrategy');
74
+ }
75
+
76
+ const search = params.toString();
77
+
78
+ window.history.replaceState(
79
+ null,
80
+ '',
81
+ `/routes/dashboard/${nextProvider}/${nextSymbol}/${nextInterval}${search ? `?${search}` : ''}`,
82
+ );
83
+ },
84
+ [filters, parseDashboardPath, setFilters],
85
+ );
86
+
87
+ return (
88
+ <ClientOnly>
89
+ <Box
90
+ as="main"
91
+ minH="100vh"
92
+ p={4}
93
+ bg="gray.900"
94
+ display="flex"
95
+ flexDirection="column"
96
+ justifyContent="space-between"
97
+ alignItems="flex-start"
98
+ >
99
+ <Filters.Root
100
+ filters={filters}
101
+ tickers={tickers}
102
+ backtestFiles={tests}
103
+ onChangeFilters={onChangeFilters}
104
+ >
105
+ <Flex mb={2} gap={4} alignItems="center" flexDirection="row">
106
+ <Filters.SelectProvider />
107
+ <Filters.SelectSymbol />
108
+ <Filters.FavoriteIndicator />
109
+ <Filters.SelectInterval />
110
+ <Filters.SelectIndicator />
111
+ </Flex>
112
+ <Flex mb={4} gap={4} flexDirection="row">
113
+ <Filters.SelectBacktest />
114
+ </Flex>
115
+ </Filters.Root>
116
+ <Box position="relative" flex="1" w="full">
117
+ <MainChart />
118
+ </Box>
119
+ </Box>
120
+ </ClientOnly>
121
+ );
122
+ };
123
+
124
+ export default Dashboard;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useFilters } from '@store';
6
+
7
+ const DashboardIndex = () => {
8
+ const { filters } = useFilters();
9
+ const router = useRouter();
10
+
11
+ useEffect(() => {
12
+ router.replace(
13
+ `/routes/dashboard/${filters.provider || 'bybit'}/${filters.symbol}/${filters.interval}`,
14
+ );
15
+ }, [filters.interval, filters.provider, filters.symbol, router]);
16
+
17
+ return null;
18
+ };
19
+
20
+ export default DashboardIndex;