@tradejs/app 1.0.0
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 +12 -0
- package/bin/tradejs-app.mjs +54 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +31 -0
- package/package.json +60 -0
- package/src/app/actions/ai.ts +33 -0
- package/src/app/actions/backtest.ts +55 -0
- package/src/app/actions/kline.ts +18 -0
- package/src/app/actions/scanner.ts +10 -0
- package/src/app/actions/signal.ts +19 -0
- package/src/app/api/ai/route.ts +151 -0
- package/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/src/app/api/backtest/files/route.ts +60 -0
- package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
- package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
- package/src/app/api/cron/route.ts +4 -0
- package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
- package/src/app/api/derivatives/summary/route.ts +20 -0
- package/src/app/api/files/screenshot/[name]/route.ts +42 -0
- package/src/app/api/indicators/route.ts +24 -0
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
- package/src/app/api/scanner/[provider]/route.ts +41 -0
- package/src/app/api/scanner/route.ts +31 -0
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
- package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
- package/src/app/api/spread/summary/route.ts +20 -0
- package/src/app/auth.ts +76 -0
- package/src/app/components/Backtest/CompareList/index.tsx +34 -0
- package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
- package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
- package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
- package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
- package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
- package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
- package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
- package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
- package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
- package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
- package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
- package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
- package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
- package/src/app/components/Backtest/TestCard/context.ts +14 -0
- package/src/app/components/Backtest/TestCard/index.ts +28 -0
- package/src/app/components/Backtest/TestList/index.tsx +124 -0
- package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
- package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
- package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
- package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
- package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
- package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
- package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
- package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
- package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
- package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
- package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
- package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
- package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
- package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
- package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
- package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
- package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
- package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
- package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
- package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
- package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
- package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
- package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
- package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
- package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
- package/src/app/components/Shared/AppShell.tsx +28 -0
- package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
- package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
- package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
- package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
- package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
- package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
- package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
- package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
- package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
- package/src/app/components/Shared/Filters/context.ts +17 -0
- package/src/app/components/Shared/Filters/index.ts +17 -0
- package/src/app/components/Shared/Sidebar/index.tsx +72 -0
- package/src/app/components/UI/ColorMode/index.tsx +112 -0
- package/src/app/components/UI/EmptyState/index.tsx +28 -0
- package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
- package/src/app/components/UI/Segment/index.tsx +23 -0
- package/src/app/components/UI/Select/index.tsx +81 -0
- package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
- package/src/app/components/UI/Switcher/index.tsx +24 -0
- package/src/app/components/UI/Toaster/index.tsx +45 -0
- package/src/app/components/UI/index.ts +8 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +5 -0
- package/src/app/layout.tsx +31 -0
- package/src/app/page.tsx +14 -0
- package/src/app/provider.tsx +39 -0
- package/src/app/routes/backtest/[test]/page.tsx +33 -0
- package/src/app/routes/backtest/page.tsx +374 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
- package/src/app/routes/dashboard/page.tsx +20 -0
- package/src/app/routes/derivatives/page.tsx +202 -0
- package/src/app/routes/signin/page.tsx +155 -0
- package/src/app/store/data.ts +144 -0
- package/src/app/store/filters.ts +29 -0
- package/src/app/store/index.ts +13 -0
- package/src/app/store/indicators.ts +229 -0
- package/src/app/store/tests.ts +464 -0
- package/src/app/store/tickers.ts +89 -0
- package/src/proxy.ts +142 -0
- package/tsconfig.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @tradejs/app
|
|
2
|
+
|
|
3
|
+
Publishable Next.js UI package for viewing TradeJS backtests, charts, and signal flows.
|
|
4
|
+
|
|
5
|
+
Typical external usage:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @tradejs/app
|
|
9
|
+
npx tradejs-app dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The launcher reads env and `tradejs.config.ts` from the caller project directory via `PROJECT_CWD`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import nextEnv from '@next/env';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { loadEnvConfig } = nextEnv;
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const appDir = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
const command = process.argv[2] || 'dev';
|
|
16
|
+
const rawArgs = process.argv.slice(3);
|
|
17
|
+
const projectCwd = path.resolve(
|
|
18
|
+
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
process.env.PROJECT_CWD = projectCwd;
|
|
22
|
+
|
|
23
|
+
const dev = command === 'dev';
|
|
24
|
+
loadEnvConfig(projectCwd, dev, console);
|
|
25
|
+
|
|
26
|
+
const nextBin = require.resolve('next/dist/bin/next');
|
|
27
|
+
const args = [nextBin, command, ...rawArgs];
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
command === 'start' &&
|
|
31
|
+
!rawArgs.includes('-H') &&
|
|
32
|
+
!rawArgs.includes('--hostname')
|
|
33
|
+
) {
|
|
34
|
+
args.push('-H', '0.0.0.0');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const child = spawn(process.execPath, args, {
|
|
38
|
+
cwd: appDir,
|
|
39
|
+
env: process.env,
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on('exit', (code, signal) => {
|
|
44
|
+
if (signal) {
|
|
45
|
+
process.kill(process.pid, signal);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
process.exit(code ?? 0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.on('error', (error) => {
|
|
52
|
+
console.error('[tradejs-app] failed to start Next.js:', error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
package/next-env.d.ts
ADDED
package/next.config.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
|
|
3
|
+
const nextConfig = {
|
|
4
|
+
// webpack(config) {
|
|
5
|
+
// config.module.parser = {
|
|
6
|
+
// ...config.module.parser,
|
|
7
|
+
// javascript: { ...config.module.parser?.javascript, dataUrlCondition: { maxSize: 1024 } }, // 1KB
|
|
8
|
+
// };
|
|
9
|
+
// return config;
|
|
10
|
+
// },
|
|
11
|
+
experimental: {
|
|
12
|
+
externalDir: true,
|
|
13
|
+
optimizePackageImports: ['@chakra-ui/react'],
|
|
14
|
+
},
|
|
15
|
+
async redirects() {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
source: '/routes/dashboard/:symbol/:interval',
|
|
19
|
+
destination: '/routes/dashboard/bybit/:symbol/:interval',
|
|
20
|
+
permanent: false,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
source: '/api/kline/:symbol/:interval',
|
|
24
|
+
destination: '/api/kline/bybit/:symbol/:interval',
|
|
25
|
+
permanent: false,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default nextConfig;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tradejs/app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"README.md",
|
|
6
|
+
"bin",
|
|
7
|
+
"src",
|
|
8
|
+
"!src/**/__tests__",
|
|
9
|
+
"!src/**/*.test.ts",
|
|
10
|
+
"!src/**/*.test.tsx",
|
|
11
|
+
"next.config.mjs",
|
|
12
|
+
"tsconfig.json",
|
|
13
|
+
"next-env.d.ts"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"tradejs-app": "./bin/tradejs-app.mjs"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@chakra-ui/charts": "^3.24.0",
|
|
23
|
+
"@chakra-ui/react": "^3.24.2",
|
|
24
|
+
"@emotion/react": "^11.14.0",
|
|
25
|
+
"@langchain/core": "^0.3.68",
|
|
26
|
+
"@langchain/openai": "^0.6.11",
|
|
27
|
+
"@tradejs/connectors": "^1.0.0",
|
|
28
|
+
"@tradejs/core": "^1.0.0",
|
|
29
|
+
"@tradejs/indicators": "^1.0.0",
|
|
30
|
+
"@tradejs/infra": "^1.0.0",
|
|
31
|
+
"@tradejs/node": "^1.0.0",
|
|
32
|
+
"@tradejs/types": "^1.0.0",
|
|
33
|
+
"bcryptjs": "^2.4.3",
|
|
34
|
+
"date-fns": "^3.3.1",
|
|
35
|
+
"idb-keyval": "^6.2.2",
|
|
36
|
+
"klinecharts": "10.0.0-alpha9",
|
|
37
|
+
"lodash": "^4.17.21",
|
|
38
|
+
"next": "^16.1.1",
|
|
39
|
+
"next-auth": "^5.0.0-beta.26",
|
|
40
|
+
"next-themes": "^0.4.6",
|
|
41
|
+
"react": "^19.2.3",
|
|
42
|
+
"react-dom": "^19.2.3",
|
|
43
|
+
"react-icons": "^5.5.0",
|
|
44
|
+
"react-is": "^19.2.0",
|
|
45
|
+
"react-virtualized-auto-sizer": "^1.0.26",
|
|
46
|
+
"react-window": "^1.8.11",
|
|
47
|
+
"recharts": "^3.1.1",
|
|
48
|
+
"shiki": "^3.9.2",
|
|
49
|
+
"technicalindicators": "^3.1.0",
|
|
50
|
+
"zustand": "^5.0.7"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "node ./bin/tradejs-app.mjs dev",
|
|
54
|
+
"build": "NODE_ENV=production node ./bin/tradejs-app.mjs build",
|
|
55
|
+
"start": "node ./bin/tradejs-app.mjs start",
|
|
56
|
+
"lint": "yarn run -T eslint src --ext .js,.jsx,.ts,.tsx"
|
|
57
|
+
},
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"author": "aleksnick (https://github.com/aleksnick)"
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import { AIChatMessage, AIChatHistory, Filters } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
interface SendMessageProps {
|
|
5
|
+
message: AIChatMessage;
|
|
6
|
+
filters: Filters;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const API_PATH = '/api/ai';
|
|
10
|
+
|
|
11
|
+
export const getHistory = async (symbol: string): Promise<AIChatHistory> => {
|
|
12
|
+
if (!symbol) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const data = await API.get<{ history?: AIChatHistory }>(
|
|
17
|
+
`${API_PATH}?symbol=${encodeURIComponent(symbol)}`,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return data.history ?? [];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const sendMessage = async ({
|
|
24
|
+
message,
|
|
25
|
+
filters,
|
|
26
|
+
}: SendMessageProps): Promise<AIChatMessage> => {
|
|
27
|
+
const data = await API.post<{ message: AIChatMessage }>(API_PATH, {
|
|
28
|
+
message,
|
|
29
|
+
filters,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return data.message;
|
|
33
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import { Item, OrderLogData, TestResult } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
const API_BASE = '/api/backtest';
|
|
5
|
+
|
|
6
|
+
export const getBacktestFiles = async (): Promise<Item[]> => {
|
|
7
|
+
const data = await API.get<{ items?: Item[] }>(`${API_BASE}/files`);
|
|
8
|
+
|
|
9
|
+
return data.items ?? [];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getOrderLog = async (
|
|
13
|
+
name: string | undefined,
|
|
14
|
+
strategyName: string | undefined,
|
|
15
|
+
): Promise<OrderLogData | null> => {
|
|
16
|
+
if (!name || !strategyName) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = await API.get<{ orderLog?: OrderLogData }>(
|
|
21
|
+
`${API_BASE}/order-log/${strategyName}/${name}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return data.orderLog ?? null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getBacktest = async (
|
|
28
|
+
name: string | undefined,
|
|
29
|
+
strategyName: string | undefined,
|
|
30
|
+
): Promise<TestResult | null> => {
|
|
31
|
+
if (!name || !strategyName) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await API.get<{ result?: TestResult }>(
|
|
36
|
+
`${API_BASE}/result/${strategyName}/${name}`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return data.result ?? null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const deleteBacktest = async (
|
|
43
|
+
name: string | undefined,
|
|
44
|
+
strategyName: string | undefined,
|
|
45
|
+
): Promise<boolean> => {
|
|
46
|
+
if (!name || !strategyName) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await API.delete<{ deleted?: boolean }>(
|
|
51
|
+
`${API_BASE}/test/${encodeURIComponent(strategyName)}/${encodeURIComponent(name)}`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return data.deleted === true;
|
|
55
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import { KlineChartData, KlineRequest, Provider } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
const API_PATH = '/api/kline';
|
|
5
|
+
|
|
6
|
+
export const kline = async ({
|
|
7
|
+
provider = 'bybit',
|
|
8
|
+
symbol,
|
|
9
|
+
interval,
|
|
10
|
+
...options
|
|
11
|
+
}: KlineRequest & { provider?: Provider }) => {
|
|
12
|
+
const data = await API.post<{ data?: KlineChartData }>(
|
|
13
|
+
`${API_PATH}/${provider}/${symbol}/${interval}`,
|
|
14
|
+
options,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return data.data ?? [];
|
|
18
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import { Items } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
const API_PATH = '/api/scanner';
|
|
5
|
+
|
|
6
|
+
export const scan = async (provider = 'bybit'): Promise<Items> => {
|
|
7
|
+
const data = await API.get<{ tickers?: Items }>(`${API_PATH}/${provider}`);
|
|
8
|
+
|
|
9
|
+
return data.tickers ?? [];
|
|
10
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { API } from '@tradejs/core/api';
|
|
2
|
+
import { Signal } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
const API_PATH = '/api/signal';
|
|
5
|
+
|
|
6
|
+
export const getSignal = async (
|
|
7
|
+
symbol: string,
|
|
8
|
+
signalId: string | undefined,
|
|
9
|
+
): Promise<Signal | null> => {
|
|
10
|
+
if (!signalId) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const data = await API.get<{ signal?: Signal }>(
|
|
15
|
+
`${API_PATH}/${symbol}/${signalId}`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return data.signal ?? null;
|
|
19
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
3
|
+
import {
|
|
4
|
+
BaseMessage,
|
|
5
|
+
HumanMessage,
|
|
6
|
+
SystemMessage,
|
|
7
|
+
} from '@langchain/core/messages';
|
|
8
|
+
import { toJson } from '@tradejs/core/json';
|
|
9
|
+
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
10
|
+
import {
|
|
11
|
+
AIChatHistory,
|
|
12
|
+
AIChatMessage,
|
|
13
|
+
ConnectorCreator,
|
|
14
|
+
Filters,
|
|
15
|
+
} from '@tradejs/types';
|
|
16
|
+
import { getFile, setFile } from '@tradejs/infra/files';
|
|
17
|
+
import { logger } from '@tradejs/infra/logger';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
|
|
21
|
+
const HISTORY_DIR = 'data/chats';
|
|
22
|
+
|
|
23
|
+
const getHistory = async (symbol: string): Promise<AIChatHistory> => {
|
|
24
|
+
const history = await getFile(HISTORY_DIR, symbol);
|
|
25
|
+
return history;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const appendMessagesToHistory = async (
|
|
29
|
+
symbol: string,
|
|
30
|
+
messages: AIChatHistory,
|
|
31
|
+
): Promise<void> => {
|
|
32
|
+
const history = await getHistory(symbol);
|
|
33
|
+
await setFile(HISTORY_DIR, symbol, [...history, ...messages]);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const buildMessages = (
|
|
37
|
+
filters: Filters,
|
|
38
|
+
historyEntry: AIChatMessage,
|
|
39
|
+
historyData: unknown,
|
|
40
|
+
) => {
|
|
41
|
+
const messages = new Array<BaseMessage>();
|
|
42
|
+
|
|
43
|
+
messages.push(
|
|
44
|
+
new SystemMessage(
|
|
45
|
+
'Ты – помощник крипто-трейдера. Отвечай на русском языке',
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
messages.push(
|
|
50
|
+
new SystemMessage(
|
|
51
|
+
`Вот данные по монете ${filters.symbol}: ${toJson(historyData)}`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (historyEntry.command === '/line') {
|
|
56
|
+
messages.push(new HumanMessage(historyEntry.text));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (historyEntry.command === 'prompt') {
|
|
60
|
+
messages.push(new HumanMessage(historyEntry.text));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return messages;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const invokeChatModel = async (messages: BaseMessage[]) => {
|
|
67
|
+
const model = new ChatOpenAI({
|
|
68
|
+
temperature: 0.7,
|
|
69
|
+
modelName: 'gpt-4o',
|
|
70
|
+
openAIApiKey: process.env.OPENAI_API_KEY,
|
|
71
|
+
configuration: {
|
|
72
|
+
baseURL: process.env.OPENAI_API_ENDPOINT || 'https://api.openai.com/v1',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return model.invoke(messages);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const GET = async (request: NextRequest) => {
|
|
80
|
+
try {
|
|
81
|
+
const symbol = request.nextUrl.searchParams.get('symbol');
|
|
82
|
+
|
|
83
|
+
if (!symbol) {
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: 'Missing required parameter: symbol' },
|
|
86
|
+
{ status: 400 },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const history = await getHistory(symbol);
|
|
91
|
+
return NextResponse.json({ history });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.log('error', `AI history error: %o`, error);
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'Internal Server Error' },
|
|
96
|
+
{ status: 500 },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const POST = async (request: NextRequest) => {
|
|
102
|
+
try {
|
|
103
|
+
const body = await request.json();
|
|
104
|
+
const { message, filters } = body as {
|
|
105
|
+
message?: AIChatMessage;
|
|
106
|
+
filters?: Filters;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (!message || !filters) {
|
|
110
|
+
return NextResponse.json(
|
|
111
|
+
{ error: 'Missing required fields: message, filters' },
|
|
112
|
+
{ status: 400 },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await appendMessagesToHistory(filters.symbol, [message]);
|
|
117
|
+
|
|
118
|
+
const connectorCreator = await getConnectorCreatorByProvider('bybit');
|
|
119
|
+
if (!connectorCreator) {
|
|
120
|
+
throw new Error('No connector available for provider');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const byBitConnector = await (connectorCreator as ConnectorCreator)({
|
|
124
|
+
userName: 'root',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const data = await byBitConnector.kline({
|
|
128
|
+
...filters,
|
|
129
|
+
interval: '60',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const chatMessages = buildMessages(filters, message, data.slice(-100));
|
|
133
|
+
|
|
134
|
+
const response = await invokeChatModel(chatMessages);
|
|
135
|
+
|
|
136
|
+
const responseMessage: AIChatMessage = {
|
|
137
|
+
from: 'ai',
|
|
138
|
+
text: response.content as string,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await appendMessagesToHistory(filters.symbol, [responseMessage]);
|
|
142
|
+
|
|
143
|
+
return NextResponse.json({ message: responseMessage });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.log('error', `AI message error: %o`, error);
|
|
146
|
+
return NextResponse.json(
|
|
147
|
+
{ error: 'Internal Server Error' },
|
|
148
|
+
{ status: 500 },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { Item, TestStat } from '@tradejs/types';
|
|
3
|
+
import { parseTestName } from '@tradejs/core/backtest';
|
|
4
|
+
import { getData, getKeys, redisKeys } from '@tradejs/infra/redis';
|
|
5
|
+
import { logger } from '@tradejs/infra/logger';
|
|
6
|
+
import { auth } from '@app/auth';
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic';
|
|
9
|
+
|
|
10
|
+
export const GET = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const session = await auth();
|
|
13
|
+
const userName = session?.user?.id || session?.user?.name;
|
|
14
|
+
|
|
15
|
+
if (!userName) {
|
|
16
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = new Array<Item>();
|
|
20
|
+
const testsPrefix = redisKeys.tests(userName);
|
|
21
|
+
const keys = await getKeys(testsPrefix);
|
|
22
|
+
const configKeys = keys.filter((key) => key.endsWith(':config'));
|
|
23
|
+
|
|
24
|
+
for await (const key of configKeys) {
|
|
25
|
+
const parts = key.split(':');
|
|
26
|
+
if (parts.length < 5) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const strategyName = parts[3];
|
|
30
|
+
const testName = parts[4];
|
|
31
|
+
const { symbol, testId } = parseTestName(testName);
|
|
32
|
+
|
|
33
|
+
const stat: TestStat = await getData(
|
|
34
|
+
redisKeys.testStat(userName, strategyName, testName),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!stat) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
result.push({
|
|
42
|
+
value: testName,
|
|
43
|
+
label: `${symbol}_${testId}`,
|
|
44
|
+
description: `${stat.netProfit}$`,
|
|
45
|
+
data: {
|
|
46
|
+
netProfit: stat.netProfit || 0,
|
|
47
|
+
strategyName,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return NextResponse.json({ items: result });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.log('error', `Backtest list error: %o`, error);
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: 'Internal Server Error' },
|
|
57
|
+
{ status: 500 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { OrderLogData } from '@tradejs/types';
|
|
5
|
+
import { getData, redisKeys } from '@tradejs/infra/redis';
|
|
6
|
+
import { logger } from '@tradejs/infra/logger';
|
|
7
|
+
import { auth } from '@app/auth';
|
|
8
|
+
|
|
9
|
+
interface Params {
|
|
10
|
+
strategy: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const GET = async (
|
|
15
|
+
_req: Request,
|
|
16
|
+
{ params }: { params: Promise<Params> },
|
|
17
|
+
) => {
|
|
18
|
+
try {
|
|
19
|
+
const { name, strategy } = await params;
|
|
20
|
+
|
|
21
|
+
if (!name || !strategy) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: 'Missing required parameter: name/strategy' },
|
|
24
|
+
{ status: 400 },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const session = await auth();
|
|
29
|
+
const userName = session?.user?.id || session?.user?.name;
|
|
30
|
+
|
|
31
|
+
if (!userName) {
|
|
32
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const orderLog: OrderLogData = await getData(
|
|
36
|
+
redisKeys.testOrders(userName, strategy, name),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ orderLog });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.log('error', `Backtest order log error: %o`, error);
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: 'Internal Server Error' },
|
|
44
|
+
{ status: 500 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { OrderLogData, Test, TestResult, TestStat } from '@tradejs/types';
|
|
5
|
+
import { compactOrderLog, getTimeline } from '@tradejs/core/backtest';
|
|
6
|
+
import { getData, redisKeys } from '@tradejs/infra/redis';
|
|
7
|
+
import { logger } from '@tradejs/infra/logger';
|
|
8
|
+
import { auth } from '@app/auth';
|
|
9
|
+
|
|
10
|
+
interface Params {
|
|
11
|
+
strategy: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GET = async (
|
|
16
|
+
_req: Request,
|
|
17
|
+
{ params }: { params: Promise<Params> },
|
|
18
|
+
) => {
|
|
19
|
+
try {
|
|
20
|
+
const { name, strategy } = await params;
|
|
21
|
+
|
|
22
|
+
if (!name || !strategy) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: 'Missing required parameter: name/strategy' },
|
|
25
|
+
{ status: 400 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const session = await auth();
|
|
30
|
+
const userName = session?.user?.id || session?.user?.name;
|
|
31
|
+
|
|
32
|
+
if (!userName) {
|
|
33
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const orderLog: OrderLogData = await getData(
|
|
37
|
+
redisKeys.testOrders(userName, strategy, name),
|
|
38
|
+
);
|
|
39
|
+
const test: Test = await getData(
|
|
40
|
+
redisKeys.testConfig(userName, strategy, name),
|
|
41
|
+
);
|
|
42
|
+
const stat: TestStat = await getData(
|
|
43
|
+
redisKeys.testStat(userName, strategy, name),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// TODO: Validate test/test.options payload and return 404/422 instead of falling into 500 on malformed cache data.
|
|
47
|
+
const timeline = getTimeline(test.options.start, test.options.end);
|
|
48
|
+
|
|
49
|
+
const payload: TestResult = {
|
|
50
|
+
test,
|
|
51
|
+
orderLog: compactOrderLog(timeline, orderLog),
|
|
52
|
+
stat,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return NextResponse.json({ result: payload });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.log('error', `Backtest load error: %o`, error);
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Internal Server Error' },
|
|
60
|
+
{ status: 500 },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { delKey, redisKeys } from '@tradejs/infra/redis';
|
|
5
|
+
import { logger } from '@tradejs/infra/logger';
|
|
6
|
+
import { auth } from '@app/auth';
|
|
7
|
+
|
|
8
|
+
interface Params {
|
|
9
|
+
strategy: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DELETE = async (
|
|
14
|
+
_req: Request,
|
|
15
|
+
{ params }: { params: Promise<Params> },
|
|
16
|
+
) => {
|
|
17
|
+
try {
|
|
18
|
+
const { name, strategy } = await params;
|
|
19
|
+
|
|
20
|
+
if (!name || !strategy) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: 'Missing required parameter: name/strategy' },
|
|
23
|
+
{ status: 400 },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const session = await auth();
|
|
28
|
+
const userName = session?.user?.id || session?.user?.name;
|
|
29
|
+
|
|
30
|
+
if (!userName) {
|
|
31
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const removeResults = await Promise.all([
|
|
35
|
+
delKey(redisKeys.testConfig(userName, strategy, name)),
|
|
36
|
+
delKey(redisKeys.testStat(userName, strategy, name)),
|
|
37
|
+
delKey(redisKeys.testOrders(userName, strategy, name)),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const removedKeys = removeResults.filter(Boolean).length;
|
|
41
|
+
|
|
42
|
+
if (removedKeys === 0) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: 'Backtest not found' },
|
|
45
|
+
{ status: 404 },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return NextResponse.json({ deleted: true, removedKeys });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.log('error', 'Backtest delete error: %o', error);
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Internal Server Error' },
|
|
54
|
+
{ status: 500 },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|