@tradejs/cli 2.0.15 → 2.0.16

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.
@@ -2078,6 +2078,115 @@ var normalizeFeaturePrimitive = (value) => {
2078
2078
  return value;
2079
2079
  };
2080
2080
  var isOutcomePath = (segments) => segments.some((segment) => OUTCOME_SEGMENTS.has(segment.toLowerCase()));
2081
+ var DATA_QUALITY_LEAVES = /* @__PURE__ */ new Set([
2082
+ "age",
2083
+ "agems",
2084
+ "available",
2085
+ "asofts",
2086
+ "calcbars",
2087
+ "coverage",
2088
+ "latestindex",
2089
+ "points",
2090
+ "rowcount",
2091
+ "rows",
2092
+ "stale",
2093
+ "timestamp",
2094
+ "windowendts"
2095
+ ]);
2096
+ var METADATA_LEAVES = /* @__PURE__ */ new Set([
2097
+ "connector",
2098
+ "exchange",
2099
+ "fingerprint",
2100
+ "interval",
2101
+ "provider",
2102
+ "source",
2103
+ "strategy",
2104
+ "symbol",
2105
+ "targetsymbol",
2106
+ "universe"
2107
+ ]);
2108
+ var DERIVED_POLICY_FRAGMENTS = [
2109
+ "approval",
2110
+ "decisionhint",
2111
+ "gatefeature",
2112
+ "gatescore",
2113
+ "hardblock",
2114
+ "qualitycap",
2115
+ "recoverycandidate",
2116
+ "rejectreason",
2117
+ "riskflag"
2118
+ ];
2119
+ var RAW_NONSTATIONARY_LEAVES = /* @__PURE__ */ new Set([
2120
+ "activeassets",
2121
+ "activemarkets",
2122
+ "close",
2123
+ "current",
2124
+ "currentprice",
2125
+ "high",
2126
+ "indexprice",
2127
+ "liquidations",
2128
+ "liquidationsusd",
2129
+ "low",
2130
+ "marketcap",
2131
+ "marketcapusd",
2132
+ "notional",
2133
+ "open",
2134
+ "openinterest",
2135
+ "openinterestusd",
2136
+ "price",
2137
+ "quotevolume",
2138
+ "turnover",
2139
+ "turnoverusd",
2140
+ "volume",
2141
+ "volumeusd",
2142
+ "vwap"
2143
+ ]);
2144
+ var classifyAiPocketFeaturePath = (path2) => {
2145
+ const segments = (Array.isArray(path2) ? path2 : path2.split(".")).filter(
2146
+ Boolean
2147
+ );
2148
+ const normalized = segments.map((segment) => segment.toLowerCase());
2149
+ const leaf = normalized.at(-1) ?? "";
2150
+ const normalizedPath = normalized.join(".");
2151
+ const strategyGateFeaturesIndex = normalized.findIndex(
2152
+ (segment) => segment !== "gatefeatures" && segment.endsWith("gatefeatures")
2153
+ );
2154
+ const strategyGateFeaturePath = normalized.slice(
2155
+ strategyGateFeaturesIndex + 1
2156
+ );
2157
+ const isStrategySetupEvidence = normalized.includes("basecontext") && strategyGateFeaturesIndex >= 0 && (strategyGateFeaturePath.length === 0 || strategyGateFeaturePath.some(
2158
+ (segment) => segment === "geometry" || segment === "path"
2159
+ ));
2160
+ if (DATA_QUALITY_LEAVES.has(leaf) || leaf.endsWith("timestamp") || leaf.endsWith("agems") || normalized.some(
2161
+ (segment) => ["availability", "dataquality", "freshness"].includes(segment)
2162
+ )) {
2163
+ return "data-quality";
2164
+ }
2165
+ if (METADATA_LEAVES.has(leaf) || leaf.endsWith("fingerprint") || normalized.some((segment) => segment === "metadata")) {
2166
+ return "metadata";
2167
+ }
2168
+ if (DERIVED_POLICY_FRAGMENTS.some(
2169
+ (fragment) => !(fragment === "gatefeature" && isStrategySetupEvidence) && normalizedPath.includes(fragment)
2170
+ )) {
2171
+ return "derived-policy";
2172
+ }
2173
+ const isDerivedFeature = normalized[0] === "derived";
2174
+ const isNormalizedLeaf = /(alpha|bps|change|delta|direction|distance|pct|percentile|ratio|regime|return|share|slope|trend|zscore)/.test(
2175
+ leaf
2176
+ );
2177
+ const isRawMovingAverage = /^(?:ema|ma|sma|wma)(?:fast|slow|\d+)$/.test(leaf);
2178
+ const isRawAbsolutePositioning = /^(?:liq(?:long|net|short|total)|oi|openinterest(?:long|short)?)$/.test(
2179
+ leaf
2180
+ );
2181
+ const isRawAbsoluteMeasure = /(?:liquidations?|marketcap|notional|openinterest|price|turnover|volume)$/.test(
2182
+ leaf
2183
+ );
2184
+ const isRawStructureLevel = leaf === "level" && normalized.includes("structure");
2185
+ if (!isDerivedFeature && !isNormalizedLeaf && (RAW_NONSTATIONARY_LEAVES.has(leaf) || isRawMovingAverage || isRawAbsolutePositioning || isRawAbsoluteMeasure || isRawStructureLevel)) {
2186
+ return "raw-nonstationary";
2187
+ }
2188
+ return "eligible";
2189
+ };
2081
2190
  var isFeaturePathPrefix = (segments, prefix) => prefix.every((segment, index) => segments[index] === segment);
2082
2191
  var isCompactFeaturePathSkipped = (segments) => {
2083
2192
  if (!segments.length) {
@@ -2339,7 +2448,9 @@ var collectAiPocketFeatures = ({
2339
2448
  gateContext,
2340
2449
  includeSymbol = false,
2341
2450
  includeGateContext = false,
2342
- featureProfile = "all"
2451
+ featureProfile = "all",
2452
+ featurePolicy = "all",
2453
+ onFeatureExcluded
2343
2454
  }) => {
2344
2455
  const features = {};
2345
2456
  const signal = payload.signal ?? {};
@@ -2359,8 +2470,35 @@ var collectAiPocketFeatures = ({
2359
2470
  value: source,
2360
2471
  segments: [],
2361
2472
  maxDepth: 8,
2362
- shouldSkipPath: featureProfile === "compact" ? isCompactFeaturePathSkipped : void 0
2473
+ shouldSkipPath: (segments) => {
2474
+ if (featureProfile === "compact" && isCompactFeaturePathSkipped(segments)) {
2475
+ return true;
2476
+ }
2477
+ if (featurePolicy !== "causal-stationary") {
2478
+ return false;
2479
+ }
2480
+ const classification = classifyAiPocketFeaturePath(segments);
2481
+ if (classification === "eligible") {
2482
+ return false;
2483
+ }
2484
+ onFeatureExcluded?.({
2485
+ path: segments.join("."),
2486
+ classification
2487
+ });
2488
+ return true;
2489
+ }
2363
2490
  });
2491
+ if (featurePolicy === "causal-stationary") {
2492
+ for (const [key, value] of Object.entries(features)) {
2493
+ if (value === null) {
2494
+ onFeatureExcluded?.({
2495
+ path: key,
2496
+ classification: "data-quality"
2497
+ });
2498
+ delete features[key];
2499
+ }
2500
+ }
2501
+ }
2364
2502
  addSignalRiskDistanceFeatures({ features, signal });
2365
2503
  addDirectionalDerivedFeatures(features);
2366
2504
  return features;
@@ -2413,6 +2551,7 @@ var createSummaryAccumulator = () => ({
2413
2551
  directionCounts: /* @__PURE__ */ new Map(),
2414
2552
  symbolCounts: /* @__PURE__ */ new Map(),
2415
2553
  monthProfits: /* @__PURE__ */ new Map(),
2554
+ eventStats: /* @__PURE__ */ new Map(),
2416
2555
  equityPoints: []
2417
2556
  });
2418
2557
  var addSummaryRow = (accumulator, row) => {
@@ -2438,6 +2577,14 @@ var addSummaryRow = (accumulator, row) => {
2438
2577
  symbolBucket.totalProfit += profit;
2439
2578
  accumulator.symbolCounts.set(symbol, symbolBucket);
2440
2579
  const timestamp = typeof row.timestamp === "number" && Number.isFinite(row.timestamp) ? row.timestamp : Number.POSITIVE_INFINITY;
2580
+ const eventKey = Number.isFinite(timestamp) ? String(timestamp) : `UNKNOWN:${accumulator.support}`;
2581
+ const event = accumulator.eventStats.get(eventKey) ?? {
2582
+ count: 0,
2583
+ totalProfit: 0
2584
+ };
2585
+ event.count += 1;
2586
+ event.totalProfit += profit;
2587
+ accumulator.eventStats.set(eventKey, event);
2441
2588
  const month = Number.isFinite(timestamp) && timestamp !== Number.POSITIVE_INFINITY ? new Date(timestamp).toISOString().slice(0, 7) : "UNKNOWN";
2442
2589
  accumulator.monthProfits.set(
2443
2590
  month,
@@ -2451,6 +2598,15 @@ var emptyAiPocketSummary = ({
2451
2598
  }) => ({
2452
2599
  support: 0,
2453
2600
  supportRatio,
2601
+ events: 0,
2602
+ eventBalancedProfit: 0,
2603
+ tradesPerEvent: null,
2604
+ p95Batch: 0,
2605
+ maxBatch: 0,
2606
+ topEventCountShare: null,
2607
+ topEventProfitShare: null,
2608
+ topSymbolCountShare: null,
2609
+ topSymbolProfitShare: null,
2454
2610
  totalProfit: 0,
2455
2611
  grossProfit: 0,
2456
2612
  grossLoss: 0,
@@ -2522,9 +2678,42 @@ var finalizeAiPocketSummary = ({
2522
2678
  const worstMonth = [...accumulator.monthProfits.entries()].sort(
2523
2679
  (left, right) => left[1] - right[1]
2524
2680
  )[0] ?? null;
2681
+ const eventStats = [...accumulator.eventStats.values()];
2682
+ const events = eventStats.length;
2683
+ const batchSizes = eventStats.map((event) => event.count).sort((left, right) => left - right);
2684
+ const p95Batch = batchSizes[Math.max(0, Math.ceil(batchSizes.length * 0.95) - 1)] ?? 0;
2685
+ const maxBatch = batchSizes.at(-1) ?? 0;
2686
+ const absoluteEventProfit = eventStats.reduce(
2687
+ (sum, event) => sum + Math.abs(event.totalProfit),
2688
+ 0
2689
+ );
2690
+ const absoluteSymbolProfit = [...accumulator.symbolCounts.values()].reduce(
2691
+ (sum, symbol) => sum + Math.abs(symbol.totalProfit),
2692
+ 0
2693
+ );
2525
2694
  return {
2526
2695
  support,
2527
2696
  supportRatio,
2697
+ events,
2698
+ eventBalancedProfit: eventStats.reduce(
2699
+ (sum, event) => sum + event.totalProfit / event.count,
2700
+ 0
2701
+ ),
2702
+ tradesPerEvent: events > 0 ? support / events : null,
2703
+ p95Batch,
2704
+ maxBatch,
2705
+ topEventCountShare: support > 0 ? maxBatch / support : null,
2706
+ topEventProfitShare: absoluteEventProfit > 0 ? Math.max(...eventStats.map((event) => Math.abs(event.totalProfit))) / absoluteEventProfit : null,
2707
+ topSymbolCountShare: support > 0 ? Math.max(
2708
+ ...[...accumulator.symbolCounts.values()].map(
2709
+ (symbol) => symbol.count
2710
+ )
2711
+ ) / support : null,
2712
+ topSymbolProfitShare: absoluteSymbolProfit > 0 ? Math.max(
2713
+ ...[...accumulator.symbolCounts.values()].map(
2714
+ (symbol) => Math.abs(symbol.totalProfit)
2715
+ )
2716
+ ) / absoluteSymbolProfit : null,
2528
2717
  totalProfit,
2529
2718
  grossProfit,
2530
2719
  grossLoss,
@@ -2829,27 +3018,82 @@ var buildAiPocketPredicateResult = (rows, options = {}) => {
2829
3018
  var scorePositivePocket = (summary) => {
2830
3019
  const profitFactor = summary.profitFactor ?? (summary.grossLoss === 0 ? 8 : 0);
2831
3020
  const winRate = summary.winRate ?? 0;
2832
- return summary.totalProfit - summary.maxDrawdown * 0.6 + Math.min(profitFactor, 8) * 12 + winRate * 30 + Math.log10(summary.support + 1) * 6;
3021
+ return summary.eventBalancedProfit - summary.maxDrawdown * 0.6 + Math.min(profitFactor, 8) * 12 + winRate * 30 + Math.log10(summary.events + 1) * 6;
2833
3022
  };
2834
3023
  var scoreNegativePocket = (summary) => -summary.totalProfit + summary.maxDrawdown * 0.4 + summary.grossLoss * 0.2 + summary.maxConsecutiveLosses * 3;
2835
3024
  var comparePositivePockets = (left, right) => (right.validationScore ?? right.score) - (left.validationScore ?? left.score) || right.score - left.score || (right.validationSummary?.totalProfit ?? right.summary.totalProfit) - (left.validationSummary?.totalProfit ?? left.summary.totalProfit) || right.summary.totalProfit - left.summary.totalProfit || (right.summary.profitFactor ?? 999) - (left.summary.profitFactor ?? 999) || right.summary.support - left.summary.support;
2836
3025
  var compareNegativePockets = (left, right) => (right.validationScore ?? right.score) - (left.validationScore ?? left.score) || right.score - left.score || (left.validationSummary?.totalProfit ?? left.summary.totalProfit) - (right.validationSummary?.totalProfit ?? right.summary.totalProfit) || left.summary.totalProfit - right.summary.totalProfit || right.summary.grossLoss - left.summary.grossLoss || right.summary.support - left.summary.support;
2837
- var createPocketResult = (rows, predicates, mask, validationRows) => {
3026
+ var mergeDistinctRows = (left, right) => {
3027
+ const seenRows = /* @__PURE__ */ new Set();
3028
+ const seenSignalIds = /* @__PURE__ */ new Set();
3029
+ return [...left, ...right].filter((row) => {
3030
+ if (seenRows.has(row)) {
3031
+ return false;
3032
+ }
3033
+ seenRows.add(row);
3034
+ const signalId = typeof row.signalId === "string" && row.signalId.trim() ? row.signalId : null;
3035
+ if (signalId == null) {
3036
+ return true;
3037
+ }
3038
+ if (seenSignalIds.has(signalId)) {
3039
+ return false;
3040
+ }
3041
+ seenSignalIds.add(signalId);
3042
+ return true;
3043
+ });
3044
+ };
3045
+ var resolveObjectiveRows = ({
3046
+ objective,
3047
+ selectedRows,
3048
+ baselineRows
3049
+ }) => objective === "add-to-gate" ? mergeDistinctRows(baselineRows, selectedRows) : selectedRows;
3050
+ var effectiveProfitFactor = (summary) => summary.profitFactor ?? (summary.grossLoss === 0 && summary.totalProfit >= 0 ? Number.POSITIVE_INFINITY : 0);
3051
+ var doesNotRegressRisk = (candidate, baseline) => candidate.totalProfit >= baseline.totalProfit - 1e-9 && effectiveProfitFactor(candidate) >= effectiveProfitFactor(baseline) && candidate.maxDrawdown <= baseline.maxDrawdown + 1e-9 && candidate.maxConsecutiveLosses <= baseline.maxConsecutiveLosses && candidate.losingMonths <= baseline.losingMonths;
3052
+ var createPocketResult = (rows, predicates, mask, validationRows, testRows, objective, baselineRows, validationBaselineRows, testBaselineRows, objectiveBaseline, validationObjectiveBaseline, testObjectiveBaseline) => {
2838
3053
  const publicPredicates = predicates.map(toPublicPredicate);
2839
3054
  const summary = summarizeMask(rows, mask);
3055
+ const selectedRows = rows.filter((_, index) => mask[index] === 1);
3056
+ const objectiveRows = resolveObjectiveRows({
3057
+ objective,
3058
+ selectedRows,
3059
+ baselineRows
3060
+ });
3061
+ const objectiveSummary = objective === "add-to-gate" ? summarizeAiPocketRows(objectiveRows) : summary;
2840
3062
  const validationMask = validationRows.length ? buildPredicateListMask(validationRows, publicPredicates).mask : null;
2841
3063
  const validationSummary = validationMask == null ? void 0 : summarizeMask(validationRows, validationMask);
3064
+ const validationSelectedRows = validationMask == null ? [] : validationRows.filter((_, index) => validationMask[index] === 1);
3065
+ const validationObjectiveRows = resolveObjectiveRows({
3066
+ objective,
3067
+ selectedRows: validationSelectedRows,
3068
+ baselineRows: validationBaselineRows
3069
+ });
3070
+ const validationObjectiveSummary = validationRows.length ? objective === "add-to-gate" ? summarizeAiPocketRows(validationObjectiveRows) : validationSummary : void 0;
3071
+ const testMask = testRows.length ? buildPredicateListMask(testRows, publicPredicates).mask : null;
3072
+ const testSummary = testMask == null ? void 0 : summarizeMask(testRows, testMask);
3073
+ const testSelectedRows = testMask == null ? [] : testRows.filter((_, index) => testMask[index] === 1);
3074
+ const testObjectiveRows = resolveObjectiveRows({
3075
+ objective,
3076
+ selectedRows: testSelectedRows,
3077
+ baselineRows: testBaselineRows
3078
+ });
3079
+ const testObjectiveSummary = testRows.length ? objective === "add-to-gate" ? summarizeAiPocketRows(testObjectiveRows) : testSummary : void 0;
2842
3080
  const condition = publicPredicates.map((predicate) => predicate.label).join(" AND ");
2843
- const validationScore = validationSummary == null ? void 0 : validationSummary.support > 0 ? scorePositivePocket(validationSummary) : Number.NEGATIVE_INFINITY;
3081
+ const validationScore = validationSummary == null || validationObjectiveSummary == null ? void 0 : validationSummary.support > 0 ? scorePositivePocket(validationObjectiveSummary) - (objective === "standalone" ? 0 : scorePositivePocket(validationObjectiveBaseline)) : Number.NEGATIVE_INFINITY;
3082
+ const testScore = testSummary == null || testObjectiveSummary == null ? void 0 : testSummary.support > 0 ? scorePositivePocket(testObjectiveSummary) - (objective === "standalone" ? 0 : scorePositivePocket(testObjectiveBaseline)) : Number.NEGATIVE_INFINITY;
2844
3083
  return {
2845
3084
  id: publicPredicates.map((predicate) => predicate.id).join("&&"),
2846
3085
  depth: publicPredicates.length,
2847
3086
  predicates: publicPredicates,
2848
3087
  condition,
2849
3088
  summary,
3089
+ ...objective === "standalone" ? {} : { objectiveSummary },
2850
3090
  ...validationSummary ? { validationSummary } : {},
3091
+ ...testSummary ? { testSummary } : {},
3092
+ ...objective !== "standalone" && validationObjectiveSummary ? { validationObjectiveSummary } : {},
3093
+ ...objective !== "standalone" && testObjectiveSummary ? { testObjectiveSummary } : {},
2851
3094
  ...validationScore != null ? { validationScore } : {},
2852
- score: scorePositivePocket(summary)
3095
+ ...testScore != null ? { testScore } : {},
3096
+ score: scorePositivePocket(objectiveSummary) - (objective === "standalone" ? 0 : scorePositivePocket(objectiveBaseline))
2853
3097
  };
2854
3098
  };
2855
3099
  var hashMask = (mask) => {
@@ -2880,6 +3124,83 @@ var estimateCombinationCount = (poolSize, maxDepth, maxCombinations) => {
2880
3124
  }
2881
3125
  return Math.min(Math.trunc(total), maxCombinations);
2882
3126
  };
3127
+ var classifyFeatureFamily = (featureKey) => {
3128
+ const key = featureKey.toLowerCase();
3129
+ if (/(setup|liquidity|doubletap|trendline|pivot|swing|zone)/.test(key)) {
3130
+ return "strategy-structure";
3131
+ }
3132
+ if (/(structure|breakout|rejection|range|level)/.test(key)) {
3133
+ return "market-structure";
3134
+ }
3135
+ if (/(participation|volume|turnover|obv|effort)/.test(key)) {
3136
+ return "participation";
3137
+ }
3138
+ if (/(relative|benchmark|cmc|breadth|targetvs|btc|eth)/.test(key)) {
3139
+ return "relative-market";
3140
+ }
3141
+ if (/(derivative|funding|openinterest|liquidation|longshort)/.test(key)) {
3142
+ return "derivatives";
3143
+ }
3144
+ if (/(risk|stop|takeprofit|execution|spread|slippage)/.test(key)) {
3145
+ return "execution-risk";
3146
+ }
3147
+ if (/(regime|momentum|trend|volatility|atr|macd|rsi|ma)/.test(key)) {
3148
+ return "regime-indicators";
3149
+ }
3150
+ return "other";
3151
+ };
3152
+ var diversifyPredicatePool = (predicates, maximum) => {
3153
+ const byFamily = /* @__PURE__ */ new Map();
3154
+ const featureCounts = /* @__PURE__ */ new Map();
3155
+ for (const predicate of predicates) {
3156
+ const family = classifyFeatureFamily(predicate.featureKey);
3157
+ const familyPredicates = byFamily.get(family) ?? [];
3158
+ familyPredicates.push(predicate);
3159
+ byFamily.set(family, familyPredicates);
3160
+ }
3161
+ const selected = [];
3162
+ const selectedIds = /* @__PURE__ */ new Set();
3163
+ while (selected.length < maximum) {
3164
+ let added = false;
3165
+ for (const familyPredicates of byFamily.values()) {
3166
+ while (familyPredicates.length) {
3167
+ const predicate = familyPredicates.shift();
3168
+ if (selectedIds.has(predicate.id) || (featureCounts.get(predicate.featureKey) ?? 0) >= 4) {
3169
+ continue;
3170
+ }
3171
+ selected.push(predicate);
3172
+ selectedIds.add(predicate.id);
3173
+ featureCounts.set(
3174
+ predicate.featureKey,
3175
+ (featureCounts.get(predicate.featureKey) ?? 0) + 1
3176
+ );
3177
+ added = true;
3178
+ break;
3179
+ }
3180
+ if (selected.length >= maximum) {
3181
+ break;
3182
+ }
3183
+ }
3184
+ if (!added) {
3185
+ break;
3186
+ }
3187
+ }
3188
+ return selected;
3189
+ };
3190
+ var canCombinePredicate = (chosen, candidate) => {
3191
+ const sameFeature = chosen.filter(
3192
+ (predicate) => predicate.featureKey === candidate.featureKey
3193
+ );
3194
+ if (!sameFeature.length) {
3195
+ return true;
3196
+ }
3197
+ if (sameFeature.length !== 1 || sameFeature[0].kind !== "numeric" || candidate.kind !== "numeric" || sameFeature[0].op === candidate.op) {
3198
+ return false;
3199
+ }
3200
+ const lower = candidate.op === ">=" ? candidate.threshold : sameFeature[0].threshold;
3201
+ const upper = candidate.op === "<=" ? candidate.threshold : sameFeature[0].threshold;
3202
+ return lower <= upper;
3203
+ };
2883
3204
  var searchAiPockets = (rows, options = {}) => {
2884
3205
  const minSupport = Math.max(1, Math.trunc(options.minSupport ?? 20));
2885
3206
  const minProfitFactor = Math.max(0, options.minProfitFactor ?? 1.2);
@@ -2896,10 +3217,36 @@ var searchAiPockets = (rows, options = {}) => {
2896
3217
  );
2897
3218
  const top = Math.max(1, Math.trunc(options.top ?? 50));
2898
3219
  const validationRows = options.validationRows ?? [];
3220
+ const testRows = options.testRows ?? [];
2899
3221
  const minValidationSupport = Math.max(
2900
3222
  0,
2901
3223
  Math.trunc(options.minValidationSupport ?? 0)
2902
3224
  );
3225
+ const minEvents = Math.max(1, Math.trunc(options.minEvents ?? 1));
3226
+ const minValidationEvents = Math.max(
3227
+ 0,
3228
+ Math.trunc(options.minValidationEvents ?? 0)
3229
+ );
3230
+ const maxBatch = Math.max(1, options.maxBatch ?? Number.POSITIVE_INFINITY);
3231
+ const maxEventCountShare = Math.max(
3232
+ 0,
3233
+ Math.min(1, options.maxEventCountShare ?? 1)
3234
+ );
3235
+ const maxSymbolCountShare = Math.max(
3236
+ 0,
3237
+ Math.min(1, options.maxSymbolCountShare ?? 1)
3238
+ );
3239
+ const objective = options.objective ?? "standalone";
3240
+ const baselineRows = options.baselineRows ?? (objective === "filter-gate" ? rows : []);
3241
+ const validationBaselineRows = options.validationBaselineRows ?? (objective === "filter-gate" ? validationRows : []);
3242
+ const testBaselineRows = options.testBaselineRows ?? (objective === "filter-gate" ? testRows : []);
3243
+ const objectiveBaseline = summarizeAiPocketRows(baselineRows);
3244
+ const validationObjectiveBaseline = summarizeAiPocketRows(
3245
+ validationBaselineRows
3246
+ );
3247
+ const testObjectiveBaseline = summarizeAiPocketRows(testBaselineRows);
3248
+ const allowRiskRegression = options.allowRiskRegression === true;
3249
+ const requireValidationEligibility = options.requireValidationEligibility === true;
2903
3250
  const dedupeEquivalentSelections = options.dedupeEquivalentSelections !== false;
2904
3251
  const progressInterval = Math.max(
2905
3252
  1,
@@ -2942,11 +3289,13 @@ var searchAiPockets = (rows, options = {}) => {
2942
3289
  const supportPool = [...scoredPredicates].sort((left, right) => right.support - left.support).slice(0, Math.ceil(maxAtomicPredicates * 0.2));
2943
3290
  const predicatePoolById = /* @__PURE__ */ new Map();
2944
3291
  [...predicatePool, ...negativePool, ...supportPool].forEach((predicate) => {
2945
- if (predicatePoolById.size < maxAtomicPredicates) {
2946
- predicatePoolById.set(predicate.id, predicate);
2947
- }
3292
+ predicatePoolById.set(predicate.id, predicate);
2948
3293
  });
2949
- const selectedPredicatePool = [...predicatePoolById.values()].map(
3294
+ const diversifiedPredicates = diversifyPredicatePool(
3295
+ [...predicatePoolById.values()],
3296
+ maxAtomicPredicates
3297
+ );
3298
+ const selectedPredicatePool = diversifiedPredicates.map(
2950
3299
  (predicate) => {
2951
3300
  const { mask, support } = buildMask(rows, predicate);
2952
3301
  return {
@@ -2990,14 +3339,30 @@ var searchAiPockets = (rows, options = {}) => {
2990
3339
  rows,
2991
3340
  pocketPredicates,
2992
3341
  mask,
2993
- validationRows
3342
+ validationRows,
3343
+ testRows,
3344
+ objective,
3345
+ baselineRows,
3346
+ validationBaselineRows,
3347
+ testBaselineRows,
3348
+ objectiveBaseline,
3349
+ validationObjectiveBaseline,
3350
+ testObjectiveBaseline
2994
3351
  );
2995
3352
  const { summary } = pocket;
2996
- const profitFactor = summary.profitFactor ?? (summary.grossLoss === 0 && summary.totalProfit > 0 ? Number.POSITIVE_INFINITY : 0);
2997
- const winRate = summary.winRate ?? 0;
3353
+ const scoredSummary = pocket.objectiveSummary ?? summary;
3354
+ const profitFactor = scoredSummary.profitFactor ?? (scoredSummary.grossLoss === 0 && scoredSummary.totalProfit > 0 ? Number.POSITIVE_INFINITY : 0);
3355
+ const winRate = scoredSummary.winRate ?? 0;
2998
3356
  const validationSupport = pocket.validationSummary?.support ?? 0;
2999
- const validationEligible = !validationRows.length || validationSupport >= minValidationSupport;
3000
- if (summary.support >= minSupport && summary.totalProfit >= minTotalProfit && profitFactor >= minProfitFactor && winRate >= minWinRate && validationEligible) {
3357
+ const validationEligible = !validationRows.length || validationSupport >= minValidationSupport && (pocket.validationSummary?.events ?? 0) >= minValidationEvents && (pocket.validationSummary?.maxBatch ?? 0) <= maxBatch && (pocket.validationSummary?.topEventCountShare ?? 0) <= maxEventCountShare && (pocket.validationSummary?.topSymbolCountShare ?? 0) <= maxSymbolCountShare && (!requireValidationEligibility || (pocket.validationObjectiveSummary ?? pocket.validationSummary).totalProfit >= minTotalProfit && effectiveProfitFactor(
3358
+ pocket.validationObjectiveSummary ?? pocket.validationSummary
3359
+ ) >= minProfitFactor && ((pocket.validationObjectiveSummary ?? pocket.validationSummary).winRate ?? 0) >= minWinRate);
3360
+ const concentrationEligible = summary.events >= minEvents && summary.maxBatch <= maxBatch && (summary.topEventCountShare ?? 0) <= maxEventCountShare && (summary.topSymbolCountShare ?? 0) <= maxSymbolCountShare;
3361
+ const riskEligible = objective === "standalone" || allowRiskRegression || (!objectiveBaseline.support || doesNotRegressRisk(scoredSummary, objectiveBaseline)) && (!validationRows.length || !validationObjectiveBaseline.support || doesNotRegressRisk(
3362
+ pocket.validationObjectiveSummary ?? pocket.validationSummary,
3363
+ validationObjectiveBaseline
3364
+ ));
3365
+ if (summary.support >= minSupport && scoredSummary.totalProfit >= minTotalProfit && profitFactor >= minProfitFactor && winRate >= minWinRate && validationEligible && concentrationEligible && riskEligible) {
3001
3366
  const selectionKey = `${summary.support}:${hashMask(mask)}`;
3002
3367
  const mapKey = dedupeEquivalentSelections ? selectionKey : pocket.id;
3003
3368
  const existing = dedupeEquivalentSelections ? positiveSelectionKeys.get(mapKey) : positivePockets.get(mapKey);
@@ -3034,8 +3399,7 @@ var searchAiPockets = (rows, options = {}) => {
3034
3399
  const visit = ({
3035
3400
  startIndex,
3036
3401
  chosen,
3037
- mask,
3038
- usedFeatureKeys
3402
+ mask
3039
3403
  }) => {
3040
3404
  if (truncated) {
3041
3405
  return;
@@ -3046,7 +3410,7 @@ var searchAiPockets = (rows, options = {}) => {
3046
3410
  return;
3047
3411
  }
3048
3412
  const predicate = selectedPredicatePool[index];
3049
- if (usedFeatureKeys.has(predicate.featureKey)) {
3413
+ if (!canCombinePredicate(chosen, predicate)) {
3050
3414
  continue;
3051
3415
  }
3052
3416
  const intersection = mask == null ? { mask: predicate.mask, support: predicate.support } : intersectMasks(mask, predicate.mask);
@@ -3062,26 +3426,27 @@ var searchAiPockets = (rows, options = {}) => {
3062
3426
  if (nextChosen.length >= maxDepth) {
3063
3427
  continue;
3064
3428
  }
3065
- const nextUsedFeatureKeys = new Set(usedFeatureKeys);
3066
- nextUsedFeatureKeys.add(predicate.featureKey);
3067
3429
  visit({
3068
3430
  startIndex: index + 1,
3069
3431
  chosen: nextChosen,
3070
- mask: nextMask,
3071
- usedFeatureKeys: nextUsedFeatureKeys
3432
+ mask: nextMask
3072
3433
  });
3073
3434
  }
3074
3435
  };
3075
3436
  visit({
3076
3437
  startIndex: 0,
3077
3438
  chosen: [],
3078
- mask: null,
3079
- usedFeatureKeys: /* @__PURE__ */ new Set()
3439
+ mask: null
3080
3440
  });
3081
3441
  emitProgress(true);
3082
3442
  return {
3443
+ objective,
3083
3444
  baseline: summarizeAiPocketRows(rows),
3084
3445
  ...validationRows.length ? { validationBaseline: summarizeAiPocketRows(validationRows) } : {},
3446
+ ...testRows.length ? { testBaseline: summarizeAiPocketRows(testRows) } : {},
3447
+ ...objective === "standalone" ? {} : { objectiveBaseline },
3448
+ ...objective !== "standalone" && validationRows.length ? { validationObjectiveBaseline } : {},
3449
+ ...objective !== "standalone" && testRows.length ? { testObjectiveBaseline } : {},
3085
3450
  predicates: predicates.map(toPublicPredicate),
3086
3451
  positivePockets: [...positivePockets.values()].sort(comparePositivePockets).slice(0, top),
3087
3452
  negativePockets: [...negativePockets.values()].sort(compareNegativePockets).slice(0, top),
@@ -3093,7 +3458,15 @@ var searchAiPockets = (rows, options = {}) => {
3093
3458
  estimatedCombinations,
3094
3459
  combinationsEvaluated,
3095
3460
  validationRows: validationRows.length,
3461
+ testRows: testRows.length,
3096
3462
  duplicatePocketsSkipped,
3463
+ featureFamiliesUsed: [
3464
+ ...new Set(
3465
+ selectedPredicatePool.map(
3466
+ (predicate) => classifyFeatureFamily(predicate.featureKey)
3467
+ )
3468
+ )
3469
+ ],
3097
3470
  truncated
3098
3471
  }
3099
3472
  };
@@ -3111,6 +3484,13 @@ var markdownTable = (headers, rows) => {
3111
3484
  };
3112
3485
  var summaryMetricRows = (summary) => [
3113
3486
  ["rows", summary.support],
3487
+ ["events", summary.events],
3488
+ ["event_balanced_profit", formatMdNumber(summary.eventBalancedProfit)],
3489
+ ["trades_per_event", formatMdNumber(summary.tradesPerEvent)],
3490
+ ["p95_batch", summary.p95Batch],
3491
+ ["max_batch", summary.maxBatch],
3492
+ ["top_event_count_share", formatMdPercent(summary.topEventCountShare)],
3493
+ ["top_symbol_count_share", formatMdPercent(summary.topSymbolCountShare)],
3114
3494
  ["win_rate", formatMdPercent(summary.winRate)],
3115
3495
  ["total_profit", formatMdNumber(summary.totalProfit)],
3116
3496
  ["gross_profit", formatMdNumber(summary.grossProfit)],
@@ -3139,15 +3519,22 @@ var summaryMetricRows = (summary) => [
3139
3519
  var pocketRows = (pockets) => pockets.map((pocket, index) => [
3140
3520
  index + 1,
3141
3521
  pocket.summary.support,
3522
+ pocket.summary.events,
3523
+ pocket.summary.maxBatch,
3142
3524
  formatMdPercent(pocket.summary.supportRatio),
3143
3525
  formatMdPercent(pocket.summary.winRate),
3144
3526
  formatMdNumber(pocket.summary.profitFactor),
3145
3527
  formatMdNumber(pocket.summary.totalProfit),
3528
+ pocket.objectiveSummary ? formatMdNumber(pocket.objectiveSummary.totalProfit) : "n/a",
3146
3529
  formatMdNumber(pocket.summary.maxDrawdown),
3147
3530
  pocket.validationSummary?.support ?? "n/a",
3148
3531
  pocket.validationSummary ? formatMdPercent(pocket.validationSummary.winRate) : "n/a",
3149
3532
  pocket.validationSummary ? formatMdNumber(pocket.validationSummary.profitFactor) : "n/a",
3150
3533
  pocket.validationSummary ? formatMdNumber(pocket.validationSummary.totalProfit) : "n/a",
3534
+ pocket.testSummary?.support ?? "n/a",
3535
+ pocket.testSummary ? formatMdNumber(pocket.testSummary.profitFactor) : "n/a",
3536
+ pocket.testSummary ? formatMdNumber(pocket.testSummary.totalProfit) : "n/a",
3537
+ pocket.testObjectiveSummary ? formatMdNumber(pocket.testObjectiveSummary.totalProfit) : "n/a",
3151
3538
  formatMdNumber(pocket.summary.avgTradesPerDay),
3152
3539
  pocket.summary.losingMonths,
3153
3540
  formatMdNumber(pocket.score),
@@ -3161,6 +3548,13 @@ var buildAiPocketMarkdownReport = ({
3161
3548
  errors
3162
3549
  }) => {
3163
3550
  const generatedIso = new Date(generatedAt).toISOString();
3551
+ const featurePolicyAuditRows = Object.entries(
3552
+ run.featurePolicyAudit ?? {}
3553
+ ).map(([classification, audit]) => [
3554
+ classification,
3555
+ audit?.paths ?? 0,
3556
+ audit?.samples.join(", ") ?? ""
3557
+ ]);
3164
3558
  const lines = [
3165
3559
  "# AI Pocket Search Report",
3166
3560
  "",
@@ -3176,6 +3570,7 @@ var buildAiPocketMarkdownReport = ({
3176
3570
  ["selected_rows", run.selectedRows],
3177
3571
  ["evaluated_rows", run.evaluatedRows],
3178
3572
  ["scope", run.scope],
3573
+ ["objective", run.objective ?? pocketSearch.objective],
3179
3574
  ["direction", run.direction ?? "all"],
3180
3575
  ["scope_rows", run.scopeRows],
3181
3576
  ["train_rows", run.trainRows],
@@ -3199,15 +3594,44 @@ var buildAiPocketMarkdownReport = ({
3199
3594
  ["min_quality", run.minQuality],
3200
3595
  ["max_depth", run.search.maxDepth],
3201
3596
  ["min_support", run.search.minSupport],
3597
+ ["min_events", run.search.minEvents ?? 1],
3598
+ ["min_validation_events", run.search.minValidationEvents ?? 0],
3599
+ ["max_batch", run.search.maxBatch ?? "off"],
3600
+ [
3601
+ "max_event_count_share",
3602
+ run.search.maxEventCountShare == null ? "off" : formatMdPercent(run.search.maxEventCountShare)
3603
+ ],
3604
+ [
3605
+ "max_symbol_count_share",
3606
+ run.search.maxSymbolCountShare == null ? "off" : formatMdPercent(run.search.maxSymbolCountShare)
3607
+ ],
3608
+ [
3609
+ "allow_risk_regression",
3610
+ run.search.allowRiskRegression ? "yes" : "no"
3611
+ ],
3612
+ [
3613
+ "require_validation_eligibility",
3614
+ run.search.requireValidationEligibility ? "yes" : "no"
3615
+ ],
3202
3616
  ["max_atomic_predicates", run.search.maxAtomicPredicates],
3203
3617
  ["max_combinations", run.search.maxCombinations],
3204
3618
  ["include_symbol", run.includeSymbol ? "on" : "off"],
3205
3619
  ["include_gate_context", run.includeGateContext ? "on" : "off"],
3206
3620
  ["feature_profile", run.featureProfile ?? "all"],
3621
+ ["feature_policy", run.featurePolicy ?? "all"],
3207
3622
  ["report_path", run.reportPath]
3208
3623
  ]
3209
3624
  ),
3210
3625
  "",
3626
+ ...featurePolicyAuditRows.length ? [
3627
+ "## Feature Policy Audit",
3628
+ "",
3629
+ markdownTable(
3630
+ ["Classification", "Excluded paths", "Samples"],
3631
+ featurePolicyAuditRows
3632
+ ),
3633
+ ""
3634
+ ] : [],
3211
3635
  "## Dataset Files",
3212
3636
  "",
3213
3637
  ...run.filePaths.map((filePath) => `- \`${filePath}\``),
@@ -3244,6 +3668,33 @@ var buildAiPocketMarkdownReport = ({
3244
3668
  ),
3245
3669
  ""
3246
3670
  ] : [],
3671
+ ...pocketSearch.testBaseline ? [
3672
+ "## Untouched Test Baseline",
3673
+ "",
3674
+ markdownTable(
3675
+ ["Metric", "Value"],
3676
+ summaryMetricRows(pocketSearch.testBaseline)
3677
+ ),
3678
+ ""
3679
+ ] : [],
3680
+ ...pocketSearch.objectiveBaseline ? [
3681
+ "## Current Gate Train Baseline",
3682
+ "",
3683
+ markdownTable(
3684
+ ["Metric", "Value"],
3685
+ summaryMetricRows(pocketSearch.objectiveBaseline)
3686
+ ),
3687
+ ""
3688
+ ] : [],
3689
+ ...pocketSearch.testObjectiveBaseline ? [
3690
+ "## Current Gate Untouched Test Baseline",
3691
+ "",
3692
+ markdownTable(
3693
+ ["Metric", "Value"],
3694
+ summaryMetricRows(pocketSearch.testObjectiveBaseline)
3695
+ ),
3696
+ ""
3697
+ ] : [],
3247
3698
  "## Search Stats",
3248
3699
  "",
3249
3700
  markdownTable(
@@ -3252,9 +3703,11 @@ var buildAiPocketMarkdownReport = ({
3252
3703
  ["feature_keys", pocketSearch.stats.featureKeys],
3253
3704
  ["predicates", pocketSearch.stats.predicates],
3254
3705
  ["atomic_used", pocketSearch.stats.atomicPredicatesUsed],
3706
+ ["feature_families", pocketSearch.stats.featureFamiliesUsed.join(", ")],
3255
3707
  ["estimated_combinations", pocketSearch.stats.estimatedCombinations],
3256
3708
  ["combinations_evaluated", pocketSearch.stats.combinationsEvaluated],
3257
3709
  ["validation_rows", pocketSearch.stats.validationRows],
3710
+ ["test_rows", pocketSearch.stats.testRows],
3258
3711
  [
3259
3712
  "duplicate_pockets_skipped",
3260
3713
  pocketSearch.stats.duplicatePocketsSkipped
@@ -3269,15 +3722,22 @@ var buildAiPocketMarkdownReport = ({
3269
3722
  [
3270
3723
  "#",
3271
3724
  "N",
3725
+ "Events",
3726
+ "Max batch",
3272
3727
  "Support",
3273
3728
  "WR",
3274
3729
  "PF",
3275
3730
  "PNL",
3731
+ "Objective PNL",
3276
3732
  "Max DD",
3277
3733
  "Val N",
3278
3734
  "Val WR",
3279
3735
  "Val PF",
3280
3736
  "Val PNL",
3737
+ "Test N",
3738
+ "Test PF",
3739
+ "Test PNL",
3740
+ "Test objective PNL",
3281
3741
  "Trades/Day",
3282
3742
  "Losing Months",
3283
3743
  "Score",
@@ -3292,15 +3752,22 @@ var buildAiPocketMarkdownReport = ({
3292
3752
  [
3293
3753
  "#",
3294
3754
  "N",
3755
+ "Events",
3756
+ "Max batch",
3295
3757
  "Support",
3296
3758
  "WR",
3297
3759
  "PF",
3298
3760
  "PNL",
3761
+ "Objective PNL",
3299
3762
  "Max DD",
3300
3763
  "Val N",
3301
3764
  "Val WR",
3302
3765
  "Val PF",
3303
3766
  "Val PNL",
3767
+ "Test N",
3768
+ "Test PF",
3769
+ "Test PNL",
3770
+ "Test objective PNL",
3304
3771
  "Trades/Day",
3305
3772
  "Losing Months",
3306
3773
  "Score",
@@ -3478,6 +3945,46 @@ import_args.default.option(
3478
3945
  "Minimum validation rows required for positive pockets (0 = auto)",
3479
3946
  0
3480
3947
  );
3948
+ import_args.default.option(
3949
+ ["H", "minEvents"],
3950
+ "Minimum independent timestamp events required (0 = auto)",
3951
+ 0
3952
+ );
3953
+ import_args.default.option(
3954
+ ["J", "minValidationEvents"],
3955
+ "Minimum validation timestamp events required (0 = auto)",
3956
+ 0
3957
+ );
3958
+ import_args.default.option(
3959
+ ["X", "maxBatch"],
3960
+ "Maximum selected rows at one timestamp (0 disables)",
3961
+ 5
3962
+ );
3963
+ import_args.default.option(
3964
+ ["U", "maxEventCountShare"],
3965
+ "Maximum row share contributed by one timestamp event",
3966
+ "0.25"
3967
+ );
3968
+ import_args.default.option(
3969
+ ["Z", "maxSymbolCountShare"],
3970
+ "Maximum row share contributed by one symbol",
3971
+ "0.5"
3972
+ );
3973
+ import_args.default.option(
3974
+ ["A", "objective"],
3975
+ "Search objective: auto, standalone, add-to-gate, or filter-gate",
3976
+ "auto"
3977
+ );
3978
+ import_args.default.option(
3979
+ ["G", "allowRiskRegression"],
3980
+ "Allow incremental pockets to worsen q4+ PF, PnL, drawdown, or loss streak",
3981
+ false
3982
+ );
3983
+ import_args.default.option(
3984
+ ["L", "allowValidationRegression"],
3985
+ "Keep train-positive pockets that fail validation PnL/PF thresholds",
3986
+ false
3987
+ );
3481
3988
  import_args.default.option(
3482
3989
  ["D", "dedupeEquivalentSelections"],
3483
3990
  "Collapse pockets that select the same train rows",
@@ -3495,6 +4002,11 @@ import_args.default.option(
3495
4002
  "Feature extraction profile: compact or all",
3496
4003
  "all"
3497
4004
  );
4005
+ import_args.default.option(
4006
+ ["K", "featurePolicy"],
4007
+ "Feature policy: causal-stationary or all",
4008
+ "causal-stationary"
4009
+ );
3498
4010
  import_args.default.option(
3499
4011
  ["r", "reportDir"],
3500
4012
  "Directory for generated Markdown reports",
@@ -3599,10 +4111,41 @@ var normalizeRatio = (value, fallback) => {
3599
4111
  }
3600
4112
  return Math.max(0, Math.min(0.9, parsed));
3601
4113
  };
4114
+ var normalizeUnitRatio = (value, fallback) => {
4115
+ const parsed = Number(value);
4116
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : fallback;
4117
+ };
3602
4118
  var normalizeFeatureProfile = (value) => {
3603
4119
  const normalized = String(value || "all").trim().toLowerCase();
3604
4120
  return normalized === "compact" ? "compact" : "all";
3605
4121
  };
4122
+ var normalizeFeaturePolicy = (value) => {
4123
+ const normalized = String(value || "causal-stationary").trim().toLowerCase();
4124
+ if (!["causal-stationary", "all"].includes(normalized)) {
4125
+ throw new Error(
4126
+ `Unsupported --featurePolicy "${normalized}". Use causal-stationary or all.`
4127
+ );
4128
+ }
4129
+ return normalized;
4130
+ };
4131
+ var normalizeObjective = (value, scope) => {
4132
+ const normalized = String(value || "auto").trim().toLowerCase();
4133
+ if (normalized === "auto") {
4134
+ if (scope === "approved") {
4135
+ return "filter-gate";
4136
+ }
4137
+ if (scope === "rejected" || scope === "candidates") {
4138
+ return "add-to-gate";
4139
+ }
4140
+ return "standalone";
4141
+ }
4142
+ if (!["standalone", "add-to-gate", "filter-gate"].includes(normalized)) {
4143
+ throw new Error(
4144
+ `Unsupported --objective "${normalized}". Use auto, standalone, add-to-gate, or filter-gate.`
4145
+ );
4146
+ }
4147
+ return normalized;
4148
+ };
3606
4149
  var normalizeQuality = (analysis) => {
3607
4150
  const quality = Number(analysis?.quality);
3608
4151
  return Number.isFinite(quality) ? Math.round(quality) : null;
@@ -3757,7 +4300,7 @@ var resolveScopeRows = (rows, scope) => {
3757
4300
  return rows.filter((row) => !row.aiApproved);
3758
4301
  }
3759
4302
  if (scope === "candidates") {
3760
- return rows.filter((row) => row.modelCandidate);
4303
+ return rows.filter((row) => row.modelCandidate && !row.aiApproved);
3761
4304
  }
3762
4305
  return rows;
3763
4306
  };
@@ -3768,6 +4311,13 @@ var printSection = (title, table) => {
3768
4311
  };
3769
4312
  var buildSummaryRows = (summary) => [
3770
4313
  ["rows", import_chalk.default.cyan(String(summary.support))],
4314
+ ["events", import_chalk.default.cyan(String(summary.events))],
4315
+ ["event_balanced_profit", colorizeProfit(summary.eventBalancedProfit)],
4316
+ ["trades_per_event", colorizeNumber(summary.tradesPerEvent)],
4317
+ ["p95_batch", import_chalk.default.cyan(String(summary.p95Batch))],
4318
+ ["max_batch", import_chalk.default.cyan(String(summary.maxBatch))],
4319
+ ["top_event_count_share", colorizeRatio(summary.topEventCountShare)],
4320
+ ["top_symbol_count_share", colorizeRatio(summary.topSymbolCountShare)],
3771
4321
  ["win_rate", colorizeRatio(summary.winRate)],
3772
4322
  ["total_profit", colorizeProfit(summary.totalProfit)],
3773
4323
  ["gross_profit", colorizeProfit(summary.grossProfit)],
@@ -3796,15 +4346,22 @@ var buildSummaryRows = (summary) => [
3796
4346
  var buildPocketRows = (pockets) => pockets.map((pocket, index) => [
3797
4347
  import_chalk.default.gray(String(index + 1)),
3798
4348
  import_chalk.default.cyan(String(pocket.summary.support)),
4349
+ import_chalk.default.cyan(String(pocket.summary.events)),
4350
+ import_chalk.default.cyan(String(pocket.summary.maxBatch)),
3799
4351
  colorizeRatio(pocket.summary.supportRatio),
3800
4352
  colorizeRatio(pocket.summary.winRate),
3801
4353
  colorizeNumber(pocket.summary.profitFactor),
3802
4354
  colorizeProfit(pocket.summary.totalProfit),
4355
+ pocket.objectiveSummary ? colorizeProfit(pocket.objectiveSummary.totalProfit) : import_chalk.default.gray("n/a"),
3803
4356
  colorizeProfit(-pocket.summary.maxDrawdown),
3804
4357
  pocket.validationSummary ? import_chalk.default.cyan(String(pocket.validationSummary.support)) : import_chalk.default.gray("n/a"),
3805
4358
  pocket.validationSummary ? colorizeRatio(pocket.validationSummary.winRate) : import_chalk.default.gray("n/a"),
3806
4359
  pocket.validationSummary ? colorizeNumber(pocket.validationSummary.profitFactor) : import_chalk.default.gray("n/a"),
3807
4360
  pocket.validationSummary ? colorizeProfit(pocket.validationSummary.totalProfit) : import_chalk.default.gray("n/a"),
4361
+ pocket.testSummary ? import_chalk.default.cyan(String(pocket.testSummary.support)) : import_chalk.default.gray("n/a"),
4362
+ pocket.testSummary ? colorizeNumber(pocket.testSummary.profitFactor) : import_chalk.default.gray("n/a"),
4363
+ pocket.testSummary ? colorizeProfit(pocket.testSummary.totalProfit) : import_chalk.default.gray("n/a"),
4364
+ pocket.testObjectiveSummary ? colorizeProfit(pocket.testObjectiveSummary.totalProfit) : import_chalk.default.gray("n/a"),
3808
4365
  colorizeNumber(pocket.summary.avgTradesPerDay),
3809
4366
  import_chalk.default.yellow(String(pocket.summary.losingMonths)),
3810
4367
  colorizeNumber(pocket.score),
@@ -3915,16 +4472,45 @@ var main = async () => {
3915
4472
  flags.minValidationSupport,
3916
4473
  0
3917
4474
  );
4475
+ const explicitMinEvents = normalizeInt(flags.minEvents, 0);
4476
+ const explicitMinValidationEvents = normalizeInt(
4477
+ flags.minValidationEvents,
4478
+ 0
4479
+ );
4480
+ const maxBatch = normalizeInt(flags.maxBatch, 5);
4481
+ const maxEventCountShare = normalizeUnitRatio(
4482
+ readAiPocketSearchCliOption({
4483
+ argv: process.argv,
4484
+ longName: "maxEventCountShare",
4485
+ shortName: "U"
4486
+ }) ?? flags.maxEventCountShare,
4487
+ 0.25
4488
+ );
4489
+ const maxSymbolCountShare = normalizeUnitRatio(
4490
+ readAiPocketSearchCliOption({
4491
+ argv: process.argv,
4492
+ longName: "maxSymbolCountShare",
4493
+ shortName: "Z"
4494
+ }) ?? flags.maxSymbolCountShare,
4495
+ 0.5
4496
+ );
4497
+ const objective = normalizeObjective(flags.objective, scope);
4498
+ const allowRiskRegression = Boolean(flags.allowRiskRegression);
4499
+ const requireValidationEligibility = !Boolean(
4500
+ flags.allowValidationRegression
4501
+ );
3918
4502
  const dedupeEquivalentSelections = Boolean(flags.dedupeEquivalentSelections);
3919
4503
  const top = normalizePositiveInt(flags.top, 30);
3920
4504
  const includeSymbol = Boolean(flags.includeSymbol);
3921
4505
  const includeGateContext = Boolean(flags.includeGateContext);
3922
4506
  const featureProfile = normalizeFeatureProfile(flags.featureProfile);
4507
+ const featurePolicy = normalizeFeaturePolicy(flags.featurePolicy);
3923
4508
  const jsonOutput = Boolean(flags.json);
3924
4509
  const outputPath = String(flags.output || "").trim();
3925
4510
  const reportDir = String(flags.reportDir || "data/ai/output").trim();
3926
4511
  const explicitReportFile = String(flags.reportFile || "").trim();
3927
4512
  const generatedAt = Date.now();
4513
+ const excludedFeaturePaths = /* @__PURE__ */ new Map();
3928
4514
  await (0, import_ai2.ensureAiStrategyPluginsLoaded)();
3929
4515
  const filePaths = await resolveMergedDatasetFiles({
3930
4516
  outDir,
@@ -4029,7 +4615,13 @@ var main = async () => {
4029
4615
  gateContext,
4030
4616
  includeSymbol,
4031
4617
  includeGateContext,
4032
- featureProfile
4618
+ featureProfile,
4619
+ featurePolicy,
4620
+ onFeatureExcluded: ({ path: featurePath, classification }) => {
4621
+ const paths = excludedFeaturePaths.get(classification) ?? /* @__PURE__ */ new Set();
4622
+ paths.add(featurePath);
4623
+ excludedFeaturePaths.set(classification, paths);
4624
+ }
4033
4625
  })
4034
4626
  });
4035
4627
  bar?.tick(1, {
@@ -4050,10 +4642,24 @@ var main = async () => {
4050
4642
  }
4051
4643
  }
4052
4644
  });
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);
4645
+ const directionRows = direction ? rows.filter((row) => row.direction === direction) : rows;
4646
+ const scopeRows = resolveScopeRows(directionRows, scope);
4647
+ const split = splitAiPocketResearchRowsByTimestamp(
4648
+ directionRows,
4649
+ validationSplit,
4650
+ testSplit
4651
+ );
4652
+ const trainRows = resolveScopeRows(split.trainRows, scope);
4653
+ const validationRows = resolveScopeRows(split.validationRows, scope);
4654
+ const testRows = resolveScopeRows(split.testRows, scope);
4655
+ const baselineRows = split.trainRows.filter((row) => row.aiApproved);
4656
+ const validationBaselineRows = split.validationRows.filter(
4657
+ (row) => row.aiApproved
4658
+ );
4659
+ const testBaselineRows = split.testRows.filter((row) => row.aiApproved);
4056
4660
  const minValidationSupport = explicitMinValidationSupport > 0 ? explicitMinValidationSupport : validationRows.length ? Math.max(3, Math.ceil(minSupport * validationSplit * 0.5)) : 0;
4661
+ const minEvents = explicitMinEvents > 0 ? explicitMinEvents : Math.max(5, Math.ceil(minSupport * 0.5));
4662
+ const minValidationEvents = explicitMinValidationEvents > 0 ? explicitMinValidationEvents : validationRows.length ? Math.max(2, Math.ceil(minEvents * validationSplit * 0.5)) : 0;
4057
4663
  const currentGateSummary = summarizeAiTrainEvaluations(rows);
4058
4664
  const currentGateQualityThresholds = summarizeAiTrainEvaluationsByQualityThreshold(rows, qualityThresholds);
4059
4665
  let searchBar = null;
@@ -4068,7 +4674,19 @@ var main = async () => {
4068
4674
  maxCombinations,
4069
4675
  top,
4070
4676
  validationRows,
4677
+ testRows,
4071
4678
  minValidationSupport,
4679
+ minEvents,
4680
+ minValidationEvents,
4681
+ ...maxBatch > 0 ? { maxBatch } : {},
4682
+ maxEventCountShare,
4683
+ maxSymbolCountShare,
4684
+ objective,
4685
+ baselineRows,
4686
+ validationBaselineRows,
4687
+ testBaselineRows,
4688
+ allowRiskRegression,
4689
+ requireValidationEligibility,
4072
4690
  dedupeEquivalentSelections,
4073
4691
  progressInterval: 250,
4074
4692
  onProgress: (progress) => {
@@ -4133,6 +4751,17 @@ var main = async () => {
4133
4751
  includeSymbol,
4134
4752
  includeGateContext,
4135
4753
  featureProfile,
4754
+ featurePolicy,
4755
+ featurePolicyAudit: Object.fromEntries(
4756
+ [...excludedFeaturePaths.entries()].map(([classification, paths]) => [
4757
+ classification,
4758
+ {
4759
+ paths: paths.size,
4760
+ samples: [...paths].sort().slice(0, 5)
4761
+ }
4762
+ ])
4763
+ ),
4764
+ objective,
4136
4765
  validationSplit,
4137
4766
  testSplit,
4138
4767
  minValidationSupport,
@@ -4145,6 +4774,13 @@ var main = async () => {
4145
4774
  minTotalProfit,
4146
4775
  maxAtomicPredicates,
4147
4776
  maxCombinations,
4777
+ minEvents,
4778
+ minValidationEvents,
4779
+ ...maxBatch > 0 ? { maxBatch } : {},
4780
+ maxEventCountShare,
4781
+ maxSymbolCountShare,
4782
+ allowRiskRegression,
4783
+ requireValidationEligibility,
4148
4784
  validationSplit,
4149
4785
  testSplit,
4150
4786
  minValidationSupport,
@@ -4188,6 +4824,7 @@ var main = async () => {
4188
4824
  ["selected", import_chalk.default.blue(String(selectedRows))],
4189
4825
  ["evaluated", import_chalk.default.blue(String(rows.length))],
4190
4826
  ["scope", import_chalk.default.yellow(scope)],
4827
+ ["objective", import_chalk.default.yellow(objective)],
4191
4828
  ["direction", direction ? import_chalk.default.yellow(direction) : import_chalk.default.gray("all")],
4192
4829
  ["scope_rows", import_chalk.default.blue(String(scopeRows.length))],
4193
4830
  ["train_rows", import_chalk.default.blue(String(trainRows.length))],
@@ -4216,6 +4853,11 @@ var main = async () => {
4216
4853
  ["min_quality", import_chalk.default.magenta(String(minQuality))],
4217
4854
  ["max_depth", import_chalk.default.magenta(String(maxDepth))],
4218
4855
  ["min_support", import_chalk.default.magenta(String(minSupport))],
4856
+ ["min_events", import_chalk.default.magenta(String(minEvents))],
4857
+ ["min_validation_events", import_chalk.default.magenta(String(minValidationEvents))],
4858
+ ["max_batch", import_chalk.default.magenta(maxBatch > 0 ? String(maxBatch) : "off")],
4859
+ ["max_event_share", import_chalk.default.magenta(formatRatio(maxEventCountShare))],
4860
+ ["max_symbol_share", import_chalk.default.magenta(formatRatio(maxSymbolCountShare))],
4219
4861
  ["max_atomic_predicates", import_chalk.default.magenta(String(maxAtomicPredicates))],
4220
4862
  ["max_combinations", import_chalk.default.magenta(String(maxCombinations))],
4221
4863
  ["validation_split", import_chalk.default.magenta(formatRatio(validationSplit))],
@@ -4234,6 +4876,15 @@ var main = async () => {
4234
4876
  includeGateContext ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
4235
4877
  ],
4236
4878
  ["feature_profile", import_chalk.default.magenta(featureProfile)],
4879
+ ["feature_policy", import_chalk.default.magenta(featurePolicy)],
4880
+ [
4881
+ "allow_risk_regression",
4882
+ allowRiskRegression ? import_chalk.default.yellow("on") : import_chalk.default.green("off")
4883
+ ],
4884
+ [
4885
+ "validation_eligibility",
4886
+ requireValidationEligibility ? import_chalk.default.green("required") : import_chalk.default.yellow("off")
4887
+ ],
4237
4888
  ["report", import_chalk.default.gray(reportPath)],
4238
4889
  ["output", outputPath ? import_chalk.default.gray(outputPath) : import_chalk.default.gray("off")]
4239
4890
  ]
@@ -4271,6 +4922,24 @@ var main = async () => {
4271
4922
  )
4272
4923
  );
4273
4924
  }
4925
+ if (search.testBaseline) {
4926
+ printSection(
4927
+ "UNTOUCHED TEST BASELINE",
4928
+ createTable(
4929
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4930
+ buildSummaryRows(search.testBaseline)
4931
+ )
4932
+ );
4933
+ }
4934
+ if (search.objectiveBaseline) {
4935
+ printSection(
4936
+ "CURRENT GATE TRAIN BASELINE",
4937
+ createTable(
4938
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4939
+ buildSummaryRows(search.objectiveBaseline)
4940
+ )
4941
+ );
4942
+ }
4274
4943
  printSection(
4275
4944
  "SEARCH STATS",
4276
4945
  createTable(
@@ -4279,6 +4948,10 @@ var main = async () => {
4279
4948
  ["feature_keys", import_chalk.default.cyan(String(search.stats.featureKeys))],
4280
4949
  ["predicates", import_chalk.default.cyan(String(search.stats.predicates))],
4281
4950
  ["atomic_used", import_chalk.default.cyan(String(search.stats.atomicPredicatesUsed))],
4951
+ [
4952
+ "feature_families",
4953
+ import_chalk.default.cyan(search.stats.featureFamiliesUsed.join(", "))
4954
+ ],
4282
4955
  [
4283
4956
  "estimated_combinations",
4284
4957
  import_chalk.default.cyan(String(search.stats.estimatedCombinations))
@@ -4288,6 +4961,7 @@ var main = async () => {
4288
4961
  import_chalk.default.cyan(String(search.stats.combinationsEvaluated))
4289
4962
  ],
4290
4963
  ["validation_rows", import_chalk.default.cyan(String(search.stats.validationRows))],
4964
+ ["test_rows", import_chalk.default.cyan(String(search.stats.testRows))],
4291
4965
  [
4292
4966
  "duplicate_pockets_skipped",
4293
4967
  import_chalk.default.cyan(String(search.stats.duplicatePocketsSkipped))
@@ -4305,15 +4979,22 @@ var main = async () => {
4305
4979
  [
4306
4980
  import_chalk.default.gray("#"),
4307
4981
  import_chalk.default.gray("N"),
4982
+ import_chalk.default.gray("EVENTS"),
4983
+ import_chalk.default.gray("MAX_B"),
4308
4984
  import_chalk.default.gray("SUP"),
4309
4985
  import_chalk.default.gray("WR"),
4310
4986
  import_chalk.default.gray("PF"),
4311
4987
  import_chalk.default.gray("PNL"),
4988
+ import_chalk.default.gray("OBJ_PNL"),
4312
4989
  import_chalk.default.gray("MAX_DD"),
4313
4990
  import_chalk.default.gray("VAL_N"),
4314
4991
  import_chalk.default.gray("VAL_WR"),
4315
4992
  import_chalk.default.gray("VAL_PF"),
4316
4993
  import_chalk.default.gray("VAL_PNL"),
4994
+ import_chalk.default.gray("TEST_N"),
4995
+ import_chalk.default.gray("TEST_PF"),
4996
+ import_chalk.default.gray("TEST_PNL"),
4997
+ import_chalk.default.gray("TEST_OBJ"),
4317
4998
  import_chalk.default.gray("TR/D"),
4318
4999
  import_chalk.default.gray("LOSS_M"),
4319
5000
  import_chalk.default.gray("SCORE"),
@@ -4328,15 +5009,22 @@ var main = async () => {
4328
5009
  [
4329
5010
  import_chalk.default.gray("#"),
4330
5011
  import_chalk.default.gray("N"),
5012
+ import_chalk.default.gray("EVENTS"),
5013
+ import_chalk.default.gray("MAX_B"),
4331
5014
  import_chalk.default.gray("SUP"),
4332
5015
  import_chalk.default.gray("WR"),
4333
5016
  import_chalk.default.gray("PF"),
4334
5017
  import_chalk.default.gray("PNL"),
5018
+ import_chalk.default.gray("OBJ_PNL"),
4335
5019
  import_chalk.default.gray("MAX_DD"),
4336
5020
  import_chalk.default.gray("VAL_N"),
4337
5021
  import_chalk.default.gray("VAL_WR"),
4338
5022
  import_chalk.default.gray("VAL_PF"),
4339
5023
  import_chalk.default.gray("VAL_PNL"),
5024
+ import_chalk.default.gray("TEST_N"),
5025
+ import_chalk.default.gray("TEST_PF"),
5026
+ import_chalk.default.gray("TEST_PNL"),
5027
+ import_chalk.default.gray("TEST_OBJ"),
4340
5028
  import_chalk.default.gray("TR/D"),
4341
5029
  import_chalk.default.gray("LOSS_M"),
4342
5030
  import_chalk.default.gray("SCORE"),