@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.
- package/package.json +14 -8
- package/src/app/actions/backtest.ts +2 -1
- package/src/app/actions/scanner.ts +2 -1
- package/src/app/api/backtest/files/route.ts +2 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +1 -1
- package/src/app/api/derivatives/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/derivatives/summary/route.ts +1 -1
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +2 -1
- package/src/app/api/spread/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/spread/summary/route.ts +1 -1
- package/src/app/api/strategies/runtime/route.ts +4 -674
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
- package/src/app/api/user/runtime-deployments/route.ts +2 -2
- package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
- package/src/app/components/Backtest/TestList/index.tsx +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
- package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
- package/src/app/components/Shared/Filters/context.ts +1 -1
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +82 -861
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +115 -184
- package/src/app/components/Strategies/StrategyEvidencePopover.tsx +259 -0
- package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
- package/src/app/components/UI/Segment/index.tsx +1 -1
- package/src/app/components/UI/Select/index.tsx +1 -1
- package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
- package/src/app/lib/backtestJobContracts.ts +67 -0
- package/src/app/lib/backtestJobProgress.ts +28 -0
- package/src/app/lib/backtestJobRequest.ts +107 -0
- package/src/app/lib/backtestJobs.ts +25 -257
- package/src/app/lib/runtimeDashboard.ts +700 -0
- package/src/app/lib/runtimeStrategies.ts +22 -457
- package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
- package/src/app/lib/runtimeStrategyLineage.ts +264 -0
- package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
- package/src/app/lib/runtimeTradeSync.ts +1 -1
- package/src/app/lib/strategyEvidenceTimeline.ts +298 -0
- package/src/app/lib/strategyPerformance.ts +387 -0
- package/src/app/routes/dashboard/Dashboard.tsx +2 -6
- package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
- package/src/app/routes/derivatives/page.tsx +70 -262
- package/src/app/store/filters.ts +2 -1
- package/src/app/store/indicators.ts +2 -1
- package/src/app/store/tests.ts +1 -1
- package/src/app/store/tickers.ts +2 -1
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 =
|
|
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 '
|
|
10
|
+
import type { Items } from '#app/types/ui';
|
|
11
11
|
|
|
12
12
|
interface ListProps {
|
|
13
13
|
tests: Items;
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
DataLoaderSubscribeBarParams,
|
|
11
11
|
} from 'klinecharts';
|
|
12
12
|
import { OverlaySpinner } from '#ui';
|
|
13
|
-
import { Indicator
|
|
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 {
|
|
5
|
+
import type { Items, OnChangeFilters, UIFilters } from '#app/types/ui';
|
|
6
6
|
|
|
7
7
|
interface RootProps {
|
|
8
8
|
tickers: Items;
|