create-tradejs 3.1.22 → 3.1.23

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.
Files changed (40) hide show
  1. package/README.md +19 -3
  2. package/dist/index.js +36 -5
  3. package/dist/skill-bundle/.codex/skills/ai-train-local-research/SKILL.md +596 -0
  4. package/dist/skill-bundle/.codex/skills/ai-train-local-research/references/gate-ablation.md +324 -0
  5. package/dist/skill-bundle/.codex/skills/ai-train-local-research/references/reporting.md +227 -0
  6. package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs +5082 -0
  7. package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs +1170 -0
  8. package/dist/skill-bundle/.codex/skills/backtest-config-redis/SKILL.md +17 -0
  9. package/dist/skill-bundle/.codex/skills/backtest-config-redis/scripts/get_backtest_config.sh +21 -0
  10. package/dist/skill-bundle/.codex/skills/runtime-parity-mismatch-analysis/SKILL.md +146 -0
  11. package/dist/skill-bundle/.codex/skills/save-strategy-config-from-backtest/SKILL.md +58 -0
  12. package/dist/skill-bundle/.codex/skills/save-strategy-config-from-backtest/agents/openai.yaml +4 -0
  13. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/SKILL.md +334 -0
  14. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/references/research-notes.md +247 -0
  15. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/scripts/backtest-run-metrics.mjs +647 -0
  16. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/scripts/backtest-run-metrics.test.mjs +321 -0
  17. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/scripts/fast-ai-export-metrics.mjs +744 -0
  18. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/scripts/fast-ai-export-metrics.test.mjs +553 -0
  19. package/dist/skill-bundle/.codex/skills/strategy-backtest-research/scripts/research-notes-check.mjs +125 -0
  20. package/dist/skill-bundle/.codex/skills/strategy-improvement-research/SKILL.md +18 -1
  21. package/dist/skill-bundle/.codex/skills/strategy-release/SKILL.md +22 -0
  22. package/dist/skill-bundle/.codex/skills/strategy-release/agents/openai.yaml +4 -0
  23. package/dist/skill-bundle/.codex/skills/strategy-release/references/diagnose-live.md +126 -0
  24. package/dist/skill-bundle/.codex/skills/strategy-release/references/direction-policy.md +141 -0
  25. package/dist/skill-bundle/.codex/skills/strategy-release/references/directional-parameter-split.md +93 -0
  26. package/dist/skill-bundle/.codex/skills/strategy-release/references/evidence-limitations.md +76 -0
  27. package/dist/skill-bundle/.codex/skills/strategy-release/references/evidence-retention.md +157 -0
  28. package/dist/skill-bundle/.codex/skills/strategy-release/references/historical-hypothesis-audit.md +163 -0
  29. package/dist/skill-bundle/.codex/skills/strategy-release/references/professional-research-loop.md +198 -0
  30. package/dist/skill-bundle/.codex/skills/strategy-release/references/release-workflow.md +755 -0
  31. package/dist/skill-bundle/.codex/skills/strategy-release/references/research-objective.md +255 -0
  32. package/dist/skill-bundle/.codex/skills/strategy-release/references/verdict-contract.md +200 -0
  33. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/direction-policy-checkpoint.mjs +137 -0
  34. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/direction-policy-checkpoint.test.mjs +85 -0
  35. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/directional-parameter-checkpoint.mjs +149 -0
  36. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/directional-parameter-checkpoint.test.mjs +120 -0
  37. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/release-progress-checkpoint.mjs +621 -0
  38. package/dist/skill-bundle/.codex/skills/strategy-release/scripts/release-progress-checkpoint.test.mjs +349 -0
  39. package/dist/skill-bundle/.codex/tradejs-skill-bundle.json +44 -3
  40. package/package.json +1 -1
@@ -0,0 +1,1170 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import fsp from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ import {
9
+ aggregateBenchmarkDiscoveryRows,
10
+ applyBenchmarkEventSnapshots,
11
+ balanceCrossStrategyRows,
12
+ buildAblationReport,
13
+ buildCrossStrategyReport,
14
+ buildMovingAverageVariants,
15
+ calculateMovingAverageGrid,
16
+ buildShiftedProfitLookups,
17
+ classifyCrossStrategyFeature,
18
+ collectSavedCrossStrategyFeatures,
19
+ evaluateRule,
20
+ evaluateCrossPocket,
21
+ ensureRuntimeBuild,
22
+ findFrameworkRepositoryRoot,
23
+ findSourceRepositoryRoot,
24
+ getSourceRepositoryKind,
25
+ loadStandaloneStrategyEntries,
26
+ filterSharedCrossStrategyFeatures,
27
+ formatCrossStrategyMarkdown,
28
+ formatMarkdownReport,
29
+ isVariantSelected,
30
+ latestDatasetGroupsByStrategy,
31
+ matchesPocket,
32
+ parseCliArgs,
33
+ parseRuleExpression,
34
+ parseVariant,
35
+ partitionCrossStrategyFeatures,
36
+ resolveArtifactProjectRoot,
37
+ splitRowsByTimestamp,
38
+ summarizeRows,
39
+ summarizeMovingAverageRedundancy,
40
+ } from './ai-gate-ablation.mjs';
41
+
42
+ const createGitCheckout = async (t, prefix) => {
43
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
44
+ t.after(() => fsp.rm(root, { recursive: true, force: true }));
45
+ execFileSync('git', ['init', '--quiet'], { cwd: root });
46
+ return fsp.realpath(root);
47
+ };
48
+
49
+ const preserveResearchRoots = (t) => {
50
+ const previousSourceRoot = process.env.TRADEJS_SOURCE_REPOSITORY_ROOT;
51
+ const previousFrameworkRoot = process.env.TRADEJS_FRAMEWORK_REPOSITORY_ROOT;
52
+ t.after(() => {
53
+ for (const [name, value] of [
54
+ ['TRADEJS_SOURCE_REPOSITORY_ROOT', previousSourceRoot],
55
+ ['TRADEJS_FRAMEWORK_REPOSITORY_ROOT', previousFrameworkRoot],
56
+ ]) {
57
+ if (value == null) delete process.env[name];
58
+ else process.env[name] = value;
59
+ }
60
+ });
61
+ };
62
+
63
+ test('requires an explicit TradeJS source repository', (t) => {
64
+ preserveResearchRoots(t);
65
+ delete process.env.TRADEJS_SOURCE_REPOSITORY_ROOT;
66
+
67
+ assert.throws(
68
+ () => findSourceRepositoryRoot(),
69
+ /TRADEJS_SOURCE_REPOSITORY_ROOT is required/,
70
+ );
71
+ });
72
+
73
+ test('separates a real standalone strategy checkout from the framework runtime checkout', async (t) => {
74
+ preserveResearchRoots(t);
75
+ const strategyRoot = await createGitCheckout(t, 'tradejs-strategy-source-');
76
+ const frameworkRoot = await createGitCheckout(t, 'tradejs-framework-source-');
77
+
78
+ await Promise.all([
79
+ fsp.mkdir(path.join(strategyRoot, 'src'), { recursive: true }),
80
+ fsp.mkdir(path.join(strategyRoot, 'dist'), { recursive: true }),
81
+ fsp.mkdir(path.join(frameworkRoot, 'packages/node'), { recursive: true }),
82
+ fsp.mkdir(path.join(frameworkRoot, 'packages/cli'), { recursive: true }),
83
+ ]);
84
+ await Promise.all([
85
+ fsp.writeFile(
86
+ path.join(strategyRoot, 'package.json'),
87
+ `${JSON.stringify(
88
+ {
89
+ name: '@tradejs/strategy-fixture',
90
+ type: 'module',
91
+ exports: { '.': { import: './dist/index.js' } },
92
+ },
93
+ null,
94
+ 2,
95
+ )}\n`,
96
+ ),
97
+ fsp.writeFile(
98
+ path.join(strategyRoot, 'src/index.ts'),
99
+ 'export const strategyEntries = [];\n',
100
+ ),
101
+ fsp.writeFile(
102
+ path.join(frameworkRoot, 'package.json'),
103
+ '{"name":"tradejs-framework-fixture","private":true}\n',
104
+ ),
105
+ ]);
106
+ await fsp.writeFile(
107
+ path.join(strategyRoot, 'dist/index.js'),
108
+ "export const strategyEntries = [{ manifest: { name: 'FixtureStrategy' } }];\n",
109
+ );
110
+
111
+ process.env.TRADEJS_SOURCE_REPOSITORY_ROOT = strategyRoot;
112
+ delete process.env.TRADEJS_FRAMEWORK_REPOSITORY_ROOT;
113
+
114
+ assert.equal(findSourceRepositoryRoot(), strategyRoot);
115
+ assert.equal(getSourceRepositoryKind(strategyRoot), 'strategy');
116
+ assert.throws(
117
+ () => findFrameworkRepositoryRoot(strategyRoot),
118
+ /TRADEJS_FRAMEWORK_REPOSITORY_ROOT is required/,
119
+ );
120
+
121
+ process.env.TRADEJS_FRAMEWORK_REPOSITORY_ROOT = frameworkRoot;
122
+ assert.equal(findFrameworkRepositoryRoot(strategyRoot), frameworkRoot);
123
+ const loaded = await loadStandaloneStrategyEntries(strategyRoot);
124
+ assert.equal(loaded.packageName, '@tradejs/strategy-fixture');
125
+ assert.equal(loaded.strategyEntries[0].manifest.name, 'FixtureStrategy');
126
+ });
127
+
128
+ test('uses a framework source checkout as its own runtime root', async (t) => {
129
+ preserveResearchRoots(t);
130
+ const frameworkRoot = await createGitCheckout(t, 'tradejs-framework-source-');
131
+ await Promise.all([
132
+ fsp.writeFile(
133
+ path.join(frameworkRoot, 'package.json'),
134
+ '{"name":"tradejs-framework-fixture","private":true}\n',
135
+ ),
136
+ fsp.mkdir(path.join(frameworkRoot, 'packages/node'), { recursive: true }),
137
+ fsp.mkdir(path.join(frameworkRoot, 'packages/cli'), { recursive: true }),
138
+ ]);
139
+ process.env.TRADEJS_SOURCE_REPOSITORY_ROOT = frameworkRoot;
140
+ delete process.env.TRADEJS_FRAMEWORK_REPOSITORY_ROOT;
141
+
142
+ assert.equal(findSourceRepositoryRoot(), frameworkRoot);
143
+ assert.equal(getSourceRepositoryKind(frameworkRoot), 'framework');
144
+ assert.equal(findFrameworkRepositoryRoot(frameworkRoot), frameworkRoot);
145
+ });
146
+
147
+ test('requires the public registry runtime module for plugin loading', async () => {
148
+ const projectRoot = await fsp.mkdtemp(
149
+ path.join(os.tmpdir(), 'ai-gate-runtime-build-'),
150
+ );
151
+ const requiredFiles = [
152
+ 'packages/node/dist/ai.mjs',
153
+ 'packages/cli/dist/lib/aiPocketSearch.js',
154
+ ];
155
+
156
+ for (const relativePath of requiredFiles) {
157
+ const filePath = path.join(projectRoot, relativePath);
158
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
159
+ await fsp.writeFile(filePath, '', 'utf8');
160
+ }
161
+
162
+ await assert.rejects(
163
+ ensureRuntimeBuild(projectRoot, null),
164
+ /packages\/node\/dist\/registry\.mjs/,
165
+ );
166
+
167
+ const registryModulePath = path.join(
168
+ projectRoot,
169
+ 'packages/node/dist/registry.mjs',
170
+ );
171
+ await fsp.writeFile(registryModulePath, '', 'utf8');
172
+ const resolved = await ensureRuntimeBuild(projectRoot, null);
173
+ assert.equal(resolved.registryModulePath, registryModulePath);
174
+ });
175
+
176
+ test('resolves datasets and reports from PROJECT_CWD independently of source cwd', () => {
177
+ const previousProjectCwd = process.env.PROJECT_CWD;
178
+ process.env.PROJECT_CWD = '/workspace/tradejs-project';
179
+ assert.equal(
180
+ resolveArtifactProjectRoot(),
181
+ path.resolve('/workspace/tradejs-project'),
182
+ );
183
+ if (previousProjectCwd == null) delete process.env.PROJECT_CWD;
184
+ else process.env.PROJECT_CWD = previousProjectCwd;
185
+ });
186
+
187
+ test('parses repeated variants and research windows', () => {
188
+ const options = parseCliArgs([
189
+ '--strategy',
190
+ 'LiquidityTails',
191
+ '--variant',
192
+ 'near-ma::filter::trend.distance <= 1.2',
193
+ '--variant=zones::exclude::structure.activeCount == 0',
194
+ '--terminalWindows=180,90,30,7',
195
+ '--qualityThresholds',
196
+ '4,5',
197
+ '--testSplit=0.2',
198
+ '--capacities=1,3,5',
199
+ '--maxLossValue=0.2',
200
+ ]);
201
+
202
+ assert.equal(options.strategy, 'LiquidityTails');
203
+ assert.deepEqual(options.variants, [
204
+ 'near-ma::filter::trend.distance <= 1.2',
205
+ 'zones::exclude::structure.activeCount == 0',
206
+ ]);
207
+ assert.deepEqual(options.terminalWindows, [180, 90, 30, 7]);
208
+ assert.deepEqual(options.qualityThresholds, [4, 5]);
209
+ assert.equal(options.testSplit, 0.2);
210
+ assert.deepEqual(options.capacities, [1, 3, 5]);
211
+ assert.equal(options.maxLossValue, 0.2);
212
+ });
213
+
214
+ test('parses cross-strategy discovery limits', () => {
215
+ const options = parseCliArgs([
216
+ '--crossStrategy',
217
+ '--validationSplit=0.2',
218
+ '--testSplit=0.2',
219
+ '--maxDepth=2',
220
+ '--minSupport=80',
221
+ '--minValidationSupport=40',
222
+ '--maxRowsPerStrategy=1200',
223
+ '--maxRowsPerEvent=2',
224
+ '--minFeatureStrategies=6',
225
+ '--minFeatureCoverage=0.6',
226
+ '--minBenchmarkFeatureCoverage=0.15',
227
+ '--portfolioCapacity=3',
228
+ ]);
229
+
230
+ assert.equal(options.crossStrategy, true);
231
+ assert.equal(options.validationSplit, 0.2);
232
+ assert.equal(options.testSplit, 0.2);
233
+ assert.equal(options.minSupport, 80);
234
+ assert.equal(options.minValidationSupport, 40);
235
+ assert.equal(options.maxRowsPerStrategy, 1200);
236
+ assert.equal(options.maxRowsPerEvent, 2);
237
+ assert.equal(options.minFeatureStrategies, 6);
238
+ assert.equal(options.minFeatureCoverage, 0.6);
239
+ assert.equal(options.minBenchmarkFeatureCoverage, 0.15);
240
+ assert.equal(options.portfolioCapacity, 3);
241
+ });
242
+
243
+ test('parses causal moving-average study options', () => {
244
+ const options = parseCliArgs([
245
+ '--movingAverageStudy',
246
+ '--maPeriods=20,0.5,10,5,15',
247
+ '--maLookbackBars=400',
248
+ '--maBatchSize=250',
249
+ '--maSqlTimeoutMs=120000',
250
+ ]);
251
+
252
+ assert.equal(options.movingAverageStudy, true);
253
+ assert.deepEqual(options.maPeriods, [5, 10, 15, 20]);
254
+ assert.equal(options.maLookbackBars, 400);
255
+ assert.equal(options.maBatchSize, 250);
256
+ assert.equal(options.maSqlTimeoutMs, 120000);
257
+ });
258
+
259
+ test('builds side, side-slope, and standalone variants for each MA', () => {
260
+ const variants = buildMovingAverageVariants([5]);
261
+ const aligned = {
262
+ movingAverages: {
263
+ SMA: {
264
+ 5: { directionalDistanceAtr: 0.1, directionalSlopeAtr5: 0.2 },
265
+ },
266
+ },
267
+ };
268
+
269
+ assert.equal(variants.length, 9);
270
+ assert.deepEqual(
271
+ variants.slice(0, 3).map((variant) => [variant.name, variant.mode]),
272
+ [
273
+ ['SMA5-side', 'filter'],
274
+ ['SMA5-side-slope', 'filter'],
275
+ ['SMA5-standalone', 'replace'],
276
+ ],
277
+ );
278
+ assert.equal(variants[0].match(aligned), true);
279
+ assert.equal(variants[1].match(aligned), true);
280
+ assert.equal(variants[2].match(aligned), true);
281
+ });
282
+
283
+ test('calculates causal SMA, EMA, and WMA from newest-first closes', () => {
284
+ const result = calculateMovingAverageGrid({
285
+ closes: [6, 5, 4, 3, 2, 1],
286
+ periods: [3],
287
+ lookbackBars: 6,
288
+ slopeBars: 1,
289
+ });
290
+
291
+ assert.equal(result.SMA[3].value, 5);
292
+ assert.equal(result.SMA[3].previous5, 4);
293
+ assert.equal(result.WMA[3].value, (6 * 3 + 5 * 2 + 4) / 6);
294
+ assert.equal(result.WMA[3].previous5, (5 * 3 + 4 * 2 + 3) / 6);
295
+ assert.ok(result.EMA[3].value > result.SMA[3].value);
296
+ assert.ok(result.EMA[3].previous5 < result.EMA[3].value);
297
+ });
298
+
299
+ test('summarizes redundancy across adjacent periods and MA families', () => {
300
+ const periods = [5, 10];
301
+ const rows = [1, 2, 3, 4].map((value) => ({
302
+ movingAverages: Object.fromEntries(
303
+ ['SMA', 'EMA', 'WMA'].map((family) => [
304
+ family,
305
+ {
306
+ 5: { directionalDistanceAtr: value },
307
+ 10: { directionalDistanceAtr: value * 2 },
308
+ },
309
+ ]),
310
+ ),
311
+ }));
312
+
313
+ const result = summarizeMovingAverageRedundancy(rows, periods);
314
+
315
+ assert.equal(result.adjacentPeriods.pairs, 3);
316
+ assert.equal(result.adjacentPeriods.min, 1);
317
+ assert.equal(result.samePeriodAcrossFamilies.pairs, 6);
318
+ assert.equal(result.samePeriodAcrossFamilies.median, 1);
319
+ });
320
+
321
+ test('selects the latest merged export independently per strategy', () => {
322
+ const groups = [
323
+ { strategyToken: 'Alpha', mergeId: '10', files: ['old'] },
324
+ { strategyToken: 'Beta', mergeId: '12', files: ['beta'] },
325
+ { strategyToken: 'alpha', mergeId: '15', files: ['new'] },
326
+ ];
327
+
328
+ assert.deepEqual(
329
+ latestDatasetGroupsByStrategy(groups).map((group) => group.files[0]),
330
+ ['new', 'beta'],
331
+ );
332
+ });
333
+
334
+ test('classifies pooled features by provenance instead of a broad blacklist', () => {
335
+ const source = {
336
+ 'signal.strategy': 'Alpha',
337
+ 'additionalIndicators.baseContext.regime.trend': 'up',
338
+ 'additionalIndicators.baseContext.regime.trend.psar.value': 100,
339
+ 'additionalIndicators.baseContext.structure.liquidityZones.nearestSupport.hitCount': 3,
340
+ 'additionalIndicators.baseContext.derivatives.intervals.15m.stale': true,
341
+ 'additionalIndicators.baseContext.derivatives.referenceContexts.BTCUSDT.intervals.1h.oiChangePct24h': 2,
342
+ 'additionalIndicators.baseContext.derivatives.source': 'cache',
343
+ 'additionalIndicators.baseContext.derivatives.referenceContexts.BTCUSDT.intervals.1h.openInterest': 100,
344
+ 'additionalIndicators.baseContext.gateFeatures.confirmations.count': 2,
345
+ 'additionalIndicators.baseContext.participation.volumeStructure.rowCount': 180,
346
+ 'additionalIndicators.baseContext.relative.cmcGlobal.totalMarketCapUsd': 1_000,
347
+ 'additionalIndicators.baseContext.relative.cmcGlobal.altMarketCapChange24hPct': 1.2,
348
+ 'additionalIndicators.baseContext.relative.cmcFearGreedValue': 45,
349
+ 'derived.stopDistanceBps': 50,
350
+ };
351
+ const profiles = partitionCrossStrategyFeatures(source);
352
+
353
+ assert.deepEqual(profiles.universal, {
354
+ 'additionalIndicators.baseContext.regime.trend': 'up',
355
+ 'additionalIndicators.baseContext.structure.liquidityZones.nearestSupport.hitCount': 3,
356
+ 'derived.stopDistanceBps': 50,
357
+ });
358
+ assert.equal(
359
+ profiles.benchmarkReference[
360
+ 'additionalIndicators.baseContext.derivatives.referenceContexts.BTCUSDT.intervals.1h.oiChangePct24h'
361
+ ],
362
+ 2,
363
+ );
364
+ assert.equal(
365
+ profiles.benchmarkReference[
366
+ 'additionalIndicators.baseContext.relative.cmcGlobal.altMarketCapChange24hPct'
367
+ ],
368
+ 1.2,
369
+ );
370
+ assert.equal(
371
+ profiles.rawNonstationary[
372
+ 'additionalIndicators.baseContext.derivatives.referenceContexts.BTCUSDT.intervals.1h.openInterest'
373
+ ],
374
+ 100,
375
+ );
376
+ assert.equal(
377
+ profiles.rawNonstationary[
378
+ 'additionalIndicators.baseContext.regime.trend.psar.value'
379
+ ],
380
+ 100,
381
+ );
382
+ assert.equal(
383
+ profiles.dataQuality[
384
+ 'additionalIndicators.baseContext.participation.volumeStructure.rowCount'
385
+ ],
386
+ 180,
387
+ );
388
+ assert.equal(
389
+ profiles.derivedPolicy[
390
+ 'additionalIndicators.baseContext.gateFeatures.confirmations.count'
391
+ ],
392
+ 2,
393
+ );
394
+ assert.equal(
395
+ classifyCrossStrategyFeature(
396
+ 'additionalIndicators.baseContext.relative.cmcGlobal.totalMarketCapUsd',
397
+ ).transform,
398
+ 'change / dominance / share / ratio',
399
+ );
400
+ assert.deepEqual(filterSharedCrossStrategyFeatures(source), {
401
+ 'additionalIndicators.baseContext.regime.trend': 'up',
402
+ 'additionalIndicators.baseContext.structure.liquidityZones.nearestSupport.hitCount': 3,
403
+ 'derived.stopDistanceBps': 50,
404
+ 'additionalIndicators.baseContext.derivatives.referenceContexts.BTCUSDT.intervals.1h.oiChangePct24h': 2,
405
+ 'additionalIndicators.baseContext.relative.cmcGlobal.altMarketCapChange24hPct': 1.2,
406
+ 'additionalIndicators.baseContext.relative.cmcFearGreedValue': 45,
407
+ });
408
+ });
409
+
410
+ test('collects cross-strategy features without flattening unrelated contexts', () => {
411
+ const features = collectSavedCrossStrategyFeatures({
412
+ signal: {
413
+ direction: 'LONG',
414
+ prices: {
415
+ currentPrice: 100,
416
+ stopLossPrice: 98,
417
+ takeProfitPrice: 104,
418
+ },
419
+ },
420
+ additionalIndicators: {
421
+ strategyContext: { specialEdge: 99 },
422
+ baseContext: {
423
+ raw: {
424
+ trend: { maFast: 99, maSlow: 101 },
425
+ momentum: { macdHistogram: 0.2 },
426
+ },
427
+ regime: { trend: { direction: 'up' } },
428
+ gateFeatures: { quality: 5, confirmations: { count: 2 } },
429
+ },
430
+ },
431
+ });
432
+
433
+ assert.equal(
434
+ features['additionalIndicators.baseContext.regime.trend.direction'],
435
+ 'up',
436
+ );
437
+ assert.equal(
438
+ features[
439
+ 'additionalIndicators.baseContext.gateFeatures.confirmations.count'
440
+ ],
441
+ 2,
442
+ );
443
+ assert.equal(
444
+ 'additionalIndicators.strategyContext.specialEdge' in features,
445
+ false,
446
+ );
447
+ assert.equal(
448
+ 'additionalIndicators.baseContext.gateFeatures.quality' in features,
449
+ false,
450
+ );
451
+ assert.equal(features['derived.stopDistanceBps'], 200);
452
+ assert.equal(features['derived.takeProfitDistanceBps'], 400);
453
+ assert.equal(features['derived.maFastAligned'], true);
454
+ assert.equal(features['derived.maSlowAligned'], false);
455
+ assert.equal(features['derived.macdHistogramAligned'], true);
456
+ });
457
+
458
+ test('balances discovery rows by strategy and timestamp', () => {
459
+ const rows = [
460
+ ...Array.from({ length: 5 }, (_, index) => ({
461
+ strategy: 'A',
462
+ timestamp: index < 3 ? 1 : index,
463
+ sequence: index,
464
+ symbol: `A${index}`,
465
+ })),
466
+ ...Array.from({ length: 2 }, (_, index) => ({
467
+ strategy: 'B',
468
+ timestamp: index + 1,
469
+ sequence: 10 + index,
470
+ symbol: `B${index}`,
471
+ })),
472
+ ];
473
+ const balanced = balanceCrossStrategyRows(rows, {
474
+ maxRowsPerStrategy: 2,
475
+ maxRowsPerEvent: 1,
476
+ });
477
+
478
+ assert.equal(balanced.filter((row) => row.strategy === 'A').length, 2);
479
+ assert.equal(balanced.filter((row) => row.strategy === 'B').length, 2);
480
+ assert.equal(
481
+ balanced.filter((row) => row.strategy === 'A' && row.timestamp === 1)
482
+ .length,
483
+ 1,
484
+ );
485
+ });
486
+
487
+ test('deduplicates benchmark discovery to timestamp events with macro strategy profit', () => {
488
+ const aggregation = aggregateBenchmarkDiscoveryRows([
489
+ {
490
+ strategy: 'A',
491
+ timestamp: 1,
492
+ sequence: 0,
493
+ direction: 'LONG',
494
+ profit: 1,
495
+ rawProfit: 1,
496
+ features: { shared: 2, conflict: 'a' },
497
+ },
498
+ {
499
+ strategy: 'A',
500
+ timestamp: 1,
501
+ sequence: 1,
502
+ direction: 'LONG',
503
+ profit: 3,
504
+ rawProfit: 3,
505
+ features: { shared: 2, conflict: 'b' },
506
+ },
507
+ {
508
+ strategy: 'B',
509
+ timestamp: 1,
510
+ sequence: 2,
511
+ direction: 'LONG',
512
+ profit: 5,
513
+ rawProfit: 5,
514
+ features: { shared: 2, conflict: 'a' },
515
+ },
516
+ ]);
517
+
518
+ assert.equal(aggregation.rows.length, 1);
519
+ assert.equal(aggregation.rows[0].profit, 3.5);
520
+ assert.deepEqual(aggregation.rows[0].features, { shared: 2 });
521
+ assert.equal(
522
+ aggregation.consistency.find((entry) => entry.feature === 'conflict')
523
+ .conflictEvents,
524
+ 1,
525
+ );
526
+ });
527
+
528
+ test('weights benchmark consensus by strategy before symbol fan-out', () => {
529
+ const rows = [
530
+ ...Array.from({ length: 5 }, (_, sequence) => ({
531
+ strategy: 'A',
532
+ timestamp: 1,
533
+ sequence,
534
+ profit: 1,
535
+ rawProfit: 1,
536
+ features: { vote: 'fanout' },
537
+ })),
538
+ {
539
+ strategy: 'B',
540
+ timestamp: 1,
541
+ sequence: 5,
542
+ profit: 1,
543
+ rawProfit: 1,
544
+ features: { vote: 'majority' },
545
+ },
546
+ {
547
+ strategy: 'C',
548
+ timestamp: 1,
549
+ sequence: 6,
550
+ profit: 1,
551
+ rawProfit: 1,
552
+ features: { vote: 'majority' },
553
+ },
554
+ ];
555
+ const aggregation = aggregateBenchmarkDiscoveryRows(rows, {
556
+ minConsensusRatio: 0.6,
557
+ });
558
+
559
+ assert.equal(aggregation.rows[0].features.vote, 'majority');
560
+ });
561
+
562
+ test('reports benchmark conflicts that occur inside strategy events', () => {
563
+ const aggregation = aggregateBenchmarkDiscoveryRows([
564
+ {
565
+ strategy: 'A',
566
+ timestamp: 1,
567
+ sequence: 0,
568
+ profit: 1,
569
+ rawProfit: 1,
570
+ features: { conflict: 'a' },
571
+ },
572
+ {
573
+ strategy: 'A',
574
+ timestamp: 1,
575
+ sequence: 1,
576
+ profit: 1,
577
+ rawProfit: 1,
578
+ features: { conflict: 'b' },
579
+ },
580
+ {
581
+ strategy: 'B',
582
+ timestamp: 1,
583
+ sequence: 2,
584
+ profit: 1,
585
+ rawProfit: 1,
586
+ features: { conflict: 'a' },
587
+ },
588
+ {
589
+ strategy: 'B',
590
+ timestamp: 1,
591
+ sequence: 3,
592
+ profit: 1,
593
+ rawProfit: 1,
594
+ features: { conflict: 'b' },
595
+ },
596
+ ]);
597
+ const consistency = aggregation.consistency.find(
598
+ (entry) => entry.feature === 'conflict',
599
+ );
600
+
601
+ assert.deepEqual(aggregation.rows[0].features, {});
602
+ assert.equal(consistency.observedEvents, 1);
603
+ assert.equal(consistency.conflictEvents, 1);
604
+ assert.equal(consistency.intraStrategyConflictEvents, 1);
605
+ assert.equal(consistency.crossStrategyConflictEvents, 0);
606
+ });
607
+
608
+ test('circular-shift controls rotate whole strategy timestamp blocks', () => {
609
+ const [lookup] = buildShiftedProfitLookups(
610
+ [
611
+ { strategy: 'A', timestamp: 1, sequence: 0, profit: 1 },
612
+ { strategy: 'A', timestamp: 1, sequence: 1, profit: 3 },
613
+ { strategy: 'A', timestamp: 2, sequence: 2, profit: 9 },
614
+ ],
615
+ { offsets: [1] },
616
+ );
617
+
618
+ assert.equal(lookup.get(0), 4.5);
619
+ assert.equal(lookup.get(1), 4.5);
620
+ assert.equal(lookup.get(2), 4);
621
+ });
622
+
623
+ test('applies one benchmark snapshot to every signal in an event', () => {
624
+ const rows = [
625
+ { timestamp: 1, sequence: 0, features: { shared: 'a' } },
626
+ { timestamp: 1, sequence: 1, features: { shared: 'b' } },
627
+ { timestamp: 2, sequence: 2, features: { shared: 'c' } },
628
+ ];
629
+ const applied = applyBenchmarkEventSnapshots(rows, [
630
+ { timestamp: 1, features: { shared: 'consensus' } },
631
+ { timestamp: 2, features: {} },
632
+ ]);
633
+
634
+ assert.deepEqual(
635
+ applied.map((row) => row.features),
636
+ [{ shared: 'consensus' }, { shared: 'consensus' }, {}],
637
+ );
638
+ assert.equal(
639
+ applied.filter((row) =>
640
+ matchesPocket(row, [
641
+ { featureKey: 'shared', op: '==', value: 'consensus' },
642
+ ]),
643
+ ).length,
644
+ 2,
645
+ );
646
+ });
647
+
648
+ test('rejects approval fan-out above configured portfolio capacity', () => {
649
+ let sequence = 0;
650
+ const buildPartition = (start) =>
651
+ Array.from({ length: 25 }, (_, eventIndex) =>
652
+ Array.from({ length: 6 }, (_, rowIndex) => ({
653
+ timestamp: start + eventIndex * 24 * 60 * 60 * 1000,
654
+ sequence: sequence++,
655
+ strategy: rowIndex < 3 ? 'A' : 'B',
656
+ symbol: `S${rowIndex}`,
657
+ direction: 'LONG',
658
+ profit: 1,
659
+ rawProfit: 1,
660
+ features: { keep: true },
661
+ })),
662
+ ).flat();
663
+ const split = {
664
+ train: buildPartition(Date.UTC(2025, 0, 1)),
665
+ tuning: buildPartition(Date.UTC(2025, 2, 1)),
666
+ test: buildPartition(Date.UTC(2025, 4, 1)),
667
+ };
668
+ const candidate = evaluateCrossPocket({
669
+ pocket: {
670
+ condition: 'keep == true',
671
+ predicates: [{ featureKey: 'keep', op: '==', value: true }],
672
+ },
673
+ split,
674
+ expectedSign: 1,
675
+ testShiftLookups: [],
676
+ minSharedStrategies: 2,
677
+ portfolioCapacity: 5,
678
+ });
679
+
680
+ assert.equal(candidate.train.maxBatch, 6);
681
+ assert.equal(candidate.checks.portfolioFanout, false);
682
+ });
683
+
684
+ test('builds a profiled cross-strategy report from tiny sharded exports', async (t) => {
685
+ const outDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'ai-cross-profile-'));
686
+ t.after(() => fsp.rm(outDir, { recursive: true, force: true }));
687
+ const start = Date.UTC(2025, 0, 1);
688
+ const buildRows = (strategy, offset) =>
689
+ Array.from({ length: 40 }, (_, index) => {
690
+ const direction = index % 2 === 0 ? 'LONG' : 'SHORT';
691
+ const timestamp = start + index * 15 * 60 * 1000;
692
+ return JSON.stringify({
693
+ timestamp,
694
+ direction,
695
+ profit: (index % 4 < 2 ? 1 : -0.8) + offset,
696
+ strategyName: strategy,
697
+ signalId: `${strategy}-${index}`,
698
+ symbol: index % 3 === 0 ? 'ETHUSDT' : 'SOLUSDT',
699
+ payload: {
700
+ signal: {
701
+ direction,
702
+ prices: {
703
+ currentPrice: 100,
704
+ stopLossPrice: 99,
705
+ takeProfitPrice: 102,
706
+ },
707
+ },
708
+ additionalIndicators: {
709
+ baseContext: {
710
+ raw: {
711
+ trend: { maFast: 99, maSlow: 101 },
712
+ volatility: { atrPct: 1.2 },
713
+ },
714
+ regime: { trend: { bias: index % 4 ? 'bull' : 'bear' } },
715
+ structure: {
716
+ localRange: { rangePosition20: index / 40 },
717
+ liquidityZones: { activeCount: index % 3 },
718
+ },
719
+ derivatives: {
720
+ source: 'coinalyze',
721
+ symbol: 'BTCUSDT',
722
+ intervals: {
723
+ '15m': {
724
+ stale: false,
725
+ points: 100,
726
+ openInterest: 1_000_000 + index,
727
+ oiChangePct1h: index / 10,
728
+ fundingZScore: index % 5,
729
+ liqImbalance: 0.1,
730
+ liqSpikeRatio: 1.1,
731
+ },
732
+ },
733
+ summary: { pressure: index % 4 ? 'long' : 'short' },
734
+ },
735
+ relative: {
736
+ cmcGlobal: {
737
+ stale: false,
738
+ totalMarketCapUsd: 1_000_000_000 + index,
739
+ altMarketCapChange24hPct: index / 20,
740
+ },
741
+ },
742
+ },
743
+ },
744
+ },
745
+ });
746
+ }).join('\n');
747
+ await Promise.all(
748
+ [
749
+ ['Alpha', 0],
750
+ ['Beta', 0.1],
751
+ ].map(([strategy, offset]) =>
752
+ fsp.writeFile(
753
+ path.join(outDir, `ai-dataset-${strategy}-merged-1.jsonl`),
754
+ `${buildRows(strategy, offset)}\n`,
755
+ ),
756
+ ),
757
+ );
758
+ const groups = [
759
+ {
760
+ strategyToken: 'Alpha',
761
+ mergeId: '1',
762
+ files: [path.join(outDir, 'ai-dataset-Alpha-merged-1.jsonl')],
763
+ },
764
+ {
765
+ strategyToken: 'Beta',
766
+ mergeId: '1',
767
+ files: [path.join(outDir, 'ai-dataset-Beta-merged-1.jsonl')],
768
+ },
769
+ ];
770
+ const report = await buildCrossStrategyReport({
771
+ projectRoot: process.cwd(),
772
+ sourceRepositoryRoot: process.cwd(),
773
+ frameworkRepositoryRoot: process.cwd(),
774
+ searchAiPockets: () => ({
775
+ positivePockets: [],
776
+ negativePockets: [],
777
+ stats: {},
778
+ }),
779
+ groups,
780
+ validationSplit: 0.2,
781
+ testSplit: 0.2,
782
+ maxDepth: 1,
783
+ minSupport: 1,
784
+ minValidationSupport: 1,
785
+ maxAtomicPredicates: 10,
786
+ maxCombinations: 20,
787
+ top: 2,
788
+ maxRowsPerStrategy: 100,
789
+ maxRowsPerEvent: 1,
790
+ minFeatureStrategies: 2,
791
+ minFeatureCoverage: 0.1,
792
+ minBenchmarkFeatureCoverage: 0.1,
793
+ });
794
+
795
+ assert.equal(report.run.overlapRows, 80);
796
+ assert.equal(
797
+ report.datasets.reduce((sum, dataset) => sum + dataset.overlapRows, 0),
798
+ 80,
799
+ );
800
+ assert.ok(report.profiles.universal.directions.LONG.features.length > 0);
801
+ assert.ok(
802
+ report.profiles.benchmarkReference.directions.SHORT.features.length > 0,
803
+ );
804
+ assert.equal(report.run.acceptance.minSharedStrategies, 2);
805
+ assert.equal(report.run.acceptance.portfolioCapacity, 5);
806
+ assert.ok(
807
+ report.profiles.benchmarkReference.directions.SHORT.featureConsistency.test,
808
+ );
809
+ assert.ok(report.audits.rawNonstationary.length > 0);
810
+ assert.match(formatCrossStrategyMarkdown(report), /Data-Quality Guard Audit/);
811
+ });
812
+
813
+ test('evaluates generated pocket predicates against feature maps', () => {
814
+ const row = { features: { trend: 'up', distance: 0.4 } };
815
+ assert.equal(
816
+ matchesPocket(row, [
817
+ { featureKey: 'trend', op: '==', value: 'up' },
818
+ { featureKey: 'distance', op: '<=', threshold: 0.5 },
819
+ ]),
820
+ true,
821
+ );
822
+ assert.equal(
823
+ matchesPocket(row, [{ featureKey: 'distance', op: '>=', threshold: 0.5 }]),
824
+ false,
825
+ );
826
+ });
827
+
828
+ test('evaluates numeric, string, boolean, and null predicates with precedence', () => {
829
+ const rule = parseRuleExpression(
830
+ '(trend.distance <= 1.2 && structure.zone == active) || flags.recovery == true',
831
+ );
832
+
833
+ assert.equal(
834
+ evaluateRule(rule, {
835
+ 'trend.distance': 1.1,
836
+ 'structure.zone': 'active',
837
+ 'flags.recovery': false,
838
+ }),
839
+ true,
840
+ );
841
+ assert.equal(
842
+ evaluateRule(rule, {
843
+ 'trend.distance': 1.3,
844
+ 'structure.zone': 'active',
845
+ 'flags.recovery': true,
846
+ }),
847
+ true,
848
+ );
849
+ assert.equal(
850
+ evaluateRule(parseRuleExpression('feature.value == null'), {
851
+ 'feature.value': null,
852
+ }),
853
+ true,
854
+ );
855
+ assert.equal(evaluateRule(rule, {}), false);
856
+ });
857
+
858
+ test('supports explicit pass-through and direction-aware policies', () => {
859
+ assert.equal(evaluateRule(parseRuleExpression('true'), {}), true);
860
+ assert.equal(evaluateRule(parseRuleExpression('false'), {}), false);
861
+
862
+ const rule = parseRuleExpression(
863
+ '(derived.direction == LONG && trend.distance <= 1) || (derived.direction == SHORT && structure.ageBars <= 47)',
864
+ );
865
+ assert.equal(
866
+ evaluateRule(rule, {
867
+ 'derived.direction': 'SHORT',
868
+ 'structure.ageBars': 42,
869
+ 'trend.distance': 3,
870
+ }),
871
+ true,
872
+ );
873
+ assert.equal(
874
+ evaluateRule(rule, {
875
+ 'derived.direction': 'LONG',
876
+ 'structure.ageBars': 42,
877
+ 'trend.distance': 3,
878
+ }),
879
+ false,
880
+ );
881
+ });
882
+
883
+ test('parses variant mode and optional assigned quality', () => {
884
+ const variant = parseVariant(
885
+ 'q3-recovery::add@4::context.bodyStrength >= 0.65',
886
+ );
887
+
888
+ assert.equal(variant.name, 'q3-recovery');
889
+ assert.equal(variant.mode, 'add');
890
+ assert.equal(variant.quality, 4);
891
+ assert.throws(
892
+ () => parseVariant('invalid::add@6::context.value == true'),
893
+ /Invalid added quality/,
894
+ );
895
+ });
896
+
897
+ test('parses and enforces an optional direction scope for gate repair', () => {
898
+ const variant = parseVariant(
899
+ 'short-rescue::add@4[SHORT]::structure.ageBars <= 42',
900
+ );
901
+
902
+ assert.equal(variant.direction, 'SHORT');
903
+ assert.equal(
904
+ isVariantSelected({
905
+ variant,
906
+ baselineSelected: false,
907
+ matches: true,
908
+ direction: 'SHORT',
909
+ threshold: 4,
910
+ defaultQuality: 4,
911
+ }),
912
+ true,
913
+ );
914
+ assert.equal(
915
+ isVariantSelected({
916
+ variant,
917
+ baselineSelected: false,
918
+ matches: true,
919
+ direction: 'LONG',
920
+ threshold: 4,
921
+ defaultQuality: 4,
922
+ }),
923
+ false,
924
+ );
925
+ assert.throws(
926
+ () => parseVariant('bad::add@4[SIDEWAYS]::feature.value == true'),
927
+ /Invalid direction scope/,
928
+ );
929
+ assert.equal(
930
+ evaluateRule(parseVariant('short-pass::add@4[SHORT]::true').rule, {}),
931
+ true,
932
+ );
933
+ });
934
+
935
+ test('applies filter, exclude, add, and replace selection semantics', () => {
936
+ const selected = (mode, baselineSelected, matches, quality = null) =>
937
+ isVariantSelected({
938
+ variant: { mode, quality },
939
+ baselineSelected,
940
+ matches,
941
+ direction: 'LONG',
942
+ threshold: 4,
943
+ defaultQuality: 4,
944
+ });
945
+
946
+ assert.equal(selected('filter', true, true), true);
947
+ assert.equal(selected('filter', true, false), false);
948
+ assert.equal(selected('exclude', true, true), false);
949
+ assert.equal(selected('exclude', true, false), true);
950
+ assert.equal(selected('add', false, true), true);
951
+ assert.equal(selected('add', false, true, 3), false);
952
+ assert.equal(selected('replace', true, false), false);
953
+ assert.equal(selected('replace', false, true), true);
954
+ });
955
+
956
+ test('calculates required profit, drawdown, strict-loss, and cadence metrics', () => {
957
+ const rows = [
958
+ {
959
+ timestamp: Date.UTC(2026, 0, 1),
960
+ profit: 10,
961
+ symbol: 'A',
962
+ direction: 'LONG',
963
+ },
964
+ {
965
+ timestamp: Date.UTC(2026, 0, 2),
966
+ profit: -4,
967
+ symbol: 'B',
968
+ direction: 'SHORT',
969
+ },
970
+ {
971
+ timestamp: Date.UTC(2026, 0, 3),
972
+ profit: -7,
973
+ symbol: 'A',
974
+ direction: 'LONG',
975
+ },
976
+ {
977
+ timestamp: Date.UTC(2026, 1, 1),
978
+ profit: 5,
979
+ symbol: 'A',
980
+ direction: 'LONG',
981
+ },
982
+ ];
983
+ const summary = summarizeRows(rows, 31);
984
+
985
+ assert.equal(summary.trades, 4);
986
+ assert.equal(summary.totalProfit, 4);
987
+ assert.equal(summary.winRate, 0.5);
988
+ assert.equal(summary.profitFactor, 15 / 11);
989
+ assert.equal(typeof summary.sharpeRatio, 'number');
990
+ assert.equal(typeof summary.sortinoRatio, 'number');
991
+ assert.equal(summary.calmarRatio, ((4 / 31) * 365) / 11);
992
+ assert.equal(summary.maxDrawdown, 11);
993
+ assert.equal(summary.largestLoss, -7);
994
+ assert.equal(summary.maxLossStreak, 2);
995
+ assert.equal(summary.losingMonths, 1);
996
+ assert.deepEqual(summary.losingMonthValues, [{ month: '2026-01', pnl: -1 }]);
997
+ assert.equal(summary.cadencePerDay, 4 / 31);
998
+ assert.equal(summary.cadencePerWeek, (4 / 31) * 7);
999
+ assert.equal(summary.averageProfitPerMonth, (4 / 31) * 30.4375);
1000
+ assert.equal(summary.events, 4);
1001
+ assert.equal(summary.eventsPerDay, 4 / 31);
1002
+ assert.equal(summary.activeDays, 4);
1003
+ assert.equal(summary.tradesPerEvent, 1);
1004
+ assert.equal(summary.p95Batch, 1);
1005
+ assert.equal(summary.maxBatch, 1);
1006
+ });
1007
+
1008
+ test('groups split and fan-out metrics by decision timestamp', () => {
1009
+ const first = Date.UTC(2026, 0, 1);
1010
+ const second = Date.UTC(2026, 0, 2);
1011
+ const third = Date.UTC(2026, 0, 3);
1012
+ const rows = [
1013
+ { timestamp: first, profit: 3, symbol: 'A', direction: 'LONG' },
1014
+ { timestamp: first, profit: -1, symbol: 'B', direction: 'LONG' },
1015
+ { timestamp: first, profit: 2, symbol: 'C', direction: 'LONG' },
1016
+ { timestamp: second, profit: 4, symbol: 'A', direction: 'LONG' },
1017
+ { timestamp: third, profit: 5, symbol: 'A', direction: 'LONG' },
1018
+ ];
1019
+ const summary = summarizeRows(rows, 3, {
1020
+ capacities: [1, 3],
1021
+ maxLossValue: 0.2,
1022
+ });
1023
+ const split = splitRowsByTimestamp(rows, 1 / 3, 1 / 3);
1024
+
1025
+ assert.equal(summary.events, 3);
1026
+ assert.equal(summary.tradesPerEvent, 5 / 3);
1027
+ assert.equal(summary.p95Batch, 3);
1028
+ assert.equal(summary.maxBatch, 3);
1029
+ assert.equal(summary.capacityStress['1'].accepted, 3);
1030
+ assert.equal(summary.capacityStress['1'].overflow, 2);
1031
+ assert.equal(summary.capacityStress['1'].overflowEvents, 1);
1032
+ assert.equal(summary.capacityStress['1'].maxSimultaneousStopRisk, 0.2);
1033
+ assert.ok(
1034
+ Math.abs(summary.capacityStress['3'].maxSimultaneousStopRisk - 0.6) <
1035
+ Number.EPSILON,
1036
+ );
1037
+ assert.deepEqual(
1038
+ [...new Set(split.train.map((row) => row.timestamp))],
1039
+ [first],
1040
+ );
1041
+ assert.deepEqual(
1042
+ [...new Set(split.tuning.map((row) => row.timestamp))],
1043
+ [second],
1044
+ );
1045
+ assert.deepEqual(
1046
+ [...new Set(split.test.map((row) => row.timestamp))],
1047
+ [third],
1048
+ );
1049
+ });
1050
+
1051
+ test('builds full and terminal period comparisons for a candidate', () => {
1052
+ const start = Date.UTC(2025, 0, 1);
1053
+ const variants = [parseVariant('keep::filter[SHORT]::feature.keep == true')];
1054
+ const rows = [
1055
+ {
1056
+ timestamp: start,
1057
+ profit: 10,
1058
+ symbol: 'A',
1059
+ direction: 'LONG',
1060
+ directionMatches: true,
1061
+ quality: 4,
1062
+ variantMatches: [true],
1063
+ },
1064
+ {
1065
+ timestamp: start + 200 * 24 * 60 * 60 * 1000,
1066
+ profit: -5,
1067
+ symbol: 'B',
1068
+ direction: 'SHORT',
1069
+ directionMatches: true,
1070
+ quality: 5,
1071
+ variantMatches: [false],
1072
+ },
1073
+ ];
1074
+ const report = buildAblationReport({
1075
+ rows,
1076
+ variants,
1077
+ minQuality: 4,
1078
+ qualityThresholds: [4, 5],
1079
+ terminalWindows: [180, 90, 30, 7],
1080
+ validationSplit: 0.25,
1081
+ testSplit: 0,
1082
+ maxLossValue: 0.2,
1083
+ filePaths: ['part1.jsonl'],
1084
+ });
1085
+
1086
+ assert.deepEqual(Object.keys(report.baseline.periods), [
1087
+ 'full',
1088
+ '180d',
1089
+ '90d',
1090
+ '30d',
1091
+ '7d',
1092
+ ]);
1093
+ assert.equal(report.baseline.periods.full.trades, 2);
1094
+ assert.equal(report.baseline.periodDirections.full.LONG.trades, 1);
1095
+ assert.equal(report.baseline.periodDirections.full.SHORT.trades, 1);
1096
+ assert.equal(report.baseline.periodDirections['180d'].LONG.trades, 0);
1097
+ assert.equal(report.baseline.periodDirections['180d'].SHORT.trades, 1);
1098
+ assert.equal(report.variants[0].periods.full.trades, 1);
1099
+ assert.equal(report.variants[0].periodDirections.full.LONG.trades, 1);
1100
+ assert.equal(report.variants[0].periodDirections.full.SHORT.trades, 0);
1101
+ assert.equal(report.variants[0].periodDirections['180d'].LONG.trades, 0);
1102
+ assert.equal(report.variants[0].periodDirections['180d'].SHORT.trades, 0);
1103
+ assert.equal(report.variants[0].direction, 'SHORT');
1104
+ assert.equal(report.variants[0].removed.trades, 1);
1105
+ assert.equal(report.run.trainEvents, 1);
1106
+ assert.equal(report.run.tuningEvents, 1);
1107
+ assert.equal(report.run.testEvents, 0);
1108
+ assert.deepEqual(report.run.partitions, {
1109
+ train: {
1110
+ rows: 1,
1111
+ events: 1,
1112
+ startTimestamp: new Date(start).toISOString(),
1113
+ endTimestamp: new Date(start).toISOString(),
1114
+ },
1115
+ tuning: {
1116
+ rows: 1,
1117
+ events: 1,
1118
+ startTimestamp: new Date(start + 200 * 24 * 60 * 60 * 1000).toISOString(),
1119
+ endTimestamp: new Date(start + 200 * 24 * 60 * 60 * 1000).toISOString(),
1120
+ },
1121
+ test: {
1122
+ rows: 0,
1123
+ events: 0,
1124
+ startTimestamp: null,
1125
+ endTimestamp: null,
1126
+ },
1127
+ });
1128
+ assert.match(formatMarkdownReport(report), /## Baseline/);
1129
+ assert.match(formatMarkdownReport(report), /Baseline Cadence and Fan-out/);
1130
+ assert.match(formatMarkdownReport(report), /Baseline Validation/);
1131
+ assert.match(formatMarkdownReport(report), /Approved Events/);
1132
+ assert.match(formatMarkdownReport(report), /## Variant: keep/);
1133
+ });
1134
+
1135
+ test('uses inclusive UTC calendar days for terminal active-day ratio', () => {
1136
+ const start = Date.UTC(2026, 0, 1, 18);
1137
+ const rows = [
1138
+ {
1139
+ timestamp: start,
1140
+ profit: 10,
1141
+ symbol: 'A',
1142
+ direction: 'LONG',
1143
+ directionMatches: true,
1144
+ quality: 4,
1145
+ variantMatches: [],
1146
+ },
1147
+ {
1148
+ timestamp: start + 7 * 24 * 60 * 60 * 1000,
1149
+ profit: 5,
1150
+ symbol: 'B',
1151
+ direction: 'LONG',
1152
+ directionMatches: true,
1153
+ quality: 4,
1154
+ variantMatches: [],
1155
+ },
1156
+ ];
1157
+ const report = buildAblationReport({
1158
+ rows,
1159
+ variants: [],
1160
+ minQuality: 4,
1161
+ qualityThresholds: [4, 5],
1162
+ terminalWindows: [7],
1163
+ validationSplit: 0,
1164
+ testSplit: 0,
1165
+ filePaths: ['part1.jsonl'],
1166
+ });
1167
+
1168
+ assert.equal(report.baseline.periods['7d'].activeDays, 2);
1169
+ assert.equal(report.baseline.periods['7d'].activeDayRatio, 0.25);
1170
+ });