@tradejs/app 1.0.6 → 1.0.9
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/bin/tradejs-app.mjs +7 -0
- package/package.json +12 -12
- package/src/app/api/ai/route.ts +63 -20
- package/src/app/api/backtest/files/route.ts +12 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +18 -1
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +350 -28
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +9 -6
- package/src/app/api/user/settings/route.ts +51 -23
- package/src/app/components/Dashboard/AiDrawer/index.tsx +38 -51
- package/src/app/components/Shared/Filters/Backtest/index.tsx +12 -5
- package/src/app/components/Shared/Filters/Root/index.tsx +12 -1
- package/src/app/components/Shared/Filters/Symbol/index.tsx +14 -19
- package/src/app/components/Shared/Filters/context.ts +2 -0
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +250 -111
- package/src/app/components/UI/ColorMode/index.tsx +62 -15
- package/src/app/components/UI/Select/index.tsx +3 -0
- package/src/app/components/UI/SelectWithSearch/index.tsx +3 -0
- package/src/app/layout.tsx +10 -8
- package/src/app/lib/klineWindow.ts +17 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
- package/src/app/store/ai.ts +174 -0
- package/src/app/store/data.ts +34 -8
- package/src/app/store/index.ts +1 -0
- package/src/app/store/tests.ts +96 -9
- package/src/app/store/tickers.ts +113 -17
- package/src/proxy.ts +23 -50
package/src/app/layout.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
-
import { ClientOnly } from '@chakra-ui/react';
|
|
3
2
|
import { AppShell } from '@shared/AppShell';
|
|
4
3
|
import Provider from './provider';
|
|
5
4
|
import './globals.css';
|
|
@@ -55,13 +54,16 @@ export default function RootLayout({
|
|
|
55
54
|
children: React.ReactNode;
|
|
56
55
|
}>) {
|
|
57
56
|
return (
|
|
58
|
-
<html
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
<html
|
|
58
|
+
lang="en"
|
|
59
|
+
className="dark"
|
|
60
|
+
style={{ colorScheme: 'dark' }}
|
|
61
|
+
suppressHydrationWarning
|
|
62
|
+
>
|
|
63
|
+
<body suppressHydrationWarning>
|
|
64
|
+
<Provider>
|
|
65
|
+
<AppShell>{children}</AppShell>
|
|
66
|
+
</Provider>
|
|
65
67
|
</body>
|
|
66
68
|
</html>
|
|
67
69
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { intervalToMs } from '@tradejs/core/data';
|
|
2
|
+
import { Interval } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
export const normalizeEndToIntervalBoundary = (
|
|
5
|
+
end: number,
|
|
6
|
+
interval: Interval,
|
|
7
|
+
): number => {
|
|
8
|
+
const stepMs = intervalToMs(interval);
|
|
9
|
+
if (!Number.isFinite(end) || stepMs <= 0) {
|
|
10
|
+
return end;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Math.floor(end / stepMs) * stepMs;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getCurrentIntervalBoundary = (interval: Interval) =>
|
|
17
|
+
normalizeEndToIntervalBoundary(Date.now(), interval);
|
|
@@ -11,8 +11,14 @@ import { Interval, OnChangeFilters, Provider } from '@tradejs/types';
|
|
|
11
11
|
const Dashboard = () => {
|
|
12
12
|
const searchParams = useSearchParams();
|
|
13
13
|
const { filters, setFilters } = useFilters();
|
|
14
|
-
const { tickers } = useTickers(
|
|
15
|
-
|
|
14
|
+
const { tickers, ensureLoaded: ensureTickersLoaded } = useTickers(
|
|
15
|
+
filters.provider || 'bybit',
|
|
16
|
+
{ enabled: false },
|
|
17
|
+
);
|
|
18
|
+
const { tests, ensureLoaded: ensureBacktestsLoaded } = useTestList({
|
|
19
|
+
symbol: filters.symbol,
|
|
20
|
+
enabled: false,
|
|
21
|
+
});
|
|
16
22
|
const hasBacktestId = searchParams.has('backtestId');
|
|
17
23
|
const hasBacktestStrategy = searchParams.has('backtestStrategy');
|
|
18
24
|
const backtestId = searchParams.get('backtestId');
|
|
@@ -103,6 +109,8 @@ const Dashboard = () => {
|
|
|
103
109
|
tickers={tickers}
|
|
104
110
|
backtestFiles={tests}
|
|
105
111
|
onChangeFilters={onChangeFilters}
|
|
112
|
+
ensureTickersLoaded={ensureTickersLoaded}
|
|
113
|
+
ensureBacktestsLoaded={ensureBacktestsLoaded}
|
|
106
114
|
>
|
|
107
115
|
<Flex mb={2} gap={4} alignItems="center" flexDirection="row">
|
|
108
116
|
<Filters.SelectProvider />
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { getHistory, sendMessage } from '@actions/ai';
|
|
3
|
+
import { AIChatHistory, AIChatMessage, Filters } from '@tradejs/types';
|
|
4
|
+
|
|
5
|
+
type AiChatEntry = {
|
|
6
|
+
loading: boolean;
|
|
7
|
+
sending: boolean;
|
|
8
|
+
loaded: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
messages: AIChatHistory;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface AiChatState {
|
|
14
|
+
chats: Record<string, AiChatEntry>;
|
|
15
|
+
getChat: (symbol: string) => AiChatEntry;
|
|
16
|
+
loadHistory: (symbol: string) => Promise<void>;
|
|
17
|
+
sendPrompt: (filters: Filters, input: string) => Promise<void>;
|
|
18
|
+
sendQuickCommand: (filters: Filters, command: string) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EMPTY_CHAT: AiChatEntry = {
|
|
22
|
+
loading: false,
|
|
23
|
+
sending: false,
|
|
24
|
+
loaded: false,
|
|
25
|
+
error: null,
|
|
26
|
+
messages: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const normalizeSymbolKey = (symbol: string) => symbol.trim().toUpperCase();
|
|
30
|
+
|
|
31
|
+
const getQuickMessage = (command: string): AIChatMessage | null => {
|
|
32
|
+
if (command === '/line') {
|
|
33
|
+
return {
|
|
34
|
+
from: 'user',
|
|
35
|
+
text: 'Какие наклонные линии можно построить на данном графике',
|
|
36
|
+
command,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const updateChat = (
|
|
44
|
+
chats: Record<string, AiChatEntry>,
|
|
45
|
+
symbol: string,
|
|
46
|
+
patch: Partial<AiChatEntry>,
|
|
47
|
+
) => ({
|
|
48
|
+
...chats,
|
|
49
|
+
[symbol]: {
|
|
50
|
+
...(chats[symbol] ?? EMPTY_CHAT),
|
|
51
|
+
...patch,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const useAiChatStore = create<AiChatState>((set, get) => ({
|
|
56
|
+
chats: {},
|
|
57
|
+
|
|
58
|
+
getChat: (symbol) => get().chats[normalizeSymbolKey(symbol)] ?? EMPTY_CHAT,
|
|
59
|
+
|
|
60
|
+
loadHistory: async (symbol) => {
|
|
61
|
+
const symbolKey = normalizeSymbolKey(symbol);
|
|
62
|
+
if (!symbolKey) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const existing = get().chats[symbolKey];
|
|
67
|
+
if (existing?.loading) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set((state) => ({
|
|
72
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
73
|
+
loading: true,
|
|
74
|
+
error: null,
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const history = await getHistory(symbol);
|
|
80
|
+
set((state) => ({
|
|
81
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
82
|
+
loading: false,
|
|
83
|
+
loaded: true,
|
|
84
|
+
messages: history,
|
|
85
|
+
}),
|
|
86
|
+
}));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
set((state) => ({
|
|
89
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
90
|
+
loading: false,
|
|
91
|
+
error: error instanceof Error ? error.message : 'Failed to load chat',
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
sendPrompt: async (filters, input) => {
|
|
98
|
+
const trimmed = input.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const message: AIChatMessage = {
|
|
104
|
+
from: 'user',
|
|
105
|
+
text: trimmed,
|
|
106
|
+
command: 'prompt',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const symbolKey = normalizeSymbolKey(filters.symbol);
|
|
110
|
+
|
|
111
|
+
set((state) => ({
|
|
112
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
113
|
+
sending: true,
|
|
114
|
+
error: null,
|
|
115
|
+
loaded: true,
|
|
116
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), message],
|
|
117
|
+
}),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await sendMessage({ message, filters });
|
|
122
|
+
set((state) => ({
|
|
123
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
124
|
+
sending: false,
|
|
125
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), response],
|
|
126
|
+
}),
|
|
127
|
+
}));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
set((state) => ({
|
|
130
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
131
|
+
sending: false,
|
|
132
|
+
error:
|
|
133
|
+
error instanceof Error ? error.message : 'Failed to send message',
|
|
134
|
+
}),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
sendQuickCommand: async (filters, command) => {
|
|
140
|
+
const message = getQuickMessage(command);
|
|
141
|
+
if (!message) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const symbolKey = normalizeSymbolKey(filters.symbol);
|
|
146
|
+
|
|
147
|
+
set((state) => ({
|
|
148
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
149
|
+
sending: true,
|
|
150
|
+
error: null,
|
|
151
|
+
loaded: true,
|
|
152
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), message],
|
|
153
|
+
}),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await sendMessage({ message, filters });
|
|
158
|
+
set((state) => ({
|
|
159
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
160
|
+
sending: false,
|
|
161
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), response],
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
set((state) => ({
|
|
166
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
167
|
+
sending: false,
|
|
168
|
+
error:
|
|
169
|
+
error instanceof Error ? error.message : 'Failed to send message',
|
|
170
|
+
}),
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}));
|
package/src/app/store/data.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
|
|
|
5
5
|
import { KlineChartData, Interval, Filters, Provider } from '@tradejs/types';
|
|
6
6
|
import { kline } from '@actions/kline';
|
|
7
7
|
import { isWrongData, mergeData } from '@tradejs/core/data';
|
|
8
|
+
import { normalizeEndToIntervalBoundary } from '@app/lib/klineWindow';
|
|
8
9
|
|
|
9
10
|
interface DataState {
|
|
10
11
|
data: Map<string, KlineChartData | null>;
|
|
@@ -23,6 +24,7 @@ interface DataRequest {
|
|
|
23
24
|
interval: Interval;
|
|
24
25
|
start: number;
|
|
25
26
|
end: number;
|
|
27
|
+
cacheBucketEnd: number;
|
|
26
28
|
cacheOnly: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -43,12 +45,28 @@ const toRequest = (filters: Filters, cacheOnly: boolean): DataRequest => {
|
|
|
43
45
|
interval: filters.interval,
|
|
44
46
|
start: filters.start,
|
|
45
47
|
end: filters.end,
|
|
48
|
+
cacheBucketEnd: normalizeEndToIntervalBoundary(
|
|
49
|
+
filters.end,
|
|
50
|
+
filters.interval,
|
|
51
|
+
),
|
|
46
52
|
cacheOnly,
|
|
47
53
|
};
|
|
48
54
|
};
|
|
49
55
|
|
|
50
|
-
const getRequestKey = ({
|
|
51
|
-
|
|
56
|
+
const getRequestKey = ({
|
|
57
|
+
key,
|
|
58
|
+
start,
|
|
59
|
+
cacheBucketEnd,
|
|
60
|
+
cacheOnly,
|
|
61
|
+
}: DataRequest) =>
|
|
62
|
+
`${key}_${start}_${cacheBucketEnd}_${cacheOnly ? 'cache' : 'live'}`;
|
|
63
|
+
|
|
64
|
+
const filterDataToWindow = (
|
|
65
|
+
data: KlineChartData,
|
|
66
|
+
start: number,
|
|
67
|
+
end: number,
|
|
68
|
+
): KlineChartData =>
|
|
69
|
+
data.filter((candle) => candle.timestamp >= start && candle.timestamp <= end);
|
|
52
70
|
|
|
53
71
|
const hasContinuityData = (data: KlineChartData) =>
|
|
54
72
|
data.length > MIN_CACHED_CANDLES;
|
|
@@ -211,10 +229,18 @@ export const useData = (filters: Filters) => {
|
|
|
211
229
|
),
|
|
212
230
|
[cacheOnly, end, interval, provider, start, symbol],
|
|
213
231
|
);
|
|
214
|
-
const
|
|
232
|
+
const requestKey = useMemo(() => getRequestKey(dataRequest), [dataRequest]);
|
|
233
|
+
const [fulfilledRequestKey, setFulfilledRequestKey] = useState<string | null>(
|
|
234
|
+
null,
|
|
235
|
+
);
|
|
215
236
|
const storedData = useDataStore((s) => s.data.get(dataRequest.key));
|
|
237
|
+
const windowedData = useMemo(
|
|
238
|
+
() =>
|
|
239
|
+
filterDataToWindow(storedData ?? [], dataRequest.start, dataRequest.end),
|
|
240
|
+
[dataRequest.end, dataRequest.start, storedData],
|
|
241
|
+
);
|
|
216
242
|
|
|
217
|
-
const fulfilled =
|
|
243
|
+
const fulfilled = fulfilledRequestKey === requestKey;
|
|
218
244
|
|
|
219
245
|
useEffect(() => {
|
|
220
246
|
let cancelled = false;
|
|
@@ -222,7 +248,7 @@ export const useData = (filters: Filters) => {
|
|
|
222
248
|
const updateData = async () => {
|
|
223
249
|
if (!dataRequest.symbol) {
|
|
224
250
|
if (!cancelled) {
|
|
225
|
-
|
|
251
|
+
setFulfilledRequestKey(requestKey);
|
|
226
252
|
}
|
|
227
253
|
return;
|
|
228
254
|
}
|
|
@@ -230,7 +256,7 @@ export const useData = (filters: Filters) => {
|
|
|
230
256
|
await fetchAndStoreData(dataRequest);
|
|
231
257
|
|
|
232
258
|
if (!cancelled) {
|
|
233
|
-
|
|
259
|
+
setFulfilledRequestKey(requestKey);
|
|
234
260
|
}
|
|
235
261
|
};
|
|
236
262
|
|
|
@@ -239,11 +265,11 @@ export const useData = (filters: Filters) => {
|
|
|
239
265
|
return () => {
|
|
240
266
|
cancelled = true;
|
|
241
267
|
};
|
|
242
|
-
}, [dataRequest]);
|
|
268
|
+
}, [dataRequest, requestKey]);
|
|
243
269
|
|
|
244
270
|
return {
|
|
245
271
|
key: dataRequest.key,
|
|
246
|
-
data:
|
|
272
|
+
data: windowedData,
|
|
247
273
|
fulfilled,
|
|
248
274
|
};
|
|
249
275
|
};
|
package/src/app/store/index.ts
CHANGED
package/src/app/store/tests.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { parseTestName } from '@tradejs/core/backtest';
|
|
|
16
16
|
|
|
17
17
|
const COMPARE_LOCAL_STORAGE_KEY = 'compare';
|
|
18
18
|
const FAVORITE_LOCAL_STORAGE_KEY = 'favorite';
|
|
19
|
+
const BACKTEST_FILES_CACHE_KEY = 'backtest-files';
|
|
20
|
+
const BACKTEST_FILES_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
19
21
|
|
|
20
22
|
const COLORS = [
|
|
21
23
|
'purple',
|
|
@@ -28,6 +30,14 @@ const COLORS = [
|
|
|
28
30
|
'green',
|
|
29
31
|
];
|
|
30
32
|
|
|
33
|
+
type BacktestFilesCacheRecord = {
|
|
34
|
+
savedAt: number;
|
|
35
|
+
items: Items;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isFresh = (savedAt: number) =>
|
|
39
|
+
Date.now() - savedAt < BACKTEST_FILES_CACHE_TTL_MS;
|
|
40
|
+
|
|
31
41
|
interface BacktestState {
|
|
32
42
|
backtests: Map<string, OrderLogData | null>;
|
|
33
43
|
setBacktest: (id: string, backtest: OrderLogData) => void;
|
|
@@ -100,15 +110,26 @@ const useFavoriteTetstsStore = create<FavotiteTestsState>()(
|
|
|
100
110
|
|
|
101
111
|
interface TestListState {
|
|
102
112
|
tests: Items;
|
|
103
|
-
|
|
113
|
+
loadedAt: number;
|
|
114
|
+
inFlight?: Promise<Items>;
|
|
115
|
+
setTest: (tests: Items, loadedAt?: number) => void;
|
|
116
|
+
setInFlight: (request?: Promise<Items>) => void;
|
|
104
117
|
removeTest: (testName: string) => void;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
const useTestListStore = create<TestListState>((set) => ({
|
|
108
121
|
tests: [],
|
|
109
|
-
|
|
122
|
+
loadedAt: 0,
|
|
123
|
+
inFlight: undefined,
|
|
124
|
+
setTest: (tests, loadedAt = Date.now()) =>
|
|
110
125
|
set(() => ({
|
|
111
126
|
tests,
|
|
127
|
+
loadedAt,
|
|
128
|
+
inFlight: undefined,
|
|
129
|
+
})),
|
|
130
|
+
setInFlight: (request) =>
|
|
131
|
+
set(() => ({
|
|
132
|
+
inFlight: request,
|
|
112
133
|
})),
|
|
113
134
|
removeTest: (testName) =>
|
|
114
135
|
set(({ tests }) => ({
|
|
@@ -184,6 +205,44 @@ const useTestsStore = create<TestsState>((set) => ({
|
|
|
184
205
|
}),
|
|
185
206
|
}));
|
|
186
207
|
|
|
208
|
+
const loadBacktestFilesList = async () => {
|
|
209
|
+
const { tests, loadedAt, inFlight, setInFlight, setTest } =
|
|
210
|
+
useTestListStore.getState();
|
|
211
|
+
|
|
212
|
+
if (loadedAt > 0 && isFresh(loadedAt)) {
|
|
213
|
+
return tests;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (inFlight) {
|
|
217
|
+
return inFlight;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pending = (async () => {
|
|
221
|
+
const cached = (await get(
|
|
222
|
+
BACKTEST_FILES_CACHE_KEY,
|
|
223
|
+
)) as BacktestFilesCacheRecord | null;
|
|
224
|
+
|
|
225
|
+
if (cached?.savedAt && isFresh(cached.savedAt)) {
|
|
226
|
+
setTest(cached.items, cached.savedAt);
|
|
227
|
+
return cached.items;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const newTests = await getBacktestFiles();
|
|
231
|
+
const savedAt = Date.now();
|
|
232
|
+
setTest(newTests, savedAt);
|
|
233
|
+
await set(BACKTEST_FILES_CACHE_KEY, {
|
|
234
|
+
savedAt,
|
|
235
|
+
items: newTests,
|
|
236
|
+
} satisfies BacktestFilesCacheRecord);
|
|
237
|
+
return newTests;
|
|
238
|
+
})().finally(() => {
|
|
239
|
+
useTestListStore.getState().setInFlight(undefined);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
setInFlight(pending);
|
|
243
|
+
return pending;
|
|
244
|
+
};
|
|
245
|
+
|
|
187
246
|
export const useFavoriteTests = () => {
|
|
188
247
|
const favotites = useFavoriteTetstsStore((s) => s.tests);
|
|
189
248
|
const toggleFavorite = useFavoriteTetstsStore((s) => s.toggleFavorite);
|
|
@@ -215,14 +274,15 @@ export const useFavoriteTests = () => {
|
|
|
215
274
|
|
|
216
275
|
interface TestListProps {
|
|
217
276
|
symbol?: string;
|
|
277
|
+
enabled?: boolean;
|
|
218
278
|
}
|
|
219
279
|
|
|
220
280
|
export const useTestList = (filters: TestListProps = {}) => {
|
|
281
|
+
const { enabled = true } = filters;
|
|
221
282
|
const [loadding, setLoading] = useState(false);
|
|
222
283
|
const [fulFilled, setFulfilled] = useState(false);
|
|
223
284
|
const [error, setError] = useState<unknown>(null);
|
|
224
285
|
const tests = useTestListStore((s) => s.tests);
|
|
225
|
-
const setTest = useTestListStore((s) => s.setTest);
|
|
226
286
|
const { favoriteItems } = useFavoriteTests();
|
|
227
287
|
|
|
228
288
|
const testStrategyMap = new Map(
|
|
@@ -266,22 +326,24 @@ export const useTestList = (filters: TestListProps = {}) => {
|
|
|
266
326
|
const noData = _.isEmpty(testItems);
|
|
267
327
|
|
|
268
328
|
useEffect(() => {
|
|
329
|
+
if (!enabled) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
269
333
|
const loadData = async () => {
|
|
270
334
|
try {
|
|
271
335
|
setLoading(true);
|
|
272
336
|
await delay();
|
|
273
|
-
|
|
337
|
+
await loadBacktestFilesList();
|
|
274
338
|
setLoading(false);
|
|
275
339
|
setFulfilled(true);
|
|
276
|
-
|
|
277
|
-
setTest(newTests);
|
|
278
340
|
} catch (err) {
|
|
279
341
|
setError(err);
|
|
280
342
|
}
|
|
281
343
|
};
|
|
282
344
|
|
|
283
345
|
void loadData();
|
|
284
|
-
}, [
|
|
346
|
+
}, [enabled]);
|
|
285
347
|
|
|
286
348
|
return {
|
|
287
349
|
loadding,
|
|
@@ -289,6 +351,7 @@ export const useTestList = (filters: TestListProps = {}) => {
|
|
|
289
351
|
error,
|
|
290
352
|
noData,
|
|
291
353
|
tests: testItems,
|
|
354
|
+
ensureLoaded: loadBacktestFilesList,
|
|
292
355
|
};
|
|
293
356
|
};
|
|
294
357
|
|
|
@@ -318,7 +381,7 @@ export const useTest = (testName: string) => {
|
|
|
318
381
|
|
|
319
382
|
let resolvedStrategy = strategyName;
|
|
320
383
|
if (!resolvedStrategy) {
|
|
321
|
-
const newTests = await
|
|
384
|
+
const newTests = await loadBacktestFilesList();
|
|
322
385
|
setTestList(newTests);
|
|
323
386
|
resolvedStrategy = newTests.find((item) => item.value === testName)
|
|
324
387
|
?.data?.strategyName as string | undefined;
|
|
@@ -400,6 +463,16 @@ export const useBacktestMutations = () => {
|
|
|
400
463
|
removeFavorite(testName);
|
|
401
464
|
removeFromCompare(testName);
|
|
402
465
|
|
|
466
|
+
const cache = (await get(
|
|
467
|
+
BACKTEST_FILES_CACHE_KEY,
|
|
468
|
+
)) as BacktestFilesCacheRecord | null;
|
|
469
|
+
if (cache?.items) {
|
|
470
|
+
await set(BACKTEST_FILES_CACHE_KEY, {
|
|
471
|
+
...cache,
|
|
472
|
+
items: cache.items.filter((item) => item.value !== testName),
|
|
473
|
+
} satisfies BacktestFilesCacheRecord);
|
|
474
|
+
}
|
|
475
|
+
|
|
403
476
|
await Promise.all([del(`test-${testName}`), del(`backtest-${testName}`)]);
|
|
404
477
|
};
|
|
405
478
|
|
|
@@ -438,7 +511,7 @@ export const useBacktest = (id: string | undefined) => {
|
|
|
438
511
|
|
|
439
512
|
let resolvedStrategy = strategyName;
|
|
440
513
|
if (!resolvedStrategy) {
|
|
441
|
-
const newTests = await
|
|
514
|
+
const newTests = await loadBacktestFilesList();
|
|
442
515
|
setTestList(newTests);
|
|
443
516
|
resolvedStrategy = newTests.find((item) => item.value === id)?.data
|
|
444
517
|
?.strategyName as string | undefined;
|
|
@@ -462,3 +535,17 @@ export const useBacktest = (id: string | undefined) => {
|
|
|
462
535
|
loading,
|
|
463
536
|
};
|
|
464
537
|
};
|
|
538
|
+
|
|
539
|
+
export const resetTestsStoreForTests = () => {
|
|
540
|
+
useDataStore.setState({
|
|
541
|
+
backtests: new Map<string, OrderLogData | null>(),
|
|
542
|
+
});
|
|
543
|
+
useTestListStore.setState({
|
|
544
|
+
tests: [],
|
|
545
|
+
loadedAt: 0,
|
|
546
|
+
inFlight: undefined,
|
|
547
|
+
});
|
|
548
|
+
useTestsStore.setState({
|
|
549
|
+
tests: new Map<string, TestResult | null>(),
|
|
550
|
+
});
|
|
551
|
+
};
|