@tradejs/cli 2.0.6 → 2.0.7

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