@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { logger } from '@tradejs/infra/logger';
|
|
3
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
4
|
+
import {
|
|
5
|
+
cancelBacktestJob,
|
|
6
|
+
deleteBacktestJob,
|
|
7
|
+
getBacktestJob,
|
|
8
|
+
heartbeatBacktestJob,
|
|
9
|
+
pauseBacktestJob,
|
|
10
|
+
resumeBacktestJob,
|
|
11
|
+
} from '#app/lib/backtestJobs';
|
|
12
|
+
|
|
13
|
+
interface Params {
|
|
14
|
+
jobId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type BacktestJobAction = 'pause' | 'stop' | 'resume' | 'cancel' | 'heartbeat';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
export const runtime = 'nodejs';
|
|
21
|
+
|
|
22
|
+
const isBacktestJobAction = (value: unknown): value is BacktestJobAction =>
|
|
23
|
+
value === 'pause' ||
|
|
24
|
+
value === 'stop' ||
|
|
25
|
+
value === 'resume' ||
|
|
26
|
+
value === 'cancel' ||
|
|
27
|
+
value === 'heartbeat';
|
|
28
|
+
|
|
29
|
+
const toErrorMessage = (error: unknown) =>
|
|
30
|
+
error instanceof Error ? error.message : String(error);
|
|
31
|
+
|
|
32
|
+
export const GET = async (
|
|
33
|
+
_req: NextRequest,
|
|
34
|
+
{ params }: { params: Promise<Params> },
|
|
35
|
+
) => {
|
|
36
|
+
try {
|
|
37
|
+
const userName = await getCurrentUserName();
|
|
38
|
+
if (!userName) {
|
|
39
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { jobId } = await params;
|
|
43
|
+
const job = await getBacktestJob(userName, jobId);
|
|
44
|
+
if (!job) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: 'Backtest job not found' },
|
|
47
|
+
{ status: 404 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({ job });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.log('error', 'Backtest run load error: %o', error);
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ error: 'Internal Server Error' },
|
|
56
|
+
{ status: 500 },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const POST = async (
|
|
62
|
+
req: NextRequest,
|
|
63
|
+
{ params }: { params: Promise<Params> },
|
|
64
|
+
) => {
|
|
65
|
+
try {
|
|
66
|
+
const userName = await getCurrentUserName();
|
|
67
|
+
if (!userName) {
|
|
68
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { jobId } = await params;
|
|
72
|
+
const payload = (await req.json()) as { action?: unknown };
|
|
73
|
+
const action = payload.action;
|
|
74
|
+
if (!isBacktestJobAction(action)) {
|
|
75
|
+
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const job =
|
|
79
|
+
action === 'pause'
|
|
80
|
+
? await pauseBacktestJob(userName, jobId, 'manual_pause')
|
|
81
|
+
: action === 'stop'
|
|
82
|
+
? await pauseBacktestJob(userName, jobId, 'manual_stop')
|
|
83
|
+
: action === 'resume'
|
|
84
|
+
? await resumeBacktestJob(userName, jobId)
|
|
85
|
+
: action === 'cancel'
|
|
86
|
+
? await cancelBacktestJob(userName, jobId)
|
|
87
|
+
: await heartbeatBacktestJob(userName, jobId);
|
|
88
|
+
|
|
89
|
+
if (!job) {
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Backtest job not found' },
|
|
92
|
+
{ status: 404 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({ job });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = toErrorMessage(error);
|
|
99
|
+
logger.log('error', 'Backtest run action error: %o', error);
|
|
100
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const DELETE = async (
|
|
105
|
+
_req: NextRequest,
|
|
106
|
+
{ params }: { params: Promise<Params> },
|
|
107
|
+
) => {
|
|
108
|
+
try {
|
|
109
|
+
const userName = await getCurrentUserName();
|
|
110
|
+
if (!userName) {
|
|
111
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { jobId } = await params;
|
|
115
|
+
const deleted = await deleteBacktestJob(userName, jobId);
|
|
116
|
+
return NextResponse.json({ deleted });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.log('error', 'Backtest run delete error: %o', error);
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{ error: 'Internal Server Error' },
|
|
121
|
+
{ status: 500 },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { logger } from '@tradejs/infra/logger';
|
|
3
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
4
|
+
import { listBacktestJobs, startBacktestJob } from '#app/lib/backtestJobs';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
export const runtime = 'nodejs';
|
|
8
|
+
|
|
9
|
+
const toErrorMessage = (error: unknown) =>
|
|
10
|
+
error instanceof Error ? error.message : String(error);
|
|
11
|
+
|
|
12
|
+
export const GET = async () => {
|
|
13
|
+
try {
|
|
14
|
+
const userName = await getCurrentUserName();
|
|
15
|
+
if (!userName) {
|
|
16
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const jobs = await listBacktestJobs(userName);
|
|
20
|
+
return NextResponse.json({ jobs });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
logger.log('error', 'Backtest runs list error: %o', error);
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: 'Internal Server Error' },
|
|
25
|
+
{ status: 500 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const POST = async (req: NextRequest) => {
|
|
31
|
+
try {
|
|
32
|
+
const userName = await getCurrentUserName();
|
|
33
|
+
if (!userName) {
|
|
34
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const payload = await req.json();
|
|
38
|
+
const job = await startBacktestJob(userName, payload);
|
|
39
|
+
return NextResponse.json({ job }, { status: 201 });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = toErrorMessage(error);
|
|
42
|
+
logger.log('error', 'Backtest run start error: %o', error);
|
|
43
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
|
+
import {
|
|
5
|
+
deletePersistedBacktestOrderLog,
|
|
6
|
+
parseBacktestArtifactRef,
|
|
7
|
+
} from '@tradejs/infra/backtestArtifacts';
|
|
8
|
+
import { TTL_1M } from '@tradejs/core/constants';
|
|
4
9
|
import { delKey, getData, redisKeys, setData } from '@tradejs/infra/redis';
|
|
5
10
|
import { Item } from '@tradejs/types';
|
|
6
11
|
import { logger } from '@tradejs/infra/logger';
|
|
7
|
-
import { auth } from '
|
|
12
|
+
import { auth } from '#app/auth';
|
|
8
13
|
|
|
9
14
|
interface Params {
|
|
10
15
|
strategy: string;
|
|
@@ -32,10 +37,22 @@ export const DELETE = async (
|
|
|
32
37
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
const storedOrderLog = await getData(
|
|
41
|
+
redisKeys.testOrders(userName, strategy, name),
|
|
42
|
+
null,
|
|
43
|
+
);
|
|
44
|
+
const orderLogRef = parseBacktestArtifactRef(storedOrderLog);
|
|
45
|
+
|
|
35
46
|
const removeResults = await Promise.all([
|
|
36
47
|
delKey(redisKeys.testConfig(userName, strategy, name)),
|
|
37
48
|
delKey(redisKeys.testStat(userName, strategy, name)),
|
|
38
49
|
delKey(redisKeys.testOrders(userName, strategy, name)),
|
|
50
|
+
deletePersistedBacktestOrderLog({
|
|
51
|
+
userName,
|
|
52
|
+
strategyName: strategy,
|
|
53
|
+
testName: name,
|
|
54
|
+
ref: orderLogRef,
|
|
55
|
+
}),
|
|
39
56
|
]);
|
|
40
57
|
|
|
41
58
|
const removedKeys = removeResults.filter(Boolean).length;
|
|
@@ -60,7 +77,7 @@ export const DELETE = async (
|
|
|
60
77
|
),
|
|
61
78
|
);
|
|
62
79
|
await setData(redisKeys.testSummaries(userName), nextIndexedItems, {
|
|
63
|
-
expire:
|
|
80
|
+
expire: TTL_1M,
|
|
64
81
|
});
|
|
65
82
|
|
|
66
83
|
return NextResponse.json({ deleted: true, removedKeys });
|
|
@@ -8,7 +8,14 @@ export const GET = async (request: NextRequest) => {
|
|
|
8
8
|
try {
|
|
9
9
|
const hoursRaw = Number(request.nextUrl.searchParams.get('hours') ?? 24);
|
|
10
10
|
const limitRaw = Number(request.nextUrl.searchParams.get('limit') ?? 500);
|
|
11
|
-
const
|
|
11
|
+
const symbolsRaw = String(
|
|
12
|
+
request.nextUrl.searchParams.get('symbols') ?? '',
|
|
13
|
+
);
|
|
14
|
+
const symbols = symbolsRaw
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((symbol) => symbol.trim().toUpperCase())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
const summary = await getDerivativesSummary(hoursRaw, limitRaw, symbols);
|
|
12
19
|
return NextResponse.json(summary);
|
|
13
20
|
} catch (error) {
|
|
14
21
|
logger.log('error', 'Derivatives summary error: %o', error);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
initializeInstallation,
|
|
4
|
+
isInstallationRequired,
|
|
5
|
+
} from '#app/lib/installation';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export const GET = async () => {
|
|
10
|
+
const required = await isInstallationRequired();
|
|
11
|
+
return NextResponse.json({ required });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const POST = async (request: Request) => {
|
|
15
|
+
const body = (await request.json().catch(() => null)) as {
|
|
16
|
+
password?: unknown;
|
|
17
|
+
confirmPassword?: unknown;
|
|
18
|
+
} | null;
|
|
19
|
+
const password = typeof body?.password === 'string' ? body.password : '';
|
|
20
|
+
const confirmPassword =
|
|
21
|
+
typeof body?.confirmPassword === 'string' ? body.confirmPassword : '';
|
|
22
|
+
|
|
23
|
+
if (password.length < 8) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: 'Password must contain at least 8 characters' },
|
|
26
|
+
{ status: 400 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (password.length > 256) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: 'Password is too long' },
|
|
32
|
+
{ status: 400 },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (password !== confirmPassword) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: 'Passwords do not match' },
|
|
38
|
+
{ status: 400 },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const initialized = await initializeInstallation(password);
|
|
43
|
+
if (!initialized) {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: 'TradeJS is already installed' },
|
|
46
|
+
{ status: 409 },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return NextResponse.json({ userName: 'root' }, { status: 201 });
|
|
51
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { dynamic, POST } from '../../../../handler';
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
createIndicators,
|
|
5
5
|
getRegisteredIndicatorEntries,
|
|
6
6
|
} from '@tradejs/core/indicators';
|
|
7
|
-
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
8
7
|
import { ensureIndicatorPluginsLoaded } from '@tradejs/node/registry';
|
|
9
8
|
import { logger } from '@tradejs/infra/logger';
|
|
10
9
|
import {
|
|
@@ -12,9 +11,13 @@ import {
|
|
|
12
11
|
KlineRequest,
|
|
13
12
|
Interval,
|
|
14
13
|
ConnectorCreator,
|
|
14
|
+
isMarketUniverse,
|
|
15
|
+
MarketUniverse,
|
|
15
16
|
} from '@tradejs/types';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
17
|
+
import { resolveConnectorCreatorByProvider } from '#app/lib/connectorCreator';
|
|
18
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
19
|
+
import { getErrorMessage } from '#app/lib/errorMessage';
|
|
20
|
+
import { normalizeEndToIntervalBoundary } from '#app/lib/klineWindow';
|
|
18
21
|
|
|
19
22
|
export const dynamic = 'force-dynamic';
|
|
20
23
|
const projectRoot =
|
|
@@ -23,8 +26,9 @@ const DEFAULT_KLINE_CACHE_TTL_MS = 30_000;
|
|
|
23
26
|
const MAX_KLINE_CACHE_ENTRIES = 500;
|
|
24
27
|
const MAX_KLINE_CACHE_BYTES = 32 * 1024 * 1024;
|
|
25
28
|
|
|
26
|
-
interface
|
|
29
|
+
interface KlineRouteParams {
|
|
27
30
|
provider: string;
|
|
31
|
+
universe?: string;
|
|
28
32
|
symbol: string;
|
|
29
33
|
interval: string;
|
|
30
34
|
}
|
|
@@ -46,17 +50,12 @@ type PluginRegistrySnapshot = {
|
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
declare global {
|
|
49
|
-
// eslint-disable-next-line no-var
|
|
50
53
|
var __tradejsKlineRawCache__: ManagedKlineCache | undefined;
|
|
51
|
-
// eslint-disable-next-line no-var
|
|
52
54
|
var __tradejsKlineBtcRawCache__: ManagedKlineCache | undefined;
|
|
53
|
-
// eslint-disable-next-line no-var
|
|
54
55
|
var __tradejsKlineEnrichedCache__: ManagedKlineCache | undefined;
|
|
55
|
-
// eslint-disable-next-line no-var
|
|
56
56
|
var __tradejsKlineInflightRequests__:
|
|
57
57
|
| Map<string, Promise<KlineChartData>>
|
|
58
58
|
| undefined;
|
|
59
|
-
// eslint-disable-next-line no-var
|
|
60
59
|
var __tradejsPluginRegistrySnapshotPromise__:
|
|
61
60
|
| Promise<PluginRegistrySnapshot>
|
|
62
61
|
| undefined;
|
|
@@ -138,6 +137,7 @@ const getCacheTtlMs = (interval: Interval) =>
|
|
|
138
137
|
|
|
139
138
|
const buildRawCacheKey = (params: {
|
|
140
139
|
provider: string;
|
|
140
|
+
universe: MarketUniverse;
|
|
141
141
|
symbol: string;
|
|
142
142
|
interval: Interval;
|
|
143
143
|
start: number;
|
|
@@ -146,6 +146,7 @@ const buildRawCacheKey = (params: {
|
|
|
146
146
|
}) =>
|
|
147
147
|
[
|
|
148
148
|
params.provider,
|
|
149
|
+
params.universe,
|
|
149
150
|
params.symbol,
|
|
150
151
|
params.interval,
|
|
151
152
|
params.start,
|
|
@@ -156,6 +157,7 @@ const buildRawCacheKey = (params: {
|
|
|
156
157
|
const buildEnrichedCacheKey = (params: {
|
|
157
158
|
userName: string;
|
|
158
159
|
provider: string;
|
|
160
|
+
universe: MarketUniverse;
|
|
159
161
|
symbol: string;
|
|
160
162
|
interval: Interval;
|
|
161
163
|
start: number;
|
|
@@ -167,6 +169,7 @@ const buildEnrichedCacheKey = (params: {
|
|
|
167
169
|
[
|
|
168
170
|
params.userName,
|
|
169
171
|
params.provider,
|
|
172
|
+
params.universe,
|
|
170
173
|
params.symbol,
|
|
171
174
|
params.interval,
|
|
172
175
|
params.start,
|
|
@@ -278,7 +281,7 @@ const getPluginRegistrySnapshot = async (): Promise<PluginRegistrySnapshot> => {
|
|
|
278
281
|
|
|
279
282
|
export const POST = async (
|
|
280
283
|
request: NextRequest,
|
|
281
|
-
{ params }: { params: Promise<
|
|
284
|
+
{ params }: { params: Promise<KlineRouteParams> },
|
|
282
285
|
) => {
|
|
283
286
|
try {
|
|
284
287
|
const userName = await getCurrentUserName();
|
|
@@ -286,7 +289,21 @@ export const POST = async (
|
|
|
286
289
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
const
|
|
292
|
+
const routeParams = await params;
|
|
293
|
+
const {
|
|
294
|
+
provider,
|
|
295
|
+
universe: requestedUniverse,
|
|
296
|
+
symbol,
|
|
297
|
+
interval,
|
|
298
|
+
} = routeParams;
|
|
299
|
+
const rawUniverse = requestedUniverse ?? 'crypto';
|
|
300
|
+
if (!isMarketUniverse(rawUniverse)) {
|
|
301
|
+
return NextResponse.json(
|
|
302
|
+
{ error: `Unknown market universe: ${rawUniverse}` },
|
|
303
|
+
{ status: 400 },
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const universe: MarketUniverse = rawUniverse;
|
|
290
307
|
const body = await request.json();
|
|
291
308
|
const options = body as
|
|
292
309
|
| Omit<KlineRequest, 'symbol' | 'interval'>
|
|
@@ -309,6 +326,7 @@ export const POST = async (
|
|
|
309
326
|
const requestKey = buildEnrichedCacheKey({
|
|
310
327
|
userName,
|
|
311
328
|
provider,
|
|
329
|
+
universe,
|
|
312
330
|
symbol,
|
|
313
331
|
interval: typedInterval,
|
|
314
332
|
start: Number(options.start ?? 0),
|
|
@@ -332,14 +350,16 @@ export const POST = async (
|
|
|
332
350
|
}
|
|
333
351
|
|
|
334
352
|
const pending = (async () => {
|
|
335
|
-
const connectorCreator =
|
|
336
|
-
|
|
337
|
-
|
|
353
|
+
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
354
|
+
provider,
|
|
355
|
+
projectRoot,
|
|
356
|
+
);
|
|
338
357
|
if (!connectorCreator) {
|
|
339
358
|
throw new Error('No connector available for provider');
|
|
340
359
|
}
|
|
341
360
|
const connector = await (connectorCreator as ConnectorCreator)({
|
|
342
361
|
userName,
|
|
362
|
+
universe,
|
|
343
363
|
});
|
|
344
364
|
|
|
345
365
|
const liveTailRequired = Number(options.end) > historicalEnd;
|
|
@@ -360,6 +380,7 @@ export const POST = async (
|
|
|
360
380
|
const cache = segmentParams.useBtcCache ? btcCache : rawCache;
|
|
361
381
|
const cacheKey = buildRawCacheKey({
|
|
362
382
|
provider,
|
|
383
|
+
universe,
|
|
363
384
|
symbol: segmentParams.symbol,
|
|
364
385
|
interval: typedInterval,
|
|
365
386
|
start: segmentParams.start,
|
|
@@ -408,27 +429,29 @@ export const POST = async (
|
|
|
408
429
|
}
|
|
409
430
|
|
|
410
431
|
const btcData =
|
|
411
|
-
|
|
412
|
-
?
|
|
413
|
-
:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
+
universe === 'tradfi'
|
|
433
|
+
? []
|
|
434
|
+
: symbol === 'BTCUSDT'
|
|
435
|
+
? baseData
|
|
436
|
+
: mergeData(
|
|
437
|
+
historicalEnd > Number(options.start ?? 0)
|
|
438
|
+
? await fetchRawSegment({
|
|
439
|
+
symbol: 'BTCUSDT',
|
|
440
|
+
start: Number(options.start ?? 0),
|
|
441
|
+
end: historicalEnd,
|
|
442
|
+
useBtcCache: true,
|
|
443
|
+
})
|
|
444
|
+
: [],
|
|
445
|
+
liveTailRequired
|
|
446
|
+
? await connector.kline({
|
|
447
|
+
symbol: 'BTCUSDT',
|
|
448
|
+
interval: typedInterval,
|
|
449
|
+
...options,
|
|
450
|
+
start: liveTailStart,
|
|
451
|
+
end: Number(options.end),
|
|
452
|
+
})
|
|
453
|
+
: [],
|
|
454
|
+
);
|
|
432
455
|
|
|
433
456
|
const data = enrichWithPluginIndicators(
|
|
434
457
|
baseData,
|
|
@@ -446,9 +469,8 @@ export const POST = async (
|
|
|
446
469
|
return NextResponse.json({ data: await pending });
|
|
447
470
|
} catch (error) {
|
|
448
471
|
logger.log('error', `Kline fetch error: %o`, error);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
);
|
|
472
|
+
const message = getErrorMessage(error);
|
|
473
|
+
const status = /unsupported market universe/i.test(message) ? 400 : 500;
|
|
474
|
+
return NextResponse.json({ error: message }, { status });
|
|
453
475
|
}
|
|
454
476
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { dynamic, GET } from '../route';
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
ConnectorCreator,
|
|
4
|
+
isMarketUniverse,
|
|
5
|
+
MarketUniverse,
|
|
6
|
+
} from '@tradejs/types';
|
|
4
7
|
import { getTopTickers } from '@tradejs/core/tickers';
|
|
5
8
|
import { logger } from '@tradejs/infra/logger';
|
|
6
|
-
import {
|
|
9
|
+
import { resolveConnectorCreatorByProvider } from '#app/lib/connectorCreator';
|
|
10
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
11
|
+
import { getErrorMessage } from '#app/lib/errorMessage';
|
|
7
12
|
|
|
8
13
|
export const dynamic = 'force-dynamic';
|
|
9
14
|
const projectRoot =
|
|
@@ -11,6 +16,7 @@ const projectRoot =
|
|
|
11
16
|
|
|
12
17
|
interface Params {
|
|
13
18
|
provider: string;
|
|
19
|
+
universe?: string;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const GET = async (
|
|
@@ -23,27 +29,39 @@ export const GET = async (
|
|
|
23
29
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const routeParams = await params;
|
|
33
|
+
const { provider, universe: requestedUniverse } = routeParams;
|
|
34
|
+
const rawUniverse = requestedUniverse ?? 'crypto';
|
|
35
|
+
if (!isMarketUniverse(rawUniverse)) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: `Unknown market universe: ${rawUniverse}` },
|
|
38
|
+
{ status: 400 },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const universe: MarketUniverse = rawUniverse;
|
|
42
|
+
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
43
|
+
provider,
|
|
44
|
+
projectRoot,
|
|
45
|
+
);
|
|
30
46
|
if (!connectorCreator) {
|
|
31
47
|
throw new Error('No connector available for provider');
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
const connector = await (connectorCreator as ConnectorCreator)({
|
|
35
51
|
userName,
|
|
52
|
+
...(requestedUniverse ? { universe } : {}),
|
|
36
53
|
});
|
|
37
54
|
|
|
38
|
-
const data = await connector.getTickers(
|
|
55
|
+
const data = await connector.getTickers(
|
|
56
|
+
requestedUniverse ? { universe } : undefined,
|
|
57
|
+
);
|
|
39
58
|
const tickers = getTopTickers(data);
|
|
40
59
|
|
|
41
60
|
return NextResponse.json({ tickers });
|
|
42
61
|
} catch (error) {
|
|
43
62
|
logger.log('error', `Scanner error: %o`, error);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
63
|
+
const message = getErrorMessage(error);
|
|
64
|
+
const status = /unsupported market universe/i.test(message) ? 400 : 500;
|
|
65
|
+
return NextResponse.json({ error: message }, { status });
|
|
48
66
|
}
|
|
49
67
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { getTopTickers } from '@tradejs/core/tickers';
|
|
3
|
-
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
4
3
|
import { logger } from '@tradejs/infra/logger';
|
|
5
4
|
import { ConnectorCreator } from '@tradejs/types';
|
|
6
|
-
import {
|
|
5
|
+
import { resolveConnectorCreatorByProvider } from '#app/lib/connectorCreator';
|
|
6
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
7
7
|
|
|
8
8
|
export const dynamic = 'force-dynamic';
|
|
9
9
|
const projectRoot =
|
|
@@ -16,7 +16,7 @@ export const GET = async () => {
|
|
|
16
16
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const connectorCreator = await
|
|
19
|
+
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
20
20
|
'bybit',
|
|
21
21
|
projectRoot,
|
|
22
22
|
);
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
|
|
|
2
2
|
import { getData, redisKeys } from '@tradejs/infra/redis';
|
|
3
3
|
import { logger } from '@tradejs/infra/logger';
|
|
4
4
|
import { Signal } from '@tradejs/types';
|
|
5
|
-
import { getCurrentUserName } from '
|
|
5
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
6
6
|
|
|
7
7
|
export const dynamic = 'force-dynamic';
|
|
8
8
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { delKey, redisKeys } from '@tradejs/infra/redis';
|
|
3
|
+
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function DELETE(
|
|
8
|
+
_req: Request,
|
|
9
|
+
{ params }: { params: Promise<{ cardId: string }> },
|
|
10
|
+
) {
|
|
11
|
+
const userName = await getCurrentUserName();
|
|
12
|
+
if (!userName) {
|
|
13
|
+
return NextResponse.json({ deleted: false }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { cardId } = await params;
|
|
17
|
+
const deleted = await delKey(
|
|
18
|
+
redisKeys.strategyChartCard(userName, 'ai', decodeURIComponent(cardId)),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return NextResponse.json({ deleted });
|
|
22
|
+
}
|