@tradejs/app 1.0.8 → 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.
Files changed (119) hide show
  1. package/README.md +7 -2
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +4 -7
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -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 { KlineChartData, Interval, Filters, Provider } from '@tradejs/types';
6
- import { kline } from '@actions/kline';
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 '@app/lib/klineWindow';
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 = (provider: Provider, symbol: string, interval: Interval) =>
34
- `${provider}_${symbol}_${interval}`;
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
- filterDataToWindow(storedData ?? [], dataRequest.start, dataRequest.end),
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,
@@ -11,6 +11,7 @@ interface FiltersState {
11
11
  const useStore = create<FiltersState>((set) => ({
12
12
  filters: {
13
13
  provider: 'bybit',
14
+ universe: 'crypto',
14
15
  symbol: 'BTCUSDT',
15
16
  interval: '15' as Interval,
16
17
  start: getTimestamp(DASHBOARD_PRELOAD_DAYS),
@@ -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
+ };
@@ -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 '@actions/backtest';
6
+ import { getBacktest, getBacktestFiles, getOrderLog } from '#actions/backtest';
7
7
  import {
8
8
  TestResult,
9
9
  TestCompareList,
@@ -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 '@actions/scanner';
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 getTickersCacheKey = (provider: string) => `tickers:${provider}`;
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 loadTickersForProvider = async (provider: string) => {
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[provider] ?? [];
95
- const loadedAt = loadedAtByProvider[provider] ?? 0;
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[provider];
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(provider),
125
+ getTickersCacheKey(providerUniverseKey),
109
126
  )) as TickersCacheRecord | null;
110
127
 
111
128
  if (cached?.items?.length && isFresh(cached.savedAt)) {
112
- setTickers(provider, cached.items, cached.savedAt);
129
+ setTickers(providerUniverseKey, cached.items, cached.savedAt);
113
130
  return cached.items;
114
131
  }
115
132
 
116
- const coins = await scan(provider);
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(provider, coins, savedAt);
119
- await set(getTickersCacheKey(provider), {
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
- })().finally(() => {
125
- useScannerStore.getState().setInFlight(provider, undefined);
126
- });
127
-
128
- setInFlight(provider, pending);
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 { enabled = true } = options;
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) => s.tickersByProvider[provider] ?? EMPTY_ITEMS,
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": ["jest", "node"],
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": ["node_modules"]
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;