@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
@@ -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
- const nextBin = require.resolve('next/dist/bin/next');
27
- const args = [nextBin, command, ...rawArgs];
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
- if (
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
- const child = spawn(process.execPath, args, {
38
- cwd: appDir,
39
- env: process.env,
40
- stdio: 'inherit',
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
- child.on('exit', (code, signal) => {
44
- if (signal) {
45
- process.kill(process.pid, signal);
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
- child.on('error', (error) => {
52
- console.error('[tradejs-app] failed to start Next.js:', error);
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.5",
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": "^0.3.68",
44
- "@langchain/openai": "^0.6.11",
45
- "@tradejs/connectors": "^1.0.5",
46
- "@tradejs/core": "^1.0.5",
47
- "@tradejs/indicators": "^1.0.5",
48
- "@tradejs/infra": "^1.0.5",
49
- "@tradejs/node": "^1.0.5",
50
- "@tradejs/types": "^1.0.5",
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.17.21",
56
- "next": "^16.1.1",
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 --ext .js,.jsx,.ts,.tsx"
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
@@ -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>
@@ -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 { getFile, setFile } from '@tradejs/infra/files';
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 getHistory = async (symbol: string): Promise<AIChatHistory> => {
26
- const history = await getFile(HISTORY_DIR, symbol, [], projectRoot);
27
- return history;
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 setFile(HISTORY_DIR, symbol, [...history, ...messages], {
36
- projectRoot,
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
- `Вот данные по монете ${filters.symbol}: ${toJson(historyData)}`,
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: 'gpt-4o',
74
- openAIApiKey: process.env.OPENAI_API_KEY,
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: process.env.OPENAI_API_ENDPOINT || 'https://api.openai.com/v1',
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
- const history = await getHistory(symbol);
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
- await appendMessagesToHistory(filters.symbol, [message]);
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: 'root',
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 chatMessages = buildMessages(filters, message, data.slice(-100));
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);