@tradejs/app 1.0.0

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 (126) hide show
  1. package/README.md +12 -0
  2. package/bin/tradejs-app.mjs +54 -0
  3. package/next-env.d.ts +6 -0
  4. package/next.config.mjs +31 -0
  5. package/package.json +60 -0
  6. package/src/app/actions/ai.ts +33 -0
  7. package/src/app/actions/backtest.ts +55 -0
  8. package/src/app/actions/kline.ts +18 -0
  9. package/src/app/actions/scanner.ts +10 -0
  10. package/src/app/actions/signal.ts +19 -0
  11. package/src/app/api/ai/route.ts +151 -0
  12. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  13. package/src/app/api/backtest/files/route.ts +60 -0
  14. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
  15. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
  16. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
  17. package/src/app/api/cron/route.ts +4 -0
  18. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
  19. package/src/app/api/derivatives/summary/route.ts +20 -0
  20. package/src/app/api/files/screenshot/[name]/route.ts +42 -0
  21. package/src/app/api/indicators/route.ts +24 -0
  22. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
  23. package/src/app/api/scanner/[provider]/route.ts +41 -0
  24. package/src/app/api/scanner/route.ts +31 -0
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
  26. package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
  27. package/src/app/api/spread/summary/route.ts +20 -0
  28. package/src/app/auth.ts +76 -0
  29. package/src/app/components/Backtest/CompareList/index.tsx +34 -0
  30. package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
  31. package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
  32. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
  33. package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
  34. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
  35. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
  36. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
  37. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
  38. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
  39. package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
  40. package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
  41. package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
  42. package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
  43. package/src/app/components/Backtest/TestCard/context.ts +14 -0
  44. package/src/app/components/Backtest/TestCard/index.ts +28 -0
  45. package/src/app/components/Backtest/TestList/index.tsx +124 -0
  46. package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
  47. package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
  48. package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
  49. package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
  50. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
  51. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
  52. package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
  53. package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
  54. package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
  55. package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
  56. package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
  57. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
  58. package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
  59. package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
  60. package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
  61. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
  62. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
  63. package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
  64. package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
  65. package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
  66. package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
  67. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
  68. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
  69. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
  70. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
  71. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
  72. package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
  73. package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
  74. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
  75. package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
  76. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
  77. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
  78. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
  79. package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
  80. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
  81. package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
  82. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
  83. package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
  84. package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
  85. package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
  86. package/src/app/components/Shared/AppShell.tsx +28 -0
  87. package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
  88. package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
  89. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
  90. package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
  91. package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
  92. package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
  93. package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
  94. package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
  95. package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
  96. package/src/app/components/Shared/Filters/context.ts +17 -0
  97. package/src/app/components/Shared/Filters/index.ts +17 -0
  98. package/src/app/components/Shared/Sidebar/index.tsx +72 -0
  99. package/src/app/components/UI/ColorMode/index.tsx +112 -0
  100. package/src/app/components/UI/EmptyState/index.tsx +28 -0
  101. package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
  102. package/src/app/components/UI/Segment/index.tsx +23 -0
  103. package/src/app/components/UI/Select/index.tsx +81 -0
  104. package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
  105. package/src/app/components/UI/Switcher/index.tsx +24 -0
  106. package/src/app/components/UI/Toaster/index.tsx +45 -0
  107. package/src/app/components/UI/index.ts +8 -0
  108. package/src/app/favicon.ico +0 -0
  109. package/src/app/globals.css +5 -0
  110. package/src/app/layout.tsx +31 -0
  111. package/src/app/page.tsx +14 -0
  112. package/src/app/provider.tsx +39 -0
  113. package/src/app/routes/backtest/[test]/page.tsx +33 -0
  114. package/src/app/routes/backtest/page.tsx +374 -0
  115. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
  116. package/src/app/routes/dashboard/page.tsx +20 -0
  117. package/src/app/routes/derivatives/page.tsx +202 -0
  118. package/src/app/routes/signin/page.tsx +155 -0
  119. package/src/app/store/data.ts +144 -0
  120. package/src/app/store/filters.ts +29 -0
  121. package/src/app/store/index.ts +13 -0
  122. package/src/app/store/indicators.ts +229 -0
  123. package/src/app/store/tests.ts +464 -0
  124. package/src/app/store/tickers.ts +89 -0
  125. package/src/proxy.ts +142 -0
  126. package/tsconfig.json +40 -0
@@ -0,0 +1,202 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import {
5
+ Box,
6
+ Heading,
7
+ Text,
8
+ Table,
9
+ HStack,
10
+ Spinner,
11
+ Button,
12
+ Input,
13
+ } from '@chakra-ui/react';
14
+ import { API } from '@tradejs/core/api';
15
+
16
+ type SummaryResponse = {
17
+ rows: Array<{
18
+ symbol: string;
19
+ interval: string;
20
+ ts: string;
21
+ open_interest: number | null;
22
+ funding_rate: number | null;
23
+ liq_long: number | null;
24
+ liq_short: number | null;
25
+ liq_total: number | null;
26
+ }>;
27
+ aggregates: Array<{
28
+ symbol: string;
29
+ interval: string;
30
+ points: number;
31
+ last_ts: string;
32
+ avg_open_interest: number | null;
33
+ avg_funding_rate: number | null;
34
+ sum_liq_total: number | null;
35
+ }>;
36
+ hours: number;
37
+ };
38
+
39
+ const fmt = (value: number | null | undefined, digits = 4) => {
40
+ if (value == null || !Number.isFinite(Number(value))) return 'n/a';
41
+ return Number(value).toFixed(digits);
42
+ };
43
+
44
+ const DerivativesPage = () => {
45
+ const [hours, setHours] = useState('24');
46
+ const [limit, setLimit] = useState('500');
47
+ const [loading, setLoading] = useState(false);
48
+ const [data, setData] = useState<SummaryResponse | null>(null);
49
+ const [error, setError] = useState<string>('');
50
+
51
+ const load = useCallback(async () => {
52
+ setLoading(true);
53
+ setError('');
54
+ try {
55
+ const response = await API.get<SummaryResponse>(
56
+ `/api/derivatives/summary?hours=${hours}&limit=${limit}`,
57
+ );
58
+ setData(response);
59
+ } catch (err) {
60
+ setError((err as Error)?.message || 'Failed to load derivatives');
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, [hours, limit]);
65
+
66
+ useEffect(() => {
67
+ void load();
68
+ }, [load]);
69
+
70
+ const totals = useMemo(() => {
71
+ const aggregates = data?.aggregates ?? [];
72
+ return {
73
+ pairs: aggregates.length,
74
+ points: aggregates.reduce(
75
+ (acc, item) => acc + Number(item.points || 0),
76
+ 0,
77
+ ),
78
+ liq: aggregates.reduce(
79
+ (acc, item) => acc + Number(item.sum_liq_total || 0),
80
+ 0,
81
+ ),
82
+ };
83
+ }, [data]);
84
+
85
+ return (
86
+ <Box p={6}>
87
+ <Heading size="lg" mb={2}>
88
+ Derivatives Dashboard
89
+ </Heading>
90
+ <Text color="gray.500" mb={4}>
91
+ OI / Funding / Liquidations from Timescale (TF 15m, 1h)
92
+ </Text>
93
+
94
+ <HStack mb={4}>
95
+ <select
96
+ value={hours}
97
+ onChange={(e) => setHours(e.target.value)}
98
+ style={{
99
+ width: '160px',
100
+ border: '1px solid #d1d5db',
101
+ borderRadius: '8px',
102
+ padding: '8px',
103
+ }}
104
+ >
105
+ <option value="6">Last 6h</option>
106
+ <option value="24">Last 24h</option>
107
+ <option value="72">Last 72h</option>
108
+ <option value="168">Last 7d</option>
109
+ </select>
110
+ <Input
111
+ value={limit}
112
+ onChange={(e) => setLimit(e.target.value)}
113
+ width="160px"
114
+ placeholder="Row limit"
115
+ />
116
+ <Button onClick={load}>Refresh</Button>
117
+ </HStack>
118
+
119
+ {loading && <Spinner />}
120
+ {error && (
121
+ <Text color="red.400" mb={4}>
122
+ {error}
123
+ </Text>
124
+ )}
125
+
126
+ <Text mb={4}>
127
+ Symbols/TF: {totals.pairs} | Points: {totals.points} | Sum liquidation:{' '}
128
+ {fmt(totals.liq, 2)}
129
+ </Text>
130
+
131
+ <Heading size="md" mb={2}>
132
+ Aggregates
133
+ </Heading>
134
+ <Box overflowX="auto" mb={8}>
135
+ <Table.Root size="sm">
136
+ <Table.Header>
137
+ <Table.Row>
138
+ <Table.ColumnHeader>symbol</Table.ColumnHeader>
139
+ <Table.ColumnHeader>tf</Table.ColumnHeader>
140
+ <Table.ColumnHeader>points</Table.ColumnHeader>
141
+ <Table.ColumnHeader>last_ts</Table.ColumnHeader>
142
+ <Table.ColumnHeader>avg_oi</Table.ColumnHeader>
143
+ <Table.ColumnHeader>avg_funding</Table.ColumnHeader>
144
+ <Table.ColumnHeader>sum_liq</Table.ColumnHeader>
145
+ </Table.Row>
146
+ </Table.Header>
147
+ <Table.Body>
148
+ {(data?.aggregates ?? []).map((row) => (
149
+ <Table.Row key={`${row.symbol}:${row.interval}`}>
150
+ <Table.Cell>{row.symbol}</Table.Cell>
151
+ <Table.Cell>{row.interval}</Table.Cell>
152
+ <Table.Cell>{row.points}</Table.Cell>
153
+ <Table.Cell>
154
+ {row.last_ts ? new Date(row.last_ts).toISOString() : 'n/a'}
155
+ </Table.Cell>
156
+ <Table.Cell>{fmt(row.avg_open_interest, 2)}</Table.Cell>
157
+ <Table.Cell>{fmt(row.avg_funding_rate, 6)}</Table.Cell>
158
+ <Table.Cell>{fmt(row.sum_liq_total, 2)}</Table.Cell>
159
+ </Table.Row>
160
+ ))}
161
+ </Table.Body>
162
+ </Table.Root>
163
+ </Box>
164
+
165
+ <Heading size="md" mb={2}>
166
+ Latest Points
167
+ </Heading>
168
+ <Box overflowX="auto">
169
+ <Table.Root size="sm">
170
+ <Table.Header>
171
+ <Table.Row>
172
+ <Table.ColumnHeader>ts</Table.ColumnHeader>
173
+ <Table.ColumnHeader>symbol</Table.ColumnHeader>
174
+ <Table.ColumnHeader>tf</Table.ColumnHeader>
175
+ <Table.ColumnHeader>oi</Table.ColumnHeader>
176
+ <Table.ColumnHeader>funding</Table.ColumnHeader>
177
+ <Table.ColumnHeader>liq_long</Table.ColumnHeader>
178
+ <Table.ColumnHeader>liq_short</Table.ColumnHeader>
179
+ <Table.ColumnHeader>liq_total</Table.ColumnHeader>
180
+ </Table.Row>
181
+ </Table.Header>
182
+ <Table.Body>
183
+ {(data?.rows ?? []).slice(0, 200).map((row, idx) => (
184
+ <Table.Row key={`${row.symbol}:${row.interval}:${row.ts}:${idx}`}>
185
+ <Table.Cell>{new Date(row.ts).toISOString()}</Table.Cell>
186
+ <Table.Cell>{row.symbol}</Table.Cell>
187
+ <Table.Cell>{row.interval}</Table.Cell>
188
+ <Table.Cell>{fmt(row.open_interest, 2)}</Table.Cell>
189
+ <Table.Cell>{fmt(row.funding_rate, 6)}</Table.Cell>
190
+ <Table.Cell>{fmt(row.liq_long, 2)}</Table.Cell>
191
+ <Table.Cell>{fmt(row.liq_short, 2)}</Table.Cell>
192
+ <Table.Cell>{fmt(row.liq_total, 2)}</Table.Cell>
193
+ </Table.Row>
194
+ ))}
195
+ </Table.Body>
196
+ </Table.Root>
197
+ </Box>
198
+ </Box>
199
+ );
200
+ };
201
+
202
+ export default DerivativesPage;
@@ -0,0 +1,155 @@
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useMemo, useState } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import Image from 'next/image';
6
+ import { signIn, useSession } from 'next-auth/react';
7
+ import { Box, Button, Field, Flex, Input, Stack, Text } from '@chakra-ui/react';
8
+
9
+ const SigninContent = () => {
10
+ const router = useRouter();
11
+ const searchParams = useSearchParams();
12
+ const { status } = useSession();
13
+ const [username, setUsername] = useState('');
14
+ const [password, setPassword] = useState('');
15
+ const [error, setError] = useState('');
16
+ const [isSubmitting, setIsSubmitting] = useState(false);
17
+
18
+ const callbackUrl = useMemo(
19
+ () => searchParams.get('callbackUrl') ?? '/routes/dashboard',
20
+ [searchParams],
21
+ );
22
+
23
+ useEffect(() => {
24
+ if (status === 'authenticated') {
25
+ router.replace(callbackUrl);
26
+ }
27
+ }, [status, router, callbackUrl]);
28
+
29
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
30
+ event.preventDefault();
31
+ setError('');
32
+ if (!username.trim() || !password) {
33
+ setError('Please enter username and password');
34
+ return;
35
+ }
36
+ setIsSubmitting(true);
37
+
38
+ const result = await signIn('credentials', {
39
+ redirect: false,
40
+ username,
41
+ password,
42
+ callbackUrl,
43
+ });
44
+
45
+ setIsSubmitting(false);
46
+
47
+ if (!result || result.error) {
48
+ setError('Invalid username or password');
49
+ return;
50
+ }
51
+
52
+ router.replace(callbackUrl);
53
+ };
54
+
55
+ return (
56
+ <Flex minH="100vh" direction={{ base: 'column', lg: 'row' }} bg="gray.950">
57
+ <Flex
58
+ w={{ base: 'full', lg: '33.333%' }}
59
+ px={{ base: 6, md: 10, lg: 12 }}
60
+ py={{ base: 10, lg: 12 }}
61
+ align="center"
62
+ justify={{ base: 'center', lg: 'flex-start' }}
63
+ minH={{ base: '100vh', lg: 'auto' }}
64
+ >
65
+ <form onSubmit={handleSubmit} style={{ width: '100%' }}>
66
+ <Box
67
+ w="full"
68
+ maxW={{ base: '360px', lg: '420px' }}
69
+ mx={{ base: 'auto', lg: 0 }}
70
+ display="flex"
71
+ flexDirection="column"
72
+ gap="6"
73
+ >
74
+ <Stack gap="4">
75
+ <Text fontSize="sm" opacity={0.7} letterSpacing="0.2em">
76
+ SIGN IN
77
+ </Text>
78
+ <Text fontSize="2xl" fontWeight="600">
79
+ TradeJS
80
+ </Text>
81
+ </Stack>
82
+
83
+ <Stack gap="4">
84
+ <Field.Root>
85
+ <Field.Label>Username</Field.Label>
86
+ <Input
87
+ value={username}
88
+ onChange={(event) => setUsername(event.target.value)}
89
+ autoComplete="username"
90
+ placeholder="Username"
91
+ />
92
+ </Field.Root>
93
+
94
+ <Field.Root>
95
+ <Field.Label>Password</Field.Label>
96
+ <Input
97
+ value={password}
98
+ onChange={(event) => setPassword(event.target.value)}
99
+ type="password"
100
+ autoComplete="current-password"
101
+ placeholder="Password"
102
+ />
103
+ </Field.Root>
104
+ </Stack>
105
+
106
+ {error ? (
107
+ <Text fontSize="sm" color="red.300">
108
+ {error}
109
+ </Text>
110
+ ) : null}
111
+
112
+ <Button
113
+ type="submit"
114
+ loading={isSubmitting}
115
+ disabled={!username || !password}
116
+ bg="gray.900"
117
+ color="white"
118
+ _hover={{ bg: 'gray.800' }}
119
+ >
120
+ Sign in
121
+ </Button>
122
+ </Box>
123
+ </form>
124
+ </Flex>
125
+
126
+ <Box
127
+ display={{ base: 'none', lg: 'block' }}
128
+ w={{ lg: '66.666%' }}
129
+ minH="100vh"
130
+ bg="gray.900"
131
+ position="relative"
132
+ overflow="hidden"
133
+ >
134
+ <Image
135
+ src="/auth-bg.jpg"
136
+ alt="Market chart background"
137
+ fill
138
+ priority
139
+ sizes="(min-width: 1024px) 66vw, 0vw"
140
+ style={{ objectFit: 'cover', objectPosition: 'center' }}
141
+ />
142
+ </Box>
143
+ </Flex>
144
+ );
145
+ };
146
+
147
+ const SigninFallback = () => <Flex minH="100vh" bg="gray.950" />;
148
+
149
+ const Signin = () => (
150
+ <Suspense fallback={<SigninFallback />}>
151
+ <SigninContent />
152
+ </Suspense>
153
+ );
154
+
155
+ export default Signin;
@@ -0,0 +1,144 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { create } from 'zustand';
3
+ import _ from 'lodash';
4
+ import { get, set } from 'idb-keyval';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import { KlineChartData, Interval, Filters, Provider } from '@tradejs/types';
7
+ import { kline } from '@actions/kline';
8
+ import { isWrongData, mergeData } from '@tradejs/core/data';
9
+
10
+ interface DataState {
11
+ data: Map<string, KlineChartData | null>;
12
+ setData: (
13
+ provider: Provider,
14
+ symbol: string,
15
+ interval: Interval,
16
+ data: KlineChartData,
17
+ ) => void;
18
+ }
19
+
20
+ const getKey = (filters: Pick<Filters, 'provider' | 'symbol' | 'interval'>) =>
21
+ `${filters.provider || 'bybit'}_${filters.symbol}_${filters.interval}`;
22
+
23
+ const useDataStore = create<DataState>((set) => ({
24
+ data: new Map<string, KlineChartData | null>(),
25
+ setData: (provider, symbol, interval, newData) =>
26
+ set(({ data }) => {
27
+ const next = new Map(data);
28
+
29
+ next.set(getKey({ provider, symbol, interval }), newData);
30
+
31
+ return {
32
+ data: next,
33
+ };
34
+ }),
35
+ }));
36
+
37
+ export const useData = (filters: Filters) => {
38
+ const key = getKey(filters);
39
+ const prevKey = useRef(key);
40
+ const retried = useRef(false);
41
+ const [fulfilled, setFulfilled] = useState(false);
42
+ const storedData = useDataStore((s) => s.data.get(key));
43
+ const setData = useDataStore((s) => s.setData);
44
+
45
+ const searchParams = useSearchParams();
46
+ const cacheOnly = Boolean(searchParams.get('cacheOnly')) ?? false;
47
+
48
+ useEffect(() => {
49
+ if (key !== prevKey.current) {
50
+ setFulfilled(false);
51
+ prevKey.current = key;
52
+ retried.current = false;
53
+ }
54
+
55
+ const updateData = async () => {
56
+ const { provider = 'bybit', symbol, interval, start, end } = filters;
57
+ if (!symbol) {
58
+ if (!fulfilled) {
59
+ setFulfilled(true);
60
+ }
61
+ return;
62
+ }
63
+ let currentData = [...(storedData ?? [])];
64
+
65
+ if (!currentData || currentData.length < 2) {
66
+ const cachedResult = (await get(key)) as KlineChartData | null;
67
+
68
+ if (cachedResult && cachedResult.length > 2) {
69
+ currentData = [...cachedResult];
70
+ }
71
+ }
72
+
73
+ if (currentData?.length > 2 && isWrongData(interval, currentData)) {
74
+ console.warn('Wrong kline continuity, drop cache', symbol, interval);
75
+ currentData = [];
76
+ set(key, []);
77
+ }
78
+
79
+ const normStart = Math.max(
80
+ start,
81
+ currentData?.length > 2
82
+ ? currentData[currentData.length - 2]?.timestamp || 0
83
+ : 0,
84
+ );
85
+
86
+ const newData = await kline({
87
+ provider,
88
+ symbol,
89
+ interval,
90
+ start: normStart,
91
+ end,
92
+ cacheOnly,
93
+ });
94
+
95
+ const finalData = mergeData(currentData, newData);
96
+
97
+ if (
98
+ !cacheOnly &&
99
+ !retried.current &&
100
+ finalData.length > 2 &&
101
+ isWrongData(interval, finalData)
102
+ ) {
103
+ console.warn(
104
+ 'Wrong kline continuity after merge, refetch full',
105
+ symbol,
106
+ interval,
107
+ );
108
+ retried.current = true;
109
+ set(key, []);
110
+ const refetchData = await kline({
111
+ provider,
112
+ symbol,
113
+ interval,
114
+ start,
115
+ end,
116
+ cacheOnly,
117
+ });
118
+ const cleaned = mergeData([], refetchData);
119
+ setData(provider as Provider, symbol, interval, cleaned);
120
+ if (!fulfilled) {
121
+ setFulfilled(true);
122
+ }
123
+ set(key, cleaned);
124
+ return;
125
+ }
126
+
127
+ setData(provider as Provider, symbol, interval, finalData);
128
+
129
+ if (!fulfilled) {
130
+ setFulfilled(true);
131
+ }
132
+
133
+ set(key, finalData);
134
+ };
135
+
136
+ void updateData();
137
+ }, [cacheOnly, filters, fulfilled, key, setData, storedData]);
138
+
139
+ return {
140
+ key,
141
+ data: storedData ?? [],
142
+ fulfilled,
143
+ };
144
+ };
@@ -0,0 +1,29 @@
1
+ import { create } from 'zustand';
2
+ import { DASHBOARD_PRELOAD_DAYS } from '@tradejs/core/constants';
3
+ import { getTimestamp } from '@tradejs/core/time';
4
+ import { Interval, UIFilters } from '@tradejs/types';
5
+
6
+ interface FiltersState {
7
+ filters: UIFilters;
8
+ setFilters: (filters: Partial<UIFilters>) => void;
9
+ }
10
+
11
+ const useStore = create<FiltersState>((set) => ({
12
+ filters: {
13
+ provider: 'bybit',
14
+ symbol: 'BTCUSDT',
15
+ interval: '15' as Interval,
16
+ start: getTimestamp(DASHBOARD_PRELOAD_DAYS),
17
+ end: getTimestamp(),
18
+ backtestId: null,
19
+ backtestStrategy: null,
20
+ } as UIFilters,
21
+ setFilters: (newFilters) =>
22
+ set(({ filters }) => ({ filters: { ...filters, ...newFilters } })),
23
+ }));
24
+
25
+ export const useFilters = () => {
26
+ const filters = useStore((s) => s.filters);
27
+ const setFilters = useStore((s) => s.setFilters);
28
+ return { filters, setFilters };
29
+ };
@@ -0,0 +1,13 @@
1
+ export { useFilters } from './filters';
2
+ export { useTickers } from './tickers';
3
+ export { useIndicators } from './indicators';
4
+ export type { IndicatorRendererConfig } from './indicators';
5
+ export { useData } from './data';
6
+ export {
7
+ useTest,
8
+ useTestsCompare,
9
+ useTestList,
10
+ useFavoriteTests,
11
+ useBacktest,
12
+ useBacktestMutations,
13
+ } from './tests';