@tradejs/app 1.0.9 → 1.0.10

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 (119) hide show
  1. package/README.md +7 -2
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
package/README.md CHANGED
@@ -5,8 +5,13 @@ Publishable Next.js UI package for the TradeJS open-source framework, with backt
5
5
  Typical external usage:
6
6
 
7
7
  ```bash
8
- npm i @tradejs/app
8
+ npm install @tradejs/app @tradejs/core @tradejs/node @tradejs/types @tradejs/base @tradejs/cli
9
9
  npx tradejs-app dev
10
10
  ```
11
11
 
12
- The launcher reads env and `tradejs.config.ts` from the caller project directory via `PROJECT_CWD`.
12
+ Use matching versions for all `@tradejs/*` packages. The installable launcher
13
+ requires `@tradejs/app@1.0.10` or newer.
14
+
15
+ The launcher reads env and `tradejs.config.ts` from the caller project directory
16
+ via `PROJECT_CWD`. When it runs from `node_modules`, it creates a generated
17
+ `.tradejs/app` working copy and runs Next.js there.
@@ -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 appDir = path.resolve(__dirname, '..');
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.9",
3
+ "version": "1.0.10",
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": "git+https://github.com/tradejs-dev/tradejs.git",
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": "^3.24.0",
42
- "@chakra-ui/react": "^3.24.2",
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.9",
47
- "@tradejs/core": "^1.0.9",
48
- "@tradejs/indicators": "^1.0.9",
49
- "@tradejs/infra": "^1.0.9",
50
- "@tradejs/node": "^1.0.9",
51
- "@tradejs/types": "^1.0.9",
54
+ "@tradejs/connectors": "^1.0.10",
55
+ "@tradejs/core": "^1.0.10",
56
+ "@tradejs/indicators": "^1.0.10",
57
+ "@tradejs/infra": "^1.0.10",
58
+ "@tradejs/node": "^1.0.10",
59
+ "@tradejs/types": "^1.0.10",
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": "^16.2.3",
58
- "next-auth": "^5.0.0-beta.26",
72
+ "next": "16.2.4",
73
+ "next-auth": "5.0.0-beta.30",
59
74
  "next-themes": "^0.4.6",
60
- "react": "^19.2.3",
61
- "react-dom": "^19.2.3",
62
- "react-icons": "^5.5.0",
63
- "react-is": "^19.2.0",
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": "^3.1.1",
67
- "shiki": "^3.9.2",
81
+ "recharts": "3.1.1",
82
+ "shiki": "3.9.2",
68
83
  "technicalindicators": "^3.1.0",
69
- "zustand": "^5.0.7"
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
+ };
@@ -1,16 +1,21 @@
1
1
  import { API } from '@tradejs/core/api';
2
- import { KlineChartData, KlineRequest, Provider } from '@tradejs/types';
3
-
4
- const API_PATH = '/api/kline';
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
- `${API_PATH}/${provider}/${symbol}/${interval}`,
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 (provider = 'bybit'): Promise<Items> => {
7
- const data = await API.get<{ tickers?: Items }>(`${API_PATH}/${provider}`);
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
+ };
@@ -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 { getCurrentUserName } from '@app/lib/currentUser';
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: 0,
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 getConnectorCreatorByProvider(
181
+ const connectorCreator = await resolveConnectorCreatorByProvider(
181
182
  'bybit',
182
183
  projectRoot,
183
184
  );
@@ -1,4 +1,4 @@
1
- import { handlers } from '@app/auth';
1
+ import { handlers } from '#app/auth';
2
2
 
3
3
  export const runtime = 'nodejs';
4
4
 
@@ -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 '@app/auth';
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, { expire: 0 });
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 '@app/auth';
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 orderLog: OrderLogData = await getData(
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 '@app/auth';
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 orderLog: OrderLogData = await getData(
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