@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/bin/tradejs-app.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import net from 'net';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
import nextEnv from '@next/env';
|
|
@@ -11,6 +12,13 @@ const { loadEnvConfig } = nextEnv;
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
13
14
|
const appDir = path.resolve(__dirname, '..');
|
|
15
|
+
const LOCAL_DEV_HOSTNAMES = new Set([
|
|
16
|
+
'localhost',
|
|
17
|
+
'127.0.0.1',
|
|
18
|
+
'0.0.0.0',
|
|
19
|
+
'::1',
|
|
20
|
+
'[::1]',
|
|
21
|
+
]);
|
|
14
22
|
|
|
15
23
|
const command = process.argv[2] || 'dev';
|
|
16
24
|
const rawArgs = process.argv.slice(3);
|
|
@@ -23,32 +31,133 @@ process.env.PROJECT_CWD = projectCwd;
|
|
|
23
31
|
const dev = command === 'dev';
|
|
24
32
|
loadEnvConfig(projectCwd, dev, console);
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
const
|
|
34
|
+
function parsePort(value) {
|
|
35
|
+
const parsed = Number.parseInt(String(value || '').trim(), 10);
|
|
36
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readArgValue(args, flags) {
|
|
40
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
41
|
+
const arg = args[index];
|
|
42
|
+
if (flags.includes(arg)) {
|
|
43
|
+
return args[index + 1] || null;
|
|
44
|
+
}
|
|
45
|
+
const matchedFlag = flags.find((flag) => arg.startsWith(`${flag}=`));
|
|
46
|
+
if (matchedFlag) {
|
|
47
|
+
return arg.slice(matchedFlag.length + 1) || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
28
50
|
|
|
29
|
-
|
|
30
|
-
command === 'start' &&
|
|
31
|
-
!rawArgs.includes('-H') &&
|
|
32
|
-
!rawArgs.includes('--hostname')
|
|
33
|
-
) {
|
|
34
|
-
args.push('-H', '0.0.0.0');
|
|
51
|
+
return null;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
async function isPortAvailable(port) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const server = net.createServer();
|
|
57
|
+
server.unref();
|
|
58
|
+
server.once('error', () => resolve(false));
|
|
59
|
+
server.listen(port, () => {
|
|
60
|
+
server.close(() => resolve(true));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function findAvailablePort(startPort, attempts = 20) {
|
|
66
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
67
|
+
const port = startPort + offset;
|
|
68
|
+
if (await isPortAvailable(port)) {
|
|
69
|
+
return port;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function syncLocalUrlEnv(name, fromPort, toPort) {
|
|
77
|
+
const rawValue = String(process.env[name] || '').trim();
|
|
78
|
+
if (!rawValue) {
|
|
79
|
+
process.env[name] = `http://localhost:${toPort}`;
|
|
46
80
|
return;
|
|
47
81
|
}
|
|
48
|
-
process.exit(code ?? 0);
|
|
49
|
-
});
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(rawValue);
|
|
85
|
+
const currentPort =
|
|
86
|
+
parsePort(url.port) || (url.protocol === 'https:' ? 443 : 80);
|
|
87
|
+
if (!LOCAL_DEV_HOSTNAMES.has(url.hostname) || currentPort !== fromPort) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
url.port = String(toPort);
|
|
91
|
+
process.env[name] = url.toString();
|
|
92
|
+
} catch {
|
|
93
|
+
// Ignore invalid URLs and leave user-provided values untouched.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const nextBin = require.resolve('next/dist/bin/next');
|
|
99
|
+
const args = [nextBin, command, ...rawArgs];
|
|
100
|
+
const explicitPort = parsePort(readArgValue(rawArgs, ['-p', '--port']));
|
|
101
|
+
const hasBundlerFlag = rawArgs.some(
|
|
102
|
+
(arg) => arg === '--webpack' || arg === '--turbopack',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if ((command === 'dev' || command === 'build') && !hasBundlerFlag) {
|
|
106
|
+
args.push('--webpack');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (dev && explicitPort === null) {
|
|
110
|
+
const requestedPort = parsePort(process.env.PORT) || 3000;
|
|
111
|
+
const resolvedPort = await findAvailablePort(requestedPort);
|
|
112
|
+
|
|
113
|
+
if (resolvedPort === null) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[tradejs-app] no available dev port found starting at ${requestedPort}`,
|
|
116
|
+
);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.env.PORT = String(resolvedPort);
|
|
121
|
+
args.push('-p', String(resolvedPort));
|
|
122
|
+
|
|
123
|
+
if (resolvedPort !== requestedPort) {
|
|
124
|
+
syncLocalUrlEnv('APP_URL', requestedPort, resolvedPort);
|
|
125
|
+
syncLocalUrlEnv('NEXTAUTH_URL', requestedPort, resolvedPort);
|
|
126
|
+
console.warn(
|
|
127
|
+
`[tradejs-app] port ${requestedPort} is busy, using ${resolvedPort} instead`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
command === 'start' &&
|
|
134
|
+
!rawArgs.includes('-H') &&
|
|
135
|
+
!rawArgs.includes('--hostname')
|
|
136
|
+
) {
|
|
137
|
+
args.push('-H', '0.0.0.0');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const child = spawn(process.execPath, args, {
|
|
141
|
+
cwd: appDir,
|
|
142
|
+
env: process.env,
|
|
143
|
+
stdio: 'inherit',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
child.on('exit', (code, signal) => {
|
|
147
|
+
if (signal) {
|
|
148
|
+
process.kill(process.pid, signal);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
process.exit(code ?? 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
child.on('error', (error) => {
|
|
155
|
+
console.error('[tradejs-app] failed to start Next.js:', error);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
main().catch((error) => {
|
|
161
|
+
console.error('[tradejs-app] failed to initialize:', error);
|
|
53
162
|
process.exit(1);
|
|
54
163
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS open-source framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"README.md",
|
|
16
16
|
"bin",
|
|
17
|
+
"public",
|
|
17
18
|
"src",
|
|
18
19
|
"!src/**/__tests__",
|
|
19
20
|
"!src/**/*.test.ts",
|
|
@@ -40,20 +41,20 @@
|
|
|
40
41
|
"@chakra-ui/charts": "^3.24.0",
|
|
41
42
|
"@chakra-ui/react": "^3.24.2",
|
|
42
43
|
"@emotion/react": "^11.14.0",
|
|
43
|
-
"@langchain/core": "^
|
|
44
|
-
"@langchain/openai": "^
|
|
45
|
-
"@tradejs/connectors": "^1.0.
|
|
46
|
-
"@tradejs/core": "^1.0.
|
|
47
|
-
"@tradejs/indicators": "^1.0.
|
|
48
|
-
"@tradejs/infra": "^1.0.
|
|
49
|
-
"@tradejs/node": "^1.0.
|
|
50
|
-
"@tradejs/types": "^1.0.
|
|
44
|
+
"@langchain/core": "^1.1.42",
|
|
45
|
+
"@langchain/openai": "^1.4.5",
|
|
46
|
+
"@tradejs/connectors": "^1.0.8",
|
|
47
|
+
"@tradejs/core": "^1.0.8",
|
|
48
|
+
"@tradejs/indicators": "^1.0.8",
|
|
49
|
+
"@tradejs/infra": "^1.0.8",
|
|
50
|
+
"@tradejs/node": "^1.0.8",
|
|
51
|
+
"@tradejs/types": "^1.0.8",
|
|
51
52
|
"bcryptjs": "^2.4.3",
|
|
52
53
|
"date-fns": "^3.3.1",
|
|
53
54
|
"idb-keyval": "^6.2.2",
|
|
54
55
|
"klinecharts": "10.0.0-alpha9",
|
|
55
|
-
"lodash": "^4.
|
|
56
|
-
"next": "^16.
|
|
56
|
+
"lodash": "^4.18.1",
|
|
57
|
+
"next": "^16.2.3",
|
|
57
58
|
"next-auth": "^5.0.0-beta.26",
|
|
58
59
|
"next-themes": "^0.4.6",
|
|
59
60
|
"react": "^19.2.3",
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
"dev": "node ./bin/tradejs-app.mjs dev",
|
|
72
73
|
"build": "NODE_ENV=production node ./bin/tradejs-app.mjs build",
|
|
73
74
|
"start": "node ./bin/tradejs-app.mjs start",
|
|
74
|
-
"lint": "yarn run -T eslint src
|
|
75
|
+
"lint": "ESLINT_USE_FLAT_CONFIG=true yarn run -T eslint src"
|
|
75
76
|
},
|
|
76
77
|
"license": "MIT",
|
|
77
78
|
"author": "aleksnick (https://github.com/aleksnick)"
|
|
Binary file
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="1200"
|
|
4
|
+
height="630"
|
|
5
|
+
viewBox="0 0 1200 630"
|
|
6
|
+
fill="none"
|
|
7
|
+
>
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="bg" x1="104" y1="44" x2="1090" y2="648" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop offset="0" stop-color="#0C1728" />
|
|
11
|
+
<stop offset="0.46" stop-color="#0A1320" />
|
|
12
|
+
<stop offset="1" stop-color="#07101A" />
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<radialGradient id="glow-left" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(168 58) rotate(56.115) scale(498.404 523.217)">
|
|
15
|
+
<stop offset="0" stop-color="#20C5BD" stop-opacity="0.23" />
|
|
16
|
+
<stop offset="0.48" stop-color="#20C5BD" stop-opacity="0.08" />
|
|
17
|
+
<stop offset="1" stop-color="#20C5BD" stop-opacity="0" />
|
|
18
|
+
</radialGradient>
|
|
19
|
+
<radialGradient id="glow-right" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1086 30) rotate(112.105) scale(468.802 625.774)">
|
|
20
|
+
<stop offset="0" stop-color="#4CA9FF" stop-opacity="0.17" />
|
|
21
|
+
<stop offset="0.44" stop-color="#4CA9FF" stop-opacity="0.06" />
|
|
22
|
+
<stop offset="1" stop-color="#4CA9FF" stop-opacity="0" />
|
|
23
|
+
</radialGradient>
|
|
24
|
+
<linearGradient id="frame" x1="160" y1="86" x2="1084" y2="544" gradientUnits="userSpaceOnUse">
|
|
25
|
+
<stop offset="0" stop-color="#27445F" stop-opacity="0.9" />
|
|
26
|
+
<stop offset="1" stop-color="#1CCAC0" stop-opacity="0.62" />
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id="glyph" x1="0" y1="0" x2="1" y2="1">
|
|
29
|
+
<stop offset="0" stop-color="#28EAD3" />
|
|
30
|
+
<stop offset="1" stop-color="#1CCAC0" />
|
|
31
|
+
</linearGradient>
|
|
32
|
+
<linearGradient id="chart" x1="706" y1="430" x2="1110" y2="236" gradientUnits="userSpaceOnUse">
|
|
33
|
+
<stop offset="0" stop-color="#20C5BD" stop-opacity="0.22" />
|
|
34
|
+
<stop offset="0.38" stop-color="#20C5BD" stop-opacity="0.95" />
|
|
35
|
+
<stop offset="1" stop-color="#7CE8E2" stop-opacity="0.95" />
|
|
36
|
+
</linearGradient>
|
|
37
|
+
<pattern id="grid" width="36" height="36" patternUnits="userSpaceOnUse">
|
|
38
|
+
<path d="M36 0H0V36" stroke="#203347" stroke-opacity="0.32" />
|
|
39
|
+
</pattern>
|
|
40
|
+
</defs>
|
|
41
|
+
|
|
42
|
+
<rect width="1200" height="630" fill="url(#bg)" />
|
|
43
|
+
<rect width="1200" height="630" fill="url(#glow-left)" />
|
|
44
|
+
<rect width="1200" height="630" fill="url(#glow-right)" />
|
|
45
|
+
<rect width="1200" height="630" fill="url(#grid)" opacity="0.62" />
|
|
46
|
+
<rect x="64" y="64" width="1072" height="502" rx="34" fill="#0E1726" fill-opacity="0.55" stroke="url(#frame)" />
|
|
47
|
+
<rect x="96" y="96" width="148" height="38" rx="19" fill="#112233" stroke="#27445F" />
|
|
48
|
+
<text x="129" y="121" fill="#9EEDE8" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="700" letter-spacing="2.3">
|
|
49
|
+
APP UI
|
|
50
|
+
</text>
|
|
51
|
+
|
|
52
|
+
<g transform="translate(96 170)">
|
|
53
|
+
<rect x="0" y="0" width="84" height="84" rx="22" fill="#0A1624" stroke="#235768" stroke-opacity="0.8" stroke-width="2" />
|
|
54
|
+
<rect x="0" y="0" width="84" height="84" rx="22" fill="url(#glow-left)" opacity="0.22" />
|
|
55
|
+
<path d="M24 54L40 38L49 47L67 29" stroke="url(#glyph)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
|
56
|
+
<path d="M59 29H67V37" stroke="url(#glyph)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
|
57
|
+
</g>
|
|
58
|
+
|
|
59
|
+
<text x="96" y="320" fill="#EEF6FF" font-family="Arial, Helvetica, sans-serif" font-size="104" font-weight="700" letter-spacing="-4">
|
|
60
|
+
Trade
|
|
61
|
+
</text>
|
|
62
|
+
<text x="442" y="320" fill="#20C5BD" font-family="Arial, Helvetica, sans-serif" font-size="104" font-weight="700" letter-spacing="-4">
|
|
63
|
+
JS
|
|
64
|
+
</text>
|
|
65
|
+
|
|
66
|
+
<text x="96" y="382" fill="#EEF6FF" font-family="Arial, Helvetica, sans-serif" font-size="44" font-weight="700" letter-spacing="-1.2">
|
|
67
|
+
App
|
|
68
|
+
</text>
|
|
69
|
+
<text x="96" y="436" fill="#A8BED5" font-family="Arial, Helvetica, sans-serif" font-size="30" font-weight="400">
|
|
70
|
+
Dashboards, backtests, charts, derivatives, and runtime data in one UI.
|
|
71
|
+
</text>
|
|
72
|
+
|
|
73
|
+
<rect x="96" y="474" width="188" height="46" rx="23" fill="#102133" stroke="#28516B" />
|
|
74
|
+
<text x="128" y="504" fill="#DAF6F4" font-family="Arial, Helvetica, sans-serif" font-size="21" font-weight="700" letter-spacing="0.4">
|
|
75
|
+
@tradejs/app
|
|
76
|
+
</text>
|
|
77
|
+
|
|
78
|
+
<rect x="686" y="162" width="392" height="250" rx="28" fill="#101B2A" fill-opacity="0.72" stroke="#1F364C" />
|
|
79
|
+
<path d="M728 346L806 288L880 318L970 246L1048 198" stroke="url(#chart)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
80
|
+
<path d="M1048 198H1090V240" stroke="url(#chart)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
81
|
+
<circle cx="806" cy="288" r="10" fill="#20C5BD" />
|
|
82
|
+
<circle cx="970" cy="246" r="10" fill="#20C5BD" />
|
|
83
|
+
<circle cx="1048" cy="198" r="11" fill="#7CE8E2" />
|
|
84
|
+
<rect x="730" y="370" width="120" height="14" rx="7" fill="#173046" />
|
|
85
|
+
<rect x="730" y="404" width="208" height="14" rx="7" fill="#12283C" />
|
|
86
|
+
<rect x="730" y="438" width="178" height="14" rx="7" fill="#12283C" />
|
|
87
|
+
|
|
88
|
+
<text x="96" y="560" fill="#7F93AC" font-family="Arial, Helvetica, sans-serif" font-size="20" font-weight="600" letter-spacing="1.2">
|
|
89
|
+
NEXT.JS UI PACKAGE FOR TRADEJS WORKFLOWS
|
|
90
|
+
</text>
|
|
91
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
package/src/app/api/ai/route.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
SystemMessage,
|
|
7
7
|
} from '@langchain/core/messages';
|
|
8
8
|
import { toJson } from '@tradejs/core/data';
|
|
9
|
+
import { getAiResponseLanguagePromptName } from '@tradejs/infra/aiLanguages';
|
|
10
|
+
import { DEFAULT_AI_MODEL, getOpenRouterModelKwargs } from '@tradejs/node/ai';
|
|
9
11
|
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
10
12
|
import {
|
|
11
13
|
AIChatHistory,
|
|
@@ -13,27 +15,50 @@ import {
|
|
|
13
15
|
ConnectorCreator,
|
|
14
16
|
Filters,
|
|
15
17
|
} from '@tradejs/types';
|
|
16
|
-
import {
|
|
18
|
+
import { getData, redisKeys, setData } from '@tradejs/infra/redis';
|
|
17
19
|
import { logger } from '@tradejs/infra/logger';
|
|
20
|
+
import { getUserSettings } from '@tradejs/infra/userSettings';
|
|
21
|
+
import { getCurrentUserName } from '@app/lib/currentUser';
|
|
18
22
|
|
|
19
23
|
export const dynamic = 'force-dynamic';
|
|
20
24
|
|
|
21
|
-
const HISTORY_DIR = 'data/chats';
|
|
22
25
|
const projectRoot =
|
|
23
26
|
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
24
27
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
const normalizeChatSymbolKey = (symbol: string): string => {
|
|
29
|
+
const normalized = symbol
|
|
30
|
+
.trim()
|
|
31
|
+
.toUpperCase()
|
|
32
|
+
.replace(/[^A-Z0-9._-]+/g, '_')
|
|
33
|
+
.replace(/_+/g, '_')
|
|
34
|
+
.replace(/^_+|_+$/g, '')
|
|
35
|
+
.slice(0, 120);
|
|
36
|
+
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
throw new Error('Invalid AI chat symbol');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return normalized;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getHistoryKey = (userName: string, symbol: string) =>
|
|
45
|
+
redisKeys.aiChatHistory(userName, normalizeChatSymbolKey(symbol));
|
|
46
|
+
|
|
47
|
+
const getHistory = async (
|
|
48
|
+
userName: string,
|
|
49
|
+
symbol: string,
|
|
50
|
+
): Promise<AIChatHistory> => {
|
|
51
|
+
return (await getData(getHistoryKey(userName, symbol), [])) as AIChatHistory;
|
|
28
52
|
};
|
|
29
53
|
|
|
30
54
|
const appendMessagesToHistory = async (
|
|
55
|
+
userName: string,
|
|
31
56
|
symbol: string,
|
|
32
57
|
messages: AIChatHistory,
|
|
33
58
|
): Promise<void> => {
|
|
34
|
-
const history = await getHistory(symbol);
|
|
35
|
-
await
|
|
36
|
-
|
|
59
|
+
const history = await getHistory(userName, symbol);
|
|
60
|
+
await setData(getHistoryKey(userName, symbol), [...history, ...messages], {
|
|
61
|
+
expire: 0,
|
|
37
62
|
});
|
|
38
63
|
};
|
|
39
64
|
|
|
@@ -41,18 +66,21 @@ const buildMessages = (
|
|
|
41
66
|
filters: Filters,
|
|
42
67
|
historyEntry: AIChatMessage,
|
|
43
68
|
historyData: unknown,
|
|
69
|
+
responseLanguage: string,
|
|
44
70
|
) => {
|
|
45
71
|
const messages = new Array<BaseMessage>();
|
|
46
72
|
|
|
47
73
|
messages.push(
|
|
48
74
|
new SystemMessage(
|
|
49
|
-
|
|
75
|
+
`You are a crypto trader assistant. Reply in ${getAiResponseLanguagePromptName(
|
|
76
|
+
responseLanguage,
|
|
77
|
+
)}.`,
|
|
50
78
|
),
|
|
51
79
|
);
|
|
52
80
|
|
|
53
81
|
messages.push(
|
|
54
82
|
new SystemMessage(
|
|
55
|
-
|
|
83
|
+
`Here is the market data for ${filters.symbol}: ${toJson(historyData)}`,
|
|
56
84
|
),
|
|
57
85
|
);
|
|
58
86
|
|
|
@@ -67,13 +95,21 @@ const buildMessages = (
|
|
|
67
95
|
return messages;
|
|
68
96
|
};
|
|
69
97
|
|
|
70
|
-
const invokeChatModel = async (messages: BaseMessage[]) => {
|
|
98
|
+
const invokeChatModel = async (messages: BaseMessage[], userName: string) => {
|
|
99
|
+
const settings = await getUserSettings(userName);
|
|
100
|
+
if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
|
|
101
|
+
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
|
|
105
|
+
|
|
71
106
|
const model = new ChatOpenAI({
|
|
72
107
|
temperature: 0.7,
|
|
73
|
-
modelName:
|
|
74
|
-
|
|
108
|
+
modelName: settings.AI_MODEL || DEFAULT_AI_MODEL,
|
|
109
|
+
apiKey: settings.AI_API_KEY,
|
|
110
|
+
...(Object.keys(modelKwargs).length ? { modelKwargs } : {}),
|
|
75
111
|
configuration: {
|
|
76
|
-
baseURL:
|
|
112
|
+
baseURL: settings.AI_API_ENDPOINT,
|
|
77
113
|
},
|
|
78
114
|
});
|
|
79
115
|
|
|
@@ -82,6 +118,11 @@ const invokeChatModel = async (messages: BaseMessage[]) => {
|
|
|
82
118
|
|
|
83
119
|
export const GET = async (request: NextRequest) => {
|
|
84
120
|
try {
|
|
121
|
+
const userName = await getCurrentUserName();
|
|
122
|
+
if (!userName) {
|
|
123
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
124
|
+
}
|
|
125
|
+
|
|
85
126
|
const symbol = request.nextUrl.searchParams.get('symbol');
|
|
86
127
|
|
|
87
128
|
if (!symbol) {
|
|
@@ -91,7 +132,13 @@ export const GET = async (request: NextRequest) => {
|
|
|
91
132
|
);
|
|
92
133
|
}
|
|
93
134
|
|
|
94
|
-
|
|
135
|
+
try {
|
|
136
|
+
normalizeChatSymbolKey(symbol);
|
|
137
|
+
} catch {
|
|
138
|
+
return NextResponse.json({ error: 'Invalid symbol' }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const history = await getHistory(userName, symbol);
|
|
95
142
|
return NextResponse.json({ history });
|
|
96
143
|
} catch (error) {
|
|
97
144
|
logger.log('error', `AI history error: %o`, error);
|
|
@@ -104,6 +151,11 @@ export const GET = async (request: NextRequest) => {
|
|
|
104
151
|
|
|
105
152
|
export const POST = async (request: NextRequest) => {
|
|
106
153
|
try {
|
|
154
|
+
const userName = await getCurrentUserName();
|
|
155
|
+
if (!userName) {
|
|
156
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
157
|
+
}
|
|
158
|
+
|
|
107
159
|
const body = await request.json();
|
|
108
160
|
const { message, filters } = body as {
|
|
109
161
|
message?: AIChatMessage;
|
|
@@ -117,7 +169,13 @@ export const POST = async (request: NextRequest) => {
|
|
|
117
169
|
);
|
|
118
170
|
}
|
|
119
171
|
|
|
120
|
-
|
|
172
|
+
try {
|
|
173
|
+
normalizeChatSymbolKey(filters.symbol);
|
|
174
|
+
} catch {
|
|
175
|
+
return NextResponse.json({ error: 'Invalid symbol' }, { status: 400 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await appendMessagesToHistory(userName, filters.symbol, [message]);
|
|
121
179
|
|
|
122
180
|
const connectorCreator = await getConnectorCreatorByProvider(
|
|
123
181
|
'bybit',
|
|
@@ -128,7 +186,7 @@ export const POST = async (request: NextRequest) => {
|
|
|
128
186
|
}
|
|
129
187
|
|
|
130
188
|
const byBitConnector = await (connectorCreator as ConnectorCreator)({
|
|
131
|
-
userName
|
|
189
|
+
userName,
|
|
132
190
|
});
|
|
133
191
|
|
|
134
192
|
const data = await byBitConnector.kline({
|
|
@@ -136,16 +194,22 @@ export const POST = async (request: NextRequest) => {
|
|
|
136
194
|
interval: '60',
|
|
137
195
|
});
|
|
138
196
|
|
|
139
|
-
const
|
|
197
|
+
const settings = await getUserSettings(userName);
|
|
198
|
+
const chatMessages = buildMessages(
|
|
199
|
+
filters,
|
|
200
|
+
message,
|
|
201
|
+
data.slice(-100),
|
|
202
|
+
settings.AI_RESPONSE_LANGUAGE,
|
|
203
|
+
);
|
|
140
204
|
|
|
141
|
-
const response = await invokeChatModel(chatMessages);
|
|
205
|
+
const response = await invokeChatModel(chatMessages, userName);
|
|
142
206
|
|
|
143
207
|
const responseMessage: AIChatMessage = {
|
|
144
208
|
from: 'ai',
|
|
145
209
|
text: response.content as string,
|
|
146
210
|
};
|
|
147
211
|
|
|
148
|
-
await appendMessagesToHistory(filters.symbol, [responseMessage]);
|
|
212
|
+
await appendMessagesToHistory(userName, filters.symbol, [responseMessage]);
|
|
149
213
|
|
|
150
214
|
return NextResponse.json({ message: responseMessage });
|
|
151
215
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { Item, TestStat } from '@tradejs/types';
|
|
3
3
|
import { parseTestName } from '@tradejs/core/backtest';
|
|
4
|
-
import { getData, getKeys, redisKeys } from '@tradejs/infra/redis';
|
|
4
|
+
import { getData, getKeys, redisKeys, setData } from '@tradejs/infra/redis';
|
|
5
5
|
import { logger } from '@tradejs/infra/logger';
|
|
6
6
|
import { auth } from '@app/auth';
|
|
7
7
|
|
|
@@ -17,6 +17,15 @@ export const GET = async () => {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const result = new Array<Item>();
|
|
20
|
+
const indexedItems = (await getData(
|
|
21
|
+
redisKeys.testSummaries(userName),
|
|
22
|
+
null,
|
|
23
|
+
)) as Item[] | null;
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(indexedItems) && indexedItems.length) {
|
|
26
|
+
return NextResponse.json({ items: indexedItems });
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
const testsPrefix = redisKeys.tests(userName);
|
|
21
30
|
const keys = await getKeys(testsPrefix);
|
|
22
31
|
const configKeys = keys.filter((key) => key.endsWith(':config'));
|
|
@@ -49,6 +58,8 @@ export const GET = async () => {
|
|
|
49
58
|
});
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
await setData(redisKeys.testSummaries(userName), result, { expire: 0 });
|
|
62
|
+
|
|
52
63
|
return NextResponse.json({ items: result });
|
|
53
64
|
} catch (error) {
|
|
54
65
|
logger.log('error', `Backtest list error: %o`, error);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
|
-
import { delKey, redisKeys } from '@tradejs/infra/redis';
|
|
4
|
+
import { delKey, getData, redisKeys, setData } from '@tradejs/infra/redis';
|
|
5
|
+
import { Item } from '@tradejs/types';
|
|
5
6
|
import { logger } from '@tradejs/infra/logger';
|
|
6
7
|
import { auth } from '@app/auth';
|
|
7
8
|
|
|
@@ -46,6 +47,22 @@ export const DELETE = async (
|
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const indexedItems = (await getData(
|
|
51
|
+
redisKeys.testSummaries(userName),
|
|
52
|
+
[],
|
|
53
|
+
)) as Item[];
|
|
54
|
+
const nextIndexedItems = indexedItems.filter(
|
|
55
|
+
(item) =>
|
|
56
|
+
!(
|
|
57
|
+
item?.value === name &&
|
|
58
|
+
typeof item?.data?.strategyName === 'string' &&
|
|
59
|
+
item.data.strategyName === strategy
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
await setData(redisKeys.testSummaries(userName), nextIndexedItems, {
|
|
63
|
+
expire: 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
49
66
|
return NextResponse.json({ deleted: true, removedKeys });
|
|
50
67
|
} catch (error) {
|
|
51
68
|
logger.log('error', 'Backtest delete error: %o', error);
|