@tradejs/node 3.0.0 → 3.1.0

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.
@@ -0,0 +1,955 @@
1
+ import {
2
+ buildExchangeFallbackRuntimeTrades,
3
+ takeClosedPnlMatch
4
+ } from "./chunk-FB5NUEOQ.mjs";
5
+ import "./chunk-LAJ7NA3Q.mjs";
6
+ import {
7
+ getConnectorCreatorByProvider
8
+ } from "./chunk-3TWULKHV.mjs";
9
+ import "./chunk-MKLTJQLY.mjs";
10
+ import {
11
+ getAvailableStrategyNames
12
+ } from "./chunk-KOQSLFT2.mjs";
13
+ import "./chunk-WS5DYEVZ.mjs";
14
+ import "./chunk-Y6FXYEAI.mjs";
15
+
16
+ // src/runtimeDashboard.ts
17
+ import { getRuntimeStorageDayKeys } from "@tradejs/core/time";
18
+ import { logger } from "@tradejs/infra/logger";
19
+ import { strategyLogicConfigFingerprint } from "@tradejs/infra/strategyReleaseEvidence";
20
+ import {
21
+ listTradingAccounts,
22
+ resolveTradingAccount
23
+ } from "@tradejs/infra/tradingAccounts";
24
+ import { listRuntimeDeployments } from "@tradejs/infra/runtimeDeployments";
25
+ import { loadRuntimeStrategyConfigs as loadStoredRuntimeStrategyConfigs } from "@tradejs/infra/runtimeStrategyConfigs";
26
+ import {
27
+ getData as getData2,
28
+ getHashJsonValues,
29
+ getKeys,
30
+ redisKeys as redisKeys2
31
+ } from "@tradejs/infra/redis";
32
+ import {
33
+ buildRuntimeStrategyAnalytics,
34
+ isRuntimeTradeRecord,
35
+ selectTradesForWindow,
36
+ toRuntimeTradeView,
37
+ assignLegacyRuntimeTradeAccountScopes,
38
+ buildRuntimeStrategyIdentityKey
39
+ } from "@tradejs/core/runtimeTrades";
40
+
41
+ // src/strategyEvidenceTimeline.ts
42
+ import fs from "fs/promises";
43
+ import path from "path";
44
+ import {
45
+ canonicalStrategyEvidenceJson,
46
+ safeStrategyEvidenceSegment,
47
+ verifyStrategyEvidenceMarkerEnvelope
48
+ } from "@tradejs/infra/strategyReleaseEvidence";
49
+ var DEFAULT_MARKER_DIRECTORY = "data/strategy-release/markers";
50
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
51
+ var isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
52
+ var strategyEvidenceTimelineSelectorKey = (selector) => [
53
+ selector.strategy,
54
+ selector.compositionId ?? "",
55
+ selector.gitSha ?? "",
56
+ selector.gateFingerprint ?? "",
57
+ selector.configFingerprint ?? "",
58
+ selector.contextFingerprint ?? "",
59
+ selector.requireCompleteLineage ? "exact" : "partial"
60
+ ].join(":");
61
+ var discoverJsonFiles = async (rootDir) => {
62
+ const files = [];
63
+ const visit = async (directory, depth) => {
64
+ if (depth > 12) return;
65
+ let entries;
66
+ try {
67
+ entries = await fs.readdir(directory, { withFileTypes: true });
68
+ } catch (error) {
69
+ if (error.code === "ENOENT") return;
70
+ throw error;
71
+ }
72
+ await Promise.all(
73
+ entries.map(async (entry) => {
74
+ if (entry.name.startsWith(".") || entry.name.includes(".tmp-")) return;
75
+ const entryPath = path.join(directory, entry.name);
76
+ if (entry.isDirectory()) {
77
+ await visit(entryPath, depth + 1);
78
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
79
+ files.push(entryPath);
80
+ }
81
+ })
82
+ );
83
+ };
84
+ await visit(rootDir, 0);
85
+ return files.sort();
86
+ };
87
+ var inferMatchingStrategies = ({
88
+ filePath,
89
+ rootDir,
90
+ parsed,
91
+ strategies
92
+ }) => {
93
+ const payload = asRecord(asRecord(parsed)?.payload);
94
+ const declaredStrategy = isNonEmptyString(payload?.strategy) ? payload.strategy : null;
95
+ const directoryStrategy = path.relative(rootDir, filePath).split(path.sep)[0];
96
+ return strategies.filter(
97
+ (strategy) => strategy === declaredStrategy || directoryStrategy === safeStrategyEvidenceSegment(strategy)
98
+ );
99
+ };
100
+ var missingTimeline = () => ({
101
+ status: "missing",
102
+ observedFrom: null,
103
+ markers: []
104
+ });
105
+ var invalidTimeline = () => ({
106
+ status: "invalid",
107
+ observedFrom: null,
108
+ markers: []
109
+ });
110
+ var loadStrategyEvidenceTimelines = async ({
111
+ projectRoot,
112
+ markerDir,
113
+ selectors: requestedSelectors,
114
+ startTime,
115
+ endTime
116
+ }) => {
117
+ const selectors = [...requestedSelectors].filter((selector) => selector.strategy.trim().length > 0).sort(
118
+ (left, right) => strategyEvidenceTimelineSelectorKey(left).localeCompare(
119
+ strategyEvidenceTimelineSelectorKey(right)
120
+ )
121
+ );
122
+ const strategies = [...new Set(selectors.map(({ strategy }) => strategy))];
123
+ const timelines = new Map(
124
+ selectors.map((selector) => [
125
+ strategyEvidenceTimelineSelectorKey(selector),
126
+ missingTimeline()
127
+ ])
128
+ );
129
+ if (!selectors.length) return timelines;
130
+ const configuredDir = markerDir?.trim() || DEFAULT_MARKER_DIRECTORY;
131
+ const rootDir = path.isAbsolute(configuredDir) ? configuredDir : path.resolve(projectRoot, configuredDir);
132
+ let files;
133
+ try {
134
+ files = await discoverJsonFiles(rootDir);
135
+ } catch {
136
+ for (const selector of selectors) {
137
+ timelines.set(
138
+ strategyEvidenceTimelineSelectorKey(selector),
139
+ invalidTimeline()
140
+ );
141
+ }
142
+ return timelines;
143
+ }
144
+ const envelopesByStrategy = /* @__PURE__ */ new Map();
145
+ const invalidStrategies = /* @__PURE__ */ new Set();
146
+ for (const filePath of files) {
147
+ let parsed = null;
148
+ try {
149
+ parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
150
+ } catch {
151
+ for (const strategy of inferMatchingStrategies({
152
+ filePath,
153
+ rootDir,
154
+ parsed,
155
+ strategies
156
+ })) {
157
+ invalidStrategies.add(strategy);
158
+ }
159
+ continue;
160
+ }
161
+ const matchingStrategies = inferMatchingStrategies({
162
+ filePath,
163
+ rootDir,
164
+ parsed,
165
+ strategies
166
+ });
167
+ if (!matchingStrategies.length) continue;
168
+ try {
169
+ const envelope = verifyStrategyEvidenceMarkerEnvelope(parsed);
170
+ for (const strategy of matchingStrategies) {
171
+ if (strategy !== envelope.payload.strategy) {
172
+ invalidStrategies.add(strategy);
173
+ }
174
+ }
175
+ if (!strategies.includes(envelope.payload.strategy)) {
176
+ continue;
177
+ }
178
+ const envelopes = envelopesByStrategy.get(envelope.payload.strategy) ?? [];
179
+ envelopes.push(envelope);
180
+ envelopesByStrategy.set(envelope.payload.strategy, envelopes);
181
+ } catch {
182
+ for (const strategy of matchingStrategies) {
183
+ invalidStrategies.add(strategy);
184
+ }
185
+ }
186
+ }
187
+ for (const selector of selectors) {
188
+ const strategy = selector.strategy;
189
+ const selectorKey = strategyEvidenceTimelineSelectorKey(selector);
190
+ if (invalidStrategies.has(strategy)) {
191
+ timelines.set(selectorKey, invalidTimeline());
192
+ continue;
193
+ }
194
+ const envelopes = envelopesByStrategy.get(strategy) ?? [];
195
+ if (!envelopes.length) continue;
196
+ const hasCompleteSelector = Boolean(selector.compositionId) && Boolean(selector.gitSha) && Boolean(selector.gateFingerprint) && Boolean(selector.configFingerprint) && Boolean(selector.contextFingerprint);
197
+ if (selector.requireCompleteLineage && !hasCompleteSelector) continue;
198
+ const markersById = /* @__PURE__ */ new Map();
199
+ let hasConflict = false;
200
+ for (const envelope of envelopes) {
201
+ for (const marker of envelope.payload.markers) {
202
+ const existing = markersById.get(marker.id);
203
+ if (existing && canonicalStrategyEvidenceJson(existing) !== canonicalStrategyEvidenceJson(marker)) {
204
+ hasConflict = true;
205
+ break;
206
+ }
207
+ markersById.set(marker.id, marker);
208
+ }
209
+ if (hasConflict) break;
210
+ }
211
+ if (hasConflict) {
212
+ timelines.set(selectorKey, invalidTimeline());
213
+ continue;
214
+ }
215
+ const matchingMarkers = [...markersById.values()].filter(
216
+ (marker) => (!selector.compositionId || marker.compositionId === selector.compositionId) && (!selector.gitSha || marker.gitSha === selector.gitSha) && (!selector.gateFingerprint || marker.gateFingerprint === selector.gateFingerprint) && (!selector.configFingerprint || marker.configFingerprint === selector.configFingerprint) && (!selector.contextFingerprint || marker.contextFingerprint === selector.contextFingerprint) && marker.timestamp >= startTime && marker.timestamp < endTime
217
+ ).sort(
218
+ (left, right) => left.timestamp - right.timestamp || left.type.localeCompare(right.type) || left.id.localeCompare(right.id)
219
+ );
220
+ let lastLossValue;
221
+ let hasLastLossValue = false;
222
+ const markers = matchingMarkers.filter((marker) => {
223
+ if (marker.type !== "L") return true;
224
+ if (hasLastLossValue && marker.maxLossValue === lastLossValue) {
225
+ return false;
226
+ }
227
+ hasLastLossValue = true;
228
+ lastLossValue = marker.maxLossValue;
229
+ return true;
230
+ });
231
+ if (!markers.length && (selector.compositionId || selector.gitSha || selector.gateFingerprint || selector.configFingerprint || selector.contextFingerprint)) {
232
+ continue;
233
+ }
234
+ timelines.set(selectorKey, {
235
+ status: "verified",
236
+ observedFrom: markers.length ? Math.min(...markers.map((marker) => marker.timestamp)) : Math.min(...envelopes.map((envelope) => envelope.payload.createdAt)),
237
+ markers
238
+ });
239
+ }
240
+ return timelines;
241
+ };
242
+
243
+ // src/runtimeTradeSync.ts
244
+ import { TTL_1M } from "@tradejs/core/constants";
245
+ import { getRuntimeStorageDayKey } from "@tradejs/core/time";
246
+ import {
247
+ delKey,
248
+ getData,
249
+ redisKeys,
250
+ setData,
251
+ setHashJsonField
252
+ } from "@tradejs/infra/redis";
253
+ var redisRuntimeTradeStore = {
254
+ async getActiveOrderId({ userName, symbol, scopeId }) {
255
+ const value = await getData(
256
+ redisKeys.runtimeActiveTrade(userName, symbol, scopeId),
257
+ null
258
+ );
259
+ return typeof value?.orderId === "string" ? value.orderId : null;
260
+ },
261
+ async saveTrade({ userName, trade, expire }) {
262
+ await Promise.all([
263
+ setData(redisKeys.runtimeTrade(userName, trade.orderId), trade, {
264
+ expire
265
+ }),
266
+ setHashJsonField(
267
+ redisKeys.runtimeTradeBucket(
268
+ userName,
269
+ getRuntimeStorageDayKey(trade.entryTimestamp)
270
+ ),
271
+ trade.orderId,
272
+ trade,
273
+ { expire }
274
+ )
275
+ ]);
276
+ },
277
+ async saveClosedTrade({ userName, trade, expire }) {
278
+ await setHashJsonField(
279
+ redisKeys.runtimeClosedTradeBucket(
280
+ userName,
281
+ getRuntimeStorageDayKey(trade.exitTimestamp)
282
+ ),
283
+ trade.orderId,
284
+ trade,
285
+ { expire }
286
+ );
287
+ },
288
+ async deleteActiveTrade({ userName, symbol, scopeId }) {
289
+ await delKey(redisKeys.runtimeActiveTrade(userName, symbol, scopeId));
290
+ }
291
+ };
292
+ var getRuntimeTradeScopeId = (trade) => trade.deploymentId ?? trade.accountId;
293
+ var isRuntimeTradeInConnectorScope = (trade, connector) => {
294
+ if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
295
+ return false;
296
+ }
297
+ if (trade.deploymentId && !connector.deploymentId) {
298
+ return false;
299
+ }
300
+ if (trade.accountId && trade.accountId !== connector.accountId) {
301
+ return false;
302
+ }
303
+ if (trade.accountId && !connector.accountId) {
304
+ return false;
305
+ }
306
+ return (trade.universe ?? "crypto") === connector.universe;
307
+ };
308
+ var buildRiskLevelsAnalysis = (position) => {
309
+ const takeProfitPrice = typeof position.takeProfitPrice === "number" && Number.isFinite(position.takeProfitPrice) ? position.takeProfitPrice : null;
310
+ const stopLossPrice = typeof position.stopLossPrice === "number" && Number.isFinite(position.stopLossPrice) ? position.stopLossPrice : null;
311
+ if (takeProfitPrice == null && stopLossPrice == null) {
312
+ return null;
313
+ }
314
+ return {
315
+ ...takeProfitPrice != null ? { takeProfitPrice } : {},
316
+ ...stopLossPrice != null ? { stopLossPrice } : {}
317
+ };
318
+ };
319
+ var hasExchangeCloseDetails = (trade) => trade.status === "closed" && typeof trade.exitPrice === "number" && Number.isFinite(trade.exitPrice) && typeof trade.actualExitPrice === "number" && Number.isFinite(trade.actualExitPrice) && typeof trade.closedPnl === "number" && Number.isFinite(trade.closedPnl) && typeof trade.openFee === "number" && Number.isFinite(trade.openFee) && typeof trade.closeFee === "number" && Number.isFinite(trade.closeFee);
320
+ var syncRuntimeTrades = async ({
321
+ userName,
322
+ connector,
323
+ trades,
324
+ endTime,
325
+ openPositions,
326
+ openPositionsReliable,
327
+ closedPnlRows,
328
+ store = redisRuntimeTradeStore
329
+ }) => {
330
+ const openPositionsBySymbol = new Map(
331
+ openPositions.map((position) => [position.symbol, position])
332
+ );
333
+ const activeOrderIdByScope = /* @__PURE__ */ new Map();
334
+ const activeTradeScopes = [
335
+ ...new Map(
336
+ trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map((trade) => {
337
+ const scope = {
338
+ symbol: trade.symbol,
339
+ scopeId: getRuntimeTradeScopeId(trade)
340
+ };
341
+ return [`${scope.scopeId ?? ""}:${scope.symbol}`, scope];
342
+ })
343
+ ).values()
344
+ ];
345
+ await Promise.all(
346
+ activeTradeScopes.map(async ({ symbol, scopeId }) => {
347
+ activeOrderIdByScope.set(
348
+ `${scopeId ?? ""}:${symbol}`,
349
+ await store.getActiveOrderId({ userName, symbol, scopeId })
350
+ );
351
+ })
352
+ );
353
+ const exactByOrderLinkId = new Map(
354
+ closedPnlRows.filter(
355
+ (row) => typeof row.orderLinkId === "string" && row.orderLinkId.length > 0
356
+ ).map((row) => [row.orderLinkId, row])
357
+ );
358
+ const exactByOrderId = new Map(
359
+ closedPnlRows.filter(
360
+ (row) => typeof row.orderId === "string" && row.orderId.length > 0
361
+ ).map((row) => [row.orderId, row])
362
+ );
363
+ const symbolBuckets = /* @__PURE__ */ new Map();
364
+ for (const row of closedPnlRows) {
365
+ const bucket = symbolBuckets.get(row.symbol) ?? [];
366
+ bucket.push(row);
367
+ symbolBuckets.set(row.symbol, bucket);
368
+ }
369
+ const syncedTrades = [];
370
+ for (const trade of trades) {
371
+ if (!isRuntimeTradeInConnectorScope(trade, connector)) {
372
+ syncedTrades.push(trade);
373
+ continue;
374
+ }
375
+ const scopeId = getRuntimeTradeScopeId(trade);
376
+ const isCurrentActiveTrade = activeOrderIdByScope.get(`${scopeId ?? ""}:${trade.symbol}`) === trade.orderId;
377
+ if (hasExchangeCloseDetails(trade)) {
378
+ if (isCurrentActiveTrade) {
379
+ await store.deleteActiveTrade({
380
+ userName,
381
+ symbol: trade.symbol,
382
+ scopeId
383
+ });
384
+ }
385
+ syncedTrades.push(trade);
386
+ continue;
387
+ }
388
+ const openPosition = openPositionsBySymbol.get(trade.symbol);
389
+ if (trade.status === "active" && !openPositionsReliable) {
390
+ syncedTrades.push(trade);
391
+ continue;
392
+ }
393
+ if (trade.status === "active" && isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
394
+ const riskLevelsAnalysis = buildRiskLevelsAnalysis(openPosition);
395
+ const nextTrade2 = {
396
+ ...trade,
397
+ status: "active",
398
+ currentPrice: openPosition.currentPrice,
399
+ currentPnl: openPosition.unrealizedPnl,
400
+ aiAnalysis: riskLevelsAnalysis ? { ...trade.aiAnalysis ?? {}, ...riskLevelsAnalysis } : trade.aiAnalysis,
401
+ lastSyncedAt: endTime
402
+ };
403
+ await store.saveTrade({ userName, trade: nextTrade2, expire: 0 });
404
+ syncedTrades.push(nextTrade2);
405
+ continue;
406
+ }
407
+ const matchedClosedPnl = takeClosedPnlMatch({
408
+ exactByOrderLinkId,
409
+ exactByOrderId,
410
+ symbolBuckets,
411
+ trade
412
+ });
413
+ if (!matchedClosedPnl) {
414
+ syncedTrades.push(trade);
415
+ continue;
416
+ }
417
+ const nextTrade = {
418
+ ...trade,
419
+ status: "closed",
420
+ currentPrice: matchedClosedPnl.exitPrice ?? trade.currentPrice ?? null,
421
+ currentPnl: matchedClosedPnl.closedPnl,
422
+ closedPnl: matchedClosedPnl.closedPnl,
423
+ actualEntryPrice: matchedClosedPnl.entryPrice ?? trade.actualEntryPrice ?? null,
424
+ exitPrice: matchedClosedPnl.exitPrice ?? trade.exitPrice ?? null,
425
+ actualExitPrice: matchedClosedPnl.exitPrice ?? trade.actualExitPrice ?? null,
426
+ exitTimestamp: matchedClosedPnl.closedAt,
427
+ exitType: trade.exitType ?? null,
428
+ openFee: matchedClosedPnl.openFee ?? trade.openFee ?? null,
429
+ closeFee: matchedClosedPnl.closeFee ?? trade.closeFee ?? null,
430
+ fundingFee: matchedClosedPnl.fundingFee ?? trade.fundingFee ?? null,
431
+ totalFee: matchedClosedPnl.totalFee ?? trade.totalFee ?? null,
432
+ lastSyncedAt: endTime
433
+ };
434
+ await Promise.all([
435
+ store.saveTrade({ userName, trade: nextTrade, expire: TTL_1M }),
436
+ store.saveClosedTrade({ userName, trade: nextTrade, expire: TTL_1M }),
437
+ ...isCurrentActiveTrade ? [
438
+ store.deleteActiveTrade({
439
+ userName,
440
+ symbol: trade.symbol,
441
+ scopeId
442
+ })
443
+ ] : []
444
+ ]);
445
+ syncedTrades.push(nextTrade);
446
+ }
447
+ return syncedTrades;
448
+ };
449
+
450
+ // src/runtimeDashboard.ts
451
+ var DEFAULT_PROVIDER = "bybit";
452
+ var DEFAULT_HOURS = 168;
453
+ var MIN_HOURS = 6;
454
+ var MAX_HOURS = 24 * 90;
455
+ var BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1e3 - 1e3;
456
+ var EXCHANGE_REQUEST_TIMEOUT_MS = 15e3;
457
+ var coerceHours = (value) => {
458
+ const parsed = Number(value ?? Number.NaN);
459
+ if (!Number.isFinite(parsed)) {
460
+ return DEFAULT_HOURS;
461
+ }
462
+ return Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.trunc(parsed)));
463
+ };
464
+ var isRuntimeStrategyConfigEnabled = (config) => {
465
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
466
+ return false;
467
+ }
468
+ return config.ENABLE !== false;
469
+ };
470
+ var loadRuntimeStrategyConfigs = async (userName) => {
471
+ return (await loadStoredRuntimeStrategyConfigs(userName)).map(
472
+ ({ strategyConfig, ...record }) => ({
473
+ ...record,
474
+ config: strategyConfig
475
+ })
476
+ );
477
+ };
478
+ var loadConfiguredStrategyNames = async (projectRoot) => {
479
+ try {
480
+ return await getAvailableStrategyNames(projectRoot);
481
+ } catch (error) {
482
+ logger.warn(
483
+ "strategies runtime: failed to load configured strategies: %s",
484
+ error?.message || String(error)
485
+ );
486
+ return [];
487
+ }
488
+ };
489
+ var resolveConnectorCreatorByProvider = async (provider, projectRoot) => await getConnectorCreatorByProvider(provider, projectRoot) ?? await getConnectorCreatorByProvider(DEFAULT_PROVIDER, projectRoot) ?? null;
490
+ var resolveConnectorAccountId = async ({
491
+ userName,
492
+ provider
493
+ }) => (await resolveTradingAccount({
494
+ userName,
495
+ provider,
496
+ universe: "crypto"
497
+ }))?.id;
498
+ var loadRuntimeTrades = async (userName, {
499
+ startTime,
500
+ endTime
501
+ }) => {
502
+ const filterByWindow = (trade) => trade.entryTimestamp >= startTime || typeof trade.exitTimestamp === "number" && trade.exitTimestamp >= startTime;
503
+ const dayKeys = getRuntimeStorageDayKeys(startTime, endTime);
504
+ const bucketTrades = (await Promise.all(
505
+ dayKeys.map(
506
+ (dayKey) => getHashJsonValues(
507
+ redisKeys2.runtimeTradeBucket(userName, dayKey)
508
+ )
509
+ )
510
+ )).flat();
511
+ const dedupedBucketTrades = /* @__PURE__ */ new Map();
512
+ for (const trade of bucketTrades) {
513
+ if (!isRuntimeTradeRecord(trade)) {
514
+ continue;
515
+ }
516
+ dedupedBucketTrades.set(trade.orderId, trade);
517
+ }
518
+ if (dedupedBucketTrades.size > 0 || dayKeys.length === 0) {
519
+ return [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
520
+ }
521
+ const keys = await getKeys(redisKeys2.runtimeTrades(userName));
522
+ const trades = await Promise.all(keys.map((key) => getData2(key, null)));
523
+ return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
524
+ };
525
+ var buildExchangeTimeRanges = (startTime, endTime) => {
526
+ const ranges = [];
527
+ let cursor = startTime;
528
+ while (cursor < endTime) {
529
+ const rangeEnd = Math.min(endTime, cursor + BYBIT_MAX_TIME_RANGE_MS);
530
+ ranges.push({ startTime: cursor, endTime: rangeEnd });
531
+ cursor = rangeEnd + 1;
532
+ }
533
+ return ranges;
534
+ };
535
+ var loadExchangeRange = async ({
536
+ label,
537
+ startTime,
538
+ endTime,
539
+ load,
540
+ errors
541
+ }) => {
542
+ try {
543
+ return await Promise.race([
544
+ load(),
545
+ new Promise((_, reject) => {
546
+ setTimeout(
547
+ () => reject(
548
+ new Error(
549
+ `${label} timed out for ${new Date(startTime).toISOString()} - ${new Date(endTime).toISOString()}`
550
+ )
551
+ ),
552
+ EXCHANGE_REQUEST_TIMEOUT_MS
553
+ );
554
+ })
555
+ ]);
556
+ } catch (error) {
557
+ const message = error?.message || String(error);
558
+ errors?.push(`${label}: ${message}`);
559
+ logger.warn("strategies runtime: %s failed: %s", label, message);
560
+ return [];
561
+ }
562
+ };
563
+ var loadActiveRuntimeOrderIds = async (userName) => {
564
+ const keys = await getKeys(redisKeys2.runtimeActiveTrades(userName));
565
+ const refs = await Promise.all(keys.map((key) => getData2(key, null)));
566
+ return new Set(
567
+ refs.map(
568
+ (ref) => typeof ref?.orderId === "string" && ref.orderId.trim() ? ref.orderId.trim() : null
569
+ ).filter((value) => Boolean(value))
570
+ );
571
+ };
572
+ var loadClosedPnlRows = async ({
573
+ connector,
574
+ startTime,
575
+ endTime,
576
+ errors
577
+ }) => {
578
+ if (typeof connector.getClosedPnl !== "function") {
579
+ return [];
580
+ }
581
+ try {
582
+ const rows = (await Promise.all(
583
+ buildExchangeTimeRanges(startTime, endTime).map(
584
+ (range) => loadExchangeRange({
585
+ label: "getClosedPnl",
586
+ ...range,
587
+ errors,
588
+ load: () => connector.getClosedPnl?.({
589
+ ...range,
590
+ limit: 100
591
+ }) ?? Promise.resolve([])
592
+ })
593
+ )
594
+ )).flatMap((items) => items ?? []);
595
+ return rows.sort((left, right) => left.closedAt - right.closedAt);
596
+ } catch (error) {
597
+ const message = error?.message || String(error);
598
+ errors?.push(`getClosedPnl: ${message}`);
599
+ logger.warn("strategies runtime: getClosedPnl failed: %s", message);
600
+ return [];
601
+ }
602
+ };
603
+ var loadExchangeEntryRows = async ({
604
+ connector,
605
+ startTime,
606
+ endTime,
607
+ errors
608
+ }) => {
609
+ if (typeof connector.getEntryExecutions !== "function") {
610
+ return [];
611
+ }
612
+ try {
613
+ const rows = (await Promise.all(
614
+ buildExchangeTimeRanges(startTime, endTime).map(
615
+ (range) => loadExchangeRange({
616
+ label: "getEntryExecutions",
617
+ ...range,
618
+ errors,
619
+ load: () => connector.getEntryExecutions?.({
620
+ ...range,
621
+ limit: 100
622
+ }) ?? Promise.resolve([])
623
+ })
624
+ )
625
+ )).flatMap((items) => items ?? []);
626
+ return rows.sort(
627
+ (left, right) => left.entryTimestamp - right.entryTimestamp
628
+ );
629
+ } catch (error) {
630
+ const message = error?.message || String(error);
631
+ errors?.push(`getEntryExecutions: ${message}`);
632
+ logger.warn("strategies runtime: getEntryExecutions failed: %s", message);
633
+ return [];
634
+ }
635
+ };
636
+ var loadOpenPositions = async (connector, errors) => {
637
+ if (typeof connector.getOpenPositionPnl !== "function") {
638
+ return { positions: [], reliable: false };
639
+ }
640
+ try {
641
+ return {
642
+ positions: await connector.getOpenPositionPnl(),
643
+ reliable: true
644
+ };
645
+ } catch (error) {
646
+ const message = error?.message || String(error);
647
+ errors?.push(`getOpenPositionPnl: ${message}`);
648
+ logger.warn("strategies runtime: getOpenPositionPnl failed: %s", message);
649
+ return { positions: [], reliable: false };
650
+ }
651
+ };
652
+ var loadRuntimeDashboard = async ({
653
+ userName,
654
+ provider: requestedProvider,
655
+ hours: requestedHours,
656
+ now,
657
+ projectRoot: requestedProjectRoot
658
+ }) => {
659
+ const provider = requestedProvider?.trim() || DEFAULT_PROVIDER;
660
+ const hours = coerceHours(requestedHours);
661
+ const endTime = now ?? Date.now();
662
+ const startTime = endTime - hours * 60 * 60 * 1e3;
663
+ const projectRoot = requestedProjectRoot?.trim() || String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
664
+ const exchangeErrors = [];
665
+ const connectorCreator = await resolveConnectorCreatorByProvider(
666
+ provider,
667
+ projectRoot
668
+ );
669
+ if (!connectorCreator) {
670
+ throw new Error(`No connector available for provider "${provider}"`);
671
+ }
672
+ const connectorAccountId = await resolveConnectorAccountId({
673
+ userName,
674
+ provider
675
+ });
676
+ const connector = await connectorCreator({
677
+ userName,
678
+ accountId: connectorAccountId,
679
+ universe: "crypto"
680
+ });
681
+ const [
682
+ runtimeStrategyConfigs,
683
+ configuredStrategyNames,
684
+ runtimeTrades,
685
+ activeOrderIds,
686
+ closedPnlRows,
687
+ entryRows,
688
+ openPositionsSnapshot,
689
+ runtimeDeployments,
690
+ tradingAccounts
691
+ ] = await Promise.all([
692
+ loadRuntimeStrategyConfigs(userName),
693
+ loadConfiguredStrategyNames(projectRoot),
694
+ loadRuntimeTrades(userName, { startTime, endTime }),
695
+ loadActiveRuntimeOrderIds(userName),
696
+ loadClosedPnlRows({
697
+ connector,
698
+ startTime,
699
+ endTime,
700
+ errors: exchangeErrors
701
+ }),
702
+ loadExchangeEntryRows({
703
+ connector,
704
+ startTime,
705
+ endTime,
706
+ errors: exchangeErrors
707
+ }),
708
+ loadOpenPositions(connector, exchangeErrors),
709
+ listRuntimeDeployments(userName),
710
+ listTradingAccounts(userName)
711
+ ]);
712
+ const relevantTrades = selectTradesForWindow(
713
+ runtimeTrades,
714
+ startTime,
715
+ activeOrderIds
716
+ );
717
+ const syncableTrades = relevantTrades.filter(
718
+ (trade) => isRuntimeTradeInConnectorScope(trade, connector)
719
+ );
720
+ const unsyncedTrades = relevantTrades.filter(
721
+ (trade) => !isRuntimeTradeInConnectorScope(trade, connector)
722
+ );
723
+ const syncedConnectorTrades = await syncRuntimeTrades({
724
+ userName,
725
+ connector,
726
+ trades: syncableTrades,
727
+ endTime,
728
+ openPositions: openPositionsSnapshot.positions,
729
+ openPositionsReliable: openPositionsSnapshot.reliable,
730
+ closedPnlRows
731
+ });
732
+ const syncedTrades = [...unsyncedTrades, ...syncedConnectorTrades];
733
+ const fallbackStrategyNames = [
734
+ .../* @__PURE__ */ new Set([
735
+ ...runtimeStrategyConfigs.map(({ strategyName }) => strategyName),
736
+ ...configuredStrategyNames
737
+ ])
738
+ ];
739
+ const fallbackTrades = buildExchangeFallbackRuntimeTrades({
740
+ entryRows,
741
+ closedPnlRows,
742
+ openPositions: openPositionsSnapshot.positions,
743
+ strategyNames: fallbackStrategyNames,
744
+ existingTrades: syncedTrades,
745
+ endTime
746
+ });
747
+ const allTrades = [...syncedTrades, ...fallbackTrades].filter(
748
+ isRuntimeTradeRecord
749
+ );
750
+ const connectedSet = new Set(
751
+ runtimeStrategyConfigs.map(
752
+ ({ strategyName, configId }) => `${strategyName}:${configId}`
753
+ )
754
+ );
755
+ const accountsById = new Map(
756
+ tradingAccounts.map((account) => [account.id, account])
757
+ );
758
+ const runtimeIdentityKey = (trade) => buildRuntimeStrategyIdentityKey({
759
+ strategyName: trade.strategy,
760
+ configId: trade.runtimeConfigId,
761
+ universe: trade.universe,
762
+ accountId: trade.accountId,
763
+ deploymentId: trade.deploymentId,
764
+ policyProfileId: trade.policyProfileId
765
+ });
766
+ const identityByKey = /* @__PURE__ */ new Map();
767
+ const runtimeConfigAccountScopes = new Array();
768
+ for (const deployment of runtimeDeployments) {
769
+ for (const deploymentStrategy of deployment.strategies) {
770
+ const runtimeKey = buildRuntimeStrategyIdentityKey({
771
+ strategyName: deploymentStrategy.strategyName,
772
+ configId: `deployment-${deployment.id}`,
773
+ universe: deployment.universe,
774
+ accountId: deployment.accountId,
775
+ deploymentId: deployment.id,
776
+ policyProfileId: deploymentStrategy.policyProfileId
777
+ });
778
+ identityByKey.set(runtimeKey, {
779
+ strategyName: deploymentStrategy.strategyName,
780
+ configId: `deployment-${deployment.id}`,
781
+ interval: String(deployment.interval),
782
+ universe: deployment.universe,
783
+ accountId: deployment.accountId,
784
+ accountLabel: accountsById.get(deployment.accountId)?.label,
785
+ deploymentId: deployment.id,
786
+ policyProfileId: deploymentStrategy.policyProfileId,
787
+ releaseCompositionId: deploymentStrategy.releaseCompositionId,
788
+ enabled: deployment.enabled && deploymentStrategy.enabled !== false,
789
+ config: deploymentStrategy.config,
790
+ connected: false,
791
+ configFingerprint: strategyLogicConfigFingerprint(
792
+ deploymentStrategy.config
793
+ )
794
+ });
795
+ }
796
+ }
797
+ for (const runtimeConfig of runtimeStrategyConfigs) {
798
+ const universe = runtimeConfig.config.UNIVERSE === "tradfi" ? "tradfi" : "crypto";
799
+ const configuredAccountId = typeof runtimeConfig.config.ACCOUNT_ID === "string" && runtimeConfig.config.ACCOUNT_ID.trim() ? runtimeConfig.config.ACCOUNT_ID.trim() : void 0;
800
+ const resolvedAccount = await resolveTradingAccount({
801
+ userName,
802
+ accountId: configuredAccountId,
803
+ provider,
804
+ universe
805
+ }).catch(() => null);
806
+ const accountId = resolvedAccount?.id ?? configuredAccountId;
807
+ runtimeConfigAccountScopes.push({
808
+ strategyName: runtimeConfig.strategyName,
809
+ configId: runtimeConfig.configId,
810
+ universe,
811
+ accountId
812
+ });
813
+ const runtimeKey = buildRuntimeStrategyIdentityKey({
814
+ strategyName: runtimeConfig.strategyName,
815
+ configId: runtimeConfig.configId,
816
+ universe,
817
+ accountId
818
+ });
819
+ identityByKey.set(runtimeKey, {
820
+ strategyName: runtimeConfig.strategyName,
821
+ configId: runtimeConfig.configId,
822
+ interval: String(runtimeConfig.config.INTERVAL ?? "15"),
823
+ universe,
824
+ accountId,
825
+ accountLabel: accountId ? accountsById.get(accountId)?.label : void 0,
826
+ enabled: isRuntimeStrategyConfigEnabled(runtimeConfig.config),
827
+ config: runtimeConfig.config,
828
+ connected: true,
829
+ configFingerprint: strategyLogicConfigFingerprint(runtimeConfig.config)
830
+ });
831
+ }
832
+ const accountScopedTrades = assignLegacyRuntimeTradeAccountScopes(
833
+ allTrades,
834
+ runtimeConfigAccountScopes
835
+ );
836
+ for (const trade of accountScopedTrades) {
837
+ const key = runtimeIdentityKey(trade);
838
+ const configuredCompositionId = identityByKey.get(key)?.releaseCompositionId;
839
+ const observedCompositionId = trade.runtimeLineage?.compositionId;
840
+ const releaseCompositionId = configuredCompositionId && observedCompositionId && configuredCompositionId !== observedCompositionId ? void 0 : observedCompositionId ?? configuredCompositionId;
841
+ identityByKey.set(key, {
842
+ ...identityByKey.get(key),
843
+ strategyName: trade.strategy,
844
+ configId: trade.runtimeConfigId ?? "config",
845
+ interval: String(trade.interval ?? "15"),
846
+ universe: trade.universe ?? "crypto",
847
+ accountId: trade.accountId,
848
+ accountLabel: trade.accountId ? accountsById.get(trade.accountId)?.label : void 0,
849
+ deploymentId: trade.deploymentId,
850
+ policyProfileId: trade.policyProfileId,
851
+ releaseCompositionId,
852
+ configFingerprint: trade.runtimeLineage?.configFingerprint,
853
+ gateFingerprint: trade.runtimeLineage?.gateFingerprint,
854
+ contextFingerprint: trade.runtimeLineage?.contextFingerprint,
855
+ gitSha: trade.runtimeLineage?.gitSha ?? void 0,
856
+ maxLossValue: trade.runtimeLineage?.maxLossValue ?? void 0
857
+ });
858
+ }
859
+ const evidenceTimelines = await loadStrategyEvidenceTimelines({
860
+ projectRoot,
861
+ markerDir: process.env.STRATEGY_RELEASE_MARKER_DIR,
862
+ selectors: [...identityByKey.values()].map((identity) => ({
863
+ strategy: identity.strategyName,
864
+ compositionId: identity.releaseCompositionId,
865
+ configFingerprint: identity.configFingerprint,
866
+ gateFingerprint: identity.gateFingerprint,
867
+ contextFingerprint: identity.contextFingerprint,
868
+ gitSha: identity.gitSha,
869
+ maxLossValue: identity.maxLossValue,
870
+ requireCompleteLineage: true
871
+ })),
872
+ startTime,
873
+ endTime
874
+ });
875
+ const strategies = await Promise.all(
876
+ [...identityByKey.entries()].map(async ([runtimeKey, identity]) => {
877
+ const { strategyName } = identity;
878
+ const strategyTrades = accountScopedTrades.filter((trade) => runtimeIdentityKey(trade) === runtimeKey).sort((left, right) => right.entryTimestamp - left.entryTimestamp);
879
+ const orders = strategyTrades.sort((left, right) => {
880
+ const leftDate = left.exitTimestamp ?? left.entryTimestamp;
881
+ const rightDate = right.exitTimestamp ?? right.entryTimestamp;
882
+ return rightDate - leftDate;
883
+ }).map((trade) => toRuntimeTradeView(trade, endTime));
884
+ const analytics = buildRuntimeStrategyAnalytics({
885
+ trades: strategyTrades,
886
+ startTime,
887
+ endTime
888
+ });
889
+ const effectiveStrategyConfig = identity.config ?? null;
890
+ return {
891
+ runtimeKey,
892
+ strategyName,
893
+ configId: identity.configId,
894
+ interval: identity.interval,
895
+ universe: identity.universe,
896
+ accountId: identity.accountId,
897
+ accountLabel: identity.accountLabel,
898
+ deploymentId: identity.deploymentId,
899
+ policyProfileId: identity.policyProfileId,
900
+ connected: identity.connected ?? connectedSet.has(`${strategyName}:${identity.configId}`),
901
+ enabled: identity.enabled ?? isRuntimeStrategyConfigEnabled(effectiveStrategyConfig),
902
+ config: effectiveStrategyConfig,
903
+ symbols: [...new Set(strategyTrades.map((trade) => trade.symbol))],
904
+ stat: analytics.stat,
905
+ summary: analytics.summary,
906
+ orderLog: analytics.orderLog,
907
+ evidenceTimeline: evidenceTimelines.get(
908
+ strategyEvidenceTimelineSelectorKey({
909
+ strategy: strategyName,
910
+ compositionId: identity.releaseCompositionId,
911
+ configFingerprint: identity.configFingerprint,
912
+ gateFingerprint: identity.gateFingerprint,
913
+ contextFingerprint: identity.contextFingerprint,
914
+ gitSha: identity.gitSha,
915
+ maxLossValue: identity.maxLossValue,
916
+ requireCompleteLineage: true
917
+ })
918
+ ) ?? {
919
+ status: "missing",
920
+ observedFrom: null,
921
+ markers: []
922
+ },
923
+ recentTrades: strategyTrades.slice(0, 8).map((trade) => toRuntimeTradeView(trade, endTime)),
924
+ orders
925
+ };
926
+ })
927
+ );
928
+ strategies.sort((left, right) => {
929
+ if (left.stat.netProfit !== right.stat.netProfit) {
930
+ return right.stat.netProfit - left.stat.netProfit;
931
+ }
932
+ if (left.summary.totalPnl !== right.summary.totalPnl) {
933
+ return right.summary.totalPnl - left.summary.totalPnl;
934
+ }
935
+ if (left.connected !== right.connected) {
936
+ return left.connected ? -1 : 1;
937
+ }
938
+ return left.strategyName.localeCompare(right.strategyName);
939
+ });
940
+ const response = {
941
+ provider,
942
+ hours,
943
+ generatedAt: endTime,
944
+ dataSources: {
945
+ localTrades: syncedTrades.length,
946
+ exchangeFallbackTrades: fallbackTrades.length,
947
+ exchangeErrors: [...new Set(exchangeErrors)].sort()
948
+ },
949
+ strategies
950
+ };
951
+ return response;
952
+ };
953
+ export {
954
+ loadRuntimeDashboard
955
+ };