@tradejs/app 1.0.5 → 1.0.8
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 +129 -20
- package/package.json +13 -12
- package/public/auth-bg.jpg +0 -0
- package/public/next.svg +1 -0
- package/public/og-image-source.svg +91 -0
- package/public/og-image.png +0 -0
- package/public/vercel.svg +1 -0
- package/src/app/api/ai/route.ts +84 -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 +357 -29
- package/src/app/api/scanner/[provider]/route.ts +7 -1
- package/src/app/api/scanner/route.ts +7 -1
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +6 -0
- package/src/app/api/user/settings/route.ts +244 -0
- 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 +948 -0
- package/src/app/components/Shared/Sidebar/index.tsx +13 -9
- 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/globals.css +11 -0
- package/src/app/layout.tsx +50 -11
- package/src/app/lib/currentUser.ts +27 -0
- package/src/app/lib/klineWindow.ts +17 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
- package/src/app/routes/signin/page.tsx +11 -2
- package/src/app/store/ai.ts +174 -0
- package/src/app/store/data.ts +219 -88
- 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/store/tickers.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { useEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import { persist } from 'zustand/middleware';
|
|
4
|
+
import { get, set } from 'idb-keyval';
|
|
4
5
|
import _ from 'lodash';
|
|
5
6
|
import { scan } from '@actions/scanner';
|
|
6
7
|
import { Items } from '@tradejs/types';
|
|
7
8
|
|
|
8
9
|
const LOCAL_STORAGE_KEY = 'tickers';
|
|
10
|
+
const TICKERS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
11
|
+
const EMPTY_ITEMS: Items = [];
|
|
12
|
+
|
|
13
|
+
type TickersCacheRecord = {
|
|
14
|
+
savedAt: number;
|
|
15
|
+
items: Items;
|
|
16
|
+
};
|
|
9
17
|
|
|
10
18
|
interface FavoriteTickersState {
|
|
11
19
|
favorites: string[];
|
|
@@ -35,38 +43,118 @@ const useFavoriteTickersStore = create<FavoriteTickersState>()(
|
|
|
35
43
|
);
|
|
36
44
|
|
|
37
45
|
interface TickersScannerState {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
tickersByProvider: Record<string, Items>;
|
|
47
|
+
loadedAtByProvider: Record<string, number>;
|
|
48
|
+
inFlightByProvider: Record<string, Promise<Items> | undefined>;
|
|
49
|
+
setTickers: (provider: string, tickers: Items, loadedAt?: number) => void;
|
|
50
|
+
setInFlight: (provider: string, request?: Promise<Items>) => void;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
const useScannerStore = create<TickersScannerState>((set) => ({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
tickersByProvider: {},
|
|
55
|
+
loadedAtByProvider: {},
|
|
56
|
+
inFlightByProvider: {},
|
|
57
|
+
setTickers: (provider, coins, loadedAt = Date.now()) =>
|
|
58
|
+
set((state) => ({
|
|
59
|
+
tickersByProvider: {
|
|
60
|
+
...state.tickersByProvider,
|
|
61
|
+
[provider]: _.sortBy(coins, 'label'),
|
|
62
|
+
},
|
|
63
|
+
loadedAtByProvider: {
|
|
64
|
+
...state.loadedAtByProvider,
|
|
65
|
+
[provider]: loadedAt,
|
|
66
|
+
},
|
|
67
|
+
inFlightByProvider: {
|
|
68
|
+
...state.inFlightByProvider,
|
|
69
|
+
[provider]: undefined,
|
|
70
|
+
},
|
|
71
|
+
})),
|
|
72
|
+
setInFlight: (provider, request) =>
|
|
73
|
+
set((state) => ({
|
|
74
|
+
inFlightByProvider: {
|
|
75
|
+
...state.inFlightByProvider,
|
|
76
|
+
[provider]: request,
|
|
77
|
+
},
|
|
78
|
+
})),
|
|
48
79
|
}));
|
|
49
80
|
|
|
50
|
-
|
|
81
|
+
const getTickersCacheKey = (provider: string) => `tickers:${provider}`;
|
|
82
|
+
|
|
83
|
+
const isFresh = (savedAt: number) =>
|
|
84
|
+
Date.now() - savedAt < TICKERS_CACHE_TTL_MS;
|
|
85
|
+
|
|
86
|
+
const loadTickersForProvider = async (provider: string) => {
|
|
87
|
+
const {
|
|
88
|
+
tickersByProvider,
|
|
89
|
+
loadedAtByProvider,
|
|
90
|
+
inFlightByProvider,
|
|
91
|
+
setTickers,
|
|
92
|
+
setInFlight,
|
|
93
|
+
} = useScannerStore.getState();
|
|
94
|
+
const memoryTickers = tickersByProvider[provider] ?? [];
|
|
95
|
+
const loadedAt = loadedAtByProvider[provider] ?? 0;
|
|
96
|
+
|
|
97
|
+
if (memoryTickers.length && isFresh(loadedAt)) {
|
|
98
|
+
return memoryTickers;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const inFlight = inFlightByProvider[provider];
|
|
102
|
+
if (inFlight) {
|
|
103
|
+
return inFlight;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pending = (async () => {
|
|
107
|
+
const cached = (await get(
|
|
108
|
+
getTickersCacheKey(provider),
|
|
109
|
+
)) as TickersCacheRecord | null;
|
|
110
|
+
|
|
111
|
+
if (cached?.items?.length && isFresh(cached.savedAt)) {
|
|
112
|
+
setTickers(provider, cached.items, cached.savedAt);
|
|
113
|
+
return cached.items;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const coins = await scan(provider);
|
|
117
|
+
const savedAt = Date.now();
|
|
118
|
+
setTickers(provider, coins, savedAt);
|
|
119
|
+
await set(getTickersCacheKey(provider), {
|
|
120
|
+
savedAt,
|
|
121
|
+
items: coins,
|
|
122
|
+
} satisfies TickersCacheRecord);
|
|
123
|
+
return coins;
|
|
124
|
+
})().finally(() => {
|
|
125
|
+
useScannerStore.getState().setInFlight(provider, undefined);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
setInFlight(provider, pending);
|
|
129
|
+
return pending;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const useTickers = (
|
|
133
|
+
provider = 'bybit',
|
|
134
|
+
options: { enabled?: boolean } = {},
|
|
135
|
+
) => {
|
|
136
|
+
const { enabled = true } = options;
|
|
51
137
|
const favorites = useFavoriteTickersStore((s) => s.favorites);
|
|
52
|
-
const
|
|
53
|
-
|
|
138
|
+
const tickers = useScannerStore(
|
|
139
|
+
(s) => s.tickersByProvider[provider] ?? EMPTY_ITEMS,
|
|
140
|
+
);
|
|
54
141
|
const toggleFavorite = useFavoriteTickersStore((s) => s.toggleFavorite);
|
|
55
|
-
const setTickers = useScannerStore((s) => s.setTickers);
|
|
56
142
|
const checkIsFavorite = useCallback(
|
|
57
143
|
(ticker: string) => favorites.includes(ticker),
|
|
58
144
|
[favorites],
|
|
59
145
|
);
|
|
146
|
+
const ensureLoaded = useCallback(
|
|
147
|
+
() => loadTickersForProvider(provider),
|
|
148
|
+
[provider],
|
|
149
|
+
);
|
|
60
150
|
|
|
61
151
|
useEffect(() => {
|
|
62
|
-
if (
|
|
152
|
+
if (!enabled) {
|
|
63
153
|
return;
|
|
64
154
|
}
|
|
65
155
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
}, [loadedProvider, provider, setTickers, tickers.length]);
|
|
156
|
+
void ensureLoaded();
|
|
157
|
+
}, [enabled, ensureLoaded]);
|
|
70
158
|
|
|
71
159
|
const items = useMemo(() => {
|
|
72
160
|
const favoriteItems = tickers
|
|
@@ -84,6 +172,14 @@ export const useTickers = (provider = 'bybit') => {
|
|
|
84
172
|
favorites,
|
|
85
173
|
checkIsFavorite,
|
|
86
174
|
toggleFavorite,
|
|
87
|
-
|
|
175
|
+
ensureLoaded,
|
|
88
176
|
};
|
|
89
177
|
};
|
|
178
|
+
|
|
179
|
+
export const resetTickersStoreForTests = () => {
|
|
180
|
+
useScannerStore.setState({
|
|
181
|
+
tickersByProvider: {},
|
|
182
|
+
loadedAtByProvider: {},
|
|
183
|
+
inFlightByProvider: {},
|
|
184
|
+
});
|
|
185
|
+
};
|
package/src/proxy.ts
CHANGED
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
2
|
import { encode, getToken } from 'next-auth/jwt';
|
|
3
|
-
import {
|
|
3
|
+
import { consumeScreenshotSessionToken } from '@tradejs/infra/redis';
|
|
4
4
|
|
|
5
5
|
const SIGNIN_PATH = '/routes/signin';
|
|
6
6
|
const SCREENSHOT_API_PREFIX = '/api/files/screenshot';
|
|
7
|
+
const SCREENSHOT_SESSION_QUERY_PARAM = 'screenshotToken';
|
|
7
8
|
const SESSION_COOKIE_NAME = 'authjs.session-token';
|
|
8
9
|
const SECURE_SESSION_COOKIE_NAME = '__Secure-authjs.session-token';
|
|
9
10
|
const SESSION_MAX_AGE = 30 * 24 * 60 * 60;
|
|
10
|
-
const PUBLIC_FILE_RE = /\.(?:png|jpg|jpeg|webp|gif|svg|ico|txt|xml|
|
|
11
|
-
|
|
12
|
-
const getRootToken = async (): Promise<string | null> => {
|
|
13
|
-
const rootUser = await getData(redisKeys.user('root'), null);
|
|
14
|
-
if (!rootUser || typeof rootUser !== 'object') return null;
|
|
15
|
-
const record = rootUser as Record<string, unknown>;
|
|
16
|
-
const token = record.token;
|
|
17
|
-
return typeof token === 'string' ? token : null;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const findUserByToken = async (token: string): Promise<string | null> => {
|
|
21
|
-
const keys = await getKeys(redisKeys.users());
|
|
22
|
-
for (const key of keys) {
|
|
23
|
-
const userNameFromKey = key.slice(redisKeys.users().length);
|
|
24
|
-
if (!userNameFromKey) continue;
|
|
25
|
-
|
|
26
|
-
const user = await getData(redisKeys.user(userNameFromKey), null);
|
|
27
|
-
if (!user || typeof user !== 'object') continue;
|
|
28
|
-
const record = user as Record<string, unknown>;
|
|
29
|
-
if (record.token !== token) continue;
|
|
30
|
-
const userName =
|
|
31
|
-
typeof record.userName === 'string' ? record.userName : userNameFromKey;
|
|
32
|
-
if (userName) return userName;
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
};
|
|
11
|
+
const PUBLIC_FILE_RE = /\.(?:png|jpg|jpeg|webp|gif|svg|ico|txt|xml|map)$/i;
|
|
36
12
|
|
|
37
13
|
const isSecureRequest = (req: NextRequest) => {
|
|
38
14
|
if (req.nextUrl.protocol === 'https:') return true;
|
|
@@ -46,6 +22,7 @@ const issueSession = async (
|
|
|
46
22
|
req: NextRequest,
|
|
47
23
|
userName: string,
|
|
48
24
|
response: NextResponse,
|
|
25
|
+
maxAge = SESSION_MAX_AGE,
|
|
49
26
|
) => {
|
|
50
27
|
const secret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;
|
|
51
28
|
if (!secret) return response;
|
|
@@ -56,7 +33,7 @@ const issueSession = async (
|
|
|
56
33
|
token: { id: userName, name: userName },
|
|
57
34
|
secret,
|
|
58
35
|
salt: cookieName,
|
|
59
|
-
maxAge
|
|
36
|
+
maxAge,
|
|
60
37
|
});
|
|
61
38
|
|
|
62
39
|
response.cookies.set(cookieName, jwt, {
|
|
@@ -64,7 +41,7 @@ const issueSession = async (
|
|
|
64
41
|
sameSite: 'lax',
|
|
65
42
|
path: '/',
|
|
66
43
|
secure,
|
|
67
|
-
maxAge
|
|
44
|
+
maxAge,
|
|
68
45
|
});
|
|
69
46
|
|
|
70
47
|
return response;
|
|
@@ -79,33 +56,29 @@ export const proxy = async (req: NextRequest) => {
|
|
|
79
56
|
pathname.startsWith(SIGNIN_PATH) ||
|
|
80
57
|
pathname.startsWith('/api/auth') ||
|
|
81
58
|
pathname.startsWith(SCREENSHOT_API_PREFIX) ||
|
|
82
|
-
PUBLIC_FILE_RE.test(pathname)
|
|
59
|
+
(!pathname.startsWith('/api') && PUBLIC_FILE_RE.test(pathname))
|
|
83
60
|
) {
|
|
84
61
|
return NextResponse.next();
|
|
85
62
|
}
|
|
86
63
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: NextResponse.next();
|
|
97
|
-
return issueSession(req, 'root', res);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const userName = await findUserByToken(queryToken);
|
|
64
|
+
const screenshotToken = req.nextUrl.searchParams.get(
|
|
65
|
+
SCREENSHOT_SESSION_QUERY_PARAM,
|
|
66
|
+
);
|
|
67
|
+
if (
|
|
68
|
+
screenshotToken &&
|
|
69
|
+
req.method === 'GET' &&
|
|
70
|
+
pathname.startsWith('/routes/dashboard/')
|
|
71
|
+
) {
|
|
72
|
+
const userName = await consumeScreenshotSessionToken(screenshotToken);
|
|
101
73
|
if (userName) {
|
|
102
74
|
const cleaned = new URL(req.url);
|
|
103
|
-
cleaned.searchParams.delete(
|
|
104
|
-
|
|
105
|
-
req
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
75
|
+
cleaned.searchParams.delete(SCREENSHOT_SESSION_QUERY_PARAM);
|
|
76
|
+
return issueSession(
|
|
77
|
+
req,
|
|
78
|
+
userName,
|
|
79
|
+
NextResponse.redirect(cleaned),
|
|
80
|
+
15 * 60,
|
|
81
|
+
);
|
|
109
82
|
}
|
|
110
83
|
}
|
|
111
84
|
|