@tradejs/cli 3.0.1 → 3.1.1

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.
@@ -34,310 +34,178 @@ __export(aiPocketSearch_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(aiPocketSearch_exports);
36
36
  var import_args = __toESM(require("args"));
37
- var import_chalk = __toESM(require("chalk"));
38
- var import_promises = __toESM(require("fs/promises"));
39
- var import_path = __toESM(require("path"));
40
- var import_progress = __toESM(require("progress"));
41
- var import_ai = require("@tradejs/infra/ai");
42
- var import_ai2 = require("@tradejs/node/ai");
43
- var import_registry = require("@tradejs/node/registry");
44
-
45
- // src/lib/aiTrainDataset.ts
46
- var extractSignalFromAiDatasetRow = (row) => {
47
- const { payload } = row;
48
- return {
49
- ...payload.signal,
50
- strategy: payload.signal.strategy,
51
- figures: payload.figures ?? {},
52
- indicators: payload.indicators ?? {},
53
- additionalIndicators: payload.additionalIndicators ?? {},
54
- prices: payload.signal.prices
55
- };
56
- };
57
37
 
58
- // src/lib/aiTrainMetrics.ts
59
- var divideOrNull = (num, denom) => {
60
- if (denom <= 0) {
61
- return null;
38
+ // src/lib/aiPocketSearchCli.ts
39
+ var DAY_MS = 24 * 60 * 60 * 1e3;
40
+ var DEFAULT_MIN_SUPPORT = 20;
41
+ var LOW_CADENCE_EVENT_LIMIT = 200;
42
+ var LOW_CADENCE_MAX_EVENT_SHARE = 1 / 3;
43
+ var countIndependentEvents = (rows) => {
44
+ const timestampEvents = /* @__PURE__ */ new Set();
45
+ let rowsWithoutTimestamp = 0;
46
+ for (const row of rows) {
47
+ if (typeof row.timestamp === "number" && Number.isFinite(row.timestamp)) {
48
+ timestampEvents.add(row.timestamp);
49
+ } else {
50
+ rowsWithoutTimestamp += 1;
51
+ }
62
52
  }
63
- return num / denom;
53
+ return timestampEvents.size + rowsWithoutTimestamp;
64
54
  };
65
- var DAY_MS = 24 * 60 * 60 * 1e3;
66
- var DAYS_PER_WEEK = 7;
67
- var DAYS_PER_MONTH = 30.4375;
68
- var DAYS_PER_YEAR = 365;
69
- var getEvaluationPeriodDays = (evaluations) => {
55
+ var getPeriodDays = (rows) => {
70
56
  let minTimestamp = null;
71
57
  let maxTimestamp = null;
72
- for (const evaluation of evaluations) {
73
- const timestamp = typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null;
74
- if (timestamp == null) {
58
+ for (const row of rows) {
59
+ const timestamp = row.timestamp;
60
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
75
61
  continue;
76
62
  }
77
- if (minTimestamp == null || timestamp < minTimestamp) {
78
- minTimestamp = timestamp;
79
- }
80
- if (maxTimestamp == null || timestamp > maxTimestamp) {
81
- maxTimestamp = timestamp;
82
- }
63
+ minTimestamp = minTimestamp == null ? timestamp : Math.min(minTimestamp, timestamp);
64
+ maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
83
65
  }
84
66
  if (minTimestamp == null || maxTimestamp == null) {
85
67
  return null;
86
68
  }
87
69
  return Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1);
88
70
  };
89
- var qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
90
- var calculateTradePnlRiskRatios = ({
91
- profits,
92
- totalProfit,
93
- maxDrawdown,
94
- periodDays
71
+ var clampInt = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, Math.ceil(value)));
72
+ var resolveAiPocketCadenceProfile = ({
73
+ trainRows,
74
+ validationRows = [],
75
+ testRows = [],
76
+ mode = "auto",
77
+ validationSplit = 0.25,
78
+ minSupport,
79
+ minEvents,
80
+ minValidationSupport,
81
+ minValidationEvents,
82
+ maxEventCountShare = 0.25,
83
+ explicitMaxEventCountShare = false
95
84
  }) => {
96
- if (!profits.length || periodDays == null || periodDays <= 0) {
97
- return {
98
- sharpeRatio: null,
99
- sortinoRatio: null,
100
- calmarRatio: null
101
- };
102
- }
103
- const meanProfit = totalProfit / profits.length;
104
- const variance = profits.reduce((sum, profit) => {
105
- const diff = profit - meanProfit;
106
- return sum + diff * diff;
107
- }, 0) / profits.length;
108
- const stdDev = Math.sqrt(variance);
109
- const downsideDeviation = Math.sqrt(
110
- profits.reduce(
111
- (sum, profit) => profit < 0 ? sum + profit * profit : sum,
112
- 0
113
- ) / profits.length
114
- );
115
- const annualizationScale = Math.sqrt(
116
- profits.length / periodDays * DAYS_PER_YEAR
117
- );
85
+ const trainEvents = countIndependentEvents(trainRows);
86
+ const validationEvents = countIndependentEvents(validationRows);
87
+ const testEvents = countIndependentEvents(testRows);
88
+ const trainPeriodDays = getPeriodDays(trainRows);
89
+ const trainEventsPerDay = trainPeriodDays == null ? null : trainEvents / trainPeriodDays;
90
+ const sparseSample = trainEvents < LOW_CADENCE_EVENT_LIMIT;
91
+ const lowCadence = trainEventsPerDay != null && trainEventsPerDay < 0.25;
92
+ const adaptiveSample = mode === "auto" && sparseSample;
93
+ const adaptiveThresholds = adaptiveSample && (minSupport == null || minEvents == null || validationRows.length > 0 && (minValidationSupport == null || minValidationEvents == null) || !explicitMaxEventCountShare);
94
+ const resolvedMinSupport = minSupport ?? (adaptiveSample ? clampInt(trainEvents * 0.1, 3, DEFAULT_MIN_SUPPORT) : DEFAULT_MIN_SUPPORT);
95
+ const resolvedMinEvents = minEvents ?? (minSupport != null || !adaptiveSample ? Math.max(5, Math.ceil(resolvedMinSupport * 0.5)) : clampInt(resolvedMinSupport * 0.5, 3, 10));
96
+ const hasValidation = validationRows.length > 0;
97
+ const resolvedMinValidationSupport = minValidationSupport ?? (hasValidation ? Math.max(3, Math.ceil(resolvedMinSupport * validationSplit * 0.5)) : 0);
98
+ const resolvedMinValidationEvents = minValidationEvents ?? (hasValidation ? Math.max(
99
+ adaptiveSample ? 3 : 2,
100
+ Math.ceil(resolvedMinEvents * validationSplit * 0.5)
101
+ ) : 0);
102
+ const resolvedMaxEventCountShare = adaptiveSample && !explicitMaxEventCountShare ? Math.max(maxEventCountShare, LOW_CADENCE_MAX_EVENT_SHARE) : maxEventCountShare;
118
103
  return {
119
- sharpeRatio: stdDev > 0 && annualizationScale > 0 ? meanProfit / stdDev * annualizationScale : null,
120
- sortinoRatio: downsideDeviation > 0 && annualizationScale > 0 ? meanProfit / downsideDeviation * annualizationScale : null,
121
- calmarRatio: maxDrawdown > 0 ? totalProfit / periodDays * DAYS_PER_YEAR / maxDrawdown : null
104
+ mode,
105
+ lowCadence,
106
+ sparseSample,
107
+ adaptiveThresholds,
108
+ trainRows: trainRows.length,
109
+ trainEvents,
110
+ trainPeriodDays,
111
+ trainEventsPerDay,
112
+ validationRows: validationRows.length,
113
+ validationEvents,
114
+ testRows: testRows.length,
115
+ testEvents,
116
+ minSupport: resolvedMinSupport,
117
+ minEvents: resolvedMinEvents,
118
+ minValidationSupport: resolvedMinValidationSupport,
119
+ minValidationEvents: resolvedMinValidationEvents,
120
+ maxEventCountShare: resolvedMaxEventCountShare
122
121
  };
123
122
  };
124
- var emptyRiskSummary = () => ({
125
- trades: 0,
126
- totalProfit: 0,
127
- grossProfit: 0,
128
- grossLoss: 0,
129
- profitFactor: null,
130
- payoffRatio: null,
131
- avgWin: null,
132
- avgLoss: null,
133
- largestWin: null,
134
- largestLoss: null,
135
- winRate: null,
136
- maxDrawdown: 0,
137
- maxDrawdownPctOfGrossProfit: null,
138
- maxDrawdownPctOfTotalProfit: null,
139
- sharpeRatio: null,
140
- sortinoRatio: null,
141
- calmarRatio: null,
142
- recoveryFactor: null,
143
- ulcerIndex: null,
144
- maxConsecutiveWins: 0,
145
- maxConsecutiveLosses: 0
146
- });
147
- var summarizeApprovedRisk = (evaluations, periodDays = getEvaluationPeriodDays(evaluations)) => {
148
- const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
149
- const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
150
- const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
151
- return leftTimestamp - rightTimestamp;
152
- });
153
- if (!approvedEvaluations.length) {
154
- return emptyRiskSummary();
123
+ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
124
+ minProfitFactor: "1.2",
125
+ minWinRate: "0",
126
+ minTotalProfit: "0",
127
+ validationSplit: "0.25",
128
+ testSplit: "0"
129
+ };
130
+ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
131
+ const timestamps = [
132
+ ...new Set(
133
+ rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
134
+ )
135
+ ].sort((left, right) => left - right);
136
+ if (timestamps.length < 2) {
137
+ return { trainRows: rows, validationRows: [], testRows: [] };
155
138
  }
156
- let grossProfit = 0;
157
- let grossLoss = 0;
158
- let wins = 0;
159
- let losses = 0;
160
- let largestWin = null;
161
- let largestLoss = null;
162
- let equity = 0;
163
- let peakEquity = 0;
164
- let maxDrawdown = 0;
165
- let drawdownSquares = 0;
166
- let currentWinStreak = 0;
167
- let currentLossStreak = 0;
168
- let maxConsecutiveWins = 0;
169
- let maxConsecutiveLosses = 0;
170
- const approvedProfits = [];
171
- for (const evaluation of approvedEvaluations) {
172
- const profit = evaluation.profit;
173
- approvedProfits.push(profit);
174
- if (profit > 0) {
175
- grossProfit += profit;
176
- wins += 1;
177
- largestWin = largestWin == null ? profit : Math.max(largestWin, profit);
178
- currentWinStreak += 1;
179
- currentLossStreak = 0;
180
- } else if (profit < 0) {
181
- grossLoss += Math.abs(profit);
182
- losses += 1;
183
- largestLoss = largestLoss == null ? profit : Math.min(largestLoss, profit);
184
- currentLossStreak += 1;
185
- currentWinStreak = 0;
186
- } else {
187
- currentWinStreak = 0;
188
- currentLossStreak = 0;
189
- }
190
- maxConsecutiveWins = Math.max(maxConsecutiveWins, currentWinStreak);
191
- maxConsecutiveLosses = Math.max(maxConsecutiveLosses, currentLossStreak);
192
- equity += profit;
193
- peakEquity = Math.max(peakEquity, equity);
194
- const drawdown = Math.max(0, peakEquity - equity);
195
- maxDrawdown = Math.max(maxDrawdown, drawdown);
196
- drawdownSquares += drawdown * drawdown;
139
+ const getSplitCount = (ratio) => ratio > 0 ? Math.max(1, Math.floor(timestamps.length * ratio)) : 0;
140
+ let testEvents = getSplitCount(testSplit);
141
+ let validationEvents = getSplitCount(validationSplit);
142
+ const maximumHeldOut = timestamps.length - 1;
143
+ if (testEvents + validationEvents > maximumHeldOut) {
144
+ validationEvents = Math.max(
145
+ 0,
146
+ validationEvents - (testEvents + validationEvents - maximumHeldOut)
147
+ );
197
148
  }
198
- const totalProfit = grossProfit - grossLoss;
199
- const avgWin = divideOrNull(grossProfit, wins);
200
- const avgLoss = divideOrNull(grossLoss, losses);
201
- const riskRatios = calculateTradePnlRiskRatios({
202
- profits: approvedProfits,
203
- totalProfit,
204
- maxDrawdown,
205
- periodDays
206
- });
149
+ if (testEvents + validationEvents > maximumHeldOut) {
150
+ testEvents = Math.max(0, maximumHeldOut - validationEvents);
151
+ }
152
+ const testStart = timestamps.length - testEvents;
153
+ const validationStart = testStart - validationEvents;
154
+ const validationTimestamps = new Set(
155
+ timestamps.slice(validationStart, testStart)
156
+ );
157
+ const testTimestamps = new Set(timestamps.slice(testStart));
158
+ const isIn = (timestampsSet, timestamp) => typeof timestamp === "number" && timestampsSet.has(timestamp);
207
159
  return {
208
- trades: approvedEvaluations.length,
209
- totalProfit,
210
- grossProfit,
211
- grossLoss,
212
- profitFactor: grossLoss > 0 ? grossProfit / grossLoss : null,
213
- payoffRatio: avgWin != null && avgLoss != null && avgLoss > 0 ? avgWin / avgLoss : null,
214
- avgWin,
215
- avgLoss,
216
- largestWin,
217
- largestLoss,
218
- winRate: divideOrNull(wins, approvedEvaluations.length),
219
- maxDrawdown,
220
- maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
221
- maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
222
- ...riskRatios,
223
- recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
224
- ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
225
- maxConsecutiveWins,
226
- maxConsecutiveLosses
160
+ trainRows: rows.filter(
161
+ (row) => !isIn(validationTimestamps, row.timestamp) && !isIn(testTimestamps, row.timestamp)
162
+ ),
163
+ validationRows: rows.filter(
164
+ (row) => isIn(validationTimestamps, row.timestamp)
165
+ ),
166
+ testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
227
167
  };
228
168
  };
229
- var summarizeAiTrainEvaluations = (evaluations) => {
230
- let approved = 0;
231
- let rejected = 0;
232
- let correct = 0;
233
- let incorrect = 0;
234
- let profitable = 0;
235
- let unprofitable = 0;
236
- let flat = 0;
237
- let truePositive = 0;
238
- let falsePositive = 0;
239
- let trueNegative = 0;
240
- let falseNegative = 0;
241
- let totalProfitAll = 0;
242
- let totalProfitApproved = 0;
243
- const bucketMap = /* @__PURE__ */ new Map();
244
- for (const evaluation of evaluations) {
245
- const { profit, profitableTrade, aiApproved, quality } = evaluation;
246
- totalProfitAll += profit;
247
- if (profitableTrade) {
248
- profitable += 1;
249
- } else if (profit < 0) {
250
- unprofitable += 1;
251
- } else {
252
- flat += 1;
253
- }
254
- if (aiApproved) {
255
- approved += 1;
256
- totalProfitApproved += profit;
257
- } else {
258
- rejected += 1;
259
- }
260
- const isCorrect = aiApproved === profitableTrade;
261
- if (isCorrect) {
262
- correct += 1;
263
- } else {
264
- incorrect += 1;
265
- }
266
- if (aiApproved && profitableTrade) {
267
- truePositive += 1;
268
- } else if (aiApproved && !profitableTrade) {
269
- falsePositive += 1;
270
- } else if (!aiApproved && profitableTrade) {
271
- falseNegative += 1;
272
- } else {
273
- trueNegative += 1;
169
+ var splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
170
+ rows.filter((row) => row.featureCoverage?.[family] === true),
171
+ validationSplit,
172
+ testSplit
173
+ );
174
+ var sealAiPocketTestPartition = (split, sealed) => {
175
+ const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
176
+ return {
177
+ discoveryRows: [
178
+ ...split.trainRows,
179
+ ...split.validationRows,
180
+ ...sealed ? [] : split.testRows
181
+ ],
182
+ searchTestRows: sealed ? [] : split.testRows,
183
+ evidence: {
184
+ sealed,
185
+ rows: split.testRows.length,
186
+ events: countIndependentEvents(split.testRows),
187
+ startTimestamp: finiteTimestamps.length ? Math.min(...finiteTimestamps) : null,
188
+ endTimestamp: finiteTimestamps.length ? Math.max(...finiteTimestamps) : null
274
189
  }
275
- const bucket = bucketMap.get(quality) ?? {
276
- quality,
277
- count: 0,
278
- approved: 0,
279
- profitable: 0,
280
- totalProfit: 0
281
- };
282
- bucket.count += 1;
283
- bucket.totalProfit += profit;
284
- if (aiApproved) {
285
- bucket.approved += 1;
190
+ };
191
+ };
192
+ var readAiPocketSearchCliOption = ({
193
+ argv,
194
+ longName,
195
+ shortName
196
+ }) => {
197
+ const longPrefix = `--${longName}=`;
198
+ for (let index = 0; index < argv.length; index += 1) {
199
+ const arg = argv[index];
200
+ if (arg.startsWith(longPrefix)) {
201
+ return arg.slice(longPrefix.length);
286
202
  }
287
- if (profitableTrade) {
288
- bucket.profitable += 1;
203
+ if (arg === `--${longName}` || shortName != null && arg === `-${shortName}`) {
204
+ return argv[index + 1];
289
205
  }
290
- bucketMap.set(quality, bucket);
291
206
  }
292
- const avgProfitAll = divideOrNull(totalProfitAll, evaluations.length);
293
- const avgProfitApproved = divideOrNull(totalProfitApproved, approved);
294
- const periodDays = getEvaluationPeriodDays(evaluations);
295
- const avgProfitApprovedPerDay = periodDays == null ? null : divideOrNull(totalProfitApproved, periodDays);
296
- const avgApprovedTradesPerDay = periodDays == null ? null : divideOrNull(approved, periodDays);
297
- const avgProfitApprovedPerMonth = avgProfitApprovedPerDay == null ? null : avgProfitApprovedPerDay * DAYS_PER_MONTH;
298
- const avgApprovedTradesPerWeek = avgApprovedTradesPerDay == null ? null : avgApprovedTradesPerDay * DAYS_PER_WEEK;
299
- const expectancyDelta = avgProfitAll == null || avgProfitApproved == null ? null : avgProfitApproved - avgProfitAll;
300
- return {
301
- approved,
302
- rejected,
303
- correct,
304
- incorrect,
305
- truePositive,
306
- falsePositive,
307
- trueNegative,
308
- falseNegative,
309
- profitable,
310
- unprofitable,
311
- flat,
312
- precisionApproved: divideOrNull(truePositive, approved),
313
- recallWinners: divideOrNull(truePositive, profitable),
314
- avgProfitAll,
315
- avgProfitApproved,
316
- avgProfitApprovedPerDay,
317
- avgProfitApprovedPerMonth,
318
- avgApprovedTradesPerDay,
319
- avgApprovedTradesPerWeek,
320
- expectancyDelta,
321
- approvedRisk: summarizeApprovedRisk(evaluations, periodDays),
322
- qualityBuckets: [...bucketMap.values()].sort(
323
- (a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
324
- )
325
- };
326
- };
327
- var isDirectionMatchAtThreshold = (evaluation, threshold) => {
328
- const directionMatches = typeof evaluation.modelDirectionMatches === "boolean" ? evaluation.modelDirectionMatches : evaluation.aiApproved;
329
- return directionMatches === true && evaluation.quality != null && evaluation.quality >= threshold;
207
+ return void 0;
330
208
  };
331
- var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [3, 4, 5]) => thresholds.map((threshold) => ({
332
- threshold,
333
- label: `q${threshold}+`,
334
- summary: summarizeAiTrainEvaluations(
335
- evaluations.map((evaluation) => ({
336
- ...evaluation,
337
- aiApproved: isDirectionMatchAtThreshold(evaluation, threshold)
338
- }))
339
- )
340
- }));
341
209
 
342
210
  // src/lib/aiTrainOptions.ts
343
211
  var parseTimestampFilter = (value) => {
@@ -411,85 +279,318 @@ var resolveAiTrainRecentLimit = ({
411
279
  return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : defaultRecent;
412
280
  };
413
281
 
414
- // src/lib/aiPocketSearch.ts
415
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
416
- var DAYS_PER_WEEK2 = 7;
417
- var DAYS_PER_MONTH2 = 30.4375;
418
- var PRODUCTION_CANDIDATE_MIN_EVENTS = 25;
419
- var OUTCOME_SEGMENTS = /* @__PURE__ */ new Set([
420
- "actual",
421
- "aiapproved",
422
- "approvalallowednow",
423
- "backtestexecution",
424
- "closedat",
425
- "closedpnl",
426
- "deterministicquality",
427
- "entrydelaybars",
428
- "entrydelaymovebps",
429
- "executionprice",
430
- "exitprice",
431
- "exitreason",
432
- "exittimestamp",
433
- "fillprice",
434
- "future",
435
- "futuremove",
436
- "futureprofit",
437
- "hardblockreasons",
438
- "label",
439
- "maxallowedquality",
440
- "maxquality",
441
- "modeldirection",
442
- "modeldirectionmatches",
443
- "outcome",
444
- "pnl",
445
- "profit",
446
- "profitabletrade",
447
- "q4continuationrecoveryallowed",
448
- "q4continuationrecoverycandidate",
449
- "q4trendshiftgatefeaturesrecoverycandidate",
450
- "q4usclosingoiconfirmationrecoverycandidate",
451
- "quality",
452
- "rawaiapproved",
453
- "recoveryallowed",
454
- "recoverycandidate",
455
- "realized",
456
- "realizedpnl",
457
- "rejectreason",
458
- "result",
459
- "structuralhardblockreasons",
460
- "target",
461
- "traderesult"
462
- ]);
463
- var isPlainRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
464
- var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
465
- var isFeaturePrimitive = (value) => value == null || typeof value === "string" || typeof value === "boolean" || isFiniteNumber(value);
466
- var normalizeFeaturePrimitive = (value) => {
467
- if (typeof value === "string") {
468
- const trimmed = value.trim();
469
- return trimmed ? trimmed.slice(0, 120) : void 0;
282
+ // src/lib/aiPocketSearch/commandOptions.ts
283
+ var normalizeInt = (value, fallback) => {
284
+ const parsed = Number(value);
285
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallback;
286
+ };
287
+ var normalizePositiveInt = (value, fallback) => {
288
+ const parsed = Number(value);
289
+ return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback;
290
+ };
291
+ var normalizeNonNegativeNumber = (value, fallback) => {
292
+ const parsed = Number(value);
293
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
294
+ };
295
+ var normalizeRatio = (value, fallback) => {
296
+ if (typeof value === "boolean") {
297
+ return fallback;
470
298
  }
471
- if (value == null) {
472
- return null;
299
+ const parsed = Number(value);
300
+ if (!Number.isFinite(parsed)) {
301
+ return fallback;
473
302
  }
474
- return value;
303
+ return Math.max(0, Math.min(0.9, parsed));
475
304
  };
476
- var isOutcomePath = (segments) => segments.some((segment) => OUTCOME_SEGMENTS.has(segment.toLowerCase()));
477
- var DATA_QUALITY_LEAVES = /* @__PURE__ */ new Set([
478
- "age",
479
- "agems",
480
- "available",
481
- "asofts",
482
- "calcbars",
483
- "coverage",
484
- "latestindex",
485
- "points",
486
- "rowcount",
487
- "rows",
488
- "stale",
489
- "timestamp",
490
- "windowendts"
491
- ]);
492
- var METADATA_LEAVES = /* @__PURE__ */ new Set([
305
+ var normalizeUnitRatio = (value, fallback) => {
306
+ const parsed = Number(value);
307
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : fallback;
308
+ };
309
+ var normalizeFeatureProfile = (value) => {
310
+ const normalized = String(value || "all").trim().toLowerCase();
311
+ return normalized === "compact" ? "compact" : "all";
312
+ };
313
+ var normalizeFeaturePolicy = (value) => {
314
+ const normalized = String(value || "causal-stationary").trim().toLowerCase();
315
+ if (!["causal-stationary", "all"].includes(normalized)) {
316
+ throw new Error(
317
+ `Unsupported --featurePolicy "${normalized}". Use causal-stationary or all.`
318
+ );
319
+ }
320
+ return normalized;
321
+ };
322
+ var normalizeCoverageMode = (value) => {
323
+ const normalized = String(value || "auto").trim().toLowerCase();
324
+ if (!["auto", "full"].includes(normalized)) {
325
+ throw new Error(
326
+ `Unsupported --coverageMode "${normalized}". Use auto or full.`
327
+ );
328
+ }
329
+ return normalized;
330
+ };
331
+ var normalizeCadenceMode = (value) => {
332
+ const normalized = String(value || "auto").trim().toLowerCase();
333
+ if (!["auto", "fixed"].includes(normalized)) {
334
+ throw new Error(
335
+ `Unsupported --cadenceMode "${normalized}". Use auto or fixed.`
336
+ );
337
+ }
338
+ return normalized;
339
+ };
340
+ var normalizeObjective = (value, scope) => {
341
+ const normalized = String(value || "auto").trim().toLowerCase();
342
+ if (normalized === "auto") {
343
+ if (scope === "approved") {
344
+ return "filter-gate";
345
+ }
346
+ if (scope === "rejected" || scope === "candidates") {
347
+ return "add-to-gate";
348
+ }
349
+ return "standalone";
350
+ }
351
+ if (!["standalone", "add-to-gate", "filter-gate"].includes(normalized)) {
352
+ throw new Error(
353
+ `Unsupported --objective "${normalized}". Use auto, standalone, add-to-gate, or filter-gate.`
354
+ );
355
+ }
356
+ return normalized;
357
+ };
358
+ var hasCliOption2 = (argv, longName, shortName) => argv.some(
359
+ (arg) => arg === `--${longName}` || arg.startsWith(`--${longName}=`) || (shortName ? arg === `-${shortName}` : false)
360
+ );
361
+ var resolveAiPocketSearchCommandOptions = ({
362
+ flags: flags2,
363
+ argv
364
+ }) => {
365
+ const outDir = String(flags2.outDir || "data/ai/export");
366
+ const strategyName = String(flags2.strategy || "").trim() || void 0;
367
+ const explicitFile = String(flags2.file || "").trim();
368
+ const skip = normalizeInt(flags2.skip, 0);
369
+ const minQuality = normalizeInt(flags2.minQuality, 4);
370
+ const sinceInput = parseTimestampFilter(flags2.since);
371
+ const untilTimestamp = parseTimestampFilter(flags2.until);
372
+ const trailingPeriodMs = parseTrailingPeriodMs(flags2.period);
373
+ const periodLabel = String(flags2.period || "").trim() || null;
374
+ const hasDateFilter = trailingPeriodMs != null || sinceInput != null || untilTimestamp != null;
375
+ const recent = resolveAiTrainRecentLimit({
376
+ argv,
377
+ recentValue: flags2.recent,
378
+ hasDateFilter
379
+ });
380
+ const qualityThresholds = parseQualityThresholds(flags2.qualityThresholds);
381
+ const scope = String(flags2.scope || "all").trim().toLowerCase();
382
+ if (!["all", "approved", "rejected", "candidates"].includes(scope)) {
383
+ throw new Error(
384
+ `Unsupported --scope "${scope}". Use all, approved, rejected, or candidates.`
385
+ );
386
+ }
387
+ const direction = String(flags2.direction || "").trim().toUpperCase();
388
+ if (direction && !["LONG", "SHORT"].includes(direction)) {
389
+ throw new Error(
390
+ `Unsupported --direction "${direction}". Use LONG or SHORT.`
391
+ );
392
+ }
393
+ const explicitMaxEventCountShare = hasCliOption2(
394
+ argv,
395
+ "maxEventCountShare",
396
+ "U"
397
+ );
398
+ const maxDepth = normalizePositiveInt(flags2.maxDepth, 2);
399
+ const explicitMinSupport = hasCliOption2(argv, "minSupport", "m") ? normalizePositiveInt(flags2.minSupport, 20) : void 0;
400
+ const minProfitFactor = normalizeNonNegativeNumber(
401
+ readAiPocketSearchCliOption({
402
+ argv,
403
+ longName: "minProfitFactor",
404
+ shortName: "F"
405
+ }) ?? flags2.minProfitFactor,
406
+ 1.2
407
+ );
408
+ const minWinRate = normalizeNonNegativeNumber(
409
+ readAiPocketSearchCliOption({
410
+ argv,
411
+ longName: "minWinRate",
412
+ shortName: "W"
413
+ }) ?? flags2.minWinRate,
414
+ 0
415
+ );
416
+ const minTotalProfitValue = readAiPocketSearchCliOption({
417
+ argv,
418
+ longName: "minTotalProfit",
419
+ shortName: "R"
420
+ }) ?? flags2.minTotalProfit;
421
+ const minTotalProfit = Number.isFinite(Number(minTotalProfitValue)) ? Number(minTotalProfitValue) : 0;
422
+ const validationSplit = hasCliOption2(argv, "validationSplit", "V") ? normalizeRatio(
423
+ readAiPocketSearchCliOption({
424
+ argv,
425
+ longName: "validationSplit",
426
+ shortName: "V"
427
+ }) ?? flags2.validationSplit,
428
+ 0.25
429
+ ) : 0.25;
430
+ const testSplit = hasCliOption2(argv, "testSplit", "T") ? normalizeRatio(
431
+ readAiPocketSearchCliOption({
432
+ argv,
433
+ longName: "testSplit",
434
+ shortName: "T"
435
+ }) ?? flags2.testSplit,
436
+ 0
437
+ ) : 0;
438
+ const sealTest = Boolean(flags2.sealTest);
439
+ if (sealTest && testSplit <= 0) {
440
+ throw new Error("--sealTest requires a positive --testSplit");
441
+ }
442
+ const maxEventCountShare = normalizeUnitRatio(
443
+ readAiPocketSearchCliOption({
444
+ argv,
445
+ longName: "maxEventCountShare",
446
+ shortName: "U"
447
+ }) ?? flags2.maxEventCountShare,
448
+ 0.25
449
+ );
450
+ const maxSymbolCountShare = normalizeUnitRatio(
451
+ readAiPocketSearchCliOption({
452
+ argv,
453
+ longName: "maxSymbolCountShare",
454
+ shortName: "Z"
455
+ }) ?? flags2.maxSymbolCountShare,
456
+ 0.5
457
+ );
458
+ const objective = normalizeObjective(flags2.objective, scope);
459
+ return {
460
+ outDir,
461
+ strategyName,
462
+ explicitFile,
463
+ skip,
464
+ minQuality,
465
+ sinceInput,
466
+ untilTimestamp,
467
+ trailingPeriodMs,
468
+ periodLabel,
469
+ recent,
470
+ qualityThresholds,
471
+ scope,
472
+ direction,
473
+ maxDepth,
474
+ ...explicitMinSupport != null ? { explicitMinSupport } : {},
475
+ minProfitFactor,
476
+ minWinRate,
477
+ minTotalProfit,
478
+ maxAtomicPredicates: normalizePositiveInt(flags2.maxAtomicPredicates, 180),
479
+ maxCombinations: normalizePositiveInt(flags2.maxCombinations, 6e4),
480
+ validationSplit,
481
+ testSplit,
482
+ sealTest,
483
+ explicitMinValidationSupport: normalizeInt(flags2.minValidationSupport, 0),
484
+ explicitMinEvents: normalizeInt(flags2.minEvents, 0),
485
+ explicitMinValidationEvents: normalizeInt(flags2.minValidationEvents, 0),
486
+ maxBatch: normalizeInt(flags2.maxBatch, 5),
487
+ maxEventCountShare,
488
+ explicitMaxEventCountShare,
489
+ maxSymbolCountShare,
490
+ objective,
491
+ allowRiskRegression: Boolean(flags2.allowRiskRegression),
492
+ requireValidationEligibility: !Boolean(flags2.allowValidationRegression),
493
+ dedupeEquivalentSelections: Boolean(flags2.dedupeEquivalentSelections),
494
+ top: normalizePositiveInt(flags2.top, 30),
495
+ includeSymbol: Boolean(flags2.includeSymbol),
496
+ includeGateContext: Boolean(flags2.includeGateContext),
497
+ featureProfile: normalizeFeatureProfile(flags2.featureProfile),
498
+ featurePolicy: normalizeFeaturePolicy(flags2.featurePolicy),
499
+ coverageMode: normalizeCoverageMode(flags2.coverageMode),
500
+ cadenceMode: normalizeCadenceMode(flags2.cadenceMode),
501
+ jsonOutput: Boolean(flags2.json),
502
+ outputPath: String(flags2.output || "").trim(),
503
+ reportDir: String(flags2.reportDir || "data/ai/output").trim(),
504
+ explicitReportFile: String(flags2.reportFile || "").trim()
505
+ };
506
+ };
507
+
508
+ // src/lib/aiPocketSearch/presenter.ts
509
+ var import_chalk = __toESM(require("chalk"));
510
+ var import_promises = __toESM(require("fs/promises"));
511
+ var import_path = __toESM(require("path"));
512
+ var import_progress = __toESM(require("progress"));
513
+ var import_ai = require("@tradejs/infra/ai");
514
+
515
+ // src/lib/aiPocketSearch/features.ts
516
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
517
+ var DAYS_PER_WEEK = 7;
518
+ var DAYS_PER_MONTH = 30.4375;
519
+ var PRODUCTION_CANDIDATE_MIN_EVENTS = 25;
520
+ var OUTCOME_SEGMENTS = /* @__PURE__ */ new Set([
521
+ "actual",
522
+ "aiapproved",
523
+ "approvalallowednow",
524
+ "backtestexecution",
525
+ "closedat",
526
+ "closedpnl",
527
+ "deterministicquality",
528
+ "entrydelaybars",
529
+ "entrydelaymovebps",
530
+ "executionprice",
531
+ "exitprice",
532
+ "exitreason",
533
+ "exittimestamp",
534
+ "fillprice",
535
+ "future",
536
+ "futuremove",
537
+ "futureprofit",
538
+ "hardblockreasons",
539
+ "label",
540
+ "maxallowedquality",
541
+ "maxquality",
542
+ "modeldirection",
543
+ "modeldirectionmatches",
544
+ "outcome",
545
+ "pnl",
546
+ "profit",
547
+ "profitabletrade",
548
+ "q4continuationrecoveryallowed",
549
+ "q4continuationrecoverycandidate",
550
+ "q4trendshiftgatefeaturesrecoverycandidate",
551
+ "q4usclosingoiconfirmationrecoverycandidate",
552
+ "quality",
553
+ "rawaiapproved",
554
+ "recoveryallowed",
555
+ "recoverycandidate",
556
+ "realized",
557
+ "realizedpnl",
558
+ "rejectreason",
559
+ "result",
560
+ "structuralhardblockreasons",
561
+ "target",
562
+ "traderesult"
563
+ ]);
564
+ var isPlainRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
565
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
566
+ var isFeaturePrimitive = (value) => value == null || typeof value === "string" || typeof value === "boolean" || isFiniteNumber(value);
567
+ var normalizeFeaturePrimitive = (value) => {
568
+ if (typeof value === "string") {
569
+ const trimmed = value.trim();
570
+ return trimmed ? trimmed.slice(0, 120) : void 0;
571
+ }
572
+ if (value == null) {
573
+ return null;
574
+ }
575
+ return value;
576
+ };
577
+ var isOutcomePath = (segments) => segments.some((segment) => OUTCOME_SEGMENTS.has(segment.toLowerCase()));
578
+ var DATA_QUALITY_LEAVES = /* @__PURE__ */ new Set([
579
+ "age",
580
+ "agems",
581
+ "available",
582
+ "asofts",
583
+ "calcbars",
584
+ "coverage",
585
+ "latestindex",
586
+ "points",
587
+ "rowcount",
588
+ "rows",
589
+ "stale",
590
+ "timestamp",
591
+ "windowendts"
592
+ ]);
593
+ var METADATA_LEAVES = /* @__PURE__ */ new Set([
493
594
  "connector",
494
595
  "exchange",
495
596
  "fingerprint",
@@ -501,9 +602,9 @@ var METADATA_LEAVES = /* @__PURE__ */ new Set([
501
602
  "targetsymbol",
502
603
  "universe"
503
604
  ]);
504
- var normalizeFeaturePath = (path2) => (Array.isArray(path2) ? path2 : path2.split(".")).filter(Boolean).map((segment) => segment.toLowerCase());
505
- var classifyAiPocketCoverageFeaturePath = (path2) => {
506
- const segments = normalizeFeaturePath(path2);
605
+ var normalizeFeaturePath = (path3) => (Array.isArray(path3) ? path3 : path3.split(".")).filter(Boolean).map((segment) => segment.toLowerCase());
606
+ var classifyAiPocketCoverageFeaturePath = (path3) => {
607
+ const segments = normalizeFeaturePath(path3);
507
608
  const normalizedPath = segments.join(".");
508
609
  if (segments.some(
509
610
  (segment) => segment.includes("cmc") || segment.includes("coinmarketcap")
@@ -677,8 +778,8 @@ var RAW_NONSTATIONARY_LEAVES = /* @__PURE__ */ new Set([
677
778
  "volumeusd",
678
779
  "vwap"
679
780
  ]);
680
- var classifyAiPocketFeaturePath = (path2) => {
681
- const segments = (Array.isArray(path2) ? path2 : path2.split(".")).filter(
781
+ var classifyAiPocketFeaturePath = (path3) => {
782
+ const segments = (Array.isArray(path3) ? path3 : path3.split(".")).filter(
682
783
  Boolean
683
784
  );
684
785
  const normalized = segments.map((segment) => segment.toLowerCase());
@@ -1053,6 +1154,8 @@ var collectAiPocketFeatureSnapshot = ({
1053
1154
  addDirectionalDerivedFeatures(features);
1054
1155
  return { features, featureCoverage };
1055
1156
  };
1157
+
1158
+ // src/lib/aiPocketSearch/summary.ts
1056
1159
  var formatNumber = (value) => {
1057
1160
  if (Number.isInteger(value)) {
1058
1161
  return String(value);
@@ -1077,7 +1180,7 @@ var quantileAt = (values, quantile) => {
1077
1180
  const index = Math.floor((values.length - 1) * quantile);
1078
1181
  return values[Math.max(0, Math.min(values.length - 1, index))];
1079
1182
  };
1080
- var getPeriodDays = (rows) => {
1183
+ var getPeriodDays2 = (rows) => {
1081
1184
  let minTimestamp = null;
1082
1185
  let maxTimestamp = null;
1083
1186
  for (const row of rows) {
@@ -1182,7 +1285,7 @@ var finalizeAiPocketSummary = ({
1182
1285
  rows,
1183
1286
  accumulator
1184
1287
  }) => {
1185
- const fullPeriodDays = getPeriodDays(rows);
1288
+ const fullPeriodDays = getPeriodDays2(rows);
1186
1289
  const support = accumulator.support;
1187
1290
  const supportRatio = rows.length > 0 ? support / rows.length : 0;
1188
1291
  if (support === 0) {
@@ -1277,9 +1380,9 @@ var finalizeAiPocketSummary = ({
1277
1380
  maxConsecutiveWins,
1278
1381
  maxConsecutiveLosses,
1279
1382
  avgTradesPerDay,
1280
- avgTradesPerWeek: avgTradesPerDay == null ? null : avgTradesPerDay * DAYS_PER_WEEK2,
1383
+ avgTradesPerWeek: avgTradesPerDay == null ? null : avgTradesPerDay * DAYS_PER_WEEK,
1281
1384
  avgProfitPerDay,
1282
- avgProfitPerMonth: avgProfitPerDay == null ? null : avgProfitPerDay * DAYS_PER_MONTH2,
1385
+ avgProfitPerMonth: avgProfitPerDay == null ? null : avgProfitPerDay * DAYS_PER_MONTH,
1283
1386
  losingMonths: losingMonthEntries.length,
1284
1387
  worstMonth: worstMonth ? {
1285
1388
  month: worstMonth[0],
@@ -1349,6 +1452,8 @@ var summarizeAiPocketFeatureCoverage = (rows, family) => {
1349
1452
  maxTimestamp
1350
1453
  };
1351
1454
  };
1455
+
1456
+ // src/lib/aiPocketSearch/predicates.ts
1352
1457
  var matchesPredicate = (value, predicate) => {
1353
1458
  if (predicate.kind === "numeric") {
1354
1459
  if (!isFiniteNumber(value)) {
@@ -1596,6 +1701,8 @@ var buildAiPocketPredicateResult = (rows, options = {}) => {
1596
1701
  emitProgress("predicates", bucketList.length, bucketList.length, true);
1597
1702
  return { featureKeys: buckets.size, predicates };
1598
1703
  };
1704
+
1705
+ // src/lib/aiPocketSearch/searchEngine.ts
1599
1706
  var scorePositivePocket = (summary) => {
1600
1707
  const profitFactor = summary.profitFactor ?? (summary.grossLoss === 0 ? 8 : 0);
1601
1708
  const winRate = summary.winRate ?? 0;
@@ -1908,7 +2015,7 @@ var searchAiPockets = (rows, options = {}) => {
1908
2015
  const onProgress = options.onProgress;
1909
2016
  const summarizePartition = (partitionRows) => {
1910
2017
  const summary = summarizeAiPocketRows(partitionRows);
1911
- const periodDays = getPeriodDays(partitionRows);
2018
+ const periodDays = getPeriodDays2(partitionRows);
1912
2019
  return {
1913
2020
  rows: partitionRows.length,
1914
2021
  events: summary.events,
@@ -2174,6 +2281,8 @@ var searchAiPockets = (rows, options = {}) => {
2174
2281
  }
2175
2282
  };
2176
2283
  };
2284
+
2285
+ // src/lib/aiPocketSearch/markdownReport.ts
2177
2286
  var formatMdNumber = (value, digits = 2) => value == null || !Number.isFinite(value) ? "n/a" : value.toFixed(digits);
2178
2287
  var formatMdPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${(value * 100).toFixed(1)}%`;
2179
2288
  var escapeMarkdownCell = (value) => String(value ?? "n/a").replace(/\|/g, "\\|").replace(/\n/g, "<br>");
@@ -2602,669 +2711,221 @@ var buildAiPocketMarkdownReport = ({
2602
2711
  `;
2603
2712
  };
2604
2713
 
2605
- // src/lib/aiPocketSearchCli.ts
2606
- var DAY_MS3 = 24 * 60 * 60 * 1e3;
2607
- var DEFAULT_MIN_SUPPORT = 20;
2608
- var LOW_CADENCE_EVENT_LIMIT = 200;
2609
- var LOW_CADENCE_MAX_EVENT_SHARE = 1 / 3;
2610
- var countIndependentEvents = (rows) => {
2611
- const timestampEvents = /* @__PURE__ */ new Set();
2612
- let rowsWithoutTimestamp = 0;
2613
- for (const row of rows) {
2614
- if (typeof row.timestamp === "number" && Number.isFinite(row.timestamp)) {
2615
- timestampEvents.add(row.timestamp);
2616
- } else {
2617
- rowsWithoutTimestamp += 1;
2618
- }
2619
- }
2620
- return timestampEvents.size + rowsWithoutTimestamp;
2621
- };
2622
- var getPeriodDays2 = (rows) => {
2623
- let minTimestamp = null;
2624
- let maxTimestamp = null;
2625
- for (const row of rows) {
2626
- const timestamp = row.timestamp;
2627
- if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
2628
- continue;
2629
- }
2630
- minTimestamp = minTimestamp == null ? timestamp : Math.min(minTimestamp, timestamp);
2631
- maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
2632
- }
2633
- if (minTimestamp == null || maxTimestamp == null) {
2634
- return null;
2714
+ // src/lib/aiPocketSearch/presenter.ts
2715
+ var ListIt = require("list-it");
2716
+ var tickProgressBarTo = (bar, target, tokens) => {
2717
+ const current = Number(bar.curr ?? 0);
2718
+ const next = Math.max(current, Math.min(target, bar.total));
2719
+ const delta = next - current;
2720
+ if (delta > 0) {
2721
+ bar.tick(delta, tokens);
2635
2722
  }
2636
- return Math.max((maxTimestamp - minTimestamp) / DAY_MS3, 1);
2637
2723
  };
2638
- var clampInt = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, Math.ceil(value)));
2639
- var resolveAiPocketCadenceProfile = ({
2640
- trainRows,
2641
- validationRows = [],
2642
- testRows = [],
2643
- mode = "auto",
2644
- validationSplit = 0.25,
2645
- minSupport,
2646
- minEvents,
2647
- minValidationSupport,
2648
- minValidationEvents,
2649
- maxEventCountShare = 0.25,
2650
- explicitMaxEventCountShare = false
2651
- }) => {
2652
- const trainEvents = countIndependentEvents(trainRows);
2653
- const validationEvents = countIndependentEvents(validationRows);
2654
- const testEvents = countIndependentEvents(testRows);
2655
- const trainPeriodDays = getPeriodDays2(trainRows);
2656
- const trainEventsPerDay = trainPeriodDays == null ? null : trainEvents / trainPeriodDays;
2657
- const sparseSample = trainEvents < LOW_CADENCE_EVENT_LIMIT;
2658
- const lowCadence = trainEventsPerDay != null && trainEventsPerDay < 0.25;
2659
- const adaptiveSample = mode === "auto" && sparseSample;
2660
- const adaptiveThresholds = adaptiveSample && (minSupport == null || minEvents == null || validationRows.length > 0 && (minValidationSupport == null || minValidationEvents == null) || !explicitMaxEventCountShare);
2661
- const resolvedMinSupport = minSupport ?? (adaptiveSample ? clampInt(trainEvents * 0.1, 3, DEFAULT_MIN_SUPPORT) : DEFAULT_MIN_SUPPORT);
2662
- const resolvedMinEvents = minEvents ?? (minSupport != null || !adaptiveSample ? Math.max(5, Math.ceil(resolvedMinSupport * 0.5)) : clampInt(resolvedMinSupport * 0.5, 3, 10));
2663
- const hasValidation = validationRows.length > 0;
2664
- const resolvedMinValidationSupport = minValidationSupport ?? (hasValidation ? Math.max(3, Math.ceil(resolvedMinSupport * validationSplit * 0.5)) : 0);
2665
- const resolvedMinValidationEvents = minValidationEvents ?? (hasValidation ? Math.max(
2666
- adaptiveSample ? 3 : 2,
2667
- Math.ceil(resolvedMinEvents * validationSplit * 0.5)
2668
- ) : 0);
2669
- const resolvedMaxEventCountShare = adaptiveSample && !explicitMaxEventCountShare ? Math.max(maxEventCountShare, LOW_CADENCE_MAX_EVENT_SHARE) : maxEventCountShare;
2670
- return {
2671
- mode,
2672
- lowCadence,
2673
- sparseSample,
2674
- adaptiveThresholds,
2675
- trainRows: trainRows.length,
2676
- trainEvents,
2677
- trainPeriodDays,
2678
- trainEventsPerDay,
2679
- validationRows: validationRows.length,
2680
- validationEvents,
2681
- testRows: testRows.length,
2682
- testEvents,
2683
- minSupport: resolvedMinSupport,
2684
- minEvents: resolvedMinEvents,
2685
- minValidationSupport: resolvedMinValidationSupport,
2686
- minValidationEvents: resolvedMinValidationEvents,
2687
- maxEventCountShare: resolvedMaxEventCountShare
2688
- };
2689
- };
2690
- var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
2691
- minProfitFactor: "1.2",
2692
- minWinRate: "0",
2693
- minTotalProfit: "0",
2694
- validationSplit: "0.25",
2695
- testSplit: "0"
2724
+ var searchPhaseLabels = {
2725
+ features: "features",
2726
+ predicates: "preds",
2727
+ masks: "masks",
2728
+ combinations: "search"
2696
2729
  };
2697
- var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
2698
- const timestamps = [
2699
- ...new Set(
2700
- rows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp))
2730
+ var presentEmptyAiPocketSearchDataset = ({
2731
+ filePaths,
2732
+ recent,
2733
+ skip
2734
+ }) => {
2735
+ console.log(
2736
+ import_chalk.default.yellow(
2737
+ `No AI prompt rows selected in ${filePaths.join(", ")} (recent=${recent || "all"}, skip=${skip})`
2701
2738
  )
2702
- ].sort((left, right) => left - right);
2703
- if (timestamps.length < 2) {
2704
- return { trainRows: rows, validationRows: [], testRows: [] };
2705
- }
2706
- const getSplitCount = (ratio) => ratio > 0 ? Math.max(1, Math.floor(timestamps.length * ratio)) : 0;
2707
- let testEvents = getSplitCount(testSplit);
2708
- let validationEvents = getSplitCount(validationSplit);
2709
- const maximumHeldOut = timestamps.length - 1;
2710
- if (testEvents + validationEvents > maximumHeldOut) {
2711
- validationEvents = Math.max(
2712
- 0,
2713
- validationEvents - (testEvents + validationEvents - maximumHeldOut)
2714
- );
2715
- }
2716
- if (testEvents + validationEvents > maximumHeldOut) {
2717
- testEvents = Math.max(0, maximumHeldOut - validationEvents);
2718
- }
2719
- const testStart = timestamps.length - testEvents;
2720
- const validationStart = testStart - validationEvents;
2721
- const validationTimestamps = new Set(
2722
- timestamps.slice(validationStart, testStart)
2723
2739
  );
2724
- const testTimestamps = new Set(timestamps.slice(testStart));
2725
- const isIn = (timestampsSet, timestamp) => typeof timestamp === "number" && timestampsSet.has(timestamp);
2726
- return {
2727
- trainRows: rows.filter(
2728
- (row) => !isIn(validationTimestamps, row.timestamp) && !isIn(testTimestamps, row.timestamp)
2729
- ),
2730
- validationRows: rows.filter(
2731
- (row) => isIn(validationTimestamps, row.timestamp)
2732
- ),
2733
- testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
2734
- };
2735
2740
  };
2736
- var splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
2737
- rows.filter((row) => row.featureCoverage?.[family] === true),
2738
- validationSplit,
2739
- testSplit
2740
- );
2741
- var sealAiPocketTestPartition = (split, sealed) => {
2742
- const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
2743
- return {
2744
- discoveryRows: [
2745
- ...split.trainRows,
2746
- ...split.validationRows,
2747
- ...sealed ? [] : split.testRows
2748
- ],
2749
- searchTestRows: sealed ? [] : split.testRows,
2750
- evidence: {
2751
- sealed,
2752
- rows: split.testRows.length,
2753
- events: countIndependentEvents(split.testRows),
2754
- startTimestamp: finiteTimestamps.length ? Math.min(...finiteTimestamps) : null,
2755
- endTimestamp: finiteTimestamps.length ? Math.max(...finiteTimestamps) : null
2741
+ var presentPendingAiPocketSearchReport = (reportPath) => {
2742
+ console.error(
2743
+ import_chalk.default.gray(`report: ${reportPath} (pending; final report after search)`)
2744
+ );
2745
+ };
2746
+ var createAiPocketSearchDatasetProgressPresenter = (selectedRows) => {
2747
+ const bar = new import_progress.default(
2748
+ "eval :current/:total [:bar] :percent :symbol :status",
2749
+ {
2750
+ total: selectedRows,
2751
+ width: 20,
2752
+ stream: process.stderr
2756
2753
  }
2754
+ );
2755
+ return ({ symbol, status }) => {
2756
+ bar.tick(1, {
2757
+ symbol: import_chalk.default.gray(symbol),
2758
+ status: status === "ok" ? import_chalk.default.green(status) : status === "error" ? import_chalk.default.yellow(status) : import_chalk.default.gray(status)
2759
+ });
2757
2760
  };
2758
2761
  };
2759
- var readAiPocketSearchCliOption = ({
2760
- argv,
2761
- longName,
2762
- shortName
2763
- }) => {
2764
- const longPrefix = `--${longName}=`;
2765
- for (let index = 0; index < argv.length; index += 1) {
2766
- const arg = argv[index];
2767
- if (arg.startsWith(longPrefix)) {
2768
- return arg.slice(longPrefix.length);
2762
+ var createAiPocketSearchResearchProgressPresenter = () => {
2763
+ let searchBar = null;
2764
+ let searchBarPhase = null;
2765
+ return ({ label, progress }) => {
2766
+ if (searchBar && searchBarPhase !== progress.phase) {
2767
+ tickProgressBarTo(searchBar, searchBar.total, {
2768
+ status: import_chalk.default.gray("done")
2769
+ });
2770
+ searchBar = null;
2769
2771
  }
2770
- if (arg === `--${longName}` || shortName != null && arg === `-${shortName}`) {
2771
- return argv[index + 1];
2772
+ if (!searchBar) {
2773
+ searchBarPhase = progress.phase;
2774
+ const phaseLabel = searchPhaseLabels[progress.phase].padEnd(8, " ");
2775
+ console.error(
2776
+ import_chalk.default.gray(
2777
+ `stage: ${label}/${searchPhaseLabels[progress.phase]} (${progress.total})`
2778
+ )
2779
+ );
2780
+ searchBar = new import_progress.default(
2781
+ `${phaseLabel} :current/:total [:bar] :percent :status`,
2782
+ {
2783
+ total: Math.max(progress.total, 1),
2784
+ width: 20,
2785
+ stream: process.stderr
2786
+ }
2787
+ );
2788
+ }
2789
+ tickProgressBarTo(searchBar, progress.current, {
2790
+ status: progress.truncated ? import_chalk.default.yellow("truncated") : "running"
2791
+ });
2792
+ if (progress.done) {
2793
+ tickProgressBarTo(searchBar, searchBar.total, {
2794
+ status: progress.truncated ? import_chalk.default.yellow("truncated") : "done"
2795
+ });
2796
+ searchBar = null;
2772
2797
  }
2798
+ };
2799
+ };
2800
+ var createListIt = () => new ListIt({
2801
+ autoAlign: true,
2802
+ headerUnderline: true
2803
+ });
2804
+ var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
2805
+ var formatRatio = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
2806
+ var formatProfit = (value) => value == null ? "n/a" : value.toFixed(2);
2807
+ var formatNumber2 = (value) => value == null ? "n/a" : value.toFixed(2);
2808
+ var colorizeRatio = (value) => {
2809
+ const text = formatRatio(value);
2810
+ if (value == null) {
2811
+ return import_chalk.default.gray(text);
2773
2812
  }
2774
- return void 0;
2813
+ if (value >= 0.55) {
2814
+ return import_chalk.default.green(text);
2815
+ }
2816
+ if (value >= 0.45) {
2817
+ return import_chalk.default.yellow(text);
2818
+ }
2819
+ return import_chalk.default.red(text);
2775
2820
  };
2776
-
2777
- // src/scripts/aiPocketSearch.ts
2778
- var ListIt = require("list-it");
2779
- import_args.default.example(
2780
- "yarn ai-pocket-search --strategy LiquidityZones -n 0 --maxDepth 2 --minSupport 25",
2781
- "Search deterministic AI-gate pockets over the latest LiquidityZones AI export"
2782
- );
2783
- import_args.default.option(["o", "outDir"], "Dataset directory", "data/ai/export");
2784
- import_args.default.option(["s", "strategy"], "Strategy name filter for merged file", "");
2785
- import_args.default.option(["f", "file"], "Explicit merged dataset file path", "");
2786
- import_args.default.option(
2787
- ["n", "recent"],
2788
- "How many recent trades to evaluate from the end (0 = all)",
2789
- 50
2790
- );
2791
- import_args.default.option(
2792
- ["k", "skip"],
2793
- "How many recent trades to skip from the end before selecting replay rows",
2794
- 0
2795
- );
2796
- import_args.default.option(
2797
- ["M", "minQuality"],
2798
- "Minimum deterministic gate quality used for current qN+ baseline",
2799
- 4
2800
- );
2801
- import_args.default.option(
2802
- ["S", "since"],
2803
- "Only evaluate rows at or after this timestamp (ISO date or epoch ms)",
2821
+ var colorizeProfit = (value) => {
2822
+ const text = formatProfit(value);
2823
+ if (value == null) {
2824
+ return import_chalk.default.gray(text);
2825
+ }
2826
+ if (value > 0) {
2827
+ return import_chalk.default.green(text);
2828
+ }
2829
+ if (value < 0) {
2830
+ return import_chalk.default.red(text);
2831
+ }
2832
+ return import_chalk.default.yellow(text);
2833
+ };
2834
+ var colorizeNumber = (value) => value == null ? import_chalk.default.gray("n/a") : import_chalk.default.cyan(formatNumber2(value));
2835
+ var toReportTimestamp = (timestamp) => new Date(timestamp).toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:]/g, "-");
2836
+ var markdownPlainTable = (headers, rows) => {
2837
+ const escapeCell = (value) => value.replace(/\|/g, "\\|");
2838
+ return [
2839
+ `| ${headers.map(escapeCell).join(" | ")} |`,
2840
+ `| ${headers.map(() => "---").join(" | ")} |`,
2841
+ ...rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`)
2842
+ ].join("\n");
2843
+ };
2844
+ var buildPendingMarkdownReport = ({
2845
+ generatedAt,
2846
+ strategy,
2847
+ filePaths,
2848
+ selectedRows,
2849
+ reportPath
2850
+ }) => [
2851
+ "# AI Pocket Search Report",
2852
+ "",
2853
+ "Status: running",
2854
+ "",
2855
+ `Generated at: ${new Date(generatedAt).toISOString()}`,
2856
+ "",
2857
+ markdownPlainTable(
2858
+ ["Field", "Value"],
2859
+ [
2860
+ ["strategy", strategy],
2861
+ ["selected_rows", String(selectedRows)],
2862
+ ["report", reportPath]
2863
+ ]
2864
+ ),
2865
+ "",
2866
+ "## Source Files",
2867
+ "",
2868
+ ...filePaths.map((filePath) => `- \`${filePath}\``),
2869
+ "",
2870
+ "Final metrics and pockets will be written when the search finishes.",
2804
2871
  ""
2805
- );
2806
- import_args.default.option(
2807
- ["u", "until"],
2808
- "Only evaluate rows at or before this timestamp (ISO date or epoch ms)",
2809
- ""
2810
- );
2811
- import_args.default.option(
2812
- ["P", "period"],
2813
- "Evaluate a trailing selected-row period such as last365d, last90d, or last30d",
2814
- ""
2815
- );
2816
- import_args.default.option(
2817
- ["q", "qualityThresholds"],
2818
- "Comma-separated qN+ thresholds for current-gate baseline",
2819
- "3,4,5"
2820
- );
2821
- import_args.default.option(
2822
- ["g", "scope"],
2823
- "Search scope: all, approved, rejected, or candidates",
2824
- "all"
2825
- );
2826
- import_args.default.option(
2827
- ["I", "direction"],
2828
- "Only search rows with this signal direction: LONG or SHORT",
2829
- ""
2830
- );
2831
- import_args.default.option(["d", "maxDepth"], "Maximum predicate-combination depth", 2);
2832
- import_args.default.option(
2833
- ["m", "minSupport"],
2834
- "Minimum rows override; cadence auto derives it when omitted",
2835
- 20
2836
- );
2837
- import_args.default.option(
2838
- ["F", "minProfitFactor"],
2839
- "Minimum profit factor required for positive pockets",
2840
- AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minProfitFactor
2841
- );
2842
- import_args.default.option(
2843
- ["W", "minWinRate"],
2844
- "Minimum win rate required for positive pockets",
2845
- AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minWinRate
2846
- );
2847
- import_args.default.option(
2848
- ["R", "minTotalProfit"],
2849
- "Minimum total PnL required for positive pockets",
2850
- AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minTotalProfit
2851
- );
2852
- import_args.default.option(
2853
- ["a", "maxAtomicPredicates"],
2854
- "Maximum strongest atomic predicates used for combinations",
2855
- 180
2856
- );
2857
- import_args.default.option(
2858
- ["C", "maxCombinations"],
2859
- "Maximum predicate combinations to evaluate",
2860
- 6e4
2861
- );
2862
- import_args.default.option(
2863
- ["V", "validationSplit"],
2864
- "Trailing time-ordered scope share reserved for validation (0 disables)",
2865
- AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
2866
- );
2867
- import_args.default.option(
2868
- ["T", "testSplit"],
2869
- "Trailing timestamp-grouped scope share withheld as untouched test",
2870
- AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
2871
- );
2872
- import_args.default.option(
2873
- ["w", "sealTest"],
2874
- "Reserve test bounds without exposing its rows or economics to discovery",
2875
- false
2876
- );
2877
- import_args.default.option(
2878
- ["N", "minValidationSupport"],
2879
- "Minimum validation rows required for positive pockets (0 = auto)",
2880
- 0
2881
- );
2882
- import_args.default.option(
2883
- ["H", "minEvents"],
2884
- "Minimum independent timestamp events required (0 = auto)",
2885
- 0
2886
- );
2887
- import_args.default.option(
2888
- ["J", "minValidationEvents"],
2889
- "Minimum validation timestamp events required (0 = auto)",
2890
- 0
2891
- );
2892
- import_args.default.option(
2893
- ["X", "maxBatch"],
2894
- "Maximum selected rows at one timestamp (0 disables)",
2895
- 5
2896
- );
2897
- import_args.default.option(
2898
- ["U", "maxEventCountShare"],
2899
- "Maximum row share contributed by one timestamp event",
2900
- "0.25"
2901
- );
2902
- import_args.default.option(
2903
- ["Z", "maxSymbolCountShare"],
2904
- "Maximum row share contributed by one symbol",
2905
- "0.5"
2906
- );
2907
- import_args.default.option(
2908
- ["A", "objective"],
2909
- "Search objective: auto, standalone, add-to-gate, or filter-gate",
2910
- "auto"
2911
- );
2912
- import_args.default.option(
2913
- ["G", "allowRiskRegression"],
2914
- "Allow incremental pockets to worsen q4+ PF, PnL, drawdown, or loss streak",
2915
- false
2916
- );
2917
- import_args.default.option(
2918
- ["L", "allowValidationRegression"],
2919
- "Keep train-positive pockets that fail validation PnL/PF thresholds",
2920
- false
2921
- );
2922
- import_args.default.option(
2923
- ["D", "dedupeEquivalentSelections"],
2924
- "Collapse pockets that select the same train rows",
2925
- true
2926
- );
2927
- import_args.default.option(["t", "top"], "Top pockets to print in each section", 30);
2928
- import_args.default.option(["Y", "includeSymbol"], "Allow symbol as a pocket feature", false);
2929
- import_args.default.option(
2930
- ["E", "includeGateContext"],
2931
- "Allow current deterministic gate output fields as pocket features",
2932
- false
2933
- );
2934
- import_args.default.option(
2935
- ["p", "featureProfile"],
2936
- "Feature extraction profile: compact or all",
2937
- "all"
2938
- );
2939
- import_args.default.option(
2940
- ["K", "featurePolicy"],
2941
- "Feature policy: causal-stationary or all",
2942
- "causal-stationary"
2943
- );
2944
- import_args.default.option(
2945
- ["Q", "coverageMode"],
2946
- "Coverage-aware search mode: auto or full",
2947
- "auto"
2948
- );
2949
- import_args.default.option(
2950
- ["c", "cadenceMode"],
2951
- "Cadence thresholds: auto adapts discovery support, fixed keeps legacy defaults",
2952
- "auto"
2953
- );
2954
- import_args.default.option(
2955
- ["r", "reportDir"],
2956
- "Directory for generated Markdown reports",
2957
- "data/ai/output"
2958
- );
2959
- import_args.default.option(["B", "reportFile"], "Explicit Markdown report file path", "");
2960
- import_args.default.option(["j", "json"], "Print structured JSON summary", false);
2961
- import_args.default.option(["O", "output"], "Write structured JSON summary to file", "");
2962
- var flags = import_args.default.parse(process.argv);
2963
- var createListIt = () => new ListIt({
2964
- autoAlign: true,
2965
- headerUnderline: true
2966
- });
2967
- var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
2968
- var formatRatio = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
2969
- var formatProfit = (value) => value == null ? "n/a" : value.toFixed(2);
2970
- var formatNumber2 = (value) => value == null ? "n/a" : value.toFixed(2);
2971
- var colorizeRatio = (value) => {
2972
- const text = formatRatio(value);
2973
- if (value == null) {
2974
- return import_chalk.default.gray(text);
2975
- }
2976
- if (value >= 0.55) {
2977
- return import_chalk.default.green(text);
2978
- }
2979
- if (value >= 0.45) {
2980
- return import_chalk.default.yellow(text);
2872
+ ].join("\n");
2873
+ var getMergedGroupId = (filePath) => {
2874
+ const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-(\d+)(?:-part(\d+))?\.jsonl$/);
2875
+ if (!match) {
2876
+ return null;
2981
2877
  }
2982
- return import_chalk.default.red(text);
2878
+ return { strategyToken: match[1], mergeId: match[2] };
2983
2879
  };
2984
- var colorizeProfit = (value) => {
2985
- const text = formatProfit(value);
2986
- if (value == null) {
2987
- return import_chalk.default.gray(text);
2988
- }
2989
- if (value > 0) {
2990
- return import_chalk.default.green(text);
2991
- }
2992
- if (value < 0) {
2993
- return import_chalk.default.red(text);
2880
+ var resolveMarkdownReportPath = ({
2881
+ explicitReportFile,
2882
+ reportDir,
2883
+ strategyName,
2884
+ filePath,
2885
+ scope,
2886
+ generatedAt
2887
+ }) => {
2888
+ if (explicitReportFile) {
2889
+ return explicitReportFile;
2994
2890
  }
2995
- return import_chalk.default.yellow(text);
2996
- };
2997
- var colorizeNumber = (value) => value == null ? import_chalk.default.gray("n/a") : import_chalk.default.cyan(formatNumber2(value));
2998
- var toReportTimestamp = (timestamp) => new Date(timestamp).toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:]/g, "-");
2999
- var markdownPlainTable = (headers, rows) => {
3000
- const escapeCell = (value) => value.replace(/\|/g, "\\|");
3001
- return [
3002
- `| ${headers.map(escapeCell).join(" | ")} |`,
3003
- `| ${headers.map(() => "---").join(" | ")} |`,
3004
- ...rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`)
3005
- ].join("\n");
2891
+ const groupId = getMergedGroupId(filePath);
2892
+ const strategyToken = groupId?.strategyToken ?? (0, import_ai.toFileToken)(strategyName);
2893
+ const mergeToken = groupId?.mergeId ? `merged-${groupId.mergeId}` : "merged";
2894
+ return import_path.default.join(
2895
+ reportDir,
2896
+ `ai-pocket-search-${strategyToken}-${mergeToken}-${scope}-${toReportTimestamp(generatedAt)}.md`
2897
+ );
3006
2898
  };
3007
- var buildPendingMarkdownReport = ({
3008
- generatedAt,
3009
- strategy,
2899
+ var writePendingAiPocketSearchReport = async ({
2900
+ explicitReportFile,
2901
+ reportDir,
2902
+ strategyName,
3010
2903
  filePaths,
3011
- selectedRows,
3012
- reportPath
3013
- }) => [
3014
- "# AI Pocket Search Report",
3015
- "",
3016
- "Status: running",
3017
- "",
3018
- `Generated at: ${new Date(generatedAt).toISOString()}`,
3019
- "",
3020
- markdownPlainTable(
3021
- ["Field", "Value"],
3022
- [
3023
- ["strategy", strategy],
3024
- ["selected_rows", String(selectedRows)],
3025
- ["report", reportPath]
3026
- ]
3027
- ),
3028
- "",
3029
- "## Source Files",
3030
- "",
3031
- ...filePaths.map((filePath) => `- \`${filePath}\``),
3032
- "",
3033
- "Final metrics and pockets will be written when the search finishes.",
3034
- ""
3035
- ].join("\n");
3036
- var normalizeInt = (value, fallback) => {
3037
- const parsed = Number(value);
3038
- return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallback;
3039
- };
3040
- var normalizePositiveInt = (value, fallback) => {
3041
- const parsed = Number(value);
3042
- return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback;
3043
- };
3044
- var normalizeNonNegativeNumber = (value, fallback) => {
3045
- const parsed = Number(value);
3046
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
3047
- };
3048
- var normalizeRatio = (value, fallback) => {
3049
- if (typeof value === "boolean") {
3050
- return fallback;
3051
- }
3052
- const parsed = Number(value);
3053
- if (!Number.isFinite(parsed)) {
3054
- return fallback;
3055
- }
3056
- return Math.max(0, Math.min(0.9, parsed));
3057
- };
3058
- var normalizeUnitRatio = (value, fallback) => {
3059
- const parsed = Number(value);
3060
- return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : fallback;
3061
- };
3062
- var normalizeFeatureProfile = (value) => {
3063
- const normalized = String(value || "all").trim().toLowerCase();
3064
- return normalized === "compact" ? "compact" : "all";
3065
- };
3066
- var normalizeFeaturePolicy = (value) => {
3067
- const normalized = String(value || "causal-stationary").trim().toLowerCase();
3068
- if (!["causal-stationary", "all"].includes(normalized)) {
3069
- throw new Error(
3070
- `Unsupported --featurePolicy "${normalized}". Use causal-stationary or all.`
3071
- );
3072
- }
3073
- return normalized;
3074
- };
3075
- var normalizeCoverageMode = (value) => {
3076
- const normalized = String(value || "auto").trim().toLowerCase();
3077
- if (!["auto", "full"].includes(normalized)) {
3078
- throw new Error(
3079
- `Unsupported --coverageMode "${normalized}". Use auto or full.`
3080
- );
3081
- }
3082
- return normalized;
3083
- };
3084
- var normalizeCadenceMode = (value) => {
3085
- const normalized = String(value || "auto").trim().toLowerCase();
3086
- if (!["auto", "fixed"].includes(normalized)) {
3087
- throw new Error(
3088
- `Unsupported --cadenceMode "${normalized}". Use auto or fixed.`
3089
- );
3090
- }
3091
- return normalized;
3092
- };
3093
- var normalizeObjective = (value, scope) => {
3094
- const normalized = String(value || "auto").trim().toLowerCase();
3095
- if (normalized === "auto") {
3096
- if (scope === "approved") {
3097
- return "filter-gate";
3098
- }
3099
- if (scope === "rejected" || scope === "candidates") {
3100
- return "add-to-gate";
3101
- }
3102
- return "standalone";
3103
- }
3104
- if (!["standalone", "add-to-gate", "filter-gate"].includes(normalized)) {
3105
- throw new Error(
3106
- `Unsupported --objective "${normalized}". Use auto, standalone, add-to-gate, or filter-gate.`
3107
- );
3108
- }
3109
- return normalized;
3110
- };
3111
- var normalizeQuality = (analysis) => {
3112
- const quality = Number(analysis?.quality);
3113
- return Number.isFinite(quality) ? Math.round(quality) : null;
3114
- };
3115
- var isAiApproval = (row, analysis, minQuality) => {
3116
- const quality = normalizeQuality(analysis);
3117
- return analysis.direction === row.direction && quality != null && quality >= minQuality;
3118
- };
3119
- var listMergedFiles = async (params) => {
3120
- const { outDir, strategyName } = params;
3121
- let entries = [];
3122
- try {
3123
- entries = await import_promises.default.readdir(outDir);
3124
- } catch {
3125
- return [];
3126
- }
3127
- const prefix = strategyName ? `ai-dataset-${(0, import_ai.toFileToken)(strategyName)}-merged-` : "ai-dataset-";
3128
- return entries.filter((name) => name.startsWith(prefix)).filter((name) => {
3129
- if (!name.endsWith(".jsonl") || !name.includes("-merged-")) {
3130
- return false;
3131
- }
3132
- return /-merged-\d+\.jsonl$/.test(name) || /-merged-\d+-part\d+\.jsonl$/.test(name);
3133
- }).sort().map((name) => import_path.default.join(outDir, name));
3134
- };
3135
- var deriveStrategyNameFromFile = (filePath) => {
3136
- const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-\d+(?:-part\d+)?\.jsonl$/);
3137
- return match?.[1] ? match[1] : "unknown";
3138
- };
3139
- var getMergedGroupId = (filePath) => {
3140
- const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-(\d+)(?:-part(\d+))?\.jsonl$/);
3141
- if (!match) {
3142
- return null;
3143
- }
3144
- return {
3145
- strategyToken: match[1],
3146
- mergeId: match[2]
3147
- };
3148
- };
3149
- var resolveMarkdownReportPath = ({
3150
- explicitReportFile,
3151
- reportDir,
3152
- strategyName,
3153
- filePath,
3154
2904
  scope,
3155
- generatedAt
2905
+ generatedAt,
2906
+ selectedRows
3156
2907
  }) => {
3157
- if (explicitReportFile) {
3158
- return explicitReportFile;
3159
- }
3160
- const groupId = getMergedGroupId(filePath);
3161
- const strategyToken = groupId?.strategyToken ?? (0, import_ai.toFileToken)(strategyName);
3162
- const mergeToken = groupId?.mergeId ? `merged-${groupId.mergeId}` : "merged";
3163
- return import_path.default.join(
2908
+ const reportPath = resolveMarkdownReportPath({
2909
+ explicitReportFile,
3164
2910
  reportDir,
3165
- `ai-pocket-search-${strategyToken}-${mergeToken}-${scope}-${toReportTimestamp(generatedAt)}.md`
3166
- );
3167
- };
3168
- var sortDatasetPartPaths = (filePaths) => [...filePaths].sort((left, right) => {
3169
- const leftMatch = import_path.default.basename(left).match(/-part(\d+)\.jsonl$/);
3170
- const rightMatch = import_path.default.basename(right).match(/-part(\d+)\.jsonl$/);
3171
- const leftPart = leftMatch ? Number(leftMatch[1]) : 0;
3172
- const rightPart = rightMatch ? Number(rightMatch[1]) : 0;
3173
- return leftPart - rightPart || left.localeCompare(right);
3174
- });
3175
- var resolveMergedDatasetFiles = async ({
3176
- outDir,
3177
- strategyName,
3178
- explicitFile
3179
- }) => {
3180
- const mergedFiles = await listMergedFiles({
3181
- outDir,
3182
- strategyName
2911
+ strategyName,
2912
+ filePath: filePaths[0] || "",
2913
+ scope,
2914
+ generatedAt
3183
2915
  });
3184
- if (!mergedFiles.length) {
3185
- throw new Error(
3186
- strategyName ? `No merged AI dataset found for strategy "${strategyName}" in ${outDir}. Run yarn ai-export first.` : `No merged AI dataset found in ${outDir}. Run yarn ai-export first.`
3187
- );
3188
- }
3189
- const resolvedExplicitFile = explicitFile ? import_path.default.resolve(explicitFile) : null;
3190
- if (resolvedExplicitFile) {
3191
- const groupId = getMergedGroupId(resolvedExplicitFile);
3192
- if (!groupId) {
3193
- return [resolvedExplicitFile];
3194
- }
3195
- const groupedFiles = sortDatasetPartPaths(
3196
- mergedFiles.filter((candidate) => {
3197
- const candidateGroup = getMergedGroupId(candidate);
3198
- return candidateGroup?.strategyToken === groupId.strategyToken && candidateGroup?.mergeId === groupId.mergeId;
3199
- })
3200
- );
3201
- return groupedFiles.length ? groupedFiles : [resolvedExplicitFile];
3202
- }
3203
- const latestFile = mergedFiles[mergedFiles.length - 1];
3204
- const latestGroupId = getMergedGroupId(latestFile);
3205
- if (!latestGroupId) {
3206
- return [latestFile];
3207
- }
3208
- return sortDatasetPartPaths(
3209
- mergedFiles.filter((candidate) => {
3210
- const candidateGroup = getMergedGroupId(candidate);
3211
- return candidateGroup?.strategyToken === latestGroupId.strategyToken && candidateGroup?.mergeId === latestGroupId.mergeId;
3212
- })
2916
+ await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(reportPath)), { recursive: true });
2917
+ await import_promises.default.writeFile(
2918
+ reportPath,
2919
+ buildPendingMarkdownReport({
2920
+ generatedAt,
2921
+ strategy: strategyName,
2922
+ filePaths,
2923
+ selectedRows,
2924
+ reportPath
2925
+ }),
2926
+ "utf8"
3213
2927
  );
3214
- };
3215
- var getDatasetRowTimestamp = (row) => {
3216
- const timestamp = Number(row.timestamp);
3217
- return Number.isFinite(timestamp) ? timestamp : null;
3218
- };
3219
- var findMaxSelectedTimestamp = async ({
3220
- filePaths,
3221
- recent,
3222
- skip
3223
- }) => {
3224
- let maxTimestamp = null;
3225
- await (0, import_ai.streamAiDatasetRows)({
3226
- filePaths,
3227
- limitFromEnd: recent,
3228
- skipFromEnd: skip,
3229
- onRow: async (row) => {
3230
- const timestamp = getDatasetRowTimestamp(row);
3231
- if (timestamp != null) {
3232
- maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
3233
- }
3234
- }
3235
- });
3236
- return maxTimestamp;
3237
- };
3238
- var getDeterministicModelCandidate = (signal) => {
3239
- if (!signal) {
3240
- return false;
3241
- }
3242
- const payload = (0, import_ai2.buildAiPayload)(signal);
3243
- const gateContext = (0, import_ai2.getDeterministicAiGateContext)(payload);
3244
- const structuralHardBlockReasons = Array.isArray(
3245
- gateContext?.structuralHardBlockReasons
3246
- ) ? gateContext.structuralHardBlockReasons.filter(
3247
- (reason) => typeof reason === "string" && reason.trim().length > 0
3248
- ) : [];
3249
- if (structuralHardBlockReasons.length) {
3250
- return false;
3251
- }
3252
- if (typeof gateContext?.approvalAllowedNow === "boolean") {
3253
- return gateContext.approvalAllowedNow;
3254
- }
3255
- return true;
3256
- };
3257
- var resolveScopeRows = (rows, scope) => {
3258
- if (scope === "approved") {
3259
- return rows.filter((row) => row.aiApproved);
3260
- }
3261
- if (scope === "rejected") {
3262
- return rows.filter((row) => !row.aiApproved);
3263
- }
3264
- if (scope === "candidates") {
3265
- return rows.filter((row) => row.modelCandidate && !row.aiApproved);
3266
- }
3267
- return rows;
2928
+ return reportPath;
3268
2929
  };
3269
2930
  var printSection = (title, table) => {
3270
2931
  console.log(import_chalk.default.gray(`${title}:`));
@@ -3343,1003 +3004,1639 @@ var buildQualityRows = (qualityThresholds) => qualityThresholds.map(({ label, su
3343
3004
  colorizeNumber(summary.avgApprovedTradesPerDay),
3344
3005
  colorizeProfit(summary.avgProfitApprovedPerDay)
3345
3006
  ]);
3346
- var tickProgressBarTo = (bar, target, tokens) => {
3347
- const current = Number(bar.curr ?? 0);
3348
- const next = Math.max(current, Math.min(target, bar.total));
3349
- const delta = next - current;
3350
- if (delta > 0) {
3351
- bar.tick(delta, tokens);
3352
- }
3353
- };
3354
- var searchPhaseLabels = {
3355
- features: "features",
3356
- predicates: "preds",
3357
- masks: "masks",
3358
- combinations: "search"
3359
- };
3360
- var hasCliOption2 = (longName, shortName) => process.argv.some(
3361
- (arg) => arg === `--${longName}` || arg.startsWith(`--${longName}=`) || (shortName ? arg === `-${shortName}` : false)
3362
- );
3363
- var main = async () => {
3364
- const outDir = String(flags.outDir || "data/ai/export");
3365
- const strategyName = String(flags.strategy || "").trim() || void 0;
3366
- const explicitFile = String(flags.file || "").trim();
3367
- const skip = normalizeInt(flags.skip, 0);
3368
- const minQuality = normalizeInt(flags.minQuality, 4);
3369
- const sinceInput = parseTimestampFilter(flags.since);
3370
- const untilTimestamp = parseTimestampFilter(flags.until);
3371
- const trailingPeriodMs = parseTrailingPeriodMs(flags.period);
3372
- const periodLabel = String(flags.period || "").trim() || null;
3373
- const hasDateFilter = trailingPeriodMs != null || sinceInput != null || untilTimestamp != null;
3374
- const recent = resolveAiTrainRecentLimit({
3375
- argv: process.argv,
3376
- recentValue: flags.recent,
3377
- hasDateFilter
3007
+ var writeAiPocketSearchArtifacts = async ({
3008
+ result,
3009
+ outputPath
3010
+ }) => {
3011
+ const reportPath = result.run.reportPath;
3012
+ const markdownReport = buildAiPocketMarkdownReport(result);
3013
+ await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(reportPath)), {
3014
+ recursive: true
3378
3015
  });
3379
- const qualityThresholds = parseQualityThresholds(flags.qualityThresholds);
3380
- const scope = String(flags.scope || "all").trim().toLowerCase();
3381
- if (!["all", "approved", "rejected", "candidates"].includes(scope)) {
3382
- throw new Error(
3383
- `Unsupported --scope "${scope}". Use all, approved, rejected, or candidates.`
3384
- );
3016
+ await import_promises.default.writeFile(reportPath, markdownReport, "utf8");
3017
+ if (outputPath) {
3018
+ await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(outputPath)), {
3019
+ recursive: true
3020
+ });
3021
+ await import_promises.default.writeFile(outputPath, `${JSON.stringify(result, null, 2)}
3022
+ `);
3385
3023
  }
3386
- const direction = String(flags.direction || "").trim().toUpperCase();
3387
- if (direction && !["LONG", "SHORT"].includes(direction)) {
3388
- throw new Error(
3389
- `Unsupported --direction "${direction}". Use LONG or SHORT.`
3390
- );
3391
- }
3392
- const maxDepth = normalizePositiveInt(flags.maxDepth, 2);
3393
- const explicitMinSupport = hasCliOption2("minSupport", "m") ? normalizePositiveInt(flags.minSupport, 20) : void 0;
3394
- const minProfitFactor = normalizeNonNegativeNumber(
3395
- readAiPocketSearchCliOption({
3396
- argv: process.argv,
3397
- longName: "minProfitFactor",
3398
- shortName: "F"
3399
- }) ?? flags.minProfitFactor,
3400
- 1.2
3024
+ };
3025
+ var presentAiPocketSearchResult = ({
3026
+ result,
3027
+ outputPath
3028
+ }) => {
3029
+ const { run, pocketSearch: search } = result;
3030
+ const coverageSearches = result.coverageSearches ?? [];
3031
+ const currentGateQualityThresholds = result.currentGate.qualityThresholds;
3032
+ const errors = result.errors;
3033
+ const {
3034
+ strategy: resolvedStrategyName,
3035
+ filePaths,
3036
+ selectedRows,
3037
+ evaluatedRows,
3038
+ scope,
3039
+ direction,
3040
+ scopeRows: scopeRowCount,
3041
+ trainRows: trainRowCount,
3042
+ validationRows: validationRowCount,
3043
+ testRows: testRowCount,
3044
+ sourceRows: totalRows,
3045
+ dateSkipped,
3046
+ failed,
3047
+ recent,
3048
+ skip,
3049
+ since: sinceTimestamp,
3050
+ until: untilTimestamp,
3051
+ period: periodLabel,
3052
+ minQuality,
3053
+ includeSymbol,
3054
+ includeGateContext,
3055
+ validationSplit,
3056
+ testSplit,
3057
+ reportPath
3058
+ } = run;
3059
+ const objective = run.objective ?? search.objective;
3060
+ const cadenceMode = run.cadenceMode ?? "auto";
3061
+ const featureProfile = run.featureProfile ?? "all";
3062
+ const featurePolicy = run.featurePolicy ?? "causal-stationary";
3063
+ const coverageMode = run.coverageMode ?? "auto";
3064
+ const sealedTest = run.sealedTest ?? {
3065
+ sealed: false,
3066
+ rows: 0,
3067
+ events: 0,
3068
+ startTimestamp: null,
3069
+ endTimestamp: null
3070
+ };
3071
+ const {
3072
+ maxDepth,
3073
+ maxAtomicPredicates,
3074
+ maxCombinations,
3075
+ maxSymbolCountShare = 0.5,
3076
+ allowRiskRegression = false,
3077
+ requireValidationEligibility = true,
3078
+ validationSplit: _searchValidationSplit,
3079
+ testSplit: _searchTestSplit,
3080
+ dedupeEquivalentSelections
3081
+ } = run.search;
3082
+ const maxBatch = run.search.maxBatch ?? 0;
3083
+ console.log("");
3084
+ console.log(import_chalk.default.green("AI pocket search finished"));
3085
+ filePaths.forEach((filePath) => console.log(import_chalk.default.gray(filePath)));
3086
+ console.log("");
3087
+ printSection(
3088
+ "RUN",
3089
+ createTable(
3090
+ [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
3091
+ [
3092
+ ["strategy", import_chalk.default.yellow(resolvedStrategyName)],
3093
+ ["selected", import_chalk.default.blue(String(selectedRows))],
3094
+ ["evaluated", import_chalk.default.blue(String(evaluatedRows))],
3095
+ ["scope", import_chalk.default.yellow(scope)],
3096
+ ["objective", import_chalk.default.yellow(objective)],
3097
+ ["direction", direction ? import_chalk.default.yellow(direction) : import_chalk.default.gray("all")],
3098
+ ["scope_rows", import_chalk.default.blue(String(scopeRowCount))],
3099
+ ["train_rows", import_chalk.default.blue(String(trainRowCount))],
3100
+ ["validation_rows", import_chalk.default.blue(String(validationRowCount))],
3101
+ ["test_rows", import_chalk.default.blue(String(testRowCount))],
3102
+ ["source_rows", import_chalk.default.blue(String(totalRows))],
3103
+ ["date_skipped", import_chalk.default.blue(String(dateSkipped))],
3104
+ [
3105
+ "failed",
3106
+ failed > 0 ? import_chalk.default.yellow(String(failed)) : import_chalk.default.green("0")
3107
+ ],
3108
+ ["recent", import_chalk.default.blue(recent === 0 ? "all" : String(recent))],
3109
+ ["skip", import_chalk.default.blue(String(skip))],
3110
+ [
3111
+ "since",
3112
+ sinceTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(sinceTimestamp).toISOString())
3113
+ ],
3114
+ [
3115
+ "until",
3116
+ untilTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(untilTimestamp).toISOString())
3117
+ ],
3118
+ [
3119
+ "period",
3120
+ periodLabel == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(periodLabel)
3121
+ ],
3122
+ ["min_quality", import_chalk.default.magenta(String(minQuality))],
3123
+ ["max_depth", import_chalk.default.magenta(String(maxDepth))],
3124
+ ["cadence_mode", import_chalk.default.magenta(cadenceMode)],
3125
+ [
3126
+ "low_cadence",
3127
+ search.stats.cadence.lowCadence ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3128
+ ],
3129
+ [
3130
+ "sparse_sample",
3131
+ search.stats.cadence.sparseSample ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3132
+ ],
3133
+ [
3134
+ "adaptive_thresholds",
3135
+ search.stats.cadence.adaptiveThresholds ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
3136
+ ],
3137
+ [
3138
+ "train_events",
3139
+ import_chalk.default.magenta(String(search.stats.cadence.trainEvents))
3140
+ ],
3141
+ [
3142
+ "train_events_per_day",
3143
+ import_chalk.default.magenta(formatNumber2(search.stats.cadence.trainEventsPerDay))
3144
+ ],
3145
+ ["min_support", import_chalk.default.magenta(String(search.stats.cadence.minSupport))],
3146
+ ["min_events", import_chalk.default.magenta(String(search.stats.cadence.minEvents))],
3147
+ [
3148
+ "min_validation_events",
3149
+ import_chalk.default.magenta(String(search.stats.cadence.minValidationEvents))
3150
+ ],
3151
+ ["max_batch", import_chalk.default.magenta(maxBatch > 0 ? String(maxBatch) : "off")],
3152
+ [
3153
+ "max_event_share",
3154
+ import_chalk.default.magenta(formatRatio(search.stats.cadence.maxEventCountShare))
3155
+ ],
3156
+ ["max_symbol_share", import_chalk.default.magenta(formatRatio(maxSymbolCountShare))],
3157
+ ["max_atomic_predicates", import_chalk.default.magenta(String(maxAtomicPredicates))],
3158
+ ["max_combinations", import_chalk.default.magenta(String(maxCombinations))],
3159
+ ["validation_split", import_chalk.default.magenta(formatRatio(validationSplit))],
3160
+ ["test_split", import_chalk.default.magenta(formatRatio(testSplit))],
3161
+ [
3162
+ "test_evidence",
3163
+ sealedTest.sealed ? import_chalk.default.green(
3164
+ `sealed (${sealedTest.rows} rows / ${sealedTest.events} events)`
3165
+ ) : import_chalk.default.yellow("open")
3166
+ ],
3167
+ [
3168
+ "min_validation_support",
3169
+ import_chalk.default.magenta(String(search.stats.cadence.minValidationSupport))
3170
+ ],
3171
+ [
3172
+ "dedupe_equivalent",
3173
+ dedupeEquivalentSelections ? import_chalk.default.green("on") : import_chalk.default.gray("off")
3174
+ ],
3175
+ [
3176
+ "include_symbol",
3177
+ includeSymbol ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
3178
+ ],
3179
+ [
3180
+ "include_gate_context",
3181
+ includeGateContext ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
3182
+ ],
3183
+ ["feature_profile", import_chalk.default.magenta(featureProfile)],
3184
+ ["feature_policy", import_chalk.default.magenta(featurePolicy)],
3185
+ ["coverage_mode", import_chalk.default.magenta(coverageMode)],
3186
+ [
3187
+ "allow_risk_regression",
3188
+ allowRiskRegression ? import_chalk.default.yellow("on") : import_chalk.default.green("off")
3189
+ ],
3190
+ [
3191
+ "validation_eligibility",
3192
+ requireValidationEligibility ? import_chalk.default.green("required") : import_chalk.default.yellow("off")
3193
+ ],
3194
+ ["report", import_chalk.default.gray(reportPath)],
3195
+ ["output", outputPath ? import_chalk.default.gray(outputPath) : import_chalk.default.gray("off")]
3196
+ ]
3197
+ )
3401
3198
  );
3402
- const minWinRate = normalizeNonNegativeNumber(
3403
- readAiPocketSearchCliOption({
3404
- argv: process.argv,
3405
- longName: "minWinRate",
3406
- shortName: "W"
3407
- }) ?? flags.minWinRate,
3408
- 0
3199
+ printSection(
3200
+ "CURRENT GATE QN+ BASELINE",
3201
+ createTable(
3202
+ [
3203
+ import_chalk.default.gray("Q"),
3204
+ import_chalk.default.gray("APPROVED"),
3205
+ import_chalk.default.gray("WR"),
3206
+ import_chalk.default.gray("PF"),
3207
+ import_chalk.default.gray("PNL"),
3208
+ import_chalk.default.gray("MAX_DD"),
3209
+ import_chalk.default.gray("TRADES/D"),
3210
+ import_chalk.default.gray("PNL/D")
3211
+ ],
3212
+ buildQualityRows(currentGateQualityThresholds)
3213
+ )
3409
3214
  );
3410
- const minTotalProfitValue = readAiPocketSearchCliOption({
3411
- argv: process.argv,
3412
- longName: "minTotalProfit",
3413
- shortName: "R"
3414
- }) ?? flags.minTotalProfit;
3415
- const minTotalProfit = Number.isFinite(Number(minTotalProfitValue)) ? Number(minTotalProfitValue) : 0;
3416
- const maxAtomicPredicates = normalizePositiveInt(
3417
- flags.maxAtomicPredicates,
3418
- 180
3215
+ printSection(
3216
+ "TRAIN BASELINE",
3217
+ createTable(
3218
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
3219
+ buildSummaryRows(search.baseline)
3220
+ )
3419
3221
  );
3420
- const maxCombinations = normalizePositiveInt(flags.maxCombinations, 6e4);
3421
- const validationSplit = hasCliOption2("validationSplit", "V") ? normalizeRatio(
3422
- readAiPocketSearchCliOption({
3423
- argv: process.argv,
3424
- longName: "validationSplit",
3425
- shortName: "V"
3426
- }) ?? flags.validationSplit,
3427
- 0.25
3428
- ) : 0.25;
3429
- const testSplit = hasCliOption2("testSplit", "T") ? normalizeRatio(
3430
- readAiPocketSearchCliOption({
3431
- argv: process.argv,
3432
- longName: "testSplit",
3433
- shortName: "T"
3434
- }) ?? flags.testSplit,
3435
- 0
3436
- ) : 0;
3437
- const sealTest = Boolean(flags.sealTest);
3438
- if (sealTest && testSplit <= 0) {
3439
- throw new Error("--sealTest requires a positive --testSplit");
3222
+ if (search.validationBaseline) {
3223
+ printSection(
3224
+ "VALIDATION BASELINE",
3225
+ createTable(
3226
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
3227
+ buildSummaryRows(search.validationBaseline)
3228
+ )
3229
+ );
3440
3230
  }
3441
- const explicitMinValidationSupport = normalizeInt(
3442
- flags.minValidationSupport,
3443
- 0
3444
- );
3445
- const explicitMinEvents = normalizeInt(flags.minEvents, 0);
3446
- const explicitMinValidationEvents = normalizeInt(
3447
- flags.minValidationEvents,
3448
- 0
3449
- );
3450
- const maxBatch = normalizeInt(flags.maxBatch, 5);
3451
- const maxEventCountShare = normalizeUnitRatio(
3452
- readAiPocketSearchCliOption({
3453
- argv: process.argv,
3454
- longName: "maxEventCountShare",
3455
- shortName: "U"
3456
- }) ?? flags.maxEventCountShare,
3457
- 0.25
3458
- );
3459
- const maxSymbolCountShare = normalizeUnitRatio(
3460
- readAiPocketSearchCliOption({
3461
- argv: process.argv,
3462
- longName: "maxSymbolCountShare",
3463
- shortName: "Z"
3464
- }) ?? flags.maxSymbolCountShare,
3465
- 0.5
3466
- );
3467
- const objective = normalizeObjective(flags.objective, scope);
3468
- const allowRiskRegression = Boolean(flags.allowRiskRegression);
3469
- const requireValidationEligibility = !Boolean(
3470
- flags.allowValidationRegression
3471
- );
3472
- const dedupeEquivalentSelections = Boolean(flags.dedupeEquivalentSelections);
3473
- const top = normalizePositiveInt(flags.top, 30);
3474
- const includeSymbol = Boolean(flags.includeSymbol);
3475
- const includeGateContext = Boolean(flags.includeGateContext);
3476
- const featureProfile = normalizeFeatureProfile(flags.featureProfile);
3477
- const featurePolicy = normalizeFeaturePolicy(flags.featurePolicy);
3478
- const coverageMode = normalizeCoverageMode(flags.coverageMode);
3479
- const cadenceMode = normalizeCadenceMode(flags.cadenceMode);
3480
- const jsonOutput = Boolean(flags.json);
3481
- const outputPath = String(flags.output || "").trim();
3482
- const reportDir = String(flags.reportDir || "data/ai/output").trim();
3483
- const explicitReportFile = String(flags.reportFile || "").trim();
3484
- const generatedAt = Date.now();
3485
- const excludedFeaturePaths = /* @__PURE__ */ new Map();
3486
- await (0, import_registry.ensureStrategyPluginsLoaded)();
3487
- const filePaths = await resolveMergedDatasetFiles({
3488
- outDir,
3489
- strategyName,
3490
- explicitFile
3491
- });
3492
- const { totalRows, selectedRows } = await (0, import_ai.countAiDatasetRows)({
3493
- filePaths,
3494
- limitFromEnd: recent,
3495
- skipFromEnd: skip
3496
- });
3497
- const maxSelectedTimestamp = trailingPeriodMs == null ? null : await findMaxSelectedTimestamp({ filePaths, recent, skip });
3498
- const sinceTimestamp = trailingPeriodMs != null && maxSelectedTimestamp != null ? maxSelectedTimestamp - trailingPeriodMs : sinceInput;
3499
- if (!selectedRows) {
3500
- console.log(
3501
- import_chalk.default.yellow(
3502
- `No AI prompt rows selected in ${filePaths.join(", ")} (recent=${recent || "all"}, skip=${skip})`
3231
+ if (search.testBaseline) {
3232
+ printSection(
3233
+ "UNTOUCHED TEST BASELINE",
3234
+ createTable(
3235
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
3236
+ buildSummaryRows(search.testBaseline)
3503
3237
  )
3504
3238
  );
3505
- process.exit(0);
3506
3239
  }
3507
- let resolvedStrategyName = deriveStrategyNameFromFile(filePaths[0] || "");
3508
- const reportPath = resolveMarkdownReportPath({
3509
- explicitReportFile,
3510
- reportDir,
3511
- strategyName: resolvedStrategyName,
3512
- filePath: filePaths[0] || "",
3513
- scope,
3514
- generatedAt
3515
- });
3516
- await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(reportPath)), {
3517
- recursive: true
3518
- });
3519
- await import_promises.default.writeFile(
3520
- reportPath,
3521
- buildPendingMarkdownReport({
3522
- generatedAt,
3523
- strategy: resolvedStrategyName,
3524
- filePaths,
3525
- selectedRows,
3526
- reportPath
3527
- }),
3528
- "utf8"
3529
- );
3530
- console.error(
3531
- import_chalk.default.gray(`report: ${reportPath} (pending; final report after search)`)
3532
- );
3533
- let scanned = 0;
3534
- let dateSkipped = 0;
3535
- let failed = 0;
3536
- const errors = [];
3537
- const rows = [];
3538
- const bar = new import_progress.default(
3539
- "eval :current/:total [:bar] :percent :symbol :status",
3540
- {
3541
- total: selectedRows,
3542
- width: 20,
3543
- stream: process.stderr
3544
- }
3545
- );
3546
- await (0, import_ai.streamAiDatasetRows)({
3547
- filePaths,
3548
- limitFromEnd: recent,
3549
- skipFromEnd: skip,
3550
- onRow: async (row) => {
3551
- scanned += 1;
3552
- if (resolvedStrategyName === "unknown" && typeof row.strategyName === "string" && row.strategyName.trim()) {
3553
- resolvedStrategyName = row.strategyName.trim();
3554
- }
3555
- const timestamp = getDatasetRowTimestamp(row);
3556
- if (sinceTimestamp != null && (timestamp == null || timestamp < sinceTimestamp) || untilTimestamp != null && (timestamp == null || timestamp > untilTimestamp)) {
3557
- dateSkipped += 1;
3558
- bar?.tick(1, {
3559
- symbol: import_chalk.default.gray(row.symbol),
3560
- status: import_chalk.default.gray("date-skip")
3561
- });
3562
- return;
3563
- }
3564
- try {
3565
- const signal = extractSignalFromAiDatasetRow(row);
3566
- const payload = (0, import_ai2.buildAiPayload)(signal);
3567
- const gateContext = (0, import_ai2.getDeterministicAiGateContext)(payload);
3568
- const analysis = await (0, import_ai2.runAiPromptLocal)(signal, { payload });
3569
- const quality = normalizeQuality(analysis);
3570
- const modelDirection = typeof analysis.direction === "string" && analysis.direction.trim() ? analysis.direction : null;
3571
- const modelDirectionMatches = modelDirection === row.direction;
3572
- const profit = Number(row.profit);
3573
- const featureSnapshot = collectAiPocketFeatureSnapshot({
3574
- payload,
3575
- gateContext,
3576
- includeSymbol,
3577
- includeGateContext,
3578
- featureProfile,
3579
- featurePolicy,
3580
- onFeatureExcluded: ({ path: featurePath, classification }) => {
3581
- const paths = excludedFeaturePaths.get(classification) ?? /* @__PURE__ */ new Set();
3582
- paths.add(featurePath);
3583
- excludedFeaturePaths.set(classification, paths);
3584
- }
3585
- });
3586
- rows.push({
3587
- signalId: row.signalId,
3588
- strategy: row.strategyName,
3589
- symbol: row.symbol,
3590
- direction: row.direction,
3591
- timestamp,
3592
- profit,
3593
- profitableTrade: profit > 0,
3594
- aiApproved: isAiApproval(row, analysis, minQuality),
3595
- quality,
3596
- modelDirectionMatches,
3597
- modelCandidate: getDeterministicModelCandidate(signal),
3598
- features: featureSnapshot.features,
3599
- featureCoverage: featureSnapshot.featureCoverage
3600
- });
3601
- bar?.tick(1, {
3602
- symbol: import_chalk.default.gray(row.symbol),
3603
- status: import_chalk.default.green("ok")
3604
- });
3605
- } catch (error) {
3606
- failed += 1;
3607
- if (errors.length < 5) {
3608
- errors.push(
3609
- `[${row.symbol}/${row.signalId}] ${error instanceof Error ? error.message : String(error)}`
3610
- );
3611
- }
3612
- bar?.tick(1, {
3613
- symbol: import_chalk.default.gray(row.symbol),
3614
- status: import_chalk.default.yellow("error")
3615
- });
3616
- }
3617
- }
3618
- });
3619
- const directionRows = direction ? rows.filter((row) => row.direction === direction) : rows;
3620
- const scopeRows = resolveScopeRows(directionRows, scope);
3621
- const fullSplit = splitAiPocketResearchRowsByTimestamp(
3622
- directionRows,
3623
- validationSplit,
3624
- testSplit
3625
- );
3626
- const sealedFullSplit = sealAiPocketTestPartition(fullSplit, sealTest);
3627
- const discoveryRows = sealedFullSplit.discoveryRows;
3628
- const trainRows = resolveScopeRows(fullSplit.trainRows, scope);
3629
- const validationRows = resolveScopeRows(fullSplit.validationRows, scope);
3630
- const testRows = resolveScopeRows(sealedFullSplit.searchTestRows, scope);
3631
- const baselineRows = fullSplit.trainRows.filter((row) => row.aiApproved);
3632
- const validationBaselineRows = fullSplit.validationRows.filter(
3633
- (row) => row.aiApproved
3240
+ if (search.objectiveBaseline) {
3241
+ printSection(
3242
+ "CURRENT GATE TRAIN BASELINE",
3243
+ createTable(
3244
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
3245
+ buildSummaryRows(search.objectiveBaseline)
3246
+ )
3247
+ );
3248
+ }
3249
+ printSection(
3250
+ "SEARCH STATS",
3251
+ createTable(
3252
+ [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
3253
+ [
3254
+ ["feature_keys", import_chalk.default.cyan(String(search.stats.featureKeys))],
3255
+ ["predicates", import_chalk.default.cyan(String(search.stats.predicates))],
3256
+ ["atomic_used", import_chalk.default.cyan(String(search.stats.atomicPredicatesUsed))],
3257
+ [
3258
+ "feature_families",
3259
+ import_chalk.default.cyan(search.stats.featureFamiliesUsed.join(", "))
3260
+ ],
3261
+ [
3262
+ "required_feature_families",
3263
+ import_chalk.default.cyan(search.stats.requiredFeatureFamilies.join(", ") || "none")
3264
+ ],
3265
+ [
3266
+ "excluded_feature_families",
3267
+ import_chalk.default.cyan(search.stats.excludedFeatureFamilies.join(", ") || "none")
3268
+ ],
3269
+ [
3270
+ "estimated_combinations",
3271
+ import_chalk.default.cyan(String(search.stats.estimatedCombinations))
3272
+ ],
3273
+ [
3274
+ "combinations_evaluated",
3275
+ import_chalk.default.cyan(String(search.stats.combinationsEvaluated))
3276
+ ],
3277
+ ["validation_rows", import_chalk.default.cyan(String(search.stats.validationRows))],
3278
+ ["test_rows", import_chalk.default.cyan(String(search.stats.testRows))],
3279
+ ["train_events", import_chalk.default.cyan(String(search.stats.cadence.trainEvents))],
3280
+ [
3281
+ "train_events_per_day",
3282
+ import_chalk.default.cyan(formatNumber2(search.stats.cadence.trainEventsPerDay))
3283
+ ],
3284
+ [
3285
+ "effective_min_support",
3286
+ import_chalk.default.cyan(String(search.stats.cadence.minSupport))
3287
+ ],
3288
+ [
3289
+ "effective_min_events",
3290
+ import_chalk.default.cyan(String(search.stats.cadence.minEvents))
3291
+ ],
3292
+ [
3293
+ "effective_min_validation_support",
3294
+ import_chalk.default.cyan(String(search.stats.cadence.minValidationSupport))
3295
+ ],
3296
+ [
3297
+ "effective_min_validation_events",
3298
+ import_chalk.default.cyan(String(search.stats.cadence.minValidationEvents))
3299
+ ],
3300
+ [
3301
+ "duplicate_pockets_skipped",
3302
+ import_chalk.default.cyan(String(search.stats.duplicatePocketsSkipped))
3303
+ ],
3304
+ [
3305
+ "truncated",
3306
+ search.stats.truncated ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3307
+ ]
3308
+ ]
3309
+ )
3634
3310
  );
3635
- const testBaselineRows = sealedFullSplit.searchTestRows.filter(
3636
- (row) => row.aiApproved
3311
+ printSection(
3312
+ "TOP POSITIVE POCKETS",
3313
+ createTable(
3314
+ [
3315
+ import_chalk.default.gray("#"),
3316
+ import_chalk.default.gray("N"),
3317
+ import_chalk.default.gray("EVENTS"),
3318
+ import_chalk.default.gray("MAX_B"),
3319
+ import_chalk.default.gray("SUP"),
3320
+ import_chalk.default.gray("WR"),
3321
+ import_chalk.default.gray("PF"),
3322
+ import_chalk.default.gray("PNL"),
3323
+ import_chalk.default.gray("OBJ_PNL"),
3324
+ import_chalk.default.gray("MAX_DD"),
3325
+ import_chalk.default.gray("VAL_N"),
3326
+ import_chalk.default.gray("VAL_EVENTS"),
3327
+ import_chalk.default.gray("VAL_WR"),
3328
+ import_chalk.default.gray("VAL_PF"),
3329
+ import_chalk.default.gray("VAL_PNL"),
3330
+ import_chalk.default.gray("TEST_N"),
3331
+ import_chalk.default.gray("TEST_EVENTS"),
3332
+ import_chalk.default.gray("TEST_PF"),
3333
+ import_chalk.default.gray("TEST_PNL"),
3334
+ import_chalk.default.gray("TEST_OBJ"),
3335
+ import_chalk.default.gray("TR/D"),
3336
+ import_chalk.default.gray("LOSS_M"),
3337
+ import_chalk.default.gray("SCORE"),
3338
+ import_chalk.default.gray("READINESS"),
3339
+ import_chalk.default.gray("REASONS"),
3340
+ import_chalk.default.gray("POCKET")
3341
+ ],
3342
+ buildPocketRows(search.positivePockets)
3343
+ )
3637
3344
  );
3638
- const currentGateSummary = summarizeAiTrainEvaluations(discoveryRows);
3639
- const currentGateQualityThresholds = summarizeAiTrainEvaluationsByQualityThreshold(
3640
- discoveryRows,
3641
- qualityThresholds
3345
+ printSection(
3346
+ "TOP LOSS POCKETS",
3347
+ createTable(
3348
+ [
3349
+ import_chalk.default.gray("#"),
3350
+ import_chalk.default.gray("N"),
3351
+ import_chalk.default.gray("EVENTS"),
3352
+ import_chalk.default.gray("MAX_B"),
3353
+ import_chalk.default.gray("SUP"),
3354
+ import_chalk.default.gray("WR"),
3355
+ import_chalk.default.gray("PF"),
3356
+ import_chalk.default.gray("PNL"),
3357
+ import_chalk.default.gray("OBJ_PNL"),
3358
+ import_chalk.default.gray("MAX_DD"),
3359
+ import_chalk.default.gray("VAL_N"),
3360
+ import_chalk.default.gray("VAL_EVENTS"),
3361
+ import_chalk.default.gray("VAL_WR"),
3362
+ import_chalk.default.gray("VAL_PF"),
3363
+ import_chalk.default.gray("VAL_PNL"),
3364
+ import_chalk.default.gray("TEST_N"),
3365
+ import_chalk.default.gray("TEST_EVENTS"),
3366
+ import_chalk.default.gray("TEST_PF"),
3367
+ import_chalk.default.gray("TEST_PNL"),
3368
+ import_chalk.default.gray("TEST_OBJ"),
3369
+ import_chalk.default.gray("TR/D"),
3370
+ import_chalk.default.gray("LOSS_M"),
3371
+ import_chalk.default.gray("SCORE"),
3372
+ import_chalk.default.gray("READINESS"),
3373
+ import_chalk.default.gray("REASONS"),
3374
+ import_chalk.default.gray("POCKET")
3375
+ ],
3376
+ buildPocketRows(search.negativePockets)
3377
+ )
3642
3378
  );
3643
- const runSearch = ({
3644
- label,
3645
- train,
3646
- validation,
3647
- test,
3648
- trainBaseline,
3649
- validationBaseline,
3650
- testBaseline,
3651
- requiredFeatureFamilies = [],
3652
- excludedFeatureFamilies = []
3653
- }) => {
3654
- const cadenceProfile = resolveAiPocketCadenceProfile({
3655
- trainRows: train,
3656
- validationRows: validation,
3657
- testRows: test,
3658
- mode: cadenceMode,
3659
- validationSplit,
3660
- ...explicitMinSupport != null ? { minSupport: explicitMinSupport } : {},
3661
- ...explicitMinEvents > 0 ? { minEvents: explicitMinEvents } : {},
3662
- ...explicitMinValidationSupport > 0 ? { minValidationSupport: explicitMinValidationSupport } : {},
3663
- ...explicitMinValidationEvents > 0 ? { minValidationEvents: explicitMinValidationEvents } : {},
3664
- maxEventCountShare,
3665
- explicitMaxEventCountShare: hasCliOption2("maxEventCountShare", "U")
3666
- });
3667
- let searchBar = null;
3668
- let searchBarPhase = null;
3669
- return searchAiPockets(train, {
3670
- minSupport: cadenceProfile.minSupport,
3671
- minProfitFactor,
3672
- minTotalProfit,
3673
- minWinRate,
3674
- maxDepth,
3675
- maxAtomicPredicates,
3676
- maxCombinations,
3677
- top,
3678
- validationRows: validation,
3679
- testRows: test,
3680
- minValidationSupport: cadenceProfile.minValidationSupport,
3681
- minEvents: cadenceProfile.minEvents,
3682
- minValidationEvents: cadenceProfile.minValidationEvents,
3683
- ...maxBatch > 0 ? { maxBatch } : {},
3684
- maxEventCountShare: cadenceProfile.maxEventCountShare,
3685
- maxSymbolCountShare,
3686
- objective,
3687
- baselineRows: trainBaseline,
3688
- validationBaselineRows: validationBaseline,
3689
- testBaselineRows: testBaseline,
3690
- allowRiskRegression,
3691
- requireValidationEligibility,
3692
- dedupeEquivalentSelections,
3693
- requiredFeatureFamilies,
3694
- excludedFeatureFamilies,
3695
- cadenceProfile,
3696
- progressInterval: 250,
3697
- onProgress: (progress) => {
3698
- if (searchBar && searchBarPhase !== progress.phase) {
3699
- tickProgressBarTo(searchBar, searchBar.total, {
3700
- status: import_chalk.default.gray("done")
3701
- });
3702
- searchBar = null;
3703
- }
3704
- if (!searchBar) {
3705
- searchBarPhase = progress.phase;
3706
- const phaseLabel = searchPhaseLabels[progress.phase].padEnd(8, " ");
3707
- console.error(
3708
- import_chalk.default.gray(
3709
- `stage: ${label}/${searchPhaseLabels[progress.phase]} (${progress.total})`
3710
- )
3711
- );
3712
- searchBar = new import_progress.default(
3713
- `${phaseLabel} :current/:total [:bar] :percent :status`,
3714
- {
3715
- total: Math.max(progress.total, 1),
3716
- width: 20,
3717
- stream: process.stderr
3718
- }
3719
- );
3720
- }
3721
- tickProgressBarTo(searchBar, progress.current, {
3722
- status: progress.truncated ? import_chalk.default.yellow("truncated") : "running"
3723
- });
3724
- if (progress.done) {
3725
- tickProgressBarTo(searchBar, searchBar.total, {
3726
- status: progress.truncated ? import_chalk.default.yellow("truncated") : "done"
3727
- });
3728
- searchBar = null;
3729
- }
3730
- }
3731
- });
3379
+ for (const cohort of coverageSearches) {
3380
+ const title = cohort.family === "cmc" ? "CMC" : "COINALYZE";
3381
+ printSection(
3382
+ `${title} COVERAGE COHORT`,
3383
+ createTable(
3384
+ [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
3385
+ [
3386
+ ["coverage_rows", import_chalk.default.cyan(String(cohort.coverage.rows))],
3387
+ ["coverage_ratio", import_chalk.default.cyan(formatRatio(cohort.coverage.rowRatio))],
3388
+ ["coverage_events", import_chalk.default.cyan(String(cohort.coverage.events))],
3389
+ ["event_ratio", import_chalk.default.cyan(formatRatio(cohort.coverage.eventRatio))],
3390
+ [
3391
+ "coverage_from",
3392
+ cohort.coverage.minTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(
3393
+ new Date(cohort.coverage.minTimestamp).toISOString()
3394
+ )
3395
+ ],
3396
+ [
3397
+ "coverage_to",
3398
+ cohort.coverage.maxTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(
3399
+ new Date(cohort.coverage.maxTimestamp).toISOString()
3400
+ )
3401
+ ],
3402
+ ["scope_rows", import_chalk.default.cyan(String(cohort.scopeRows))],
3403
+ ["train_rows", import_chalk.default.cyan(String(cohort.trainRows))],
3404
+ ["validation_rows", import_chalk.default.cyan(String(cohort.validationRows))],
3405
+ ["test_rows", import_chalk.default.cyan(String(cohort.testRows))],
3406
+ [
3407
+ "low_cadence",
3408
+ cohort.search.stats.cadence.lowCadence ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3409
+ ],
3410
+ [
3411
+ "sparse_sample",
3412
+ cohort.search.stats.cadence.sparseSample ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3413
+ ],
3414
+ [
3415
+ "adaptive_thresholds",
3416
+ cohort.search.stats.cadence.adaptiveThresholds ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
3417
+ ],
3418
+ [
3419
+ "train_events",
3420
+ import_chalk.default.cyan(String(cohort.search.stats.cadence.trainEvents))
3421
+ ],
3422
+ [
3423
+ "effective_min_support",
3424
+ import_chalk.default.cyan(String(cohort.search.stats.cadence.minSupport))
3425
+ ],
3426
+ [
3427
+ "effective_min_events",
3428
+ import_chalk.default.cyan(String(cohort.search.stats.cadence.minEvents))
3429
+ ]
3430
+ ]
3431
+ )
3432
+ );
3433
+ printSection(
3434
+ `${title} TRAIN BASELINE`,
3435
+ createTable(
3436
+ [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
3437
+ buildSummaryRows(cohort.search.baseline)
3438
+ )
3439
+ );
3440
+ printSection(
3441
+ `${title} TOP POSITIVE POCKETS`,
3442
+ createTable(
3443
+ [
3444
+ import_chalk.default.gray("#"),
3445
+ import_chalk.default.gray("N"),
3446
+ import_chalk.default.gray("EVENTS"),
3447
+ import_chalk.default.gray("MAX_B"),
3448
+ import_chalk.default.gray("SUP"),
3449
+ import_chalk.default.gray("WR"),
3450
+ import_chalk.default.gray("PF"),
3451
+ import_chalk.default.gray("PNL"),
3452
+ import_chalk.default.gray("OBJ_PNL"),
3453
+ import_chalk.default.gray("MAX_DD"),
3454
+ import_chalk.default.gray("VAL_N"),
3455
+ import_chalk.default.gray("VAL_EVENTS"),
3456
+ import_chalk.default.gray("VAL_WR"),
3457
+ import_chalk.default.gray("VAL_PF"),
3458
+ import_chalk.default.gray("VAL_PNL"),
3459
+ import_chalk.default.gray("TEST_N"),
3460
+ import_chalk.default.gray("TEST_EVENTS"),
3461
+ import_chalk.default.gray("TEST_PF"),
3462
+ import_chalk.default.gray("TEST_PNL"),
3463
+ import_chalk.default.gray("TEST_OBJ"),
3464
+ import_chalk.default.gray("TR/D"),
3465
+ import_chalk.default.gray("LOSS_M"),
3466
+ import_chalk.default.gray("SCORE"),
3467
+ import_chalk.default.gray("READINESS"),
3468
+ import_chalk.default.gray("REASONS"),
3469
+ import_chalk.default.gray("POCKET")
3470
+ ],
3471
+ buildPocketRows(cohort.search.positivePockets)
3472
+ )
3473
+ );
3474
+ printSection(
3475
+ `${title} TOP LOSS POCKETS`,
3476
+ createTable(
3477
+ [
3478
+ import_chalk.default.gray("#"),
3479
+ import_chalk.default.gray("N"),
3480
+ import_chalk.default.gray("EVENTS"),
3481
+ import_chalk.default.gray("MAX_B"),
3482
+ import_chalk.default.gray("SUP"),
3483
+ import_chalk.default.gray("WR"),
3484
+ import_chalk.default.gray("PF"),
3485
+ import_chalk.default.gray("PNL"),
3486
+ import_chalk.default.gray("OBJ_PNL"),
3487
+ import_chalk.default.gray("MAX_DD"),
3488
+ import_chalk.default.gray("VAL_N"),
3489
+ import_chalk.default.gray("VAL_EVENTS"),
3490
+ import_chalk.default.gray("VAL_WR"),
3491
+ import_chalk.default.gray("VAL_PF"),
3492
+ import_chalk.default.gray("VAL_PNL"),
3493
+ import_chalk.default.gray("TEST_N"),
3494
+ import_chalk.default.gray("TEST_EVENTS"),
3495
+ import_chalk.default.gray("TEST_PF"),
3496
+ import_chalk.default.gray("TEST_PNL"),
3497
+ import_chalk.default.gray("TEST_OBJ"),
3498
+ import_chalk.default.gray("TR/D"),
3499
+ import_chalk.default.gray("LOSS_M"),
3500
+ import_chalk.default.gray("SCORE"),
3501
+ import_chalk.default.gray("READINESS"),
3502
+ import_chalk.default.gray("REASONS"),
3503
+ import_chalk.default.gray("POCKET")
3504
+ ],
3505
+ buildPocketRows(cohort.search.negativePockets)
3506
+ )
3507
+ );
3508
+ }
3509
+ if (errors.length) {
3510
+ console.log(import_chalk.default.yellow("Errors:"));
3511
+ errors.forEach((error) => console.log(import_chalk.default.yellow(`- ${error}`)));
3512
+ }
3513
+ };
3514
+
3515
+ // src/lib/aiPocketSearch/dataset.ts
3516
+ var import_promises2 = __toESM(require("fs/promises"));
3517
+ var import_path2 = __toESM(require("path"));
3518
+ var import_ai2 = require("@tradejs/infra/ai");
3519
+ var import_ai3 = require("@tradejs/node/ai");
3520
+ var import_registry = require("@tradejs/node/registry");
3521
+
3522
+ // src/lib/aiTrainDataset.ts
3523
+ var extractSignalFromAiDatasetRow = (row) => {
3524
+ const { payload } = row;
3525
+ return {
3526
+ ...payload.signal,
3527
+ strategy: payload.signal.strategy,
3528
+ figures: payload.figures ?? {},
3529
+ indicators: payload.indicators ?? {},
3530
+ additionalIndicators: payload.additionalIndicators ?? {},
3531
+ prices: payload.signal.prices
3732
3532
  };
3733
- const search = runSearch({
3734
- label: "full",
3735
- train: trainRows,
3736
- validation: validationRows,
3737
- test: testRows,
3738
- trainBaseline: baselineRows,
3739
- validationBaseline: validationBaselineRows,
3740
- testBaseline: testBaselineRows,
3741
- ...coverageMode === "auto" ? {
3742
- excludedFeatureFamilies: [
3743
- "cmc",
3744
- "coinalyze"
3745
- ]
3746
- } : {}
3747
- });
3748
- const coverageSearches = [];
3749
- if (coverageMode === "auto") {
3750
- for (const family of ["cmc", "coinalyze"]) {
3751
- const cohortSplit = splitAiPocketCoverageRowsByTimestamp(
3752
- directionRows,
3753
- family,
3754
- validationSplit,
3755
- testSplit
3756
- );
3757
- const sealedCohortSplit = sealAiPocketTestPartition(
3758
- cohortSplit,
3759
- sealTest
3760
- );
3761
- const cohortTrainRows = resolveScopeRows(cohortSplit.trainRows, scope);
3762
- const cohortValidationRows = resolveScopeRows(
3763
- cohortSplit.validationRows,
3764
- scope
3765
- );
3766
- const cohortTestRows = resolveScopeRows(
3767
- sealedCohortSplit.searchTestRows,
3768
- scope
3769
- );
3770
- const cohortSearch = runSearch({
3771
- label: family,
3772
- train: cohortTrainRows,
3773
- validation: cohortValidationRows,
3774
- test: cohortTestRows,
3775
- trainBaseline: cohortSplit.trainRows.filter((row) => row.aiApproved),
3776
- validationBaseline: cohortSplit.validationRows.filter(
3777
- (row) => row.aiApproved
3778
- ),
3779
- testBaseline: sealedCohortSplit.searchTestRows.filter(
3780
- (row) => row.aiApproved
3781
- ),
3782
- requiredFeatureFamilies: [family]
3783
- });
3784
- coverageSearches.push({
3785
- family,
3786
- coverage: summarizeAiPocketFeatureCoverage(
3787
- sealedCohortSplit.discoveryRows,
3788
- family
3789
- ),
3790
- scopeRows: resolveScopeRows(
3791
- sealedCohortSplit.discoveryRows.filter(
3792
- (row) => row.featureCoverage?.[family] === true
3793
- ),
3794
- scope
3795
- ).length,
3796
- trainRows: cohortTrainRows.length,
3797
- validationRows: cohortValidationRows.length,
3798
- testRows: cohortTestRows.length,
3799
- search: cohortSearch
3800
- });
3533
+ };
3534
+
3535
+ // src/lib/aiPocketSearch/dataset.ts
3536
+ var normalizeQuality = (analysis) => {
3537
+ const quality = Number(analysis?.quality);
3538
+ return Number.isFinite(quality) ? Math.round(quality) : null;
3539
+ };
3540
+ var isAiApproval = (row, analysis, minQuality) => {
3541
+ const quality = normalizeQuality(analysis);
3542
+ return analysis.direction === row.direction && quality != null && quality >= minQuality;
3543
+ };
3544
+ var listMergedFiles = async (params) => {
3545
+ const { outDir, strategyName } = params;
3546
+ let entries = [];
3547
+ try {
3548
+ entries = await import_promises2.default.readdir(outDir);
3549
+ } catch {
3550
+ return [];
3551
+ }
3552
+ const prefix = strategyName ? `ai-dataset-${(0, import_ai2.toFileToken)(strategyName)}-merged-` : "ai-dataset-";
3553
+ return entries.filter((name) => name.startsWith(prefix)).filter((name) => {
3554
+ if (!name.endsWith(".jsonl") || !name.includes("-merged-")) {
3555
+ return false;
3801
3556
  }
3557
+ return /-merged-\d+\.jsonl$/.test(name) || /-merged-\d+-part\d+\.jsonl$/.test(name);
3558
+ }).sort().map((name) => import_path2.default.join(outDir, name));
3559
+ };
3560
+ var deriveStrategyNameFromFile = (filePath) => {
3561
+ const match = import_path2.default.basename(filePath).match(/^ai-dataset-(.+)-merged-\d+(?:-part\d+)?\.jsonl$/);
3562
+ return match?.[1] ? match[1] : "unknown";
3563
+ };
3564
+ var getMergedGroupId2 = (filePath) => {
3565
+ const match = import_path2.default.basename(filePath).match(/^ai-dataset-(.+)-merged-(\d+)(?:-part(\d+))?\.jsonl$/);
3566
+ if (!match) {
3567
+ return null;
3802
3568
  }
3803
- const result = {
3804
- generatedAt,
3805
- run: {
3806
- strategy: resolvedStrategyName,
3807
- filePaths,
3808
- sourceRows: totalRows,
3809
- selectedRows,
3810
- evaluatedRows: rows.length,
3811
- scope,
3812
- direction: direction || null,
3813
- scopeRows: scopeRows.length,
3814
- trainRows: trainRows.length,
3815
- validationRows: validationRows.length,
3816
- testRows: testRows.length,
3817
- scanned,
3818
- dateSkipped,
3819
- failed,
3820
- recent,
3821
- skip,
3822
- since: sinceTimestamp,
3823
- until: untilTimestamp,
3824
- period: periodLabel,
3825
- minQuality,
3826
- qualityThresholds,
3827
- includeSymbol,
3828
- includeGateContext,
3829
- featureProfile,
3830
- featurePolicy,
3831
- coverageMode,
3832
- cadenceMode,
3833
- coverageSearches: coverageSearches.map(
3834
- ({
3835
- family,
3836
- coverage,
3837
- scopeRows: scopeRows2,
3838
- trainRows: trainRows2,
3839
- validationRows: validationRows2,
3840
- testRows: testRows2
3841
- }) => ({
3842
- family,
3843
- coverage,
3844
- scopeRows: scopeRows2,
3845
- trainRows: trainRows2,
3846
- validationRows: validationRows2,
3847
- testRows: testRows2
3848
- })
3849
- ),
3850
- featurePolicyAudit: Object.fromEntries(
3851
- [...excludedFeaturePaths.entries()].map(([classification, paths]) => [
3852
- classification,
3853
- {
3854
- paths: paths.size,
3855
- samples: [...paths].sort().slice(0, 5)
3856
- }
3857
- ])
3858
- ),
3859
- objective,
3860
- validationSplit,
3861
- testSplit,
3862
- sealedTest: sealedFullSplit.evidence,
3863
- minValidationSupport: search.stats.cadence.minValidationSupport,
3864
- reportPath,
3865
- search: {
3866
- maxDepth,
3867
- minSupport: search.stats.cadence.minSupport,
3868
- minProfitFactor,
3869
- minWinRate,
3870
- minTotalProfit,
3871
- maxAtomicPredicates,
3872
- maxCombinations,
3873
- minEvents: search.stats.cadence.minEvents,
3874
- minValidationEvents: search.stats.cadence.minValidationEvents,
3875
- ...maxBatch > 0 ? { maxBatch } : {},
3876
- maxEventCountShare: search.stats.cadence.maxEventCountShare,
3877
- maxSymbolCountShare,
3878
- allowRiskRegression,
3879
- requireValidationEligibility,
3880
- cadence: search.stats.cadence,
3881
- validationSplit,
3882
- testSplit,
3883
- dedupeEquivalentSelections,
3884
- top
3885
- }
3886
- },
3887
- currentGate: {
3888
- summary: currentGateSummary,
3889
- qualityThresholds: currentGateQualityThresholds
3890
- },
3891
- pocketSearch: search,
3892
- coverageSearches,
3893
- errors
3569
+ return {
3570
+ strategyToken: match[1],
3571
+ mergeId: match[2]
3894
3572
  };
3895
- const markdownReport = buildAiPocketMarkdownReport(result);
3896
- await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(reportPath)), {
3897
- recursive: true
3573
+ };
3574
+ var sortDatasetPartPaths = (filePaths) => [...filePaths].sort((left, right) => {
3575
+ const leftMatch = import_path2.default.basename(left).match(/-part(\d+)\.jsonl$/);
3576
+ const rightMatch = import_path2.default.basename(right).match(/-part(\d+)\.jsonl$/);
3577
+ const leftPart = leftMatch ? Number(leftMatch[1]) : 0;
3578
+ const rightPart = rightMatch ? Number(rightMatch[1]) : 0;
3579
+ return leftPart - rightPart || left.localeCompare(right);
3580
+ });
3581
+ var resolveMergedDatasetFiles = async ({
3582
+ outDir,
3583
+ strategyName,
3584
+ explicitFile
3585
+ }) => {
3586
+ const mergedFiles = await listMergedFiles({
3587
+ outDir,
3588
+ strategyName
3898
3589
  });
3899
- await import_promises.default.writeFile(reportPath, markdownReport, "utf8");
3900
- if (outputPath) {
3901
- await import_promises.default.mkdir(import_path.default.dirname(import_path.default.resolve(outputPath)), {
3902
- recursive: true
3903
- });
3904
- await import_promises.default.writeFile(outputPath, `${JSON.stringify(result, null, 2)}
3905
- `);
3590
+ if (!mergedFiles.length) {
3591
+ throw new Error(
3592
+ strategyName ? `No merged AI dataset found for strategy "${strategyName}" in ${outDir}. Run yarn ai-export first.` : `No merged AI dataset found in ${outDir}. Run yarn ai-export first.`
3593
+ );
3906
3594
  }
3907
- if (jsonOutput) {
3908
- console.log(JSON.stringify(result));
3909
- process.exit(failed === selectedRows ? 1 : 0);
3595
+ const resolvedExplicitFile = explicitFile ? import_path2.default.resolve(explicitFile) : null;
3596
+ if (resolvedExplicitFile) {
3597
+ const groupId = getMergedGroupId2(resolvedExplicitFile);
3598
+ if (!groupId) {
3599
+ return [resolvedExplicitFile];
3600
+ }
3601
+ const groupedFiles = sortDatasetPartPaths(
3602
+ mergedFiles.filter((candidate) => {
3603
+ const candidateGroup = getMergedGroupId2(candidate);
3604
+ return candidateGroup?.strategyToken === groupId.strategyToken && candidateGroup?.mergeId === groupId.mergeId;
3605
+ })
3606
+ );
3607
+ return groupedFiles.length ? groupedFiles : [resolvedExplicitFile];
3910
3608
  }
3911
- console.log("");
3912
- console.log(import_chalk.default.green("AI pocket search finished"));
3913
- filePaths.forEach((filePath) => console.log(import_chalk.default.gray(filePath)));
3914
- console.log("");
3915
- printSection(
3916
- "RUN",
3917
- createTable(
3918
- [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
3919
- [
3920
- ["strategy", import_chalk.default.yellow(resolvedStrategyName)],
3921
- ["selected", import_chalk.default.blue(String(selectedRows))],
3922
- ["evaluated", import_chalk.default.blue(String(rows.length))],
3923
- ["scope", import_chalk.default.yellow(scope)],
3924
- ["objective", import_chalk.default.yellow(objective)],
3925
- ["direction", direction ? import_chalk.default.yellow(direction) : import_chalk.default.gray("all")],
3926
- ["scope_rows", import_chalk.default.blue(String(scopeRows.length))],
3927
- ["train_rows", import_chalk.default.blue(String(trainRows.length))],
3928
- ["validation_rows", import_chalk.default.blue(String(validationRows.length))],
3929
- ["test_rows", import_chalk.default.blue(String(testRows.length))],
3930
- ["source_rows", import_chalk.default.blue(String(totalRows))],
3931
- ["date_skipped", import_chalk.default.blue(String(dateSkipped))],
3932
- [
3933
- "failed",
3934
- failed > 0 ? import_chalk.default.yellow(String(failed)) : import_chalk.default.green("0")
3935
- ],
3936
- ["recent", import_chalk.default.blue(recent === 0 ? "all" : String(recent))],
3937
- ["skip", import_chalk.default.blue(String(skip))],
3938
- [
3939
- "since",
3940
- sinceTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(sinceTimestamp).toISOString())
3941
- ],
3942
- [
3943
- "until",
3944
- untilTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(untilTimestamp).toISOString())
3945
- ],
3946
- [
3947
- "period",
3948
- periodLabel == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(periodLabel)
3949
- ],
3950
- ["min_quality", import_chalk.default.magenta(String(minQuality))],
3951
- ["max_depth", import_chalk.default.magenta(String(maxDepth))],
3952
- ["cadence_mode", import_chalk.default.magenta(cadenceMode)],
3953
- [
3954
- "low_cadence",
3955
- search.stats.cadence.lowCadence ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3956
- ],
3957
- [
3958
- "sparse_sample",
3959
- search.stats.cadence.sparseSample ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
3960
- ],
3961
- [
3962
- "adaptive_thresholds",
3963
- search.stats.cadence.adaptiveThresholds ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
3964
- ],
3965
- [
3966
- "train_events",
3967
- import_chalk.default.magenta(String(search.stats.cadence.trainEvents))
3968
- ],
3969
- [
3970
- "train_events_per_day",
3971
- import_chalk.default.magenta(formatNumber2(search.stats.cadence.trainEventsPerDay))
3972
- ],
3973
- ["min_support", import_chalk.default.magenta(String(search.stats.cadence.minSupport))],
3974
- ["min_events", import_chalk.default.magenta(String(search.stats.cadence.minEvents))],
3975
- [
3976
- "min_validation_events",
3977
- import_chalk.default.magenta(String(search.stats.cadence.minValidationEvents))
3978
- ],
3979
- ["max_batch", import_chalk.default.magenta(maxBatch > 0 ? String(maxBatch) : "off")],
3980
- [
3981
- "max_event_share",
3982
- import_chalk.default.magenta(formatRatio(search.stats.cadence.maxEventCountShare))
3983
- ],
3984
- ["max_symbol_share", import_chalk.default.magenta(formatRatio(maxSymbolCountShare))],
3985
- ["max_atomic_predicates", import_chalk.default.magenta(String(maxAtomicPredicates))],
3986
- ["max_combinations", import_chalk.default.magenta(String(maxCombinations))],
3987
- ["validation_split", import_chalk.default.magenta(formatRatio(validationSplit))],
3988
- ["test_split", import_chalk.default.magenta(formatRatio(testSplit))],
3989
- [
3990
- "test_evidence",
3991
- sealedFullSplit.evidence.sealed ? import_chalk.default.green(
3992
- `sealed (${sealedFullSplit.evidence.rows} rows / ${sealedFullSplit.evidence.events} events)`
3993
- ) : import_chalk.default.yellow("open")
3994
- ],
3995
- [
3996
- "min_validation_support",
3997
- import_chalk.default.magenta(String(search.stats.cadence.minValidationSupport))
3998
- ],
3999
- [
4000
- "dedupe_equivalent",
4001
- dedupeEquivalentSelections ? import_chalk.default.green("on") : import_chalk.default.gray("off")
4002
- ],
4003
- [
4004
- "include_symbol",
4005
- includeSymbol ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
4006
- ],
4007
- [
4008
- "include_gate_context",
4009
- includeGateContext ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
4010
- ],
4011
- ["feature_profile", import_chalk.default.magenta(featureProfile)],
4012
- ["feature_policy", import_chalk.default.magenta(featurePolicy)],
4013
- ["coverage_mode", import_chalk.default.magenta(coverageMode)],
4014
- [
4015
- "allow_risk_regression",
4016
- allowRiskRegression ? import_chalk.default.yellow("on") : import_chalk.default.green("off")
4017
- ],
4018
- [
4019
- "validation_eligibility",
4020
- requireValidationEligibility ? import_chalk.default.green("required") : import_chalk.default.yellow("off")
4021
- ],
4022
- ["report", import_chalk.default.gray(reportPath)],
4023
- ["output", outputPath ? import_chalk.default.gray(outputPath) : import_chalk.default.gray("off")]
4024
- ]
4025
- )
3609
+ const latestFile = mergedFiles[mergedFiles.length - 1];
3610
+ const latestGroupId = getMergedGroupId2(latestFile);
3611
+ if (!latestGroupId) {
3612
+ return [latestFile];
3613
+ }
3614
+ return sortDatasetPartPaths(
3615
+ mergedFiles.filter((candidate) => {
3616
+ const candidateGroup = getMergedGroupId2(candidate);
3617
+ return candidateGroup?.strategyToken === latestGroupId.strategyToken && candidateGroup?.mergeId === latestGroupId.mergeId;
3618
+ })
4026
3619
  );
4027
- printSection(
4028
- "CURRENT GATE QN+ BASELINE",
4029
- createTable(
4030
- [
4031
- import_chalk.default.gray("Q"),
4032
- import_chalk.default.gray("APPROVED"),
4033
- import_chalk.default.gray("WR"),
4034
- import_chalk.default.gray("PF"),
4035
- import_chalk.default.gray("PNL"),
4036
- import_chalk.default.gray("MAX_DD"),
4037
- import_chalk.default.gray("TRADES/D"),
4038
- import_chalk.default.gray("PNL/D")
4039
- ],
4040
- buildQualityRows(currentGateQualityThresholds)
4041
- )
4042
- );
4043
- printSection(
4044
- "TRAIN BASELINE",
4045
- createTable(
4046
- [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4047
- buildSummaryRows(search.baseline)
4048
- )
4049
- );
4050
- if (search.validationBaseline) {
4051
- printSection(
4052
- "VALIDATION BASELINE",
4053
- createTable(
4054
- [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4055
- buildSummaryRows(search.validationBaseline)
4056
- )
4057
- );
3620
+ };
3621
+ var getDatasetRowTimestamp = (row) => {
3622
+ const timestamp = Number(row.timestamp);
3623
+ return Number.isFinite(timestamp) ? timestamp : null;
3624
+ };
3625
+ var findMaxSelectedTimestamp = async ({
3626
+ filePaths,
3627
+ recent,
3628
+ skip
3629
+ }) => {
3630
+ let maxTimestamp = null;
3631
+ await (0, import_ai2.streamAiDatasetRows)({
3632
+ filePaths,
3633
+ limitFromEnd: recent,
3634
+ skipFromEnd: skip,
3635
+ onRow: async (row) => {
3636
+ const timestamp = getDatasetRowTimestamp(row);
3637
+ if (timestamp != null) {
3638
+ maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
3639
+ }
3640
+ }
3641
+ });
3642
+ return maxTimestamp;
3643
+ };
3644
+ var getDeterministicModelCandidate = (signal) => {
3645
+ if (!signal) {
3646
+ return false;
4058
3647
  }
4059
- if (search.testBaseline) {
4060
- printSection(
4061
- "UNTOUCHED TEST BASELINE",
4062
- createTable(
4063
- [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4064
- buildSummaryRows(search.testBaseline)
4065
- )
4066
- );
3648
+ const payload = (0, import_ai3.buildAiPayload)(signal);
3649
+ const gateContext = (0, import_ai3.getDeterministicAiGateContext)(payload);
3650
+ const structuralHardBlockReasons = Array.isArray(
3651
+ gateContext?.structuralHardBlockReasons
3652
+ ) ? gateContext.structuralHardBlockReasons.filter(
3653
+ (reason) => typeof reason === "string" && reason.trim().length > 0
3654
+ ) : [];
3655
+ if (structuralHardBlockReasons.length) {
3656
+ return false;
4067
3657
  }
4068
- if (search.objectiveBaseline) {
4069
- printSection(
4070
- "CURRENT GATE TRAIN BASELINE",
4071
- createTable(
4072
- [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4073
- buildSummaryRows(search.objectiveBaseline)
4074
- )
4075
- );
3658
+ if (typeof gateContext?.approvalAllowedNow === "boolean") {
3659
+ return gateContext.approvalAllowedNow;
4076
3660
  }
4077
- printSection(
4078
- "SEARCH STATS",
4079
- createTable(
4080
- [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
4081
- [
4082
- ["feature_keys", import_chalk.default.cyan(String(search.stats.featureKeys))],
4083
- ["predicates", import_chalk.default.cyan(String(search.stats.predicates))],
4084
- ["atomic_used", import_chalk.default.cyan(String(search.stats.atomicPredicatesUsed))],
4085
- [
4086
- "feature_families",
4087
- import_chalk.default.cyan(search.stats.featureFamiliesUsed.join(", "))
4088
- ],
4089
- [
4090
- "required_feature_families",
4091
- import_chalk.default.cyan(search.stats.requiredFeatureFamilies.join(", ") || "none")
4092
- ],
4093
- [
4094
- "excluded_feature_families",
4095
- import_chalk.default.cyan(search.stats.excludedFeatureFamilies.join(", ") || "none")
4096
- ],
4097
- [
4098
- "estimated_combinations",
4099
- import_chalk.default.cyan(String(search.stats.estimatedCombinations))
4100
- ],
4101
- [
4102
- "combinations_evaluated",
4103
- import_chalk.default.cyan(String(search.stats.combinationsEvaluated))
4104
- ],
4105
- ["validation_rows", import_chalk.default.cyan(String(search.stats.validationRows))],
4106
- ["test_rows", import_chalk.default.cyan(String(search.stats.testRows))],
4107
- ["train_events", import_chalk.default.cyan(String(search.stats.cadence.trainEvents))],
4108
- [
4109
- "train_events_per_day",
4110
- import_chalk.default.cyan(formatNumber2(search.stats.cadence.trainEventsPerDay))
4111
- ],
4112
- [
4113
- "effective_min_support",
4114
- import_chalk.default.cyan(String(search.stats.cadence.minSupport))
4115
- ],
4116
- [
4117
- "effective_min_events",
4118
- import_chalk.default.cyan(String(search.stats.cadence.minEvents))
4119
- ],
4120
- [
4121
- "effective_min_validation_support",
4122
- import_chalk.default.cyan(String(search.stats.cadence.minValidationSupport))
4123
- ],
4124
- [
4125
- "effective_min_validation_events",
4126
- import_chalk.default.cyan(String(search.stats.cadence.minValidationEvents))
4127
- ],
4128
- [
4129
- "duplicate_pockets_skipped",
4130
- import_chalk.default.cyan(String(search.stats.duplicatePocketsSkipped))
4131
- ],
4132
- [
4133
- "truncated",
4134
- search.stats.truncated ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
4135
- ]
4136
- ]
4137
- )
4138
- );
4139
- printSection(
4140
- "TOP POSITIVE POCKETS",
4141
- createTable(
4142
- [
4143
- import_chalk.default.gray("#"),
4144
- import_chalk.default.gray("N"),
4145
- import_chalk.default.gray("EVENTS"),
4146
- import_chalk.default.gray("MAX_B"),
4147
- import_chalk.default.gray("SUP"),
4148
- import_chalk.default.gray("WR"),
4149
- import_chalk.default.gray("PF"),
4150
- import_chalk.default.gray("PNL"),
4151
- import_chalk.default.gray("OBJ_PNL"),
4152
- import_chalk.default.gray("MAX_DD"),
4153
- import_chalk.default.gray("VAL_N"),
4154
- import_chalk.default.gray("VAL_EVENTS"),
4155
- import_chalk.default.gray("VAL_WR"),
4156
- import_chalk.default.gray("VAL_PF"),
4157
- import_chalk.default.gray("VAL_PNL"),
4158
- import_chalk.default.gray("TEST_N"),
4159
- import_chalk.default.gray("TEST_EVENTS"),
4160
- import_chalk.default.gray("TEST_PF"),
4161
- import_chalk.default.gray("TEST_PNL"),
4162
- import_chalk.default.gray("TEST_OBJ"),
4163
- import_chalk.default.gray("TR/D"),
4164
- import_chalk.default.gray("LOSS_M"),
4165
- import_chalk.default.gray("SCORE"),
4166
- import_chalk.default.gray("READINESS"),
4167
- import_chalk.default.gray("REASONS"),
4168
- import_chalk.default.gray("POCKET")
4169
- ],
4170
- buildPocketRows(search.positivePockets)
4171
- )
4172
- );
4173
- printSection(
4174
- "TOP LOSS POCKETS",
4175
- createTable(
4176
- [
4177
- import_chalk.default.gray("#"),
4178
- import_chalk.default.gray("N"),
4179
- import_chalk.default.gray("EVENTS"),
4180
- import_chalk.default.gray("MAX_B"),
4181
- import_chalk.default.gray("SUP"),
4182
- import_chalk.default.gray("WR"),
4183
- import_chalk.default.gray("PF"),
4184
- import_chalk.default.gray("PNL"),
4185
- import_chalk.default.gray("OBJ_PNL"),
4186
- import_chalk.default.gray("MAX_DD"),
4187
- import_chalk.default.gray("VAL_N"),
4188
- import_chalk.default.gray("VAL_EVENTS"),
4189
- import_chalk.default.gray("VAL_WR"),
4190
- import_chalk.default.gray("VAL_PF"),
4191
- import_chalk.default.gray("VAL_PNL"),
4192
- import_chalk.default.gray("TEST_N"),
4193
- import_chalk.default.gray("TEST_EVENTS"),
4194
- import_chalk.default.gray("TEST_PF"),
4195
- import_chalk.default.gray("TEST_PNL"),
4196
- import_chalk.default.gray("TEST_OBJ"),
4197
- import_chalk.default.gray("TR/D"),
4198
- import_chalk.default.gray("LOSS_M"),
4199
- import_chalk.default.gray("SCORE"),
4200
- import_chalk.default.gray("READINESS"),
4201
- import_chalk.default.gray("REASONS"),
4202
- import_chalk.default.gray("POCKET")
4203
- ],
4204
- buildPocketRows(search.negativePockets)
4205
- )
4206
- );
4207
- for (const cohort of coverageSearches) {
4208
- const title = cohort.family === "cmc" ? "CMC" : "COINALYZE";
4209
- printSection(
4210
- `${title} COVERAGE COHORT`,
4211
- createTable(
4212
- [import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
4213
- [
4214
- ["coverage_rows", import_chalk.default.cyan(String(cohort.coverage.rows))],
4215
- ["coverage_ratio", import_chalk.default.cyan(formatRatio(cohort.coverage.rowRatio))],
4216
- ["coverage_events", import_chalk.default.cyan(String(cohort.coverage.events))],
4217
- ["event_ratio", import_chalk.default.cyan(formatRatio(cohort.coverage.eventRatio))],
4218
- [
4219
- "coverage_from",
4220
- cohort.coverage.minTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(
4221
- new Date(cohort.coverage.minTimestamp).toISOString()
4222
- )
4223
- ],
4224
- [
4225
- "coverage_to",
4226
- cohort.coverage.maxTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(
4227
- new Date(cohort.coverage.maxTimestamp).toISOString()
4228
- )
4229
- ],
4230
- ["scope_rows", import_chalk.default.cyan(String(cohort.scopeRows))],
4231
- ["train_rows", import_chalk.default.cyan(String(cohort.trainRows))],
4232
- ["validation_rows", import_chalk.default.cyan(String(cohort.validationRows))],
4233
- ["test_rows", import_chalk.default.cyan(String(cohort.testRows))],
4234
- [
4235
- "low_cadence",
4236
- cohort.search.stats.cadence.lowCadence ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
4237
- ],
4238
- [
4239
- "sparse_sample",
4240
- cohort.search.stats.cadence.sparseSample ? import_chalk.default.yellow("yes") : import_chalk.default.green("no")
4241
- ],
4242
- [
4243
- "adaptive_thresholds",
4244
- cohort.search.stats.cadence.adaptiveThresholds ? import_chalk.default.yellow("on") : import_chalk.default.gray("off")
4245
- ],
4246
- [
4247
- "train_events",
4248
- import_chalk.default.cyan(String(cohort.search.stats.cadence.trainEvents))
4249
- ],
4250
- [
4251
- "effective_min_support",
4252
- import_chalk.default.cyan(String(cohort.search.stats.cadence.minSupport))
4253
- ],
4254
- [
4255
- "effective_min_events",
4256
- import_chalk.default.cyan(String(cohort.search.stats.cadence.minEvents))
4257
- ]
4258
- ]
4259
- )
4260
- );
4261
- printSection(
4262
- `${title} TRAIN BASELINE`,
4263
- createTable(
4264
- [import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
4265
- buildSummaryRows(cohort.search.baseline)
4266
- )
4267
- );
4268
- printSection(
4269
- `${title} TOP POSITIVE POCKETS`,
4270
- createTable(
4271
- [
4272
- import_chalk.default.gray("#"),
4273
- import_chalk.default.gray("N"),
4274
- import_chalk.default.gray("EVENTS"),
4275
- import_chalk.default.gray("MAX_B"),
4276
- import_chalk.default.gray("SUP"),
4277
- import_chalk.default.gray("WR"),
4278
- import_chalk.default.gray("PF"),
4279
- import_chalk.default.gray("PNL"),
4280
- import_chalk.default.gray("OBJ_PNL"),
4281
- import_chalk.default.gray("MAX_DD"),
4282
- import_chalk.default.gray("VAL_N"),
4283
- import_chalk.default.gray("VAL_EVENTS"),
4284
- import_chalk.default.gray("VAL_WR"),
4285
- import_chalk.default.gray("VAL_PF"),
4286
- import_chalk.default.gray("VAL_PNL"),
4287
- import_chalk.default.gray("TEST_N"),
4288
- import_chalk.default.gray("TEST_EVENTS"),
4289
- import_chalk.default.gray("TEST_PF"),
4290
- import_chalk.default.gray("TEST_PNL"),
4291
- import_chalk.default.gray("TEST_OBJ"),
4292
- import_chalk.default.gray("TR/D"),
4293
- import_chalk.default.gray("LOSS_M"),
4294
- import_chalk.default.gray("SCORE"),
4295
- import_chalk.default.gray("READINESS"),
4296
- import_chalk.default.gray("REASONS"),
4297
- import_chalk.default.gray("POCKET")
4298
- ],
4299
- buildPocketRows(cohort.search.positivePockets)
4300
- )
4301
- );
4302
- printSection(
4303
- `${title} TOP LOSS POCKETS`,
4304
- createTable(
4305
- [
4306
- import_chalk.default.gray("#"),
4307
- import_chalk.default.gray("N"),
4308
- import_chalk.default.gray("EVENTS"),
4309
- import_chalk.default.gray("MAX_B"),
4310
- import_chalk.default.gray("SUP"),
4311
- import_chalk.default.gray("WR"),
4312
- import_chalk.default.gray("PF"),
4313
- import_chalk.default.gray("PNL"),
4314
- import_chalk.default.gray("OBJ_PNL"),
4315
- import_chalk.default.gray("MAX_DD"),
4316
- import_chalk.default.gray("VAL_N"),
4317
- import_chalk.default.gray("VAL_EVENTS"),
4318
- import_chalk.default.gray("VAL_WR"),
4319
- import_chalk.default.gray("VAL_PF"),
4320
- import_chalk.default.gray("VAL_PNL"),
4321
- import_chalk.default.gray("TEST_N"),
4322
- import_chalk.default.gray("TEST_EVENTS"),
4323
- import_chalk.default.gray("TEST_PF"),
4324
- import_chalk.default.gray("TEST_PNL"),
4325
- import_chalk.default.gray("TEST_OBJ"),
4326
- import_chalk.default.gray("TR/D"),
4327
- import_chalk.default.gray("LOSS_M"),
4328
- import_chalk.default.gray("SCORE"),
4329
- import_chalk.default.gray("READINESS"),
4330
- import_chalk.default.gray("REASONS"),
4331
- import_chalk.default.gray("POCKET")
4332
- ],
4333
- buildPocketRows(cohort.search.negativePockets)
4334
- )
4335
- );
3661
+ return true;
3662
+ };
3663
+ var prepareAiPocketSearchDataset = async (options) => {
3664
+ await (0, import_registry.ensureStrategyPluginsLoaded)();
3665
+ const filePaths = await resolveMergedDatasetFiles({
3666
+ outDir: options.outDir,
3667
+ strategyName: options.strategyName,
3668
+ explicitFile: options.explicitFile
3669
+ });
3670
+ const { totalRows, selectedRows } = await (0, import_ai2.countAiDatasetRows)({
3671
+ filePaths,
3672
+ limitFromEnd: options.recent,
3673
+ skipFromEnd: options.skip
3674
+ });
3675
+ const maxSelectedTimestamp = options.trailingPeriodMs == null ? null : await findMaxSelectedTimestamp({
3676
+ filePaths,
3677
+ recent: options.recent,
3678
+ skip: options.skip
3679
+ });
3680
+ const sinceTimestamp = options.trailingPeriodMs != null && maxSelectedTimestamp != null ? maxSelectedTimestamp - options.trailingPeriodMs : options.sinceInput;
3681
+ return {
3682
+ filePaths,
3683
+ totalRows,
3684
+ selectedRows,
3685
+ sinceTimestamp,
3686
+ resolvedStrategyName: deriveStrategyNameFromFile(filePaths[0] || "")
3687
+ };
3688
+ };
3689
+ var evaluateAiPocketSearchDataset = async ({
3690
+ prepared,
3691
+ options,
3692
+ onProgress
3693
+ }) => {
3694
+ let resolvedStrategyName = prepared.resolvedStrategyName;
3695
+ let scanned = 0;
3696
+ let dateSkipped = 0;
3697
+ let failed = 0;
3698
+ const errors = [];
3699
+ const rows = [];
3700
+ const excludedFeaturePaths = /* @__PURE__ */ new Map();
3701
+ await (0, import_ai2.streamAiDatasetRows)({
3702
+ filePaths: prepared.filePaths,
3703
+ limitFromEnd: options.recent,
3704
+ skipFromEnd: options.skip,
3705
+ onRow: async (row) => {
3706
+ scanned += 1;
3707
+ if (resolvedStrategyName === "unknown" && typeof row.strategyName === "string" && row.strategyName.trim()) {
3708
+ resolvedStrategyName = row.strategyName.trim();
3709
+ }
3710
+ const timestamp = getDatasetRowTimestamp(row);
3711
+ if (prepared.sinceTimestamp != null && (timestamp == null || timestamp < prepared.sinceTimestamp) || options.untilTimestamp != null && (timestamp == null || timestamp > options.untilTimestamp)) {
3712
+ dateSkipped += 1;
3713
+ onProgress?.({ symbol: row.symbol, status: "date-skip" });
3714
+ return;
3715
+ }
3716
+ try {
3717
+ const signal = extractSignalFromAiDatasetRow(row);
3718
+ const payload = (0, import_ai3.buildAiPayload)(signal);
3719
+ const gateContext = (0, import_ai3.getDeterministicAiGateContext)(payload);
3720
+ const analysis = await (0, import_ai3.runAiPromptLocal)(signal, { payload });
3721
+ const quality = normalizeQuality(analysis);
3722
+ const modelDirection = typeof analysis.direction === "string" && analysis.direction.trim() ? analysis.direction : null;
3723
+ const modelDirectionMatches = modelDirection === row.direction;
3724
+ const profit = Number(row.profit);
3725
+ const featureSnapshot = collectAiPocketFeatureSnapshot({
3726
+ payload,
3727
+ gateContext,
3728
+ includeSymbol: options.includeSymbol,
3729
+ includeGateContext: options.includeGateContext,
3730
+ featureProfile: options.featureProfile,
3731
+ featurePolicy: options.featurePolicy,
3732
+ onFeatureExcluded: ({ path: featurePath, classification }) => {
3733
+ const paths = excludedFeaturePaths.get(classification) ?? /* @__PURE__ */ new Set();
3734
+ paths.add(featurePath);
3735
+ excludedFeaturePaths.set(classification, paths);
3736
+ }
3737
+ });
3738
+ rows.push({
3739
+ signalId: row.signalId,
3740
+ strategy: row.strategyName,
3741
+ symbol: row.symbol,
3742
+ direction: row.direction,
3743
+ timestamp,
3744
+ profit,
3745
+ profitableTrade: profit > 0,
3746
+ aiApproved: isAiApproval(row, analysis, options.minQuality),
3747
+ quality,
3748
+ modelDirectionMatches,
3749
+ modelCandidate: getDeterministicModelCandidate(signal),
3750
+ features: featureSnapshot.features,
3751
+ featureCoverage: featureSnapshot.featureCoverage
3752
+ });
3753
+ onProgress?.({ symbol: row.symbol, status: "ok" });
3754
+ } catch (error) {
3755
+ failed += 1;
3756
+ if (errors.length < 5) {
3757
+ errors.push(
3758
+ `[${row.symbol}/${row.signalId}] ${error instanceof Error ? error.message : String(error)}`
3759
+ );
3760
+ }
3761
+ onProgress?.({ symbol: row.symbol, status: "error" });
3762
+ }
3763
+ }
3764
+ });
3765
+ return {
3766
+ rows,
3767
+ resolvedStrategyName,
3768
+ scanned,
3769
+ dateSkipped,
3770
+ failed,
3771
+ errors,
3772
+ excludedFeaturePaths
3773
+ };
3774
+ };
3775
+
3776
+ // src/lib/aiTrainMetrics.ts
3777
+ var divideOrNull = (num, denom) => {
3778
+ if (denom <= 0) {
3779
+ return null;
3780
+ }
3781
+ return num / denom;
3782
+ };
3783
+ var DAY_MS3 = 24 * 60 * 60 * 1e3;
3784
+ var DAYS_PER_WEEK2 = 7;
3785
+ var DAYS_PER_MONTH2 = 30.4375;
3786
+ var DAYS_PER_YEAR = 365;
3787
+ var getEvaluationPeriodDays = (evaluations) => {
3788
+ let minTimestamp = null;
3789
+ let maxTimestamp = null;
3790
+ for (const evaluation of evaluations) {
3791
+ const timestamp = typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null;
3792
+ if (timestamp == null) {
3793
+ continue;
3794
+ }
3795
+ if (minTimestamp == null || timestamp < minTimestamp) {
3796
+ minTimestamp = timestamp;
3797
+ }
3798
+ if (maxTimestamp == null || timestamp > maxTimestamp) {
3799
+ maxTimestamp = timestamp;
3800
+ }
3801
+ }
3802
+ if (minTimestamp == null || maxTimestamp == null) {
3803
+ return null;
3804
+ }
3805
+ return Math.max((maxTimestamp - minTimestamp) / DAY_MS3, 1);
3806
+ };
3807
+ var qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
3808
+ var calculateTradePnlRiskRatios = ({
3809
+ profits,
3810
+ totalProfit,
3811
+ maxDrawdown,
3812
+ periodDays
3813
+ }) => {
3814
+ if (!profits.length || periodDays == null || periodDays <= 0) {
3815
+ return {
3816
+ sharpeRatio: null,
3817
+ sortinoRatio: null,
3818
+ calmarRatio: null
3819
+ };
3820
+ }
3821
+ const meanProfit = totalProfit / profits.length;
3822
+ const variance = profits.reduce((sum, profit) => {
3823
+ const diff = profit - meanProfit;
3824
+ return sum + diff * diff;
3825
+ }, 0) / profits.length;
3826
+ const stdDev = Math.sqrt(variance);
3827
+ const downsideDeviation = Math.sqrt(
3828
+ profits.reduce(
3829
+ (sum, profit) => profit < 0 ? sum + profit * profit : sum,
3830
+ 0
3831
+ ) / profits.length
3832
+ );
3833
+ const annualizationScale = Math.sqrt(
3834
+ profits.length / periodDays * DAYS_PER_YEAR
3835
+ );
3836
+ return {
3837
+ sharpeRatio: stdDev > 0 && annualizationScale > 0 ? meanProfit / stdDev * annualizationScale : null,
3838
+ sortinoRatio: downsideDeviation > 0 && annualizationScale > 0 ? meanProfit / downsideDeviation * annualizationScale : null,
3839
+ calmarRatio: maxDrawdown > 0 ? totalProfit / periodDays * DAYS_PER_YEAR / maxDrawdown : null
3840
+ };
3841
+ };
3842
+ var emptyRiskSummary = () => ({
3843
+ trades: 0,
3844
+ totalProfit: 0,
3845
+ grossProfit: 0,
3846
+ grossLoss: 0,
3847
+ profitFactor: null,
3848
+ payoffRatio: null,
3849
+ avgWin: null,
3850
+ avgLoss: null,
3851
+ largestWin: null,
3852
+ largestLoss: null,
3853
+ winRate: null,
3854
+ maxDrawdown: 0,
3855
+ maxDrawdownPctOfGrossProfit: null,
3856
+ maxDrawdownPctOfTotalProfit: null,
3857
+ sharpeRatio: null,
3858
+ sortinoRatio: null,
3859
+ calmarRatio: null,
3860
+ recoveryFactor: null,
3861
+ ulcerIndex: null,
3862
+ maxConsecutiveWins: 0,
3863
+ maxConsecutiveLosses: 0
3864
+ });
3865
+ var summarizeApprovedRisk = (evaluations, periodDays = getEvaluationPeriodDays(evaluations)) => {
3866
+ const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
3867
+ const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
3868
+ const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
3869
+ return leftTimestamp - rightTimestamp;
3870
+ });
3871
+ if (!approvedEvaluations.length) {
3872
+ return emptyRiskSummary();
3873
+ }
3874
+ let grossProfit = 0;
3875
+ let grossLoss = 0;
3876
+ let wins = 0;
3877
+ let losses = 0;
3878
+ let largestWin = null;
3879
+ let largestLoss = null;
3880
+ let equity = 0;
3881
+ let peakEquity = 0;
3882
+ let maxDrawdown = 0;
3883
+ let drawdownSquares = 0;
3884
+ let currentWinStreak = 0;
3885
+ let currentLossStreak = 0;
3886
+ let maxConsecutiveWins = 0;
3887
+ let maxConsecutiveLosses = 0;
3888
+ const approvedProfits = [];
3889
+ for (const evaluation of approvedEvaluations) {
3890
+ const profit = evaluation.profit;
3891
+ approvedProfits.push(profit);
3892
+ if (profit > 0) {
3893
+ grossProfit += profit;
3894
+ wins += 1;
3895
+ largestWin = largestWin == null ? profit : Math.max(largestWin, profit);
3896
+ currentWinStreak += 1;
3897
+ currentLossStreak = 0;
3898
+ } else if (profit < 0) {
3899
+ grossLoss += Math.abs(profit);
3900
+ losses += 1;
3901
+ largestLoss = largestLoss == null ? profit : Math.min(largestLoss, profit);
3902
+ currentLossStreak += 1;
3903
+ currentWinStreak = 0;
3904
+ } else {
3905
+ currentWinStreak = 0;
3906
+ currentLossStreak = 0;
3907
+ }
3908
+ maxConsecutiveWins = Math.max(maxConsecutiveWins, currentWinStreak);
3909
+ maxConsecutiveLosses = Math.max(maxConsecutiveLosses, currentLossStreak);
3910
+ equity += profit;
3911
+ peakEquity = Math.max(peakEquity, equity);
3912
+ const drawdown = Math.max(0, peakEquity - equity);
3913
+ maxDrawdown = Math.max(maxDrawdown, drawdown);
3914
+ drawdownSquares += drawdown * drawdown;
3915
+ }
3916
+ const totalProfit = grossProfit - grossLoss;
3917
+ const avgWin = divideOrNull(grossProfit, wins);
3918
+ const avgLoss = divideOrNull(grossLoss, losses);
3919
+ const riskRatios = calculateTradePnlRiskRatios({
3920
+ profits: approvedProfits,
3921
+ totalProfit,
3922
+ maxDrawdown,
3923
+ periodDays
3924
+ });
3925
+ return {
3926
+ trades: approvedEvaluations.length,
3927
+ totalProfit,
3928
+ grossProfit,
3929
+ grossLoss,
3930
+ profitFactor: grossLoss > 0 ? grossProfit / grossLoss : null,
3931
+ payoffRatio: avgWin != null && avgLoss != null && avgLoss > 0 ? avgWin / avgLoss : null,
3932
+ avgWin,
3933
+ avgLoss,
3934
+ largestWin,
3935
+ largestLoss,
3936
+ winRate: divideOrNull(wins, approvedEvaluations.length),
3937
+ maxDrawdown,
3938
+ maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
3939
+ maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
3940
+ ...riskRatios,
3941
+ recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
3942
+ ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
3943
+ maxConsecutiveWins,
3944
+ maxConsecutiveLosses
3945
+ };
3946
+ };
3947
+ var summarizeAiTrainEvaluations = (evaluations) => {
3948
+ let approved = 0;
3949
+ let rejected = 0;
3950
+ let correct = 0;
3951
+ let incorrect = 0;
3952
+ let profitable = 0;
3953
+ let unprofitable = 0;
3954
+ let flat = 0;
3955
+ let truePositive = 0;
3956
+ let falsePositive = 0;
3957
+ let trueNegative = 0;
3958
+ let falseNegative = 0;
3959
+ let totalProfitAll = 0;
3960
+ let totalProfitApproved = 0;
3961
+ const bucketMap = /* @__PURE__ */ new Map();
3962
+ for (const evaluation of evaluations) {
3963
+ const { profit, profitableTrade, aiApproved, quality } = evaluation;
3964
+ totalProfitAll += profit;
3965
+ if (profitableTrade) {
3966
+ profitable += 1;
3967
+ } else if (profit < 0) {
3968
+ unprofitable += 1;
3969
+ } else {
3970
+ flat += 1;
3971
+ }
3972
+ if (aiApproved) {
3973
+ approved += 1;
3974
+ totalProfitApproved += profit;
3975
+ } else {
3976
+ rejected += 1;
3977
+ }
3978
+ const isCorrect = aiApproved === profitableTrade;
3979
+ if (isCorrect) {
3980
+ correct += 1;
3981
+ } else {
3982
+ incorrect += 1;
3983
+ }
3984
+ if (aiApproved && profitableTrade) {
3985
+ truePositive += 1;
3986
+ } else if (aiApproved && !profitableTrade) {
3987
+ falsePositive += 1;
3988
+ } else if (!aiApproved && profitableTrade) {
3989
+ falseNegative += 1;
3990
+ } else {
3991
+ trueNegative += 1;
3992
+ }
3993
+ const bucket = bucketMap.get(quality) ?? {
3994
+ quality,
3995
+ count: 0,
3996
+ approved: 0,
3997
+ profitable: 0,
3998
+ totalProfit: 0
3999
+ };
4000
+ bucket.count += 1;
4001
+ bucket.totalProfit += profit;
4002
+ if (aiApproved) {
4003
+ bucket.approved += 1;
4004
+ }
4005
+ if (profitableTrade) {
4006
+ bucket.profitable += 1;
4007
+ }
4008
+ bucketMap.set(quality, bucket);
4009
+ }
4010
+ const avgProfitAll = divideOrNull(totalProfitAll, evaluations.length);
4011
+ const avgProfitApproved = divideOrNull(totalProfitApproved, approved);
4012
+ const periodDays = getEvaluationPeriodDays(evaluations);
4013
+ const avgProfitApprovedPerDay = periodDays == null ? null : divideOrNull(totalProfitApproved, periodDays);
4014
+ const avgApprovedTradesPerDay = periodDays == null ? null : divideOrNull(approved, periodDays);
4015
+ const avgProfitApprovedPerMonth = avgProfitApprovedPerDay == null ? null : avgProfitApprovedPerDay * DAYS_PER_MONTH2;
4016
+ const avgApprovedTradesPerWeek = avgApprovedTradesPerDay == null ? null : avgApprovedTradesPerDay * DAYS_PER_WEEK2;
4017
+ const expectancyDelta = avgProfitAll == null || avgProfitApproved == null ? null : avgProfitApproved - avgProfitAll;
4018
+ return {
4019
+ approved,
4020
+ rejected,
4021
+ correct,
4022
+ incorrect,
4023
+ truePositive,
4024
+ falsePositive,
4025
+ trueNegative,
4026
+ falseNegative,
4027
+ profitable,
4028
+ unprofitable,
4029
+ flat,
4030
+ precisionApproved: divideOrNull(truePositive, approved),
4031
+ recallWinners: divideOrNull(truePositive, profitable),
4032
+ avgProfitAll,
4033
+ avgProfitApproved,
4034
+ avgProfitApprovedPerDay,
4035
+ avgProfitApprovedPerMonth,
4036
+ avgApprovedTradesPerDay,
4037
+ avgApprovedTradesPerWeek,
4038
+ expectancyDelta,
4039
+ approvedRisk: summarizeApprovedRisk(evaluations, periodDays),
4040
+ qualityBuckets: [...bucketMap.values()].sort(
4041
+ (a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
4042
+ )
4043
+ };
4044
+ };
4045
+ var isDirectionMatchAtThreshold = (evaluation, threshold) => {
4046
+ const directionMatches = typeof evaluation.modelDirectionMatches === "boolean" ? evaluation.modelDirectionMatches : evaluation.aiApproved;
4047
+ return directionMatches === true && evaluation.quality != null && evaluation.quality >= threshold;
4048
+ };
4049
+ var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [3, 4, 5]) => thresholds.map((threshold) => ({
4050
+ threshold,
4051
+ label: `q${threshold}+`,
4052
+ summary: summarizeAiTrainEvaluations(
4053
+ evaluations.map((evaluation) => ({
4054
+ ...evaluation,
4055
+ aiApproved: isDirectionMatchAtThreshold(evaluation, threshold)
4056
+ }))
4057
+ )
4058
+ }));
4059
+
4060
+ // src/lib/aiPocketSearch/research.ts
4061
+ var resolveScopeRows = (rows, scope) => {
4062
+ if (scope === "approved") {
4063
+ return rows.filter((row) => row.aiApproved);
4336
4064
  }
4337
- if (errors.length) {
4338
- console.log(import_chalk.default.yellow("Errors:"));
4339
- errors.forEach((error) => console.log(import_chalk.default.yellow(`- ${error}`)));
4065
+ if (scope === "rejected") {
4066
+ return rows.filter((row) => !row.aiApproved);
4067
+ }
4068
+ if (scope === "candidates") {
4069
+ return rows.filter((row) => row.modelCandidate && !row.aiApproved);
4070
+ }
4071
+ return rows;
4072
+ };
4073
+ var runAiPocketSearchResearch = ({
4074
+ rows,
4075
+ options,
4076
+ onProgress
4077
+ }) => {
4078
+ const {
4079
+ direction,
4080
+ scope,
4081
+ qualityThresholds,
4082
+ validationSplit,
4083
+ testSplit,
4084
+ sealTest,
4085
+ cadenceMode,
4086
+ explicitMinSupport,
4087
+ explicitMinEvents,
4088
+ explicitMinValidationSupport,
4089
+ explicitMinValidationEvents,
4090
+ maxEventCountShare,
4091
+ explicitMaxEventCountShare,
4092
+ minProfitFactor,
4093
+ minTotalProfit,
4094
+ minWinRate,
4095
+ maxDepth,
4096
+ maxAtomicPredicates,
4097
+ maxCombinations,
4098
+ top,
4099
+ maxBatch,
4100
+ maxSymbolCountShare,
4101
+ objective,
4102
+ allowRiskRegression,
4103
+ requireValidationEligibility,
4104
+ dedupeEquivalentSelections,
4105
+ coverageMode
4106
+ } = options;
4107
+ const directionRows = direction ? rows.filter((row) => row.direction === direction) : rows;
4108
+ const scopeRows = resolveScopeRows(directionRows, scope);
4109
+ const fullSplit = splitAiPocketResearchRowsByTimestamp(
4110
+ directionRows,
4111
+ validationSplit,
4112
+ testSplit
4113
+ );
4114
+ const sealedFullSplit = sealAiPocketTestPartition(fullSplit, sealTest);
4115
+ const discoveryRows = sealedFullSplit.discoveryRows;
4116
+ const trainRows = resolveScopeRows(fullSplit.trainRows, scope);
4117
+ const validationRows = resolveScopeRows(fullSplit.validationRows, scope);
4118
+ const testRows = resolveScopeRows(sealedFullSplit.searchTestRows, scope);
4119
+ const baselineRows = fullSplit.trainRows.filter((row) => row.aiApproved);
4120
+ const validationBaselineRows = fullSplit.validationRows.filter(
4121
+ (row) => row.aiApproved
4122
+ );
4123
+ const testBaselineRows = sealedFullSplit.searchTestRows.filter(
4124
+ (row) => row.aiApproved
4125
+ );
4126
+ const currentGateSummary = summarizeAiTrainEvaluations(discoveryRows);
4127
+ const currentGateQualityThresholds = summarizeAiTrainEvaluationsByQualityThreshold(
4128
+ discoveryRows,
4129
+ qualityThresholds
4130
+ );
4131
+ const runSearch = ({
4132
+ label,
4133
+ train,
4134
+ validation,
4135
+ test,
4136
+ trainBaseline,
4137
+ validationBaseline,
4138
+ testBaseline,
4139
+ requiredFeatureFamilies = [],
4140
+ excludedFeatureFamilies = []
4141
+ }) => {
4142
+ const cadenceProfile = resolveAiPocketCadenceProfile({
4143
+ trainRows: train,
4144
+ validationRows: validation,
4145
+ testRows: test,
4146
+ mode: cadenceMode,
4147
+ validationSplit,
4148
+ ...explicitMinSupport != null ? { minSupport: explicitMinSupport } : {},
4149
+ ...explicitMinEvents > 0 ? { minEvents: explicitMinEvents } : {},
4150
+ ...explicitMinValidationSupport > 0 ? { minValidationSupport: explicitMinValidationSupport } : {},
4151
+ ...explicitMinValidationEvents > 0 ? { minValidationEvents: explicitMinValidationEvents } : {},
4152
+ maxEventCountShare,
4153
+ explicitMaxEventCountShare
4154
+ });
4155
+ return searchAiPockets(train, {
4156
+ minSupport: cadenceProfile.minSupport,
4157
+ minProfitFactor,
4158
+ minTotalProfit,
4159
+ minWinRate,
4160
+ maxDepth,
4161
+ maxAtomicPredicates,
4162
+ maxCombinations,
4163
+ top,
4164
+ validationRows: validation,
4165
+ testRows: test,
4166
+ minValidationSupport: cadenceProfile.minValidationSupport,
4167
+ minEvents: cadenceProfile.minEvents,
4168
+ minValidationEvents: cadenceProfile.minValidationEvents,
4169
+ ...maxBatch > 0 ? { maxBatch } : {},
4170
+ maxEventCountShare: cadenceProfile.maxEventCountShare,
4171
+ maxSymbolCountShare,
4172
+ objective,
4173
+ baselineRows: trainBaseline,
4174
+ validationBaselineRows: validationBaseline,
4175
+ testBaselineRows: testBaseline,
4176
+ allowRiskRegression,
4177
+ requireValidationEligibility,
4178
+ dedupeEquivalentSelections,
4179
+ requiredFeatureFamilies,
4180
+ excludedFeatureFamilies,
4181
+ cadenceProfile,
4182
+ progressInterval: 250,
4183
+ onProgress: (progress) => onProgress?.({ label, progress })
4184
+ });
4185
+ };
4186
+ const search = runSearch({
4187
+ label: "full",
4188
+ train: trainRows,
4189
+ validation: validationRows,
4190
+ test: testRows,
4191
+ trainBaseline: baselineRows,
4192
+ validationBaseline: validationBaselineRows,
4193
+ testBaseline: testBaselineRows,
4194
+ ...coverageMode === "auto" ? {
4195
+ excludedFeatureFamilies: [
4196
+ "cmc",
4197
+ "coinalyze"
4198
+ ]
4199
+ } : {}
4200
+ });
4201
+ const coverageSearches = [];
4202
+ if (coverageMode === "auto") {
4203
+ for (const family of ["cmc", "coinalyze"]) {
4204
+ const cohortSplit = splitAiPocketCoverageRowsByTimestamp(
4205
+ directionRows,
4206
+ family,
4207
+ validationSplit,
4208
+ testSplit
4209
+ );
4210
+ const sealedCohortSplit = sealAiPocketTestPartition(
4211
+ cohortSplit,
4212
+ sealTest
4213
+ );
4214
+ const cohortTrainRows = resolveScopeRows(cohortSplit.trainRows, scope);
4215
+ const cohortValidationRows = resolveScopeRows(
4216
+ cohortSplit.validationRows,
4217
+ scope
4218
+ );
4219
+ const cohortTestRows = resolveScopeRows(
4220
+ sealedCohortSplit.searchTestRows,
4221
+ scope
4222
+ );
4223
+ const cohortSearch = runSearch({
4224
+ label: family,
4225
+ train: cohortTrainRows,
4226
+ validation: cohortValidationRows,
4227
+ test: cohortTestRows,
4228
+ trainBaseline: cohortSplit.trainRows.filter((row) => row.aiApproved),
4229
+ validationBaseline: cohortSplit.validationRows.filter(
4230
+ (row) => row.aiApproved
4231
+ ),
4232
+ testBaseline: sealedCohortSplit.searchTestRows.filter(
4233
+ (row) => row.aiApproved
4234
+ ),
4235
+ requiredFeatureFamilies: [family]
4236
+ });
4237
+ coverageSearches.push({
4238
+ family,
4239
+ coverage: summarizeAiPocketFeatureCoverage(
4240
+ sealedCohortSplit.discoveryRows,
4241
+ family
4242
+ ),
4243
+ scopeRows: resolveScopeRows(
4244
+ sealedCohortSplit.discoveryRows.filter(
4245
+ (row) => row.featureCoverage?.[family] === true
4246
+ ),
4247
+ scope
4248
+ ).length,
4249
+ trainRows: cohortTrainRows.length,
4250
+ validationRows: cohortValidationRows.length,
4251
+ testRows: cohortTestRows.length,
4252
+ search: cohortSearch
4253
+ });
4254
+ }
4255
+ }
4256
+ return {
4257
+ scopeRows,
4258
+ trainRows,
4259
+ validationRows,
4260
+ testRows,
4261
+ sealedTest: sealedFullSplit.evidence,
4262
+ currentGateSummary,
4263
+ currentGateQualityThresholds,
4264
+ search,
4265
+ coverageSearches
4266
+ };
4267
+ };
4268
+
4269
+ // src/lib/aiPocketSearch/command.ts
4270
+ var runAiPocketSearchCommand = async ({
4271
+ flags: flags2
4272
+ }) => {
4273
+ const options = resolveAiPocketSearchCommandOptions({
4274
+ flags: flags2,
4275
+ argv: process.argv
4276
+ });
4277
+ const {
4278
+ skip,
4279
+ minQuality,
4280
+ untilTimestamp,
4281
+ periodLabel,
4282
+ recent,
4283
+ qualityThresholds,
4284
+ scope,
4285
+ direction,
4286
+ maxDepth,
4287
+ minProfitFactor,
4288
+ minWinRate,
4289
+ minTotalProfit,
4290
+ maxAtomicPredicates,
4291
+ maxCombinations,
4292
+ validationSplit,
4293
+ testSplit,
4294
+ maxBatch,
4295
+ maxSymbolCountShare,
4296
+ objective,
4297
+ allowRiskRegression,
4298
+ requireValidationEligibility,
4299
+ dedupeEquivalentSelections,
4300
+ top,
4301
+ includeSymbol,
4302
+ includeGateContext,
4303
+ featureProfile,
4304
+ featurePolicy,
4305
+ coverageMode,
4306
+ cadenceMode,
4307
+ jsonOutput,
4308
+ outputPath,
4309
+ reportDir,
4310
+ explicitReportFile
4311
+ } = options;
4312
+ const generatedAt = Date.now();
4313
+ const preparedDataset = await prepareAiPocketSearchDataset(options);
4314
+ const { filePaths, totalRows, selectedRows, sinceTimestamp } = preparedDataset;
4315
+ if (!selectedRows) {
4316
+ presentEmptyAiPocketSearchDataset({ filePaths, recent, skip });
4317
+ process.exit(0);
4318
+ }
4319
+ let resolvedStrategyName = preparedDataset.resolvedStrategyName;
4320
+ const reportPath = await writePendingAiPocketSearchReport({
4321
+ explicitReportFile,
4322
+ reportDir,
4323
+ strategyName: resolvedStrategyName,
4324
+ filePaths,
4325
+ scope,
4326
+ generatedAt,
4327
+ selectedRows
4328
+ });
4329
+ presentPendingAiPocketSearchReport(reportPath);
4330
+ const evaluatedDataset = await evaluateAiPocketSearchDataset({
4331
+ prepared: preparedDataset,
4332
+ options,
4333
+ onProgress: createAiPocketSearchDatasetProgressPresenter(selectedRows)
4334
+ });
4335
+ const { rows, scanned, dateSkipped, failed, errors, excludedFeaturePaths } = evaluatedDataset;
4336
+ resolvedStrategyName = evaluatedDataset.resolvedStrategyName;
4337
+ const research = runAiPocketSearchResearch({
4338
+ rows,
4339
+ options,
4340
+ onProgress: createAiPocketSearchResearchProgressPresenter()
4341
+ });
4342
+ const {
4343
+ scopeRows,
4344
+ trainRows,
4345
+ validationRows,
4346
+ testRows,
4347
+ sealedTest,
4348
+ currentGateSummary,
4349
+ currentGateQualityThresholds,
4350
+ search,
4351
+ coverageSearches
4352
+ } = research;
4353
+ const result = {
4354
+ generatedAt,
4355
+ run: {
4356
+ strategy: resolvedStrategyName,
4357
+ filePaths,
4358
+ sourceRows: totalRows,
4359
+ selectedRows,
4360
+ evaluatedRows: rows.length,
4361
+ scope,
4362
+ direction: direction || null,
4363
+ scopeRows: scopeRows.length,
4364
+ trainRows: trainRows.length,
4365
+ validationRows: validationRows.length,
4366
+ testRows: testRows.length,
4367
+ scanned,
4368
+ dateSkipped,
4369
+ failed,
4370
+ recent,
4371
+ skip,
4372
+ since: sinceTimestamp,
4373
+ until: untilTimestamp,
4374
+ period: periodLabel,
4375
+ minQuality,
4376
+ qualityThresholds,
4377
+ includeSymbol,
4378
+ includeGateContext,
4379
+ featureProfile,
4380
+ featurePolicy,
4381
+ coverageMode,
4382
+ cadenceMode,
4383
+ coverageSearches: coverageSearches.map(
4384
+ ({
4385
+ family,
4386
+ coverage,
4387
+ scopeRows: scopeRows2,
4388
+ trainRows: trainRows2,
4389
+ validationRows: validationRows2,
4390
+ testRows: testRows2
4391
+ }) => ({
4392
+ family,
4393
+ coverage,
4394
+ scopeRows: scopeRows2,
4395
+ trainRows: trainRows2,
4396
+ validationRows: validationRows2,
4397
+ testRows: testRows2
4398
+ })
4399
+ ),
4400
+ featurePolicyAudit: Object.fromEntries(
4401
+ [...excludedFeaturePaths.entries()].map(([classification, paths]) => [
4402
+ classification,
4403
+ {
4404
+ paths: paths.size,
4405
+ samples: [...paths].sort().slice(0, 5)
4406
+ }
4407
+ ])
4408
+ ),
4409
+ objective,
4410
+ validationSplit,
4411
+ testSplit,
4412
+ sealedTest,
4413
+ minValidationSupport: search.stats.cadence.minValidationSupport,
4414
+ reportPath,
4415
+ search: {
4416
+ maxDepth,
4417
+ minSupport: search.stats.cadence.minSupport,
4418
+ minProfitFactor,
4419
+ minWinRate,
4420
+ minTotalProfit,
4421
+ maxAtomicPredicates,
4422
+ maxCombinations,
4423
+ minEvents: search.stats.cadence.minEvents,
4424
+ minValidationEvents: search.stats.cadence.minValidationEvents,
4425
+ ...maxBatch > 0 ? { maxBatch } : {},
4426
+ maxEventCountShare: search.stats.cadence.maxEventCountShare,
4427
+ maxSymbolCountShare,
4428
+ allowRiskRegression,
4429
+ requireValidationEligibility,
4430
+ cadence: search.stats.cadence,
4431
+ validationSplit,
4432
+ testSplit,
4433
+ dedupeEquivalentSelections,
4434
+ top
4435
+ }
4436
+ },
4437
+ currentGate: {
4438
+ summary: currentGateSummary,
4439
+ qualityThresholds: currentGateQualityThresholds
4440
+ },
4441
+ pocketSearch: search,
4442
+ coverageSearches,
4443
+ errors
4444
+ };
4445
+ await writeAiPocketSearchArtifacts({ result, outputPath });
4446
+ if (jsonOutput) {
4447
+ console.log(JSON.stringify(result));
4448
+ process.exit(failed === selectedRows ? 1 : 0);
4340
4449
  }
4450
+ presentAiPocketSearchResult({ result, outputPath });
4341
4451
  process.exit(failed === selectedRows ? 1 : 0);
4342
4452
  };
4453
+
4454
+ // src/scripts/aiPocketSearch.ts
4455
+ import_args.default.example(
4456
+ "yarn ai-pocket-search --strategy LiquidityZones -n 0 --maxDepth 2 --minSupport 25",
4457
+ "Search deterministic AI-gate pockets over the latest LiquidityZones AI export"
4458
+ );
4459
+ import_args.default.option(["o", "outDir"], "Dataset directory", "data/ai/export");
4460
+ import_args.default.option(["s", "strategy"], "Strategy name filter for merged file", "");
4461
+ import_args.default.option(["f", "file"], "Explicit merged dataset file path", "");
4462
+ import_args.default.option(
4463
+ ["n", "recent"],
4464
+ "How many recent trades to evaluate from the end (0 = all)",
4465
+ 50
4466
+ );
4467
+ import_args.default.option(
4468
+ ["k", "skip"],
4469
+ "How many recent trades to skip from the end before selecting replay rows",
4470
+ 0
4471
+ );
4472
+ import_args.default.option(
4473
+ ["M", "minQuality"],
4474
+ "Minimum deterministic gate quality used for current qN+ baseline",
4475
+ 4
4476
+ );
4477
+ import_args.default.option(
4478
+ ["S", "since"],
4479
+ "Only evaluate rows at or after this timestamp (ISO date or epoch ms)",
4480
+ ""
4481
+ );
4482
+ import_args.default.option(
4483
+ ["u", "until"],
4484
+ "Only evaluate rows at or before this timestamp (ISO date or epoch ms)",
4485
+ ""
4486
+ );
4487
+ import_args.default.option(
4488
+ ["P", "period"],
4489
+ "Evaluate a trailing selected-row period such as last365d, last90d, or last30d",
4490
+ ""
4491
+ );
4492
+ import_args.default.option(
4493
+ ["q", "qualityThresholds"],
4494
+ "Comma-separated qN+ thresholds for current-gate baseline",
4495
+ "3,4,5"
4496
+ );
4497
+ import_args.default.option(
4498
+ ["g", "scope"],
4499
+ "Search scope: all, approved, rejected, or candidates",
4500
+ "all"
4501
+ );
4502
+ import_args.default.option(
4503
+ ["I", "direction"],
4504
+ "Only search rows with this signal direction: LONG or SHORT",
4505
+ ""
4506
+ );
4507
+ import_args.default.option(["d", "maxDepth"], "Maximum predicate-combination depth", 2);
4508
+ import_args.default.option(
4509
+ ["m", "minSupport"],
4510
+ "Minimum rows override; cadence auto derives it when omitted",
4511
+ 20
4512
+ );
4513
+ import_args.default.option(
4514
+ ["F", "minProfitFactor"],
4515
+ "Minimum profit factor required for positive pockets",
4516
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minProfitFactor
4517
+ );
4518
+ import_args.default.option(
4519
+ ["W", "minWinRate"],
4520
+ "Minimum win rate required for positive pockets",
4521
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minWinRate
4522
+ );
4523
+ import_args.default.option(
4524
+ ["R", "minTotalProfit"],
4525
+ "Minimum total PnL required for positive pockets",
4526
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.minTotalProfit
4527
+ );
4528
+ import_args.default.option(
4529
+ ["a", "maxAtomicPredicates"],
4530
+ "Maximum strongest atomic predicates used for combinations",
4531
+ 180
4532
+ );
4533
+ import_args.default.option(
4534
+ ["C", "maxCombinations"],
4535
+ "Maximum predicate combinations to evaluate",
4536
+ 6e4
4537
+ );
4538
+ import_args.default.option(
4539
+ ["V", "validationSplit"],
4540
+ "Trailing time-ordered scope share reserved for validation (0 disables)",
4541
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
4542
+ );
4543
+ import_args.default.option(
4544
+ ["T", "testSplit"],
4545
+ "Trailing timestamp-grouped scope share withheld as untouched test",
4546
+ AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
4547
+ );
4548
+ import_args.default.option(
4549
+ ["w", "sealTest"],
4550
+ "Reserve test bounds without exposing its rows or economics to discovery",
4551
+ false
4552
+ );
4553
+ import_args.default.option(
4554
+ ["N", "minValidationSupport"],
4555
+ "Minimum validation rows required for positive pockets (0 = auto)",
4556
+ 0
4557
+ );
4558
+ import_args.default.option(
4559
+ ["H", "minEvents"],
4560
+ "Minimum independent timestamp events required (0 = auto)",
4561
+ 0
4562
+ );
4563
+ import_args.default.option(
4564
+ ["J", "minValidationEvents"],
4565
+ "Minimum validation timestamp events required (0 = auto)",
4566
+ 0
4567
+ );
4568
+ import_args.default.option(
4569
+ ["X", "maxBatch"],
4570
+ "Maximum selected rows at one timestamp (0 disables)",
4571
+ 5
4572
+ );
4573
+ import_args.default.option(
4574
+ ["U", "maxEventCountShare"],
4575
+ "Maximum row share contributed by one timestamp event",
4576
+ "0.25"
4577
+ );
4578
+ import_args.default.option(
4579
+ ["Z", "maxSymbolCountShare"],
4580
+ "Maximum row share contributed by one symbol",
4581
+ "0.5"
4582
+ );
4583
+ import_args.default.option(
4584
+ ["A", "objective"],
4585
+ "Search objective: auto, standalone, add-to-gate, or filter-gate",
4586
+ "auto"
4587
+ );
4588
+ import_args.default.option(
4589
+ ["G", "allowRiskRegression"],
4590
+ "Allow incremental pockets to worsen q4+ PF, PnL, drawdown, or loss streak",
4591
+ false
4592
+ );
4593
+ import_args.default.option(
4594
+ ["L", "allowValidationRegression"],
4595
+ "Keep train-positive pockets that fail validation PnL/PF thresholds",
4596
+ false
4597
+ );
4598
+ import_args.default.option(
4599
+ ["D", "dedupeEquivalentSelections"],
4600
+ "Collapse pockets that select the same train rows",
4601
+ true
4602
+ );
4603
+ import_args.default.option(["t", "top"], "Top pockets to print in each section", 30);
4604
+ import_args.default.option(["Y", "includeSymbol"], "Allow symbol as a pocket feature", false);
4605
+ import_args.default.option(
4606
+ ["E", "includeGateContext"],
4607
+ "Allow current deterministic gate output fields as pocket features",
4608
+ false
4609
+ );
4610
+ import_args.default.option(
4611
+ ["p", "featureProfile"],
4612
+ "Feature extraction profile: compact or all",
4613
+ "all"
4614
+ );
4615
+ import_args.default.option(
4616
+ ["K", "featurePolicy"],
4617
+ "Feature policy: causal-stationary or all",
4618
+ "causal-stationary"
4619
+ );
4620
+ import_args.default.option(
4621
+ ["Q", "coverageMode"],
4622
+ "Coverage-aware search mode: auto or full",
4623
+ "auto"
4624
+ );
4625
+ import_args.default.option(
4626
+ ["c", "cadenceMode"],
4627
+ "Cadence thresholds: auto adapts discovery support, fixed keeps legacy defaults",
4628
+ "auto"
4629
+ );
4630
+ import_args.default.option(
4631
+ ["r", "reportDir"],
4632
+ "Directory for generated Markdown reports",
4633
+ "data/ai/output"
4634
+ );
4635
+ import_args.default.option(["B", "reportFile"], "Explicit Markdown report file path", "");
4636
+ import_args.default.option(["j", "json"], "Print structured JSON summary", false);
4637
+ import_args.default.option(["O", "output"], "Write structured JSON summary to file", "");
4638
+ var flags = import_args.default.parse(process.argv);
4639
+ var main = () => runAiPocketSearchCommand({ flags });
4343
4640
  // Annotate the CommonJS export names for ESM import in node:
4344
4641
  0 && (module.exports = {
4345
4642
  main