@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.
Files changed (37) hide show
  1. package/bin/tradejs-app.mjs +129 -20
  2. package/package.json +13 -12
  3. package/public/auth-bg.jpg +0 -0
  4. package/public/next.svg +1 -0
  5. package/public/og-image-source.svg +91 -0
  6. package/public/og-image.png +0 -0
  7. package/public/vercel.svg +1 -0
  8. package/src/app/api/ai/route.ts +84 -20
  9. package/src/app/api/backtest/files/route.ts +12 -1
  10. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +18 -1
  11. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +357 -29
  12. package/src/app/api/scanner/[provider]/route.ts +7 -1
  13. package/src/app/api/scanner/route.ts +7 -1
  14. package/src/app/api/signal/[symbol]/[signalId]/route.ts +6 -0
  15. package/src/app/api/user/settings/route.ts +244 -0
  16. package/src/app/components/Dashboard/AiDrawer/index.tsx +38 -51
  17. package/src/app/components/Shared/Filters/Backtest/index.tsx +12 -5
  18. package/src/app/components/Shared/Filters/Root/index.tsx +12 -1
  19. package/src/app/components/Shared/Filters/Symbol/index.tsx +14 -19
  20. package/src/app/components/Shared/Filters/context.ts +2 -0
  21. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +948 -0
  22. package/src/app/components/Shared/Sidebar/index.tsx +13 -9
  23. package/src/app/components/UI/ColorMode/index.tsx +62 -15
  24. package/src/app/components/UI/Select/index.tsx +3 -0
  25. package/src/app/components/UI/SelectWithSearch/index.tsx +3 -0
  26. package/src/app/globals.css +11 -0
  27. package/src/app/layout.tsx +50 -11
  28. package/src/app/lib/currentUser.ts +27 -0
  29. package/src/app/lib/klineWindow.ts +17 -0
  30. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
  31. package/src/app/routes/signin/page.tsx +11 -2
  32. package/src/app/store/ai.ts +174 -0
  33. package/src/app/store/data.ts +219 -88
  34. package/src/app/store/index.ts +1 -0
  35. package/src/app/store/tests.ts +96 -9
  36. package/src/app/store/tickers.ts +113 -17
  37. package/src/proxy.ts +23 -50
@@ -4,6 +4,7 @@ import { Box, Flex, IconButton, VStack } from '@chakra-ui/react';
4
4
  import { useRouter, usePathname } from 'next/navigation';
5
5
  import { signOut } from 'next-auth/react';
6
6
  import { FiActivity, FiBarChart2, FiLogOut, FiPlay } from 'react-icons/fi';
7
+ import { AccountSettingsDrawer } from './AccountSettingsDrawer';
7
8
 
8
9
  export const Sidebar = () => {
9
10
  const router = useRouter();
@@ -57,15 +58,18 @@ export const Sidebar = () => {
57
58
  ))}
58
59
  </VStack>
59
60
 
60
- <IconButton
61
- aria-label="Sign out"
62
- size="md"
63
- colorPalette="teal"
64
- variant="outline"
65
- onClick={() => signOut({ callbackUrl: '/routes/signin' })}
66
- >
67
- <FiLogOut />
68
- </IconButton>
61
+ <VStack gap={2}>
62
+ <AccountSettingsDrawer />
63
+ <IconButton
64
+ aria-label="Sign out"
65
+ size="md"
66
+ colorPalette="teal"
67
+ variant="outline"
68
+ onClick={() => signOut({ callbackUrl: '/routes/signin' })}
69
+ >
70
+ <FiLogOut />
71
+ </IconButton>
72
+ </VStack>
69
73
  </Flex>
70
74
  </Box>
71
75
  );
@@ -8,18 +8,9 @@ import {
8
8
  type IconButtonProps,
9
9
  type SpanProps,
10
10
  } from '@chakra-ui/react';
11
- import { ThemeProvider, useTheme, type ThemeProviderProps } from 'next-themes';
12
11
  import * as React from 'react';
13
12
  import { LuMoon, LuSun } from 'react-icons/lu';
14
13
 
15
- export interface ColorModeProviderProps extends ThemeProviderProps {}
16
-
17
- export function ColorModeProvider(props: ColorModeProviderProps) {
18
- return (
19
- <ThemeProvider attribute="class" disableTransitionOnChange {...props} />
20
- );
21
- }
22
-
23
14
  export type ColorMode = 'light' | 'dark';
24
15
 
25
16
  export interface UseColorModeReturn {
@@ -28,16 +19,72 @@ export interface UseColorModeReturn {
28
19
  toggleColorMode: () => void;
29
20
  }
30
21
 
31
- export function useColorMode(): UseColorModeReturn {
32
- const { resolvedTheme, setTheme } = useTheme();
22
+ export interface ColorModeProviderProps {
23
+ children: React.ReactNode;
24
+ forcedTheme?: ColorMode;
25
+ }
26
+
27
+ const ColorModeContext = React.createContext<UseColorModeReturn | null>(null);
28
+
29
+ export function ColorModeProvider({
30
+ children,
31
+ forcedTheme,
32
+ }: ColorModeProviderProps) {
33
+ const [colorMode, setColorModeState] = React.useState<ColorMode>(
34
+ forcedTheme ?? 'dark',
35
+ );
36
+
37
+ React.useEffect(() => {
38
+ if (!forcedTheme) {
39
+ return;
40
+ }
41
+
42
+ setColorModeState(forcedTheme);
43
+ }, [forcedTheme]);
44
+
45
+ React.useEffect(() => {
46
+ const root = document.documentElement;
47
+ const nextColorMode = forcedTheme ?? colorMode;
48
+ const previousColorMode = nextColorMode === 'dark' ? 'light' : 'dark';
49
+
50
+ root.classList.remove(previousColorMode);
51
+ root.classList.add(nextColorMode);
52
+ root.style.colorScheme = nextColorMode;
53
+ }, [colorMode, forcedTheme]);
54
+
55
+ const setColorMode = (nextColorMode: ColorMode) => {
56
+ if (forcedTheme) {
57
+ return;
58
+ }
59
+
60
+ setColorModeState(nextColorMode);
61
+ };
62
+
33
63
  const toggleColorMode = () => {
34
- setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
64
+ setColorMode(colorMode === 'dark' ? 'light' : 'dark');
35
65
  };
36
- return {
37
- colorMode: resolvedTheme as ColorMode,
38
- setColorMode: setTheme,
66
+
67
+ const value: UseColorModeReturn = {
68
+ colorMode: forcedTheme ?? colorMode,
69
+ setColorMode,
39
70
  toggleColorMode,
40
71
  };
72
+
73
+ return (
74
+ <ColorModeContext.Provider value={value}>
75
+ {children}
76
+ </ColorModeContext.Provider>
77
+ );
78
+ }
79
+
80
+ export function useColorMode(): UseColorModeReturn {
81
+ return (
82
+ React.useContext(ColorModeContext) ?? {
83
+ colorMode: 'dark',
84
+ setColorMode: () => {},
85
+ toggleColorMode: () => {},
86
+ }
87
+ );
41
88
  }
42
89
 
43
90
  export function useColorModeValue<T>(light: T, dark: T) {
@@ -19,6 +19,7 @@ interface SelectProps {
19
19
  multiple?: boolean;
20
20
  size?: 'xs' | 'sm' | 'md' | 'lg';
21
21
  onChange?: (value: string[]) => void;
22
+ onOpenChange?: (open: boolean) => void;
22
23
  }
23
24
 
24
25
  export const Select = ({
@@ -30,6 +31,7 @@ export const Select = ({
30
31
  width = '320px',
31
32
  size = 'sm',
32
33
  onChange,
34
+ onOpenChange,
33
35
  }: SelectProps) => {
34
36
  const collection = useMemo(
35
37
  () =>
@@ -44,6 +46,7 @@ export const Select = ({
44
46
  collection={collection}
45
47
  {...(value ? { value } : { defaultValue })}
46
48
  onValueChange={(details) => onChange?.(details.value)}
49
+ onOpenChange={(details) => onOpenChange?.(details.open)}
47
50
  size={size}
48
51
  multiple={multiple}
49
52
  width={width}
@@ -23,6 +23,7 @@ interface SelectWithSearchProps {
23
23
  multiple?: boolean;
24
24
  size?: 'xs' | 'sm' | 'md' | 'lg';
25
25
  onChange?: (value: string[]) => void;
26
+ onOpenChange?: (open: boolean) => void;
26
27
  }
27
28
 
28
29
  export const SelectWithSearch = ({
@@ -35,6 +36,7 @@ export const SelectWithSearch = ({
35
36
  width = '320px',
36
37
  size = 'sm',
37
38
  onChange,
39
+ onOpenChange,
38
40
  }: SelectWithSearchProps) => {
39
41
  const { contains } = useFilter({ sensitivity: 'base' });
40
42
  const [inputValue, setInputValue] = useState(
@@ -61,6 +63,7 @@ export const SelectWithSearch = ({
61
63
  setInputValue(e.inputValue);
62
64
  }}
63
65
  onOpenChange={(details) => {
66
+ onOpenChange?.(details.open);
64
67
  if (details.open) {
65
68
  filter('');
66
69
  setInputValue('');
@@ -3,3 +3,14 @@
3
3
  [data-nextjs-devtools] {
4
4
  left: 76px !important;
5
5
  } */
6
+
7
+ body {
8
+ font-family:
9
+ Inter,
10
+ ui-sans-serif,
11
+ system-ui,
12
+ -apple-system,
13
+ BlinkMacSystemFont,
14
+ 'Segoe UI',
15
+ sans-serif;
16
+ }
@@ -1,15 +1,51 @@
1
1
  import type { Metadata } from 'next';
2
- import { Inter } from 'next/font/google';
3
- import { ClientOnly } from '@chakra-ui/react';
4
2
  import { AppShell } from '@shared/AppShell';
5
3
  import Provider from './provider';
6
4
  import './globals.css';
7
5
 
8
- const inter = Inter({ subsets: ['latin'] });
6
+ const fallbackMetadataBase = 'http://localhost:3000';
7
+
8
+ const metadataBase = (() => {
9
+ const rawAppUrl = String(process.env.APP_URL || '').trim();
10
+
11
+ if (!rawAppUrl) {
12
+ return new URL(fallbackMetadataBase);
13
+ }
14
+
15
+ try {
16
+ return new URL(rawAppUrl);
17
+ } catch {
18
+ return new URL(fallbackMetadataBase);
19
+ }
20
+ })();
9
21
 
10
22
  export const metadata: Metadata = {
23
+ metadataBase,
11
24
  title: 'TradeJS App',
12
- description: 'Trading Strategies Framework',
25
+ description:
26
+ 'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
27
+ applicationName: 'TradeJS App',
28
+ openGraph: {
29
+ title: 'TradeJS App',
30
+ description:
31
+ 'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
32
+ type: 'website',
33
+ images: [
34
+ {
35
+ url: '/og-image.png',
36
+ width: 1200,
37
+ height: 630,
38
+ alt: 'TradeJS App',
39
+ },
40
+ ],
41
+ },
42
+ twitter: {
43
+ card: 'summary_large_image',
44
+ title: 'TradeJS App',
45
+ description:
46
+ 'Dashboards, backtests, charts, derivatives, and runtime data in one UI.',
47
+ images: ['/og-image.png'],
48
+ },
13
49
  };
14
50
 
15
51
  export default function RootLayout({
@@ -18,13 +54,16 @@ export default function RootLayout({
18
54
  children: React.ReactNode;
19
55
  }>) {
20
56
  return (
21
- <html lang="en" suppressHydrationWarning>
22
- <body className={inter.className}>
23
- <ClientOnly>
24
- <Provider>
25
- <AppShell>{children}</AppShell>
26
- </Provider>
27
- </ClientOnly>
57
+ <html
58
+ lang="en"
59
+ className="dark"
60
+ style={{ colorScheme: 'dark' }}
61
+ suppressHydrationWarning
62
+ >
63
+ <body suppressHydrationWarning>
64
+ <Provider>
65
+ <AppShell>{children}</AppShell>
66
+ </Provider>
28
67
  </body>
29
68
  </html>
30
69
  );
@@ -0,0 +1,27 @@
1
+ import { auth } from '@app/auth';
2
+
3
+ type SessionLike = {
4
+ user?: {
5
+ id?: string;
6
+ name?: string | null;
7
+ };
8
+ } | null;
9
+
10
+ const readSessionUserName = (session: SessionLike) => {
11
+ const fromId = session?.user?.id;
12
+ if (typeof fromId === 'string' && fromId.trim()) {
13
+ return fromId.trim();
14
+ }
15
+
16
+ const fromName = session?.user?.name;
17
+ if (typeof fromName === 'string' && fromName.trim()) {
18
+ return fromName.trim();
19
+ }
20
+
21
+ return null;
22
+ };
23
+
24
+ export const getCurrentUserName = async (): Promise<string | null> => {
25
+ const session = (await auth()) as SessionLike;
26
+ return readSessionUserName(session);
27
+ };
@@ -0,0 +1,17 @@
1
+ import { intervalToMs } from '@tradejs/core/data';
2
+ import { Interval } from '@tradejs/types';
3
+
4
+ export const normalizeEndToIntervalBoundary = (
5
+ end: number,
6
+ interval: Interval,
7
+ ): number => {
8
+ const stepMs = intervalToMs(interval);
9
+ if (!Number.isFinite(end) || stepMs <= 0) {
10
+ return end;
11
+ }
12
+
13
+ return Math.floor(end / stepMs) * stepMs;
14
+ };
15
+
16
+ export const getCurrentIntervalBoundary = (interval: Interval) =>
17
+ normalizeEndToIntervalBoundary(Date.now(), interval);
@@ -11,8 +11,14 @@ import { Interval, OnChangeFilters, Provider } from '@tradejs/types';
11
11
  const Dashboard = () => {
12
12
  const searchParams = useSearchParams();
13
13
  const { filters, setFilters } = useFilters();
14
- const { tickers } = useTickers(filters.provider || 'bybit');
15
- const { tests } = useTestList({ symbol: filters.symbol });
14
+ const { tickers, ensureLoaded: ensureTickersLoaded } = useTickers(
15
+ filters.provider || 'bybit',
16
+ { enabled: false },
17
+ );
18
+ const { tests, ensureLoaded: ensureBacktestsLoaded } = useTestList({
19
+ symbol: filters.symbol,
20
+ enabled: false,
21
+ });
16
22
  const hasBacktestId = searchParams.has('backtestId');
17
23
  const hasBacktestStrategy = searchParams.has('backtestStrategy');
18
24
  const backtestId = searchParams.get('backtestId');
@@ -103,6 +109,8 @@ const Dashboard = () => {
103
109
  tickers={tickers}
104
110
  backtestFiles={tests}
105
111
  onChangeFilters={onChangeFilters}
112
+ ensureTickersLoaded={ensureTickersLoaded}
113
+ ensureBacktestsLoaded={ensureBacktestsLoaded}
106
114
  >
107
115
  <Flex mb={2} gap={4} alignItems="center" flexDirection="row">
108
116
  <Filters.SelectProvider />
@@ -75,8 +75,17 @@ const SigninContent = () => {
75
75
  <Text fontSize="sm" opacity={0.7} letterSpacing="0.2em">
76
76
  SIGN IN
77
77
  </Text>
78
- <Text fontSize="2xl" fontWeight="600">
79
- TradeJS
78
+ <Text
79
+ fontSize="2xl"
80
+ fontWeight="700"
81
+ letterSpacing="-0.03em"
82
+ lineHeight="1"
83
+ color="white"
84
+ >
85
+ <Box as="span">Trade</Box>
86
+ <Box as="span" color="#20c5bd">
87
+ JS
88
+ </Box>
80
89
  </Text>
81
90
  </Stack>
82
91
 
@@ -0,0 +1,174 @@
1
+ import { create } from 'zustand';
2
+ import { getHistory, sendMessage } from '@actions/ai';
3
+ import { AIChatHistory, AIChatMessage, Filters } from '@tradejs/types';
4
+
5
+ type AiChatEntry = {
6
+ loading: boolean;
7
+ sending: boolean;
8
+ loaded: boolean;
9
+ error: string | null;
10
+ messages: AIChatHistory;
11
+ };
12
+
13
+ interface AiChatState {
14
+ chats: Record<string, AiChatEntry>;
15
+ getChat: (symbol: string) => AiChatEntry;
16
+ loadHistory: (symbol: string) => Promise<void>;
17
+ sendPrompt: (filters: Filters, input: string) => Promise<void>;
18
+ sendQuickCommand: (filters: Filters, command: string) => Promise<void>;
19
+ }
20
+
21
+ const EMPTY_CHAT: AiChatEntry = {
22
+ loading: false,
23
+ sending: false,
24
+ loaded: false,
25
+ error: null,
26
+ messages: [],
27
+ };
28
+
29
+ const normalizeSymbolKey = (symbol: string) => symbol.trim().toUpperCase();
30
+
31
+ const getQuickMessage = (command: string): AIChatMessage | null => {
32
+ if (command === '/line') {
33
+ return {
34
+ from: 'user',
35
+ text: 'Какие наклонные линии можно построить на данном графике',
36
+ command,
37
+ };
38
+ }
39
+
40
+ return null;
41
+ };
42
+
43
+ const updateChat = (
44
+ chats: Record<string, AiChatEntry>,
45
+ symbol: string,
46
+ patch: Partial<AiChatEntry>,
47
+ ) => ({
48
+ ...chats,
49
+ [symbol]: {
50
+ ...(chats[symbol] ?? EMPTY_CHAT),
51
+ ...patch,
52
+ },
53
+ });
54
+
55
+ export const useAiChatStore = create<AiChatState>((set, get) => ({
56
+ chats: {},
57
+
58
+ getChat: (symbol) => get().chats[normalizeSymbolKey(symbol)] ?? EMPTY_CHAT,
59
+
60
+ loadHistory: async (symbol) => {
61
+ const symbolKey = normalizeSymbolKey(symbol);
62
+ if (!symbolKey) {
63
+ return;
64
+ }
65
+
66
+ const existing = get().chats[symbolKey];
67
+ if (existing?.loading) {
68
+ return;
69
+ }
70
+
71
+ set((state) => ({
72
+ chats: updateChat(state.chats, symbolKey, {
73
+ loading: true,
74
+ error: null,
75
+ }),
76
+ }));
77
+
78
+ try {
79
+ const history = await getHistory(symbol);
80
+ set((state) => ({
81
+ chats: updateChat(state.chats, symbolKey, {
82
+ loading: false,
83
+ loaded: true,
84
+ messages: history,
85
+ }),
86
+ }));
87
+ } catch (error) {
88
+ set((state) => ({
89
+ chats: updateChat(state.chats, symbolKey, {
90
+ loading: false,
91
+ error: error instanceof Error ? error.message : 'Failed to load chat',
92
+ }),
93
+ }));
94
+ }
95
+ },
96
+
97
+ sendPrompt: async (filters, input) => {
98
+ const trimmed = input.trim();
99
+ if (!trimmed) {
100
+ return;
101
+ }
102
+
103
+ const message: AIChatMessage = {
104
+ from: 'user',
105
+ text: trimmed,
106
+ command: 'prompt',
107
+ };
108
+
109
+ const symbolKey = normalizeSymbolKey(filters.symbol);
110
+
111
+ set((state) => ({
112
+ chats: updateChat(state.chats, symbolKey, {
113
+ sending: true,
114
+ error: null,
115
+ loaded: true,
116
+ messages: [...(state.chats[symbolKey]?.messages ?? []), message],
117
+ }),
118
+ }));
119
+
120
+ try {
121
+ const response = await sendMessage({ message, filters });
122
+ set((state) => ({
123
+ chats: updateChat(state.chats, symbolKey, {
124
+ sending: false,
125
+ messages: [...(state.chats[symbolKey]?.messages ?? []), response],
126
+ }),
127
+ }));
128
+ } catch (error) {
129
+ set((state) => ({
130
+ chats: updateChat(state.chats, symbolKey, {
131
+ sending: false,
132
+ error:
133
+ error instanceof Error ? error.message : 'Failed to send message',
134
+ }),
135
+ }));
136
+ }
137
+ },
138
+
139
+ sendQuickCommand: async (filters, command) => {
140
+ const message = getQuickMessage(command);
141
+ if (!message) {
142
+ return;
143
+ }
144
+
145
+ const symbolKey = normalizeSymbolKey(filters.symbol);
146
+
147
+ set((state) => ({
148
+ chats: updateChat(state.chats, symbolKey, {
149
+ sending: true,
150
+ error: null,
151
+ loaded: true,
152
+ messages: [...(state.chats[symbolKey]?.messages ?? []), message],
153
+ }),
154
+ }));
155
+
156
+ try {
157
+ const response = await sendMessage({ message, filters });
158
+ set((state) => ({
159
+ chats: updateChat(state.chats, symbolKey, {
160
+ sending: false,
161
+ messages: [...(state.chats[symbolKey]?.messages ?? []), response],
162
+ }),
163
+ }));
164
+ } catch (error) {
165
+ set((state) => ({
166
+ chats: updateChat(state.chats, symbolKey, {
167
+ sending: false,
168
+ error:
169
+ error instanceof Error ? error.message : 'Failed to send message',
170
+ }),
171
+ }));
172
+ }
173
+ },
174
+ }));