@tradejs/cli 3.1.29-beta.266 → 3.1.29-beta.269

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.
package/dist/cli.js CHANGED
@@ -1565,7 +1565,7 @@ var init_aiExport2 = __esm({
1565
1565
  });
1566
1566
 
1567
1567
  // src/lib/aiPocketSearchCli.ts
1568
- var DAY_MS, DEFAULT_MIN_SUPPORT, LOW_CADENCE_EVENT_LIMIT, LOW_CADENCE_MAX_EVENT_SHARE, countIndependentEvents, getPeriodDays, clampInt, resolveAiPocketCadenceProfile, AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, splitAiPocketResearchRowsByTimestamp, sealAiPocketTestPartition, readAiPocketSearchCliOption;
1568
+ var DAY_MS, DEFAULT_MIN_SUPPORT, LOW_CADENCE_EVENT_LIMIT, LOW_CADENCE_MAX_EVENT_SHARE, countIndependentEvents, getPeriodDays, clampInt, resolveAiPocketCadenceProfile, AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, parseAiPocketTestSince, classifyAiPocketLabelHorizon, splitAiPocketResearchRowsByTimestamp, sealAiPocketTestPartition, readAiPocketSearchCliOption;
1569
1569
  var init_aiPocketSearchCli = __esm({
1570
1570
  "src/lib/aiPocketSearchCli.ts"() {
1571
1571
  "use strict";
@@ -1660,7 +1660,70 @@ var init_aiPocketSearchCli = __esm({
1660
1660
  validationSplit: "0",
1661
1661
  testSplit: "0.4"
1662
1662
  };
1663
- splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
1663
+ parseAiPocketTestSince = (value) => {
1664
+ const text = typeof value === "string" ? value.trim() : "";
1665
+ const numeric = typeof value === "number" || /^\d+$/.test(text);
1666
+ const iso = text.match(
1667
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/
1668
+ );
1669
+ const validCalendar = iso != null && Number(iso[2]) >= 1 && Number(iso[2]) <= 12 && Number(iso[3]) >= 1 && Number(iso[3]) <= new Date(Date.UTC(Number(iso[1]), Number(iso[2]), 0)).getUTCDate() && Number(iso[4]) < 24 && Number(iso[5]) < 60 && Number(iso[6] ?? 0) < 60;
1670
+ const timestamp = numeric ? Number(value) : validCalendar ? Date.parse(text) : NaN;
1671
+ if (!Number.isSafeInteger(timestamp) || timestamp <= 0 || timestamp > 864e13) {
1672
+ throw new Error("--testSince requires a valid UTC timestamp or epoch ms");
1673
+ }
1674
+ return timestamp;
1675
+ };
1676
+ classifyAiPocketLabelHorizon = (decisionTimestamp, exitTimestamp, cutoff) => {
1677
+ if (decisionTimestamp >= cutoff) return null;
1678
+ if (typeof exitTimestamp !== "number" || !Number.isFinite(exitTimestamp)) {
1679
+ return "unknown";
1680
+ }
1681
+ if (exitTimestamp < decisionTimestamp) {
1682
+ throw new Error(
1683
+ "--testSince requires label exitTimestamp >= decision timestamp"
1684
+ );
1685
+ }
1686
+ return exitTimestamp >= cutoff ? "crossing" : null;
1687
+ };
1688
+ splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0, testSince) => {
1689
+ if (testSince != null) {
1690
+ const cutoff = parseAiPocketTestSince(testSince);
1691
+ if (validationSplit !== 0) {
1692
+ throw new Error("--testSince requires --validationSplit 0");
1693
+ }
1694
+ const trainRows = [];
1695
+ const testRows = [];
1696
+ const labelHorizonPurge = {
1697
+ crossing: 0,
1698
+ unknown: 0
1699
+ };
1700
+ for (const row of rows) {
1701
+ if (typeof row.timestamp !== "number" || !Number.isFinite(row.timestamp)) {
1702
+ throw new Error("--testSince requires a finite timestamp on every row");
1703
+ }
1704
+ const purge = classifyAiPocketLabelHorizon(
1705
+ row.timestamp,
1706
+ row.exitTimestamp,
1707
+ cutoff
1708
+ );
1709
+ if (purge) {
1710
+ labelHorizonPurge[purge] += 1;
1711
+ continue;
1712
+ }
1713
+ (row.timestamp < cutoff ? trainRows : testRows).push(row);
1714
+ }
1715
+ if (!trainRows.length || !testRows.length) {
1716
+ throw new Error(
1717
+ "--testSince requires non-empty development and test partitions"
1718
+ );
1719
+ }
1720
+ return {
1721
+ trainRows,
1722
+ validationRows: [],
1723
+ testRows,
1724
+ labelHorizonPurge
1725
+ };
1726
+ }
1664
1727
  const timestamps = [
1665
1728
  ...new Set(
1666
1729
  rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
@@ -2043,6 +2106,12 @@ var init_commandOptions = __esm({
2043
2106
  if (sealTest && testSplit <= 0) {
2044
2107
  throw new Error("--sealTest requires a positive --testSplit");
2045
2108
  }
2109
+ const explicitTestSince = hasCliOption2(argv2, "testSince");
2110
+ const testSinceInput = explicitTestSince ? readAiPocketSearchCliOption({ argv: argv2, longName: "testSince" }) : flags35.testSince;
2111
+ const testSince = explicitTestSince || testSinceInput != null && testSinceInput !== "" ? parseAiPocketTestSince(testSinceInput) : null;
2112
+ if (testSince != null && validationSplit !== 0) {
2113
+ throw new Error("--testSince requires --validationSplit 0");
2114
+ }
2046
2115
  const maxEventCountShare = normalizeUnitRatio(
2047
2116
  readAiPocketSearchCliOption({
2048
2117
  argv: argv2,
@@ -2083,6 +2152,7 @@ var init_commandOptions = __esm({
2083
2152
  maxCombinations: normalizePositiveInt(flags35.maxCombinations, 6e4),
2084
2153
  validationSplit,
2085
2154
  testSplit,
2155
+ testSince,
2086
2156
  sealTest,
2087
2157
  explicitMinValidationSupport: normalizeInt(flags35.minValidationSupport, 0),
2088
2158
  explicitMinEvents: normalizeInt(flags35.minEvents, 0),
@@ -4079,6 +4149,19 @@ var init_markdownReport = __esm({
4079
4149
  ["test_rows", run.testRows],
4080
4150
  ["validation_split", formatMdPercent(run.validationSplit)],
4081
4151
  ["test_split", formatMdPercent(run.testSplit)],
4152
+ ["partition_mode", run.partitionMode ?? "timestamp-ratio"],
4153
+ [
4154
+ "purged_development_crossing_labels",
4155
+ run.labelHorizonPurge?.crossing ?? 0
4156
+ ],
4157
+ [
4158
+ "purged_development_unknown_labels",
4159
+ run.labelHorizonPurge?.unknown ?? 0
4160
+ ],
4161
+ [
4162
+ "test_since",
4163
+ run.testSince == null ? "n/a" : new Date(run.testSince).toISOString()
4164
+ ],
4082
4165
  ["test_sealed", run.sealedTest?.sealed ? "yes" : "no"],
4083
4166
  ["sealed_test_rows", run.sealedTest?.rows ?? run.testRows],
4084
4167
  ["sealed_test_events", run.sealedTest?.events ?? "n/a"],
@@ -4855,6 +4938,11 @@ var init_presenter = __esm({
4855
4938
  ["max_combinations", import_chalk4.default.magenta(String(maxCombinations))],
4856
4939
  ["validation_split", import_chalk4.default.magenta(formatRatio(validationSplit))],
4857
4940
  ["test_split", import_chalk4.default.magenta(formatRatio(testSplit))],
4941
+ ["partition_mode", run.partitionMode ?? "timestamp-ratio"],
4942
+ [
4943
+ "test_since",
4944
+ run.testSince == null ? "n/a" : new Date(run.testSince).toISOString()
4945
+ ],
4858
4946
  [
4859
4947
  "test_evidence",
4860
4948
  sealedTest.sealed ? import_chalk4.default.green(
@@ -5248,6 +5336,7 @@ var init_dataset = __esm({
5248
5336
  import_registry = require("@tradejs/node/registry");
5249
5337
  init_aiPocketSearch();
5250
5338
  init_aiTrainDataset();
5339
+ init_aiPocketSearchCli();
5251
5340
  normalizeQuality = (analysis) => {
5252
5341
  const quality = Number(analysis?.quality);
5253
5342
  return Number.isFinite(quality) ? Math.round(quality) : null;
@@ -5412,6 +5501,10 @@ var init_dataset = __esm({
5412
5501
  let failed = 0;
5413
5502
  const errors = [];
5414
5503
  const rows = [];
5504
+ const labelHorizonPurge = {
5505
+ crossing: 0,
5506
+ unknown: 0
5507
+ };
5415
5508
  const excludedFeaturePaths = /* @__PURE__ */ new Map();
5416
5509
  await (0, import_ai4.streamAiDatasetRows)({
5417
5510
  filePaths: prepared.filePaths,
@@ -5422,12 +5515,43 @@ var init_dataset = __esm({
5422
5515
  if (resolvedStrategyName === "unknown" && typeof row.strategyName === "string" && row.strategyName.trim()) {
5423
5516
  resolvedStrategyName = row.strategyName.trim();
5424
5517
  }
5518
+ if (options.testSince != null && (typeof row.timestamp !== "number" || !Number.isFinite(row.timestamp))) {
5519
+ throw new Error(
5520
+ "--testSince requires a finite numeric timestamp on every export row"
5521
+ );
5522
+ }
5425
5523
  const timestamp = getDatasetRowTimestamp(row);
5426
5524
  if (prepared.sinceTimestamp != null && (timestamp == null || timestamp < prepared.sinceTimestamp) || options.untilTimestamp != null && (timestamp == null || timestamp > options.untilTimestamp)) {
5427
5525
  dateSkipped += 1;
5428
5526
  onProgress?.({ symbol: row.symbol, status: "date-skip" });
5429
5527
  return;
5430
5528
  }
5529
+ if (options.testSince != null) {
5530
+ const purge = classifyAiPocketLabelHorizon(
5531
+ timestamp,
5532
+ row.tradeResult?.exitTimestamp,
5533
+ options.testSince
5534
+ );
5535
+ if (purge) {
5536
+ labelHorizonPurge[purge] += 1;
5537
+ return;
5538
+ }
5539
+ if (options.sealTest && timestamp >= options.testSince) {
5540
+ rows.push({
5541
+ signalId: row.signalId,
5542
+ strategy: row.strategyName,
5543
+ symbol: row.symbol,
5544
+ direction: row.direction,
5545
+ timestamp,
5546
+ profit: 0,
5547
+ profitableTrade: false,
5548
+ aiApproved: false,
5549
+ quality: null,
5550
+ features: {}
5551
+ });
5552
+ return;
5553
+ }
5554
+ }
5431
5555
  try {
5432
5556
  const signal = extractSignalFromAiDatasetRow(row);
5433
5557
  const payload = (0, import_ai5.buildAiPayload)(signal);
@@ -5457,6 +5581,7 @@ var init_dataset = __esm({
5457
5581
  symbol: row.symbol,
5458
5582
  direction: row.direction,
5459
5583
  timestamp,
5584
+ ...options.testSince != null ? { exitTimestamp: row.tradeResult?.exitTimestamp } : {},
5460
5585
  profit,
5461
5586
  profitableTrade: profit > 0,
5462
5587
  aiApproved: isAiApproval(row, analysis, options.minQuality),
@@ -5479,6 +5604,7 @@ var init_dataset = __esm({
5479
5604
  }
5480
5605
  });
5481
5606
  return {
5607
+ labelHorizonPurge,
5482
5608
  rows,
5483
5609
  resolvedStrategyName,
5484
5610
  scanned,
@@ -5915,7 +6041,8 @@ var init_research = __esm({
5915
6041
  runAiPocketSearchResearch = ({
5916
6042
  rows,
5917
6043
  options,
5918
- onProgress
6044
+ onProgress,
6045
+ datasetLabelHorizonPurge
5919
6046
  }) => {
5920
6047
  const {
5921
6048
  direction,
@@ -5923,6 +6050,7 @@ var init_research = __esm({
5923
6050
  qualityThresholds,
5924
6051
  validationSplit,
5925
6052
  testSplit,
6053
+ testSince,
5926
6054
  sealTest,
5927
6055
  cadenceMode,
5928
6056
  explicitMinSupport,
@@ -5947,14 +6075,30 @@ var init_research = __esm({
5947
6075
  coverageMode
5948
6076
  } = options;
5949
6077
  const directionRows = direction ? rows.filter((row) => row.direction === direction) : rows;
5950
- const scopeRows = resolveScopeRows(directionRows, scope);
5951
- const fullSplit = splitAiPocketResearchRowsByTimestamp(
6078
+ const calendarSplit = testSince != null ? splitAiPocketResearchRowsByTimestamp(
6079
+ rows,
6080
+ validationSplit,
6081
+ testSplit,
6082
+ testSince
6083
+ ) : null;
6084
+ const inDirection = (row) => !direction || row.direction === direction;
6085
+ const fullSplit = calendarSplit ? {
6086
+ trainRows: calendarSplit.trainRows.filter(inDirection),
6087
+ validationRows: calendarSplit.validationRows.filter(inDirection),
6088
+ testRows: calendarSplit.testRows.filter(inDirection)
6089
+ } : splitAiPocketResearchRowsByTimestamp(
5952
6090
  directionRows,
5953
6091
  validationSplit,
5954
6092
  testSplit
5955
6093
  );
6094
+ if (testSince != null && (!fullSplit.trainRows.length || !fullSplit.testRows.length)) {
6095
+ throw new Error(
6096
+ "--testSince requires non-empty development and test partitions for the selected direction"
6097
+ );
6098
+ }
5956
6099
  const sealedFullSplit = sealAiPocketTestPartition(fullSplit, sealTest);
5957
6100
  const discoveryRows = sealedFullSplit.discoveryRows;
6101
+ const scopeRows = resolveScopeRows(discoveryRows, scope);
5958
6102
  const trainRows = resolveScopeRows(fullSplit.trainRows, scope);
5959
6103
  const validationRows = resolveScopeRows(fullSplit.validationRows, scope);
5960
6104
  const testRows = resolveScopeRows(sealedFullSplit.searchTestRows, scope);
@@ -6107,6 +6251,12 @@ var init_research = __esm({
6107
6251
  }
6108
6252
  }
6109
6253
  return {
6254
+ ...calendarSplit ? {
6255
+ labelHorizonPurge: {
6256
+ crossing: (calendarSplit.labelHorizonPurge?.crossing ?? 0) + (datasetLabelHorizonPurge?.crossing ?? 0),
6257
+ unknown: (calendarSplit.labelHorizonPurge?.unknown ?? 0) + (datasetLabelHorizonPurge?.unknown ?? 0)
6258
+ }
6259
+ } : {},
6110
6260
  scopeRows,
6111
6261
  trainRows,
6112
6262
  validationRows,
@@ -6154,6 +6304,7 @@ var init_command = __esm({
6154
6304
  maxCombinations,
6155
6305
  validationSplit,
6156
6306
  testSplit,
6307
+ testSince,
6157
6308
  maxBatch,
6158
6309
  maxSymbolCountShare,
6159
6310
  objective,
@@ -6201,6 +6352,7 @@ var init_command = __esm({
6201
6352
  const research = runAiPocketSearchResearch({
6202
6353
  rows,
6203
6354
  options,
6355
+ datasetLabelHorizonPurge: evaluatedDataset.labelHorizonPurge,
6204
6356
  onProgress: createAiPocketSearchResearchProgressPresenter()
6205
6357
  });
6206
6358
  const {
@@ -6274,6 +6426,9 @@ var init_command = __esm({
6274
6426
  objective,
6275
6427
  validationSplit,
6276
6428
  testSplit,
6429
+ testSince,
6430
+ partitionMode: testSince == null ? "timestamp-ratio" : "calendar-cutoff",
6431
+ ...research.labelHorizonPurge ? { labelHorizonPurge: research.labelHorizonPurge } : {},
6277
6432
  sealedTest,
6278
6433
  minValidationSupport: search.stats.cadence.minValidationSupport,
6279
6434
  reportPath,
@@ -6423,6 +6578,11 @@ var init_aiPocketSearch2 = __esm({
6423
6578
  "Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
6424
6579
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
6425
6580
  );
6581
+ import_args3.default.option(
6582
+ ["z", "testSince"],
6583
+ "Fixed calendar test boundary (UTC ISO or epoch ms); overrides the split ratio and requires validationSplit 0",
6584
+ ""
6585
+ );
6426
6586
  import_args3.default.option(
6427
6587
  ["w", "sealTest"],
6428
6588
  "Reserve test bounds without exposing its rows or economics to discovery",
@@ -28332,7 +28492,12 @@ var init_artifacts = __esm({
28332
28492
  "controlPnl",
28333
28493
  "candidatePnl",
28334
28494
  "pnlDelta",
28335
- "exitReasonChanged"
28495
+ "controlExitReason",
28496
+ "candidateExitReason",
28497
+ "exitReasonChanged",
28498
+ "controlExitCode",
28499
+ "candidateExitCode",
28500
+ "exitCodeChanged"
28336
28501
  ].map(csv).join(",") + "\n"
28337
28502
  );
28338
28503
  for (const comparison of params.comparisons) {
@@ -28350,7 +28515,12 @@ var init_artifacts = __esm({
28350
28515
  pair.control.netProfit,
28351
28516
  pair.candidate.netProfit,
28352
28517
  pair.pnlDelta,
28353
- pair.exitReasonChanged
28518
+ pair.control.exitReason,
28519
+ pair.candidate.exitReason,
28520
+ pair.exitReasonChanged,
28521
+ pair.control.exitCode,
28522
+ pair.candidate.exitCode,
28523
+ pair.exitCodeChanged
28354
28524
  ].map(csv).join(",") + "\n"
28355
28525
  );
28356
28526
  }
@@ -28995,6 +29165,7 @@ var init_comparison = __esm({
28995
29165
  candidate: candidateTrade,
28996
29166
  pnlDelta: candidateTrade.netProfit - controlTrade.netProfit,
28997
29167
  exitReasonChanged: candidateTrade.exitReason !== controlTrade.exitReason,
29168
+ exitCodeChanged: candidateTrade.exitCode !== controlTrade.exitCode,
28998
29169
  entryTimestampDeltaMs: candidateTrade.entryTimestamp - controlTrade.entryTimestamp,
28999
29170
  exitTimestampDeltaMs: candidateTrade.exitTimestamp - controlTrade.exitTimestamp
29000
29171
  });
@@ -29212,6 +29383,7 @@ var init_dataset2 = __esm({
29212
29383
  "totalSlippageCost"
29213
29384
  ),
29214
29385
  exitReason: result.exitReason,
29386
+ ...result.exitCode ? { exitCode: result.exitCode } : {},
29215
29387
  regime: resolveCoreResearchRegime(row)
29216
29388
  };
29217
29389
  };
@@ -29255,7 +29427,7 @@ var init_dataset2 = __esm({
29255
29427
  }
29256
29428
  const primary = primaryRows[0];
29257
29429
  if (cycle.some(
29258
- (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
29430
+ (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason || trade.exitCode !== primary.exitCode
29259
29431
  )) {
29260
29432
  throw new Error(
29261
29433
  `Position cycle ${identity} contains inconsistent final exits`
@@ -29444,7 +29616,7 @@ var init_report = __esm({
29444
29616
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml4(entry.id)}</span>`).join("")}</div>`;
29445
29617
  };
29446
29618
  comparisonCards = (comparisons) => comparisons.map(
29447
- (comparison) => `<article class="card"><h3>${escapeHtml4(comparison.candidateId)} vs ${escapeHtml4(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>${format2(comparison.matchedIdentityPctOfControl)}%</dd><dt>Matched \u0394PnL</dt><dd>${format2(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format2(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format2(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format2(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format2(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format2(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format2(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml4(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
29619
+ (comparison) => `<article class="card"><h3>${escapeHtml4(comparison.candidateId)} vs ${escapeHtml4(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>${format2(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>${format2(comparison.cohorts.ALL.matchedPnlDelta)}</dd><dt>Removed control PnL</dt><dd>${format2(comparison.cohorts.ALL.controlOnlyPnl)}</dd><dt>Added candidate PnL</dt><dd>${format2(comparison.cohorts.ALL.candidateOnlyPnl)}</dd><dt>Aggregate \u0394PnL</dt><dd>${format2(comparison.cohorts.ALL.delta.pnl)}</dd><dt>Aggregate \u0394DD</dt><dd>${format2(comparison.cohorts.ALL.delta.realizedMaxDrawdown)}</dd><dt>Bootstrap P(\u0394&gt;0)</dt><dd>${format2(comparison.bootstrap.probabilityPositive, 4)}</dd><dt>Holm p</dt><dd>${format2(comparison.bootstrap.holmAdjustedPValue, 4)}</dd></dl><p>${comparison.selection.failedRules.length ? escapeHtml4(comparison.selection.failedRules.map((rule) => `${rule.scope}.${rule.metric} ${rule.expected}`).join("; ")) : "All preregistered rules passed."}</p></article>`
29448
29620
  ).join("");
29449
29621
  regimeRows = (variants) => {
29450
29622
  const keys = [
@@ -29458,7 +29630,7 @@ var init_report = __esm({
29458
29630
  ).join("");
29459
29631
  };
29460
29632
  traceRows = (variants) => variants.map(
29461
- (variant) => `<tr><td>${escapeHtml4(variant.variant.label)}</td><td>${escapeHtml4(variant.reconciliation.status)}</td><td>${escapeHtml4(JSON.stringify(variant.setupIdentitySources))}</td><td>${escapeHtml4(JSON.stringify(variant.traceFunnel.events))}</td><td>${escapeHtml4(JSON.stringify(variant.traceFunnel.skipCounts))}</td><td>${escapeHtml4(variant.latestSignalTimeRegime?.key ?? "n/a")}</td></tr>`
29633
+ (variant) => `<tr><td>${escapeHtml4(variant.variant.label)}</td><td>${escapeHtml4(variant.reconciliation.status)}</td><td>${escapeHtml4(JSON.stringify(variant.setupIdentitySources))}</td><td>${escapeHtml4(JSON.stringify(variant.traceFunnel.events))}</td><td>${escapeHtml4(JSON.stringify(variant.traceFunnel.skipCounts))}</td><td>${escapeHtml4(JSON.stringify(variant.traceFunnel.exitCodes))}</td><td>${escapeHtml4(variant.latestSignalTimeRegime?.key ?? "n/a")}</td></tr>`
29462
29634
  ).join("");
29463
29635
  buildCoreResearchHtml = (params) => {
29464
29636
  const { spec, result, tradesByVariant } = params;
@@ -29484,7 +29656,7 @@ var init_report = __esm({
29484
29656
  ([key, value]) => `<span class="${value === "present" ? "positive" : "negative"}">${escapeHtml4(key)}: ${value}</span>`
29485
29657
  ).join(
29486
29658
  ""
29487
- )}</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>${escapeHtml4(JSON.stringify({ multipleTesting: result.multipleTesting, overfittingDiagnostics: result.overfittingDiagnostics }, null, 2))}</pre><h2>Lineage</h2><pre>${escapeHtml4(JSON.stringify({ stage: spec.stage, universeSha256: spec.universe.sha256, lineage: spec.lineage ?? null, artifactHashes: result.artifactHashes }, null, 2))}</pre></main></body></html>`;
29659
+ )}</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>${escapeHtml4(JSON.stringify({ multipleTesting: result.multipleTesting, overfittingDiagnostics: result.overfittingDiagnostics }, null, 2))}</pre><h2>Lineage</h2><pre>${escapeHtml4(JSON.stringify({ stage: spec.stage, universeSha256: spec.universe.sha256, lineage: spec.lineage ?? null, artifactHashes: result.artifactHashes }, null, 2))}</pre></main></body></html>`;
29488
29660
  };
29489
29661
  }
29490
29662
  });
@@ -29786,8 +29958,9 @@ var init_trace = __esm({
29786
29958
  import_promises24 = __toESM(require("fs/promises"));
29787
29959
  import_node_path18 = __toESM(require("path"));
29788
29960
  summarizeCoreResearchTrace = async (filePaths = []) => {
29789
- const events = {};
29790
- const skipCounts = {};
29961
+ const events = /* @__PURE__ */ Object.create(null);
29962
+ const skipCounts = /* @__PURE__ */ Object.create(null);
29963
+ const exitCodes = /* @__PURE__ */ Object.create(null);
29791
29964
  for (const inputPath of filePaths) {
29792
29965
  const filePath = import_node_path18.default.resolve(inputPath);
29793
29966
  const text = await import_promises24.default.readFile(filePath, "utf8");
@@ -29807,6 +29980,9 @@ var init_trace = __esm({
29807
29980
  skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
29808
29981
  }
29809
29982
  }
29983
+ if (event.event === "position_exited" && event.exitCode) {
29984
+ exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
29985
+ }
29810
29986
  }
29811
29987
  }
29812
29988
  return {
@@ -29815,6 +29991,11 @@ var init_trace = __esm({
29815
29991
  Object.entries(skipCounts).sort(
29816
29992
  ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
29817
29993
  )
29994
+ ),
29995
+ exitCodes: Object.fromEntries(
29996
+ Object.entries(exitCodes).sort(
29997
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
29998
+ )
29818
29999
  )
29819
30000
  };
29820
30001
  };
@@ -1966,6 +1966,19 @@ var buildAiPocketMarkdownReport = ({
1966
1966
  ["test_rows", run.testRows],
1967
1967
  ["validation_split", formatMdPercent(run.validationSplit)],
1968
1968
  ["test_split", formatMdPercent(run.testSplit)],
1969
+ ["partition_mode", run.partitionMode ?? "timestamp-ratio"],
1970
+ [
1971
+ "purged_development_crossing_labels",
1972
+ run.labelHorizonPurge?.crossing ?? 0
1973
+ ],
1974
+ [
1975
+ "purged_development_unknown_labels",
1976
+ run.labelHorizonPurge?.unknown ?? 0
1977
+ ],
1978
+ [
1979
+ "test_since",
1980
+ run.testSince == null ? "n/a" : new Date(run.testSince).toISOString()
1981
+ ],
1969
1982
  ["test_sealed", run.sealedTest?.sealed ? "yes" : "no"],
1970
1983
  ["sealed_test_rows", run.sealedTest?.rows ?? run.testRows],
1971
1984
  ["sealed_test_events", run.sealedTest?.events ?? "n/a"],
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var aiPocketSearchCli_exports = {};
22
22
  __export(aiPocketSearchCli_exports, {
23
23
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS: () => AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS,
24
+ classifyAiPocketLabelHorizon: () => classifyAiPocketLabelHorizon,
25
+ parseAiPocketTestSince: () => parseAiPocketTestSince,
24
26
  readAiPocketSearchCliOption: () => readAiPocketSearchCliOption,
25
27
  resolveAiPocketCadenceProfile: () => resolveAiPocketCadenceProfile,
26
28
  sealAiPocketTestPartition: () => sealAiPocketTestPartition,
@@ -119,7 +121,70 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
119
121
  validationSplit: "0",
120
122
  testSplit: "0.4"
121
123
  };
122
- var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
124
+ var parseAiPocketTestSince = (value) => {
125
+ const text = typeof value === "string" ? value.trim() : "";
126
+ const numeric = typeof value === "number" || /^\d+$/.test(text);
127
+ const iso = text.match(
128
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/
129
+ );
130
+ const validCalendar = iso != null && Number(iso[2]) >= 1 && Number(iso[2]) <= 12 && Number(iso[3]) >= 1 && Number(iso[3]) <= new Date(Date.UTC(Number(iso[1]), Number(iso[2]), 0)).getUTCDate() && Number(iso[4]) < 24 && Number(iso[5]) < 60 && Number(iso[6] ?? 0) < 60;
131
+ const timestamp = numeric ? Number(value) : validCalendar ? Date.parse(text) : NaN;
132
+ if (!Number.isSafeInteger(timestamp) || timestamp <= 0 || timestamp > 864e13) {
133
+ throw new Error("--testSince requires a valid UTC timestamp or epoch ms");
134
+ }
135
+ return timestamp;
136
+ };
137
+ var classifyAiPocketLabelHorizon = (decisionTimestamp, exitTimestamp, cutoff) => {
138
+ if (decisionTimestamp >= cutoff) return null;
139
+ if (typeof exitTimestamp !== "number" || !Number.isFinite(exitTimestamp)) {
140
+ return "unknown";
141
+ }
142
+ if (exitTimestamp < decisionTimestamp) {
143
+ throw new Error(
144
+ "--testSince requires label exitTimestamp >= decision timestamp"
145
+ );
146
+ }
147
+ return exitTimestamp >= cutoff ? "crossing" : null;
148
+ };
149
+ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0, testSince) => {
150
+ if (testSince != null) {
151
+ const cutoff = parseAiPocketTestSince(testSince);
152
+ if (validationSplit !== 0) {
153
+ throw new Error("--testSince requires --validationSplit 0");
154
+ }
155
+ const trainRows = [];
156
+ const testRows = [];
157
+ const labelHorizonPurge = {
158
+ crossing: 0,
159
+ unknown: 0
160
+ };
161
+ for (const row of rows) {
162
+ if (typeof row.timestamp !== "number" || !Number.isFinite(row.timestamp)) {
163
+ throw new Error("--testSince requires a finite timestamp on every row");
164
+ }
165
+ const purge = classifyAiPocketLabelHorizon(
166
+ row.timestamp,
167
+ row.exitTimestamp,
168
+ cutoff
169
+ );
170
+ if (purge) {
171
+ labelHorizonPurge[purge] += 1;
172
+ continue;
173
+ }
174
+ (row.timestamp < cutoff ? trainRows : testRows).push(row);
175
+ }
176
+ if (!trainRows.length || !testRows.length) {
177
+ throw new Error(
178
+ "--testSince requires non-empty development and test partitions"
179
+ );
180
+ }
181
+ return {
182
+ trainRows,
183
+ validationRows: [],
184
+ testRows,
185
+ labelHorizonPurge
186
+ };
187
+ }
123
188
  const timestamps = [
124
189
  ...new Set(
125
190
  rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
@@ -201,6 +266,8 @@ var readAiPocketSearchCliOption = ({
201
266
  // Annotate the CommonJS export names for ESM import in node:
202
267
  0 && (module.exports = {
203
268
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS,
269
+ classifyAiPocketLabelHorizon,
270
+ parseAiPocketTestSince,
204
271
  readAiPocketSearchCliOption,
205
272
  resolveAiPocketCadenceProfile,
206
273
  sealAiPocketTestPartition,
@@ -691,7 +691,12 @@ var writeCoreResearchMatches = async (params) => {
691
691
  "controlPnl",
692
692
  "candidatePnl",
693
693
  "pnlDelta",
694
- "exitReasonChanged"
694
+ "controlExitReason",
695
+ "candidateExitReason",
696
+ "exitReasonChanged",
697
+ "controlExitCode",
698
+ "candidateExitCode",
699
+ "exitCodeChanged"
695
700
  ].map(csv).join(",") + "\n"
696
701
  );
697
702
  for (const comparison of params.comparisons) {
@@ -709,7 +714,12 @@ var writeCoreResearchMatches = async (params) => {
709
714
  pair.control.netProfit,
710
715
  pair.candidate.netProfit,
711
716
  pair.pnlDelta,
712
- pair.exitReasonChanged
717
+ pair.control.exitReason,
718
+ pair.candidate.exitReason,
719
+ pair.exitReasonChanged,
720
+ pair.control.exitCode,
721
+ pair.candidate.exitCode,
722
+ pair.exitCodeChanged
713
723
  ].map(csv).join(",") + "\n"
714
724
  );
715
725
  }
@@ -1326,6 +1336,7 @@ var compareCoreResearchVariants = (params) => {
1326
1336
  candidate: candidateTrade,
1327
1337
  pnlDelta: candidateTrade.netProfit - controlTrade.netProfit,
1328
1338
  exitReasonChanged: candidateTrade.exitReason !== controlTrade.exitReason,
1339
+ exitCodeChanged: candidateTrade.exitCode !== controlTrade.exitCode,
1329
1340
  entryTimestampDeltaMs: candidateTrade.entryTimestamp - controlTrade.entryTimestamp,
1330
1341
  exitTimestampDeltaMs: candidateTrade.exitTimestamp - controlTrade.exitTimestamp
1331
1342
  });
@@ -1537,6 +1548,7 @@ var normalizeCompletedTrade = (params) => {
1537
1548
  "totalSlippageCost"
1538
1549
  ),
1539
1550
  exitReason: result.exitReason,
1551
+ ...result.exitCode ? { exitCode: result.exitCode } : {},
1540
1552
  regime: resolveCoreResearchRegime(row)
1541
1553
  };
1542
1554
  };
@@ -1580,7 +1592,7 @@ var aggregatePositionCycles = (trades) => {
1580
1592
  }
1581
1593
  const primary = primaryRows[0];
1582
1594
  if (cycle.some(
1583
- (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
1595
+ (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason || trade.exitCode !== primary.exitCode
1584
1596
  )) {
1585
1597
  throw new Error(
1586
1598
  `Position cycle ${identity} contains inconsistent final exits`
@@ -1762,7 +1774,7 @@ var buildCurveSvg = (tradesByVariant, cohort, curve) => {
1762
1774
  )}</svg><div class="legend">${series.map((entry) => `<span><i style="background:${entry.color}"></i>${escapeHtml(entry.id)}</span>`).join("")}</div>`;
1763
1775
  };
1764
1776
  var comparisonCards = (comparisons) => comparisons.map(
1765
- (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>`
1777
+ (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>`
1766
1778
  ).join("");
1767
1779
  var regimeRows = (variants) => {
1768
1780
  const keys = [
@@ -1776,7 +1788,7 @@ var regimeRows = (variants) => {
1776
1788
  ).join("");
1777
1789
  };
1778
1790
  var traceRows = (variants) => variants.map(
1779
- (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>`
1791
+ (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>`
1780
1792
  ).join("");
1781
1793
  var buildCoreResearchHtml = (params) => {
1782
1794
  const { spec, result, tradesByVariant } = params;
@@ -1802,7 +1814,7 @@ var buildCoreResearchHtml = (params) => {
1802
1814
  ([key, value]) => `<span class="${value === "present" ? "positive" : "negative"}">${escapeHtml(key)}: ${value}</span>`
1803
1815
  ).join(
1804
1816
  ""
1805
- )}</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>`;
1817
+ )}</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>`;
1806
1818
  };
1807
1819
 
1808
1820
  // src/lib/backtest/checkpoint.ts
@@ -2108,8 +2120,9 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
2108
2120
  var import_promises3 = __toESM(require("fs/promises"));
2109
2121
  var import_node_path4 = __toESM(require("path"));
2110
2122
  var summarizeCoreResearchTrace = async (filePaths = []) => {
2111
- const events = {};
2112
- const skipCounts = {};
2123
+ const events = /* @__PURE__ */ Object.create(null);
2124
+ const skipCounts = /* @__PURE__ */ Object.create(null);
2125
+ const exitCodes = /* @__PURE__ */ Object.create(null);
2113
2126
  for (const inputPath of filePaths) {
2114
2127
  const filePath = import_node_path4.default.resolve(inputPath);
2115
2128
  const text = await import_promises3.default.readFile(filePath, "utf8");
@@ -2129,6 +2142,9 @@ var summarizeCoreResearchTrace = async (filePaths = []) => {
2129
2142
  skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
2130
2143
  }
2131
2144
  }
2145
+ if (event.event === "position_exited" && event.exitCode) {
2146
+ exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
2147
+ }
2132
2148
  }
2133
2149
  }
2134
2150
  return {
@@ -2137,6 +2153,11 @@ var summarizeCoreResearchTrace = async (filePaths = []) => {
2137
2153
  Object.entries(skipCounts).sort(
2138
2154
  ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2139
2155
  )
2156
+ ),
2157
+ exitCodes: Object.fromEntries(
2158
+ Object.entries(exitCodes).sort(
2159
+ ([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
2160
+ )
2140
2161
  )
2141
2162
  };
2142
2163
  };
@@ -127,7 +127,70 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
127
127
  validationSplit: "0",
128
128
  testSplit: "0.4"
129
129
  };
130
- var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
130
+ var parseAiPocketTestSince = (value) => {
131
+ const text = typeof value === "string" ? value.trim() : "";
132
+ const numeric = typeof value === "number" || /^\d+$/.test(text);
133
+ const iso = text.match(
134
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/
135
+ );
136
+ const validCalendar = iso != null && Number(iso[2]) >= 1 && Number(iso[2]) <= 12 && Number(iso[3]) >= 1 && Number(iso[3]) <= new Date(Date.UTC(Number(iso[1]), Number(iso[2]), 0)).getUTCDate() && Number(iso[4]) < 24 && Number(iso[5]) < 60 && Number(iso[6] ?? 0) < 60;
137
+ const timestamp = numeric ? Number(value) : validCalendar ? Date.parse(text) : NaN;
138
+ if (!Number.isSafeInteger(timestamp) || timestamp <= 0 || timestamp > 864e13) {
139
+ throw new Error("--testSince requires a valid UTC timestamp or epoch ms");
140
+ }
141
+ return timestamp;
142
+ };
143
+ var classifyAiPocketLabelHorizon = (decisionTimestamp, exitTimestamp, cutoff) => {
144
+ if (decisionTimestamp >= cutoff) return null;
145
+ if (typeof exitTimestamp !== "number" || !Number.isFinite(exitTimestamp)) {
146
+ return "unknown";
147
+ }
148
+ if (exitTimestamp < decisionTimestamp) {
149
+ throw new Error(
150
+ "--testSince requires label exitTimestamp >= decision timestamp"
151
+ );
152
+ }
153
+ return exitTimestamp >= cutoff ? "crossing" : null;
154
+ };
155
+ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0, testSince) => {
156
+ if (testSince != null) {
157
+ const cutoff = parseAiPocketTestSince(testSince);
158
+ if (validationSplit !== 0) {
159
+ throw new Error("--testSince requires --validationSplit 0");
160
+ }
161
+ const trainRows = [];
162
+ const testRows = [];
163
+ const labelHorizonPurge = {
164
+ crossing: 0,
165
+ unknown: 0
166
+ };
167
+ for (const row of rows) {
168
+ if (typeof row.timestamp !== "number" || !Number.isFinite(row.timestamp)) {
169
+ throw new Error("--testSince requires a finite timestamp on every row");
170
+ }
171
+ const purge = classifyAiPocketLabelHorizon(
172
+ row.timestamp,
173
+ row.exitTimestamp,
174
+ cutoff
175
+ );
176
+ if (purge) {
177
+ labelHorizonPurge[purge] += 1;
178
+ continue;
179
+ }
180
+ (row.timestamp < cutoff ? trainRows : testRows).push(row);
181
+ }
182
+ if (!trainRows.length || !testRows.length) {
183
+ throw new Error(
184
+ "--testSince requires non-empty development and test partitions"
185
+ );
186
+ }
187
+ return {
188
+ trainRows,
189
+ validationRows: [],
190
+ testRows,
191
+ labelHorizonPurge
192
+ };
193
+ }
131
194
  const timestamps = [
132
195
  ...new Set(
133
196
  rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
@@ -445,6 +508,12 @@ var resolveAiPocketSearchCommandOptions = ({
445
508
  if (sealTest && testSplit <= 0) {
446
509
  throw new Error("--sealTest requires a positive --testSplit");
447
510
  }
511
+ const explicitTestSince = hasCliOption2(argv, "testSince");
512
+ const testSinceInput = explicitTestSince ? readAiPocketSearchCliOption({ argv, longName: "testSince" }) : flags2.testSince;
513
+ const testSince = explicitTestSince || testSinceInput != null && testSinceInput !== "" ? parseAiPocketTestSince(testSinceInput) : null;
514
+ if (testSince != null && validationSplit !== 0) {
515
+ throw new Error("--testSince requires --validationSplit 0");
516
+ }
448
517
  const maxEventCountShare = normalizeUnitRatio(
449
518
  readAiPocketSearchCliOption({
450
519
  argv,
@@ -485,6 +554,7 @@ var resolveAiPocketSearchCommandOptions = ({
485
554
  maxCombinations: normalizePositiveInt(flags2.maxCombinations, 6e4),
486
555
  validationSplit,
487
556
  testSplit,
557
+ testSince,
488
558
  sealTest,
489
559
  explicitMinValidationSupport: normalizeInt(flags2.minValidationSupport, 0),
490
560
  explicitMinEvents: normalizeInt(flags2.minEvents, 0),
@@ -2452,6 +2522,19 @@ var buildAiPocketMarkdownReport = ({
2452
2522
  ["test_rows", run.testRows],
2453
2523
  ["validation_split", formatMdPercent(run.validationSplit)],
2454
2524
  ["test_split", formatMdPercent(run.testSplit)],
2525
+ ["partition_mode", run.partitionMode ?? "timestamp-ratio"],
2526
+ [
2527
+ "purged_development_crossing_labels",
2528
+ run.labelHorizonPurge?.crossing ?? 0
2529
+ ],
2530
+ [
2531
+ "purged_development_unknown_labels",
2532
+ run.labelHorizonPurge?.unknown ?? 0
2533
+ ],
2534
+ [
2535
+ "test_since",
2536
+ run.testSince == null ? "n/a" : new Date(run.testSince).toISOString()
2537
+ ],
2455
2538
  ["test_sealed", run.sealedTest?.sealed ? "yes" : "no"],
2456
2539
  ["sealed_test_rows", run.sealedTest?.rows ?? run.testRows],
2457
2540
  ["sealed_test_events", run.sealedTest?.events ?? "n/a"],
@@ -3204,6 +3287,11 @@ var presentAiPocketSearchResult = ({
3204
3287
  ["max_combinations", import_chalk.default.magenta(String(maxCombinations))],
3205
3288
  ["validation_split", import_chalk.default.magenta(formatRatio(validationSplit))],
3206
3289
  ["test_split", import_chalk.default.magenta(formatRatio(testSplit))],
3290
+ ["partition_mode", run.partitionMode ?? "timestamp-ratio"],
3291
+ [
3292
+ "test_since",
3293
+ run.testSince == null ? "n/a" : new Date(run.testSince).toISOString()
3294
+ ],
3207
3295
  [
3208
3296
  "test_evidence",
3209
3297
  sealedTest.sealed ? import_chalk.default.green(
@@ -3749,6 +3837,10 @@ var evaluateAiPocketSearchDataset = async ({
3749
3837
  let failed = 0;
3750
3838
  const errors = [];
3751
3839
  const rows = [];
3840
+ const labelHorizonPurge = {
3841
+ crossing: 0,
3842
+ unknown: 0
3843
+ };
3752
3844
  const excludedFeaturePaths = /* @__PURE__ */ new Map();
3753
3845
  await (0, import_ai2.streamAiDatasetRows)({
3754
3846
  filePaths: prepared.filePaths,
@@ -3759,12 +3851,43 @@ var evaluateAiPocketSearchDataset = async ({
3759
3851
  if (resolvedStrategyName === "unknown" && typeof row.strategyName === "string" && row.strategyName.trim()) {
3760
3852
  resolvedStrategyName = row.strategyName.trim();
3761
3853
  }
3854
+ if (options.testSince != null && (typeof row.timestamp !== "number" || !Number.isFinite(row.timestamp))) {
3855
+ throw new Error(
3856
+ "--testSince requires a finite numeric timestamp on every export row"
3857
+ );
3858
+ }
3762
3859
  const timestamp = getDatasetRowTimestamp(row);
3763
3860
  if (prepared.sinceTimestamp != null && (timestamp == null || timestamp < prepared.sinceTimestamp) || options.untilTimestamp != null && (timestamp == null || timestamp > options.untilTimestamp)) {
3764
3861
  dateSkipped += 1;
3765
3862
  onProgress?.({ symbol: row.symbol, status: "date-skip" });
3766
3863
  return;
3767
3864
  }
3865
+ if (options.testSince != null) {
3866
+ const purge = classifyAiPocketLabelHorizon(
3867
+ timestamp,
3868
+ row.tradeResult?.exitTimestamp,
3869
+ options.testSince
3870
+ );
3871
+ if (purge) {
3872
+ labelHorizonPurge[purge] += 1;
3873
+ return;
3874
+ }
3875
+ if (options.sealTest && timestamp >= options.testSince) {
3876
+ rows.push({
3877
+ signalId: row.signalId,
3878
+ strategy: row.strategyName,
3879
+ symbol: row.symbol,
3880
+ direction: row.direction,
3881
+ timestamp,
3882
+ profit: 0,
3883
+ profitableTrade: false,
3884
+ aiApproved: false,
3885
+ quality: null,
3886
+ features: {}
3887
+ });
3888
+ return;
3889
+ }
3890
+ }
3768
3891
  try {
3769
3892
  const signal = extractSignalFromAiDatasetRow(row);
3770
3893
  const payload = (0, import_ai3.buildAiPayload)(signal);
@@ -3794,6 +3917,7 @@ var evaluateAiPocketSearchDataset = async ({
3794
3917
  symbol: row.symbol,
3795
3918
  direction: row.direction,
3796
3919
  timestamp,
3920
+ ...options.testSince != null ? { exitTimestamp: row.tradeResult?.exitTimestamp } : {},
3797
3921
  profit,
3798
3922
  profitableTrade: profit > 0,
3799
3923
  aiApproved: isAiApproval(row, analysis, options.minQuality),
@@ -3816,6 +3940,7 @@ var evaluateAiPocketSearchDataset = async ({
3816
3940
  }
3817
3941
  });
3818
3942
  return {
3943
+ labelHorizonPurge,
3819
3944
  rows,
3820
3945
  resolvedStrategyName,
3821
3946
  scanned,
@@ -4182,7 +4307,8 @@ var resolveScopeRows = (rows, scope) => {
4182
4307
  var runAiPocketSearchResearch = ({
4183
4308
  rows,
4184
4309
  options,
4185
- onProgress
4310
+ onProgress,
4311
+ datasetLabelHorizonPurge
4186
4312
  }) => {
4187
4313
  const {
4188
4314
  direction,
@@ -4190,6 +4316,7 @@ var runAiPocketSearchResearch = ({
4190
4316
  qualityThresholds,
4191
4317
  validationSplit,
4192
4318
  testSplit,
4319
+ testSince,
4193
4320
  sealTest,
4194
4321
  cadenceMode,
4195
4322
  explicitMinSupport,
@@ -4214,14 +4341,30 @@ var runAiPocketSearchResearch = ({
4214
4341
  coverageMode
4215
4342
  } = options;
4216
4343
  const directionRows = direction ? rows.filter((row) => row.direction === direction) : rows;
4217
- const scopeRows = resolveScopeRows(directionRows, scope);
4218
- const fullSplit = splitAiPocketResearchRowsByTimestamp(
4344
+ const calendarSplit = testSince != null ? splitAiPocketResearchRowsByTimestamp(
4345
+ rows,
4346
+ validationSplit,
4347
+ testSplit,
4348
+ testSince
4349
+ ) : null;
4350
+ const inDirection = (row) => !direction || row.direction === direction;
4351
+ const fullSplit = calendarSplit ? {
4352
+ trainRows: calendarSplit.trainRows.filter(inDirection),
4353
+ validationRows: calendarSplit.validationRows.filter(inDirection),
4354
+ testRows: calendarSplit.testRows.filter(inDirection)
4355
+ } : splitAiPocketResearchRowsByTimestamp(
4219
4356
  directionRows,
4220
4357
  validationSplit,
4221
4358
  testSplit
4222
4359
  );
4360
+ if (testSince != null && (!fullSplit.trainRows.length || !fullSplit.testRows.length)) {
4361
+ throw new Error(
4362
+ "--testSince requires non-empty development and test partitions for the selected direction"
4363
+ );
4364
+ }
4223
4365
  const sealedFullSplit = sealAiPocketTestPartition(fullSplit, sealTest);
4224
4366
  const discoveryRows = sealedFullSplit.discoveryRows;
4367
+ const scopeRows = resolveScopeRows(discoveryRows, scope);
4225
4368
  const trainRows = resolveScopeRows(fullSplit.trainRows, scope);
4226
4369
  const validationRows = resolveScopeRows(fullSplit.validationRows, scope);
4227
4370
  const testRows = resolveScopeRows(sealedFullSplit.searchTestRows, scope);
@@ -4374,6 +4517,12 @@ var runAiPocketSearchResearch = ({
4374
4517
  }
4375
4518
  }
4376
4519
  return {
4520
+ ...calendarSplit ? {
4521
+ labelHorizonPurge: {
4522
+ crossing: (calendarSplit.labelHorizonPurge?.crossing ?? 0) + (datasetLabelHorizonPurge?.crossing ?? 0),
4523
+ unknown: (calendarSplit.labelHorizonPurge?.unknown ?? 0) + (datasetLabelHorizonPurge?.unknown ?? 0)
4524
+ }
4525
+ } : {},
4377
4526
  scopeRows,
4378
4527
  trainRows,
4379
4528
  validationRows,
@@ -4411,6 +4560,7 @@ var runAiPocketSearchCommand = async ({
4411
4560
  maxCombinations,
4412
4561
  validationSplit,
4413
4562
  testSplit,
4563
+ testSince,
4414
4564
  maxBatch,
4415
4565
  maxSymbolCountShare,
4416
4566
  objective,
@@ -4458,6 +4608,7 @@ var runAiPocketSearchCommand = async ({
4458
4608
  const research = runAiPocketSearchResearch({
4459
4609
  rows,
4460
4610
  options,
4611
+ datasetLabelHorizonPurge: evaluatedDataset.labelHorizonPurge,
4461
4612
  onProgress: createAiPocketSearchResearchProgressPresenter()
4462
4613
  });
4463
4614
  const {
@@ -4531,6 +4682,9 @@ var runAiPocketSearchCommand = async ({
4531
4682
  objective,
4532
4683
  validationSplit,
4533
4684
  testSplit,
4685
+ testSince,
4686
+ partitionMode: testSince == null ? "timestamp-ratio" : "calendar-cutoff",
4687
+ ...research.labelHorizonPurge ? { labelHorizonPurge: research.labelHorizonPurge } : {},
4534
4688
  sealedTest,
4535
4689
  minValidationSupport: search.stats.cadence.minValidationSupport,
4536
4690
  reportPath,
@@ -4667,6 +4821,11 @@ import_args.default.option(
4667
4821
  "Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
4668
4822
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
4669
4823
  );
4824
+ import_args.default.option(
4825
+ ["z", "testSince"],
4826
+ "Fixed calendar test boundary (UTC ISO or epoch ms); overrides the split ratio and requires validationSplit 0",
4827
+ ""
4828
+ );
4670
4829
  import_args.default.option(
4671
4830
  ["w", "sealTest"],
4672
4831
  "Reserve test bounds without exposing its rows or economics to discovery",
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/cli",
3
- "version": "3.1.29-beta.266",
3
+ "version": "3.1.29-beta.269",
4
4
  "description": "Official CLI for the TradeJS TypeScript framework: infra setup, backtests, signals, bots, and ML workflows.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -37,13 +37,13 @@
37
37
  "tradejs": "dist/cli.js"
38
38
  },
39
39
  "dependencies": {
40
- "@tradejs/connectors": "^3.1.29-beta.266",
41
- "@tradejs/core": "^3.1.29-beta.266",
42
- "@tradejs/indicators": "^3.1.29-beta.266",
43
- "@tradejs/infra": "^3.1.29-beta.266",
44
- "@tradejs/node": "^3.1.29-beta.266",
40
+ "@tradejs/connectors": "^3.1.29-beta.269",
41
+ "@tradejs/core": "^3.1.29-beta.269",
42
+ "@tradejs/indicators": "^3.1.29-beta.269",
43
+ "@tradejs/infra": "^3.1.29-beta.269",
44
+ "@tradejs/node": "^3.1.29-beta.269",
45
45
  "@tradejs/strategy-kit": "3.0.6",
46
- "@tradejs/types": "^3.1.29-beta.266",
46
+ "@tradejs/types": "^3.1.29-beta.269",
47
47
  "args": "^5.0.3",
48
48
  "bcryptjs": "^2.4.3",
49
49
  "chalk": "4.1.2",