@tradejs/cli 2.0.10 → 2.0.12

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
@@ -4728,10 +4728,13 @@ var init_aiPocketSearch = __esm({
4728
4728
  ["selected_rows", run.selectedRows],
4729
4729
  ["evaluated_rows", run.evaluatedRows],
4730
4730
  ["scope", run.scope],
4731
+ ["direction", run.direction ?? "all"],
4731
4732
  ["scope_rows", run.scopeRows],
4732
4733
  ["train_rows", run.trainRows],
4733
4734
  ["validation_rows", run.validationRows],
4735
+ ["test_rows", run.testRows],
4734
4736
  ["validation_split", formatMdPercent(run.validationSplit)],
4737
+ ["test_split", formatMdPercent(run.testSplit)],
4735
4738
  ["min_validation_support", run.minValidationSupport],
4736
4739
  ["failed", run.failed],
4737
4740
  ["recent", run.recent === 0 ? "all" : run.recent],
@@ -4869,7 +4872,7 @@ var init_aiPocketSearch = __esm({
4869
4872
  });
4870
4873
 
4871
4874
  // src/lib/aiPocketSearchCli.ts
4872
- var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, readAiPocketSearchCliOption;
4875
+ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, splitAiPocketResearchRowsByTimestamp, readAiPocketSearchCliOption;
4873
4876
  var init_aiPocketSearchCli = __esm({
4874
4877
  "src/lib/aiPocketSearchCli.ts"() {
4875
4878
  "use strict";
@@ -4877,7 +4880,47 @@ var init_aiPocketSearchCli = __esm({
4877
4880
  minProfitFactor: "1.2",
4878
4881
  minWinRate: "0",
4879
4882
  minTotalProfit: "0",
4880
- validationSplit: "0.25"
4883
+ validationSplit: "0.25",
4884
+ testSplit: "0"
4885
+ };
4886
+ splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
4887
+ const timestamps = [
4888
+ ...new Set(
4889
+ rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
4890
+ )
4891
+ ].sort((left, right) => left - right);
4892
+ if (timestamps.length < 2) {
4893
+ return { trainRows: rows, validationRows: [], testRows: [] };
4894
+ }
4895
+ const getSplitCount = (ratio) => ratio > 0 ? Math.max(1, Math.floor(timestamps.length * ratio)) : 0;
4896
+ let testEvents = getSplitCount(testSplit);
4897
+ let validationEvents = getSplitCount(validationSplit);
4898
+ const maximumHeldOut = timestamps.length - 1;
4899
+ if (testEvents + validationEvents > maximumHeldOut) {
4900
+ validationEvents = Math.max(
4901
+ 0,
4902
+ validationEvents - (testEvents + validationEvents - maximumHeldOut)
4903
+ );
4904
+ }
4905
+ if (testEvents + validationEvents > maximumHeldOut) {
4906
+ testEvents = Math.max(0, maximumHeldOut - validationEvents);
4907
+ }
4908
+ const testStart = timestamps.length - testEvents;
4909
+ const validationStart = testStart - validationEvents;
4910
+ const validationTimestamps = new Set(
4911
+ timestamps.slice(validationStart, testStart)
4912
+ );
4913
+ const testTimestamps = new Set(timestamps.slice(testStart));
4914
+ const isIn = (timestampsSet, timestamp) => typeof timestamp === "number" && timestampsSet.has(timestamp);
4915
+ return {
4916
+ trainRows: rows.filter(
4917
+ (row) => !isIn(validationTimestamps, row.timestamp) && !isIn(testTimestamps, row.timestamp)
4918
+ ),
4919
+ validationRows: rows.filter(
4920
+ (row) => isIn(validationTimestamps, row.timestamp)
4921
+ ),
4922
+ testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
4923
+ };
4881
4924
  };
4882
4925
  readAiPocketSearchCliOption = ({
4883
4926
  argv,
@@ -4904,7 +4947,7 @@ var aiPocketSearch_exports = {};
4904
4947
  __export(aiPocketSearch_exports, {
4905
4948
  main: () => main3
4906
4949
  });
4907
- var import_args3, import_chalk4, import_promises4, import_path2, import_progress2, import_ai3, import_ai4, ListIt2, flags3, createListIt2, createTable2, formatRatio, formatProfit, formatNumber2, colorizeRatio, colorizeProfit, colorizeNumber, toReportTimestamp, markdownPlainTable, buildPendingMarkdownReport, normalizeInt, normalizePositiveInt, normalizeNonNegativeNumber, normalizeRatio, normalizeFeatureProfile, normalizeQuality, isAiApproval, listMergedFiles, deriveStrategyNameFromFile, getMergedGroupId, resolveMarkdownReportPath, sortDatasetPartPaths, resolveMergedDatasetFiles, getDatasetRowTimestamp, findMaxSelectedTimestamp, getDeterministicModelCandidate, resolveScopeRows, splitValidationRows, printSection, buildSummaryRows, buildPocketRows, buildQualityRows, tickProgressBarTo, searchPhaseLabels, hasCliOption2, main3;
4950
+ var import_args3, import_chalk4, import_promises4, import_path2, import_progress2, import_ai3, import_ai4, ListIt2, flags3, createListIt2, createTable2, formatRatio, formatProfit, formatNumber2, colorizeRatio, colorizeProfit, colorizeNumber, toReportTimestamp, markdownPlainTable, buildPendingMarkdownReport, normalizeInt, normalizePositiveInt, normalizeNonNegativeNumber, normalizeRatio, normalizeFeatureProfile, normalizeQuality, isAiApproval, listMergedFiles, deriveStrategyNameFromFile, getMergedGroupId, resolveMarkdownReportPath, sortDatasetPartPaths, resolveMergedDatasetFiles, getDatasetRowTimestamp, findMaxSelectedTimestamp, getDeterministicModelCandidate, resolveScopeRows, printSection, buildSummaryRows, buildPocketRows, buildQualityRows, tickProgressBarTo, searchPhaseLabels, hasCliOption2, main3;
4908
4951
  var init_aiPocketSearch2 = __esm({
4909
4952
  "src/scripts/aiPocketSearch.ts"() {
4910
4953
  "use strict";
@@ -4968,6 +5011,11 @@ var init_aiPocketSearch2 = __esm({
4968
5011
  "Search scope: all, approved, rejected, or candidates",
4969
5012
  "all"
4970
5013
  );
5014
+ import_args3.default.option(
5015
+ ["I", "direction"],
5016
+ "Only search rows with this signal direction: LONG or SHORT",
5017
+ ""
5018
+ );
4971
5019
  import_args3.default.option(["d", "maxDepth"], "Maximum predicate-combination depth", 2);
4972
5020
  import_args3.default.option(["m", "minSupport"], "Minimum rows required for a pocket", 20);
4973
5021
  import_args3.default.option(
@@ -5000,6 +5048,11 @@ var init_aiPocketSearch2 = __esm({
5000
5048
  "Trailing time-ordered scope share reserved for validation (0 disables)",
5001
5049
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
5002
5050
  );
5051
+ import_args3.default.option(
5052
+ ["T", "testSplit"],
5053
+ "Trailing timestamp-grouped scope share withheld as untouched test",
5054
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
5055
+ );
5003
5056
  import_args3.default.option(
5004
5057
  ["N", "minValidationSupport"],
5005
5058
  "Minimum validation rows required for positive pockets (0 = auto)",
@@ -5288,31 +5341,6 @@ var init_aiPocketSearch2 = __esm({
5288
5341
  }
5289
5342
  return rows;
5290
5343
  };
5291
- splitValidationRows = (rows, validationSplit) => {
5292
- if (validationSplit <= 0 || rows.length < 2) {
5293
- return {
5294
- trainRows: rows,
5295
- validationRows: []
5296
- };
5297
- }
5298
- const sortedRows = [...rows].sort((left, right) => {
5299
- const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
5300
- const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
5301
- return leftTimestamp - rightTimestamp;
5302
- });
5303
- const validationCount = Math.max(
5304
- 1,
5305
- Math.min(
5306
- sortedRows.length - 1,
5307
- Math.floor(sortedRows.length * validationSplit)
5308
- )
5309
- );
5310
- const splitIndex = sortedRows.length - validationCount;
5311
- return {
5312
- trainRows: sortedRows.slice(0, splitIndex),
5313
- validationRows: sortedRows.slice(splitIndex)
5314
- };
5315
- };
5316
5344
  printSection = (title, table) => {
5317
5345
  console.log(import_chalk4.default.gray(`${title}:`));
5318
5346
  console.log(table);
@@ -5412,6 +5440,12 @@ var init_aiPocketSearch2 = __esm({
5412
5440
  `Unsupported --scope "${scope}". Use all, approved, rejected, or candidates.`
5413
5441
  );
5414
5442
  }
5443
+ const direction = String(flags3.direction || "").trim().toUpperCase();
5444
+ if (direction && !["LONG", "SHORT"].includes(direction)) {
5445
+ throw new Error(
5446
+ `Unsupported --direction "${direction}". Use LONG or SHORT.`
5447
+ );
5448
+ }
5415
5449
  const maxDepth = normalizePositiveInt(flags3.maxDepth, 2);
5416
5450
  const minSupport = normalizePositiveInt(flags3.minSupport, 20);
5417
5451
  const minProfitFactor = normalizeNonNegativeNumber(
@@ -5449,6 +5483,14 @@ var init_aiPocketSearch2 = __esm({
5449
5483
  }) ?? flags3.validationSplit,
5450
5484
  0.25
5451
5485
  ) : 0.25;
5486
+ const testSplit = hasCliOption2("testSplit", "T") ? normalizeRatio(
5487
+ readAiPocketSearchCliOption({
5488
+ argv: process.argv,
5489
+ longName: "testSplit",
5490
+ shortName: "T"
5491
+ }) ?? flags3.testSplit,
5492
+ 0
5493
+ ) : 0;
5452
5494
  const explicitMinValidationSupport = normalizeInt(
5453
5495
  flags3.minValidationSupport,
5454
5496
  0
@@ -5588,11 +5630,9 @@ var init_aiPocketSearch2 = __esm({
5588
5630
  }
5589
5631
  }
5590
5632
  });
5591
- const scopeRows = resolveScopeRows(rows, scope);
5592
- const { trainRows, validationRows } = splitValidationRows(
5593
- scopeRows,
5594
- validationSplit
5595
- );
5633
+ const scopedRows = resolveScopeRows(rows, scope);
5634
+ const scopeRows = direction ? scopedRows.filter((row) => row.direction === direction) : scopedRows;
5635
+ const { trainRows, validationRows, testRows } = splitAiPocketResearchRowsByTimestamp(scopeRows, validationSplit, testSplit);
5596
5636
  const minValidationSupport = explicitMinValidationSupport > 0 ? explicitMinValidationSupport : validationRows.length ? Math.max(3, Math.ceil(minSupport * validationSplit * 0.5)) : 0;
5597
5637
  const currentGateSummary = summarizeAiTrainEvaluations(rows);
5598
5638
  const currentGateQualityThresholds = summarizeAiTrainEvaluationsByQualityThreshold(rows, qualityThresholds);
@@ -5655,9 +5695,11 @@ var init_aiPocketSearch2 = __esm({
5655
5695
  selectedRows,
5656
5696
  evaluatedRows: rows.length,
5657
5697
  scope,
5698
+ direction: direction || null,
5658
5699
  scopeRows: scopeRows.length,
5659
5700
  trainRows: trainRows.length,
5660
5701
  validationRows: validationRows.length,
5702
+ testRows: testRows.length,
5661
5703
  scanned,
5662
5704
  dateSkipped,
5663
5705
  failed,
@@ -5672,6 +5714,7 @@ var init_aiPocketSearch2 = __esm({
5672
5714
  includeGateContext,
5673
5715
  featureProfile,
5674
5716
  validationSplit,
5717
+ testSplit,
5675
5718
  minValidationSupport,
5676
5719
  reportPath,
5677
5720
  search: {
@@ -5683,6 +5726,7 @@ var init_aiPocketSearch2 = __esm({
5683
5726
  maxAtomicPredicates,
5684
5727
  maxCombinations,
5685
5728
  validationSplit,
5729
+ testSplit,
5686
5730
  minValidationSupport,
5687
5731
  dedupeEquivalentSelections,
5688
5732
  top
@@ -5724,9 +5768,11 @@ var init_aiPocketSearch2 = __esm({
5724
5768
  ["selected", import_chalk4.default.blue(String(selectedRows))],
5725
5769
  ["evaluated", import_chalk4.default.blue(String(rows.length))],
5726
5770
  ["scope", import_chalk4.default.yellow(scope)],
5771
+ ["direction", direction ? import_chalk4.default.yellow(direction) : import_chalk4.default.gray("all")],
5727
5772
  ["scope_rows", import_chalk4.default.blue(String(scopeRows.length))],
5728
5773
  ["train_rows", import_chalk4.default.blue(String(trainRows.length))],
5729
5774
  ["validation_rows", import_chalk4.default.blue(String(validationRows.length))],
5775
+ ["test_rows", import_chalk4.default.blue(String(testRows.length))],
5730
5776
  ["source_rows", import_chalk4.default.blue(String(totalRows))],
5731
5777
  ["date_skipped", import_chalk4.default.blue(String(dateSkipped))],
5732
5778
  [
@@ -5753,6 +5799,7 @@ var init_aiPocketSearch2 = __esm({
5753
5799
  ["max_atomic_predicates", import_chalk4.default.magenta(String(maxAtomicPredicates))],
5754
5800
  ["max_combinations", import_chalk4.default.magenta(String(maxCombinations))],
5755
5801
  ["validation_split", import_chalk4.default.magenta(formatRatio(validationSplit))],
5802
+ ["test_split", import_chalk4.default.magenta(formatRatio(testSplit))],
5756
5803
  ["min_validation_support", import_chalk4.default.magenta(String(minValidationSupport))],
5757
5804
  [
5758
5805
  "dedupe_equivalent",
@@ -1187,10 +1187,13 @@ var buildAiPocketMarkdownReport = ({
1187
1187
  ["selected_rows", run.selectedRows],
1188
1188
  ["evaluated_rows", run.evaluatedRows],
1189
1189
  ["scope", run.scope],
1190
+ ["direction", run.direction ?? "all"],
1190
1191
  ["scope_rows", run.scopeRows],
1191
1192
  ["train_rows", run.trainRows],
1192
1193
  ["validation_rows", run.validationRows],
1194
+ ["test_rows", run.testRows],
1193
1195
  ["validation_split", formatMdPercent(run.validationSplit)],
1196
+ ["test_split", formatMdPercent(run.testSplit)],
1194
1197
  ["min_validation_support", run.minValidationSupport],
1195
1198
  ["failed", run.failed],
1196
1199
  ["recent", run.recent === 0 ? "all" : run.recent],
@@ -21,14 +21,55 @@ 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
- readAiPocketSearchCliOption: () => readAiPocketSearchCliOption
24
+ readAiPocketSearchCliOption: () => readAiPocketSearchCliOption,
25
+ splitAiPocketResearchRowsByTimestamp: () => splitAiPocketResearchRowsByTimestamp
25
26
  });
26
27
  module.exports = __toCommonJS(aiPocketSearchCli_exports);
27
28
  var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
28
29
  minProfitFactor: "1.2",
29
30
  minWinRate: "0",
30
31
  minTotalProfit: "0",
31
- validationSplit: "0.25"
32
+ validationSplit: "0.25",
33
+ testSplit: "0"
34
+ };
35
+ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
36
+ const timestamps = [
37
+ ...new Set(
38
+ rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
39
+ )
40
+ ].sort((left, right) => left - right);
41
+ if (timestamps.length < 2) {
42
+ return { trainRows: rows, validationRows: [], testRows: [] };
43
+ }
44
+ const getSplitCount = (ratio) => ratio > 0 ? Math.max(1, Math.floor(timestamps.length * ratio)) : 0;
45
+ let testEvents = getSplitCount(testSplit);
46
+ let validationEvents = getSplitCount(validationSplit);
47
+ const maximumHeldOut = timestamps.length - 1;
48
+ if (testEvents + validationEvents > maximumHeldOut) {
49
+ validationEvents = Math.max(
50
+ 0,
51
+ validationEvents - (testEvents + validationEvents - maximumHeldOut)
52
+ );
53
+ }
54
+ if (testEvents + validationEvents > maximumHeldOut) {
55
+ testEvents = Math.max(0, maximumHeldOut - validationEvents);
56
+ }
57
+ const testStart = timestamps.length - testEvents;
58
+ const validationStart = testStart - validationEvents;
59
+ const validationTimestamps = new Set(
60
+ timestamps.slice(validationStart, testStart)
61
+ );
62
+ const testTimestamps = new Set(timestamps.slice(testStart));
63
+ const isIn = (timestampsSet, timestamp) => typeof timestamp === "number" && timestampsSet.has(timestamp);
64
+ return {
65
+ trainRows: rows.filter(
66
+ (row) => !isIn(validationTimestamps, row.timestamp) && !isIn(testTimestamps, row.timestamp)
67
+ ),
68
+ validationRows: rows.filter(
69
+ (row) => isIn(validationTimestamps, row.timestamp)
70
+ ),
71
+ testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
72
+ };
32
73
  };
33
74
  var readAiPocketSearchCliOption = ({
34
75
  argv,
@@ -50,5 +91,6 @@ var readAiPocketSearchCliOption = ({
50
91
  // Annotate the CommonJS export names for ESM import in node:
51
92
  0 && (module.exports = {
52
93
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS,
53
- readAiPocketSearchCliOption
94
+ readAiPocketSearchCliOption,
95
+ splitAiPocketResearchRowsByTimestamp
54
96
  });
@@ -3176,10 +3176,13 @@ var buildAiPocketMarkdownReport = ({
3176
3176
  ["selected_rows", run.selectedRows],
3177
3177
  ["evaluated_rows", run.evaluatedRows],
3178
3178
  ["scope", run.scope],
3179
+ ["direction", run.direction ?? "all"],
3179
3180
  ["scope_rows", run.scopeRows],
3180
3181
  ["train_rows", run.trainRows],
3181
3182
  ["validation_rows", run.validationRows],
3183
+ ["test_rows", run.testRows],
3182
3184
  ["validation_split", formatMdPercent(run.validationSplit)],
3185
+ ["test_split", formatMdPercent(run.testSplit)],
3183
3186
  ["min_validation_support", run.minValidationSupport],
3184
3187
  ["failed", run.failed],
3185
3188
  ["recent", run.recent === 0 ? "all" : run.recent],
@@ -3319,7 +3322,47 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
3319
3322
  minProfitFactor: "1.2",
3320
3323
  minWinRate: "0",
3321
3324
  minTotalProfit: "0",
3322
- validationSplit: "0.25"
3325
+ validationSplit: "0.25",
3326
+ testSplit: "0"
3327
+ };
3328
+ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
3329
+ const timestamps = [
3330
+ ...new Set(
3331
+ rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
3332
+ )
3333
+ ].sort((left, right) => left - right);
3334
+ if (timestamps.length < 2) {
3335
+ return { trainRows: rows, validationRows: [], testRows: [] };
3336
+ }
3337
+ const getSplitCount = (ratio) => ratio > 0 ? Math.max(1, Math.floor(timestamps.length * ratio)) : 0;
3338
+ let testEvents = getSplitCount(testSplit);
3339
+ let validationEvents = getSplitCount(validationSplit);
3340
+ const maximumHeldOut = timestamps.length - 1;
3341
+ if (testEvents + validationEvents > maximumHeldOut) {
3342
+ validationEvents = Math.max(
3343
+ 0,
3344
+ validationEvents - (testEvents + validationEvents - maximumHeldOut)
3345
+ );
3346
+ }
3347
+ if (testEvents + validationEvents > maximumHeldOut) {
3348
+ testEvents = Math.max(0, maximumHeldOut - validationEvents);
3349
+ }
3350
+ const testStart = timestamps.length - testEvents;
3351
+ const validationStart = testStart - validationEvents;
3352
+ const validationTimestamps = new Set(
3353
+ timestamps.slice(validationStart, testStart)
3354
+ );
3355
+ const testTimestamps = new Set(timestamps.slice(testStart));
3356
+ const isIn = (timestampsSet, timestamp) => typeof timestamp === "number" && timestampsSet.has(timestamp);
3357
+ return {
3358
+ trainRows: rows.filter(
3359
+ (row) => !isIn(validationTimestamps, row.timestamp) && !isIn(testTimestamps, row.timestamp)
3360
+ ),
3361
+ validationRows: rows.filter(
3362
+ (row) => isIn(validationTimestamps, row.timestamp)
3363
+ ),
3364
+ testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
3365
+ };
3323
3366
  };
3324
3367
  var readAiPocketSearchCliOption = ({
3325
3368
  argv,
@@ -3388,6 +3431,11 @@ import_args.default.option(
3388
3431
  "Search scope: all, approved, rejected, or candidates",
3389
3432
  "all"
3390
3433
  );
3434
+ import_args.default.option(
3435
+ ["I", "direction"],
3436
+ "Only search rows with this signal direction: LONG or SHORT",
3437
+ ""
3438
+ );
3391
3439
  import_args.default.option(["d", "maxDepth"], "Maximum predicate-combination depth", 2);
3392
3440
  import_args.default.option(["m", "minSupport"], "Minimum rows required for a pocket", 20);
3393
3441
  import_args.default.option(
@@ -3420,6 +3468,11 @@ import_args.default.option(
3420
3468
  "Trailing time-ordered scope share reserved for validation (0 disables)",
3421
3469
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
3422
3470
  );
3471
+ import_args.default.option(
3472
+ ["T", "testSplit"],
3473
+ "Trailing timestamp-grouped scope share withheld as untouched test",
3474
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
3475
+ );
3423
3476
  import_args.default.option(
3424
3477
  ["N", "minValidationSupport"],
3425
3478
  "Minimum validation rows required for positive pockets (0 = auto)",
@@ -3708,31 +3761,6 @@ var resolveScopeRows = (rows, scope) => {
3708
3761
  }
3709
3762
  return rows;
3710
3763
  };
3711
- var splitValidationRows = (rows, validationSplit) => {
3712
- if (validationSplit <= 0 || rows.length < 2) {
3713
- return {
3714
- trainRows: rows,
3715
- validationRows: []
3716
- };
3717
- }
3718
- const sortedRows = [...rows].sort((left, right) => {
3719
- const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
3720
- const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
3721
- return leftTimestamp - rightTimestamp;
3722
- });
3723
- const validationCount = Math.max(
3724
- 1,
3725
- Math.min(
3726
- sortedRows.length - 1,
3727
- Math.floor(sortedRows.length * validationSplit)
3728
- )
3729
- );
3730
- const splitIndex = sortedRows.length - validationCount;
3731
- return {
3732
- trainRows: sortedRows.slice(0, splitIndex),
3733
- validationRows: sortedRows.slice(splitIndex)
3734
- };
3735
- };
3736
3764
  var printSection = (title, table) => {
3737
3765
  console.log(import_chalk.default.gray(`${title}:`));
3738
3766
  console.log(table);
@@ -3832,6 +3860,12 @@ var main = async () => {
3832
3860
  `Unsupported --scope "${scope}". Use all, approved, rejected, or candidates.`
3833
3861
  );
3834
3862
  }
3863
+ const direction = String(flags.direction || "").trim().toUpperCase();
3864
+ if (direction && !["LONG", "SHORT"].includes(direction)) {
3865
+ throw new Error(
3866
+ `Unsupported --direction "${direction}". Use LONG or SHORT.`
3867
+ );
3868
+ }
3835
3869
  const maxDepth = normalizePositiveInt(flags.maxDepth, 2);
3836
3870
  const minSupport = normalizePositiveInt(flags.minSupport, 20);
3837
3871
  const minProfitFactor = normalizeNonNegativeNumber(
@@ -3869,6 +3903,14 @@ var main = async () => {
3869
3903
  }) ?? flags.validationSplit,
3870
3904
  0.25
3871
3905
  ) : 0.25;
3906
+ const testSplit = hasCliOption2("testSplit", "T") ? normalizeRatio(
3907
+ readAiPocketSearchCliOption({
3908
+ argv: process.argv,
3909
+ longName: "testSplit",
3910
+ shortName: "T"
3911
+ }) ?? flags.testSplit,
3912
+ 0
3913
+ ) : 0;
3872
3914
  const explicitMinValidationSupport = normalizeInt(
3873
3915
  flags.minValidationSupport,
3874
3916
  0
@@ -4008,11 +4050,9 @@ var main = async () => {
4008
4050
  }
4009
4051
  }
4010
4052
  });
4011
- const scopeRows = resolveScopeRows(rows, scope);
4012
- const { trainRows, validationRows } = splitValidationRows(
4013
- scopeRows,
4014
- validationSplit
4015
- );
4053
+ const scopedRows = resolveScopeRows(rows, scope);
4054
+ const scopeRows = direction ? scopedRows.filter((row) => row.direction === direction) : scopedRows;
4055
+ const { trainRows, validationRows, testRows } = splitAiPocketResearchRowsByTimestamp(scopeRows, validationSplit, testSplit);
4016
4056
  const minValidationSupport = explicitMinValidationSupport > 0 ? explicitMinValidationSupport : validationRows.length ? Math.max(3, Math.ceil(minSupport * validationSplit * 0.5)) : 0;
4017
4057
  const currentGateSummary = summarizeAiTrainEvaluations(rows);
4018
4058
  const currentGateQualityThresholds = summarizeAiTrainEvaluationsByQualityThreshold(rows, qualityThresholds);
@@ -4075,9 +4115,11 @@ var main = async () => {
4075
4115
  selectedRows,
4076
4116
  evaluatedRows: rows.length,
4077
4117
  scope,
4118
+ direction: direction || null,
4078
4119
  scopeRows: scopeRows.length,
4079
4120
  trainRows: trainRows.length,
4080
4121
  validationRows: validationRows.length,
4122
+ testRows: testRows.length,
4081
4123
  scanned,
4082
4124
  dateSkipped,
4083
4125
  failed,
@@ -4092,6 +4134,7 @@ var main = async () => {
4092
4134
  includeGateContext,
4093
4135
  featureProfile,
4094
4136
  validationSplit,
4137
+ testSplit,
4095
4138
  minValidationSupport,
4096
4139
  reportPath,
4097
4140
  search: {
@@ -4103,6 +4146,7 @@ var main = async () => {
4103
4146
  maxAtomicPredicates,
4104
4147
  maxCombinations,
4105
4148
  validationSplit,
4149
+ testSplit,
4106
4150
  minValidationSupport,
4107
4151
  dedupeEquivalentSelections,
4108
4152
  top
@@ -4144,9 +4188,11 @@ var main = async () => {
4144
4188
  ["selected", import_chalk.default.blue(String(selectedRows))],
4145
4189
  ["evaluated", import_chalk.default.blue(String(rows.length))],
4146
4190
  ["scope", import_chalk.default.yellow(scope)],
4191
+ ["direction", direction ? import_chalk.default.yellow(direction) : import_chalk.default.gray("all")],
4147
4192
  ["scope_rows", import_chalk.default.blue(String(scopeRows.length))],
4148
4193
  ["train_rows", import_chalk.default.blue(String(trainRows.length))],
4149
4194
  ["validation_rows", import_chalk.default.blue(String(validationRows.length))],
4195
+ ["test_rows", import_chalk.default.blue(String(testRows.length))],
4150
4196
  ["source_rows", import_chalk.default.blue(String(totalRows))],
4151
4197
  ["date_skipped", import_chalk.default.blue(String(dateSkipped))],
4152
4198
  [
@@ -4173,6 +4219,7 @@ var main = async () => {
4173
4219
  ["max_atomic_predicates", import_chalk.default.magenta(String(maxAtomicPredicates))],
4174
4220
  ["max_combinations", import_chalk.default.magenta(String(maxCombinations))],
4175
4221
  ["validation_split", import_chalk.default.magenta(formatRatio(validationSplit))],
4222
+ ["test_split", import_chalk.default.magenta(formatRatio(testSplit))],
4176
4223
  ["min_validation_support", import_chalk.default.magenta(String(minValidationSupport))],
4177
4224
  [
4178
4225
  "dedupe_equivalent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/cli",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "Official CLI for the TradeJS TypeScript framework: infra setup, backtests, signals, bots, and ML workflows.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -29,14 +29,14 @@
29
29
  "tradejs": "dist/cli.js"
30
30
  },
31
31
  "dependencies": {
32
- "@tradejs/base": "^2.0.10",
33
- "@tradejs/connectors": "^2.0.10",
34
- "@tradejs/core": "^2.0.10",
35
- "@tradejs/indicators": "^2.0.10",
36
- "@tradejs/infra": "^2.0.10",
37
- "@tradejs/node": "^2.0.10",
38
- "@tradejs/strategies": "^2.0.10",
39
- "@tradejs/types": "^2.0.10",
32
+ "@tradejs/base": "^2.0.12",
33
+ "@tradejs/connectors": "^2.0.12",
34
+ "@tradejs/core": "^2.0.12",
35
+ "@tradejs/indicators": "^2.0.12",
36
+ "@tradejs/infra": "^2.0.12",
37
+ "@tradejs/node": "^2.0.12",
38
+ "@tradejs/strategies": "^2.0.12",
39
+ "@tradejs/types": "^2.0.12",
40
40
  "args": "^5.0.3",
41
41
  "bcryptjs": "^2.4.3",
42
42
  "chalk": "4.1.2",