@tradejs/app 2.0.18 → 2.0.19
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/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 +81 -858
- 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 +684 -0
- package/src/app/lib/runtimeStrategies.ts +24 -454
- 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/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,366 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { getRuntimeStorageDayKeys } from '@tradejs/core/time';
|
|
3
2
|
import { logger } from '@tradejs/infra/logger';
|
|
4
|
-
import {
|
|
5
|
-
listRuntimeDeployments,
|
|
6
|
-
listTradingAccounts,
|
|
7
|
-
resolveTradingAccount,
|
|
8
|
-
} from '@tradejs/infra/tradingAccounts';
|
|
9
|
-
import { strategyEntries } from '@tradejs/strategies';
|
|
10
|
-
import {
|
|
11
|
-
getData,
|
|
12
|
-
getHashJsonValues,
|
|
13
|
-
getKeys,
|
|
14
|
-
redisKeys,
|
|
15
|
-
} from '@tradejs/infra/redis';
|
|
16
|
-
import type {
|
|
17
|
-
Connector,
|
|
18
|
-
RuntimeTradeRecord,
|
|
19
|
-
Interval,
|
|
20
|
-
StrategyConfig,
|
|
21
|
-
} from '@tradejs/types';
|
|
22
|
-
import { getAvailableStrategyNames } from '@tradejs/node/strategies';
|
|
23
|
-
import {
|
|
24
|
-
DEFAULT_CONNECTOR_PROVIDER,
|
|
25
|
-
resolveConnectorAccountId,
|
|
26
|
-
resolveConnectorCreatorByProvider,
|
|
27
|
-
} from '#app/lib/connectorCreator';
|
|
28
3
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
29
|
-
import {
|
|
30
|
-
assignLegacyRuntimeTradeAccountScopes,
|
|
31
|
-
buildRuntimeStrategyAnalytics,
|
|
32
|
-
buildRuntimeStrategyAiGateChanges,
|
|
33
|
-
buildRuntimeStrategyMaxLossValueTimeline,
|
|
34
|
-
buildRuntimeStrategyIdentityKey,
|
|
35
|
-
buildExchangeFallbackRuntimeTrades,
|
|
36
|
-
getRuntimeStrategyAiGateObservedFrom,
|
|
37
|
-
isRuntimeTradeRecord,
|
|
38
|
-
isRuntimeStrategyLineageScope,
|
|
39
|
-
resolveStrategyConfigIdentityByKey,
|
|
40
|
-
RuntimeStrategyLineageScope,
|
|
41
|
-
RuntimeStrategiesResponse,
|
|
42
|
-
selectTradesForWindow,
|
|
43
|
-
toRuntimeTradeView,
|
|
44
|
-
} from '#app/lib/runtimeStrategies';
|
|
45
|
-
import {
|
|
46
|
-
isRuntimeTradeInConnectorScope,
|
|
47
|
-
syncRuntimeTrades,
|
|
48
|
-
} from '#app/lib/runtimeTradeSync';
|
|
4
|
+
import { loadRuntimeDashboard } from '#app/lib/runtimeDashboard';
|
|
49
5
|
|
|
50
6
|
export const dynamic = 'force-dynamic';
|
|
51
7
|
|
|
52
|
-
const projectRoot =
|
|
53
|
-
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
54
|
-
const DEFAULT_PROVIDER = DEFAULT_CONNECTOR_PROVIDER;
|
|
55
|
-
const DEFAULT_HOURS = 168;
|
|
56
|
-
const MIN_HOURS = 6;
|
|
57
|
-
const MAX_HOURS = 24 * 90;
|
|
58
|
-
const BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1000 - 1_000;
|
|
59
|
-
const EXCHANGE_REQUEST_TIMEOUT_MS = 15_000;
|
|
60
|
-
const RUNTIME_LINEAGE_HISTORY_MS = 30 * 24 * 60 * 60 * 1000;
|
|
61
|
-
|
|
62
|
-
const coerceHours = (value: string | null) => {
|
|
63
|
-
const parsed = Number(value ?? Number.NaN);
|
|
64
|
-
if (!Number.isFinite(parsed)) {
|
|
65
|
-
return DEFAULT_HOURS;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.trunc(parsed)));
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const isRuntimeStrategyConfigEnabled = (config: StrategyConfig | null) => {
|
|
72
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return (config as Record<string, unknown>).ENABLE !== false;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const loadRuntimeStrategyConfigs = async (userName: string) => {
|
|
80
|
-
const keys = await getKeys(`${redisKeys.strategies(userName)}:`);
|
|
81
|
-
const entries = await Promise.all(
|
|
82
|
-
keys.map(async (key) => {
|
|
83
|
-
const identity = resolveStrategyConfigIdentityByKey(userName, key);
|
|
84
|
-
if (!identity) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const config = (await getData(key, null)) as StrategyConfig | null;
|
|
89
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { ...identity, key, config };
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
96
|
-
return entries.filter(
|
|
97
|
-
(entry): entry is NonNullable<typeof entry> => entry != null,
|
|
98
|
-
);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const loadConfiguredStrategyNames = async () => {
|
|
102
|
-
try {
|
|
103
|
-
const names = await getAvailableStrategyNames(projectRoot);
|
|
104
|
-
const builtInNames = strategyEntries
|
|
105
|
-
.map((entry) => entry.manifest?.name)
|
|
106
|
-
.filter((value): value is string => Boolean(value));
|
|
107
|
-
|
|
108
|
-
return [...new Set([...names, ...builtInNames])].sort((left, right) =>
|
|
109
|
-
left.localeCompare(right),
|
|
110
|
-
);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
logger.warn(
|
|
113
|
-
'strategies runtime: failed to load configured strategies: %s',
|
|
114
|
-
(error as Error)?.message || String(error),
|
|
115
|
-
);
|
|
116
|
-
return strategyEntries
|
|
117
|
-
.map((entry) => entry.manifest?.name)
|
|
118
|
-
.filter((value): value is string => Boolean(value))
|
|
119
|
-
.sort((left, right) => left.localeCompare(right));
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const loadRuntimeTrades = async (
|
|
124
|
-
userName: string,
|
|
125
|
-
{
|
|
126
|
-
startTime,
|
|
127
|
-
endTime,
|
|
128
|
-
}: {
|
|
129
|
-
startTime: number;
|
|
130
|
-
endTime: number;
|
|
131
|
-
},
|
|
132
|
-
): Promise<RuntimeTradeRecord[]> => {
|
|
133
|
-
const filterByWindow = (trade: RuntimeTradeRecord) =>
|
|
134
|
-
trade.entryTimestamp >= startTime ||
|
|
135
|
-
(typeof trade.exitTimestamp === 'number' &&
|
|
136
|
-
trade.exitTimestamp >= startTime);
|
|
137
|
-
const dayKeys = getRuntimeStorageDayKeys(startTime, endTime);
|
|
138
|
-
const bucketTrades = (
|
|
139
|
-
await Promise.all(
|
|
140
|
-
dayKeys.map((dayKey) =>
|
|
141
|
-
getHashJsonValues<RuntimeTradeRecord>(
|
|
142
|
-
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
143
|
-
),
|
|
144
|
-
),
|
|
145
|
-
)
|
|
146
|
-
).flat();
|
|
147
|
-
const dedupedBucketTrades = new Map<string, RuntimeTradeRecord>();
|
|
148
|
-
|
|
149
|
-
for (const trade of bucketTrades) {
|
|
150
|
-
if (!isRuntimeTradeRecord(trade)) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
dedupedBucketTrades.set(trade.orderId, trade);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (dedupedBucketTrades.size > 0 || dayKeys.length === 0) {
|
|
157
|
-
return [...dedupedBucketTrades.values()]
|
|
158
|
-
.filter(filterByWindow)
|
|
159
|
-
.sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const keys = await getKeys(redisKeys.runtimeTrades(userName));
|
|
163
|
-
const trades = await Promise.all(keys.map((key) => getData(key, null)));
|
|
164
|
-
|
|
165
|
-
return trades
|
|
166
|
-
.filter(isRuntimeTradeRecord)
|
|
167
|
-
.filter(filterByWindow)
|
|
168
|
-
.sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const loadRuntimeLineageScopes = async (
|
|
172
|
-
userName: string,
|
|
173
|
-
endTime: number,
|
|
174
|
-
): Promise<RuntimeStrategyLineageScope[]> =>
|
|
175
|
-
(
|
|
176
|
-
await Promise.all(
|
|
177
|
-
getRuntimeStorageDayKeys(
|
|
178
|
-
Math.max(0, endTime - RUNTIME_LINEAGE_HISTORY_MS),
|
|
179
|
-
endTime,
|
|
180
|
-
).map((dayKey) =>
|
|
181
|
-
getHashJsonValues<RuntimeStrategyLineageScope>(
|
|
182
|
-
redisKeys.runtimeLineageScopeBucket(userName, dayKey),
|
|
183
|
-
),
|
|
184
|
-
),
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
.flat()
|
|
188
|
-
.filter(isRuntimeStrategyLineageScope);
|
|
189
|
-
|
|
190
|
-
const buildExchangeTimeRanges = (startTime: number, endTime: number) => {
|
|
191
|
-
const ranges: Array<{ startTime: number; endTime: number }> = [];
|
|
192
|
-
let cursor = startTime;
|
|
193
|
-
|
|
194
|
-
while (cursor < endTime) {
|
|
195
|
-
const rangeEnd = Math.min(endTime, cursor + BYBIT_MAX_TIME_RANGE_MS);
|
|
196
|
-
ranges.push({ startTime: cursor, endTime: rangeEnd });
|
|
197
|
-
cursor = rangeEnd + 1;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return ranges;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const loadExchangeRange = async <T>({
|
|
204
|
-
label,
|
|
205
|
-
startTime,
|
|
206
|
-
endTime,
|
|
207
|
-
load,
|
|
208
|
-
errors,
|
|
209
|
-
}: {
|
|
210
|
-
label: string;
|
|
211
|
-
startTime: number;
|
|
212
|
-
endTime: number;
|
|
213
|
-
load: () => Promise<T[]>;
|
|
214
|
-
errors?: string[];
|
|
215
|
-
}) => {
|
|
216
|
-
try {
|
|
217
|
-
return await Promise.race([
|
|
218
|
-
load(),
|
|
219
|
-
new Promise<T[]>((_, reject) => {
|
|
220
|
-
setTimeout(
|
|
221
|
-
() =>
|
|
222
|
-
reject(
|
|
223
|
-
new Error(
|
|
224
|
-
`${label} timed out for ${new Date(startTime).toISOString()} - ${new Date(endTime).toISOString()}`,
|
|
225
|
-
),
|
|
226
|
-
),
|
|
227
|
-
EXCHANGE_REQUEST_TIMEOUT_MS,
|
|
228
|
-
);
|
|
229
|
-
}),
|
|
230
|
-
]);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
const message = (error as Error)?.message || String(error);
|
|
233
|
-
errors?.push(`${label}: ${message}`);
|
|
234
|
-
logger.warn('strategies runtime: %s failed: %s', label, message);
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const loadActiveRuntimeOrderIds = async (userName: string) => {
|
|
240
|
-
const keys = await getKeys(redisKeys.runtimeActiveTrades(userName));
|
|
241
|
-
const refs = await Promise.all(keys.map((key) => getData(key, null)));
|
|
242
|
-
|
|
243
|
-
return new Set(
|
|
244
|
-
refs
|
|
245
|
-
.map((ref) =>
|
|
246
|
-
typeof ref?.orderId === 'string' && ref.orderId.trim()
|
|
247
|
-
? ref.orderId.trim()
|
|
248
|
-
: null,
|
|
249
|
-
)
|
|
250
|
-
.filter((value): value is string => Boolean(value)),
|
|
251
|
-
);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const loadClosedPnlRows = async ({
|
|
255
|
-
connector,
|
|
256
|
-
startTime,
|
|
257
|
-
endTime,
|
|
258
|
-
errors,
|
|
259
|
-
}: {
|
|
260
|
-
connector: Connector;
|
|
261
|
-
startTime: number;
|
|
262
|
-
endTime: number;
|
|
263
|
-
errors?: string[];
|
|
264
|
-
}) => {
|
|
265
|
-
if (typeof connector.getClosedPnl !== 'function') {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const rows = (
|
|
271
|
-
await Promise.all(
|
|
272
|
-
buildExchangeTimeRanges(startTime, endTime).map((range) =>
|
|
273
|
-
loadExchangeRange({
|
|
274
|
-
label: 'getClosedPnl',
|
|
275
|
-
...range,
|
|
276
|
-
errors,
|
|
277
|
-
load: () =>
|
|
278
|
-
connector.getClosedPnl?.({
|
|
279
|
-
...range,
|
|
280
|
-
limit: 100,
|
|
281
|
-
}) ?? Promise.resolve([]),
|
|
282
|
-
}),
|
|
283
|
-
),
|
|
284
|
-
)
|
|
285
|
-
).flatMap((items) => items ?? []);
|
|
286
|
-
|
|
287
|
-
return rows.sort((left, right) => left.closedAt - right.closedAt);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
const message = (error as Error)?.message || String(error);
|
|
290
|
-
errors?.push(`getClosedPnl: ${message}`);
|
|
291
|
-
logger.warn('strategies runtime: getClosedPnl failed: %s', message);
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const loadExchangeEntryRows = async ({
|
|
297
|
-
connector,
|
|
298
|
-
startTime,
|
|
299
|
-
endTime,
|
|
300
|
-
errors,
|
|
301
|
-
}: {
|
|
302
|
-
connector: Connector;
|
|
303
|
-
startTime: number;
|
|
304
|
-
endTime: number;
|
|
305
|
-
errors?: string[];
|
|
306
|
-
}) => {
|
|
307
|
-
if (typeof connector.getEntryExecutions !== 'function') {
|
|
308
|
-
return [];
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const rows = (
|
|
313
|
-
await Promise.all(
|
|
314
|
-
buildExchangeTimeRanges(startTime, endTime).map((range) =>
|
|
315
|
-
loadExchangeRange({
|
|
316
|
-
label: 'getEntryExecutions',
|
|
317
|
-
...range,
|
|
318
|
-
errors,
|
|
319
|
-
load: () =>
|
|
320
|
-
connector.getEntryExecutions?.({
|
|
321
|
-
...range,
|
|
322
|
-
limit: 100,
|
|
323
|
-
}) ?? Promise.resolve([]),
|
|
324
|
-
}),
|
|
325
|
-
),
|
|
326
|
-
)
|
|
327
|
-
).flatMap((items) => items ?? []);
|
|
328
|
-
|
|
329
|
-
return rows.sort(
|
|
330
|
-
(left, right) => left.entryTimestamp - right.entryTimestamp,
|
|
331
|
-
);
|
|
332
|
-
} catch (error) {
|
|
333
|
-
const message = (error as Error)?.message || String(error);
|
|
334
|
-
errors?.push(`getEntryExecutions: ${message}`);
|
|
335
|
-
logger.warn('strategies runtime: getEntryExecutions failed: %s', message);
|
|
336
|
-
return [];
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const loadOpenPositions = async (
|
|
341
|
-
connector: Connector,
|
|
342
|
-
errors?: string[],
|
|
343
|
-
): Promise<{
|
|
344
|
-
positions: Awaited<ReturnType<NonNullable<Connector['getOpenPositionPnl']>>>;
|
|
345
|
-
reliable: boolean;
|
|
346
|
-
}> => {
|
|
347
|
-
if (typeof connector.getOpenPositionPnl !== 'function') {
|
|
348
|
-
return { positions: [], reliable: false };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
return {
|
|
353
|
-
positions: await connector.getOpenPositionPnl(),
|
|
354
|
-
reliable: true,
|
|
355
|
-
};
|
|
356
|
-
} catch (error) {
|
|
357
|
-
const message = (error as Error)?.message || String(error);
|
|
358
|
-
errors?.push(`getOpenPositionPnl: ${message}`);
|
|
359
|
-
logger.warn('strategies runtime: getOpenPositionPnl failed: %s', message);
|
|
360
|
-
return { positions: [], reliable: false };
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
8
|
export const GET = async (request: NextRequest) => {
|
|
365
9
|
try {
|
|
366
10
|
const userName = await getCurrentUserName();
|
|
@@ -368,325 +12,11 @@ export const GET = async (request: NextRequest) => {
|
|
|
368
12
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
369
13
|
}
|
|
370
14
|
|
|
371
|
-
const
|
|
372
|
-
request.nextUrl.searchParams.get('provider')?.trim() || DEFAULT_PROVIDER;
|
|
373
|
-
const hours = coerceHours(request.nextUrl.searchParams.get('hours'));
|
|
374
|
-
const endTime = Date.now();
|
|
375
|
-
const startTime = endTime - hours * 60 * 60 * 1000;
|
|
376
|
-
const exchangeErrors: string[] = [];
|
|
377
|
-
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
378
|
-
provider,
|
|
379
|
-
projectRoot,
|
|
380
|
-
DEFAULT_PROVIDER,
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
if (!connectorCreator) {
|
|
384
|
-
throw new Error(`No connector available for provider "${provider}"`);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const connectorAccountId = await resolveConnectorAccountId({
|
|
15
|
+
const response = await loadRuntimeDashboard({
|
|
388
16
|
userName,
|
|
389
|
-
provider,
|
|
390
|
-
|
|
17
|
+
provider: request.nextUrl.searchParams.get('provider'),
|
|
18
|
+
hours: request.nextUrl.searchParams.get('hours'),
|
|
391
19
|
});
|
|
392
|
-
const connector = await connectorCreator({
|
|
393
|
-
userName,
|
|
394
|
-
accountId: connectorAccountId,
|
|
395
|
-
universe: 'crypto',
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const [
|
|
399
|
-
runtimeStrategyConfigs,
|
|
400
|
-
configuredStrategyNames,
|
|
401
|
-
runtimeTrades,
|
|
402
|
-
activeOrderIds,
|
|
403
|
-
closedPnlRows,
|
|
404
|
-
entryRows,
|
|
405
|
-
openPositionsSnapshot,
|
|
406
|
-
runtimeDeployments,
|
|
407
|
-
tradingAccounts,
|
|
408
|
-
runtimeLineageScopes,
|
|
409
|
-
] = await Promise.all([
|
|
410
|
-
loadRuntimeStrategyConfigs(userName),
|
|
411
|
-
loadConfiguredStrategyNames(),
|
|
412
|
-
loadRuntimeTrades(userName, { startTime, endTime }),
|
|
413
|
-
loadActiveRuntimeOrderIds(userName),
|
|
414
|
-
loadClosedPnlRows({
|
|
415
|
-
connector,
|
|
416
|
-
startTime,
|
|
417
|
-
endTime,
|
|
418
|
-
errors: exchangeErrors,
|
|
419
|
-
}),
|
|
420
|
-
loadExchangeEntryRows({
|
|
421
|
-
connector,
|
|
422
|
-
startTime,
|
|
423
|
-
endTime,
|
|
424
|
-
errors: exchangeErrors,
|
|
425
|
-
}),
|
|
426
|
-
loadOpenPositions(connector, exchangeErrors),
|
|
427
|
-
listRuntimeDeployments(userName),
|
|
428
|
-
listTradingAccounts(userName),
|
|
429
|
-
loadRuntimeLineageScopes(userName, endTime),
|
|
430
|
-
]);
|
|
431
|
-
const relevantTrades = selectTradesForWindow(
|
|
432
|
-
runtimeTrades,
|
|
433
|
-
startTime,
|
|
434
|
-
activeOrderIds,
|
|
435
|
-
);
|
|
436
|
-
const syncableTrades = relevantTrades.filter((trade) =>
|
|
437
|
-
isRuntimeTradeInConnectorScope(trade, connector),
|
|
438
|
-
);
|
|
439
|
-
const unsyncedTrades = relevantTrades.filter(
|
|
440
|
-
(trade) => !isRuntimeTradeInConnectorScope(trade, connector),
|
|
441
|
-
);
|
|
442
|
-
const syncedConnectorTrades = await syncRuntimeTrades({
|
|
443
|
-
userName,
|
|
444
|
-
connector,
|
|
445
|
-
trades: syncableTrades,
|
|
446
|
-
endTime,
|
|
447
|
-
openPositions: openPositionsSnapshot.positions,
|
|
448
|
-
openPositionsReliable: openPositionsSnapshot.reliable,
|
|
449
|
-
closedPnlRows,
|
|
450
|
-
});
|
|
451
|
-
const syncedTrades = [...unsyncedTrades, ...syncedConnectorTrades];
|
|
452
|
-
const fallbackStrategyNames = [
|
|
453
|
-
...new Set([
|
|
454
|
-
...runtimeStrategyConfigs.map(({ strategyName }) => strategyName),
|
|
455
|
-
...configuredStrategyNames,
|
|
456
|
-
]),
|
|
457
|
-
];
|
|
458
|
-
const fallbackTrades = buildExchangeFallbackRuntimeTrades({
|
|
459
|
-
entryRows,
|
|
460
|
-
closedPnlRows,
|
|
461
|
-
openPositions: openPositionsSnapshot.positions,
|
|
462
|
-
strategyNames: fallbackStrategyNames,
|
|
463
|
-
existingTrades: syncedTrades,
|
|
464
|
-
endTime,
|
|
465
|
-
});
|
|
466
|
-
const allTrades = [...syncedTrades, ...fallbackTrades].filter(
|
|
467
|
-
isRuntimeTradeRecord,
|
|
468
|
-
);
|
|
469
|
-
const connectedSet = new Set(
|
|
470
|
-
runtimeStrategyConfigs.map(
|
|
471
|
-
({ strategyName, configId }) => `${strategyName}:${configId}`,
|
|
472
|
-
),
|
|
473
|
-
);
|
|
474
|
-
const accountsById = new Map(
|
|
475
|
-
tradingAccounts.map((account) => [account.id, account]),
|
|
476
|
-
);
|
|
477
|
-
const runtimeIdentityKey = (trade: RuntimeTradeRecord) =>
|
|
478
|
-
buildRuntimeStrategyIdentityKey({
|
|
479
|
-
strategyName: trade.strategy,
|
|
480
|
-
configId: trade.runtimeConfigId,
|
|
481
|
-
universe: trade.universe,
|
|
482
|
-
accountId: trade.accountId,
|
|
483
|
-
deploymentId: trade.deploymentId,
|
|
484
|
-
policyProfileId: trade.policyProfileId,
|
|
485
|
-
});
|
|
486
|
-
const identityByKey = new Map<
|
|
487
|
-
string,
|
|
488
|
-
{
|
|
489
|
-
strategyName: string;
|
|
490
|
-
configId: string;
|
|
491
|
-
interval: Interval;
|
|
492
|
-
universe: 'crypto' | 'tradfi';
|
|
493
|
-
accountId?: string;
|
|
494
|
-
accountLabel?: string;
|
|
495
|
-
deploymentId?: string;
|
|
496
|
-
policyProfileId?: string;
|
|
497
|
-
enabled?: boolean;
|
|
498
|
-
config?: Record<string, unknown>;
|
|
499
|
-
connected?: boolean;
|
|
500
|
-
}
|
|
501
|
-
>();
|
|
502
|
-
const runtimeConfigAccountScopes = new Array<{
|
|
503
|
-
strategyName: string;
|
|
504
|
-
configId: string;
|
|
505
|
-
universe: 'crypto' | 'tradfi';
|
|
506
|
-
accountId?: string;
|
|
507
|
-
}>();
|
|
508
|
-
for (const deployment of runtimeDeployments) {
|
|
509
|
-
for (const deploymentStrategy of deployment.strategies) {
|
|
510
|
-
const runtimeKey = buildRuntimeStrategyIdentityKey({
|
|
511
|
-
strategyName: deploymentStrategy.strategyName,
|
|
512
|
-
configId: `deployment-${deployment.id}`,
|
|
513
|
-
universe: deployment.universe,
|
|
514
|
-
accountId: deployment.accountId,
|
|
515
|
-
deploymentId: deployment.id,
|
|
516
|
-
policyProfileId: deploymentStrategy.policyProfileId,
|
|
517
|
-
});
|
|
518
|
-
identityByKey.set(runtimeKey, {
|
|
519
|
-
strategyName: deploymentStrategy.strategyName,
|
|
520
|
-
configId: `deployment-${deployment.id}`,
|
|
521
|
-
interval: String(deployment.interval) as Interval,
|
|
522
|
-
universe: deployment.universe,
|
|
523
|
-
accountId: deployment.accountId,
|
|
524
|
-
accountLabel: accountsById.get(deployment.accountId)?.label,
|
|
525
|
-
deploymentId: deployment.id,
|
|
526
|
-
policyProfileId: deploymentStrategy.policyProfileId,
|
|
527
|
-
enabled: deployment.enabled && deploymentStrategy.enabled !== false,
|
|
528
|
-
config: deploymentStrategy.config,
|
|
529
|
-
connected: false,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
for (const runtimeConfig of runtimeStrategyConfigs) {
|
|
534
|
-
const universe =
|
|
535
|
-
runtimeConfig.config.UNIVERSE === 'tradfi' ? 'tradfi' : 'crypto';
|
|
536
|
-
const configuredAccountId =
|
|
537
|
-
typeof runtimeConfig.config.ACCOUNT_ID === 'string' &&
|
|
538
|
-
runtimeConfig.config.ACCOUNT_ID.trim()
|
|
539
|
-
? runtimeConfig.config.ACCOUNT_ID.trim()
|
|
540
|
-
: undefined;
|
|
541
|
-
const resolvedAccount = await resolveTradingAccount({
|
|
542
|
-
userName,
|
|
543
|
-
accountId: configuredAccountId,
|
|
544
|
-
provider,
|
|
545
|
-
universe,
|
|
546
|
-
}).catch(() => null);
|
|
547
|
-
const accountId = resolvedAccount?.id ?? configuredAccountId;
|
|
548
|
-
runtimeConfigAccountScopes.push({
|
|
549
|
-
strategyName: runtimeConfig.strategyName,
|
|
550
|
-
configId: runtimeConfig.configId,
|
|
551
|
-
universe,
|
|
552
|
-
accountId,
|
|
553
|
-
});
|
|
554
|
-
const runtimeKey = buildRuntimeStrategyIdentityKey({
|
|
555
|
-
strategyName: runtimeConfig.strategyName,
|
|
556
|
-
configId: runtimeConfig.configId,
|
|
557
|
-
universe,
|
|
558
|
-
accountId,
|
|
559
|
-
});
|
|
560
|
-
identityByKey.set(runtimeKey, {
|
|
561
|
-
strategyName: runtimeConfig.strategyName,
|
|
562
|
-
configId: runtimeConfig.configId,
|
|
563
|
-
interval: String(runtimeConfig.config.INTERVAL ?? '15') as Interval,
|
|
564
|
-
universe,
|
|
565
|
-
accountId,
|
|
566
|
-
accountLabel: accountId
|
|
567
|
-
? accountsById.get(accountId)?.label
|
|
568
|
-
: undefined,
|
|
569
|
-
enabled: isRuntimeStrategyConfigEnabled(runtimeConfig.config),
|
|
570
|
-
config: runtimeConfig.config,
|
|
571
|
-
connected: true,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
const accountScopedTrades = assignLegacyRuntimeTradeAccountScopes(
|
|
575
|
-
allTrades,
|
|
576
|
-
runtimeConfigAccountScopes,
|
|
577
|
-
);
|
|
578
|
-
for (const trade of accountScopedTrades) {
|
|
579
|
-
const key = runtimeIdentityKey(trade);
|
|
580
|
-
identityByKey.set(key, {
|
|
581
|
-
...identityByKey.get(key),
|
|
582
|
-
strategyName: trade.strategy,
|
|
583
|
-
configId: trade.runtimeConfigId ?? 'config',
|
|
584
|
-
interval: String(trade.interval ?? '15') as Interval,
|
|
585
|
-
universe: trade.universe ?? 'crypto',
|
|
586
|
-
accountId: trade.accountId,
|
|
587
|
-
accountLabel: trade.accountId
|
|
588
|
-
? accountsById.get(trade.accountId)?.label
|
|
589
|
-
: undefined,
|
|
590
|
-
deploymentId: trade.deploymentId,
|
|
591
|
-
policyProfileId: trade.policyProfileId,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const strategies = await Promise.all(
|
|
596
|
-
[...identityByKey.entries()].map(async ([runtimeKey, identity]) => {
|
|
597
|
-
const { strategyName } = identity;
|
|
598
|
-
const strategyTrades = accountScopedTrades
|
|
599
|
-
.filter((trade) => runtimeIdentityKey(trade) === runtimeKey)
|
|
600
|
-
.sort((left, right) => right.entryTimestamp - left.entryTimestamp);
|
|
601
|
-
const orders = strategyTrades
|
|
602
|
-
.sort((left, right) => {
|
|
603
|
-
const leftDate = left.exitTimestamp ?? left.entryTimestamp;
|
|
604
|
-
const rightDate = right.exitTimestamp ?? right.entryTimestamp;
|
|
605
|
-
|
|
606
|
-
return rightDate - leftDate;
|
|
607
|
-
})
|
|
608
|
-
.map((trade) => toRuntimeTradeView(trade, endTime));
|
|
609
|
-
const analytics = buildRuntimeStrategyAnalytics({
|
|
610
|
-
trades: strategyTrades,
|
|
611
|
-
startTime,
|
|
612
|
-
endTime,
|
|
613
|
-
});
|
|
614
|
-
const maxLossValueTimeline = buildRuntimeStrategyMaxLossValueTimeline({
|
|
615
|
-
scopes: runtimeLineageScopes,
|
|
616
|
-
strategyName,
|
|
617
|
-
configId: identity.configId,
|
|
618
|
-
startTime,
|
|
619
|
-
endTime,
|
|
620
|
-
});
|
|
621
|
-
const effectiveStrategyConfig = identity.config ?? null;
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
runtimeKey,
|
|
625
|
-
strategyName,
|
|
626
|
-
configId: identity.configId,
|
|
627
|
-
interval: identity.interval,
|
|
628
|
-
universe: identity.universe,
|
|
629
|
-
accountId: identity.accountId,
|
|
630
|
-
accountLabel: identity.accountLabel,
|
|
631
|
-
deploymentId: identity.deploymentId,
|
|
632
|
-
policyProfileId: identity.policyProfileId,
|
|
633
|
-
connected:
|
|
634
|
-
identity.connected ??
|
|
635
|
-
connectedSet.has(`${strategyName}:${identity.configId}`),
|
|
636
|
-
enabled:
|
|
637
|
-
identity.enabled ??
|
|
638
|
-
isRuntimeStrategyConfigEnabled(effectiveStrategyConfig),
|
|
639
|
-
config: effectiveStrategyConfig,
|
|
640
|
-
symbols: [...new Set(strategyTrades.map((trade) => trade.symbol))],
|
|
641
|
-
stat: analytics.stat,
|
|
642
|
-
summary: analytics.summary,
|
|
643
|
-
orderLog: analytics.orderLog,
|
|
644
|
-
aiGateObservedFrom: getRuntimeStrategyAiGateObservedFrom({
|
|
645
|
-
scopes: runtimeLineageScopes,
|
|
646
|
-
strategyName,
|
|
647
|
-
configId: identity.configId,
|
|
648
|
-
endTime,
|
|
649
|
-
}),
|
|
650
|
-
aiGateChanges: buildRuntimeStrategyAiGateChanges({
|
|
651
|
-
scopes: runtimeLineageScopes,
|
|
652
|
-
strategyName,
|
|
653
|
-
configId: identity.configId,
|
|
654
|
-
startTime,
|
|
655
|
-
endTime,
|
|
656
|
-
}),
|
|
657
|
-
maxLossValueTimeline,
|
|
658
|
-
recentTrades: strategyTrades
|
|
659
|
-
.slice(0, 8)
|
|
660
|
-
.map((trade) => toRuntimeTradeView(trade, endTime)),
|
|
661
|
-
orders,
|
|
662
|
-
};
|
|
663
|
-
}),
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
strategies.sort((left, right) => {
|
|
667
|
-
if (left.stat.netProfit !== right.stat.netProfit) {
|
|
668
|
-
return right.stat.netProfit - left.stat.netProfit;
|
|
669
|
-
}
|
|
670
|
-
if (left.summary.totalPnl !== right.summary.totalPnl) {
|
|
671
|
-
return right.summary.totalPnl - left.summary.totalPnl;
|
|
672
|
-
}
|
|
673
|
-
if (left.connected !== right.connected) {
|
|
674
|
-
return left.connected ? -1 : 1;
|
|
675
|
-
}
|
|
676
|
-
return left.strategyName.localeCompare(right.strategyName);
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
const response: RuntimeStrategiesResponse = {
|
|
680
|
-
provider,
|
|
681
|
-
hours,
|
|
682
|
-
generatedAt: endTime,
|
|
683
|
-
dataSources: {
|
|
684
|
-
localTrades: syncedTrades.length,
|
|
685
|
-
exchangeFallbackTrades: fallbackTrades.length,
|
|
686
|
-
exchangeErrors: [...new Set(exchangeErrors)].sort(),
|
|
687
|
-
},
|
|
688
|
-
strategies,
|
|
689
|
-
};
|
|
690
20
|
|
|
691
21
|
return NextResponse.json(response);
|
|
692
22
|
} catch (error) {
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
|
|
|
2
2
|
import {
|
|
3
3
|
deleteRuntimeDeployment,
|
|
4
4
|
getRuntimeDeployment,
|
|
5
|
-
} from '@tradejs/infra/
|
|
5
|
+
} from '@tradejs/infra/runtimeDeployments';
|
|
6
6
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
7
7
|
|
|
8
8
|
export const DELETE = async (
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import {
|
|
3
|
-
getTradingAccount,
|
|
4
3
|
getRuntimeDeploymentHeartbeat,
|
|
5
4
|
listRuntimeDeployments,
|
|
6
5
|
saveRuntimeDeployment,
|
|
7
|
-
} from '@tradejs/infra/
|
|
6
|
+
} from '@tradejs/infra/runtimeDeployments';
|
|
7
|
+
import { getTradingAccount } from '@tradejs/infra/tradingAccounts';
|
|
8
8
|
import { isMarketUniverse, type RuntimeDeployment } from '@tradejs/types';
|
|
9
9
|
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
10
10
|
|