@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
package/src/app/store/data.ts
CHANGED
|
@@ -2,10 +2,20 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import { get, set } from 'idb-keyval';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
KlineChartData,
|
|
7
|
+
Interval,
|
|
8
|
+
Filters,
|
|
9
|
+
MarketUniverse,
|
|
10
|
+
Provider,
|
|
11
|
+
} from '@tradejs/types';
|
|
12
|
+
import { kline } from '#actions/kline';
|
|
7
13
|
import { isWrongData, mergeData } from '@tradejs/core/data';
|
|
8
|
-
import { normalizeEndToIntervalBoundary } from '
|
|
14
|
+
import { normalizeEndToIntervalBoundary } from '#app/lib/klineWindow';
|
|
15
|
+
import {
|
|
16
|
+
buildDashboardKlineTopic,
|
|
17
|
+
subscribeMarketKline,
|
|
18
|
+
} from './marketKlineStream';
|
|
9
19
|
|
|
10
20
|
interface DataState {
|
|
11
21
|
data: Map<string, KlineChartData | null>;
|
|
@@ -14,6 +24,7 @@ interface DataState {
|
|
|
14
24
|
symbol: string,
|
|
15
25
|
interval: Interval,
|
|
16
26
|
data: KlineChartData,
|
|
27
|
+
universe?: MarketUniverse,
|
|
17
28
|
) => void;
|
|
18
29
|
}
|
|
19
30
|
|
|
@@ -22,6 +33,7 @@ interface DataRequest {
|
|
|
22
33
|
provider: Provider;
|
|
23
34
|
symbol: string;
|
|
24
35
|
interval: Interval;
|
|
36
|
+
universe: MarketUniverse;
|
|
25
37
|
start: number;
|
|
26
38
|
end: number;
|
|
27
39
|
cacheBucketEnd: number;
|
|
@@ -30,17 +42,23 @@ interface DataRequest {
|
|
|
30
42
|
|
|
31
43
|
const MIN_CACHED_CANDLES = 2;
|
|
32
44
|
|
|
33
|
-
const getKey = (
|
|
34
|
-
|
|
45
|
+
const getKey = (
|
|
46
|
+
provider: Provider,
|
|
47
|
+
universe: MarketUniverse,
|
|
48
|
+
symbol: string,
|
|
49
|
+
interval: Interval,
|
|
50
|
+
) => `${provider}_${universe}_${symbol}_${interval}`;
|
|
35
51
|
|
|
36
52
|
const getProvider = (provider?: Provider): Provider => provider || 'bybit';
|
|
37
53
|
|
|
38
54
|
const toRequest = (filters: Filters, cacheOnly: boolean): DataRequest => {
|
|
39
55
|
const provider = getProvider(filters.provider);
|
|
56
|
+
const universe = filters.universe ?? 'crypto';
|
|
40
57
|
|
|
41
58
|
return {
|
|
42
|
-
key: getKey(provider, filters.symbol, filters.interval),
|
|
59
|
+
key: getKey(provider, universe, filters.symbol, filters.interval),
|
|
43
60
|
provider,
|
|
61
|
+
universe,
|
|
44
62
|
symbol: filters.symbol,
|
|
45
63
|
interval: filters.interval,
|
|
46
64
|
start: filters.start,
|
|
@@ -75,11 +93,11 @@ const inFlightRequests = new Map<string, Promise<KlineChartData>>();
|
|
|
75
93
|
|
|
76
94
|
const useDataStore = create<DataState>((set) => ({
|
|
77
95
|
data: new Map<string, KlineChartData | null>(),
|
|
78
|
-
setData: (provider, symbol, interval, newData) =>
|
|
96
|
+
setData: (provider, symbol, interval, newData, universe = 'crypto') =>
|
|
79
97
|
set(({ data }) => {
|
|
80
98
|
const next = new Map(data);
|
|
81
99
|
|
|
82
|
-
next.set(getKey(provider, symbol, interval), newData);
|
|
100
|
+
next.set(getKey(provider, universe, symbol, interval), newData);
|
|
83
101
|
|
|
84
102
|
return {
|
|
85
103
|
data: next,
|
|
@@ -103,11 +121,12 @@ const getFetchStart = (start: number, data: KlineChartData) =>
|
|
|
103
121
|
const requestKline = async (
|
|
104
122
|
request: Pick<
|
|
105
123
|
DataRequest,
|
|
106
|
-
'provider' | 'symbol' | 'interval' | 'end' | 'cacheOnly'
|
|
124
|
+
'provider' | 'universe' | 'symbol' | 'interval' | 'end' | 'cacheOnly'
|
|
107
125
|
> & { start: number },
|
|
108
126
|
) =>
|
|
109
127
|
kline({
|
|
110
128
|
provider: request.provider,
|
|
129
|
+
universe: request.universe,
|
|
111
130
|
symbol: request.symbol,
|
|
112
131
|
interval: request.interval,
|
|
113
132
|
start: request.start,
|
|
@@ -178,13 +197,14 @@ const mergeFreshData = async (
|
|
|
178
197
|
const persistData = async (
|
|
179
198
|
{
|
|
180
199
|
provider,
|
|
200
|
+
universe,
|
|
181
201
|
symbol,
|
|
182
202
|
interval,
|
|
183
203
|
key,
|
|
184
|
-
}: Pick<DataRequest, 'provider' | 'symbol' | 'interval' | 'key'>,
|
|
204
|
+
}: Pick<DataRequest, 'provider' | 'universe' | 'symbol' | 'interval' | 'key'>,
|
|
185
205
|
data: KlineChartData,
|
|
186
206
|
) => {
|
|
187
|
-
useDataStore.getState().setData(provider, symbol, interval, data);
|
|
207
|
+
useDataStore.getState().setData(provider, symbol, interval, data, universe);
|
|
188
208
|
await set(key, data);
|
|
189
209
|
};
|
|
190
210
|
|
|
@@ -211,10 +231,10 @@ const fetchAndStoreData = async (dataRequest: DataRequest) => {
|
|
|
211
231
|
return pendingRequest;
|
|
212
232
|
};
|
|
213
233
|
|
|
214
|
-
export const useData = (filters: Filters) => {
|
|
234
|
+
export const useData = (filters: Filters, live = true) => {
|
|
215
235
|
const searchParams = useSearchParams();
|
|
216
236
|
const cacheOnly = Boolean(searchParams.get('cacheOnly')) ?? false;
|
|
217
|
-
const { end, interval, provider, start, symbol } = filters;
|
|
237
|
+
const { end, interval, provider, start, symbol, universe } = filters;
|
|
218
238
|
const dataRequest = useMemo(
|
|
219
239
|
() =>
|
|
220
240
|
toRequest(
|
|
@@ -222,22 +242,27 @@ export const useData = (filters: Filters) => {
|
|
|
222
242
|
end,
|
|
223
243
|
interval,
|
|
224
244
|
provider,
|
|
245
|
+
universe,
|
|
225
246
|
start,
|
|
226
247
|
symbol,
|
|
227
248
|
} as Filters,
|
|
228
249
|
cacheOnly,
|
|
229
250
|
),
|
|
230
|
-
[cacheOnly, end, interval, provider, start, symbol],
|
|
251
|
+
[cacheOnly, end, interval, provider, start, symbol, universe],
|
|
231
252
|
);
|
|
232
253
|
const requestKey = useMemo(() => getRequestKey(dataRequest), [dataRequest]);
|
|
233
254
|
const [fulfilledRequestKey, setFulfilledRequestKey] = useState<string | null>(
|
|
234
255
|
null,
|
|
235
256
|
);
|
|
257
|
+
const [liveWindow, setLiveWindow] = useState({ key: '', end: 0 });
|
|
236
258
|
const storedData = useDataStore((s) => s.data.get(dataRequest.key));
|
|
259
|
+
const effectiveEnd = Math.max(
|
|
260
|
+
dataRequest.end,
|
|
261
|
+
liveWindow.key === dataRequest.key ? liveWindow.end : dataRequest.end,
|
|
262
|
+
);
|
|
237
263
|
const windowedData = useMemo(
|
|
238
|
-
() =>
|
|
239
|
-
|
|
240
|
-
[dataRequest.end, dataRequest.start, storedData],
|
|
264
|
+
() => filterDataToWindow(storedData ?? [], dataRequest.start, effectiveEnd),
|
|
265
|
+
[dataRequest.start, effectiveEnd, storedData],
|
|
241
266
|
);
|
|
242
267
|
|
|
243
268
|
const fulfilled = fulfilledRequestKey === requestKey;
|
|
@@ -267,6 +292,37 @@ export const useData = (filters: Filters) => {
|
|
|
267
292
|
};
|
|
268
293
|
}, [dataRequest, requestKey]);
|
|
269
294
|
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (!live || cacheOnly || !dataRequest.symbol) return;
|
|
297
|
+
const topic = buildDashboardKlineTopic(dataRequest);
|
|
298
|
+
const catchUp = () => {
|
|
299
|
+
const end = Date.now();
|
|
300
|
+
void fetchAndStoreData({
|
|
301
|
+
...dataRequest,
|
|
302
|
+
end,
|
|
303
|
+
cacheBucketEnd: normalizeEndToIntervalBoundary(
|
|
304
|
+
end,
|
|
305
|
+
dataRequest.interval,
|
|
306
|
+
),
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
return subscribeMarketKline({
|
|
310
|
+
topic,
|
|
311
|
+
onReconnect: catchUp,
|
|
312
|
+
onEvent: (event) => {
|
|
313
|
+
setLiveWindow((current) => ({
|
|
314
|
+
key: dataRequest.key,
|
|
315
|
+
end: Math.max(
|
|
316
|
+
current.key === dataRequest.key ? current.end : dataRequest.end,
|
|
317
|
+
event.candle.timestamp,
|
|
318
|
+
),
|
|
319
|
+
}));
|
|
320
|
+
const current = useDataStore.getState().data.get(dataRequest.key) ?? [];
|
|
321
|
+
void persistData(dataRequest, mergeData(current, [event.candle]));
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}, [cacheOnly, dataRequest, live]);
|
|
325
|
+
|
|
270
326
|
return {
|
|
271
327
|
key: dataRequest.key,
|
|
272
328
|
data: windowedData,
|
package/src/app/store/filters.ts
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { MarketKlineEvent } from '@tradejs/types';
|
|
2
|
+
|
|
3
|
+
type MarketKlineMessage = {
|
|
4
|
+
type: 'kline';
|
|
5
|
+
topic: string;
|
|
6
|
+
event: MarketKlineEvent;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const buildDashboardKlineTopic = ({
|
|
10
|
+
provider,
|
|
11
|
+
universe,
|
|
12
|
+
symbol,
|
|
13
|
+
interval,
|
|
14
|
+
}: Pick<MarketKlineEvent, 'provider' | 'universe' | 'symbol' | 'interval'>) =>
|
|
15
|
+
[provider, universe, symbol.trim().toUpperCase(), interval].join(':');
|
|
16
|
+
|
|
17
|
+
export const parseMarketKlineMessage = (
|
|
18
|
+
payload: string,
|
|
19
|
+
): MarketKlineMessage | null => {
|
|
20
|
+
try {
|
|
21
|
+
const message = JSON.parse(payload) as Partial<MarketKlineMessage>;
|
|
22
|
+
if (
|
|
23
|
+
message.type !== 'kline' ||
|
|
24
|
+
typeof message.topic !== 'string' ||
|
|
25
|
+
!message.event?.candle ||
|
|
26
|
+
!Number.isFinite(message.event.candle.timestamp)
|
|
27
|
+
) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return message as MarketKlineMessage;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getMarketWebSocketUrl = (location: Location) => {
|
|
37
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
38
|
+
return `${protocol}//${location.host}/ws/market`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const subscribeMarketKline = ({
|
|
42
|
+
topic,
|
|
43
|
+
onEvent,
|
|
44
|
+
onReconnect,
|
|
45
|
+
}: {
|
|
46
|
+
topic: string;
|
|
47
|
+
onEvent: (event: MarketKlineEvent) => void;
|
|
48
|
+
onReconnect: () => void;
|
|
49
|
+
}) => {
|
|
50
|
+
let socket: WebSocket | null = null;
|
|
51
|
+
let stopped = false;
|
|
52
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
let reconnectAttempt = 0;
|
|
54
|
+
let openedOnce = false;
|
|
55
|
+
|
|
56
|
+
const connect = () => {
|
|
57
|
+
if (stopped) return;
|
|
58
|
+
socket = new WebSocket(getMarketWebSocketUrl(window.location));
|
|
59
|
+
socket.onopen = () => {
|
|
60
|
+
reconnectAttempt = 0;
|
|
61
|
+
socket?.send(JSON.stringify({ op: 'subscribe', topics: [topic] }));
|
|
62
|
+
if (openedOnce) onReconnect();
|
|
63
|
+
openedOnce = true;
|
|
64
|
+
};
|
|
65
|
+
socket.onmessage = ({ data }) => {
|
|
66
|
+
const message = parseMarketKlineMessage(String(data));
|
|
67
|
+
if (message?.topic === topic) onEvent(message.event);
|
|
68
|
+
};
|
|
69
|
+
socket.onclose = () => {
|
|
70
|
+
if (stopped) return;
|
|
71
|
+
const delayMs = Math.min(30_000, 1_000 * 2 ** reconnectAttempt);
|
|
72
|
+
reconnectAttempt += 1;
|
|
73
|
+
reconnectTimer = setTimeout(connect, delayMs);
|
|
74
|
+
};
|
|
75
|
+
socket.onerror = () => socket?.close();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
connect();
|
|
79
|
+
return () => {
|
|
80
|
+
stopped = true;
|
|
81
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
82
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
83
|
+
socket.send(JSON.stringify({ op: 'unsubscribe', topics: [topic] }));
|
|
84
|
+
}
|
|
85
|
+
socket?.close();
|
|
86
|
+
};
|
|
87
|
+
};
|
package/src/app/store/tests.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
|
|
3
3
|
import { persist } from 'zustand/middleware';
|
|
4
4
|
import _ from 'lodash';
|
|
5
5
|
import { del, get, set } from 'idb-keyval';
|
|
6
|
-
import { getBacktest, getBacktestFiles, getOrderLog } from '
|
|
6
|
+
import { getBacktest, getBacktestFiles, getOrderLog } from '#actions/backtest';
|
|
7
7
|
import {
|
|
8
8
|
TestResult,
|
|
9
9
|
TestCompareList,
|
package/src/app/store/tickers.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { create } from 'zustand';
|
|
|
3
3
|
import { persist } from 'zustand/middleware';
|
|
4
4
|
import { get, set } from 'idb-keyval';
|
|
5
5
|
import _ from 'lodash';
|
|
6
|
-
import { scan } from '
|
|
7
|
-
import { Items } from '@tradejs/types';
|
|
6
|
+
import { scan } from '#actions/scanner';
|
|
7
|
+
import { Items, MarketUniverse } from '@tradejs/types';
|
|
8
8
|
|
|
9
9
|
const LOCAL_STORAGE_KEY = 'tickers';
|
|
10
10
|
const TICKERS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
@@ -78,12 +78,29 @@ const useScannerStore = create<TickersScannerState>((set) => ({
|
|
|
78
78
|
})),
|
|
79
79
|
}));
|
|
80
80
|
|
|
81
|
-
const
|
|
81
|
+
const getProviderUniverseKey = (provider: string, universe: MarketUniverse) =>
|
|
82
|
+
universe === 'crypto' ? provider : `${provider}:${universe}`;
|
|
83
|
+
const getTickersCacheKey = (providerUniverseKey: string) =>
|
|
84
|
+
`tickers:${providerUniverseKey}`;
|
|
82
85
|
|
|
83
86
|
const isFresh = (savedAt: number) =>
|
|
84
87
|
Date.now() - savedAt < TICKERS_CACHE_TTL_MS;
|
|
85
88
|
|
|
86
|
-
const
|
|
89
|
+
const reportTickerLoadError = (
|
|
90
|
+
provider: string,
|
|
91
|
+
universe: MarketUniverse,
|
|
92
|
+
error: unknown,
|
|
93
|
+
) => {
|
|
94
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
95
|
+
console.warn(`Ticker loading failed for ${provider}:${universe}`, error);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const loadTickersForProvider = async (
|
|
100
|
+
provider: string,
|
|
101
|
+
universe: MarketUniverse,
|
|
102
|
+
) => {
|
|
103
|
+
const providerUniverseKey = getProviderUniverseKey(provider, universe);
|
|
87
104
|
const {
|
|
88
105
|
tickersByProvider,
|
|
89
106
|
loadedAtByProvider,
|
|
@@ -91,52 +108,72 @@ const loadTickersForProvider = async (provider: string) => {
|
|
|
91
108
|
setTickers,
|
|
92
109
|
setInFlight,
|
|
93
110
|
} = useScannerStore.getState();
|
|
94
|
-
const memoryTickers = tickersByProvider[
|
|
95
|
-
const loadedAt = loadedAtByProvider[
|
|
111
|
+
const memoryTickers = tickersByProvider[providerUniverseKey] ?? [];
|
|
112
|
+
const loadedAt = loadedAtByProvider[providerUniverseKey] ?? 0;
|
|
96
113
|
|
|
97
114
|
if (memoryTickers.length && isFresh(loadedAt)) {
|
|
98
115
|
return memoryTickers;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
const inFlight = inFlightByProvider[
|
|
118
|
+
const inFlight = inFlightByProvider[providerUniverseKey];
|
|
102
119
|
if (inFlight) {
|
|
103
120
|
return inFlight;
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
const pending = (async () => {
|
|
107
124
|
const cached = (await get(
|
|
108
|
-
getTickersCacheKey(
|
|
125
|
+
getTickersCacheKey(providerUniverseKey),
|
|
109
126
|
)) as TickersCacheRecord | null;
|
|
110
127
|
|
|
111
128
|
if (cached?.items?.length && isFresh(cached.savedAt)) {
|
|
112
|
-
setTickers(
|
|
129
|
+
setTickers(providerUniverseKey, cached.items, cached.savedAt);
|
|
113
130
|
return cached.items;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
let coins: Items;
|
|
134
|
+
try {
|
|
135
|
+
coins = await scan(provider, universe);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (cached?.items?.length) {
|
|
138
|
+
setTickers(providerUniverseKey, cached.items, cached.savedAt);
|
|
139
|
+
return cached.items;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
117
143
|
const savedAt = Date.now();
|
|
118
|
-
setTickers(
|
|
119
|
-
await set(getTickersCacheKey(
|
|
144
|
+
setTickers(providerUniverseKey, coins, savedAt);
|
|
145
|
+
await set(getTickersCacheKey(providerUniverseKey), {
|
|
120
146
|
savedAt,
|
|
121
147
|
items: coins,
|
|
122
148
|
} satisfies TickersCacheRecord);
|
|
123
149
|
return coins;
|
|
124
|
-
})()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
})()
|
|
151
|
+
.catch((error) => {
|
|
152
|
+
reportTickerLoadError(provider, universe, error);
|
|
153
|
+
throw error;
|
|
154
|
+
})
|
|
155
|
+
.finally(() => {
|
|
156
|
+
useScannerStore.getState().setInFlight(providerUniverseKey, undefined);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
setInFlight(providerUniverseKey, pending);
|
|
129
160
|
return pending;
|
|
130
161
|
};
|
|
131
162
|
|
|
132
163
|
export const useTickers = (
|
|
133
164
|
provider = 'bybit',
|
|
165
|
+
universeOrOptions: MarketUniverse | { enabled?: boolean } = 'crypto',
|
|
134
166
|
options: { enabled?: boolean } = {},
|
|
135
167
|
) => {
|
|
136
|
-
const
|
|
168
|
+
const universe =
|
|
169
|
+
typeof universeOrOptions === 'string' ? universeOrOptions : 'crypto';
|
|
170
|
+
const { enabled = true } =
|
|
171
|
+
typeof universeOrOptions === 'string' ? options : universeOrOptions;
|
|
137
172
|
const favorites = useFavoriteTickersStore((s) => s.favorites);
|
|
138
173
|
const tickers = useScannerStore(
|
|
139
|
-
(s) =>
|
|
174
|
+
(s) =>
|
|
175
|
+
s.tickersByProvider[getProviderUniverseKey(provider, universe)] ??
|
|
176
|
+
EMPTY_ITEMS,
|
|
140
177
|
);
|
|
141
178
|
const toggleFavorite = useFavoriteTickersStore((s) => s.toggleFavorite);
|
|
142
179
|
const checkIsFavorite = useCallback(
|
|
@@ -144,8 +181,8 @@ export const useTickers = (
|
|
|
144
181
|
[favorites],
|
|
145
182
|
);
|
|
146
183
|
const ensureLoaded = useCallback(
|
|
147
|
-
() => loadTickersForProvider(provider),
|
|
148
|
-
[provider],
|
|
184
|
+
() => loadTickersForProvider(provider, universe),
|
|
185
|
+
[provider, universe],
|
|
149
186
|
);
|
|
150
187
|
|
|
151
188
|
useEffect(() => {
|
|
@@ -153,7 +190,7 @@ export const useTickers = (
|
|
|
153
190
|
return;
|
|
154
191
|
}
|
|
155
192
|
|
|
156
|
-
void ensureLoaded();
|
|
193
|
+
void ensureLoaded().catch(() => undefined);
|
|
157
194
|
}, [enabled, ensureLoaded]);
|
|
158
195
|
|
|
159
196
|
const items = useMemo(() => {
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
-
"types": ["
|
|
4
|
+
"types": ["node"],
|
|
5
5
|
"allowJs": true,
|
|
6
6
|
"skipLibCheck": true,
|
|
7
7
|
"strict": true,
|
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
"incremental": true,
|
|
16
16
|
"target": "ES2017",
|
|
17
17
|
"baseUrl": ".",
|
|
18
|
+
"resolvePackageJsonImports": true,
|
|
19
|
+
"paths": {
|
|
20
|
+
"#app/*": ["./src/app/*"],
|
|
21
|
+
"#actions/*": ["./src/app/actions/*"],
|
|
22
|
+
"#store": ["./src/app/store/index.ts"],
|
|
23
|
+
"#shared/*": ["./src/app/components/Shared/*"],
|
|
24
|
+
"#ui": ["./src/app/components/UI/index.ts"],
|
|
25
|
+
"#components/*": ["./src/app/components/*"]
|
|
26
|
+
},
|
|
18
27
|
"plugins": [
|
|
19
28
|
{
|
|
20
29
|
"name": "next"
|
|
21
30
|
}
|
|
22
|
-
]
|
|
23
|
-
"paths": {
|
|
24
|
-
"@/*": ["./src/*"],
|
|
25
|
-
"@app/*": ["./src/app/*"],
|
|
26
|
-
"@actions/*": ["./src/app/actions/*"],
|
|
27
|
-
"@store": ["./src/app/store/index"],
|
|
28
|
-
"@shared/*": ["./src/app/components/Shared/*"],
|
|
29
|
-
"@UI": ["./src/app/components/UI/index"],
|
|
30
|
-
"@components/*": ["./src/app/components/*"]
|
|
31
|
-
}
|
|
31
|
+
]
|
|
32
32
|
},
|
|
33
33
|
"include": [
|
|
34
34
|
"next-env.d.ts",
|
|
@@ -37,5 +37,10 @@
|
|
|
37
37
|
".next/types/**/*.ts",
|
|
38
38
|
".next/dev/types/**/*.ts"
|
|
39
39
|
],
|
|
40
|
-
"exclude": [
|
|
40
|
+
"exclude": [
|
|
41
|
+
"node_modules",
|
|
42
|
+
"src/**/__tests__",
|
|
43
|
+
"src/**/*.test.ts",
|
|
44
|
+
"src/**/*.test.tsx"
|
|
45
|
+
]
|
|
41
46
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { IconButton } from '@chakra-ui/react';
|
|
2
|
-
import { FiExternalLink } from 'react-icons/fi';
|
|
3
|
-
import { useTestContext } from '../context';
|
|
4
|
-
|
|
5
|
-
export const TestCardOpenReportButton = () => {
|
|
6
|
-
const { testResult } = useTestContext();
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
<IconButton
|
|
10
|
-
size="xs"
|
|
11
|
-
colorPalette="teal"
|
|
12
|
-
variant="outline"
|
|
13
|
-
onClick={() =>
|
|
14
|
-
window.open(
|
|
15
|
-
`/routes/backtest/${testResult.test.name}`,
|
|
16
|
-
'_blank',
|
|
17
|
-
'noopener,noreferrer',
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
>
|
|
21
|
-
<FiExternalLink />
|
|
22
|
-
</IconButton>
|
|
23
|
-
);
|
|
24
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Box, ClientOnly } from '@chakra-ui/react';
|
|
5
|
-
import { useParams } from 'next/navigation';
|
|
6
|
-
import { TestCard } from '@components/Backtest/TestCard';
|
|
7
|
-
|
|
8
|
-
const BacktestReport = () => {
|
|
9
|
-
const { test: testName } = useParams();
|
|
10
|
-
|
|
11
|
-
if (typeof testName !== 'string') {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<ClientOnly>
|
|
17
|
-
<Box minH="100vh" bg="gray.900">
|
|
18
|
-
<Box minW="1200px" pl={4} pt={4}>
|
|
19
|
-
<TestCard.Root testName={testName} noWrapper={true}>
|
|
20
|
-
<TestCard.Title>
|
|
21
|
-
<TestCard.FavoriteIndicator />
|
|
22
|
-
<TestCard.ConfigDrawer />
|
|
23
|
-
</TestCard.Title>
|
|
24
|
-
<TestCard.Chart height="500px" />
|
|
25
|
-
<TestCard.StatTable />
|
|
26
|
-
</TestCard.Root>
|
|
27
|
-
</Box>
|
|
28
|
-
</Box>
|
|
29
|
-
</ClientOnly>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default BacktestReport;
|