@tradejs/cli 3.1.29-beta.289 → 3.1.29-beta.291

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.
@@ -827,7 +827,9 @@ var BASE_CONTEXT_FEATURE_SECTIONS = [
827
827
  "relative",
828
828
  "derivatives",
829
829
  "mtf",
830
- "gateFeatures"
830
+ "gateFeatures",
831
+ "tickerProfile",
832
+ "portfolioContext"
831
833
  ];
832
834
  var asRecord = (value) => typeof value === "object" && value != null && !Array.isArray(value) ? value : null;
833
835
  var collectStrategyGateFeatures = (baseContext) => {
@@ -844,6 +846,7 @@ var pickBaseContextFeatureSections = (baseContext) => Object.fromEntries(
844
846
  );
845
847
  var buildAiTrainEvaluationFeatureSnapshot = ({
846
848
  additionalIndicators,
849
+ featureSnapshot,
847
850
  mode
848
851
  }) => {
849
852
  if (mode === "none") {
@@ -853,12 +856,14 @@ var buildAiTrainEvaluationFeatureSnapshot = ({
853
856
  const baseContext = asRecord(additional?.baseContext);
854
857
  if (!baseContext) {
855
858
  return {
856
- baseContextAvailable: false
859
+ baseContextAvailable: false,
860
+ ...featureSnapshot ? { featureSnapshot } : {}
857
861
  };
858
862
  }
859
863
  const strategyGateFeatures = collectStrategyGateFeatures(baseContext);
860
864
  const snapshot = {
861
865
  baseContextAvailable: true,
866
+ ...featureSnapshot ? { featureSnapshot } : {},
862
867
  gateFeatures: baseContext.gateFeatures ?? null,
863
868
  ...strategyGateFeatures ? { strategyGateFeatures } : {}
864
869
  };
@@ -1059,6 +1064,8 @@ var RESEARCH_CONTEXT_ENV_KEYS = [
1059
1064
  "INTERVAL",
1060
1065
  "DERIVATIVES_CONTEXT_ENABLED",
1061
1066
  "DERIVATIVES_CONTEXT_TARGET_ENABLED",
1067
+ "TICKER_PROFILES_PROVIDER",
1068
+ "TICKER_PROFILES_UNIVERSE",
1062
1069
  "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
1063
1070
  "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
1064
1071
  "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
@@ -1093,13 +1100,15 @@ var writeAiTrainResearchSnapshot = async ({
1093
1100
  }) => {
1094
1101
  const resolvedPath = import_node_path2.default.resolve(outputPath);
1095
1102
  await import_promises2.default.mkdir(import_node_path2.default.dirname(resolvedPath), { recursive: true });
1096
- await import_promises2.default.writeFile(
1097
- resolvedPath,
1098
- `${JSON.stringify(result, null, 2)}
1099
- `,
1100
- "utf8"
1101
- );
1102
- return resolvedPath;
1103
+ const bytes = `${JSON.stringify(result)}
1104
+ `;
1105
+ await import_promises2.default.writeFile(resolvedPath, bytes, "utf8");
1106
+ return {
1107
+ schema: "tradejs-research-report-receipt/v1",
1108
+ path: resolvedPath,
1109
+ bytes: Buffer.byteLength(bytes),
1110
+ sha256: (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex")
1111
+ };
1103
1112
  };
1104
1113
  var normalizeForStableJson = (value) => {
1105
1114
  if (Array.isArray(value)) {
@@ -1416,6 +1425,7 @@ var extractSignalFromAiDatasetRow = (row) => {
1416
1425
  const { payload } = row;
1417
1426
  return {
1418
1427
  ...payload.signal,
1428
+ aiFeatureReplay: { snapshot: payload.featureSnapshot ?? null },
1419
1429
  strategy: payload.signal.strategy,
1420
1430
  figures: payload.figures ?? {},
1421
1431
  indicators: payload.indicators ?? {},
@@ -1474,7 +1484,11 @@ import_args.default.option(
1474
1484
  "Save compact AI approval chart data for the strategies UI",
1475
1485
  false
1476
1486
  );
1477
- import_args.default.option(["j", "json"], "Print structured JSON summary", false);
1487
+ import_args.default.option(
1488
+ ["j", "json"],
1489
+ "Print full JSON without --output; otherwise print its receipt",
1490
+ false
1491
+ );
1478
1492
  import_args.default.option(
1479
1493
  ["O", "output"],
1480
1494
  "Write the structured JSON summary to a file and suppress table output",
@@ -1938,6 +1952,7 @@ var main = async () => {
1938
1952
  const isCorrect = aiApproved === profitableTrade;
1939
1953
  const features = buildAiTrainEvaluationFeatureSnapshot({
1940
1954
  additionalIndicators: payload.additionalIndicators,
1955
+ featureSnapshot: payload.featureSnapshot,
1941
1956
  mode: dumpFeatureMode
1942
1957
  });
1943
1958
  evaluations.push({
@@ -2182,14 +2197,11 @@ var main = async () => {
2182
2197
  ttlMs: import_constants.TTL_1M
2183
2198
  });
2184
2199
  }
2185
- if (jsonOutputPath) {
2186
- await writeAiTrainResearchSnapshot({
2187
- outputPath: jsonOutputPath,
2188
- result
2189
- });
2190
- }
2200
+ const reportReceipt = jsonOutputPath ? await writeAiTrainResearchSnapshot({ outputPath: jsonOutputPath, result }) : null;
2191
2201
  if (jsonOutput) {
2192
- if (printJsonOutput) {
2202
+ if (reportReceipt) {
2203
+ console.log(JSON.stringify(reportReceipt));
2204
+ } else if (printJsonOutput) {
2193
2205
  console.log(JSON.stringify(result));
2194
2206
  }
2195
2207
  process.exit(failed === selectedRows ? 1 : 0);
@@ -46,7 +46,7 @@ var import_node_path = __toESM(require("path"));
46
46
 
47
47
  // src/lib/coreResearch/types.ts
48
48
  var CORE_RESEARCH_SCHEMA = "tradejs-core-research/v1";
49
- var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v1";
49
+ var CORE_RESEARCH_RESULT_SCHEMA = "tradejs-core-research-result/v2";
50
50
  var CORE_RESEARCH_LEDGER_SCHEMA = "tradejs-core-research-ledger/v1";
51
51
 
52
52
  // src/lib/coreResearch/io.ts
@@ -731,6 +731,14 @@ var writeCoreResearchTrades = async (params) => {
731
731
  }
732
732
  await closeOutput(output);
733
733
  };
734
+ var summarizeCoreResearchComparison = (comparison) => {
735
+ const { matchedPairs, ...summary } = comparison;
736
+ return {
737
+ ...summary,
738
+ exitReasonChanges: matchedPairs.filter((pair) => pair.exitReasonChanged).length,
739
+ exitCodeChanges: matchedPairs.filter((pair) => pair.exitCodeChanged).length
740
+ };
741
+ };
734
742
 
735
743
  // src/lib/coreResearch/metrics.ts
736
744
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1806,7 +1814,7 @@ var buildCurveSvg = (tradesByVariant, cohort, curve) => {
1806
1814
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml(entry.id)}</span>`).join("")}</div>`;
1807
1815
  };
1808
1816
  var comparisonCards = (comparisons) => comparisons.map(
1809
- (comparison) => `<article class="card"><h3>${escapeHtml(comparison.candidateId)} vs ${escapeHtml(comparison.controlId)}</h3><p class="verdict ${comparison.selection.passed ? "positive" : "negative"}">${comparison.selection.passed ? "PASS selection" : "REJECT selection"}</p><dl><dt>Matched</dt><dd>${comparison.matched}</dd><dt>Control only</dt><dd>${comparison.controlOnly}</dd><dt>Candidate only</dt><dd>${comparison.candidateOnly}</dd><dt>Identity retained</dt><dd>${format(comparison.matchedIdentityPctOfControl)}%</dd><dt>Exit reason changes</dt><dd>${comparison.matchedPairs.filter((pair) => pair.exitReasonChanged).length}</dd><dt>Exit code changes</dt><dd>${comparison.matchedPairs.filter((pair) => pair.exitCodeChanged).length}</dd><dt>Matched \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
1817
+ (comparison) => `<article class="card"><h3>${escapeHtml(comparison.candidateId)} vs ${escapeHtml(comparison.controlId)}</h3><p class="verdict ${comparison.selection.passed ? "positive" : "negative"}">${comparison.selection.passed ? "PASS selection" : "REJECT selection"}</p><dl><dt>Matched</dt><dd>${comparison.matched}</dd><dt>Control only</dt><dd>${comparison.controlOnly}</dd><dt>Candidate only</dt><dd>${comparison.candidateOnly}</dd><dt>Identity retained</dt><dd>${format(comparison.matchedIdentityPctOfControl)}%</dd><dt>Exit reason changes</dt><dd>${comparison.exitReasonChanges}</dd><dt>Exit code changes</dt><dd>${comparison.exitCodeChanges}</dd><dt>Matched \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
1810
1818
  ).join("");
1811
1819
  var regimeRows = (variants) => {
1812
1820
  const keys = [
@@ -2491,7 +2499,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2491
2499
  regimeCausality: "signal-time payload.additionalIndicators.baseContext only"
2492
2500
  },
2493
2501
  variants: analyses,
2494
- comparisons,
2502
+ comparisons: comparisons.map(summarizeCoreResearchComparison),
2495
2503
  multipleTesting: {
2496
2504
  family: spec.hypothesis.family,
2497
2505
  hypotheses: Math.max(comparisons.length, familyHypotheses),
@@ -2524,7 +2532,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2524
2532
  artifactHashes: sourceHashes
2525
2533
  };
2526
2534
  await writeJsonAtomic(paths.resultPath, result);
2527
- await writeJsonAtomic(paths.comparisonPath, comparisons);
2535
+ await writeJsonAtomic(paths.comparisonPath, result.comparisons);
2528
2536
  await writeCoreResearchMatches({ filePath: paths.matchesPath, comparisons });
2529
2537
  await writeCoreResearchTrades({
2530
2538
  filePath: paths.tradesPath,
@@ -4985,7 +4985,13 @@ var loadReplayRuntimeEvidenceMetadata = async (params) => {
4985
4985
  userName: artifact.userName,
4986
4986
  startTime: artifact.startTime,
4987
4987
  endTime: artifact.endTime,
4988
- deployment: artifact.deployment
4988
+ deployment: artifact.deployment,
4989
+ decisionTraceTargets: unwrapRows(
4990
+ artifact.runtime.trades,
4991
+ "trade"
4992
+ ).flatMap(
4993
+ ({ strategy, symbol, signalTimestamp }) => typeof signalTimestamp === "number" && Number.isFinite(signalTimestamp) && artifact.startTime != null && artifact.endTime != null && signalTimestamp >= artifact.startTime && signalTimestamp < artifact.endTime ? [{ strategy, symbol, signalTimestamp }] : []
4994
+ )
4989
4995
  };
4990
4996
  };
4991
4997
  var buildRuntimeEvidenceLineageScopes = ({
@@ -5177,6 +5183,9 @@ var createPortfolioReplayConnector = (connector) => {
5177
5183
  let executionTimestamp = null;
5178
5184
  const positionsBySymbol = /* @__PURE__ */ new Map();
5179
5185
  const currentPriceBySymbol = /* @__PURE__ */ new Map();
5186
+ const markedAtBySymbol = /* @__PURE__ */ new Map();
5187
+ const entryFacts = /* @__PURE__ */ new Map();
5188
+ let portfolioObserved = false;
5180
5189
  const orderLog = [];
5181
5190
  const positionLog = [];
5182
5191
  const orderLogByStrategy = /* @__PURE__ */ new Map();
@@ -5414,14 +5423,51 @@ var createPortfolioReplayConnector = (connector) => {
5414
5423
  const { strategyName: _strategyName, ...nextPosition } = position;
5415
5424
  return nextPosition;
5416
5425
  }),
5426
+ getPortfolioObservation: async (asOf, reservations) => {
5427
+ portfolioObserved = true;
5428
+ const pendingIds = new Set(reservations.map((r) => r.id));
5429
+ for (const id of entryFacts.keys())
5430
+ if (!pendingIds.has(id)) entryFacts.delete(id);
5431
+ return {
5432
+ asOf,
5433
+ positions: Array.from(positionsBySymbol.values()).map(
5434
+ ({ position, stopLossPrice }) => ({
5435
+ id: position.orderId ?? `${position.strategyName}:${position.symbol}:${position.timestamp}`,
5436
+ strategy: position.strategyName === UNKNOWN_STRATEGY ? null : position.strategyName,
5437
+ symbol: position.symbol,
5438
+ direction: position.direction,
5439
+ qty: position.qty,
5440
+ entryPrice: position.price,
5441
+ markPrice: currentPriceBySymbol.get(position.symbol) ?? null,
5442
+ markedAt: markedAtBySymbol.get(position.symbol) ?? null,
5443
+ stopLossPrice
5444
+ })
5445
+ ),
5446
+ orders: reservations.map(
5447
+ (r) => entryFacts.get(r.id) ?? {
5448
+ id: r.id,
5449
+ status: "pending",
5450
+ filledQty: 0
5451
+ }
5452
+ )
5453
+ };
5454
+ },
5417
5455
  getOpenPositionPnl: async () => Array.from(positionsBySymbol.values()).map(
5418
- ({ position }) => getPositionPnlSnapshot({
5419
- position,
5420
- currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5456
+ ({ position, stopLossPrice }) => ({
5457
+ ...getPositionPnlSnapshot({
5458
+ position,
5459
+ currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5460
+ }),
5461
+ observedAt: markedAtBySymbol.get(position.symbol),
5462
+ stopLossPrice
5421
5463
  })
5422
5464
  ),
5423
5465
  placeOrder: async (order) => {
5424
- const { beforeSubmit, ...submittedOrder } = order;
5466
+ const {
5467
+ beforeSubmit,
5468
+ onRejected: _onRejected,
5469
+ ...submittedOrder
5470
+ } = order;
5425
5471
  if (positionsBySymbol.has(order.symbol)) {
5426
5472
  return false;
5427
5473
  }
@@ -5465,6 +5511,14 @@ var createPortfolioReplayConnector = (connector) => {
5465
5511
  stopLossPrice: null
5466
5512
  };
5467
5513
  positionsBySymbol.set(order.symbol, positionState);
5514
+ markedAtBySymbol.set(order.symbol, executionTimestamp ?? order.timestamp);
5515
+ if (portfolioObserved && order.orderId)
5516
+ entryFacts.set(order.orderId, {
5517
+ id: order.orderId,
5518
+ status: "filled",
5519
+ filledQty: order.qty,
5520
+ positionId: order.orderId
5521
+ });
5468
5522
  logOrder({
5469
5523
  positionState,
5470
5524
  data: {
@@ -5548,6 +5602,7 @@ var createPortfolioReplayConnector = (connector) => {
5548
5602
  getTickers: connector.getTickers,
5549
5603
  advanceMarket: async ({ symbol, candle }) => {
5550
5604
  executionTimestamp = candle.timestamp;
5605
+ markedAtBySymbol.set(symbol, candle.timestamp);
5551
5606
  currentPriceBySymbol.set(symbol, candle.close);
5552
5607
  await checkSl({ symbol, candle });
5553
5608
  await checkTp({ symbol, candle });
@@ -5588,6 +5643,73 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
5588
5643
  }
5589
5644
  };
5590
5645
 
5646
+ // src/lib/replay/historicalSignalsReplayDecisionTrace.ts
5647
+ var replayDecisionTraceKey = ({
5648
+ strategy,
5649
+ symbol,
5650
+ signalTimestamp
5651
+ }) => JSON.stringify([strategy, symbol, signalTimestamp]);
5652
+ var describeOutcome = (result) => {
5653
+ if (typeof result === "string") {
5654
+ return {
5655
+ stage: result.startsWith("BACKTEST_ENTRY_DELAY_") ? "entry_delay" : /(?:NO_PATTERN|POSITION_EXISTS)$/.test(result) ? "core" : "evaluation",
5656
+ reason: result
5657
+ };
5658
+ }
5659
+ if (!result) return { stage: "evaluation", reason: "NO_SIGNAL" };
5660
+ const reason = result.orderSkipReason || result.orderFailureReason || result.orderStatus || "SIGNAL";
5661
+ const patternContext = result.additionalIndicators?.tradingPatternsContext;
5662
+ return {
5663
+ stage: result.riskDecision?.status === "rejected" || /^ENTRY_(?:RR_BELOW|RISK_EXCEEDS)/.test(reason) ? "risk" : /^(?:AI_|ML_|HOOK_BEFORE_ENTRY_GATE|RESEARCH_ENTRY_POLICY)/.test(
5664
+ reason
5665
+ ) ? "gate" : result.orderStatus ? "execution" : "evaluation",
5666
+ reason,
5667
+ direction: result.direction,
5668
+ orderStatus: result.orderStatus,
5669
+ ...typeof patternContext?.selectedPattern === "string" ? { pattern: patternContext.selectedPattern } : {},
5670
+ ...typeof patternContext?.sourceCode === "string" ? { patternCode: patternContext.sourceCode } : {}
5671
+ };
5672
+ };
5673
+ var createReplayDecisionTrace = (targets = []) => {
5674
+ const traces = /* @__PURE__ */ new Map();
5675
+ const symbols = new Set(targets.map(({ symbol }) => symbol));
5676
+ for (const target of targets) {
5677
+ traces.set(replayDecisionTraceKey(target), {
5678
+ ...target,
5679
+ decision: {
5680
+ stage: "not_evaluated",
5681
+ reason: "REPLAY_CANDLE_NOT_EVALUATED"
5682
+ }
5683
+ });
5684
+ }
5685
+ const record = (strategy, symbol, candle, result, notEvaluatedReason) => {
5686
+ if (!symbols.has(symbol)) return;
5687
+ const { timestamp, open, high, low, close, volume } = candle;
5688
+ const trace = traces.get(
5689
+ replayDecisionTraceKey({ strategy, symbol, signalTimestamp: timestamp })
5690
+ );
5691
+ if (result && typeof result !== "string" && result.timestamp !== timestamp) {
5692
+ const original = traces.get(
5693
+ replayDecisionTraceKey({
5694
+ strategy,
5695
+ symbol,
5696
+ signalTimestamp: result.timestamp
5697
+ })
5698
+ );
5699
+ if (original)
5700
+ original.followUp = {
5701
+ ...describeOutcome(result),
5702
+ candleTimestamp: timestamp
5703
+ };
5704
+ notEvaluatedReason = "REPLAY_PROCESSED_DELAYED_ENTRY";
5705
+ }
5706
+ if (!trace) return;
5707
+ trace.candle = { timestamp, open, high, low, close, volume };
5708
+ trace.decision = notEvaluatedReason ? { stage: "not_evaluated", reason: notEvaluatedReason } : describeOutcome(result);
5709
+ };
5710
+ return { record, values: () => [...traces.values()] };
5711
+ };
5712
+
5591
5713
  // src/lib/replay/historicalSignalsReplaySkipEvidence.ts
5592
5714
  var import_data2 = require("@tradejs/core/data");
5593
5715
  var replayScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
@@ -5698,6 +5820,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5698
5820
  context.plan.runtimeLineages,
5699
5821
  context.collectSkipEvidence
5700
5822
  );
5823
+ const decisionTrace = createReplayDecisionTrace(context.decisionTraceTargets);
5701
5824
  const reportProgress = (timestamp) => {
5702
5825
  if (adapters.progress.enabled === false) return;
5703
5826
  adapters.progress.tick({
@@ -5726,6 +5849,20 @@ var executeHistoricalReplay = async (context, adapters) => {
5726
5849
  status: "completed",
5727
5850
  durationMs: adapters.clock.now() - cycleStartedAt
5728
5851
  });
5852
+ if (context.decisionTraceTargets?.length) {
5853
+ for (const symbolRuntime of cycleSymbols) {
5854
+ const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
5855
+ for (const strategy of symbolRuntime.strategies) {
5856
+ decisionTrace.record(
5857
+ strategy.strategyName,
5858
+ symbolRuntime.symbol,
5859
+ candle,
5860
+ void 0,
5861
+ "REPLAY_CYCLE_ABORTED"
5862
+ );
5863
+ }
5864
+ }
5865
+ }
5729
5866
  advanceAbortedCycle(cycleSymbols, timestamp);
5730
5867
  reportProgress(timestamp);
5731
5868
  continue;
@@ -5736,6 +5873,17 @@ var executeHistoricalReplay = async (context, adapters) => {
5736
5873
  const btcCandle = symbolRuntime.btcReplayData[symbolRuntime.currentIndex];
5737
5874
  const ethCandle = symbolRuntime.ethReplayData[symbolRuntime.currentIndex];
5738
5875
  if (!candle || !btcCandle || candle.timestamp + (symbolRuntime.timestampOffsetMs ?? 0) !== timestamp || btcCandle.timestamp !== candle.timestamp) {
5876
+ if (candle && context.decisionTraceTargets?.length) {
5877
+ for (const strategy of symbolRuntime.strategies) {
5878
+ decisionTrace.record(
5879
+ strategy.strategyName,
5880
+ symbolRuntime.symbol,
5881
+ candle,
5882
+ void 0,
5883
+ "REPLAY_CANDLE_OR_REFERENCE_NOT_ALIGNED"
5884
+ );
5885
+ }
5886
+ }
5739
5887
  continue;
5740
5888
  }
5741
5889
  for (const strategyRuntime of symbolRuntime.strategies) {
@@ -5744,6 +5892,12 @@ var executeHistoricalReplay = async (context, adapters) => {
5744
5892
  btcCandle,
5745
5893
  ethCandle?.timestamp === candle.timestamp ? ethCandle : void 0
5746
5894
  );
5895
+ decisionTrace.record(
5896
+ strategyRuntime.strategyName,
5897
+ symbolRuntime.symbol,
5898
+ candle,
5899
+ result
5900
+ );
5747
5901
  skipEvidence.record({
5748
5902
  strategyName: strategyRuntime.strategyName,
5749
5903
  symbol: symbolRuntime.symbol,
@@ -5775,7 +5929,8 @@ var executeHistoricalReplay = async (context, adapters) => {
5775
5929
  return {
5776
5930
  signals,
5777
5931
  abortedCycles,
5778
- replayLineageScopes: skipEvidence.values()
5932
+ replayLineageScopes: skipEvidence.values(),
5933
+ replayDecisionTraces: decisionTrace.values()
5779
5934
  };
5780
5935
  };
5781
5936
 
@@ -5826,7 +5981,8 @@ var collectHistoricalReplayResult = (context) => ({
5826
5981
  cycleCount: context.cycleCount,
5827
5982
  abortedCycles: context.abortedCycles,
5828
5983
  runtimeLineages: context.runtimeLineages,
5829
- replayLineageScopes: context.replayLineageScopes
5984
+ replayLineageScopes: context.replayLineageScopes,
5985
+ ...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
5830
5986
  });
5831
5987
  var compactHistoricalReplayResultForPortfolio = (result) => ({
5832
5988
  ...result,
@@ -5872,7 +6028,10 @@ var mergeHistoricalReplayResults = (parts) => {
5872
6028
  cycleCount: parts.reduce((sum, part) => sum + part.cycleCount, 0),
5873
6029
  abortedCycles: parts.reduce((sum, part) => sum + part.abortedCycles, 0),
5874
6030
  runtimeLineages: parts.flatMap((part) => part.runtimeLineages),
5875
- replayLineageScopes: parts.flatMap((part) => part.replayLineageScopes)
6031
+ replayLineageScopes: parts.flatMap((part) => part.replayLineageScopes),
6032
+ replayDecisionTraces: parts.flatMap(
6033
+ (part) => part.replayDecisionTraces ?? []
6034
+ )
5876
6035
  };
5877
6036
  };
5878
6037
 
@@ -6023,7 +6182,8 @@ var runHistoricalSignalsReplay = async ({
6023
6182
  references,
6024
6183
  showProgress = true,
6025
6184
  collectSkipEvidence = true,
6026
- collectSignals = true
6185
+ collectSignals = true,
6186
+ decisionTraceTargets
6027
6187
  }) => {
6028
6188
  const memory = createReplayMemoryProfile();
6029
6189
  memory?.("prepare:start");
@@ -6110,6 +6270,7 @@ var runHistoricalSignalsReplay = async ({
6110
6270
  connector: replayConnector,
6111
6271
  collectSkipEvidence,
6112
6272
  collectSignals,
6273
+ decisionTraceTargets,
6113
6274
  hooks: projectHooks,
6114
6275
  hookContext
6115
6276
  },
@@ -6146,7 +6307,8 @@ var runHistoricalSignalsReplay = async ({
6146
6307
  cycleCount: plan.orderedTimestamps.length,
6147
6308
  abortedCycles: execution.abortedCycles,
6148
6309
  runtimeLineages: plan.runtimeLineages,
6149
- replayLineageScopes: execution.replayLineageScopes
6310
+ replayLineageScopes: execution.replayLineageScopes,
6311
+ replayDecisionTraces: execution.replayDecisionTraces
6150
6312
  });
6151
6313
  memory?.("result:collected");
6152
6314
  return result;
@@ -6374,6 +6536,7 @@ var extractRuntimeParityEntries = (trades) => trades.filter(
6374
6536
  direction: trade.direction,
6375
6537
  qty: toFiniteNumberOrNull2(trade.qty),
6376
6538
  timestamp: trade.entryTimestamp,
6539
+ ...Number.isFinite(trade.signalTimestamp) ? { signalTimestamp: trade.signalTimestamp } : {},
6377
6540
  price: toFiniteNumberOrNull2(trade.entryPrice),
6378
6541
  orderId: trade.orderId,
6379
6542
  signalId: typeof trade.signalId === "string" ? trade.signalId : void 0,
@@ -7119,6 +7282,7 @@ var toRuntimeParityDetail = (entry) => ({
7119
7282
  direction: entry.direction,
7120
7283
  qty: entry.qty ?? null,
7121
7284
  timestamp: entry.timestamp,
7285
+ ...entry.signalTimestamp != null ? { signalTimestamp: entry.signalTimestamp } : {},
7122
7286
  price: entry.price,
7123
7287
  exitType: entry.exitType ?? null,
7124
7288
  exitTimestamp: entry.exitTimestamp ?? null,
@@ -7192,7 +7356,8 @@ var findNearestEvaluation = ({
7192
7356
  evaluations,
7193
7357
  toleranceMs,
7194
7358
  timestampOffsetMs,
7195
- entryTimestampOffsetMs = 0
7359
+ entryTimestampOffsetMs = 0,
7360
+ matchDirection = false
7196
7361
  }) => {
7197
7362
  const entryStrategy2 = getEntryStrategy(entry);
7198
7363
  if (!entryStrategy2) {
@@ -7202,7 +7367,7 @@ var findNearestEvaluation = ({
7202
7367
  let bestDiff = Number.POSITIVE_INFINITY;
7203
7368
  const entryTimestamp = entry.timestamp + entryTimestampOffsetMs;
7204
7369
  for (const evaluation of evaluations) {
7205
- if (evaluation.strategy !== entryStrategy2 || evaluation.symbol !== entry.symbol) {
7370
+ if (evaluation.strategy !== entryStrategy2 || evaluation.symbol !== entry.symbol || matchDirection && evaluation.direction != null && evaluation.direction !== entry.direction) {
7206
7371
  continue;
7207
7372
  }
7208
7373
  const diff = Math.abs(
@@ -7548,6 +7713,7 @@ var buildReplayMismatchDrilldown = ({
7548
7713
  replaySignals = [],
7549
7714
  replaySignalEvaluations = [],
7550
7715
  replayLineageScopes = [],
7716
+ replayDecisionTraces = [],
7551
7717
  runtimeLineageScopes = [],
7552
7718
  toleranceMs,
7553
7719
  backtestTimestampOffsetMs,
@@ -7556,48 +7722,66 @@ var buildReplayMismatchDrilldown = ({
7556
7722
  const nearestByEntry = new Map(
7557
7723
  nearestCandidates.map((candidate) => [candidate.entry, candidate])
7558
7724
  );
7725
+ const traceByCandle = new Map(
7726
+ replayDecisionTraces.map((trace) => [replayDecisionTraceKey(trace), trace])
7727
+ );
7559
7728
  const artifactLimit = Math.min(limit, resolveReplayParityArtifactLimit());
7560
7729
  let runtimeOnlyArtifactsIncluded = 0;
7561
7730
  let backtestOnlyArtifactsIncluded = 0;
7562
7731
  const runtimeOnlyDiagnostics = runtimeOnly.map((entry, index) => {
7563
- const entryOffsetMs = resolveTradeParityTimestampOffset(
7564
- getEntryStrategy(entry) ?? "",
7565
- backtestTimestampOffsetMs
7566
- );
7732
+ const isRuntimeTrade = entry.source === "runtime";
7733
+ const decisionEntry = isRuntimeTrade ? entry.signalTimestamp == null ? null : { ...entry, timestamp: entry.signalTimestamp } : entry;
7734
+ const decisionClock = {
7735
+ toleranceMs: isRuntimeTrade ? 0 : toleranceMs,
7736
+ timestampOffsetMs: isRuntimeTrade ? 0 : resolveTradeParityTimestampOffset(
7737
+ getEntryStrategy(entry) ?? "",
7738
+ backtestTimestampOffsetMs
7739
+ )
7740
+ };
7567
7741
  const includeArtifacts = index < artifactLimit;
7568
7742
  const nearestCandidate = nearestByEntry.get(entry);
7569
- const nearestReplaySignal = findNearestSignal({
7570
- entry,
7743
+ const nearestReplaySignal = decisionEntry && findNearestSignal({
7744
+ entry: decisionEntry,
7571
7745
  signals: replaySignals,
7572
- toleranceMs,
7573
- timestampOffsetMs: entryOffsetMs
7746
+ ...decisionClock
7574
7747
  });
7575
- const nearestReplayEvaluation = findNearestEvaluation({
7576
- entry,
7748
+ const nearestReplayEvaluation = decisionEntry && findNearestEvaluation({
7749
+ entry: decisionEntry,
7577
7750
  evaluations: replaySignalEvaluations,
7578
- toleranceMs,
7579
- timestampOffsetMs: entryOffsetMs
7751
+ matchDirection: isRuntimeTrade,
7752
+ ...decisionClock
7580
7753
  });
7581
- const nearestReplayEvaluationOutcome = findNearestReplayEvaluationOutcome({
7582
- entry,
7754
+ const nearestReplayEvaluationOutcome = decisionEntry && findNearestReplayEvaluationOutcome({
7755
+ entry: decisionEntry,
7583
7756
  lineageScopes: replayLineageScopes,
7584
- toleranceMs,
7585
- timestampOffsetMs: entryOffsetMs
7757
+ ...decisionClock
7586
7758
  });
7759
+ const strategy = getEntryStrategy(entry);
7760
+ const replayDecisionTrace = isRuntimeTrade && strategy && decisionEntry ? traceByCandle.get(
7761
+ replayDecisionTraceKey({
7762
+ strategy,
7763
+ symbol: entry.symbol,
7764
+ signalTimestamp: decisionEntry.timestamp
7765
+ })
7766
+ ) : void 0;
7767
+ const unavailableReason = !isRuntimeTrade ? "no_replay_signal_or_backtest_candidate" : !decisionEntry ? "replay_signal_timestamp_unavailable" : replayDecisionTrace?.decision.stage === "not_evaluated" ? replayDecisionTrace.decision.reason : "replay_evaluation_unavailable";
7587
7768
  const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestReplayEvaluationOutcome ? {
7588
- classification: "core_skipped",
7769
+ classification: nearestReplayEvaluationOutcome.reason.startsWith(
7770
+ "BACKTEST_ENTRY_DELAY_"
7771
+ ) ? "entry_delayed" : "core_skipped",
7589
7772
  reason: nearestReplayEvaluationOutcome.reason
7590
- } : nearestCandidate?.nearest ? {
7773
+ } : !isRuntimeTrade && nearestCandidate?.nearest ? {
7591
7774
  classification: "timing_or_price_drift",
7592
7775
  reason: nearestCandidate.reason
7593
7776
  } : {
7594
7777
  classification: "no_replay_candidate",
7595
- reason: "no_replay_signal_or_backtest_candidate"
7778
+ reason: unavailableReason
7596
7779
  };
7597
7780
  return {
7598
7781
  entry,
7599
7782
  ...classification,
7600
7783
  ...nearestCandidate ? { nearestCandidate } : {},
7784
+ ...replayDecisionTrace ? { replayDecisionTrace } : {},
7601
7785
  ...nearestReplaySignal ? {
7602
7786
  replaySignal: toSignalDiagnostic({
7603
7787
  signal: nearestReplaySignal.signal,
@@ -7723,6 +7907,7 @@ var buildReplayRuntimeComparisonDetails = ({
7723
7907
  replaySignals,
7724
7908
  replaySignalEvaluations,
7725
7909
  replayLineageScopes,
7910
+ replayDecisionTraces,
7726
7911
  runtimeLineageScopes
7727
7912
  }) => {
7728
7913
  const runtimeDetails = runtimeEntries.map(toRuntimeParityDetail);
@@ -7783,6 +7968,7 @@ var buildReplayRuntimeComparisonDetails = ({
7783
7968
  replaySignals,
7784
7969
  replaySignalEvaluations,
7785
7970
  replayLineageScopes,
7971
+ replayDecisionTraces,
7786
7972
  runtimeLineageScopes,
7787
7973
  toleranceMs,
7788
7974
  backtestTimestampOffsetMs,
@@ -8808,6 +8994,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8808
8994
  replaySignals,
8809
8995
  replayLineages,
8810
8996
  replayLineageScopes,
8997
+ replayDecisionTraces,
8811
8998
  runtimeEvidencePath
8812
8999
  }) => {
8813
9000
  const { connector, connectorName, window } = getRuntimeCompareContext();
@@ -8959,7 +9146,8 @@ var saveAndPrintReplayRuntimeComparison = async ({
8959
9146
  replaySignals,
8960
9147
  replaySignalEvaluations,
8961
9148
  runtimeLineageScopes: comparable.runtimeLineageScopes,
8962
- replayLineageScopes
9149
+ replayLineageScopes,
9150
+ replayDecisionTraces
8963
9151
  });
8964
9152
  const parityRows = summarizeTradeParityByStrategy({
8965
9153
  runtimeEntries: runtimeDedupe.entries,
@@ -9886,6 +10074,7 @@ var finishReplay = async ({
9886
10074
  replaySignals: replayResult.signals,
9887
10075
  replayLineages: replayResult.runtimeLineages,
9888
10076
  replayLineageScopes: replayResult.replayLineageScopes,
10077
+ replayDecisionTraces: replayResult.replayDecisionTraces,
9889
10078
  runtimeEvidencePath: replayRuntimeEvidencePath
9890
10079
  });
9891
10080
  const finishedAt = /* @__PURE__ */ new Date();
@@ -10235,7 +10424,8 @@ var replayBacktest = async () => {
10235
10424
  references: historicalReplayReferences,
10236
10425
  showProgress: !replayFlags.portfolioReport,
10237
10426
  collectSkipEvidence: !replayFlags.portfolioReport,
10238
- collectSignals: !replayFlags.portfolioReport
10427
+ collectSignals: !replayFlags.portfolioReport,
10428
+ decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
10239
10429
  });
10240
10430
  return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
10241
10431
  };