@tradejs/app 1.0.9 → 1.0.11
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/README.md +16 -3
- package/bin/tradejs-app.mjs +146 -2
- package/next.config.mjs +0 -14
- package/package.json +35 -19
- package/src/app/actions/backtest.ts +54 -0
- package/src/app/actions/kline.ts +10 -5
- package/src/app/actions/scanner.ts +8 -3
- package/src/app/actions/strategies.ts +39 -0
- package/src/app/api/ai/route.ts +5 -4
- package/src/app/api/auth/[...nextauth]/route.ts +1 -1
- package/src/app/api/backtest/configs/route.ts +25 -0
- package/src/app/api/backtest/files/route.ts +5 -2
- package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
- package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
- package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
- package/src/app/api/backtest/runs/route.ts +45 -0
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
- package/src/app/api/derivatives/summary/route.ts +8 -1
- package/src/app/api/install/route.ts +51 -0
- package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
- package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
- package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
- package/src/app/api/scanner/[provider]/route.ts +30 -12
- package/src/app/api/scanner/route.ts +3 -3
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
- package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
- package/src/app/api/strategies/ai/route.ts +139 -0
- package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
- package/src/app/api/strategies/replay/route.ts +44 -0
- package/src/app/api/strategies/runtime/route.ts +831 -0
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
- package/src/app/api/user/runtime-deployments/route.ts +94 -0
- package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
- package/src/app/api/user/settings/route.ts +18 -1
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
- package/src/app/api/user/trading-accounts/route.ts +134 -0
- package/src/app/auth.ts +1 -19
- package/src/app/components/Backtest/CompareList/index.tsx +1 -1
- package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
- package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
- package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
- package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
- package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
- package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
- package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
- package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
- package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
- package/src/app/components/Backtest/TestCard/Root/index.tsx +18 -4
- package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
- package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
- package/src/app/components/Backtest/TestCard/index.ts +6 -2
- package/src/app/components/Backtest/TestList/index.tsx +4 -9
- package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
- package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
- package/src/app/components/Dashboard/KlineChart/index.tsx +21 -7
- package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
- package/src/app/components/Shared/AppShell.tsx +2 -2
- package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
- package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
- package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
- package/src/app/components/Shared/Filters/Backtest/index.tsx +51 -51
- package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
- package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
- package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
- package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
- package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
- package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
- package/src/app/components/Shared/Filters/index.ts +2 -0
- package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
- package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
- package/src/app/components/Shared/Sidebar/index.tsx +17 -3
- package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
- package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
- package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
- package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
- package/src/app/components/UI/Segment/index.tsx +8 -1
- package/src/app/components/UI/Select/index.tsx +24 -13
- package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
- package/src/app/layout.tsx +1 -1
- package/src/app/lib/backtestJobs.ts +925 -0
- package/src/app/lib/cardPageLayout.ts +1 -0
- package/src/app/lib/connectorCreator.ts +28 -0
- package/src/app/lib/currentUser.ts +1 -1
- package/src/app/lib/errorMessage.ts +34 -0
- package/src/app/lib/installation.ts +83 -0
- package/src/app/lib/marketDefaults.ts +12 -0
- package/src/app/lib/marketRoutes.ts +58 -0
- package/src/app/lib/runtimeStrategies.ts +1376 -0
- package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
- package/src/app/provider.tsx +1 -1
- package/src/app/routes/backtest/page.tsx +1100 -302
- package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +76 -35
- package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
- package/src/app/routes/dashboard/page.tsx +15 -3
- package/src/app/routes/derivatives/page.tsx +1222 -165
- package/src/app/routes/install/page.tsx +175 -0
- package/src/app/routes/signin/page.tsx +7 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
- package/src/app/routes/strategies/[mode]/page.tsx +20 -0
- package/src/app/routes/strategies/page.tsx +7 -0
- package/src/app/store/ai.ts +1 -1
- package/src/app/store/data.ts +73 -17
- package/src/app/store/filters.ts +1 -0
- package/src/app/store/marketKlineStream.ts +87 -0
- package/src/app/store/tests.ts +83 -45
- package/src/app/store/tickers.ts +59 -22
- package/src/proxy.ts +4 -0
- package/tsconfig.json +17 -12
- package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
- package/src/app/routes/backtest/[test]/page.tsx +0 -33
package/README.md
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Publishable Next.js UI package for the TradeJS open-source framework, with backtests, charts, and signal flows.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Recommended external usage:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
npx create-tradejs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The generator starts local infrastructure and opens the install page. Choose
|
|
12
|
+
the local `root` password there; TradeJS then opens the dashboard with a
|
|
13
|
+
**Create backtest** action. For manual installation into an existing project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @tradejs/app @tradejs/core @tradejs/node @tradejs/types @tradejs/base @tradejs/cli
|
|
9
17
|
npx tradejs-app dev
|
|
10
18
|
```
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
Use matching versions for all `@tradejs/*` packages. The installable launcher
|
|
21
|
+
requires `@tradejs/app@1.0.10` or newer.
|
|
22
|
+
|
|
23
|
+
The launcher reads env and `tradejs.config.ts` from the caller project directory
|
|
24
|
+
via `PROJECT_CWD`. When it runs from `node_modules`, it creates a generated
|
|
25
|
+
`.tradejs/app` working copy and runs Next.js there.
|
package/bin/tradejs-app.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from 'child_process';
|
|
3
|
+
import { spawn, spawnSync } from 'child_process';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import fs from 'fs';
|
|
5
6
|
import net from 'net';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
@@ -11,7 +12,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
12
|
const { loadEnvConfig } = nextEnv;
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
|
-
const
|
|
15
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
15
16
|
const LOCAL_DEV_HOSTNAMES = new Set([
|
|
16
17
|
'localhost',
|
|
17
18
|
'127.0.0.1',
|
|
@@ -31,6 +32,93 @@ process.env.PROJECT_CWD = projectCwd;
|
|
|
31
32
|
const dev = command === 'dev';
|
|
32
33
|
loadEnvConfig(projectCwd, dev, console);
|
|
33
34
|
|
|
35
|
+
function isInsideNodeModules(dir) {
|
|
36
|
+
return dir.split(path.sep).includes('node_modules');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function copyPackageEntry(entryName, targetDir) {
|
|
40
|
+
const sourcePath = path.join(packageDir, entryName);
|
|
41
|
+
if (!fs.existsSync(sourcePath)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const targetPath = path.join(targetDir, entryName);
|
|
46
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
47
|
+
fs.cpSync(sourcePath, targetPath, {
|
|
48
|
+
recursive: true,
|
|
49
|
+
force: true,
|
|
50
|
+
errorOnExist: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureLinkedNodeModules(targetDir) {
|
|
55
|
+
const sourceNodeModules = path.join(packageDir, 'node_modules');
|
|
56
|
+
const targetNodeModules = path.join(targetDir, 'node_modules');
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(sourceNodeModules)) {
|
|
59
|
+
if (
|
|
60
|
+
fs.existsSync(targetNodeModules) &&
|
|
61
|
+
fs.lstatSync(targetNodeModules).isSymbolicLink()
|
|
62
|
+
) {
|
|
63
|
+
fs.rmSync(targetNodeModules, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (fs.existsSync(targetNodeModules)) {
|
|
69
|
+
const current = fs.lstatSync(targetNodeModules);
|
|
70
|
+
if (current.isSymbolicLink()) {
|
|
71
|
+
const currentTarget = fs.realpathSync(targetNodeModules);
|
|
72
|
+
if (currentTarget === fs.realpathSync(sourceNodeModules)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fs.rmSync(targetNodeModules, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fs.symlinkSync(
|
|
81
|
+
sourceNodeModules,
|
|
82
|
+
targetNodeModules,
|
|
83
|
+
process.platform === 'win32' ? 'junction' : 'dir',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ensureGeneratedGitignore() {
|
|
88
|
+
const tradejsDir = path.join(projectCwd, '.tradejs');
|
|
89
|
+
const gitignorePath = path.join(tradejsDir, '.gitignore');
|
|
90
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
91
|
+
fs.mkdirSync(tradejsDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(gitignorePath, 'app\n', 'utf8');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveAppDir() {
|
|
97
|
+
if (!isInsideNodeModules(packageDir)) {
|
|
98
|
+
return packageDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const targetDir = path.join(projectCwd, '.tradejs', 'app');
|
|
102
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
103
|
+
ensureGeneratedGitignore();
|
|
104
|
+
|
|
105
|
+
for (const entryName of [
|
|
106
|
+
'README.md',
|
|
107
|
+
'bin',
|
|
108
|
+
'next-env.d.ts',
|
|
109
|
+
'next.config.mjs',
|
|
110
|
+
'package.json',
|
|
111
|
+
'public',
|
|
112
|
+
'src',
|
|
113
|
+
'tsconfig.json',
|
|
114
|
+
]) {
|
|
115
|
+
copyPackageEntry(entryName, targetDir);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ensureLinkedNodeModules(targetDir);
|
|
119
|
+
return targetDir;
|
|
120
|
+
}
|
|
121
|
+
|
|
34
122
|
function parsePort(value) {
|
|
35
123
|
const parsed = Number.parseInt(String(value || '').trim(), 10);
|
|
36
124
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -73,6 +161,53 @@ async function findAvailablePort(startPort, attempts = 20) {
|
|
|
73
161
|
return null;
|
|
74
162
|
}
|
|
75
163
|
|
|
164
|
+
function getListeningProcess(port) {
|
|
165
|
+
if (process.platform === 'win32') {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const lsof = spawnSync('lsof', [
|
|
170
|
+
'-nP',
|
|
171
|
+
`-iTCP:${port}`,
|
|
172
|
+
'-sTCP:LISTEN',
|
|
173
|
+
'-Fp',
|
|
174
|
+
]);
|
|
175
|
+
const output = String(lsof.stdout || '');
|
|
176
|
+
const pid = output
|
|
177
|
+
.split('\n')
|
|
178
|
+
.find((line) => line.startsWith('p'))
|
|
179
|
+
?.slice(1);
|
|
180
|
+
|
|
181
|
+
if (!pid) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ps = spawnSync('ps', ['-p', pid, '-o', 'command=']);
|
|
186
|
+
const cwdInfo = spawnSync('lsof', ['-a', '-p', pid, '-d', 'cwd', '-Fn']);
|
|
187
|
+
const cwd = String(cwdInfo.stdout || '')
|
|
188
|
+
.split('\n')
|
|
189
|
+
.find((line) => line.startsWith('n'))
|
|
190
|
+
?.slice(1);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
pid,
|
|
194
|
+
command: String(ps.stdout || '').trim(),
|
|
195
|
+
cwd: cwd || null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isAppDevProcess(processInfo, appDir) {
|
|
200
|
+
if (!processInfo?.command) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (processInfo.cwd === appDir || processInfo.cwd === packageDir) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return processInfo.command.includes('next-server');
|
|
209
|
+
}
|
|
210
|
+
|
|
76
211
|
function syncLocalUrlEnv(name, fromPort, toPort) {
|
|
77
212
|
const rawValue = String(process.env[name] || '').trim();
|
|
78
213
|
if (!rawValue) {
|
|
@@ -95,6 +230,7 @@ function syncLocalUrlEnv(name, fromPort, toPort) {
|
|
|
95
230
|
}
|
|
96
231
|
|
|
97
232
|
async function main() {
|
|
233
|
+
const appDir = resolveAppDir();
|
|
98
234
|
const nextBin = require.resolve('next/dist/bin/next');
|
|
99
235
|
const args = [nextBin, command, ...rawArgs];
|
|
100
236
|
const explicitPort = parsePort(readArgValue(rawArgs, ['-p', '--port']));
|
|
@@ -121,6 +257,14 @@ async function main() {
|
|
|
121
257
|
args.push('-p', String(resolvedPort));
|
|
122
258
|
|
|
123
259
|
if (resolvedPort !== requestedPort) {
|
|
260
|
+
const existingProcess = getListeningProcess(requestedPort);
|
|
261
|
+
if (isAppDevProcess(existingProcess, appDir)) {
|
|
262
|
+
console.error(
|
|
263
|
+
`[tradejs-app] another app dev server is already listening on port ${requestedPort} (PID ${existingProcess.pid}). Stop it before starting a new app server, or pass an explicit --port/PORT for a separate project.`,
|
|
264
|
+
);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
124
268
|
syncLocalUrlEnv('APP_URL', requestedPort, resolvedPort);
|
|
125
269
|
syncLocalUrlEnv('NEXTAUTH_URL', requestedPort, resolvedPort);
|
|
126
270
|
console.warn(
|
package/next.config.mjs
CHANGED
|
@@ -13,20 +13,6 @@ const nextConfig = {
|
|
|
13
13
|
externalDir: true,
|
|
14
14
|
optimizePackageImports: ['@chakra-ui/react'],
|
|
15
15
|
},
|
|
16
|
-
async redirects() {
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
source: '/routes/dashboard/:symbol/:interval',
|
|
20
|
-
destination: '/routes/dashboard/bybit/:symbol/:interval',
|
|
21
|
-
permanent: false,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
source: '/api/kline/:symbol/:interval',
|
|
25
|
-
destination: '/api/kline/bybit/:symbol/:interval',
|
|
26
|
-
permanent: false,
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
},
|
|
30
16
|
};
|
|
31
17
|
|
|
32
18
|
export default nextConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
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",
|
|
@@ -29,44 +29,60 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
+
"imports": {
|
|
33
|
+
"#app/*": "./src/app/*",
|
|
34
|
+
"#actions/*": "./src/app/actions/*",
|
|
35
|
+
"#store": "./src/app/store/index.ts",
|
|
36
|
+
"#shared/*": "./src/app/components/Shared/*",
|
|
37
|
+
"#ui": "./src/app/components/UI/index.ts",
|
|
38
|
+
"#components/*": "./src/app/components/*"
|
|
39
|
+
},
|
|
32
40
|
"repository": {
|
|
33
41
|
"type": "git",
|
|
34
|
-
"url": "
|
|
42
|
+
"url": "https://github.com/TradeJS-Dev/TradeJS",
|
|
35
43
|
"directory": "apps/app"
|
|
36
44
|
},
|
|
37
45
|
"bugs": {
|
|
38
46
|
"url": "https://github.com/tradejs-dev/tradejs/issues"
|
|
39
47
|
},
|
|
40
48
|
"dependencies": {
|
|
41
|
-
"@chakra-ui/charts": "
|
|
42
|
-
"@chakra-ui/react": "
|
|
49
|
+
"@chakra-ui/charts": "3.24.0",
|
|
50
|
+
"@chakra-ui/react": "3.24.2",
|
|
43
51
|
"@emotion/react": "^11.14.0",
|
|
44
52
|
"@langchain/core": "^1.1.42",
|
|
45
53
|
"@langchain/openai": "^1.4.5",
|
|
46
|
-
"@tradejs/connectors": "^1.0.
|
|
47
|
-
"@tradejs/core": "^1.0.
|
|
48
|
-
"@tradejs/indicators": "^1.0.
|
|
49
|
-
"@tradejs/infra": "^1.0.
|
|
50
|
-
"@tradejs/node": "^1.0.
|
|
51
|
-
"@tradejs/types": "^1.0.
|
|
54
|
+
"@tradejs/connectors": "^1.0.11",
|
|
55
|
+
"@tradejs/core": "^1.0.11",
|
|
56
|
+
"@tradejs/indicators": "^1.0.11",
|
|
57
|
+
"@tradejs/infra": "^1.0.11",
|
|
58
|
+
"@tradejs/node": "^1.0.11",
|
|
59
|
+
"@tradejs/types": "^1.0.11",
|
|
60
|
+
"@types/bcryptjs": "2.4.6",
|
|
61
|
+
"@types/lodash": "4.14.202",
|
|
62
|
+
"@types/node": "24.13.3",
|
|
63
|
+
"@types/react": "19.2.17",
|
|
64
|
+
"@types/react-dom": "19.2.3",
|
|
65
|
+
"@types/react-window": "1.8.8",
|
|
52
66
|
"bcryptjs": "^2.4.3",
|
|
53
67
|
"date-fns": "^3.3.1",
|
|
68
|
+
"fast-technical-indicators": "^1.1.4",
|
|
54
69
|
"idb-keyval": "^6.2.2",
|
|
55
70
|
"klinecharts": "10.0.0-alpha9",
|
|
56
71
|
"lodash": "^4.18.1",
|
|
57
|
-
"next": "
|
|
58
|
-
"next-auth": "
|
|
72
|
+
"next": "16.2.4",
|
|
73
|
+
"next-auth": "5.0.0-beta.30",
|
|
59
74
|
"next-themes": "^0.4.6",
|
|
60
|
-
"react": "
|
|
61
|
-
"react-dom": "
|
|
62
|
-
"react-icons": "
|
|
63
|
-
"react-is": "
|
|
75
|
+
"react": "19.2.3",
|
|
76
|
+
"react-dom": "19.2.3",
|
|
77
|
+
"react-icons": "5.5.0",
|
|
78
|
+
"react-is": "19.2.4",
|
|
64
79
|
"react-virtualized-auto-sizer": "^1.0.26",
|
|
65
80
|
"react-window": "^1.8.11",
|
|
66
|
-
"recharts": "
|
|
67
|
-
"shiki": "
|
|
81
|
+
"recharts": "3.1.1",
|
|
82
|
+
"shiki": "3.9.2",
|
|
68
83
|
"technicalindicators": "^3.1.0",
|
|
69
|
-
"
|
|
84
|
+
"typescript": "5.9.3",
|
|
85
|
+
"zustand": "5.0.7"
|
|
70
86
|
},
|
|
71
87
|
"scripts": {
|
|
72
88
|
"dev": "node ./bin/tradejs-app.mjs dev",
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { API } from '@tradejs/core/api';
|
|
2
2
|
import { Item, OrderLogData, TestResult } from '@tradejs/types';
|
|
3
|
+
import type {
|
|
4
|
+
BacktestConfigSummary,
|
|
5
|
+
BacktestJobRecord,
|
|
6
|
+
BacktestJobRequest,
|
|
7
|
+
} from '#app/lib/backtestJobs';
|
|
3
8
|
|
|
4
9
|
const API_BASE = '/api/backtest';
|
|
5
10
|
|
|
@@ -53,3 +58,52 @@ export const deleteBacktest = async (
|
|
|
53
58
|
|
|
54
59
|
return data.deleted === true;
|
|
55
60
|
};
|
|
61
|
+
|
|
62
|
+
export const getBacktestRunConfigs = async (): Promise<
|
|
63
|
+
BacktestConfigSummary[]
|
|
64
|
+
> => {
|
|
65
|
+
const data = await API.get<{ configs?: BacktestConfigSummary[] }>(
|
|
66
|
+
`${API_BASE}/configs`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return data.configs ?? [];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getBacktestRuns = async (): Promise<BacktestJobRecord[]> => {
|
|
73
|
+
const data = await API.get<{ jobs?: BacktestJobRecord[] }>(
|
|
74
|
+
`${API_BASE}/runs`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return data.jobs ?? [];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const startBacktestRun = async (
|
|
81
|
+
request: Partial<BacktestJobRequest>,
|
|
82
|
+
): Promise<BacktestJobRecord> => {
|
|
83
|
+
const data = await API.post<{ job: BacktestJobRecord }>(
|
|
84
|
+
`${API_BASE}/runs`,
|
|
85
|
+
request,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return data.job;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const controlBacktestRun = async (
|
|
92
|
+
jobId: string,
|
|
93
|
+
action: 'pause' | 'stop' | 'resume' | 'cancel' | 'heartbeat',
|
|
94
|
+
): Promise<BacktestJobRecord> => {
|
|
95
|
+
const data = await API.post<{ job: BacktestJobRecord }>(
|
|
96
|
+
`${API_BASE}/runs/${encodeURIComponent(jobId)}`,
|
|
97
|
+
{ action },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return data.job;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const deleteBacktestRun = async (jobId: string): Promise<boolean> => {
|
|
104
|
+
const data = await API.delete<{ deleted?: boolean }>(
|
|
105
|
+
`${API_BASE}/runs/${encodeURIComponent(jobId)}`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return data.deleted === true;
|
|
109
|
+
};
|
package/src/app/actions/kline.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { API } from '@tradejs/core/api';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
KlineChartData,
|
|
4
|
+
KlineRequest,
|
|
5
|
+
MarketUniverse,
|
|
6
|
+
Provider,
|
|
7
|
+
} from '@tradejs/types';
|
|
8
|
+
import { buildKlinePath } from '#app/lib/marketRoutes';
|
|
5
9
|
|
|
6
10
|
export const kline = async ({
|
|
7
11
|
provider = 'bybit',
|
|
12
|
+
universe = 'crypto',
|
|
8
13
|
symbol,
|
|
9
14
|
interval,
|
|
10
15
|
...options
|
|
11
|
-
}: KlineRequest & { provider?: Provider }) => {
|
|
16
|
+
}: KlineRequest & { provider?: Provider; universe?: MarketUniverse }) => {
|
|
12
17
|
const data = await API.post<{ data?: KlineChartData }>(
|
|
13
|
-
|
|
18
|
+
buildKlinePath({ provider, universe, symbol, interval }),
|
|
14
19
|
options,
|
|
15
20
|
);
|
|
16
21
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { API } from '@tradejs/core/api';
|
|
2
|
-
import { Items } from '@tradejs/types';
|
|
2
|
+
import { Items, MarketUniverse } from '@tradejs/types';
|
|
3
3
|
|
|
4
4
|
const API_PATH = '/api/scanner';
|
|
5
5
|
|
|
6
|
-
export const scan = async (
|
|
7
|
-
|
|
6
|
+
export const scan = async (
|
|
7
|
+
provider = 'bybit',
|
|
8
|
+
universe: MarketUniverse = 'crypto',
|
|
9
|
+
): Promise<Items> => {
|
|
10
|
+
const data = await API.get<{ tickers?: Items }>(
|
|
11
|
+
`${API_PATH}/${provider}/${universe}`,
|
|
12
|
+
);
|
|
8
13
|
|
|
9
14
|
return data.tickers ?? [];
|
|
10
15
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import type { StrategyChartsSnapshotResponse } from '@tradejs/types';
|
|
3
|
+
import type { RuntimeStrategiesResponse } from '#app/lib/runtimeStrategies';
|
|
4
|
+
|
|
5
|
+
const API_PATH = '/api/strategies/runtime';
|
|
6
|
+
const REPLAY_API_PATH = '/api/strategies/replay';
|
|
7
|
+
const AI_API_PATH = '/api/strategies/ai';
|
|
8
|
+
|
|
9
|
+
export const getRuntimeStrategies = async ({
|
|
10
|
+
provider = 'bybit',
|
|
11
|
+
hours = 168,
|
|
12
|
+
}: {
|
|
13
|
+
provider?: string;
|
|
14
|
+
hours?: number;
|
|
15
|
+
} = {}) =>
|
|
16
|
+
API.get<RuntimeStrategiesResponse>(
|
|
17
|
+
`${API_PATH}?provider=${encodeURIComponent(provider)}&hours=${encodeURIComponent(String(hours))}`,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const getReplayStrategies = async () =>
|
|
21
|
+
API.get<StrategyChartsSnapshotResponse>(REPLAY_API_PATH);
|
|
22
|
+
|
|
23
|
+
export const getAiStrategies = async () =>
|
|
24
|
+
API.get<StrategyChartsSnapshotResponse>(AI_API_PATH);
|
|
25
|
+
|
|
26
|
+
export const deleteStrategyCard = async (
|
|
27
|
+
mode: 'replay' | 'ai',
|
|
28
|
+
cardId: string | undefined,
|
|
29
|
+
): Promise<boolean> => {
|
|
30
|
+
if (!cardId) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = await API.delete<{ deleted?: boolean }>(
|
|
35
|
+
`/api/strategies/${encodeURIComponent(mode)}/${encodeURIComponent(cardId)}`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return data.deleted === true;
|
|
39
|
+
};
|
package/src/app/api/ai/route.ts
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
HumanMessage,
|
|
6
6
|
SystemMessage,
|
|
7
7
|
} from '@langchain/core/messages';
|
|
8
|
+
import { TTL_1M } from '@tradejs/core/constants';
|
|
8
9
|
import { toJson } from '@tradejs/core/data';
|
|
9
10
|
import { getAiResponseLanguagePromptName } from '@tradejs/infra/aiLanguages';
|
|
10
11
|
import { DEFAULT_AI_MODEL, getOpenRouterModelKwargs } from '@tradejs/node/ai';
|
|
11
|
-
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
12
12
|
import {
|
|
13
13
|
AIChatHistory,
|
|
14
14
|
AIChatMessage,
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
import { getData, redisKeys, setData } from '@tradejs/infra/redis';
|
|
19
19
|
import { logger } from '@tradejs/infra/logger';
|
|
20
20
|
import { getUserSettings } from '@tradejs/infra/userSettings';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveConnectorCreatorByProvider } from '#app/lib/connectorCreator';
|
|
22
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
22
23
|
|
|
23
24
|
export const dynamic = 'force-dynamic';
|
|
24
25
|
|
|
@@ -58,7 +59,7 @@ const appendMessagesToHistory = async (
|
|
|
58
59
|
): Promise<void> => {
|
|
59
60
|
const history = await getHistory(userName, symbol);
|
|
60
61
|
await setData(getHistoryKey(userName, symbol), [...history, ...messages], {
|
|
61
|
-
expire:
|
|
62
|
+
expire: TTL_1M,
|
|
62
63
|
});
|
|
63
64
|
};
|
|
64
65
|
|
|
@@ -177,7 +178,7 @@ export const POST = async (request: NextRequest) => {
|
|
|
177
178
|
|
|
178
179
|
await appendMessagesToHistory(userName, filters.symbol, [message]);
|
|
179
180
|
|
|
180
|
-
const connectorCreator = await
|
|
181
|
+
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
181
182
|
'bybit',
|
|
182
183
|
projectRoot,
|
|
183
184
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { logger } from '@tradejs/infra/logger';
|
|
3
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
4
|
+
import { listBacktestConfigs } from '#app/lib/backtestJobs';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
export const runtime = 'nodejs';
|
|
8
|
+
|
|
9
|
+
export const GET = async () => {
|
|
10
|
+
try {
|
|
11
|
+
const userName = await getCurrentUserName();
|
|
12
|
+
if (!userName) {
|
|
13
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const configs = await listBacktestConfigs(userName);
|
|
17
|
+
return NextResponse.json({ configs });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.log('error', 'Backtest configs error: %o', error);
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: 'Internal Server Error' },
|
|
22
|
+
{ status: 500 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
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 { TTL_1M } from '@tradejs/core/constants';
|
|
4
5
|
import { getData, getKeys, redisKeys, setData } from '@tradejs/infra/redis';
|
|
5
6
|
import { logger } from '@tradejs/infra/logger';
|
|
6
|
-
import { auth } from '
|
|
7
|
+
import { auth } from '#app/auth';
|
|
7
8
|
|
|
8
9
|
export const dynamic = 'force-dynamic';
|
|
9
10
|
|
|
@@ -58,7 +59,9 @@ export const GET = async () => {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
await setData(redisKeys.testSummaries(userName), result, {
|
|
62
|
+
await setData(redisKeys.testSummaries(userName), result, {
|
|
63
|
+
expire: TTL_1M,
|
|
64
|
+
});
|
|
62
65
|
|
|
63
66
|
return NextResponse.json({ items: result });
|
|
64
67
|
} catch (error) {
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
4
|
import { OrderLogData } from '@tradejs/types';
|
|
5
|
+
import {
|
|
6
|
+
parseBacktestArtifactRef,
|
|
7
|
+
readPersistedBacktestOrderLog,
|
|
8
|
+
} from '@tradejs/infra/backtestArtifacts';
|
|
5
9
|
import { getData, redisKeys } from '@tradejs/infra/redis';
|
|
6
10
|
import { logger } from '@tradejs/infra/logger';
|
|
7
|
-
import { auth } from '
|
|
11
|
+
import { auth } from '#app/auth';
|
|
8
12
|
|
|
9
13
|
interface Params {
|
|
10
14
|
strategy: string;
|
|
@@ -32,9 +36,29 @@ export const GET = async (
|
|
|
32
36
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
const
|
|
39
|
+
const storedOrderLog = await getData(
|
|
36
40
|
redisKeys.testOrders(userName, strategy, name),
|
|
37
41
|
);
|
|
42
|
+
const orderLogRef = parseBacktestArtifactRef(storedOrderLog);
|
|
43
|
+
if (!orderLogRef) {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: 'Backtest order log not found' },
|
|
46
|
+
{ status: 404 },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const orderLog: OrderLogData | null = await readPersistedBacktestOrderLog({
|
|
51
|
+
userName,
|
|
52
|
+
strategyName: strategy,
|
|
53
|
+
testName: name,
|
|
54
|
+
ref: orderLogRef,
|
|
55
|
+
});
|
|
56
|
+
if (!orderLog) {
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{ error: 'Backtest order log not found' },
|
|
59
|
+
{ status: 404 },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
38
62
|
|
|
39
63
|
return NextResponse.json({ orderLog });
|
|
40
64
|
} catch (error) {
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
4
|
import { OrderLogData, Test, TestResult, TestStat } from '@tradejs/types';
|
|
5
5
|
import { compactOrderLog, getTimeline } from '@tradejs/core/backtest';
|
|
6
|
+
import {
|
|
7
|
+
parseBacktestArtifactRef,
|
|
8
|
+
readPersistedBacktestOrderLog,
|
|
9
|
+
} from '@tradejs/infra/backtestArtifacts';
|
|
6
10
|
import { getData, redisKeys } from '@tradejs/infra/redis';
|
|
7
11
|
import { logger } from '@tradejs/infra/logger';
|
|
8
|
-
import { auth } from '
|
|
12
|
+
import { auth } from '#app/auth';
|
|
9
13
|
|
|
10
14
|
interface Params {
|
|
11
15
|
strategy: string;
|
|
@@ -33,7 +37,7 @@ export const GET = async (
|
|
|
33
37
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
const
|
|
40
|
+
const storedOrderLog = await getData(
|
|
37
41
|
redisKeys.testOrders(userName, strategy, name),
|
|
38
42
|
);
|
|
39
43
|
const test: Test = await getData(
|
|
@@ -43,6 +47,27 @@ export const GET = async (
|
|
|
43
47
|
redisKeys.testStat(userName, strategy, name),
|
|
44
48
|
);
|
|
45
49
|
|
|
50
|
+
const orderLogRef = parseBacktestArtifactRef(storedOrderLog);
|
|
51
|
+
if (!orderLogRef) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Backtest order log not found' },
|
|
54
|
+
{ status: 404 },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const orderLog: OrderLogData | null = await readPersistedBacktestOrderLog({
|
|
59
|
+
userName,
|
|
60
|
+
strategyName: strategy,
|
|
61
|
+
testName: name,
|
|
62
|
+
ref: orderLogRef,
|
|
63
|
+
});
|
|
64
|
+
if (!orderLog) {
|
|
65
|
+
return NextResponse.json(
|
|
66
|
+
{ error: 'Backtest order log not found' },
|
|
67
|
+
{ status: 404 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
// TODO: Validate test/test.options payload and return 404/422 instead of falling into 500 on malformed cache data.
|
|
47
72
|
const timeline = getTimeline(test.options.start, test.options.end);
|
|
48
73
|
|