@tradejs/cli 2.0.6 → 2.0.8

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.
@@ -1643,8 +1643,9 @@ module.exports = __toCommonJS(replay_exports);
1643
1643
  // src/scripts/replayRunner.ts
1644
1644
  var import_chalk12 = __toESM(require("chalk"));
1645
1645
  var import_constants6 = require("@tradejs/core/constants");
1646
+ var import_data2 = require("@tradejs/core/data");
1646
1647
  var import_time7 = require("@tradejs/core/time");
1647
- var import_redis5 = require("@tradejs/infra/redis");
1648
+ var import_redis6 = require("@tradejs/infra/redis");
1648
1649
 
1649
1650
  // src/lib/runFormatting.ts
1650
1651
  var import_date_fns = require("date-fns");
@@ -1706,11 +1707,30 @@ var resolveTimeWindow = ({
1706
1707
  endTime,
1707
1708
  nowMs = Date.now(),
1708
1709
  defaultStartMs,
1709
- defaultEndMs
1710
+ defaultEndMs,
1711
+ closedIntervalMs
1710
1712
  }) => {
1711
1713
  const explicitStart = toEpochMs(startTime);
1712
1714
  const explicitEnd = toEpochMs(endTime);
1713
1715
  const resolvedDays = toPositiveNumber(days);
1716
+ const alignToLastClosedInterval = (window2, preserveDuration) => {
1717
+ if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
1718
+ return window2;
1719
+ }
1720
+ const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
1721
+ const delta = window2.end - alignedEnd;
1722
+ const alignedStart = preserveDuration ? window2.start - delta : window2.start;
1723
+ if (alignedStart >= alignedEnd) {
1724
+ throw new Error(
1725
+ `Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
1726
+ );
1727
+ }
1728
+ return {
1729
+ ...window2,
1730
+ start: Math.trunc(alignedStart),
1731
+ end: Math.trunc(alignedEnd)
1732
+ };
1733
+ };
1714
1734
  if (explicitStart != null || explicitEnd != null) {
1715
1735
  const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
1716
1736
  const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
@@ -1719,11 +1739,14 @@ var resolveTimeWindow = ({
1719
1739
  `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
1720
1740
  );
1721
1741
  }
1722
- return {
1723
- start: resolvedStart,
1724
- end: resolvedEnd2,
1725
- source: "explicit"
1726
- };
1742
+ return alignToLastClosedInterval(
1743
+ {
1744
+ start: resolvedStart,
1745
+ end: resolvedEnd2,
1746
+ source: "explicit"
1747
+ },
1748
+ explicitStart == null && resolvedDays != null
1749
+ );
1727
1750
  }
1728
1751
  if (resolvedDays != null) {
1729
1752
  const resolvedEnd2 = defaultEndMs ?? nowMs;
@@ -1733,11 +1756,14 @@ var resolveTimeWindow = ({
1733
1756
  `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
1734
1757
  );
1735
1758
  }
1736
- return {
1737
- start: Math.trunc(resolvedStart),
1738
- end: Math.trunc(resolvedEnd2),
1739
- source: "days"
1740
- };
1759
+ return alignToLastClosedInterval(
1760
+ {
1761
+ start: Math.trunc(resolvedStart),
1762
+ end: Math.trunc(resolvedEnd2),
1763
+ source: "days"
1764
+ },
1765
+ true
1766
+ );
1741
1767
  }
1742
1768
  const resolvedEnd = defaultEndMs ?? nowMs;
1743
1769
  if (defaultStartMs >= resolvedEnd) {
@@ -1745,11 +1771,14 @@ var resolveTimeWindow = ({
1745
1771
  `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
1746
1772
  );
1747
1773
  }
1748
- return {
1749
- start: Math.trunc(defaultStartMs),
1750
- end: Math.trunc(resolvedEnd),
1751
- source: "default"
1752
- };
1774
+ return alignToLastClosedInterval(
1775
+ {
1776
+ start: Math.trunc(defaultStartMs),
1777
+ end: Math.trunc(resolvedEnd),
1778
+ source: "default"
1779
+ },
1780
+ true
1781
+ );
1753
1782
  };
1754
1783
 
1755
1784
  // src/lib/runtimeStrategyBacktest.ts
@@ -2189,7 +2218,8 @@ var prepareRunEnvironment = async ({
2189
2218
  accountId,
2190
2219
  deploymentId,
2191
2220
  assetClasses,
2192
- deployment
2221
+ deployment,
2222
+ closedIntervalMs
2193
2223
  }) => {
2194
2224
  const connectorName = await resolveRunConnectorName({
2195
2225
  value: connector,
@@ -2252,7 +2282,8 @@ var prepareRunEnvironment = async ({
2252
2282
  startTime,
2253
2283
  endTime,
2254
2284
  defaultStartMs: (0, import_time2.getTimestamp)(import_constants.BACKTEST_DEFAULT_DAYS),
2255
- defaultEndMs: (0, import_time2.getTimestamp)()
2285
+ defaultEndMs: (0, import_time2.getTimestamp)(),
2286
+ closedIntervalMs
2256
2287
  });
2257
2288
  const preloadStart = (0, import_time2.getBacktestPreloadStart)(
2258
2289
  window2.start,
@@ -2321,6 +2352,7 @@ var import_progress = __toESM(require("progress"));
2321
2352
  var import_chalk4 = __toESM(require("chalk"));
2322
2353
  var import_connectors4 = require("@tradejs/connectors");
2323
2354
  var import_connectors5 = require("@tradejs/node/connectors");
2355
+ var import_strategies = require("@tradejs/node/strategies");
2324
2356
  var import_timescale = require("@tradejs/infra/timescale");
2325
2357
 
2326
2358
  // src/lib/binanceMarketData.ts
@@ -2330,14 +2362,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
2330
2362
  "15m": 9e5,
2331
2363
  "1h": 36e5
2332
2364
  };
2333
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
2334
- "USDCUSDT",
2335
- "FDUSDUSDT",
2336
- "TUSDUSDT",
2337
- "BUSDUSDT",
2338
- "USDPUSDT",
2339
- "DAIUSDT"
2340
- ]);
2341
2365
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
2342
2366
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
2343
2367
  var standardDeviation = (values) => {
@@ -2381,9 +2405,6 @@ var classifyBtcAltRegime = ({
2381
2405
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
2382
2406
  return "neutral";
2383
2407
  };
2384
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
2385
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
2386
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
2387
2408
  var aggregateAggTradesToRows = ({
2388
2409
  symbol,
2389
2410
  interval,
@@ -2865,10 +2886,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2865
2886
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
2866
2887
  2
2867
2888
  );
2868
- const breadthLimit = asInt(
2869
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
2870
- 30
2871
- );
2872
2889
  const breadthChunkDays = asFloat(
2873
2890
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
2874
2891
  30
@@ -2991,88 +3008,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2991
3008
  }
2992
3009
  }
2993
3010
  if (includeBreadth) {
2994
- const tickers = await connector.getTickers();
2995
- const breadthSymbols = selectBreadthUniverseFromTickers(
2996
- tickers,
2997
- breadthLimit
2998
- );
2999
- const universe = `binance_top${breadthSymbols.length}_usdt`;
3000
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
3001
- universe,
3002
- interval,
3003
- startMs: breadthStartMs,
3004
- endMs
3005
- });
3006
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
3007
- if (!hasCoverage({
3008
- coverage,
3011
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
3012
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
3013
+ const connectorInterval = marketIntervalToConnectorInterval(interval);
3014
+ const chunks = buildBreadthBackfillChunks({
3009
3015
  startMs: breadthStartMs,
3010
3016
  endMs,
3011
- intervalMs
3012
- }) || !hasBtcAltMetrics) {
3013
- const connectorInterval = marketIntervalToConnectorInterval(interval);
3014
- const chunks = buildBreadthBackfillChunks({
3015
- startMs: breadthStartMs,
3016
- endMs,
3017
- intervalMs,
3018
- chunkDays: breadthChunkDays
3019
- });
3020
- const missingChunks = filterMissingBreadthBackfillChunks({
3021
- chunks,
3022
- coverage,
3023
- intervalMs
3024
- });
3025
- const bar = new import_progress.default(
3026
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3027
- {
3028
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
3029
- width: 24
3030
- }
3031
- );
3032
- let candlesRead = 0;
3033
- let skippedBreadthChunks = chunks.length - missingChunks.length;
3034
- if (!missingChunks.length) {
3035
- bar.tick(1, {
3036
- candles: 0,
3037
- skip: skippedBreadthChunks,
3038
- chunk: "cached",
3039
- symbol: universe
3017
+ intervalMs,
3018
+ chunkDays: breadthChunkDays
3019
+ });
3020
+ const plans = await Promise.all(
3021
+ breadthUniverses.map(async (definition) => {
3022
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
3023
+ universe: definition.universe,
3024
+ interval,
3025
+ startMs: breadthStartMs,
3026
+ endMs
3040
3027
  });
3041
- }
3042
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3043
- const chunk = missingChunks[chunkIndex];
3044
- const originalChunkIndex = chunks.findIndex(
3045
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3046
- );
3047
- const candlesBySymbol = {};
3048
- for (const symbol of breadthSymbols) {
3049
- const candles = await connector.kline({
3050
- symbol,
3051
- interval: connectorInterval,
3052
- start: chunk.fetchStartMs,
3053
- end: chunk.endMs,
3054
- silent: true
3055
- });
3056
- candlesBySymbol[symbol] = candles;
3057
- candlesRead += candles.length;
3058
- bar.tick(1, {
3059
- candles: candlesRead,
3060
- skip: skippedBreadthChunks,
3061
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3062
- symbol
3063
- });
3028
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
3029
+ const missingChunks2 = !hasCoverage({
3030
+ coverage,
3031
+ startMs: breadthStartMs,
3032
+ endMs,
3033
+ intervalMs
3034
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
3035
+ chunks,
3036
+ coverage,
3037
+ intervalMs
3038
+ }) : [];
3039
+ if (!missingChunks2.length) {
3040
+ console.log(
3041
+ import_chalk4.default.gray(`breadth cached: universe=${definition.universe}`)
3042
+ );
3064
3043
  }
3065
- const btcCandles = await connector.kline({
3066
- symbol: "BTCUSDT",
3044
+ return {
3045
+ definition,
3046
+ missingChunkKeys: new Set(
3047
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
3048
+ )
3049
+ };
3050
+ })
3051
+ );
3052
+ const missingChunks = chunks.filter((chunk) => {
3053
+ const key = `${chunk.startMs}:${chunk.endMs}`;
3054
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
3055
+ });
3056
+ const bar = new import_progress.default(
3057
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3058
+ {
3059
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
3060
+ width: 24
3061
+ }
3062
+ );
3063
+ let candlesRead = 0;
3064
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
3065
+ if (!missingChunks.length) {
3066
+ bar.tick(1, {
3067
+ candles: 0,
3068
+ skip: skippedBreadthChunks,
3069
+ chunk: "cached",
3070
+ symbol: "top5/top10/top30/top50/top100"
3071
+ });
3072
+ }
3073
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3074
+ const chunk = missingChunks[chunkIndex];
3075
+ const originalChunkIndex = chunks.findIndex(
3076
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3077
+ );
3078
+ const candlesBySymbol = {};
3079
+ for (const symbol of breadthSymbols) {
3080
+ const candles = await connector.kline({
3081
+ symbol,
3067
3082
  interval: connectorInterval,
3068
3083
  start: chunk.fetchStartMs,
3069
3084
  end: chunk.endMs,
3070
3085
  silent: true
3071
3086
  });
3087
+ candlesBySymbol[symbol] = candles;
3088
+ candlesRead += candles.length;
3089
+ bar.tick(1, {
3090
+ candles: candlesRead,
3091
+ skip: skippedBreadthChunks,
3092
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3093
+ symbol
3094
+ });
3095
+ }
3096
+ const btcCandles = await connector.kline({
3097
+ symbol: "BTCUSDT",
3098
+ interval: connectorInterval,
3099
+ start: chunk.fetchStartMs,
3100
+ end: chunk.endMs,
3101
+ silent: true
3102
+ });
3103
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
3104
+ for (const plan of plans) {
3105
+ if (!plan.missingChunkKeys.has(chunkKey)) {
3106
+ continue;
3107
+ }
3108
+ const symbolSet = new Set(plan.definition.symbols);
3109
+ const universeCandles = Object.fromEntries(
3110
+ Object.entries(candlesBySymbol).filter(
3111
+ ([symbol]) => symbolSet.has(symbol)
3112
+ )
3113
+ );
3072
3114
  const rows = buildMarketBreadthRows({
3073
- universe,
3115
+ universe: plan.definition.universe,
3074
3116
  interval,
3075
- candlesBySymbol,
3117
+ candlesBySymbol: universeCandles,
3076
3118
  btcCandles
3077
3119
  }).filter((row) => {
3078
3120
  const ts = row.ts.getTime();
@@ -3081,8 +3123,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
3081
3123
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
3082
3124
  breadthRows += rows.length;
3083
3125
  }
3084
- } else {
3085
- console.log(import_chalk4.default.gray(`breadth cached: universe=${universe}`));
3086
3126
  }
3087
3127
  }
3088
3128
  console.log(
@@ -4307,27 +4347,27 @@ var createCoinMarketCapFetchStats = () => ({
4307
4347
  totalCredits: 0,
4308
4348
  byPath: /* @__PURE__ */ new Map()
4309
4349
  });
4310
- var getCoinMarketCapPathLabel = (path2) => {
4311
- if (path2.includes("global-metrics")) return "global";
4312
- if (path2.includes("cryptocurrency/quotes")) return "reference";
4313
- if (path2.includes("exchange/quotes")) return "exchangeLiquidity";
4314
- if (path2.includes("fear-and-greed")) return "fearGreed";
4315
- if (path2.includes("cmc100")) return "cmc100";
4316
- if (path2.includes("cmc20")) return "cmc20";
4317
- return path2;
4318
- };
4319
- var recordCoinMarketCapFetchStats = (stats, path2, creditCount) => {
4350
+ var getCoinMarketCapPathLabel = (path3) => {
4351
+ if (path3.includes("global-metrics")) return "global";
4352
+ if (path3.includes("cryptocurrency/quotes")) return "reference";
4353
+ if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
4354
+ if (path3.includes("fear-and-greed")) return "fearGreed";
4355
+ if (path3.includes("cmc100")) return "cmc100";
4356
+ if (path3.includes("cmc20")) return "cmc20";
4357
+ return path3;
4358
+ };
4359
+ var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
4320
4360
  if (!stats) return;
4321
- const existing = stats.byPath.get(path2) ?? { requests: 0, credits: 0 };
4361
+ const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
4322
4362
  existing.requests += 1;
4323
4363
  existing.credits += creditCount ?? 0;
4324
4364
  stats.requests += 1;
4325
4365
  stats.totalCredits += creditCount ?? 0;
4326
- stats.byPath.set(path2, existing);
4366
+ stats.byPath.set(path3, existing);
4327
4367
  };
4328
4368
  var formatCoinMarketCapFetchStats = (stats) => {
4329
4369
  const byPath = [...stats.byPath.entries()].map(
4330
- ([path2, item]) => `${getCoinMarketCapPathLabel(path2)}=${item.requests}req/${item.credits}cr`
4370
+ ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
4331
4371
  ).join(" ");
4332
4372
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
4333
4373
  };
@@ -5243,8 +5283,12 @@ var prepareMarketContextForRun = async (params) => {
5243
5283
  };
5244
5284
 
5245
5285
  // src/lib/backtest/runState.ts
5286
+ var import_types = require("@tradejs/types");
5246
5287
  var successTests = 0;
5247
5288
  var errorTests = 0;
5289
+ var warningCounts = {
5290
+ [import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
5291
+ };
5248
5292
  var errorMessages = [];
5249
5293
  var topResults = [];
5250
5294
  var bestTickerResults = /* @__PURE__ */ new Map();
@@ -5289,6 +5333,9 @@ var getRuntimeCompareContext = () => ({
5289
5333
  var resetRunState = () => {
5290
5334
  successTests = 0;
5291
5335
  errorTests = 0;
5336
+ for (const code of Object.values(import_types.BACKTEST_WARNING_CODES)) {
5337
+ warningCounts[code] = 0;
5338
+ }
5292
5339
  errorMessages.length = 0;
5293
5340
  topResults = [];
5294
5341
  bestTickerResults.clear();
@@ -5433,13 +5480,16 @@ var REPLAY_RUNTIME_COMPARISON_HEADERS = [
5433
5480
  import_chalk8.default.yellow("RT TRADES"),
5434
5481
  import_chalk8.default.yellow("RT PNL"),
5435
5482
  import_chalk8.default.green("MATCHED"),
5483
+ import_chalk8.default.red("ORDER FAILED"),
5436
5484
  import_chalk8.default.yellow("RT ONLY"),
5437
- import_chalk8.default.magenta("BT ONLY")
5485
+ import_chalk8.default.magenta("BT ONLY"),
5486
+ import_chalk8.default.red("UNMATCHED")
5438
5487
  ];
5439
5488
  var REPLAY_RESULTS_CONFIG = "replay";
5440
5489
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
5441
5490
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
5442
5491
  var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
5492
+ var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
5443
5493
  var buildReplayStrategyConfig = ({
5444
5494
  strategyConfig,
5445
5495
  interval
@@ -5504,12 +5554,13 @@ var compactReplaySignal = (signal) => {
5504
5554
  var import_chalk9 = __toESM(require("chalk"));
5505
5555
  var import_progress3 = __toESM(require("progress"));
5506
5556
  var import_backtest = require("@tradejs/core/backtest");
5507
- var import_strategies = require("@tradejs/core/strategies");
5557
+ var import_strategies2 = require("@tradejs/core/strategies");
5508
5558
  var import_time3 = require("@tradejs/core/time");
5509
5559
  var import_logger = require("@tradejs/infra/logger");
5560
+ var import_redis3 = require("@tradejs/infra/redis");
5510
5561
  var import_connectors6 = require("@tradejs/node/connectors");
5511
5562
  var import_cli4 = require("@tradejs/node/cli");
5512
- var import_strategies2 = require("@tradejs/node/strategies");
5563
+ var import_strategies3 = require("@tradejs/node/strategies");
5513
5564
 
5514
5565
  // src/lib/replay/portfolioReplayConnector.ts
5515
5566
  var import_constants3 = require("@tradejs/core/constants");
@@ -5959,21 +6010,231 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
5959
6010
  }
5960
6011
  };
5961
6012
 
6013
+ // src/lib/runtimeLineage.ts
6014
+ var import_node_child_process = require("child_process");
6015
+ var import_node_crypto = require("crypto");
6016
+ var import_promises = __toESM(require("fs/promises"));
6017
+ var import_node_path = __toESM(require("path"));
6018
+ var RUNTIME_CONTEXT_ENV_KEYS = [
6019
+ "AI_MODE",
6020
+ "MIN_AI_QUALITY",
6021
+ "INTERVAL",
6022
+ "DERIVATIVES_CONTEXT_ENABLED",
6023
+ "DERIVATIVES_CONTEXT_TARGET_ENABLED",
6024
+ "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
6025
+ "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
6026
+ "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
6027
+ "COINMARKETCAP_CONTEXT_ENABLED",
6028
+ "COINMARKETCAP_CONTEXT_MAX_AGE_MS",
6029
+ "COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
6030
+ "COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
6031
+ "COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
6032
+ "COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
6033
+ "COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
6034
+ "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
6035
+ "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
6036
+ ];
6037
+ var RUNTIME_CONTEXT_DATA_MODEL = {
6038
+ derivativesSourceIntervals: "15m",
6039
+ derivativesDerivedIntervals: "1h",
6040
+ derivativesHourlyFallback: "stored-1h",
6041
+ derivativesDataModelVersion: 2,
6042
+ coinMarketCapAvailabilityModel: "closed-interval-all-daily",
6043
+ coinMarketCapDataModelVersion: 3,
6044
+ binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
6045
+ binanceBreadthDataModelVersion: 2
6046
+ };
6047
+ var normalizeForStableJson = (value) => {
6048
+ if (Array.isArray(value)) {
6049
+ return value.map(normalizeForStableJson);
6050
+ }
6051
+ if (value && typeof value === "object") {
6052
+ return Object.fromEntries(
6053
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
6054
+ );
6055
+ }
6056
+ return value;
6057
+ };
6058
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
6059
+ var gitLineageCache = /* @__PURE__ */ new Map();
6060
+ var gateFingerprintCache = /* @__PURE__ */ new Map();
6061
+ var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
6062
+ var resolveGitLineage = (projectRoot) => {
6063
+ const cached = gitLineageCache.get(projectRoot);
6064
+ if (cached) {
6065
+ return cached;
6066
+ }
6067
+ const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
6068
+ if (envSha && envSha !== "unknown") {
6069
+ const result = {
6070
+ gitSha: envSha,
6071
+ gitDirty: false
6072
+ };
6073
+ gitLineageCache.set(projectRoot, result);
6074
+ return result;
6075
+ }
6076
+ try {
6077
+ const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
6078
+ cwd: projectRoot,
6079
+ encoding: "utf8",
6080
+ stdio: ["ignore", "pipe", "ignore"]
6081
+ }).trim();
6082
+ const status = (0, import_node_child_process.execFileSync)(
6083
+ "git",
6084
+ ["status", "--porcelain", "--untracked-files=normal"],
6085
+ {
6086
+ cwd: projectRoot,
6087
+ encoding: "utf8",
6088
+ stdio: ["ignore", "pipe", "ignore"]
6089
+ }
6090
+ ).trim();
6091
+ const result = {
6092
+ gitSha: gitSha || null,
6093
+ gitDirty: status.length > 0
6094
+ };
6095
+ gitLineageCache.set(projectRoot, result);
6096
+ return result;
6097
+ } catch {
6098
+ const result = {
6099
+ gitSha: null,
6100
+ gitDirty: null
6101
+ };
6102
+ gitLineageCache.set(projectRoot, result);
6103
+ return result;
6104
+ }
6105
+ };
6106
+ var readOptionalFile = async (filePath) => {
6107
+ try {
6108
+ return await import_promises.default.readFile(filePath);
6109
+ } catch {
6110
+ return null;
6111
+ }
6112
+ };
6113
+ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
6114
+ if (binanceBreadthFingerprintCache.has(projectRoot)) {
6115
+ return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
6116
+ }
6117
+ const content = await readOptionalFile(
6118
+ import_node_path.default.join(
6119
+ projectRoot,
6120
+ "packages/node/src/config/binanceBreadthUniverses.json"
6121
+ )
6122
+ );
6123
+ let result = null;
6124
+ if (content) {
6125
+ try {
6126
+ const parsed = JSON.parse(content.toString("utf8"));
6127
+ result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
6128
+ } catch {
6129
+ result = null;
6130
+ }
6131
+ }
6132
+ binanceBreadthFingerprintCache.set(projectRoot, result);
6133
+ return result;
6134
+ };
6135
+ var resolveGateFingerprint = async ({
6136
+ projectRoot,
6137
+ strategyName,
6138
+ gitSha
6139
+ }) => {
6140
+ const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
6141
+ const cached = gateFingerprintCache.get(cacheKey);
6142
+ if (cached) {
6143
+ return cached;
6144
+ }
6145
+ const relativeCandidates = [
6146
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
6147
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
6148
+ `packages/strategies/src/${strategyName}/pockets.ts`,
6149
+ `packages/strategies/src/${strategyName}/config.ts`,
6150
+ "packages/node/src/ai.ts"
6151
+ ];
6152
+ const optionalEntries = await Promise.all(
6153
+ relativeCandidates.map(async (relativePath) => {
6154
+ const content = await readOptionalFile(
6155
+ import_node_path.default.join(projectRoot, relativePath)
6156
+ );
6157
+ return content == null ? null : { relativePath, content };
6158
+ })
6159
+ );
6160
+ const sourceEntries = [];
6161
+ for (const entry of optionalEntries) {
6162
+ if (entry != null) {
6163
+ sourceEntries.push(entry);
6164
+ }
6165
+ }
6166
+ const hash = (0, import_node_crypto.createHash)("sha256");
6167
+ hash.update(strategyName);
6168
+ if (sourceEntries.length) {
6169
+ for (const entry of sourceEntries) {
6170
+ hash.update(entry.relativePath);
6171
+ hash.update(entry.content);
6172
+ }
6173
+ } else {
6174
+ hash.update(gitSha ?? "unknown-git-sha");
6175
+ }
6176
+ const fingerprint = hash.digest("hex").slice(0, 16);
6177
+ gateFingerprintCache.set(cacheKey, fingerprint);
6178
+ return fingerprint;
6179
+ };
6180
+ var buildRuntimeLineage = async ({
6181
+ projectRoot,
6182
+ strategyName,
6183
+ config,
6184
+ runContext,
6185
+ env = process.env
6186
+ }) => {
6187
+ const git = resolveGitLineage(projectRoot);
6188
+ const context = {
6189
+ ...Object.fromEntries(
6190
+ RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
6191
+ ),
6192
+ ...RUNTIME_CONTEXT_DATA_MODEL,
6193
+ binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
6194
+ ...runContext
6195
+ };
6196
+ return {
6197
+ schemaVersion: 1,
6198
+ ...git,
6199
+ gateFingerprint: await resolveGateFingerprint({
6200
+ projectRoot,
6201
+ strategyName,
6202
+ gitSha: git.gitSha
6203
+ }),
6204
+ configFingerprint: fingerprintRuntimeValue(config),
6205
+ contextFingerprint: fingerprintRuntimeValue(context)
6206
+ };
6207
+ };
6208
+ var runtimeLineageKey = (lineage) => [
6209
+ lineage.schemaVersion,
6210
+ lineage.gitSha ?? "unknown",
6211
+ lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
6212
+ lineage.gateFingerprint,
6213
+ lineage.configFingerprint,
6214
+ lineage.contextFingerprint
6215
+ ].join(":");
6216
+ var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
6217
+
5962
6218
  // src/lib/replay/historicalSignalsReplay.ts
5963
6219
  var loadRuntimeStrategies = async (runtimeStrategies) => {
5964
6220
  const strategies = await Promise.all(
5965
6221
  runtimeStrategies.map(async ({ strategyName, strategyConfig }) => {
5966
- const strategyCreator = await (0, import_strategies2.getStrategyCreator)(
6222
+ const strategyCreator = await (0, import_strategies3.getStrategyCreator)(
5967
6223
  strategyName,
5968
6224
  replayProjectRoot
5969
6225
  );
5970
6226
  if (!strategyCreator) {
5971
6227
  throw new Error(`Unknown strategy: ${strategyName}`);
5972
6228
  }
6229
+ const strategyResults = await (0, import_redis3.getData)(
6230
+ import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
6231
+ {}
6232
+ );
5973
6233
  return {
5974
6234
  strategyName,
5975
6235
  strategyCreator,
5976
- strategyConfig
6236
+ strategyConfig,
6237
+ strategyResults
5977
6238
  };
5978
6239
  })
5979
6240
  );
@@ -6074,6 +6335,7 @@ var runHistoricalSignalsReplay = async ({
6074
6335
  }) => {
6075
6336
  const startedAt = Date.now();
6076
6337
  const signals = [];
6338
+ const runtimeLineages = [];
6077
6339
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
6078
6340
  const projectHooks = projectConfig.hooks;
6079
6341
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
@@ -6161,26 +6423,54 @@ var runHistoricalSignalsReplay = async ({
6161
6423
  sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
6162
6424
  const strategiesForSymbol = await Promise.all(
6163
6425
  loadedStrategies.map(
6164
- async ({ strategyName, strategyCreator, strategyConfig }) => ({
6426
+ async ({
6165
6427
  strategyName,
6428
+ strategyCreator,
6166
6429
  strategyConfig,
6167
- run: await strategyCreator({
6168
- userName: replayUserName,
6169
- connectorName,
6170
- config: buildReplayStrategyConfig({
6430
+ strategyResults
6431
+ }) => {
6432
+ const runtimeLineage = await buildRuntimeLineage({
6433
+ projectRoot: replayProjectRoot,
6434
+ strategyName,
6435
+ config: {
6436
+ configId: "config",
6171
6437
  strategyConfig,
6172
- interval
6173
- }),
6438
+ symbolResultConfig: strategyResults?.[symbol]?.config ?? null
6439
+ },
6440
+ runContext: {
6441
+ connectorName: connectorName.toLowerCase(),
6442
+ interval: String(interval),
6443
+ universe: preparedRun.universe ?? null
6444
+ }
6445
+ });
6446
+ runtimeLineages.push({
6447
+ strategy: strategyName,
6174
6448
  symbol,
6175
- data: preparedData.prevData,
6176
- btcData: preparedData.btcPrevData,
6177
- ethData: preparedData.ethPrevData,
6178
- btcBinanceData: preparedData.btcBinancePrevData,
6179
- btcCoinbaseData: preparedData.btcCoinbasePrevData,
6180
- connector: replayConnector,
6181
- sharedIndicatorsReplayKey
6182
- })
6183
- })
6449
+ lineage: runtimeLineage
6450
+ });
6451
+ return {
6452
+ strategyName,
6453
+ strategyConfig,
6454
+ runtimeLineage,
6455
+ run: await strategyCreator({
6456
+ userName: replayUserName,
6457
+ connectorName,
6458
+ runtimeConfigId: "config",
6459
+ config: buildReplayStrategyConfig({
6460
+ strategyConfig,
6461
+ interval
6462
+ }),
6463
+ symbol,
6464
+ data: preparedData.prevData,
6465
+ btcData: preparedData.btcPrevData,
6466
+ ethData: preparedData.ethPrevData,
6467
+ btcBinanceData: preparedData.btcBinancePrevData,
6468
+ btcCoinbaseData: preparedData.btcCoinbasePrevData,
6469
+ connector: replayConnector,
6470
+ sharedIndicatorsReplayKey
6471
+ })
6472
+ };
6473
+ }
6184
6474
  )
6185
6475
  );
6186
6476
  const symbolRuntime = {
@@ -6274,7 +6564,8 @@ var runHistoricalSignalsReplay = async ({
6274
6564
  ethCandle?.timestamp === timestamp ? ethCandle : void 0
6275
6565
  );
6276
6566
  if (result && typeof result !== "string") {
6277
- await (0, import_strategies2.enrichSignalWithBinanceMarketContext)({
6567
+ result.runtimeLineage = strategyRuntime.runtimeLineage;
6568
+ await (0, import_strategies3.enrichSignalWithBinanceMarketContext)({
6278
6569
  signal: result,
6279
6570
  env: "PARITY"
6280
6571
  });
@@ -6298,8 +6589,8 @@ var runHistoricalSignalsReplay = async ({
6298
6589
  }
6299
6590
  } finally {
6300
6591
  for (const keyPrefix of sharedReplayKeyPrefixes) {
6301
- (0, import_strategies.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6302
- (0, import_strategies.releaseStrategyReplayCache)(keyPrefix);
6592
+ (0, import_strategies2.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6593
+ (0, import_strategies2.releaseStrategyReplayCache)(keyPrefix);
6303
6594
  }
6304
6595
  }
6305
6596
  const artifacts = replayConnector.getReplayArtifacts();
@@ -6333,7 +6624,8 @@ var runHistoricalSignalsReplay = async ({
6333
6624
  orderLog: artifacts.orderLog,
6334
6625
  positionLog: artifacts.positionLog,
6335
6626
  cycleCount: orderedTimestamps.length,
6336
- abortedCycles
6627
+ abortedCycles,
6628
+ runtimeLineages
6337
6629
  };
6338
6630
  };
6339
6631
 
@@ -6817,7 +7109,7 @@ var summarizeTradeParityByStrategy = ({
6817
7109
  };
6818
7110
 
6819
7111
  // src/lib/runtimeSignalsLoader.ts
6820
- var import_redis3 = require("@tradejs/infra/redis");
7112
+ var import_redis4 = require("@tradejs/infra/redis");
6821
7113
 
6822
7114
  // src/lib/runtimeSignalsStorage.ts
6823
7115
  var import_constants4 = require("@tradejs/core/constants");
@@ -6867,14 +7159,14 @@ var loadRuntimeSignals = async (userName, {
6867
7159
  startTime,
6868
7160
  endTime
6869
7161
  } = {}) => {
6870
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalBuckets(userName);
7162
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalBuckets(userName);
6871
7163
  const bucketKeys = filterBucketKeysByWindow(
6872
- await (0, import_redis3.getKeys)(bucketPrefix),
7164
+ await (0, import_redis4.getKeys)(bucketPrefix),
6873
7165
  bucketPrefix,
6874
7166
  startTime,
6875
7167
  endTime
6876
7168
  );
6877
- const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
7169
+ const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
6878
7170
  const seen = /* @__PURE__ */ new Set();
6879
7171
  const payloads = await Promise.all(
6880
7172
  refs.map(async (ref) => {
@@ -6883,8 +7175,8 @@ var loadRuntimeSignals = async (userName, {
6883
7175
  return null;
6884
7176
  }
6885
7177
  seen.add(dedupeKey);
6886
- const signal = await (0, import_redis3.getData)(
6887
- import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
7178
+ const signal = await (0, import_redis4.getData)(
7179
+ import_redis4.redisKeys.storeSignal(ref.symbol, ref.signalId),
6888
7180
  null
6889
7181
  );
6890
7182
  return isSignalRecord(signal) ? signal : null;
@@ -6896,25 +7188,42 @@ var loadRuntimeSignalEvaluations = async (userName, {
6896
7188
  startTime,
6897
7189
  endTime
6898
7190
  } = {}) => {
6899
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalEvaluationBuckets(userName);
7191
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName);
6900
7192
  const bucketKeys = filterBucketKeysByWindow(
6901
- await (0, import_redis3.getKeys)(bucketPrefix),
7193
+ await (0, import_redis4.getKeys)(bucketPrefix),
6902
7194
  bucketPrefix,
6903
7195
  startTime,
6904
7196
  endTime
6905
7197
  );
6906
- const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
7198
+ const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
6907
7199
  const deduped = /* @__PURE__ */ new Map();
6908
7200
  for (const evaluation of evaluations) {
6909
7201
  deduped.set(evaluation.evaluationId, evaluation);
6910
7202
  }
6911
7203
  return sortByTimestamp([...deduped.values()]);
6912
7204
  };
7205
+ var loadRuntimeLineageScopes = async (userName, {
7206
+ startTime,
7207
+ endTime
7208
+ }) => {
7209
+ const records = (await Promise.all(
7210
+ (0, import_time4.getRuntimeStorageDayKeys)(startTime, endTime).map(
7211
+ (dayKey) => (0, import_redis4.getHashJsonValues)(
7212
+ import_redis4.redisKeys.runtimeLineageScopeBucket(userName, dayKey)
7213
+ )
7214
+ )
7215
+ )).flat().filter(
7216
+ (record) => record != null && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.firstTimestamp === "number" && typeof record.lastTimestamp === "number" && record.lineage != null
7217
+ );
7218
+ return records.sort(
7219
+ (left, right) => left.firstTimestamp - right.firstTimestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
7220
+ );
7221
+ };
6913
7222
 
6914
7223
  // src/lib/runtimeTradeSync.ts
6915
7224
  var import_constants5 = require("@tradejs/core/constants");
6916
7225
  var import_time5 = require("@tradejs/core/time");
6917
- var import_redis4 = require("@tradejs/infra/redis");
7226
+ var import_redis5 = require("@tradejs/infra/redis");
6918
7227
  var CLOSED_PNL_LIMIT = 100;
6919
7228
  var CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
6920
7229
  var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -7008,6 +7317,18 @@ var consumeClosedPnlMatch = (buckets, trade) => {
7008
7317
  const [row] = rows.splice(matchIndex, 1);
7009
7318
  return row ?? null;
7010
7319
  };
7320
+ var isRuntimeTradeInConnectorScope = (trade, connector) => {
7321
+ if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
7322
+ return false;
7323
+ }
7324
+ if (trade.accountId && trade.accountId !== connector.accountId) {
7325
+ return false;
7326
+ }
7327
+ if (trade.universe && connector.universe && trade.universe !== connector.universe) {
7328
+ return false;
7329
+ }
7330
+ return true;
7331
+ };
7011
7332
  var syncRuntimeTrades = async ({
7012
7333
  userName,
7013
7334
  connector,
@@ -7031,16 +7352,23 @@ var syncRuntimeTrades = async ({
7031
7352
  const openPositionsBySymbol = new Map(
7032
7353
  openPositions.map((position) => [position.symbol, position])
7033
7354
  );
7034
- const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
7035
- const symbols = [...new Set(trades.map((trade) => trade.symbol))];
7355
+ const activeOrderIdByKey = /* @__PURE__ */ new Map();
7356
+ const activeTradeKeys = [
7357
+ ...new Set(
7358
+ trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map(
7359
+ (trade) => import_redis5.redisKeys.runtimeActiveTrade(
7360
+ userName,
7361
+ trade.symbol,
7362
+ trade.deploymentId ?? trade.accountId
7363
+ )
7364
+ )
7365
+ )
7366
+ ];
7036
7367
  await Promise.all(
7037
- symbols.map(async (symbol) => {
7038
- const activeRef = await (0, import_redis4.getData)(
7039
- import_redis4.redisKeys.runtimeActiveTrade(userName, symbol),
7040
- null
7041
- );
7042
- activeOrderIdBySymbol.set(
7043
- symbol,
7368
+ activeTradeKeys.map(async (key) => {
7369
+ const activeRef = await (0, import_redis5.getData)(key, null);
7370
+ activeOrderIdByKey.set(
7371
+ key,
7044
7372
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7045
7373
  );
7046
7374
  })
@@ -7059,12 +7387,21 @@ var syncRuntimeTrades = async ({
7059
7387
  }
7060
7388
  const syncedTrades = [];
7061
7389
  for (const trade of trades) {
7390
+ if (!isRuntimeTradeInConnectorScope(trade, connector)) {
7391
+ syncedTrades.push(trade);
7392
+ continue;
7393
+ }
7062
7394
  if (trade.status !== "active" && !isRuntimeTradeSyncFallbackClose(trade)) {
7063
7395
  syncedTrades.push(trade);
7064
7396
  continue;
7065
7397
  }
7398
+ const activeTradeKey = import_redis5.redisKeys.runtimeActiveTrade(
7399
+ userName,
7400
+ trade.symbol,
7401
+ trade.deploymentId ?? trade.accountId
7402
+ );
7066
7403
  const openPosition = openPositionsBySymbol.get(trade.symbol);
7067
- const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
7404
+ const activeOrderId = activeOrderIdByKey.get(activeTradeKey);
7068
7405
  const isCurrentActiveTrade = activeOrderId === trade.orderId;
7069
7406
  if (trade.status === "active" && !openPositionsReliable) {
7070
7407
  syncedTrades.push({
@@ -7082,15 +7419,15 @@ var syncRuntimeTrades = async ({
7082
7419
  currentPnl: openPosition.unrealizedPnl,
7083
7420
  lastSyncedAt: endTime
7084
7421
  };
7085
- await (0, import_redis4.setData)(
7086
- import_redis4.redisKeys.runtimeTrade(userName, trade.orderId),
7422
+ await (0, import_redis5.setData)(
7423
+ import_redis5.redisKeys.runtimeTrade(userName, trade.orderId),
7087
7424
  nextTrade2,
7088
7425
  {
7089
7426
  expire: 0
7090
7427
  }
7091
7428
  );
7092
- await (0, import_redis4.setHashJsonField)(
7093
- import_redis4.redisKeys.runtimeTradeBucket(
7429
+ await (0, import_redis5.setHashJsonField)(
7430
+ import_redis5.redisKeys.runtimeTradeBucket(
7094
7431
  userName,
7095
7432
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7096
7433
  ),
@@ -7103,6 +7440,7 @@ var syncRuntimeTrades = async ({
7103
7440
  }
7104
7441
  const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
7105
7442
  if (!matchedClosedPnl) {
7443
+ syncedTrades.push(trade);
7106
7444
  continue;
7107
7445
  }
7108
7446
  const nextTrade = {
@@ -7123,11 +7461,11 @@ var syncRuntimeTrades = async ({
7123
7461
  lastSyncedAt: endTime
7124
7462
  };
7125
7463
  await Promise.all([
7126
- (0, import_redis4.setData)(import_redis4.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7464
+ (0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7127
7465
  expire: import_constants5.TTL_1M
7128
7466
  }),
7129
- (0, import_redis4.setHashJsonField)(
7130
- import_redis4.redisKeys.runtimeTradeBucket(
7467
+ (0, import_redis5.setHashJsonField)(
7468
+ import_redis5.redisKeys.runtimeTradeBucket(
7131
7469
  userName,
7132
7470
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7133
7471
  ),
@@ -7135,7 +7473,7 @@ var syncRuntimeTrades = async ({
7135
7473
  nextTrade,
7136
7474
  { expire: import_constants5.TTL_1M }
7137
7475
  ),
7138
- ...isCurrentActiveTrade ? [(0, import_redis4.delKey)(import_redis4.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
7476
+ ...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
7139
7477
  ]);
7140
7478
  syncedTrades.push(nextTrade);
7141
7479
  }
@@ -7745,6 +8083,7 @@ var buildReplayRuntimeComparisonDetails = ({
7745
8083
  ),
7746
8084
  limit
7747
8085
  ),
8086
+ orderFailed: [],
7748
8087
  runtimeOnly: cappedRuntimeOnlyDetails,
7749
8088
  backtestOnly: cappedBacktestOnlyDetails,
7750
8089
  nearestCandidates: cappedNearestCandidates,
@@ -7764,6 +8103,7 @@ var buildReplayRuntimeComparisonDetails = ({
7764
8103
  };
7765
8104
  var buildReplayExchangeComparisonDetails = ({
7766
8105
  matched,
8106
+ orderFailed = [],
7767
8107
  exchangeOnly,
7768
8108
  backtestOnly,
7769
8109
  exchangeEntries,
@@ -7814,7 +8154,7 @@ var buildReplayExchangeComparisonDetails = ({
7814
8154
  limit
7815
8155
  );
7816
8156
  return {
7817
- capped: matched.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
8157
+ capped: matched.length > limit || orderFailed.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
7818
8158
  limit,
7819
8159
  matched: capReplayDetails(
7820
8160
  matched.map(
@@ -7830,6 +8170,27 @@ var buildReplayExchangeComparisonDetails = ({
7830
8170
  ),
7831
8171
  limit
7832
8172
  ),
8173
+ orderFailed: capReplayDetails(
8174
+ orderFailed.map((item) => {
8175
+ const detail = buildMatchedReplayDetail({
8176
+ runtime: toExchangeDetail(item.exchange),
8177
+ backtest: toBacktestParityDetail(
8178
+ item.backtest,
8179
+ backtestTimestampOffsetMs
8180
+ ),
8181
+ timestampDiffMs: item.timestampDiffMs,
8182
+ priceDeltaPct: item.priceDeltaPct
8183
+ });
8184
+ return {
8185
+ runtime: detail.runtime,
8186
+ backtest: detail.backtest,
8187
+ timestampDiffMs: detail.timestampDiffMs,
8188
+ priceDeltaPct: detail.priceDeltaPct,
8189
+ reason: item.reason
8190
+ };
8191
+ }),
8192
+ limit
8193
+ ),
7833
8194
  runtimeOnly: cappedExchangeOnlyDetails,
7834
8195
  backtestOnly: cappedBacktestOnlyDetails,
7835
8196
  nearestCandidates: cappedNearestCandidates,
@@ -7865,11 +8226,147 @@ var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
7865
8226
  reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
7866
8227
  signalId: signal.signalId,
7867
8228
  direction: signal.direction,
8229
+ runtimeLineage: signal.runtimeLineage,
7868
8230
  orderStatus: signal.orderStatus,
7869
8231
  orderSkipReason: signal.orderSkipReason,
7870
8232
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
7871
8233
  ...signal.ml ? { ml: signal.ml } : {}
7872
8234
  }));
8235
+ var lineageScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
8236
+ var buildExpectedLineageByScope = (replayLineages) => new Map(
8237
+ replayLineages.map((record) => [
8238
+ lineageScopeKey(record.strategy, record.symbol),
8239
+ record.lineage
8240
+ ])
8241
+ );
8242
+ var hasExpectedLineage = ({
8243
+ strategy,
8244
+ symbol,
8245
+ lineage,
8246
+ expectedByScope
8247
+ }) => runtimeLineagesMatch(
8248
+ lineage,
8249
+ expectedByScope.get(lineageScopeKey(strategy, symbol))
8250
+ );
8251
+ var filterReplayComparisonByLineage = ({
8252
+ replayLineages,
8253
+ runtimeTrades,
8254
+ runtimeSignals,
8255
+ runtimeSignalEvaluations,
8256
+ runtimeLineageScopes,
8257
+ backtestEntries
8258
+ }) => {
8259
+ const expectedByScope = buildExpectedLineageByScope(replayLineages);
8260
+ const comparableRuntimeSignals = runtimeSignals.filter(
8261
+ (signal) => hasExpectedLineage({
8262
+ strategy: signal.strategy,
8263
+ symbol: signal.symbol,
8264
+ lineage: signal.runtimeLineage,
8265
+ expectedByScope
8266
+ })
8267
+ );
8268
+ const comparableRuntimeEvaluations = runtimeSignalEvaluations.filter(
8269
+ (evaluation) => hasExpectedLineage({
8270
+ strategy: evaluation.strategy,
8271
+ symbol: evaluation.symbol,
8272
+ lineage: evaluation.runtimeLineage,
8273
+ expectedByScope
8274
+ })
8275
+ );
8276
+ const comparableRuntimeLineageScopes = runtimeLineageScopes.filter(
8277
+ (scope) => hasExpectedLineage({
8278
+ strategy: scope.strategy,
8279
+ symbol: scope.symbol,
8280
+ lineage: scope.lineage,
8281
+ expectedByScope
8282
+ })
8283
+ );
8284
+ const deploymentWindows = /* @__PURE__ */ new Map();
8285
+ for (const scope of comparableRuntimeLineageScopes) {
8286
+ const key = lineageScopeKey(scope.strategy, scope.symbol);
8287
+ const existing = deploymentWindows.get(key);
8288
+ deploymentWindows.set(key, {
8289
+ firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8290
+ lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8291
+ });
8292
+ }
8293
+ const lineageBySignalId = /* @__PURE__ */ new Map();
8294
+ for (const signal of runtimeSignals) {
8295
+ if (signal.runtimeLineage) {
8296
+ lineageBySignalId.set(signal.signalId, signal.runtimeLineage);
8297
+ }
8298
+ }
8299
+ for (const evaluation of runtimeSignalEvaluations) {
8300
+ if (evaluation.signalId && evaluation.runtimeLineage) {
8301
+ lineageBySignalId.set(evaluation.signalId, evaluation.runtimeLineage);
8302
+ }
8303
+ }
8304
+ const comparableRuntimeTrades = runtimeTrades.filter((trade) => {
8305
+ const lineage = trade.signalId ? lineageBySignalId.get(trade.signalId) : void 0;
8306
+ if (lineage) {
8307
+ return hasExpectedLineage({
8308
+ strategy: trade.strategy,
8309
+ symbol: trade.symbol,
8310
+ lineage,
8311
+ expectedByScope
8312
+ });
8313
+ }
8314
+ const window2 = deploymentWindows.get(
8315
+ lineageScopeKey(trade.strategy, trade.symbol)
8316
+ );
8317
+ const signalTimestamp = trade.signalTimestamp ?? trade.entryTimestamp;
8318
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8319
+ });
8320
+ for (const artifact of [
8321
+ ...comparableRuntimeSignals,
8322
+ ...comparableRuntimeEvaluations
8323
+ ]) {
8324
+ const key = lineageScopeKey(artifact.strategy, artifact.symbol);
8325
+ const existing = deploymentWindows.get(key);
8326
+ deploymentWindows.set(key, {
8327
+ firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8328
+ lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8329
+ });
8330
+ }
8331
+ const comparableBacktestEntries = backtestEntries.filter((entry) => {
8332
+ const window2 = deploymentWindows.get(
8333
+ lineageScopeKey(entry.strategy, entry.symbol)
8334
+ );
8335
+ const signalTimestamp = entry.signalTimestamp ?? entry.timestamp;
8336
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8337
+ });
8338
+ const comparableScopeKeys = new Set(deploymentWindows.keys());
8339
+ return {
8340
+ runtimeTrades: comparableRuntimeTrades,
8341
+ runtimeSignals: comparableRuntimeSignals,
8342
+ runtimeSignalEvaluations: comparableRuntimeEvaluations,
8343
+ backtestEntries: comparableBacktestEntries,
8344
+ lineage: {
8345
+ enforced: true,
8346
+ replayScopes: expectedByScope.size,
8347
+ comparableScopes: comparableScopeKeys.size,
8348
+ excludedRuntimeTrades: runtimeTrades.length - comparableRuntimeTrades.length,
8349
+ excludedRuntimeSignals: runtimeSignals.length - comparableRuntimeSignals.length,
8350
+ excludedRuntimeEvaluations: runtimeSignalEvaluations.length - comparableRuntimeEvaluations.length,
8351
+ excludedRuntimeLineageScopes: runtimeLineageScopes.length - comparableRuntimeLineageScopes.length,
8352
+ excludedExchangeEntries: 0,
8353
+ excludedBacktestEntries: backtestEntries.length - comparableBacktestEntries.length,
8354
+ reason: comparableScopeKeys.size > 0 ? null : "no_runtime_artifacts_with_matching_lineage",
8355
+ replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
8356
+ const separator = scope.indexOf("::");
8357
+ return {
8358
+ strategy: scope.slice(0, separator),
8359
+ symbol: scope.slice(separator + 2),
8360
+ lineage
8361
+ };
8362
+ }).sort(
8363
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
8364
+ runtimeLineageKey(right.lineage)
8365
+ )
8366
+ )
8367
+ }
8368
+ };
8369
+ };
7873
8370
  var formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
7874
8371
  var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7875
8372
  var sumOptionalNumbers = (values) => {
@@ -7893,6 +8390,14 @@ var firstFiniteNumber = (values) => {
7893
8390
  return null;
7894
8391
  };
7895
8392
  var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
8393
+ var summarizeBacktestPnlByStrategy = (entries) => {
8394
+ const totals = /* @__PURE__ */ new Map();
8395
+ for (const entry of entries) {
8396
+ const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
8397
+ totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
8398
+ }
8399
+ return totals;
8400
+ };
7896
8401
  var getExchangeExecutionMergeKey = (entry) => {
7897
8402
  const orderId = firstString([entry.orderId, entry.orderLinkId]);
7898
8403
  return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
@@ -8159,16 +8664,89 @@ var compareExchangeEntriesToBacktest = ({
8159
8664
  backtestOnly
8160
8665
  };
8161
8666
  };
8667
+ var findExchangeRuntimeOutcome = ({
8668
+ exchangeEntry,
8669
+ strategyName,
8670
+ runtimeSignals,
8671
+ runtimeSignalEvaluations,
8672
+ toleranceMs,
8673
+ signalTimestampOffsetMs
8674
+ }) => {
8675
+ const candidates = [];
8676
+ for (const signal of runtimeSignals) {
8677
+ if (signal.strategy === strategyName && signal.symbol === exchangeEntry.symbol && signal.direction === exchangeEntry.direction) {
8678
+ candidates.push({
8679
+ timestamp: signal.timestamp,
8680
+ orderStatus: signal.orderStatus,
8681
+ reason: signal.orderFailureReason || signal.orderSkipReason || signal.orderStatus
8682
+ });
8683
+ }
8684
+ }
8685
+ for (const evaluation of runtimeSignalEvaluations) {
8686
+ if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
8687
+ candidates.push({
8688
+ timestamp: evaluation.timestamp,
8689
+ orderStatus: evaluation.orderStatus,
8690
+ reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
8691
+ });
8692
+ }
8693
+ }
8694
+ return candidates.map((candidate) => ({
8695
+ ...candidate,
8696
+ timestampDiffMs: Math.abs(
8697
+ candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
8698
+ )
8699
+ })).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
8700
+ (left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
8701
+ )[0] ?? null;
8702
+ };
8703
+ var splitExchangeMatchesByRuntimeOrderStatus = ({
8704
+ matched,
8705
+ strategyNameByOrderLinkKey,
8706
+ runtimeSignals,
8707
+ runtimeSignalEvaluations,
8708
+ toleranceMs,
8709
+ signalTimestampOffsetMs
8710
+ }) => {
8711
+ const orderFailed = [];
8712
+ const completed = matched.filter((item) => {
8713
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8714
+ exchangeEntry: item.exchange,
8715
+ strategyNameByOrderLinkKey
8716
+ });
8717
+ if (!strategyName) {
8718
+ return true;
8719
+ }
8720
+ const outcome = findExchangeRuntimeOutcome({
8721
+ exchangeEntry: item.exchange,
8722
+ strategyName,
8723
+ runtimeSignals,
8724
+ runtimeSignalEvaluations,
8725
+ toleranceMs,
8726
+ signalTimestampOffsetMs
8727
+ });
8728
+ if (outcome?.orderStatus !== "failed") {
8729
+ return true;
8730
+ }
8731
+ orderFailed.push({
8732
+ ...item,
8733
+ reason: outcome.reason || "orderStatus=failed"
8734
+ });
8735
+ return false;
8736
+ });
8737
+ return { completed, orderFailed };
8738
+ };
8162
8739
  var saveAndPrintReplayExchangeComparison = async ({
8163
8740
  liveStrategySummaries,
8164
8741
  backtestEntries,
8165
8742
  replaySignals,
8166
8743
  replaySignalEvaluations,
8167
8744
  runtimeSignals,
8168
- runtimeSignalEvaluations
8745
+ runtimeSignalEvaluations,
8746
+ lineage
8169
8747
  }) => {
8170
8748
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8171
- const exchangeEntries = await loadExchangeEntriesForComparison({
8749
+ const rawExchangeEntries = await loadExchangeEntriesForComparison({
8172
8750
  connector,
8173
8751
  startTime: window2.start,
8174
8752
  endTime: window2.end
@@ -8176,11 +8754,36 @@ var saveAndPrintReplayExchangeComparison = async ({
8176
8754
  const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
8177
8755
  liveStrategySummaries.map((summary) => summary.strategyName)
8178
8756
  );
8757
+ const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
8758
+ const exchangeEntries = rawExchangeEntries.filter((entry) => {
8759
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8760
+ exchangeEntry: entry,
8761
+ strategyNameByOrderLinkKey
8762
+ });
8763
+ if (!strategyName) {
8764
+ return false;
8765
+ }
8766
+ const outcome = findExchangeRuntimeOutcome({
8767
+ exchangeEntry: entry,
8768
+ strategyName,
8769
+ runtimeSignals,
8770
+ runtimeSignalEvaluations,
8771
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8772
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8773
+ });
8774
+ exchangeOutcomeByEntry.set(entry, outcome);
8775
+ return outcome != null;
8776
+ });
8777
+ const comparisonLineage = {
8778
+ ...lineage,
8779
+ excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
8780
+ };
8781
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
8179
8782
  if (!exchangeEntries.length) {
8180
8783
  console.log("");
8181
8784
  console.log(
8182
8785
  import_chalk11.default.yellow(
8183
- `SIGNALS REPLAY VS EXCHANGE: no exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8786
+ `SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8184
8787
  window2.start
8185
8788
  )} -> ${(0, import_time6.formatUnix)(window2.end)}`
8186
8789
  )
@@ -8207,18 +8810,21 @@ var saveAndPrintReplayExchangeComparison = async ({
8207
8810
  runtimeEntriesCount: 0,
8208
8811
  backtestEntriesCount: backtestEntries.length,
8209
8812
  matchedCount: 0,
8813
+ orderFailedCount: 0,
8210
8814
  runtimeOnlyCount: 0,
8211
8815
  backtestOnlyCount: backtestEntries.length,
8212
8816
  details: details2,
8817
+ lineage: comparisonLineage,
8213
8818
  rows: liveStrategySummaries.map((summary) => ({
8214
8819
  strategyName: summary.strategyName,
8215
8820
  backtestEntries: backtestEntries.filter(
8216
8821
  (entry) => entry.strategy === summary.strategyName
8217
8822
  ).length,
8218
- backtestNetProfit: summary.netProfit,
8823
+ backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
8219
8824
  runtimeTrades: 0,
8220
8825
  runtimePnl: 0,
8221
8826
  matched: 0,
8827
+ orderFailed: 0,
8222
8828
  runtimeOnly: 0,
8223
8829
  backtestOnly: backtestEntries.filter(
8224
8830
  (entry) => entry.strategy === summary.strategyName
@@ -8232,6 +8838,14 @@ var saveAndPrintReplayExchangeComparison = async ({
8232
8838
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8233
8839
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8234
8840
  });
8841
+ const { completed: matched, orderFailed } = splitExchangeMatchesByRuntimeOrderStatus({
8842
+ matched: comparison.matched,
8843
+ strategyNameByOrderLinkKey,
8844
+ runtimeSignals,
8845
+ runtimeSignalEvaluations,
8846
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8847
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8848
+ });
8235
8849
  const liveSummaryByStrategy = new Map(
8236
8850
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8237
8851
  );
@@ -8244,10 +8858,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8244
8858
  const next = {
8245
8859
  strategyName,
8246
8860
  backtestEntries: 0,
8247
- backtestNetProfit: liveSummaryByStrategy.get(strategyName)?.netProfit ?? 0,
8861
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8248
8862
  runtimeTrades: 0,
8249
8863
  runtimePnl: 0,
8250
8864
  matched: 0,
8865
+ orderFailed: 0,
8251
8866
  runtimeOnly: 0,
8252
8867
  backtestOnly: 0
8253
8868
  };
@@ -8260,7 +8875,7 @@ var saveAndPrintReplayExchangeComparison = async ({
8260
8875
  for (const entry of backtestEntries) {
8261
8876
  ensureRow(entry.strategy).backtestEntries += 1;
8262
8877
  }
8263
- for (const item of comparison.matched) {
8878
+ for (const item of matched) {
8264
8879
  const row = ensureRow(item.backtest.strategy);
8265
8880
  row.runtimeTrades += 1;
8266
8881
  row.matched += 1;
@@ -8268,6 +8883,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8268
8883
  row.runtimePnl += item.exchange.closedPnl;
8269
8884
  }
8270
8885
  }
8886
+ for (const item of orderFailed) {
8887
+ const row = ensureRow(item.backtest.strategy);
8888
+ row.runtimeTrades += 1;
8889
+ row.orderFailed += 1;
8890
+ }
8271
8891
  for (const entry of comparison.backtestOnly) {
8272
8892
  ensureRow(entry.strategy).backtestOnly += 1;
8273
8893
  }
@@ -8286,7 +8906,8 @@ var saveAndPrintReplayExchangeComparison = async ({
8286
8906
  }
8287
8907
  }
8288
8908
  const details = buildReplayExchangeComparisonDetails({
8289
- matched: comparison.matched,
8909
+ matched,
8910
+ orderFailed,
8290
8911
  exchangeOnly: comparison.exchangeOnly,
8291
8912
  backtestOnly: comparison.backtestOnly,
8292
8913
  exchangeEntries,
@@ -8313,8 +8934,10 @@ var saveAndPrintReplayExchangeComparison = async ({
8313
8934
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8314
8935
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8315
8936
  import_chalk11.default.green(String(row.matched)),
8937
+ import_chalk11.default.red(String(row.orderFailed)),
8316
8938
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8317
- import_chalk11.default.magenta(String(row.backtestOnly))
8939
+ import_chalk11.default.magenta(String(row.backtestOnly)),
8940
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8318
8941
  ];
8319
8942
  });
8320
8943
  console.log("");
@@ -8342,17 +8965,20 @@ var saveAndPrintReplayExchangeComparison = async ({
8342
8965
  windowTradesCount: exchangeEntries.length,
8343
8966
  runtimeEntriesCount: exchangeEntries.length,
8344
8967
  backtestEntriesCount: backtestEntries.length,
8345
- matchedCount: comparison.matched.length,
8968
+ matchedCount: matched.length,
8969
+ orderFailedCount: orderFailed.length,
8346
8970
  runtimeOnlyCount: comparison.exchangeOnly.length,
8347
8971
  backtestOnlyCount: comparison.backtestOnly.length,
8348
8972
  rows,
8349
- details
8973
+ details,
8974
+ lineage: comparisonLineage
8350
8975
  };
8351
8976
  };
8352
8977
  var saveAndPrintReplayRuntimeComparison = async ({
8353
8978
  liveStrategySummaries,
8354
8979
  backtestEntries,
8355
- replaySignals
8980
+ replaySignals,
8981
+ replayLineages
8356
8982
  }) => {
8357
8983
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8358
8984
  if (!connector || !window2) {
@@ -8362,8 +8988,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8362
8988
  liveStrategySummaries.map((summary) => summary.strategyName)
8363
8989
  );
8364
8990
  const replaySignalEvaluations = buildReplaySignalEvaluations(replaySignals);
8365
- const [rawRuntimeTrades, runtimeSignals, runtimeSignalEvaluations] = await Promise.all([
8366
- loadRuntimeTrades(replayUserName),
8991
+ const [
8992
+ rawRuntimeTrades,
8993
+ runtimeSignals,
8994
+ runtimeSignalEvaluations,
8995
+ runtimeLineageScopes
8996
+ ] = await Promise.all([
8997
+ loadRuntimeTrades(replayUserName, {
8998
+ startTime: window2.start,
8999
+ endTime: window2.end
9000
+ }),
8367
9001
  loadRuntimeSignals(replayUserName, {
8368
9002
  startTime: window2.start,
8369
9003
  endTime: window2.end
@@ -8371,6 +9005,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8371
9005
  loadRuntimeSignalEvaluations(replayUserName, {
8372
9006
  startTime: window2.start,
8373
9007
  endTime: window2.end
9008
+ }),
9009
+ loadRuntimeLineageScopes(replayUserName, {
9010
+ startTime: window2.start,
9011
+ endTime: window2.end
8374
9012
  })
8375
9013
  ]);
8376
9014
  const syncedRuntimeTrades = await syncRuntimeTrades({
@@ -8412,7 +9050,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8412
9050
  }
8413
9051
  }
8414
9052
  });
8415
- const windowRuntimeTrades = syncedRuntimeTrades.filter(
9053
+ const comparable = filterReplayComparisonByLineage({
9054
+ replayLineages,
9055
+ runtimeTrades: syncedRuntimeTrades,
9056
+ runtimeSignals,
9057
+ runtimeSignalEvaluations,
9058
+ runtimeLineageScopes,
9059
+ backtestEntries
9060
+ });
9061
+ const comparableBacktestEntries = comparable.backtestEntries;
9062
+ const windowRuntimeTrades = comparable.runtimeTrades.filter(
8416
9063
  (trade) => trade.entryTimestamp >= window2.start && trade.entryTimestamp < window2.end && relevantStrategies.has(trade.strategy)
8417
9064
  );
8418
9065
  if (!windowRuntimeTrades.length) {
@@ -8421,17 +9068,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8421
9068
  import_chalk11.default.yellow(
8422
9069
  `SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
8423
9070
  window2.start
8424
- )} -> ${(0, import_time6.formatUnix)(window2.end)}; falling back to direct exchange comparison`
9071
+ )} -> ${(0, import_time6.formatUnix)(window2.end)} with matching lineage; checking lineage-linked exchange executions`
8425
9072
  )
8426
9073
  );
8427
9074
  console.log("");
8428
9075
  return saveAndPrintReplayExchangeComparison({
8429
9076
  liveStrategySummaries,
8430
- backtestEntries,
9077
+ backtestEntries: comparableBacktestEntries,
8431
9078
  replaySignals,
8432
9079
  replaySignalEvaluations,
8433
- runtimeSignals,
8434
- runtimeSignalEvaluations
9080
+ runtimeSignals: comparable.runtimeSignals,
9081
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
9082
+ lineage: comparable.lineage
8435
9083
  });
8436
9084
  }
8437
9085
  const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
@@ -8442,7 +9090,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8442
9090
  const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
8443
9091
  const comparison = compareTradeParityEntries({
8444
9092
  runtimeEntries: runtimeDedupe.entries,
8445
- backtestEntries,
9093
+ backtestEntries: comparableBacktestEntries,
8446
9094
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8447
9095
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8448
9096
  });
@@ -8451,18 +9099,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8451
9099
  runtimeOnly: comparison.runtimeOnly,
8452
9100
  backtestOnly: comparison.backtestOnly,
8453
9101
  runtimeEntries: runtimeDedupe.entries,
8454
- backtestEntries,
9102
+ backtestEntries: comparableBacktestEntries,
8455
9103
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8456
9104
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
8457
- runtimeSignals,
8458
- runtimeSignalEvaluations,
9105
+ runtimeSignals: comparable.runtimeSignals,
9106
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
8459
9107
  replaySignals,
8460
9108
  replaySignalEvaluations
8461
9109
  });
8462
9110
  const parityRows = summarizeTradeParityByStrategy({
8463
9111
  runtimeEntries: runtimeDedupe.entries,
8464
9112
  runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
8465
- backtestEntries,
9113
+ backtestEntries: comparableBacktestEntries,
8466
9114
  matchedEntries: comparison.matched,
8467
9115
  runtimeOnlyEntries: comparison.runtimeOnly,
8468
9116
  backtestOnlyEntries: comparison.backtestOnly
@@ -8471,22 +9119,25 @@ var saveAndPrintReplayRuntimeComparison = async ({
8471
9119
  const liveSummaryByStrategy = new Map(
8472
9120
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8473
9121
  );
9122
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(
9123
+ comparableBacktestEntries
9124
+ );
8474
9125
  const strategyNames = /* @__PURE__ */ new Set([
8475
9126
  ...liveSummaryByStrategy.keys(),
8476
9127
  ...runtimeSummaryByStrategy.keys(),
8477
9128
  ...parityByStrategy.keys()
8478
9129
  ]);
8479
9130
  const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
8480
- const liveSummary = liveSummaryByStrategy.get(strategyName);
8481
9131
  const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
8482
9132
  const parity = parityByStrategy.get(strategyName);
8483
9133
  return {
8484
9134
  strategyName,
8485
9135
  backtestEntries: parity?.backtest ?? 0,
8486
- backtestNetProfit: liveSummary?.netProfit ?? 0,
9136
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8487
9137
  runtimeTrades: runtimeSummary?.trades ?? 0,
8488
9138
  runtimePnl: runtimeSummary?.totalPnl ?? 0,
8489
9139
  matched: parity?.matched ?? 0,
9140
+ orderFailed: 0,
8490
9141
  runtimeOnly: parity?.runtimeOnly ?? 0,
8491
9142
  backtestOnly: parity?.backtestOnly ?? 0
8492
9143
  };
@@ -8501,8 +9152,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8501
9152
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8502
9153
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8503
9154
  import_chalk11.default.green(String(row.matched)),
9155
+ import_chalk11.default.red(String(row.orderFailed)),
8504
9156
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8505
- import_chalk11.default.magenta(String(row.backtestOnly))
9157
+ import_chalk11.default.magenta(String(row.backtestOnly)),
9158
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8506
9159
  ];
8507
9160
  });
8508
9161
  console.log("");
@@ -8529,18 +9182,20 @@ var saveAndPrintReplayRuntimeComparison = async ({
8529
9182
  syncedTradesCount: syncedRuntimeTrades.length,
8530
9183
  windowTradesCount: windowRuntimeTrades.length,
8531
9184
  runtimeEntriesCount: runtimeDedupe.entries.length,
8532
- backtestEntriesCount: backtestEntries.length,
9185
+ backtestEntriesCount: comparableBacktestEntries.length,
8533
9186
  matchedCount: comparison.matched.length,
9187
+ orderFailedCount: 0,
8534
9188
  runtimeOnlyCount: comparison.runtimeOnly.length,
8535
9189
  backtestOnlyCount: comparison.backtestOnly.length,
8536
9190
  rows,
8537
- details
9191
+ details,
9192
+ lineage: comparable.lineage
8538
9193
  };
8539
9194
  };
8540
9195
 
8541
9196
  // src/lib/replay/outputReport.ts
8542
- var import_promises = __toESM(require("fs/promises"));
8543
- var import_node_path = __toESM(require("path"));
9197
+ var import_promises2 = __toESM(require("fs/promises"));
9198
+ var import_node_path2 = __toESM(require("path"));
8544
9199
  var import_data = require("@tradejs/core/data");
8545
9200
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8546
9201
  timeZone: "Europe/Moscow",
@@ -8575,6 +9230,7 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8575
9230
  if (!details) {
8576
9231
  return {
8577
9232
  matched: [],
9233
+ orderFailed: [],
8578
9234
  runtimeOnly: [],
8579
9235
  backtestOnly: []
8580
9236
  };
@@ -8603,6 +9259,17 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8603
9259
  slippage: item.slippage
8604
9260
  }
8605
9261
  })),
9262
+ orderFailed: details.orderFailed.map((item) => ({
9263
+ status: "orderFailed",
9264
+ strategy: entryStrategy(item.backtest),
9265
+ symbol: item.backtest.symbol,
9266
+ direction: item.backtest.direction,
9267
+ runtime: item.runtime,
9268
+ backtest: item.backtest,
9269
+ reason: item.reason,
9270
+ timestampDiffMs: item.timestampDiffMs,
9271
+ entryPriceDeltaPct: item.priceDeltaPct
9272
+ })),
8606
9273
  runtimeOnly: details.runtimeOnly.map((entry) => ({
8607
9274
  status: "runtimeOnly",
8608
9275
  strategy: entryStrategy(entry),
@@ -8686,6 +9353,26 @@ var renderRuntimeOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8686
9353
  ];
8687
9354
  })
8688
9355
  ) : "_No runtime-only trades._";
9356
+ var renderOrderFailedRows = (perTrade) => perTrade.length ? createMarkdownTable(
9357
+ [
9358
+ "Strategy",
9359
+ "Symbol",
9360
+ "Dir",
9361
+ "BT time",
9362
+ "RT time",
9363
+ "Entry delta",
9364
+ "Reason"
9365
+ ],
9366
+ perTrade.map((item) => [
9367
+ item.strategy,
9368
+ item.symbol,
9369
+ item.direction,
9370
+ formatMsk(item.backtest.timestamp),
9371
+ formatMsk(item.runtime.timestamp),
9372
+ formatPct(item.entryPriceDeltaPct),
9373
+ item.reason
9374
+ ])
9375
+ ) : "_No failed runtime orders._";
8689
9376
  var renderBacktestOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8690
9377
  [
8691
9378
  "Strategy",
@@ -8723,8 +9410,10 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8723
9410
  "RT trades",
8724
9411
  "RT PnL",
8725
9412
  "Matched",
9413
+ "Order failed",
8726
9414
  "RT only",
8727
- "BT only"
9415
+ "BT only",
9416
+ "Unmatched"
8728
9417
  ],
8729
9418
  runtimeComparison.rows.map((row) => [
8730
9419
  row.strategyName,
@@ -8733,10 +9422,69 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8733
9422
  row.runtimeTrades,
8734
9423
  formatMoney(row.runtimePnl),
8735
9424
  row.matched,
9425
+ row.orderFailed,
8736
9426
  row.runtimeOnly,
8737
- row.backtestOnly
9427
+ row.backtestOnly,
9428
+ getReplayRuntimeUnmatchedCount(row)
8738
9429
  ])
8739
9430
  ) : "_No runtime comparison rows._";
9431
+ var renderLineageRows = (runtimeComparison) => {
9432
+ const grouped = /* @__PURE__ */ new Map();
9433
+ for (const record of runtimeComparison?.lineage.replay ?? []) {
9434
+ const existing = grouped.get(record.strategy) ?? {
9435
+ strategy: record.strategy,
9436
+ scopes: 0,
9437
+ gitShas: /* @__PURE__ */ new Set(),
9438
+ dirty: /* @__PURE__ */ new Set(),
9439
+ gates: /* @__PURE__ */ new Set(),
9440
+ configs: /* @__PURE__ */ new Set(),
9441
+ contexts: /* @__PURE__ */ new Set()
9442
+ };
9443
+ existing.scopes += 1;
9444
+ existing.gitShas.add(record.lineage.gitSha ?? "unknown");
9445
+ existing.dirty.add(
9446
+ record.lineage.gitDirty == null ? "unknown" : String(record.lineage.gitDirty)
9447
+ );
9448
+ existing.gates.add(record.lineage.gateFingerprint);
9449
+ existing.configs.add(record.lineage.configFingerprint);
9450
+ existing.contexts.add(record.lineage.contextFingerprint);
9451
+ grouped.set(record.strategy, existing);
9452
+ }
9453
+ const formatSet = (values) => {
9454
+ const sorted = [...values].sort();
9455
+ return sorted.length <= 3 ? sorted.join(", ") : `${sorted.slice(0, 3).join(", ")} \u2026 (+${sorted.length - 3})`;
9456
+ };
9457
+ return grouped.size ? createMarkdownTable(
9458
+ [
9459
+ "Strategy",
9460
+ "Scopes",
9461
+ "Git SHA",
9462
+ "Dirty",
9463
+ "Gate",
9464
+ "Config variants",
9465
+ "Context"
9466
+ ],
9467
+ [...grouped.values()].sort((left, right) => left.strategy.localeCompare(right.strategy)).map(
9468
+ ({
9469
+ strategy,
9470
+ scopes,
9471
+ gitShas,
9472
+ dirty,
9473
+ gates,
9474
+ configs,
9475
+ contexts
9476
+ }) => [
9477
+ strategy,
9478
+ scopes,
9479
+ formatSet(gitShas),
9480
+ formatSet(dirty),
9481
+ formatSet(gates),
9482
+ `${configs.size}: ${formatSet(configs)}`,
9483
+ formatSet(contexts)
9484
+ ]
9485
+ )
9486
+ ) : "_No replay lineage records._";
9487
+ };
8740
9488
  var writeReplayOutputReport = async ({
8741
9489
  projectRoot,
8742
9490
  timestamp,
@@ -8751,8 +9499,8 @@ var writeReplayOutputReport = async ({
8751
9499
  strategySnapshot,
8752
9500
  runtimeComparison
8753
9501
  }) => {
8754
- const outputDir = import_node_path.default.resolve(projectRoot, "data/replay/output");
8755
- await import_promises.default.mkdir(outputDir, { recursive: true });
9502
+ const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
9503
+ await import_promises2.default.mkdir(outputDir, { recursive: true });
8756
9504
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
8757
9505
  const report = {
8758
9506
  reportType: "replay-output",
@@ -8778,15 +9526,17 @@ var writeReplayOutputReport = async ({
8778
9526
  cycleCount: replayResult.cycleCount,
8779
9527
  abortedCycles: replayResult.abortedCycles,
8780
9528
  matched: runtimeComparison?.matchedCount ?? 0,
9529
+ orderFailed: runtimeComparison?.orderFailedCount ?? 0,
8781
9530
  runtimeOnly: runtimeComparison?.runtimeOnlyCount ?? 0,
8782
9531
  backtestOnly: runtimeComparison?.backtestOnlyCount ?? 0
8783
9532
  },
8784
9533
  resultsByStrategies: strategySnapshot.summaries,
9534
+ replayLineage: replayResult.runtimeLineages,
8785
9535
  runtimeComparison,
8786
9536
  perTradeAnalysis
8787
9537
  };
8788
- const markdownPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.md`);
8789
- const jsonPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.json`);
9538
+ const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
9539
+ const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
8790
9540
  const lines = [
8791
9541
  "# Replay Report",
8792
9542
  "",
@@ -8806,13 +9556,26 @@ var writeReplayOutputReport = async ({
8806
9556
  ["Window", `${formatMsk(window2.start)} - ${formatMsk(window2.end)} MSK`],
8807
9557
  ["Runtime compare mode", runtimeComparison?.mode ?? "none"],
8808
9558
  ["Matched", runtimeComparison?.matchedCount ?? 0],
9559
+ ["Order failed", runtimeComparison?.orderFailedCount ?? 0],
8809
9560
  ["Runtime-only", runtimeComparison?.runtimeOnlyCount ?? 0],
8810
9561
  ["Backtest-only", runtimeComparison?.backtestOnlyCount ?? 0],
9562
+ [
9563
+ "Comparable lineage scopes",
9564
+ runtimeComparison ? `${runtimeComparison.lineage.comparableScopes}/${runtimeComparison.lineage.replayScopes}` : "0/0"
9565
+ ],
9566
+ [
9567
+ "Lineage exclusions",
9568
+ runtimeComparison ? `runtime=${runtimeComparison.lineage.excludedRuntimeTrades}, exchange=${runtimeComparison.lineage.excludedExchangeEntries}, backtest=${runtimeComparison.lineage.excludedBacktestEntries}` : ""
9569
+ ],
8811
9570
  ["Command", `\`${process.argv.join(" ")}\``],
8812
9571
  ["JSON report", jsonPath]
8813
9572
  ]
8814
9573
  ),
8815
9574
  "",
9575
+ "## Replay Lineage",
9576
+ "",
9577
+ renderLineageRows(runtimeComparison),
9578
+ "",
8816
9579
  "## Strategy Comparison",
8817
9580
  "",
8818
9581
  renderStrategyRows(runtimeComparison),
@@ -8821,6 +9584,10 @@ var writeReplayOutputReport = async ({
8821
9584
  "",
8822
9585
  renderMatchedRows(perTradeAnalysis.matched),
8823
9586
  "",
9587
+ "## Failed Runtime Orders",
9588
+ "",
9589
+ renderOrderFailedRows(perTradeAnalysis.orderFailed),
9590
+ "",
8824
9591
  "## Runtime-Only Trades",
8825
9592
  "",
8826
9593
  renderRuntimeOnlyRows(perTradeAnalysis.runtimeOnly),
@@ -8835,8 +9602,8 @@ var writeReplayOutputReport = async ({
8835
9602
  ""
8836
9603
  ];
8837
9604
  await Promise.all([
8838
- import_promises.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8839
- import_promises.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
9605
+ import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9606
+ import_promises2.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8840
9607
  `, "utf8")
8841
9608
  ]);
8842
9609
  return {
@@ -8875,7 +9642,8 @@ var finishReplay = async ({
8875
9642
  const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
8876
9643
  liveStrategySummaries: replayStrategySnapshot.summaries,
8877
9644
  backtestEntries: replayStrategySnapshot.backtestEntries,
8878
- replaySignals: replayResult.signals
9645
+ replaySignals: replayResult.signals,
9646
+ replayLineages: replayResult.runtimeLineages
8879
9647
  });
8880
9648
  const finishedAt = /* @__PURE__ */ new Date();
8881
9649
  const durationSeconds = Number(
@@ -8891,7 +9659,7 @@ var finishReplay = async ({
8891
9659
  replayResult.orderLog[replayResult.orderLog.length - 1]?.timestamp ?? finishedAt.getTime()
8892
9660
  )}`
8893
9661
  }) : null;
8894
- const replayKey = import_redis5.redisKeys.backtestResults(
9662
+ const replayKey = import_redis6.redisKeys.backtestResults(
8895
9663
  replayUserName,
8896
9664
  REPLAY_RESULTS_CONFIG,
8897
9665
  timestamp
@@ -8912,7 +9680,7 @@ var finishReplay = async ({
8912
9680
  });
8913
9681
  console.log(import_chalk12.default.green(`Replay report: ${outputReport.markdownPath}`));
8914
9682
  console.log(import_chalk12.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
8915
- await (0, import_redis5.setData)(
9683
+ await (0, import_redis6.setData)(
8916
9684
  replayKey,
8917
9685
  {
8918
9686
  config: REPLAY_RESULTS_CONFIG,
@@ -8933,6 +9701,7 @@ var finishReplay = async ({
8933
9701
  cycleCount: replayResult.cycleCount,
8934
9702
  abortedCycles: replayResult.abortedCycles,
8935
9703
  signalsCount: replayResult.signals.length,
9704
+ replayLineage: replayResult.runtimeLineages,
8936
9705
  strategyCharts: replayChartSnapshot,
8937
9706
  outputReport
8938
9707
  },
@@ -8943,8 +9712,8 @@ var finishReplay = async ({
8943
9712
  if (replayChartSnapshot) {
8944
9713
  await Promise.all(
8945
9714
  replayChartSnapshot.strategies.map(
8946
- (card) => (0, import_redis5.setData)(
8947
- import_redis5.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
9715
+ (card) => (0, import_redis6.setData)(
9716
+ import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
8948
9717
  card,
8949
9718
  {
8950
9719
  expire: import_constants6.TTL_1M
@@ -8969,7 +9738,8 @@ var replayBacktest = async () => {
8969
9738
  endTime: replayFlags.endTime,
8970
9739
  cacheOnly: replayFlags.cacheOnly,
8971
9740
  interval: replayInterval,
8972
- projectRoot: replayProjectRoot
9741
+ projectRoot: replayProjectRoot,
9742
+ closedIntervalMs: (0, import_data2.intervalToMs)(replayInterval)
8973
9743
  });
8974
9744
  if (!preparedRun || isReplayUpdateOnlyRun) {
8975
9745
  return;