@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,140 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import {
5
+ Button,
6
+ Checkbox,
7
+ CloseButton,
8
+ Dialog,
9
+ Flex,
10
+ Portal,
11
+ Text,
12
+ } from '@chakra-ui/react';
13
+ import { useBulkSelection } from './useBulkSelection';
14
+ import type { BulkCheckboxState } from './utils';
15
+
16
+ interface BulkDeleteToolbarProps {
17
+ selectedCount: number;
18
+ checkboxState: BulkCheckboxState;
19
+ hasSelection: boolean;
20
+ isDeleting: boolean;
21
+ dialogOpen: boolean;
22
+ deleteTitle: ReactNode;
23
+ deleteDescription: ReactNode;
24
+ deleteHint?: ReactNode;
25
+ selectAllLabel?: string;
26
+ selectedLabel?: ReactNode;
27
+ onDialogOpenChange: (open: boolean) => void;
28
+ onToggleAll: (checked: boolean) => void;
29
+ onRequestDelete?: () => void;
30
+ onConfirmDelete: () => void;
31
+ }
32
+
33
+ export const BulkDeleteToolbar = ({
34
+ selectedCount,
35
+ checkboxState,
36
+ hasSelection,
37
+ isDeleting,
38
+ dialogOpen,
39
+ deleteTitle,
40
+ deleteDescription,
41
+ deleteHint = 'This action cannot be undone.',
42
+ selectAllLabel = 'Select filtered items',
43
+ selectedLabel,
44
+ onDialogOpenChange,
45
+ onToggleAll,
46
+ onRequestDelete,
47
+ onConfirmDelete,
48
+ }: BulkDeleteToolbarProps) => (
49
+ <Flex mb={4} gap={4} alignItems="center" w="full" minH="32px">
50
+ <Checkbox.Root
51
+ size="sm"
52
+ colorPalette="teal"
53
+ checked={checkboxState}
54
+ onCheckedChange={(details) => onToggleAll(details.checked === true)}
55
+ >
56
+ <Checkbox.HiddenInput aria-label={selectAllLabel} />
57
+ <Checkbox.Control bg="gray.800" borderColor="gray.500" />
58
+ </Checkbox.Root>
59
+ <Text color="gray.200" fontWeight="semibold">
60
+ {selectedLabel ?? `Selected: ${selectedCount}`}
61
+ </Text>
62
+
63
+ <Button
64
+ type="button"
65
+ size="sm"
66
+ colorPalette="red"
67
+ variant="outline"
68
+ aria-label="Open delete confirmation"
69
+ disabled={!hasSelection || isDeleting}
70
+ onClick={() => {
71
+ if (!hasSelection || isDeleting) {
72
+ return;
73
+ }
74
+
75
+ if (onRequestDelete) {
76
+ onRequestDelete();
77
+ return;
78
+ }
79
+
80
+ onDialogOpenChange(true);
81
+ }}
82
+ >
83
+ Delete
84
+ </Button>
85
+
86
+ <Dialog.Root
87
+ open={dialogOpen}
88
+ onOpenChange={(event) => onDialogOpenChange(event.open)}
89
+ >
90
+ <Portal>
91
+ <Dialog.Backdrop />
92
+ <Dialog.Positioner>
93
+ <Dialog.Content>
94
+ <Dialog.Header>
95
+ <Dialog.Title>{deleteTitle}</Dialog.Title>
96
+ <Dialog.CloseTrigger asChild>
97
+ <CloseButton position="absolute" right="3" top="3" />
98
+ </Dialog.CloseTrigger>
99
+ </Dialog.Header>
100
+ <Dialog.Body>
101
+ <Text fontSize="sm" color="gray.200">
102
+ {deleteDescription}
103
+ </Text>
104
+ {deleteHint ? (
105
+ <Text fontSize="sm" color="gray.400" mt={2}>
106
+ {deleteHint}
107
+ </Text>
108
+ ) : null}
109
+ </Dialog.Body>
110
+ <Dialog.Footer>
111
+ <Dialog.ActionTrigger asChild>
112
+ <Button
113
+ type="button"
114
+ variant="outline"
115
+ size="sm"
116
+ disabled={isDeleting}
117
+ >
118
+ Cancel
119
+ </Button>
120
+ </Dialog.ActionTrigger>
121
+ <Button
122
+ type="button"
123
+ colorPalette="red"
124
+ size="sm"
125
+ aria-label="Confirm delete"
126
+ onClick={onConfirmDelete}
127
+ loading={isDeleting}
128
+ >
129
+ Delete
130
+ </Button>
131
+ </Dialog.Footer>
132
+ </Dialog.Content>
133
+ </Dialog.Positioner>
134
+ </Portal>
135
+ </Dialog.Root>
136
+ </Flex>
137
+ );
138
+
139
+ export { useBulkSelection };
140
+ export type { BulkCheckboxState };
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import {
5
+ getBulkSelectionState,
6
+ pruneSelection,
7
+ removeSelectedIds,
8
+ sameIds,
9
+ setSelectionForId,
10
+ setSelectionForScope,
11
+ } from './utils';
12
+
13
+ interface UseBulkSelectionOptions {
14
+ scopeIds: readonly string[];
15
+ validIds?: readonly string[];
16
+ enabled?: boolean;
17
+ }
18
+
19
+ export const useBulkSelection = ({
20
+ scopeIds,
21
+ validIds = scopeIds,
22
+ enabled = true,
23
+ }: UseBulkSelectionOptions) => {
24
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
25
+
26
+ useEffect(() => {
27
+ setSelectedIds((prev) => {
28
+ if (!enabled) {
29
+ return prev.length === 0 ? prev : [];
30
+ }
31
+
32
+ const next = pruneSelection({ selectedIds: prev, validIds });
33
+ return sameIds(prev, next) ? prev : next;
34
+ });
35
+ }, [enabled, validIds]);
36
+
37
+ const state = useMemo(
38
+ () =>
39
+ getBulkSelectionState({
40
+ selectedIds,
41
+ scopeIds: enabled ? scopeIds : [],
42
+ }),
43
+ [enabled, scopeIds, selectedIds],
44
+ );
45
+
46
+ const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
47
+
48
+ const toggleSelection = useCallback(
49
+ (id: string, checked: boolean) => {
50
+ if (!enabled) {
51
+ return;
52
+ }
53
+
54
+ setSelectedIds((prev) =>
55
+ setSelectionForId({ selectedIds: prev, id, checked }),
56
+ );
57
+ },
58
+ [enabled],
59
+ );
60
+
61
+ const setScopeSelected = useCallback(
62
+ (checked: boolean) => {
63
+ if (!enabled) {
64
+ return;
65
+ }
66
+
67
+ setSelectedIds((prev) =>
68
+ setSelectionForScope({
69
+ selectedIds: prev,
70
+ scopeIds,
71
+ checked,
72
+ }),
73
+ );
74
+ },
75
+ [enabled, scopeIds],
76
+ );
77
+
78
+ const removeSelection = useCallback((idsToRemove: readonly string[]) => {
79
+ setSelectedIds((prev) =>
80
+ removeSelectedIds({ selectedIds: prev, idsToRemove }),
81
+ );
82
+ }, []);
83
+
84
+ const clearSelection = useCallback(() => {
85
+ setSelectedIds((prev) => (prev.length === 0 ? prev : []));
86
+ }, []);
87
+
88
+ return {
89
+ selectedIds,
90
+ selectedIdSet,
91
+ ...state,
92
+ toggleSelection,
93
+ setScopeSelected,
94
+ removeSelection,
95
+ clearSelection,
96
+ };
97
+ };
@@ -0,0 +1,126 @@
1
+ export type BulkCheckboxState = boolean | 'indeterminate';
2
+
3
+ export const uniqueIds = (ids: readonly string[]) => Array.from(new Set(ids));
4
+
5
+ export const sameIds = (left: readonly string[], right: readonly string[]) =>
6
+ left.length === right.length &&
7
+ left.every((value, index) => value === right[index]);
8
+
9
+ export const getScopedSelectedIds = ({
10
+ selectedIds,
11
+ scopeIds,
12
+ }: {
13
+ selectedIds: readonly string[];
14
+ scopeIds: readonly string[];
15
+ }) => {
16
+ const scopeSet = new Set(scopeIds);
17
+
18
+ return uniqueIds(selectedIds).filter((id) => scopeSet.has(id));
19
+ };
20
+
21
+ export const getBulkSelectionState = ({
22
+ selectedIds,
23
+ scopeIds,
24
+ }: {
25
+ selectedIds: readonly string[];
26
+ scopeIds: readonly string[];
27
+ }) => {
28
+ const uniqueScopeIds = uniqueIds(scopeIds);
29
+ const selectedScopedIds = getScopedSelectedIds({
30
+ selectedIds,
31
+ scopeIds: uniqueScopeIds,
32
+ });
33
+ const selectedScopedCount = selectedScopedIds.length;
34
+ const allScopedSelected =
35
+ uniqueScopeIds.length > 0 && selectedScopedCount === uniqueScopeIds.length;
36
+ const hasScopedSelection = selectedScopedCount > 0;
37
+ const checkboxState: BulkCheckboxState = allScopedSelected
38
+ ? true
39
+ : hasScopedSelection
40
+ ? 'indeterminate'
41
+ : false;
42
+
43
+ return {
44
+ selectedScopedIds,
45
+ selectedScopedCount,
46
+ allScopedSelected,
47
+ hasScopedSelection,
48
+ checkboxState,
49
+ };
50
+ };
51
+
52
+ export const setSelectionForId = ({
53
+ selectedIds,
54
+ id,
55
+ checked,
56
+ }: {
57
+ selectedIds: readonly string[];
58
+ id: string;
59
+ checked: boolean;
60
+ }) => {
61
+ const uniqueSelectedIds = uniqueIds(selectedIds);
62
+
63
+ if (!checked) {
64
+ return uniqueSelectedIds.filter((selectedId) => selectedId !== id);
65
+ }
66
+
67
+ if (uniqueSelectedIds.includes(id)) {
68
+ return uniqueSelectedIds;
69
+ }
70
+
71
+ return [...uniqueSelectedIds, id];
72
+ };
73
+
74
+ export const setSelectionForScope = ({
75
+ selectedIds,
76
+ scopeIds,
77
+ checked,
78
+ }: {
79
+ selectedIds: readonly string[];
80
+ scopeIds: readonly string[];
81
+ checked: boolean;
82
+ }) => {
83
+ const uniqueSelectedIds = uniqueIds(selectedIds);
84
+ const uniqueScopeIds = uniqueIds(scopeIds);
85
+ const scopeSet = new Set(uniqueScopeIds);
86
+
87
+ if (!checked) {
88
+ return uniqueSelectedIds.filter((id) => !scopeSet.has(id));
89
+ }
90
+
91
+ const nextSet = new Set(uniqueSelectedIds);
92
+ const nextIds = [...uniqueSelectedIds];
93
+
94
+ for (const id of uniqueScopeIds) {
95
+ if (!nextSet.has(id)) {
96
+ nextSet.add(id);
97
+ nextIds.push(id);
98
+ }
99
+ }
100
+
101
+ return nextIds;
102
+ };
103
+
104
+ export const removeSelectedIds = ({
105
+ selectedIds,
106
+ idsToRemove,
107
+ }: {
108
+ selectedIds: readonly string[];
109
+ idsToRemove: readonly string[];
110
+ }) => {
111
+ const removedSet = new Set(idsToRemove);
112
+
113
+ return uniqueIds(selectedIds).filter((id) => !removedSet.has(id));
114
+ };
115
+
116
+ export const pruneSelection = ({
117
+ selectedIds,
118
+ validIds,
119
+ }: {
120
+ selectedIds: readonly string[];
121
+ validIds: readonly string[];
122
+ }) => {
123
+ const validSet = new Set(validIds);
124
+
125
+ return uniqueIds(selectedIds).filter((id) => validSet.has(id));
126
+ };
@@ -2,13 +2,12 @@
2
2
 
3
3
  import _ from 'lodash';
4
4
  import { useEffect, useMemo, useState } from 'react';
5
- import { Select } from '@UI';
5
+ import { Select } from '#ui';
6
6
  import { useFiltersContext } from '../context';
7
7
 
8
8
  export const SelectBacktest = () => {
9
9
  const { filters, backtestFiles, onChangeFilters, ensureBacktestsLoaded } =
10
10
  useFiltersContext();
11
- const STORAGE_KEY = 'backtest-strategy';
12
11
 
13
12
  const tests = useMemo(
14
13
  () => backtestFiles.filter((file) => file.value.startsWith(filters.symbol)),
@@ -17,6 +16,9 @@ export const SelectBacktest = () => {
17
16
 
18
17
  const strategyItems = useMemo(() => {
19
18
  const names = new Set<string>();
19
+ if (filters.backtestStrategy) {
20
+ names.add(filters.backtestStrategy);
21
+ }
20
22
  for (const test of tests) {
21
23
  const strategyName = test.data?.strategyName;
22
24
  if (typeof strategyName === 'string' && strategyName) {
@@ -30,69 +32,56 @@ export const SelectBacktest = () => {
30
32
  label: strategyName,
31
33
  value: strategyName,
32
34
  }));
33
- }, [tests]);
35
+ }, [filters.backtestStrategy, tests]);
36
+
37
+ const [selectedStrategy, setSelectedStrategy] = useState<string>(
38
+ filters.backtestStrategy || '',
39
+ );
40
+ const selectedTestStrategy = useMemo(() => {
41
+ if (!filters.backtestId) return null;
34
42
 
35
- const [selectedStrategy, setSelectedStrategy] = useState<string>('');
43
+ const strategyName = tests.find((test) => test.value === filters.backtestId)
44
+ ?.data?.strategyName;
45
+
46
+ return typeof strategyName === 'string' && strategyName
47
+ ? strategyName
48
+ : null;
49
+ }, [filters.backtestId, tests]);
36
50
 
37
51
  useEffect(() => {
38
52
  if (_.isEmpty(strategyItems)) {
39
- setSelectedStrategy('');
53
+ setSelectedStrategy(filters.backtestStrategy || '');
40
54
  return;
41
55
  }
42
56
 
43
- if (
44
- filters.backtestStrategy &&
45
- strategyItems.some((item) => item.value === filters.backtestStrategy) &&
46
- selectedStrategy !== filters.backtestStrategy
47
- ) {
48
- setSelectedStrategy(filters.backtestStrategy);
49
- return;
50
- }
57
+ const requestedStrategy = filters.backtestStrategy ?? selectedTestStrategy;
51
58
 
52
59
  if (
53
- selectedStrategy &&
54
- strategyItems.some((item) => item.value === selectedStrategy)
60
+ requestedStrategy &&
61
+ strategyItems.some((item) => item.value === requestedStrategy) &&
62
+ selectedStrategy !== requestedStrategy
55
63
  ) {
64
+ setSelectedStrategy(requestedStrategy);
56
65
  return;
57
66
  }
58
67
 
59
- const saved =
60
- typeof window !== 'undefined'
61
- ? window.localStorage.getItem(STORAGE_KEY)
62
- : null;
63
-
64
- const defaultStrategy =
65
- saved && strategyItems.some((item) => item.value === saved)
66
- ? saved
67
- : strategyItems[0]?.value || '';
68
-
69
- if (defaultStrategy) {
70
- setSelectedStrategy(defaultStrategy);
68
+ if (!requestedStrategy && selectedStrategy) {
69
+ setSelectedStrategy('');
70
+ return;
71
71
  }
72
- }, [strategyItems, filters.backtestStrategy, selectedStrategy]);
73
-
74
- useEffect(() => {
75
- if (!filters.backtestId) return;
76
-
77
- const selectedTest = tests.find(
78
- (test) => test.value === filters.backtestId,
79
- );
80
- const strategyName = selectedTest?.data?.strategyName;
81
72
 
82
73
  if (
83
- typeof strategyName === 'string' &&
84
- strategyName &&
85
- selectedStrategy !== strategyName
74
+ selectedStrategy &&
75
+ !strategyItems.some((item) => item.value === selectedStrategy)
86
76
  ) {
87
- setSelectedStrategy(strategyName);
88
- }
89
- }, [filters.backtestId, tests, selectedStrategy]);
90
-
91
- useEffect(() => {
92
- if (selectedStrategy && typeof window !== 'undefined') {
93
- window.localStorage.setItem(STORAGE_KEY, selectedStrategy);
77
+ setSelectedStrategy('');
94
78
  }
95
- }, [selectedStrategy]);
79
+ }, [
80
+ filters.backtestStrategy,
81
+ selectedStrategy,
82
+ selectedTestStrategy,
83
+ strategyItems,
84
+ ]);
96
85
 
97
86
  const onChange = (value: string[]) => {
98
87
  onChangeFilters?.({
@@ -103,9 +92,6 @@ export const SelectBacktest = () => {
103
92
  const onChangeStrategy = (value: string[]) => {
104
93
  const nextStrategy = value[0] || '';
105
94
  setSelectedStrategy(nextStrategy);
106
- if (nextStrategy && typeof window !== 'undefined') {
107
- window.localStorage.setItem(STORAGE_KEY, nextStrategy);
108
- }
109
95
 
110
96
  const hasSelectedTest =
111
97
  filters.backtestId &&
@@ -131,11 +117,24 @@ export const SelectBacktest = () => {
131
117
  const strategyTests = tests.filter(
132
118
  (test) => test.data?.strategyName === selectedStrategy,
133
119
  );
120
+ const backtestItems = [...strategyTests];
121
+ if (
122
+ filters.backtestId &&
123
+ selectedStrategy &&
124
+ !backtestItems.some((test) => test.value === filters.backtestId)
125
+ ) {
126
+ backtestItems.unshift({
127
+ label: filters.backtestId,
128
+ value: filters.backtestId,
129
+ data: { strategyName: selectedStrategy },
130
+ });
131
+ }
134
132
 
135
133
  return (
136
134
  <>
137
135
  <Select
138
136
  placeholder="Strategy"
137
+ emptyState="No strategies for this symbol"
139
138
  defaultValue={[selectedStrategy]}
140
139
  value={[selectedStrategy]}
141
140
  onChange={onChangeStrategy}
@@ -162,8 +161,9 @@ export const SelectBacktest = () => {
162
161
  label: 'Not selected',
163
162
  value: '',
164
163
  },
165
- ...strategyTests,
164
+ ...backtestItems,
166
165
  ]}
166
+ disabled={!selectedStrategy}
167
167
  width="240px"
168
168
  />
169
169
  </>
@@ -1,12 +1,14 @@
1
1
  import { useFiltersContext } from '../context';
2
- import { FavoriteButton } from '@shared/FavoriteButton';
3
- import { useTickers } from '@store';
2
+ import { FavoriteButton } from '#shared/FavoriteButton';
3
+ import { useTickers } from '#store';
4
4
 
5
5
  export const FavoriteIndicator = () => {
6
6
  const {
7
7
  filters: { symbol, provider },
8
8
  } = useFiltersContext();
9
- const { checkIsFavorite, toggleFavorite } = useTickers(provider || 'bybit');
9
+ const { checkIsFavorite, toggleFavorite } = useTickers(provider || 'bybit', {
10
+ enabled: false,
11
+ });
10
12
  const isFavorite = checkIsFavorite(symbol);
11
13
 
12
14
  return (
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import _ from 'lodash';
4
- import { useIndicators } from '@store';
5
- import { Select } from '@UI';
4
+ import { useIndicators } from '#store';
5
+ import { Select } from '#ui';
6
6
 
7
7
  export const SelectIndicator = () => {
8
8
  const { selectedIndicators, indicatorsItems, setEnabledIndicators } =
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Segment } from '@UI';
3
+ import { Segment } from '#ui';
4
4
  import { Interval } from '@tradejs/types';
5
5
  import { intervals } from './intervals';
6
6
  import { useFiltersContext } from '../context';
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { Select } from '@UI';
3
+ import { Select } from '#ui';
4
+ import { getDefaultMarketSymbol } from '#app/lib/marketDefaults';
4
5
  import { useFiltersContext } from '../context';
5
6
 
6
7
  const items = [
@@ -14,8 +15,13 @@ export const SelectProvider = () => {
14
15
 
15
16
  const onChange = (value: string[]) => {
16
17
  if (!value[0]) return;
18
+ const provider = value[0] as 'bybit' | 'binance' | 'coinbase';
19
+ const universe =
20
+ provider === 'bybit' ? filters.universe ?? 'crypto' : 'crypto';
17
21
  onChangeFilters?.({
18
- provider: value[0] as 'bybit' | 'binance' | 'coinbase',
22
+ provider,
23
+ universe,
24
+ symbol: getDefaultMarketSymbol(provider, universe),
19
25
  backtestId: null,
20
26
  backtestStrategy: null,
21
27
  });
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import _ from 'lodash';
4
- import { SelectWithSearch } from '@UI';
4
+ import { SelectWithSearch } from '#ui';
5
5
  import { useFiltersContext } from '../context';
6
6
 
7
7
  interface SelectSymbolProps {}
@@ -11,7 +11,7 @@ export const SelectSymbol = ({}: SelectSymbolProps) => {
11
11
  useFiltersContext();
12
12
  const defaultInputValue =
13
13
  tickers.find(({ value }) => value === filters.symbol)?.label ||
14
- filters.symbol;
14
+ filters.symbol.replace(/USDT$/i, '');
15
15
 
16
16
  const onChange = (value: string[]) => {
17
17
  if (_.isEmpty(value)) {
@@ -29,12 +29,13 @@ export const SelectSymbol = ({}: SelectSymbolProps) => {
29
29
 
30
30
  return (
31
31
  <SelectWithSearch
32
+ key={`${filters.provider}:${filters.universe}:${filters.symbol}`}
32
33
  defaultValue={[filters.symbol]}
33
34
  defaultInputValue={defaultInputValue}
34
35
  onChange={onChange}
35
36
  onOpenChange={(open) => {
36
37
  if (open) {
37
- void ensureTickersLoaded?.();
38
+ void Promise.resolve(ensureTickersLoaded?.()).catch(() => undefined);
38
39
  }
39
40
  }}
40
41
  items={tickers}
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { Select } from '#ui';
4
+ import type { MarketUniverse } from '@tradejs/types';
5
+ import { getDefaultMarketSymbol } from '#app/lib/marketDefaults';
6
+ import { useFiltersContext } from '../context';
7
+
8
+ export const SelectUniverse = () => {
9
+ const { filters, onChangeFilters } = useFiltersContext();
10
+ const items =
11
+ filters.provider === 'bybit'
12
+ ? [
13
+ { value: 'crypto', label: 'Crypto' },
14
+ { value: 'tradfi', label: 'TradFi' },
15
+ ]
16
+ : [{ value: 'crypto', label: 'Crypto' }];
17
+
18
+ return (
19
+ <Select
20
+ key={`${filters.provider}:${filters.universe}`}
21
+ defaultValue={[filters.universe ?? 'crypto']}
22
+ onChange={(value: string[]) => {
23
+ if (!value[0]) return;
24
+ const universe = value[0] as MarketUniverse;
25
+ onChangeFilters?.({
26
+ universe,
27
+ symbol: getDefaultMarketSymbol(filters.provider ?? 'bybit', universe),
28
+ backtestId: null,
29
+ backtestStrategy: null,
30
+ });
31
+ }}
32
+ items={items}
33
+ width="120px"
34
+ />
35
+ );
36
+ };
@@ -5,10 +5,12 @@ import { SelectSymbol } from './Symbol';
5
5
  import { SelectBacktest } from './Backtest';
6
6
  import { SelectIndicator } from './Indicators';
7
7
  import { FavoriteIndicator } from './FavoriteIndicator';
8
+ import { SelectUniverse } from './Universe';
8
9
 
9
10
  export const Filters = {
10
11
  Root,
11
12
  SelectProvider,
13
+ SelectUniverse,
12
14
  SelectInterval,
13
15
  SelectSymbol,
14
16
  SelectBacktest,