@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.
- package/README.md +16 -3
- package/bin/tradejs-app.mjs +146 -2
- package/next.config.mjs +0 -14
- package/package.json +35 -19
- package/src/app/actions/backtest.ts +54 -0
- package/src/app/actions/kline.ts +10 -5
- package/src/app/actions/scanner.ts +8 -3
- package/src/app/actions/strategies.ts +39 -0
- package/src/app/api/ai/route.ts +5 -4
- package/src/app/api/auth/[...nextauth]/route.ts +1 -1
- package/src/app/api/backtest/configs/route.ts +25 -0
- package/src/app/api/backtest/files/route.ts +5 -2
- package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
- package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
- package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
- package/src/app/api/backtest/runs/route.ts +45 -0
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
- package/src/app/api/derivatives/summary/route.ts +8 -1
- package/src/app/api/install/route.ts +51 -0
- package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
- package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
- package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
- package/src/app/api/scanner/[provider]/route.ts +30 -12
- package/src/app/api/scanner/route.ts +3 -3
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
- package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
- package/src/app/api/strategies/ai/route.ts +139 -0
- package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
- package/src/app/api/strategies/replay/route.ts +44 -0
- package/src/app/api/strategies/runtime/route.ts +831 -0
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
- package/src/app/api/user/runtime-deployments/route.ts +94 -0
- package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
- package/src/app/api/user/settings/route.ts +18 -1
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
- package/src/app/api/user/trading-accounts/route.ts +134 -0
- package/src/app/auth.ts +1 -19
- package/src/app/components/Backtest/CompareList/index.tsx +1 -1
- package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
- package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
- package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
- package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
- package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
- package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
- package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
- package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
- package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
- package/src/app/components/Backtest/TestCard/Root/index.tsx +18 -4
- package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
- package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
- package/src/app/components/Backtest/TestCard/index.ts +6 -2
- package/src/app/components/Backtest/TestList/index.tsx +4 -9
- package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
- package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
- package/src/app/components/Dashboard/KlineChart/index.tsx +21 -7
- package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
- package/src/app/components/Shared/AppShell.tsx +2 -2
- package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
- package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
- package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
- package/src/app/components/Shared/Filters/Backtest/index.tsx +51 -51
- package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
- package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
- package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
- package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
- package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
- package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
- package/src/app/components/Shared/Filters/index.ts +2 -0
- package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
- package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
- package/src/app/components/Shared/Sidebar/index.tsx +17 -3
- package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
- package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
- package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
- package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
- package/src/app/components/UI/Segment/index.tsx +8 -1
- package/src/app/components/UI/Select/index.tsx +24 -13
- package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
- package/src/app/layout.tsx +1 -1
- package/src/app/lib/backtestJobs.ts +925 -0
- package/src/app/lib/cardPageLayout.ts +1 -0
- package/src/app/lib/connectorCreator.ts +28 -0
- package/src/app/lib/currentUser.ts +1 -1
- package/src/app/lib/errorMessage.ts +34 -0
- package/src/app/lib/installation.ts +83 -0
- package/src/app/lib/marketDefaults.ts +12 -0
- package/src/app/lib/marketRoutes.ts +58 -0
- package/src/app/lib/runtimeStrategies.ts +1376 -0
- package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
- package/src/app/provider.tsx +1 -1
- package/src/app/routes/backtest/page.tsx +1100 -302
- package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +76 -35
- package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
- package/src/app/routes/dashboard/page.tsx +15 -3
- package/src/app/routes/derivatives/page.tsx +1222 -165
- package/src/app/routes/install/page.tsx +175 -0
- package/src/app/routes/signin/page.tsx +7 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
- package/src/app/routes/strategies/[mode]/page.tsx +20 -0
- package/src/app/routes/strategies/page.tsx +7 -0
- package/src/app/store/ai.ts +1 -1
- package/src/app/store/data.ts +73 -17
- package/src/app/store/filters.ts +1 -0
- package/src/app/store/marketKlineStream.ts +87 -0
- package/src/app/store/tests.ts +83 -45
- package/src/app/store/tickers.ts +59 -22
- package/src/proxy.ts +4 -0
- package/tsconfig.json +17 -12
- package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
- 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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
strategyItems.some((item) => item.value ===
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
selectedStrategy !== strategyName
|
|
74
|
+
selectedStrategy &&
|
|
75
|
+
!strategyItems.some((item) => item.value === selectedStrategy)
|
|
86
76
|
) {
|
|
87
|
-
setSelectedStrategy(
|
|
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
|
-
}, [
|
|
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
|
-
...
|
|
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 '
|
|
3
|
-
import { useTickers } from '
|
|
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 '
|
|
5
|
-
import { Select } from '
|
|
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,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Select } from '
|
|
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
|
|
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 '
|
|
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,
|