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

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",
@@ -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,
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/cli",
3
- "version": "3.1.29-beta.268",
3
+ "version": "3.1.29-beta.270",
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.268",
41
- "@tradejs/core": "^3.1.29-beta.268",
42
- "@tradejs/indicators": "^3.1.29-beta.268",
43
- "@tradejs/infra": "^3.1.29-beta.268",
44
- "@tradejs/node": "^3.1.29-beta.268",
40
+ "@tradejs/connectors": "^3.1.29-beta.270",
41
+ "@tradejs/core": "^3.1.29-beta.270",
42
+ "@tradejs/indicators": "^3.1.29-beta.270",
43
+ "@tradejs/infra": "^3.1.29-beta.270",
44
+ "@tradejs/node": "^3.1.29-beta.270",
45
45
  "@tradejs/strategy-kit": "3.0.6",
46
- "@tradejs/types": "^3.1.29-beta.268",
46
+ "@tradejs/types": "^3.1.29-beta.270",
47
47
  "args": "^5.0.3",
48
48
  "bcryptjs": "^2.4.3",
49
49
  "chalk": "4.1.2",