@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.
- package/README.md +7 -2
- 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/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/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 +1 -1
- 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 +16 -6
- package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
- package/src/app/components/Shared/AppShell.tsx +1 -1
- 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 +33 -48
- 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 +792 -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/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 +1051 -304
- package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
- 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/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 +1 -1
- package/src/app/store/tickers.ts +59 -22
- 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)),
|
|
@@ -33,6 +32,16 @@ export const SelectBacktest = () => {
|
|
|
33
32
|
}, [tests]);
|
|
34
33
|
|
|
35
34
|
const [selectedStrategy, setSelectedStrategy] = useState<string>('');
|
|
35
|
+
const selectedTestStrategy = useMemo(() => {
|
|
36
|
+
if (!filters.backtestId) return null;
|
|
37
|
+
|
|
38
|
+
const strategyName = tests.find((test) => test.value === filters.backtestId)
|
|
39
|
+
?.data?.strategyName;
|
|
40
|
+
|
|
41
|
+
return typeof strategyName === 'string' && strategyName
|
|
42
|
+
? strategyName
|
|
43
|
+
: null;
|
|
44
|
+
}, [filters.backtestId, tests]);
|
|
36
45
|
|
|
37
46
|
useEffect(() => {
|
|
38
47
|
if (_.isEmpty(strategyItems)) {
|
|
@@ -40,59 +49,34 @@ export const SelectBacktest = () => {
|
|
|
40
49
|
return;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
filters.backtestStrategy &&
|
|
45
|
-
strategyItems.some((item) => item.value === filters.backtestStrategy) &&
|
|
46
|
-
selectedStrategy !== filters.backtestStrategy
|
|
47
|
-
) {
|
|
48
|
-
setSelectedStrategy(filters.backtestStrategy);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
52
|
+
const requestedStrategy = filters.backtestStrategy ?? selectedTestStrategy;
|
|
51
53
|
|
|
52
54
|
if (
|
|
53
|
-
|
|
54
|
-
strategyItems.some((item) => item.value ===
|
|
55
|
+
requestedStrategy &&
|
|
56
|
+
strategyItems.some((item) => item.value === requestedStrategy) &&
|
|
57
|
+
selectedStrategy !== requestedStrategy
|
|
55
58
|
) {
|
|
59
|
+
setSelectedStrategy(requestedStrategy);
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
|
|
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);
|
|
63
|
+
if (!requestedStrategy && selectedStrategy) {
|
|
64
|
+
setSelectedStrategy('');
|
|
65
|
+
return;
|
|
71
66
|
}
|
|
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
67
|
|
|
82
68
|
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
selectedStrategy !== strategyName
|
|
69
|
+
selectedStrategy &&
|
|
70
|
+
!strategyItems.some((item) => item.value === selectedStrategy)
|
|
86
71
|
) {
|
|
87
|
-
setSelectedStrategy(
|
|
88
|
-
}
|
|
89
|
-
}, [filters.backtestId, tests, selectedStrategy]);
|
|
90
|
-
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (selectedStrategy && typeof window !== 'undefined') {
|
|
93
|
-
window.localStorage.setItem(STORAGE_KEY, selectedStrategy);
|
|
72
|
+
setSelectedStrategy('');
|
|
94
73
|
}
|
|
95
|
-
}, [
|
|
74
|
+
}, [
|
|
75
|
+
filters.backtestStrategy,
|
|
76
|
+
selectedStrategy,
|
|
77
|
+
selectedTestStrategy,
|
|
78
|
+
strategyItems,
|
|
79
|
+
]);
|
|
96
80
|
|
|
97
81
|
const onChange = (value: string[]) => {
|
|
98
82
|
onChangeFilters?.({
|
|
@@ -103,9 +87,6 @@ export const SelectBacktest = () => {
|
|
|
103
87
|
const onChangeStrategy = (value: string[]) => {
|
|
104
88
|
const nextStrategy = value[0] || '';
|
|
105
89
|
setSelectedStrategy(nextStrategy);
|
|
106
|
-
if (nextStrategy && typeof window !== 'undefined') {
|
|
107
|
-
window.localStorage.setItem(STORAGE_KEY, nextStrategy);
|
|
108
|
-
}
|
|
109
90
|
|
|
110
91
|
const hasSelectedTest =
|
|
111
92
|
filters.backtestId &&
|
|
@@ -131,11 +112,13 @@ export const SelectBacktest = () => {
|
|
|
131
112
|
const strategyTests = tests.filter(
|
|
132
113
|
(test) => test.data?.strategyName === selectedStrategy,
|
|
133
114
|
);
|
|
115
|
+
const hasStrategyItems = !_.isEmpty(strategyItems);
|
|
134
116
|
|
|
135
117
|
return (
|
|
136
118
|
<>
|
|
137
119
|
<Select
|
|
138
|
-
placeholder=
|
|
120
|
+
placeholder={hasStrategyItems ? 'Strategy' : 'No strategies'}
|
|
121
|
+
emptyState="No strategies for this symbol"
|
|
139
122
|
defaultValue={[selectedStrategy]}
|
|
140
123
|
value={[selectedStrategy]}
|
|
141
124
|
onChange={onChangeStrategy}
|
|
@@ -145,6 +128,7 @@ export const SelectBacktest = () => {
|
|
|
145
128
|
}
|
|
146
129
|
}}
|
|
147
130
|
items={strategyItems}
|
|
131
|
+
disabled={!hasStrategyItems}
|
|
148
132
|
width="220px"
|
|
149
133
|
/>
|
|
150
134
|
<Select
|
|
@@ -164,6 +148,7 @@ export const SelectBacktest = () => {
|
|
|
164
148
|
},
|
|
165
149
|
...strategyTests,
|
|
166
150
|
]}
|
|
151
|
+
disabled={!selectedStrategy}
|
|
167
152
|
width="240px"
|
|
168
153
|
/>
|
|
169
154
|
</>
|
|
@@ -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,
|