@tradejs/cli 3.1.29-beta.265 → 3.1.29-beta.268

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.
@@ -20,23 +20,50 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/lib/runtimeParity.ts
21
21
  var runtimeParity_exports = {};
22
22
  __export(runtimeParity_exports, {
23
+ buildTradeParityTimestampOffset: () => buildTradeParityTimestampOffset,
23
24
  compareTradeParityEntries: () => compareTradeParityEntries,
24
25
  dedupeRuntimeParityEntries: () => dedupeRuntimeParityEntries,
25
26
  extractBacktestEntryParityEntries: () => extractBacktestEntryParityEntries,
26
27
  extractRuntimeParityEntries: () => extractRuntimeParityEntries,
27
28
  findRuntimeDuplicateGroups: () => findRuntimeDuplicateGroups,
28
29
  getBacktestParityComparisonTimestamp: () => getBacktestParityComparisonTimestamp,
30
+ resolveTradeParityTimestampOffset: () => resolveTradeParityTimestampOffset,
29
31
  summarizeMatchedParity: () => summarizeMatchedParity
30
32
  });
31
33
  module.exports = __toCommonJS(runtimeParity_exports);
34
+ var resolveTradeParityTimestampOffset = (strategy, offset) => typeof offset === "function" ? offset(strategy) : offset;
35
+ var buildTradeParityTimestampOffset = (strategies) => {
36
+ const intervals = /* @__PURE__ */ new Map();
37
+ for (const { strategyName, interval } of strategies) {
38
+ const minutes = Number(interval);
39
+ if (!Number.isFinite(minutes) || minutes <= 0) {
40
+ throw new Error(`Invalid embedded replay interval for ${strategyName}`);
41
+ }
42
+ intervals.set(strategyName, minutes * 60 * 1e3);
43
+ }
44
+ return (strategy) => {
45
+ const offsetMs = intervals.get(strategy);
46
+ if (offsetMs == null) {
47
+ throw new Error(`Missing embedded replay interval for ${strategy}`);
48
+ }
49
+ return offsetMs;
50
+ };
51
+ };
32
52
  var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
33
53
  var toGroupKey = (entry) => `${entry.strategy}::${entry.symbol}::${entry.direction}`;
34
54
  var toRuntimeDuplicateKey = (entry) => `${toGroupKey(entry)}::${entry.timestamp}`;
35
55
  var getBacktestParityComparisonTimestamp = (entry, timestampOffsetMs = 0) => {
36
- if (timestampOffsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + timestampOffsetMs) {
56
+ const offsetMs = resolveTradeParityTimestampOffset(
57
+ entry.strategy,
58
+ timestampOffsetMs
59
+ );
60
+ if (!Number.isFinite(offsetMs) || offsetMs < 0) {
61
+ throw new Error(`Invalid backtest timestamp offset for ${entry.strategy}`);
62
+ }
63
+ if (offsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + offsetMs) {
37
64
  return entry.timestamp;
38
65
  }
39
- return (entry.signalTimestamp ?? entry.timestamp) + timestampOffsetMs;
66
+ return (entry.signalTimestamp ?? entry.timestamp) + offsetMs;
40
67
  };
41
68
  var sortTradeParityEntries = (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction) || left.id.localeCompare(right.id);
42
69
  var buildPriceDeltaPct = (leftPrice, rightPrice) => {
@@ -301,11 +328,13 @@ var summarizeMatchedParity = (matched) => {
301
328
  };
302
329
  // Annotate the CommonJS export names for ESM import in node:
303
330
  0 && (module.exports = {
331
+ buildTradeParityTimestampOffset,
304
332
  compareTradeParityEntries,
305
333
  dedupeRuntimeParityEntries,
306
334
  extractBacktestEntryParityEntries,
307
335
  extractRuntimeParityEntries,
308
336
  findRuntimeDuplicateGroups,
309
337
  getBacktestParityComparisonTimestamp,
338
+ resolveTradeParityTimestampOffset,
310
339
  summarizeMatchedParity
311
340
  });
@@ -30,11 +30,19 @@ module.exports = __toCommonJS(runtimeParityDetails_exports);
30
30
  var import_trade = require("@tradejs/core/trade");
31
31
 
32
32
  // src/lib/runtimeParity.ts
33
+ var resolveTradeParityTimestampOffset = (strategy, offset) => typeof offset === "function" ? offset(strategy) : offset;
33
34
  var getBacktestParityComparisonTimestamp = (entry, timestampOffsetMs = 0) => {
34
- if (timestampOffsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + timestampOffsetMs) {
35
+ const offsetMs = resolveTradeParityTimestampOffset(
36
+ entry.strategy,
37
+ timestampOffsetMs
38
+ );
39
+ if (!Number.isFinite(offsetMs) || offsetMs < 0) {
40
+ throw new Error(`Invalid backtest timestamp offset for ${entry.strategy}`);
41
+ }
42
+ if (offsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + offsetMs) {
35
43
  return entry.timestamp;
36
44
  }
37
- return (entry.signalTimestamp ?? entry.timestamp) + timestampOffsetMs;
45
+ return (entry.signalTimestamp ?? entry.timestamp) + offsetMs;
38
46
  };
39
47
 
40
48
  // src/lib/runtimeParityDetails.ts
@@ -78,9 +86,13 @@ var buildCostDetail = (entry) => ({
78
86
  totalFee: entry.totalFee ?? null
79
87
  });
80
88
  var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
89
+ const offsetMs = resolveTradeParityTimestampOffset(
90
+ entry.strategy,
91
+ timestampOffsetMs
92
+ );
81
93
  const comparisonTimestamp = getBacktestParityComparisonTimestamp(
82
94
  entry,
83
- timestampOffsetMs
95
+ offsetMs
84
96
  );
85
97
  return {
86
98
  source: "backtest",
@@ -90,7 +102,7 @@ var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
90
102
  qty: entry.qty ?? null,
91
103
  timestamp: entry.timestamp,
92
104
  signalTimestamp: entry.signalTimestamp ?? null,
93
- comparisonTimestamp: timestampOffsetMs === 0 ? null : comparisonTimestamp,
105
+ comparisonTimestamp: offsetMs === 0 ? null : comparisonTimestamp,
94
106
  price: entry.price,
95
107
  exitType: entry.exitType ?? null,
96
108
  exitTimestamp: entry.exitTimestamp ?? null,
@@ -549,25 +561,29 @@ var buildReplayMismatchDrilldown = ({
549
561
  let runtimeOnlyArtifactsIncluded = 0;
550
562
  let backtestOnlyArtifactsIncluded = 0;
551
563
  const runtimeOnlyDiagnostics = runtimeOnly.map((entry, index) => {
564
+ const entryOffsetMs = resolveTradeParityTimestampOffset(
565
+ getEntryStrategy(entry) ?? "",
566
+ backtestTimestampOffsetMs
567
+ );
552
568
  const includeArtifacts = index < artifactLimit;
553
569
  const nearestCandidate = nearestByEntry.get(entry);
554
570
  const nearestReplaySignal = findNearestSignal({
555
571
  entry,
556
572
  signals: replaySignals,
557
573
  toleranceMs,
558
- timestampOffsetMs: backtestTimestampOffsetMs
574
+ timestampOffsetMs: entryOffsetMs
559
575
  });
560
576
  const nearestReplayEvaluation = findNearestEvaluation({
561
577
  entry,
562
578
  evaluations: replaySignalEvaluations,
563
579
  toleranceMs,
564
- timestampOffsetMs: backtestTimestampOffsetMs
580
+ timestampOffsetMs: entryOffsetMs
565
581
  });
566
582
  const nearestReplayEvaluationOutcome = findNearestReplayEvaluationOutcome({
567
583
  entry,
568
584
  lineageScopes: replayLineageScopes,
569
585
  toleranceMs,
570
- timestampOffsetMs: backtestTimestampOffsetMs
586
+ timestampOffsetMs: entryOffsetMs
571
587
  });
572
588
  const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestReplayEvaluationOutcome ? {
573
589
  classification: "core_skipped",
@@ -682,7 +682,12 @@ var writeCoreResearchMatches = async (params) => {
682
682
  "controlPnl",
683
683
  "candidatePnl",
684
684
  "pnlDelta",
685
- "exitReasonChanged"
685
+ "controlExitReason",
686
+ "candidateExitReason",
687
+ "exitReasonChanged",
688
+ "controlExitCode",
689
+ "candidateExitCode",
690
+ "exitCodeChanged"
686
691
  ].map(csv).join(",") + "\n"
687
692
  );
688
693
  for (const comparison of params.comparisons) {
@@ -700,7 +705,12 @@ var writeCoreResearchMatches = async (params) => {
700
705
  pair.control.netProfit,
701
706
  pair.candidate.netProfit,
702
707
  pair.pnlDelta,
703
- pair.exitReasonChanged
708
+ pair.control.exitReason,
709
+ pair.candidate.exitReason,
710
+ pair.exitReasonChanged,
711
+ pair.control.exitCode,
712
+ pair.candidate.exitCode,
713
+ pair.exitCodeChanged
704
714
  ].map(csv).join(",") + "\n"
705
715
  );
706
716
  }
@@ -1317,6 +1327,7 @@ var compareCoreResearchVariants = (params) => {
1317
1327
  candidate: candidateTrade,
1318
1328
  pnlDelta: candidateTrade.netProfit - controlTrade.netProfit,
1319
1329
  exitReasonChanged: candidateTrade.exitReason !== controlTrade.exitReason,
1330
+ exitCodeChanged: candidateTrade.exitCode !== controlTrade.exitCode,
1320
1331
  entryTimestampDeltaMs: candidateTrade.entryTimestamp - controlTrade.entryTimestamp,
1321
1332
  exitTimestampDeltaMs: candidateTrade.exitTimestamp - controlTrade.exitTimestamp
1322
1333
  });
@@ -1528,6 +1539,7 @@ var normalizeCompletedTrade = (params) => {
1528
1539
  "totalSlippageCost"
1529
1540
  ),
1530
1541
  exitReason: result.exitReason,
1542
+ ...result.exitCode ? { exitCode: result.exitCode } : {},
1531
1543
  regime: resolveCoreResearchRegime(row)
1532
1544
  };
1533
1545
  };
@@ -1571,7 +1583,7 @@ var aggregatePositionCycles = (trades) => {
1571
1583
  }
1572
1584
  const primary = primaryRows[0];
1573
1585
  if (cycle.some(
1574
- (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
1586
+ (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason || trade.exitCode !== primary.exitCode
1575
1587
  )) {
1576
1588
  throw new Error(
1577
1589
  `Position cycle ${identity} contains inconsistent final exits`
@@ -1753,7 +1765,7 @@ var buildCurveSvg = (tradesByVariant, cohort, curve) => {
1753
1765
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml(entry.id)}</span>`).join("")}</div>`;
1754
1766
  };
1755
1767
  var comparisonCards = (comparisons) => comparisons.map(
1756
- (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>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>`
1768
+ (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>`
1757
1769
  ).join("");
1758
1770
  var regimeRows = (variants) => {
1759
1771
  const keys = [
@@ -1767,7 +1779,7 @@ var regimeRows = (variants) => {
1767
1779
  ).join("");
1768
1780
  };
1769
1781
  var traceRows = (variants) => variants.map(
1770
- (variant) => `<tr><td>${escapeHtml(variant.variant.label)}</td><td>${escapeHtml(variant.reconciliation.status)}</td><td>${escapeHtml(JSON.stringify(variant.setupIdentitySources))}</td><td>${escapeHtml(JSON.stringify(variant.traceFunnel.events))}</td><td>${escapeHtml(JSON.stringify(variant.traceFunnel.skipCounts))}</td><td>${escapeHtml(variant.latestSignalTimeRegime?.key ?? "n/a")}</td></tr>`
1782
+ (variant) => `<tr><td>${escapeHtml(variant.variant.label)}</td><td>${escapeHtml(variant.reconciliation.status)}</td><td>${escapeHtml(JSON.stringify(variant.setupIdentitySources))}</td><td>${escapeHtml(JSON.stringify(variant.traceFunnel.events))}</td><td>${escapeHtml(JSON.stringify(variant.traceFunnel.skipCounts))}</td><td>${escapeHtml(JSON.stringify(variant.traceFunnel.exitCodes))}</td><td>${escapeHtml(variant.latestSignalTimeRegime?.key ?? "n/a")}</td></tr>`
1771
1783
  ).join("");
1772
1784
  var buildCoreResearchHtml = (params) => {
1773
1785
  const { spec, result, tradesByVariant } = params;
@@ -1793,7 +1805,7 @@ var buildCoreResearchHtml = (params) => {
1793
1805
  ([key, value]) => `<span class="${value === "present" ? "positive" : "negative"}">${escapeHtml(key)}: ${value}</span>`
1794
1806
  ).join(
1795
1807
  ""
1796
- )}</section><h2>ALL equity</h2>${buildCurveSvg(tradesByVariant, "ALL", "equity")}<h2>ALL realized drawdown</h2>${buildCurveSvg(tradesByVariant, "ALL", "drawdown")}<h2>LONG equity</h2>${buildCurveSvg(tradesByVariant, "LONG", "equity")}<h2>LONG realized drawdown</h2>${buildCurveSvg(tradesByVariant, "LONG", "drawdown")}<h2>SHORT equity</h2>${buildCurveSvg(tradesByVariant, "SHORT", "equity")}<h2>SHORT realized drawdown</h2>${buildCurveSvg(tradesByVariant, "SHORT", "drawdown")}<h2>Fixed cohort metrics</h2><p class="muted">Avg PnL/trade = cohort PnL / cohort N. ALL drawdown is aggregate portfolio; LONG/SHORT drawdowns are side-only.</p><div class="scroll"><table><thead><tr><th>Variant</th><th>Window</th><th>Cohort</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>WR</th><th>Realized DD</th><th>Cad/day</th><th>Avg win</th><th>Avg loss</th><th>Payoff</th><th>Median</th><th>P05</th><th>P95</th><th>Median hold h</th><th>Max loss streak</th></tr></thead><tbody>${allRows}</tbody></table></div><h2>Fold and monthly robustness</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Window</th><th>Cohort</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>WR</th><th>Realized DD</th><th>Cad/day</th><th>Avg win</th><th>Avg loss</th><th>Payoff</th><th>Median</th><th>P05</th><th>P95</th><th>Median hold h</th><th>Max loss streak</th></tr></thead><tbody>${robustnessRows}</tbody></table></div><h2>Cost stress</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Extra round-trip bps</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>Realized DD</th></tr></thead><tbody>${costRows}</tbody></table></div><h2>Signal-time market regimes</h2><div class="scroll"><table><thead><tr><th>Trend | Volatility | Breadth | Derivatives</th><th>Variant</th><th>N</th><th>PnL</th><th>PF</th><th>WR</th></tr></thead><tbody>${regimeRows(result.variants)}</tbody></table></div><h2>Trace funnel and reconciliation</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Reconcile</th><th>Setup identity source</th><th>Events</th><th>Top skips</th><th>Latest signal-time regime</th></tr></thead><tbody>${traceRows(result.variants)}</tbody></table></div><h2>Multiple testing / overfitting</h2><pre>${escapeHtml(JSON.stringify({ multipleTesting: result.multipleTesting, overfittingDiagnostics: result.overfittingDiagnostics }, null, 2))}</pre><h2>Lineage</h2><pre>${escapeHtml(JSON.stringify({ stage: spec.stage, universeSha256: spec.universe.sha256, lineage: spec.lineage ?? null, artifactHashes: result.artifactHashes }, null, 2))}</pre></main></body></html>`;
1808
+ )}</section><h2>ALL equity</h2>${buildCurveSvg(tradesByVariant, "ALL", "equity")}<h2>ALL realized drawdown</h2>${buildCurveSvg(tradesByVariant, "ALL", "drawdown")}<h2>LONG equity</h2>${buildCurveSvg(tradesByVariant, "LONG", "equity")}<h2>LONG realized drawdown</h2>${buildCurveSvg(tradesByVariant, "LONG", "drawdown")}<h2>SHORT equity</h2>${buildCurveSvg(tradesByVariant, "SHORT", "equity")}<h2>SHORT realized drawdown</h2>${buildCurveSvg(tradesByVariant, "SHORT", "drawdown")}<h2>Fixed cohort metrics</h2><p class="muted">Avg PnL/trade = cohort PnL / cohort completed positions. ALL drawdown is aggregate portfolio; LONG/SHORT drawdowns are side-only.</p><div class="scroll"><table><thead><tr><th>Variant</th><th>Window</th><th>Cohort</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>WR</th><th>Realized DD</th><th>Cad/day</th><th>Avg win</th><th>Avg loss</th><th>Payoff</th><th>Median</th><th>P05</th><th>P95</th><th>Median hold h</th><th>Max loss streak</th></tr></thead><tbody>${allRows}</tbody></table></div><h2>Fold and monthly robustness</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Window</th><th>Cohort</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>WR</th><th>Realized DD</th><th>Cad/day</th><th>Avg win</th><th>Avg loss</th><th>Payoff</th><th>Median</th><th>P05</th><th>P95</th><th>Median hold h</th><th>Max loss streak</th></tr></thead><tbody>${robustnessRows}</tbody></table></div><h2>Cost stress</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Extra round-trip bps</th><th>N</th><th>PnL</th><th>Avg PnL/trade</th><th>PF</th><th>Realized DD</th></tr></thead><tbody>${costRows}</tbody></table></div><h2>Signal-time market regimes</h2><div class="scroll"><table><thead><tr><th>Trend | Volatility | Breadth | Derivatives</th><th>Variant</th><th>N</th><th>PnL</th><th>PF</th><th>WR</th></tr></thead><tbody>${regimeRows(result.variants)}</tbody></table></div><h2>Trace funnel and reconciliation</h2><div class="scroll"><table><thead><tr><th>Variant</th><th>Reconcile</th><th>Setup identity source</th><th>Events</th><th>Top skips</th><th>Exit codes</th><th>Latest signal-time regime</th></tr></thead><tbody>${traceRows(result.variants)}</tbody></table></div><h2>Multiple testing / overfitting</h2><pre>${escapeHtml(JSON.stringify({ multipleTesting: result.multipleTesting, overfittingDiagnostics: result.overfittingDiagnostics }, null, 2))}</pre><h2>Lineage</h2><pre>${escapeHtml(JSON.stringify({ stage: spec.stage, universeSha256: spec.universe.sha256, lineage: spec.lineage ?? null, artifactHashes: result.artifactHashes }, null, 2))}</pre></main></body></html>`;
1797
1809
  };
1798
1810
 
1799
1811
  // src/lib/backtest/checkpoint.ts
@@ -2099,8 +2111,9 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
2099
2111
  var import_promises3 = __toESM(require("fs/promises"));
2100
2112
  var import_node_path4 = __toESM(require("path"));
2101
2113
  var summarizeCoreResearchTrace = async (filePaths = []) => {
2102
- const events = {};
2103
- const skipCounts = {};
2114
+ const events = /* @__PURE__ */ Object.create(null);
2115
+ const skipCounts = /* @__PURE__ */ Object.create(null);
2116
+ const exitCodes = /* @__PURE__ */ Object.create(null);
2104
2117
  for (const inputPath of filePaths) {
2105
2118
  const filePath = import_node_path4.default.resolve(inputPath);
2106
2119
  const text = await import_promises3.default.readFile(filePath, "utf8");
@@ -2120,6 +2133,9 @@ var summarizeCoreResearchTrace = async (filePaths = []) => {
2120
2133
  skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
2121
2134
  }
2122
2135
  }
2136
+ if (event.event === "position_exited" && event.exitCode) {
2137
+ exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
2138
+ }
2123
2139
  }
2124
2140
  }
2125
2141
  return {
@@ -2128,6 +2144,11 @@ var summarizeCoreResearchTrace = async (filePaths = []) => {
2128
2144
  Object.entries(skipCounts).sort(
2129
2145
  ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2130
2146
  )
2147
+ ),
2148
+ exitCodes: Object.fromEntries(
2149
+ Object.entries(exitCodes).sort(
2150
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2151
+ )
2131
2152
  )
2132
2153
  };
2133
2154
  };
@@ -6056,14 +6056,39 @@ var buildReplayChartSnapshot = (params) => {
6056
6056
  var import_chalk10 = __toESM(require("chalk"));
6057
6057
 
6058
6058
  // src/lib/runtimeParity.ts
6059
+ var resolveTradeParityTimestampOffset = (strategy, offset) => typeof offset === "function" ? offset(strategy) : offset;
6060
+ var buildTradeParityTimestampOffset = (strategies) => {
6061
+ const intervals = /* @__PURE__ */ new Map();
6062
+ for (const { strategyName, interval } of strategies) {
6063
+ const minutes = Number(interval);
6064
+ if (!Number.isFinite(minutes) || minutes <= 0) {
6065
+ throw new Error(`Invalid embedded replay interval for ${strategyName}`);
6066
+ }
6067
+ intervals.set(strategyName, minutes * 60 * 1e3);
6068
+ }
6069
+ return (strategy) => {
6070
+ const offsetMs = intervals.get(strategy);
6071
+ if (offsetMs == null) {
6072
+ throw new Error(`Missing embedded replay interval for ${strategy}`);
6073
+ }
6074
+ return offsetMs;
6075
+ };
6076
+ };
6059
6077
  var toFiniteNumberOrNull2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
6060
6078
  var toGroupKey = (entry) => `${entry.strategy}::${entry.symbol}::${entry.direction}`;
6061
6079
  var toRuntimeDuplicateKey = (entry) => `${toGroupKey(entry)}::${entry.timestamp}`;
6062
6080
  var getBacktestParityComparisonTimestamp = (entry, timestampOffsetMs = 0) => {
6063
- if (timestampOffsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + timestampOffsetMs) {
6081
+ const offsetMs = resolveTradeParityTimestampOffset(
6082
+ entry.strategy,
6083
+ timestampOffsetMs
6084
+ );
6085
+ if (!Number.isFinite(offsetMs) || offsetMs < 0) {
6086
+ throw new Error(`Invalid backtest timestamp offset for ${entry.strategy}`);
6087
+ }
6088
+ if (offsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + offsetMs) {
6064
6089
  return entry.timestamp;
6065
6090
  }
6066
- return (entry.signalTimestamp ?? entry.timestamp) + timestampOffsetMs;
6091
+ return (entry.signalTimestamp ?? entry.timestamp) + offsetMs;
6067
6092
  };
6068
6093
  var sortTradeParityEntries = (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction) || left.id.localeCompare(right.id);
6069
6094
  var buildPriceDeltaPct = (leftPrice, rightPrice) => {
@@ -6869,9 +6894,13 @@ var buildCostDetail = (entry) => ({
6869
6894
  totalFee: entry.totalFee ?? null
6870
6895
  });
6871
6896
  var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
6897
+ const offsetMs = resolveTradeParityTimestampOffset(
6898
+ entry.strategy,
6899
+ timestampOffsetMs
6900
+ );
6872
6901
  const comparisonTimestamp = getBacktestParityComparisonTimestamp(
6873
6902
  entry,
6874
- timestampOffsetMs
6903
+ offsetMs
6875
6904
  );
6876
6905
  return {
6877
6906
  source: "backtest",
@@ -6881,7 +6910,7 @@ var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
6881
6910
  qty: entry.qty ?? null,
6882
6911
  timestamp: entry.timestamp,
6883
6912
  signalTimestamp: entry.signalTimestamp ?? null,
6884
- comparisonTimestamp: timestampOffsetMs === 0 ? null : comparisonTimestamp,
6913
+ comparisonTimestamp: offsetMs === 0 ? null : comparisonTimestamp,
6885
6914
  price: entry.price,
6886
6915
  exitType: entry.exitType ?? null,
6887
6916
  exitTimestamp: entry.exitTimestamp ?? null,
@@ -7340,25 +7369,29 @@ var buildReplayMismatchDrilldown = ({
7340
7369
  let runtimeOnlyArtifactsIncluded = 0;
7341
7370
  let backtestOnlyArtifactsIncluded = 0;
7342
7371
  const runtimeOnlyDiagnostics = runtimeOnly.map((entry, index) => {
7372
+ const entryOffsetMs = resolveTradeParityTimestampOffset(
7373
+ getEntryStrategy(entry) ?? "",
7374
+ backtestTimestampOffsetMs
7375
+ );
7343
7376
  const includeArtifacts = index < artifactLimit;
7344
7377
  const nearestCandidate = nearestByEntry.get(entry);
7345
7378
  const nearestReplaySignal = findNearestSignal({
7346
7379
  entry,
7347
7380
  signals: replaySignals,
7348
7381
  toleranceMs,
7349
- timestampOffsetMs: backtestTimestampOffsetMs
7382
+ timestampOffsetMs: entryOffsetMs
7350
7383
  });
7351
7384
  const nearestReplayEvaluation = findNearestEvaluation({
7352
7385
  entry,
7353
7386
  evaluations: replaySignalEvaluations,
7354
7387
  toleranceMs,
7355
- timestampOffsetMs: backtestTimestampOffsetMs
7388
+ timestampOffsetMs: entryOffsetMs
7356
7389
  });
7357
7390
  const nearestReplayEvaluationOutcome = findNearestReplayEvaluationOutcome({
7358
7391
  entry,
7359
7392
  lineageScopes: replayLineageScopes,
7360
7393
  toleranceMs,
7361
- timestampOffsetMs: backtestTimestampOffsetMs
7394
+ timestampOffsetMs: entryOffsetMs
7362
7395
  });
7363
7396
  const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestReplayEvaluationOutcome ? {
7364
7397
  classification: "core_skipped",
@@ -8593,6 +8626,9 @@ var saveAndPrintReplayRuntimeComparison = async ({
8593
8626
  expectedUserName: replayUserName,
8594
8627
  expectedWindow: window
8595
8628
  }) : null;
8629
+ const replayTimestampOffset = runtimeEvidence ? buildTradeParityTimestampOffset(
8630
+ (0, runtimeEvidenceDeployment_exports.activeRuntimeEvidenceStrategies)(runtimeEvidence.deployment)
8631
+ ) : getReplayEntryTimestampCompareOffsetMs();
8596
8632
  const [
8597
8633
  rawRuntimeTrades,
8598
8634
  runtimeSignals,
@@ -8702,7 +8738,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8702
8738
  runtimeEntries: runtimeDedupe.entries,
8703
8739
  backtestEntries: comparableBacktestEntries,
8704
8740
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8705
- backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8741
+ backtestTimestampOffsetMs: replayTimestampOffset
8706
8742
  });
8707
8743
  const details = buildReplayRuntimeComparisonDetails({
8708
8744
  matched: comparison.matched,
@@ -8711,7 +8747,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8711
8747
  runtimeEntries: runtimeDedupe.entries,
8712
8748
  backtestEntries: comparableBacktestEntries,
8713
8749
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8714
- backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
8750
+ backtestTimestampOffsetMs: replayTimestampOffset,
8715
8751
  runtimeSignals: comparable.runtimeSignals,
8716
8752
  runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
8717
8753
  replaySignals,
@@ -6059,14 +6059,39 @@ var buildReplayChartSnapshot = (params) => {
6059
6059
  var import_chalk10 = __toESM(require("chalk"));
6060
6060
 
6061
6061
  // src/lib/runtimeParity.ts
6062
+ var resolveTradeParityTimestampOffset = (strategy, offset) => typeof offset === "function" ? offset(strategy) : offset;
6063
+ var buildTradeParityTimestampOffset = (strategies) => {
6064
+ const intervals = /* @__PURE__ */ new Map();
6065
+ for (const { strategyName, interval } of strategies) {
6066
+ const minutes = Number(interval);
6067
+ if (!Number.isFinite(minutes) || minutes <= 0) {
6068
+ throw new Error(`Invalid embedded replay interval for ${strategyName}`);
6069
+ }
6070
+ intervals.set(strategyName, minutes * 60 * 1e3);
6071
+ }
6072
+ return (strategy) => {
6073
+ const offsetMs = intervals.get(strategy);
6074
+ if (offsetMs == null) {
6075
+ throw new Error(`Missing embedded replay interval for ${strategy}`);
6076
+ }
6077
+ return offsetMs;
6078
+ };
6079
+ };
6062
6080
  var toFiniteNumberOrNull2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
6063
6081
  var toGroupKey = (entry) => `${entry.strategy}::${entry.symbol}::${entry.direction}`;
6064
6082
  var toRuntimeDuplicateKey = (entry) => `${toGroupKey(entry)}::${entry.timestamp}`;
6065
6083
  var getBacktestParityComparisonTimestamp = (entry, timestampOffsetMs = 0) => {
6066
- if (timestampOffsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + timestampOffsetMs) {
6084
+ const offsetMs = resolveTradeParityTimestampOffset(
6085
+ entry.strategy,
6086
+ timestampOffsetMs
6087
+ );
6088
+ if (!Number.isFinite(offsetMs) || offsetMs < 0) {
6089
+ throw new Error(`Invalid backtest timestamp offset for ${entry.strategy}`);
6090
+ }
6091
+ if (offsetMs > 0 && entry.signalTimestamp != null && entry.timestamp >= entry.signalTimestamp + offsetMs) {
6067
6092
  return entry.timestamp;
6068
6093
  }
6069
- return (entry.signalTimestamp ?? entry.timestamp) + timestampOffsetMs;
6094
+ return (entry.signalTimestamp ?? entry.timestamp) + offsetMs;
6070
6095
  };
6071
6096
  var sortTradeParityEntries = (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction) || left.id.localeCompare(right.id);
6072
6097
  var buildPriceDeltaPct = (leftPrice, rightPrice) => {
@@ -6872,9 +6897,13 @@ var buildCostDetail = (entry) => ({
6872
6897
  totalFee: entry.totalFee ?? null
6873
6898
  });
6874
6899
  var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
6900
+ const offsetMs = resolveTradeParityTimestampOffset(
6901
+ entry.strategy,
6902
+ timestampOffsetMs
6903
+ );
6875
6904
  const comparisonTimestamp = getBacktestParityComparisonTimestamp(
6876
6905
  entry,
6877
- timestampOffsetMs
6906
+ offsetMs
6878
6907
  );
6879
6908
  return {
6880
6909
  source: "backtest",
@@ -6884,7 +6913,7 @@ var toBacktestParityDetail = (entry, timestampOffsetMs = 0) => {
6884
6913
  qty: entry.qty ?? null,
6885
6914
  timestamp: entry.timestamp,
6886
6915
  signalTimestamp: entry.signalTimestamp ?? null,
6887
- comparisonTimestamp: timestampOffsetMs === 0 ? null : comparisonTimestamp,
6916
+ comparisonTimestamp: offsetMs === 0 ? null : comparisonTimestamp,
6888
6917
  price: entry.price,
6889
6918
  exitType: entry.exitType ?? null,
6890
6919
  exitTimestamp: entry.exitTimestamp ?? null,
@@ -7343,25 +7372,29 @@ var buildReplayMismatchDrilldown = ({
7343
7372
  let runtimeOnlyArtifactsIncluded = 0;
7344
7373
  let backtestOnlyArtifactsIncluded = 0;
7345
7374
  const runtimeOnlyDiagnostics = runtimeOnly.map((entry, index) => {
7375
+ const entryOffsetMs = resolveTradeParityTimestampOffset(
7376
+ getEntryStrategy(entry) ?? "",
7377
+ backtestTimestampOffsetMs
7378
+ );
7346
7379
  const includeArtifacts = index < artifactLimit;
7347
7380
  const nearestCandidate = nearestByEntry.get(entry);
7348
7381
  const nearestReplaySignal = findNearestSignal({
7349
7382
  entry,
7350
7383
  signals: replaySignals,
7351
7384
  toleranceMs,
7352
- timestampOffsetMs: backtestTimestampOffsetMs
7385
+ timestampOffsetMs: entryOffsetMs
7353
7386
  });
7354
7387
  const nearestReplayEvaluation = findNearestEvaluation({
7355
7388
  entry,
7356
7389
  evaluations: replaySignalEvaluations,
7357
7390
  toleranceMs,
7358
- timestampOffsetMs: backtestTimestampOffsetMs
7391
+ timestampOffsetMs: entryOffsetMs
7359
7392
  });
7360
7393
  const nearestReplayEvaluationOutcome = findNearestReplayEvaluationOutcome({
7361
7394
  entry,
7362
7395
  lineageScopes: replayLineageScopes,
7363
7396
  toleranceMs,
7364
- timestampOffsetMs: backtestTimestampOffsetMs
7397
+ timestampOffsetMs: entryOffsetMs
7365
7398
  });
7366
7399
  const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestReplayEvaluationOutcome ? {
7367
7400
  classification: "core_skipped",
@@ -8596,6 +8629,9 @@ var saveAndPrintReplayRuntimeComparison = async ({
8596
8629
  expectedUserName: replayUserName,
8597
8630
  expectedWindow: window
8598
8631
  }) : null;
8632
+ const replayTimestampOffset = runtimeEvidence ? buildTradeParityTimestampOffset(
8633
+ (0, runtimeEvidenceDeployment_exports.activeRuntimeEvidenceStrategies)(runtimeEvidence.deployment)
8634
+ ) : getReplayEntryTimestampCompareOffsetMs();
8599
8635
  const [
8600
8636
  rawRuntimeTrades,
8601
8637
  runtimeSignals,
@@ -8705,7 +8741,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8705
8741
  runtimeEntries: runtimeDedupe.entries,
8706
8742
  backtestEntries: comparableBacktestEntries,
8707
8743
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8708
- backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8744
+ backtestTimestampOffsetMs: replayTimestampOffset
8709
8745
  });
8710
8746
  const details = buildReplayRuntimeComparisonDetails({
8711
8747
  matched: comparison.matched,
@@ -8714,7 +8750,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8714
8750
  runtimeEntries: runtimeDedupe.entries,
8715
8751
  backtestEntries: comparableBacktestEntries,
8716
8752
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8717
- backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
8753
+ backtestTimestampOffsetMs: replayTimestampOffset,
8718
8754
  runtimeSignals: comparable.runtimeSignals,
8719
8755
  runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
8720
8756
  replaySignals,
@@ -137,41 +137,52 @@ var buildRuntimeFeedbackReplayCommands = ({
137
137
  interval,
138
138
  startTime,
139
139
  endTime
140
- }) => [
141
- [
142
- cliPath,
143
- "replay",
144
- "--user",
145
- userName,
146
- "--connector",
147
- connectorName,
148
- "--deployment",
149
- deploymentId,
150
- "--timeframe",
151
- interval,
152
- "--startTime",
153
- String(startTime),
154
- "--endTime",
155
- String(endTime),
156
- "--runtimeEvidence",
157
- runtimeEvidencePath,
158
- "--cacheOnly"
159
- ],
160
- [
161
- cliPath,
162
- "replay-runtime-evidence",
163
- "--user",
164
- userName,
165
- "--runtimeEvidence",
166
- runtimeEvidencePath,
167
- "--startTime",
168
- String(startTime),
169
- "--endTime",
170
- String(endTime),
171
- "--out",
172
- replayEvidencePath
173
- ]
174
- ];
140
+ }) => {
141
+ const intervals = interval.split(",").map((value) => value.trim());
142
+ if (!intervals.length || intervals.some(
143
+ (value) => !value || !Number.isFinite(Number(value)) || Number(value) <= 0
144
+ )) {
145
+ throw new Error(`Invalid runtime feedback replay intervals: ${interval}`);
146
+ }
147
+ const replayTimeframe = intervals.sort(
148
+ (left, right) => Number(left) - Number(right)
149
+ )[0];
150
+ return [
151
+ [
152
+ cliPath,
153
+ "replay",
154
+ "--user",
155
+ userName,
156
+ "--connector",
157
+ connectorName,
158
+ "--deployment",
159
+ deploymentId,
160
+ "--timeframe",
161
+ replayTimeframe,
162
+ "--startTime",
163
+ String(startTime),
164
+ "--endTime",
165
+ String(endTime),
166
+ "--runtimeEvidence",
167
+ runtimeEvidencePath,
168
+ "--cacheOnly"
169
+ ],
170
+ [
171
+ cliPath,
172
+ "replay-runtime-evidence",
173
+ "--user",
174
+ userName,
175
+ "--runtimeEvidence",
176
+ runtimeEvidencePath,
177
+ "--startTime",
178
+ String(startTime),
179
+ "--endTime",
180
+ String(endTime),
181
+ "--out",
182
+ replayEvidencePath
183
+ ]
184
+ ];
185
+ };
175
186
  var runLoggedCommand = async ({
176
187
  executable,
177
188
  command,
@@ -258,10 +269,8 @@ var runRuntimeFeedbackReplay = async ({
258
269
  const intervals = new Set(
259
270
  activeStrategies.map(({ interval }) => String(interval))
260
271
  );
261
- if (intervals.size !== 1) {
262
- throw new Error(
263
- `Runtime feedback replay requires one interval, received ${[...intervals].join(",")}`
264
- );
272
+ if (!intervals.size) {
273
+ throw new Error("Runtime feedback replay requires an active interval");
265
274
  }
266
275
  await import_promises.default.mkdir(outDir, { recursive: true });
267
276
  const existing = await import_promises.default.readdir(outDir);
@@ -282,7 +291,7 @@ var runRuntimeFeedbackReplay = async ({
282
291
  userName: runtimeEvidenceBundle.manifest.userName,
283
292
  connectorName: deployment.connectorName,
284
293
  deploymentId: deployment.id,
285
- interval: [...intervals][0],
294
+ interval: [...intervals].join(","),
286
295
  startTime: runtimeEvidenceBundle.manifest.window.startTime,
287
296
  endTime: runtimeEvidenceBundle.manifest.window.endTime
288
297
  });