@tradejs/app 2.0.18 → 2.0.20

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 (52) hide show
  1. package/package.json +14 -8
  2. package/src/app/actions/backtest.ts +2 -1
  3. package/src/app/actions/scanner.ts +2 -1
  4. package/src/app/api/backtest/files/route.ts +2 -1
  5. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +1 -1
  6. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +1 -1
  7. package/src/app/api/derivatives/summary/route.ts +1 -1
  8. package/src/app/api/signal/[symbol]/[signalId]/route.ts +2 -1
  9. package/src/app/api/spread/[symbol]/[interval]/route.ts +1 -1
  10. package/src/app/api/spread/summary/route.ts +1 -1
  11. package/src/app/api/strategies/runtime/route.ts +4 -674
  12. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
  13. package/src/app/api/user/runtime-deployments/route.ts +2 -2
  14. package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
  15. package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
  16. package/src/app/components/Backtest/TestList/index.tsx +1 -1
  17. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
  18. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
  19. package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
  20. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
  21. package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
  22. package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
  23. package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
  24. package/src/app/components/Shared/Filters/context.ts +1 -1
  25. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +82 -861
  26. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +115 -184
  27. package/src/app/components/Strategies/StrategyEvidencePopover.tsx +259 -0
  28. package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
  29. package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
  30. package/src/app/components/UI/Segment/index.tsx +1 -1
  31. package/src/app/components/UI/Select/index.tsx +1 -1
  32. package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
  33. package/src/app/lib/backtestJobContracts.ts +67 -0
  34. package/src/app/lib/backtestJobProgress.ts +28 -0
  35. package/src/app/lib/backtestJobRequest.ts +107 -0
  36. package/src/app/lib/backtestJobs.ts +25 -257
  37. package/src/app/lib/runtimeDashboard.ts +700 -0
  38. package/src/app/lib/runtimeStrategies.ts +22 -457
  39. package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
  40. package/src/app/lib/runtimeStrategyLineage.ts +264 -0
  41. package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
  42. package/src/app/lib/runtimeTradeSync.ts +1 -1
  43. package/src/app/lib/strategyEvidenceTimeline.ts +298 -0
  44. package/src/app/lib/strategyPerformance.ts +387 -0
  45. package/src/app/routes/dashboard/Dashboard.tsx +2 -6
  46. package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
  47. package/src/app/routes/derivatives/page.tsx +70 -262
  48. package/src/app/store/filters.ts +2 -1
  49. package/src/app/store/indicators.ts +2 -1
  50. package/src/app/store/tests.ts +1 -1
  51. package/src/app/store/tickers.ts +2 -1
  52. package/src/app/types/ui.ts +20 -0
@@ -1,176 +1,23 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { getAvailableStrategyNames } from '@tradejs/node/strategies';
3
2
  import {
4
- listTradingAccounts,
5
- resolveTradingAccount,
6
- } from '@tradejs/infra/tradingAccounts';
7
- import { getData, getKeys, redisKeys, setData } from '@tradejs/infra/redis';
8
- import type { Interval, MarketUniverse, StrategyConfig } from '@tradejs/types';
3
+ getRuntimeStrategyConfigOptions,
4
+ RuntimeStrategyConfigServiceError,
5
+ saveRuntimeStrategyConfigForUser,
6
+ } from '#app/lib/runtimeStrategyConfigService';
9
7
  import { getCurrentUserName } from '#app/lib/currentUser';
10
- import { resolveStrategyConfigIdentityByKey } from '#app/lib/runtimeStrategies';
11
8
 
12
9
  export const dynamic = 'force-dynamic';
13
10
 
14
11
  const projectRoot =
15
12
  String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
16
- const INTERVALS = new Set([
17
- '1',
18
- '3',
19
- '5',
20
- '15',
21
- '30',
22
- '60',
23
- '120',
24
- '240',
25
- '360',
26
- '720',
27
- '1440',
28
- ]);
29
-
30
- type StoredRuntimeConfig = {
31
- key: string;
32
- strategyName: string;
33
- configId: string;
34
- config: StrategyConfig;
35
- };
36
-
37
- const loadConfigs = async (
38
- userName: string,
39
- ): Promise<StoredRuntimeConfig[]> => {
40
- const keys = await getKeys(`${redisKeys.strategies(userName)}:`);
41
- const rows = await Promise.all(
42
- keys.map(async (key): Promise<StoredRuntimeConfig | null> => {
43
- const identity = resolveStrategyConfigIdentityByKey(userName, key);
44
- if (!identity) return null;
45
- const config = (await getData(key, null)) as StrategyConfig | null;
46
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
47
- return null;
48
- }
49
- return { key, ...identity, config };
50
- }),
51
- );
52
- return rows.filter((row): row is StoredRuntimeConfig => row != null);
53
- };
54
-
55
- const normalizeConfigId = (value: unknown) => {
56
- const configId = String(value ?? '').trim();
57
- if (!configId) throw new Error('Config id is required');
58
- if (!/^[a-zA-Z0-9_-]+$/.test(configId)) {
59
- throw new Error('Config id may contain only letters, numbers, _ and -');
60
- }
61
- if (configId === 'results')
62
- throw new Error('Config id "results" is reserved');
63
- return configId;
64
- };
65
-
66
- const normalizeUniverse = (value: unknown): MarketUniverse =>
67
- value === 'tradfi' ? 'tradfi' : 'crypto';
68
-
69
- const normalizeInterval = (value: unknown): Interval => {
70
- const interval = String(value ?? '15');
71
- if (!INTERVALS.has(interval))
72
- throw new Error(`Unsupported timeframe: ${interval}`);
73
- return interval as Interval;
74
- };
75
-
76
- const normalizeAccountId = (value: unknown) => {
77
- const accountId = String(value ?? '').trim();
78
- return accountId || undefined;
79
- };
80
-
81
- const resolveEffectiveAccountId = async ({
82
- userName,
83
- config,
84
- }: {
85
- userName: string;
86
- config: StrategyConfig;
87
- }) => {
88
- const universe = normalizeUniverse(config.UNIVERSE);
89
- const account = await resolveTradingAccount({
90
- userName,
91
- accountId: normalizeAccountId(config.ACCOUNT_ID),
92
- provider: 'bybit',
93
- universe,
94
- });
95
- return account?.id ?? null;
96
- };
97
-
98
- const assertNoEnabledAccountConflict = async ({
99
- userName,
100
- strategyName,
101
- configId,
102
- config,
103
- existingConfigs,
104
- }: {
105
- userName: string;
106
- strategyName: string;
107
- configId: string;
108
- config: StrategyConfig;
109
- existingConfigs: StoredRuntimeConfig[];
110
- }) => {
111
- if (config.ENABLE === false) return;
112
- const accountId = await resolveEffectiveAccountId({ userName, config });
113
- if (!accountId) {
114
- throw new Error(
115
- `No enabled Bybit account supports ${normalizeUniverse(config.UNIVERSE)}. Connect an account or save this config as disabled.`,
116
- );
117
- }
118
- for (const candidate of existingConfigs) {
119
- if (
120
- candidate.strategyName !== strategyName ||
121
- candidate.configId === configId ||
122
- candidate.config.ENABLE === false
123
- ) {
124
- continue;
125
- }
126
- const candidateAccountId = await resolveEffectiveAccountId({
127
- userName,
128
- config: candidate.config,
129
- });
130
- if (candidateAccountId === accountId) {
131
- throw new Error(
132
- `${strategyName} config "${candidate.configId}" already uses account "${accountId}". One strategy can run only once per account.`,
133
- );
134
- }
135
- }
136
- };
137
-
138
- const toResponseConfig = async (
139
- userName: string,
140
- row: StoredRuntimeConfig,
141
- ) => ({
142
- strategyName: row.strategyName,
143
- configId: row.configId,
144
- interval: normalizeInterval(row.config.INTERVAL),
145
- universe: normalizeUniverse(row.config.UNIVERSE),
146
- accountId: normalizeAccountId(row.config.ACCOUNT_ID) ?? null,
147
- effectiveAccountId: await resolveEffectiveAccountId({
148
- userName,
149
- config: row.config,
150
- }).catch(() => null),
151
- enabled: row.config.ENABLE !== false,
152
- config: row.config,
153
- });
154
13
 
155
14
  export const GET = async () => {
156
15
  const userName = await getCurrentUserName();
157
16
  if (!userName)
158
17
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
159
- const [configs, strategyNames, accounts] = await Promise.all([
160
- loadConfigs(userName),
161
- getAvailableStrategyNames(projectRoot),
162
- listTradingAccounts(userName),
163
- ]);
164
- return NextResponse.json({
165
- configs: await Promise.all(
166
- configs.map((row) => toResponseConfig(userName, row)),
167
- ),
168
- strategyNames,
169
- accounts: accounts.map(
170
- ({ apiKey: _apiKey, apiSecret: _apiSecret, ...account }) => account,
171
- ),
172
- intervals: [...INTERVALS],
173
- });
18
+ return NextResponse.json(
19
+ await getRuntimeStrategyConfigOptions({ userName, projectRoot }),
20
+ );
174
21
  };
175
22
 
176
23
  const save = async (request: NextRequest, editing: boolean) => {
@@ -178,61 +25,24 @@ const save = async (request: NextRequest, editing: boolean) => {
178
25
  if (!userName)
179
26
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
180
27
  try {
181
- const body = (await request.json()) as Record<string, unknown>;
182
- const strategyName = String(body.strategyName ?? '').trim();
183
- const configId = normalizeConfigId(body.configId);
184
- const availableStrategies = await getAvailableStrategyNames(projectRoot);
185
- if (!strategyName || !availableStrategies.includes(strategyName)) {
186
- throw new Error(`Unknown strategy: ${strategyName || '(empty)'}`);
187
- }
188
- const existingConfigs = await loadConfigs(userName);
189
- const existing = existingConfigs.find(
190
- (row) => row.strategyName === strategyName && row.configId === configId,
191
- );
192
- if (editing && !existing)
193
- throw new Error('Runtime strategy config not found');
194
- if (!editing && existing)
195
- throw new Error('Runtime strategy config already exists');
196
- const parameters = body.parameters;
197
- if (
198
- !parameters ||
199
- typeof parameters !== 'object' ||
200
- Array.isArray(parameters)
201
- ) {
202
- throw new Error('Strategy parameters must be a JSON object');
203
- }
204
- const interval = normalizeInterval(body.interval);
205
- const universe = normalizeUniverse(body.universe);
206
- const accountId = normalizeAccountId(body.accountId);
207
- const config: StrategyConfig = {
208
- ...(parameters as StrategyConfig),
209
- ENABLE: body.enabled !== false,
210
- INTERVAL: interval,
211
- UNIVERSE: universe,
212
- };
213
- if (accountId) config.ACCOUNT_ID = accountId;
214
- else delete config.ACCOUNT_ID;
215
-
216
- await assertNoEnabledAccountConflict({
217
- userName,
218
- strategyName,
219
- configId,
220
- config,
221
- existingConfigs,
222
- });
223
- const key = redisKeys.strategyConfig(userName, strategyName, configId);
224
- await setData(key, config, { expire: 0 });
225
- return NextResponse.json({
226
- config: await toResponseConfig(userName, {
227
- key,
228
- strategyName,
229
- configId,
230
- config,
28
+ return NextResponse.json(
29
+ await saveRuntimeStrategyConfigForUser({
30
+ userName,
31
+ projectRoot,
32
+ input: (await request.json()) as Record<string, unknown>,
33
+ editing,
231
34
  }),
232
- });
35
+ );
233
36
  } catch (error) {
234
37
  const message = error instanceof Error ? error.message : String(error);
235
- const status = message.includes('already') ? 409 : 400;
38
+ const status =
39
+ error instanceof RuntimeStrategyConfigServiceError &&
40
+ error.code === 'conflict'
41
+ ? 409
42
+ : error instanceof RuntimeStrategyConfigServiceError &&
43
+ error.code === 'not_found'
44
+ ? 404
45
+ : 400;
236
46
  return NextResponse.json({ error: message }, { status });
237
47
  }
238
48
  };
@@ -2,8 +2,8 @@ import { NextResponse } from 'next/server';
2
2
  import {
3
3
  deleteTradingAccount,
4
4
  getTradingAccount,
5
- listRuntimeDeployments,
6
5
  } from '@tradejs/infra/tradingAccounts';
6
+ import { listRuntimeDeployments } from '@tradejs/infra/runtimeDeployments';
7
7
  import { getCurrentUserName } from '#app/lib/currentUser';
8
8
 
9
9
  export const DELETE = async (
@@ -7,7 +7,7 @@ import { FiFolder } from 'react-icons/fi';
7
7
  import { Box, Checkbox, Code } from '@chakra-ui/react';
8
8
  import { TestCard } from '#components/Backtest/TestCard';
9
9
  import { EmptyState } from '#ui';
10
- import { Items } from '@tradejs/types';
10
+ import type { Items } from '#app/types/ui';
11
11
 
12
12
  interface ListProps {
13
13
  tests: Items;
@@ -1,4 +1,4 @@
1
- import { Figure } from '@tradejs/types';
1
+ import type { Figure } from '#app/types/ui';
2
2
 
3
3
  export const circle = ({ ctx, x, y, width, height, color }: Figure) => {
4
4
  const radius = Math.min(width, height) / 2;
@@ -1,4 +1,4 @@
1
- import { Figure } from '@tradejs/types';
1
+ import type { Figure } from '#app/types/ui';
2
2
 
3
3
  export const diamond = ({
4
4
  ctx,
@@ -1,4 +1,4 @@
1
- import { Figure } from '@tradejs/types';
1
+ import type { Figure } from '#app/types/ui';
2
2
 
3
3
  export const label = ({ ctx, x, y, text = '', color }: Figure) => {
4
4
  ctx.save();
@@ -1,4 +1,4 @@
1
- import { Figure } from '@tradejs/types';
1
+ import type { Figure } from '#app/types/ui';
2
2
 
3
3
  export const rectangle = ({
4
4
  ctx,
@@ -1,4 +1,4 @@
1
- import { Figure } from '@tradejs/types';
1
+ import type { Figure } from '#app/types/ui';
2
2
 
3
3
  export const star = ({
4
4
  ctx,
@@ -10,7 +10,8 @@ import {
10
10
  DataLoaderSubscribeBarParams,
11
11
  } from 'klinecharts';
12
12
  import { OverlaySpinner } from '#ui';
13
- import { Indicator, UIFilters } from '@tradejs/types';
13
+ import { Indicator } from '@tradejs/types';
14
+ import type { UIFilters } from '#app/types/ui';
14
15
  import {
15
16
  useBbIndicator,
16
17
  useAtrIndicator,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { PropsWithChildren } from 'react';
4
4
  import { FiltersContext } from '../context';
5
- import { UIFilters, Items, OnChangeFilters } from '@tradejs/types';
5
+ import type { Items, OnChangeFilters, UIFilters } from '#app/types/ui';
6
6
 
7
7
  interface RootProps {
8
8
  tickers: Items;
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from 'react';
2
- import { UIFilters, Items, OnChangeFilters } from '@tradejs/types';
2
+ import type { Items, OnChangeFilters, UIFilters } from '#app/types/ui';
3
3
 
4
4
  interface FiltersContextProps {
5
5
  filters: UIFilters;