@tradejs/app 1.0.6 → 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 +7 -0
- package/package.json +12 -12
- package/src/app/api/ai/route.ts +63 -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 +350 -28
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +6 -0
- package/src/app/api/user/settings/route.ts +51 -23
- 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 +250 -111
- 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/layout.tsx +10 -8
- package/src/app/lib/klineWindow.ts +17 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
- package/src/app/store/ai.ts +174 -0
- package/src/app/store/data.ts +34 -8
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
CloseButton,
|
|
@@ -12,74 +12,42 @@ import {
|
|
|
12
12
|
HStack,
|
|
13
13
|
Stack,
|
|
14
14
|
Text,
|
|
15
|
+
Alert,
|
|
15
16
|
SkeletonCircle,
|
|
16
17
|
SkeletonText,
|
|
17
18
|
} from '@chakra-ui/react';
|
|
18
|
-
import { AIChatMessage, AIChatHistory } from '@tradejs/types';
|
|
19
19
|
import { GiArtificialHive } from 'react-icons/gi';
|
|
20
|
-
import { useFilters } from '@store';
|
|
21
|
-
import { sendMessage, getHistory } from '@actions/ai';
|
|
20
|
+
import { useAiChatStore, useFilters } from '@store';
|
|
22
21
|
import { Message } from './Message';
|
|
23
22
|
|
|
24
23
|
export const AiDrawer = () => {
|
|
25
24
|
const [open, setOpen] = useState(false);
|
|
26
|
-
const [loading, setLoading] = useState(true);
|
|
27
25
|
const [input, setInput] = useState('');
|
|
28
|
-
const [messages, setMessages] = useState<AIChatHistory>([]);
|
|
29
26
|
const { filters } = useFilters();
|
|
30
|
-
|
|
31
|
-
const loadHistory =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
const getChat = useAiChatStore((s) => s.getChat);
|
|
28
|
+
const loadHistory = useAiChatStore((s) => s.loadHistory);
|
|
29
|
+
const sendPrompt = useAiChatStore((s) => s.sendPrompt);
|
|
30
|
+
const sendQuickCommand = useAiChatStore((s) => s.sendQuickCommand);
|
|
31
|
+
const chat = getChat(filters.symbol);
|
|
32
|
+
const { loading, sending, error, messages } = chat;
|
|
33
|
+
const isBusy = loading || sending;
|
|
34
|
+
const canSend = useMemo(
|
|
35
|
+
() => input.trim().length > 0 && !sending,
|
|
36
|
+
[input, sending],
|
|
37
|
+
);
|
|
40
38
|
|
|
41
39
|
useEffect(() => {
|
|
42
|
-
void loadHistory();
|
|
43
|
-
}, [loadHistory]);
|
|
40
|
+
void loadHistory(filters.symbol);
|
|
41
|
+
}, [filters.symbol, loadHistory]);
|
|
44
42
|
|
|
45
43
|
const handleSend = async () => {
|
|
46
44
|
if (!input.trim()) return;
|
|
47
|
-
|
|
48
|
-
const message = {
|
|
49
|
-
from: 'user',
|
|
50
|
-
text: input,
|
|
51
|
-
command: 'prompt',
|
|
52
|
-
} as AIChatMessage;
|
|
53
|
-
|
|
54
|
-
setMessages((state) => [...state, message]);
|
|
55
|
-
|
|
56
|
-
const response = await sendMessage({ message, filters });
|
|
57
|
-
|
|
58
|
-
setMessages((state) => [...state, response]);
|
|
59
|
-
|
|
45
|
+
await sendPrompt(filters, input);
|
|
60
46
|
setInput('');
|
|
61
47
|
};
|
|
62
48
|
|
|
63
49
|
const handleQuick = async (command: string) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (command === '/line') {
|
|
67
|
-
message = {
|
|
68
|
-
from: 'user',
|
|
69
|
-
text: 'Какие наклонные линии можно построить на данном графике',
|
|
70
|
-
command,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!message) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
setMessages((state) => [...state, message as AIChatMessage]);
|
|
79
|
-
|
|
80
|
-
const response = await sendMessage({ message, filters });
|
|
81
|
-
|
|
82
|
-
setMessages((state) => [...state, response]);
|
|
50
|
+
await sendQuickCommand(filters, command);
|
|
83
51
|
};
|
|
84
52
|
|
|
85
53
|
return (
|
|
@@ -106,6 +74,15 @@ export const AiDrawer = () => {
|
|
|
106
74
|
</Drawer.Header>
|
|
107
75
|
|
|
108
76
|
<Drawer.Body overflowY="auto" flex="1">
|
|
77
|
+
{error ? (
|
|
78
|
+
<Alert.Root status="error" mb={4}>
|
|
79
|
+
<Alert.Indicator />
|
|
80
|
+
<Alert.Content>
|
|
81
|
+
<Alert.Title>AI chat error</Alert.Title>
|
|
82
|
+
<Alert.Description>{error}</Alert.Description>
|
|
83
|
+
</Alert.Content>
|
|
84
|
+
</Alert.Root>
|
|
85
|
+
) : null}
|
|
109
86
|
{loading ? (
|
|
110
87
|
<Stack gap="4" maxW="xs">
|
|
111
88
|
<HStack width="full">
|
|
@@ -128,6 +105,7 @@ export const AiDrawer = () => {
|
|
|
128
105
|
<Button
|
|
129
106
|
size="sm"
|
|
130
107
|
variant="outline"
|
|
108
|
+
disabled={isBusy}
|
|
131
109
|
onClick={() => handleQuick('/line')}
|
|
132
110
|
>
|
|
133
111
|
/line
|
|
@@ -135,6 +113,7 @@ export const AiDrawer = () => {
|
|
|
135
113
|
<Button
|
|
136
114
|
size="sm"
|
|
137
115
|
variant="outline"
|
|
116
|
+
disabled={isBusy}
|
|
138
117
|
onClick={() => handleQuick('/analyze')}
|
|
139
118
|
>
|
|
140
119
|
/analyze
|
|
@@ -148,10 +127,18 @@ export const AiDrawer = () => {
|
|
|
148
127
|
rows={3}
|
|
149
128
|
maxH="15lh"
|
|
150
129
|
value={input}
|
|
130
|
+
disabled={sending}
|
|
151
131
|
onChange={(e) => setInput(e.target.value)}
|
|
152
132
|
/>
|
|
153
133
|
|
|
154
|
-
<Button
|
|
134
|
+
<Button
|
|
135
|
+
mt={2}
|
|
136
|
+
size={'sm'}
|
|
137
|
+
variant="subtle"
|
|
138
|
+
disabled={!canSend}
|
|
139
|
+
loading={sending}
|
|
140
|
+
onClick={handleSend}
|
|
141
|
+
>
|
|
155
142
|
Send
|
|
156
143
|
</Button>
|
|
157
144
|
</Drawer.Footer>
|
|
@@ -6,7 +6,8 @@ import { Select } from '@UI';
|
|
|
6
6
|
import { useFiltersContext } from '../context';
|
|
7
7
|
|
|
8
8
|
export const SelectBacktest = () => {
|
|
9
|
-
const { filters, backtestFiles, onChangeFilters } =
|
|
9
|
+
const { filters, backtestFiles, onChangeFilters, ensureBacktestsLoaded } =
|
|
10
|
+
useFiltersContext();
|
|
10
11
|
const STORAGE_KEY = 'backtest-strategy';
|
|
11
12
|
|
|
12
13
|
const tests = useMemo(
|
|
@@ -127,10 +128,6 @@ export const SelectBacktest = () => {
|
|
|
127
128
|
}
|
|
128
129
|
};
|
|
129
130
|
|
|
130
|
-
if (_.isEmpty(tests) || _.isEmpty(strategyItems) || !selectedStrategy) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
131
|
const strategyTests = tests.filter(
|
|
135
132
|
(test) => test.data?.strategyName === selectedStrategy,
|
|
136
133
|
);
|
|
@@ -142,6 +139,11 @@ export const SelectBacktest = () => {
|
|
|
142
139
|
defaultValue={[selectedStrategy]}
|
|
143
140
|
value={[selectedStrategy]}
|
|
144
141
|
onChange={onChangeStrategy}
|
|
142
|
+
onOpenChange={(open) => {
|
|
143
|
+
if (open) {
|
|
144
|
+
void ensureBacktestsLoaded?.();
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
145
147
|
items={strategyItems}
|
|
146
148
|
width="220px"
|
|
147
149
|
/>
|
|
@@ -150,6 +152,11 @@ export const SelectBacktest = () => {
|
|
|
150
152
|
defaultValue={[filters.backtestId || '']}
|
|
151
153
|
value={[filters.backtestId || '']}
|
|
152
154
|
onChange={onChange}
|
|
155
|
+
onOpenChange={(open) => {
|
|
156
|
+
if (open) {
|
|
157
|
+
void ensureBacktestsLoaded?.();
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
153
160
|
items={[
|
|
154
161
|
{
|
|
155
162
|
label: 'Not selected',
|
|
@@ -9,6 +9,8 @@ interface RootProps {
|
|
|
9
9
|
backtestFiles: Items;
|
|
10
10
|
filters: UIFilters;
|
|
11
11
|
onChangeFilters?: OnChangeFilters;
|
|
12
|
+
ensureTickersLoaded?: () => void | Promise<unknown>;
|
|
13
|
+
ensureBacktestsLoaded?: () => void | Promise<unknown>;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const Root = ({
|
|
@@ -16,11 +18,20 @@ export const Root = ({
|
|
|
16
18
|
tickers,
|
|
17
19
|
backtestFiles,
|
|
18
20
|
onChangeFilters,
|
|
21
|
+
ensureTickersLoaded,
|
|
22
|
+
ensureBacktestsLoaded,
|
|
19
23
|
children,
|
|
20
24
|
}: PropsWithChildren<RootProps>) => {
|
|
21
25
|
return (
|
|
22
26
|
<FiltersContext.Provider
|
|
23
|
-
value={{
|
|
27
|
+
value={{
|
|
28
|
+
filters,
|
|
29
|
+
tickers,
|
|
30
|
+
backtestFiles,
|
|
31
|
+
onChangeFilters,
|
|
32
|
+
ensureTickersLoaded,
|
|
33
|
+
ensureBacktestsLoaded,
|
|
34
|
+
}}
|
|
24
35
|
>
|
|
25
36
|
{children}
|
|
26
37
|
</FiltersContext.Provider>
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import { SelectWithSearch } from '@UI';
|
|
5
|
-
import { SkeletonText, Stack, Show } from '@chakra-ui/react';
|
|
6
5
|
import { useFiltersContext } from '../context';
|
|
7
6
|
|
|
8
7
|
interface SelectSymbolProps {}
|
|
9
8
|
|
|
10
9
|
export const SelectSymbol = ({}: SelectSymbolProps) => {
|
|
11
|
-
const { filters, tickers, onChangeFilters } =
|
|
12
|
-
|
|
10
|
+
const { filters, tickers, onChangeFilters, ensureTickersLoaded } =
|
|
11
|
+
useFiltersContext();
|
|
13
12
|
const defaultInputValue =
|
|
14
13
|
tickers.find(({ value }) => value === filters.symbol)?.label ||
|
|
15
14
|
filters.symbol;
|
|
@@ -29,21 +28,17 @@ export const SelectSymbol = ({}: SelectSymbolProps) => {
|
|
|
29
28
|
};
|
|
30
29
|
|
|
31
30
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
items={tickers}
|
|
45
|
-
width="240px"
|
|
46
|
-
/>
|
|
47
|
-
</Show>
|
|
31
|
+
<SelectWithSearch
|
|
32
|
+
defaultValue={[filters.symbol]}
|
|
33
|
+
defaultInputValue={defaultInputValue}
|
|
34
|
+
onChange={onChange}
|
|
35
|
+
onOpenChange={(open) => {
|
|
36
|
+
if (open) {
|
|
37
|
+
void ensureTickersLoaded?.();
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
items={tickers}
|
|
41
|
+
width="240px"
|
|
42
|
+
/>
|
|
48
43
|
);
|
|
49
44
|
};
|
|
@@ -6,6 +6,8 @@ interface FiltersContextProps {
|
|
|
6
6
|
tickers: Items;
|
|
7
7
|
backtestFiles: Items;
|
|
8
8
|
onChangeFilters?: OnChangeFilters;
|
|
9
|
+
ensureTickersLoaded?: () => void | Promise<unknown>;
|
|
10
|
+
ensureBacktestsLoaded?: () => void | Promise<unknown>;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export const FiltersContext = createContext<FiltersContextProps>(
|