@tradejs/node 3.0.1 → 3.1.1

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