@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
@@ -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 '@app/auth';
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: 0,
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 summary = await getDerivativesSummary(hoursRaw, limitRaw);
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 @@
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 { getCurrentUserName } from '@app/lib/currentUser';
17
- import { normalizeEndToIntervalBoundary } from '@app/lib/klineWindow';
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 Params {
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<Params> },
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 { provider, symbol, interval } = await params;
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
- (await getConnectorCreatorByProvider(provider, projectRoot)) ||
337
- (await getConnectorCreatorByProvider('bybit', projectRoot));
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
- symbol === 'BTCUSDT'
412
- ? baseData
413
- : mergeData(
414
- historicalEnd > Number(options.start ?? 0)
415
- ? await fetchRawSegment({
416
- symbol: 'BTCUSDT',
417
- start: Number(options.start ?? 0),
418
- end: historicalEnd,
419
- useBtcCache: true,
420
- })
421
- : [],
422
- liveTailRequired
423
- ? await connector.kline({
424
- symbol: 'BTCUSDT',
425
- interval: typedInterval,
426
- ...options,
427
- start: liveTailStart,
428
- end: Number(options.end),
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
- return NextResponse.json(
450
- { error: 'Internal Server Error' },
451
- { status: 500 },
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 { ConnectorCreator } from '@tradejs/types';
3
- import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
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 { getCurrentUserName } from '@app/lib/currentUser';
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 { provider } = await params;
27
- const connectorCreator =
28
- (await getConnectorCreatorByProvider(provider, projectRoot)) ||
29
- (await getConnectorCreatorByProvider('bybit', projectRoot));
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
- return NextResponse.json(
45
- { error: 'Internal Server Error' },
46
- { status: 500 },
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 { getCurrentUserName } from '@app/lib/currentUser';
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 getConnectorCreatorByProvider(
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 '@app/lib/currentUser';
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
+ }