@tradejs/cli 3.1.29-beta.261 → 3.1.29-beta.262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,8 +102,10 @@ yarn exec tradejs backtest -c TrendFollow:base -d 30 --cacheOnly --jev --jevReco
102
102
  Without `--jev`, ordinary backtests do not evaluate Jev, even if a configuration
103
103
  contains `JEV`. Use `--ai --jev` to save completed trades with Jev features for
104
104
  AI training. `--jev` alone records Jev assessments without enabling AI export.
105
- Jev can ask eight independent questions: trend, swing, current participation,
106
- setup participation, entry extension, confirmation, setup strength, and geometry.
105
+ Jev uses one current set of nine questions: directional agreement, correction
106
+ containment, current volume expansion, setup volume contraction, entry proximity,
107
+ entry candle conviction, impulse persistence, boundary regularity, and overall
108
+ signal strength.
107
109
  It asks each question only when the corresponding signal-time facts exist and
108
110
  marks missing facts explicitly. The shared projection
109
111
  selects a few signal-time trend, swing, volume, delta, entry-distance and
@@ -114,6 +116,38 @@ which is available to the strategy's AI gate and AI export. Jev scores do not
114
116
  approve or reject entries. Backtests do not apply the AI gate; `ai-train`
115
117
  evaluates it on exported outcomes, while runtime follows `AI_MODE`.
116
118
 
119
+ `--jev` and runtime `JEV` always use the current compact nine-question set.
120
+ There is no question-set selector or fallback to older rubrics. The recorded
121
+ `micro-v3` marker and question hash identify the current meanings; old or
122
+ unversioned recordings and local models are rejected. Use a new recording
123
+ directory for the new backtest and retrain local models from current answers.
124
+
125
+ The first eight questions describe separate signal-time facts. The ninth asks
126
+ how strong and actionable the signal is: do the available facts support opening
127
+ a trade now? It weighs direction, setup, confirmation, placement and the
128
+ stop/target plan together. Essential missing facts and unresolved conflicts
129
+ reduce support. All eligible questions share the same compact state in one
130
+ request; outcomes and other Jev answers are excluded.
131
+
132
+ Its ten rubric levels appear as
133
+ `additionalIndicators.jev.scores.signalStrength`, from **1 to 10** (decimal
134
+ expected scores are retained). Confidence remains 0–1. The API returns a
135
+ zero-based 0–9 score; recordings and local teacher models normalize it to 0–1,
136
+ and AI features convert it to 1–10. This is an entry assessment, not a calibrated
137
+ probability of profit, and never automatically approves or blocks a trade.
138
+
139
+ ```bash
140
+ yarn exec tradejs backtest -c <Strategy>:<config> -d 365 --cacheOnly --ai --jev \
141
+ --jevRecordsDir data/ai/jev/<new-study>
142
+ ```
143
+
144
+ Use `--ai` to retain completed outcomes for comparing the score with realized
145
+ PnL. Freeze score thresholds on the first 60% with three internal stability
146
+ blocks before checking the last 40%. Compare with the current gate and ordinary
147
+ features on the same rows, include independent event support and costs, and do
148
+ not choose thresholds on the outer test. A fresh run does not make previously
149
+ inspected historical outcomes untouched.
150
+
117
151
  Any strategy can optionally attach `jevEvidence` through its `StrategyAPI.entry`
118
152
  `additionalIndicators` with a version, `knownAt` timestamp, up to 16 scalar
119
153
  setup facts and 16 scalar geometry facts:
@@ -147,9 +181,12 @@ Provider responses and decisions are saved under `data/ai/jev`, or the directory
147
181
  selected with `--jevRecordsDir`. Input, selected question version and provider
148
182
  identity determine each signal record. Equivalent facts reuse one provider
149
183
  response even when signal timestamps or symbols differ. Input v4 records
150
- explicitly store missing facts and their sources. The changed question hash
151
- prevents older four-question recordings and local models from being replayed as
152
- answers to the new questions.
184
+ explicitly store missing facts and their sources. Paid state groups fact paths
185
+ by namespace and retains exact values, units and missing reasons; signal identity,
186
+ internal versions and source metadata remain local. The current questions use
187
+ short rubrics with explicit thresholds and unknown-data rules. Recordings and
188
+ local models must match the current question marker and hash. Older versions
189
+ are not migrated or used as substitutes for missing current answers.
153
190
  `--jevRecorded` requires the same signal-time input facts, question set and
154
191
  provider as the recording run. A shorter backtest may warm up indicators from
155
192
  a different history or reach the same date with a different trade state, so its
@@ -179,9 +216,11 @@ path for each run.
179
216
 
180
217
  Training fits shallow trees to Jev scores, without trade outcomes as features or
181
218
  labels. It requires at least 30 samples across 10 distinct signal times, selects
182
- one strategy and one teacher/provider lineage, and separates train/validation/test
183
- chronologically as 60/20/20. Three additional expanding-window folds check later
184
- periods with independently fitted trees. The report measures teacher agreement, score error
219
+ one strategy and one teacher/provider lineage, and splits timestamp groups
220
+ chronologically into 60% development and 40% test. Three expanding-window folds
221
+ fit earlier development data and check later development data, entirely inside
222
+ the first 60%. The final teacher model uses the full development partition.
223
+ The outer test never fits or selects the model. The report measures teacher agreement, score error
185
224
  and matched-outcome economics. These summaries are not sequential portfolio
186
225
  backtests and do not establish profitability. Validate a frozen gate on a later
187
226
  period with `--jevModelFile` before runtime use. Studies are bounded to 20,000
package/dist/cli.js CHANGED
@@ -1564,7 +1564,7 @@ var init_aiExport2 = __esm({
1564
1564
  });
1565
1565
 
1566
1566
  // src/lib/aiPocketSearchCli.ts
1567
- var DAY_MS, DEFAULT_MIN_SUPPORT, LOW_CADENCE_EVENT_LIMIT, LOW_CADENCE_MAX_EVENT_SHARE, countIndependentEvents, getPeriodDays, clampInt, resolveAiPocketCadenceProfile, AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, splitAiPocketResearchRowsByTimestamp, splitAiPocketCoverageRowsByTimestamp, sealAiPocketTestPartition, readAiPocketSearchCliOption;
1567
+ var DAY_MS, DEFAULT_MIN_SUPPORT, LOW_CADENCE_EVENT_LIMIT, LOW_CADENCE_MAX_EVENT_SHARE, countIndependentEvents, getPeriodDays, clampInt, resolveAiPocketCadenceProfile, AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS, splitAiPocketResearchRowsByTimestamp, sealAiPocketTestPartition, readAiPocketSearchCliOption;
1568
1568
  var init_aiPocketSearchCli = __esm({
1569
1569
  "src/lib/aiPocketSearchCli.ts"() {
1570
1570
  "use strict";
@@ -1606,7 +1606,7 @@ var init_aiPocketSearchCli = __esm({
1606
1606
  validationRows = [],
1607
1607
  testRows = [],
1608
1608
  mode = "auto",
1609
- validationSplit = 0.25,
1609
+ validationSplit = 0,
1610
1610
  minSupport,
1611
1611
  minEvents,
1612
1612
  minValidationSupport,
@@ -1656,8 +1656,8 @@ var init_aiPocketSearchCli = __esm({
1656
1656
  minProfitFactor: "1.2",
1657
1657
  minWinRate: "0",
1658
1658
  minTotalProfit: "0",
1659
- validationSplit: "0.25",
1660
- testSplit: "0"
1659
+ validationSplit: "0",
1660
+ testSplit: "0.4"
1661
1661
  };
1662
1662
  splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
1663
1663
  const timestamps = [
@@ -1698,11 +1698,6 @@ var init_aiPocketSearchCli = __esm({
1698
1698
  testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
1699
1699
  };
1700
1700
  };
1701
- splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
1702
- rows.filter((row) => row.featureCoverage?.[family] === true),
1703
- validationSplit,
1704
- testSplit
1705
- );
1706
1701
  sealAiPocketTestPartition = (split, sealed) => {
1707
1702
  const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
1708
1703
  return {
@@ -2033,17 +2028,17 @@ var init_commandOptions = __esm({
2033
2028
  longName: "validationSplit",
2034
2029
  shortName: "V"
2035
2030
  }) ?? flags35.validationSplit,
2036
- 0.25
2037
- ) : 0.25;
2031
+ 0
2032
+ ) : 0;
2038
2033
  const testSplit = hasCliOption2(argv2, "testSplit", "T") ? normalizeRatio(
2039
2034
  readAiPocketSearchCliOption({
2040
2035
  argv: argv2,
2041
2036
  longName: "testSplit",
2042
2037
  shortName: "T"
2043
2038
  }) ?? flags35.testSplit,
2044
- 0
2045
- ) : 0;
2046
- const sealTest = Boolean(flags35.sealTest);
2039
+ 0.4
2040
+ ) : 0.4;
2041
+ const sealTest = flags35.sealTest == null ? true : Boolean(flags35.sealTest);
2047
2042
  if (sealTest && testSplit <= 0) {
2048
2043
  throw new Error("--sealTest requires a positive --testSplit");
2049
2044
  }
@@ -4275,6 +4270,22 @@ var init_markdownReport = __esm({
4275
4270
  "",
4276
4271
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.positivePockets)),
4277
4272
  "",
4273
+ "## Development Stability (inside discovery, not independent validation)",
4274
+ "",
4275
+ markdownTable(
4276
+ ["Condition", "Status", "Block", "Events", "PnL", "PF"],
4277
+ pocketSearch.positivePockets.flatMap(
4278
+ (pocket) => (pocket.stability?.folds ?? []).map((fold, index) => [
4279
+ pocket.condition,
4280
+ pocket.stability?.status,
4281
+ index + 1,
4282
+ fold.summary.events,
4283
+ formatMdNumber(fold.summary.totalProfit),
4284
+ formatMdNumber(fold.summary.profitFactor)
4285
+ ])
4286
+ )
4287
+ ),
4288
+ "",
4278
4289
  "## Top Loss Pockets",
4279
4290
  "",
4280
4291
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.negativePockets)),
@@ -5815,6 +5826,70 @@ var init_aiTrainMetrics = __esm({
5815
5826
  }
5816
5827
  });
5817
5828
 
5829
+ // src/lib/aiPocketSearch/stability.ts
5830
+ var assessDevelopmentStability;
5831
+ var init_stability = __esm({
5832
+ "src/lib/aiPocketSearch/stability.ts"() {
5833
+ "use strict";
5834
+ init_predicates();
5835
+ init_summary();
5836
+ assessDevelopmentStability = (result, options) => {
5837
+ const minEvents = options.minEventsPerFold ?? 5;
5838
+ const timestamps = [...new Set(options.timestamps)].sort((a, b) => a - b);
5839
+ const boundaries = [0, 1, 2, 3].map(
5840
+ (index) => index === 3 ? timestamps.at(-1) == null ? Number.POSITIVE_INFINITY : timestamps.at(-1) + 1 : timestamps[Math.floor(timestamps.length * index / 3)] ?? Number.POSITIVE_INFINITY
5841
+ );
5842
+ const blocks = [0, 1, 2].map((index) => ({
5843
+ start: boundaries[index],
5844
+ end: boundaries[index + 1],
5845
+ rows: options.developmentRows.filter(
5846
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
5847
+ ),
5848
+ baseline: options.baselineRows.filter(
5849
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
5850
+ )
5851
+ }));
5852
+ for (const pocket of result.positivePockets) {
5853
+ const folds = blocks.map((block) => {
5854
+ const mask = buildPredicateListMask(block.rows, pocket.predicates).mask;
5855
+ const selected = block.rows.filter((_3, index) => mask[index] === 1);
5856
+ const summary = summarizeAiPocketRows(selected);
5857
+ const baseline = summarizeAiPocketRows(block.baseline);
5858
+ const objectiveSummary = result.objective === "add-to-gate" ? summarizeAiPocketRows([
5859
+ .../* @__PURE__ */ new Set([...block.baseline, ...selected])
5860
+ ]) : summary;
5861
+ return {
5862
+ startTimestamp: Number.isFinite(block.start) ? block.start : null,
5863
+ endTimestamp: Number.isFinite(block.end) ? block.end : null,
5864
+ summary,
5865
+ objectiveSummary,
5866
+ baseline
5867
+ };
5868
+ });
5869
+ const supported = folds.filter((fold) => fold.summary.events >= minEvents);
5870
+ const unstable = supported.some(
5871
+ (fold) => fold.objectiveSummary.totalProfit < (result.objective === "add-to-gate" ? fold.baseline.totalProfit : 0) || (fold.objectiveSummary.profitFactor ?? (fold.objectiveSummary.grossLoss === 0 ? Infinity : 0)) < 1
5872
+ );
5873
+ const status = unstable ? "unstable" : supported.length < 3 ? "insufficient-support" : "stable";
5874
+ pocket.stability = {
5875
+ status,
5876
+ role: "development-only diagnostic",
5877
+ minEventsPerFold: minEvents,
5878
+ folds
5879
+ };
5880
+ if (status !== "stable") {
5881
+ pocket.readiness = "research-only";
5882
+ pocket.readinessReasons.push(`development stability: ${status}`);
5883
+ }
5884
+ }
5885
+ const rank = { stable: 0, "insufficient-support": 1, unstable: 2 };
5886
+ result.positivePockets.sort(
5887
+ (a, b) => rank[a.stability.status] - rank[b.stability.status]
5888
+ );
5889
+ };
5890
+ }
5891
+ });
5892
+
5818
5893
  // src/lib/aiPocketSearch/research.ts
5819
5894
  var resolveScopeRows, runAiPocketSearchResearch;
5820
5895
  var init_research = __esm({
@@ -5823,6 +5898,7 @@ var init_research = __esm({
5823
5898
  init_aiTrainMetrics();
5824
5899
  init_aiPocketSearch();
5825
5900
  init_aiPocketSearchCli();
5901
+ init_stability();
5826
5902
  resolveScopeRows = (rows, scope) => {
5827
5903
  if (scope === "approved") {
5828
5904
  return rows.filter((row) => row.aiApproved);
@@ -5917,7 +5993,7 @@ var init_research = __esm({
5917
5993
  maxEventCountShare,
5918
5994
  explicitMaxEventCountShare
5919
5995
  });
5920
- return searchAiPockets(train, {
5996
+ const result = searchAiPockets(train, {
5921
5997
  minSupport: cadenceProfile.minSupport,
5922
5998
  minProfitFactor,
5923
5999
  minTotalProfit,
@@ -5947,6 +6023,17 @@ var init_research = __esm({
5947
6023
  progressInterval: 250,
5948
6024
  onProgress: (progress) => onProgress?.({ label, progress })
5949
6025
  });
6026
+ assessDevelopmentStability(result, {
6027
+ developmentRows: [...train, ...validation],
6028
+ baselineRows: [...trainBaseline, ...validationBaseline],
6029
+ // Every provider/scope uses the same outer development timestamps.
6030
+ timestamps: [
6031
+ ...new Set(
6032
+ fullSplit.trainRows.concat(fullSplit.validationRows).map((row) => row.timestamp)
6033
+ )
6034
+ ].sort((a, b) => a - b)
6035
+ });
6036
+ return result;
5950
6037
  };
5951
6038
  const search = runSearch({
5952
6039
  label: "full",
@@ -5966,12 +6053,12 @@ var init_research = __esm({
5966
6053
  const coverageSearches = [];
5967
6054
  if (coverageMode === "auto") {
5968
6055
  for (const family of ["cmc", "coinalyze"]) {
5969
- const cohortSplit = splitAiPocketCoverageRowsByTimestamp(
5970
- directionRows,
5971
- family,
5972
- validationSplit,
5973
- testSplit
5974
- );
6056
+ const covered = (row) => row.featureCoverage?.[family] === true;
6057
+ const cohortSplit = {
6058
+ trainRows: fullSplit.trainRows.filter(covered),
6059
+ validationRows: fullSplit.validationRows.filter(covered),
6060
+ testRows: fullSplit.testRows.filter(covered)
6061
+ };
5975
6062
  const sealedCohortSplit = sealAiPocketTestPartition(
5976
6063
  cohortSplit,
5977
6064
  sealTest
@@ -6327,18 +6414,18 @@ var init_aiPocketSearch2 = __esm({
6327
6414
  );
6328
6415
  import_args3.default.option(
6329
6416
  ["V", "validationSplit"],
6330
- "Trailing time-ordered scope share reserved for validation (0 disables)",
6417
+ "Optional legacy tuning share; default 0, stability is checked inside development",
6331
6418
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
6332
6419
  );
6333
6420
  import_args3.default.option(
6334
6421
  ["T", "testSplit"],
6335
- "Trailing timestamp-grouped scope share withheld as untouched test",
6422
+ "Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
6336
6423
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
6337
6424
  );
6338
6425
  import_args3.default.option(
6339
6426
  ["w", "sealTest"],
6340
6427
  "Reserve test bounds without exposing its rows or economics to discovery",
6341
- false
6428
+ true
6342
6429
  );
6343
6430
  import_args3.default.option(
6344
6431
  ["N", "minValidationSupport"],
@@ -8823,6 +8910,17 @@ var init_jevResearch = __esm({
8823
8910
  throw new Error(
8824
8911
  "Export exceeds 20000 samples; select a bounded recording directory"
8825
8912
  );
8913
+ await (0, import_jev2.evaluateJevInput)({
8914
+ input: record.input,
8915
+ config: {
8916
+ source: "recorded",
8917
+ mode: "observe",
8918
+ provider: record.provider,
8919
+ recordsDir: options.recordsDir
8920
+ },
8921
+ userName: options.userName,
8922
+ projectRoot: options.projectRoot
8923
+ });
8826
8924
  const outcome = byRecord.get(record.id);
8827
8925
  rows2.push({
8828
8926
  schema: "tradejs-jev-study/v4",
@@ -8942,6 +9040,10 @@ var init_jevOptions = __esm({
8942
9040
  import_jev5 = require("@tradejs/node/jev");
8943
9041
  resolveBacktestAiExportEnabled = (flags35) => Boolean(flags35.ai);
8944
9042
  resolveBacktestJev = async (flags35, userName3, projectRoot18) => {
9043
+ if (flags35.jevQuestionSet != null)
9044
+ throw new Error(
9045
+ "--jevQuestionSet was removed; --jev always uses the current nine-question set"
9046
+ );
8945
9047
  if (!flags35.jev) {
8946
9048
  if (flags35.jevRecorded || flags35.jevModelFile)
8947
9049
  throw new Error("Jev options require --jev");
@@ -2158,6 +2158,22 @@ var buildAiPocketMarkdownReport = ({
2158
2158
  "",
2159
2159
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.positivePockets)),
2160
2160
  "",
2161
+ "## Development Stability (inside discovery, not independent validation)",
2162
+ "",
2163
+ markdownTable(
2164
+ ["Condition", "Status", "Block", "Events", "PnL", "PF"],
2165
+ pocketSearch.positivePockets.flatMap(
2166
+ (pocket) => (pocket.stability?.folds ?? []).map((fold, index) => [
2167
+ pocket.condition,
2168
+ pocket.stability?.status,
2169
+ index + 1,
2170
+ fold.summary.events,
2171
+ formatMdNumber(fold.summary.totalProfit),
2172
+ formatMdNumber(fold.summary.profitFactor)
2173
+ ])
2174
+ )
2175
+ ),
2176
+ "",
2161
2177
  "## Top Loss Pockets",
2162
2178
  "",
2163
2179
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.negativePockets)),
@@ -66,7 +66,7 @@ var resolveAiPocketCadenceProfile = ({
66
66
  validationRows = [],
67
67
  testRows = [],
68
68
  mode = "auto",
69
- validationSplit = 0.25,
69
+ validationSplit = 0,
70
70
  minSupport,
71
71
  minEvents,
72
72
  minValidationSupport,
@@ -116,8 +116,8 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
116
116
  minProfitFactor: "1.2",
117
117
  minWinRate: "0",
118
118
  minTotalProfit: "0",
119
- validationSplit: "0.25",
120
- testSplit: "0"
119
+ validationSplit: "0",
120
+ testSplit: "0.4"
121
121
  };
122
122
  var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
123
123
  const timestamps = [
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/lib/jevMicroReferences.ts
21
+ var jevMicroReferences_exports = {};
22
+ __export(jevMicroReferences_exports, {
23
+ microReferenceFeatures: () => microReferenceFeatures
24
+ });
25
+ module.exports = __toCommonJS(jevMicroReferences_exports);
26
+ var microReferenceFeatures = (input) => {
27
+ const number = (...keys) => {
28
+ for (const key of keys) {
29
+ const value = input.features[key];
30
+ if (typeof value === "number" && Number.isFinite(value)) return value;
31
+ }
32
+ return null;
33
+ };
34
+ const bands = (value, cuts) => {
35
+ if (value == null || value < 0) return null;
36
+ const level = cuts.filter((cut) => value >= cut).length;
37
+ return level / 4;
38
+ };
39
+ const ratio = number("setup.retracementRatio");
40
+ const contraction = number(
41
+ "setup.consolidationToImpulseVolumeRatio",
42
+ "setup.flagToPoleVolumeRatio"
43
+ );
44
+ const extension = number("setup.breakoutDistanceAtr");
45
+ const efficiency = number(
46
+ "setup.impulseEfficiencyRatio",
47
+ "setup.poleEfficiencyRatio"
48
+ );
49
+ const body = number("entry.directionalBodyAtr");
50
+ const close = number("entry.directionalCloseLocation");
51
+ const upper = number("geometry.upperR2"), lower = number("geometry.lowerR2"), divergence = number("geometry.slopeDivergenceRatio");
52
+ const fit = upper != null && lower != null ? Math.min(upper, lower) : null;
53
+ const bias = (key) => {
54
+ const value = input.features[key];
55
+ if (!["bull", "bear"].includes(String(value))) return 0;
56
+ return value === (input.direction === "LONG" ? "bull" : "bear") ? 1 : -1;
57
+ };
58
+ const trend = bias("market.trendBias"), swing = bias("market.swingBias");
59
+ return {
60
+ directionalAgreement: input.features["market.trendBias"] == null && input.features["market.swingBias"] == null ? null : (trend + swing + 2) / 4,
61
+ correctionContainment: ratio == null || ratio < 0 ? null : (ratio <= 0.2 ? 4 : ratio <= 0.4 ? 3 : ratio <= 0.65 ? 2 : ratio <= 1 ? 1 : 0) / 4,
62
+ volumeExpansion: bands(number("market.volumeRel20"), [0.5, 0.8, 1.2, 2]),
63
+ volumeContraction: contraction == null || contraction < 0 ? null : (contraction <= 0.4 ? 4 : contraction <= 0.7 ? 3 : contraction <= 0.9 ? 2 : contraction <= 1.2 ? 1 : 0) / 4,
64
+ entryProximity: extension == null || extension < 0 ? null : (extension <= 0.2 ? 4 : extension <= 0.5 ? 3 : extension <= 1 ? 2 : extension <= 2 ? 1 : 0) / 4,
65
+ entryConviction: body == null || close == null || close < 0 || close > 1 ? null : (body < 0 && close < 0.25 ? 0 : body < 0 || close < 0.5 ? 1 : body < 0.2 ? 2 : body >= 0.5 && close >= 0.75 ? 4 : 3) / 4,
66
+ impulsePersistence: efficiency == null || efficiency > 1 ? null : bands(efficiency, [0.2, 0.4, 0.6, 0.8]),
67
+ boundaryRegularity: fit == null || divergence == null || fit < 0 || fit > 1 || divergence < 0 ? null : (fit < 0.5 || divergence > 0.5 ? 0 : fit < 0.7 || divergence > 0.3 ? 1 : fit < 0.85 || divergence > 0.15 ? 2 : fit >= 0.95 && divergence <= 0.05 ? 4 : 3) / 4
68
+ };
69
+ };
70
+ // Annotate the CommonJS export names for ESM import in node:
71
+ 0 && (module.exports = {
72
+ microReferenceFeatures
73
+ });
@@ -170,6 +170,17 @@ var runJevResearch = async (options) => {
170
170
  throw new Error(
171
171
  "Export exceeds 20000 samples; select a bounded recording directory"
172
172
  );
173
+ await (0, import_jev2.evaluateJevInput)({
174
+ input: record.input,
175
+ config: {
176
+ source: "recorded",
177
+ mode: "observe",
178
+ provider: record.provider,
179
+ recordsDir: options.recordsDir
180
+ },
181
+ userName: options.userName,
182
+ projectRoot: options.projectRoot
183
+ });
173
184
  const outcome = byRecord.get(record.id);
174
185
  rows2.push({
175
186
  schema: "tradejs-jev-study/v4",
@@ -74,7 +74,7 @@ var resolveAiPocketCadenceProfile = ({
74
74
  validationRows = [],
75
75
  testRows = [],
76
76
  mode = "auto",
77
- validationSplit = 0.25,
77
+ validationSplit = 0,
78
78
  minSupport,
79
79
  minEvents,
80
80
  minValidationSupport,
@@ -124,8 +124,8 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
124
124
  minProfitFactor: "1.2",
125
125
  minWinRate: "0",
126
126
  minTotalProfit: "0",
127
- validationSplit: "0.25",
128
- testSplit: "0"
127
+ validationSplit: "0",
128
+ testSplit: "0.4"
129
129
  };
130
130
  var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
131
131
  const timestamps = [
@@ -166,11 +166,6 @@ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0
166
166
  testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
167
167
  };
168
168
  };
169
- var splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
170
- rows.filter((row) => row.featureCoverage?.[family] === true),
171
- validationSplit,
172
- testSplit
173
- );
174
169
  var sealAiPocketTestPartition = (split, sealed) => {
175
170
  const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
176
171
  return {
@@ -436,17 +431,17 @@ var resolveAiPocketSearchCommandOptions = ({
436
431
  longName: "validationSplit",
437
432
  shortName: "V"
438
433
  }) ?? flags2.validationSplit,
439
- 0.25
440
- ) : 0.25;
434
+ 0
435
+ ) : 0;
441
436
  const testSplit = hasCliOption2(argv, "testSplit", "T") ? normalizeRatio(
442
437
  readAiPocketSearchCliOption({
443
438
  argv,
444
439
  longName: "testSplit",
445
440
  shortName: "T"
446
441
  }) ?? flags2.testSplit,
447
- 0
448
- ) : 0;
449
- const sealTest = Boolean(flags2.sealTest);
442
+ 0.4
443
+ ) : 0.4;
444
+ const sealTest = flags2.sealTest == null ? true : Boolean(flags2.sealTest);
450
445
  if (sealTest && testSplit <= 0) {
451
446
  throw new Error("--sealTest requires a positive --testSplit");
452
447
  }
@@ -2649,6 +2644,22 @@ var buildAiPocketMarkdownReport = ({
2649
2644
  "",
2650
2645
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.positivePockets)),
2651
2646
  "",
2647
+ "## Development Stability (inside discovery, not independent validation)",
2648
+ "",
2649
+ markdownTable(
2650
+ ["Condition", "Status", "Block", "Events", "PnL", "PF"],
2651
+ pocketSearch.positivePockets.flatMap(
2652
+ (pocket) => (pocket.stability?.folds ?? []).map((fold, index) => [
2653
+ pocket.condition,
2654
+ pocket.stability?.status,
2655
+ index + 1,
2656
+ fold.summary.events,
2657
+ formatMdNumber(fold.summary.totalProfit),
2658
+ formatMdNumber(fold.summary.profitFactor)
2659
+ ])
2660
+ )
2661
+ ),
2662
+ "",
2652
2663
  "## Top Loss Pockets",
2653
2664
  "",
2654
2665
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.negativePockets)),
@@ -4099,6 +4110,62 @@ var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [
4099
4110
  )
4100
4111
  }));
4101
4112
 
4113
+ // src/lib/aiPocketSearch/stability.ts
4114
+ var assessDevelopmentStability = (result, options) => {
4115
+ const minEvents = options.minEventsPerFold ?? 5;
4116
+ const timestamps = [...new Set(options.timestamps)].sort((a, b) => a - b);
4117
+ const boundaries = [0, 1, 2, 3].map(
4118
+ (index) => index === 3 ? timestamps.at(-1) == null ? Number.POSITIVE_INFINITY : timestamps.at(-1) + 1 : timestamps[Math.floor(timestamps.length * index / 3)] ?? Number.POSITIVE_INFINITY
4119
+ );
4120
+ const blocks = [0, 1, 2].map((index) => ({
4121
+ start: boundaries[index],
4122
+ end: boundaries[index + 1],
4123
+ rows: options.developmentRows.filter(
4124
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
4125
+ ),
4126
+ baseline: options.baselineRows.filter(
4127
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
4128
+ )
4129
+ }));
4130
+ for (const pocket of result.positivePockets) {
4131
+ const folds = blocks.map((block) => {
4132
+ const mask = buildPredicateListMask(block.rows, pocket.predicates).mask;
4133
+ const selected = block.rows.filter((_, index) => mask[index] === 1);
4134
+ const summary = summarizeAiPocketRows(selected);
4135
+ const baseline = summarizeAiPocketRows(block.baseline);
4136
+ const objectiveSummary = result.objective === "add-to-gate" ? summarizeAiPocketRows([
4137
+ .../* @__PURE__ */ new Set([...block.baseline, ...selected])
4138
+ ]) : summary;
4139
+ return {
4140
+ startTimestamp: Number.isFinite(block.start) ? block.start : null,
4141
+ endTimestamp: Number.isFinite(block.end) ? block.end : null,
4142
+ summary,
4143
+ objectiveSummary,
4144
+ baseline
4145
+ };
4146
+ });
4147
+ const supported = folds.filter((fold) => fold.summary.events >= minEvents);
4148
+ const unstable = supported.some(
4149
+ (fold) => fold.objectiveSummary.totalProfit < (result.objective === "add-to-gate" ? fold.baseline.totalProfit : 0) || (fold.objectiveSummary.profitFactor ?? (fold.objectiveSummary.grossLoss === 0 ? Infinity : 0)) < 1
4150
+ );
4151
+ const status = unstable ? "unstable" : supported.length < 3 ? "insufficient-support" : "stable";
4152
+ pocket.stability = {
4153
+ status,
4154
+ role: "development-only diagnostic",
4155
+ minEventsPerFold: minEvents,
4156
+ folds
4157
+ };
4158
+ if (status !== "stable") {
4159
+ pocket.readiness = "research-only";
4160
+ pocket.readinessReasons.push(`development stability: ${status}`);
4161
+ }
4162
+ }
4163
+ const rank = { stable: 0, "insufficient-support": 1, unstable: 2 };
4164
+ result.positivePockets.sort(
4165
+ (a, b) => rank[a.stability.status] - rank[b.stability.status]
4166
+ );
4167
+ };
4168
+
4102
4169
  // src/lib/aiPocketSearch/research.ts
4103
4170
  var resolveScopeRows = (rows, scope) => {
4104
4171
  if (scope === "approved") {
@@ -4194,7 +4261,7 @@ var runAiPocketSearchResearch = ({
4194
4261
  maxEventCountShare,
4195
4262
  explicitMaxEventCountShare
4196
4263
  });
4197
- return searchAiPockets(train, {
4264
+ const result = searchAiPockets(train, {
4198
4265
  minSupport: cadenceProfile.minSupport,
4199
4266
  minProfitFactor,
4200
4267
  minTotalProfit,
@@ -4224,6 +4291,17 @@ var runAiPocketSearchResearch = ({
4224
4291
  progressInterval: 250,
4225
4292
  onProgress: (progress) => onProgress?.({ label, progress })
4226
4293
  });
4294
+ assessDevelopmentStability(result, {
4295
+ developmentRows: [...train, ...validation],
4296
+ baselineRows: [...trainBaseline, ...validationBaseline],
4297
+ // Every provider/scope uses the same outer development timestamps.
4298
+ timestamps: [
4299
+ ...new Set(
4300
+ fullSplit.trainRows.concat(fullSplit.validationRows).map((row) => row.timestamp)
4301
+ )
4302
+ ].sort((a, b) => a - b)
4303
+ });
4304
+ return result;
4227
4305
  };
4228
4306
  const search = runSearch({
4229
4307
  label: "full",
@@ -4243,12 +4321,12 @@ var runAiPocketSearchResearch = ({
4243
4321
  const coverageSearches = [];
4244
4322
  if (coverageMode === "auto") {
4245
4323
  for (const family of ["cmc", "coinalyze"]) {
4246
- const cohortSplit = splitAiPocketCoverageRowsByTimestamp(
4247
- directionRows,
4248
- family,
4249
- validationSplit,
4250
- testSplit
4251
- );
4324
+ const covered = (row) => row.featureCoverage?.[family] === true;
4325
+ const cohortSplit = {
4326
+ trainRows: fullSplit.trainRows.filter(covered),
4327
+ validationRows: fullSplit.validationRows.filter(covered),
4328
+ testRows: fullSplit.testRows.filter(covered)
4329
+ };
4252
4330
  const sealedCohortSplit = sealAiPocketTestPartition(
4253
4331
  cohortSplit,
4254
4332
  sealTest
@@ -4581,18 +4659,18 @@ import_args.default.option(
4581
4659
  );
4582
4660
  import_args.default.option(
4583
4661
  ["V", "validationSplit"],
4584
- "Trailing time-ordered scope share reserved for validation (0 disables)",
4662
+ "Optional legacy tuning share; default 0, stability is checked inside development",
4585
4663
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
4586
4664
  );
4587
4665
  import_args.default.option(
4588
4666
  ["T", "testSplit"],
4589
- "Trailing timestamp-grouped scope share withheld as untouched test",
4667
+ "Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
4590
4668
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
4591
4669
  );
4592
4670
  import_args.default.option(
4593
4671
  ["w", "sealTest"],
4594
4672
  "Reserve test bounds without exposing its rows or economics to discovery",
4595
- false
4673
+ true
4596
4674
  );
4597
4675
  import_args.default.option(
4598
4676
  ["N", "minValidationSupport"],
@@ -74,6 +74,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
74
74
  var import_jev3 = require("@tradejs/node/jev");
75
75
  var resolveBacktestAiExportEnabled = (flags2) => Boolean(flags2.ai);
76
76
  var resolveBacktestJev = async (flags2, userName2, projectRoot2) => {
77
+ if (flags2.jevQuestionSet != null)
78
+ throw new Error(
79
+ "--jevQuestionSet was removed; --jev always uses the current nine-question set"
80
+ );
77
81
  if (!flags2.jev) {
78
82
  if (flags2.jevRecorded || flags2.jevModelFile)
79
83
  throw new Error("Jev options require --jev");
@@ -172,6 +172,17 @@ var runJevResearch = async (options) => {
172
172
  throw new Error(
173
173
  "Export exceeds 20000 samples; select a bounded recording directory"
174
174
  );
175
+ await (0, import_jev2.evaluateJevInput)({
176
+ input: record.input,
177
+ config: {
178
+ source: "recorded",
179
+ mode: "observe",
180
+ provider: record.provider,
181
+ recordsDir: options.recordsDir
182
+ },
183
+ userName: options.userName,
184
+ projectRoot: options.projectRoot
185
+ });
175
186
  const outcome = byRecord.get(record.id);
176
187
  rows2.push({
177
188
  schema: "tradejs-jev-study/v4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/cli",
3
- "version": "3.1.29-beta.261",
3
+ "version": "3.1.29-beta.262",
4
4
  "description": "Official CLI for the TradeJS TypeScript framework: infra setup, backtests, signals, bots, and ML workflows.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -16,7 +16,8 @@
16
16
  "coinbase",
17
17
  "ai",
18
18
  "claude",
19
- "codex"
19
+ "codex",
20
+ "jev"
20
21
  ],
21
22
  "homepage": "https://tradejs.dev",
22
23
  "repository": {
@@ -36,13 +37,13 @@
36
37
  "tradejs": "dist/cli.js"
37
38
  },
38
39
  "dependencies": {
39
- "@tradejs/connectors": "^3.1.29-beta.261",
40
- "@tradejs/core": "^3.1.29-beta.261",
41
- "@tradejs/indicators": "^3.1.29-beta.261",
42
- "@tradejs/infra": "^3.1.29-beta.261",
43
- "@tradejs/node": "^3.1.29-beta.261",
40
+ "@tradejs/connectors": "^3.1.29-beta.262",
41
+ "@tradejs/core": "^3.1.29-beta.262",
42
+ "@tradejs/indicators": "^3.1.29-beta.262",
43
+ "@tradejs/infra": "^3.1.29-beta.262",
44
+ "@tradejs/node": "^3.1.29-beta.262",
44
45
  "@tradejs/strategy-kit": "3.0.6",
45
- "@tradejs/types": "^3.1.29-beta.261",
46
+ "@tradejs/types": "^3.1.29-beta.262",
46
47
  "args": "^5.0.3",
47
48
  "bcryptjs": "^2.4.3",
48
49
  "chalk": "4.1.2",