const test = require('node:test'); const assert = require('node:assert/strict'); const { normalizeEventType, horizonBucket, cohortKey, legacyCohortKey, calibrateOutcomes, EVENT_FAMILY_NAMES, } = require('../src/autonomy/calibration'); const { decide, DEFAULT_POLICY_RULES } = require('../src/autonomy/policy'); test('event types collapse onto a small closed set of families', () => { assert.equal(normalizeEventType('earnings_beat'), 'earnings'); assert.equal(normalizeEventType('Q3 Earnings Report'), 'earnings'); assert.equal(normalizeEventType('guidance_raise'), 'guidance'); assert.equal(normalizeEventType('supply_constraint'), 'supply_chain'); assert.equal(normalizeEventType('supplyConstraint'), 'supply_chain'); assert.equal(normalizeEventType('antitrust probe'), 'regulatory'); assert.equal(normalizeEventType('ceo_resignation'), 'leadership'); assert.equal(normalizeEventType('analyst-downgrade'), 'analyst_action'); assert.equal(normalizeEventType('share buyback'), 'capital'); assert.equal(normalizeEventType('data breach'), 'security_incident'); assert.equal(normalizeEventType('interest rate decision'), 'macro'); assert.equal(normalizeEventType('product_launch'), 'product'); assert.equal(normalizeEventType('acquisition_rumor'), 'm_and_a'); assert.equal(normalizeEventType('patent lawsuit'), 'legal'); }); test('unrecognised or empty event types fall back to other, never to their own cohort', () => { assert.equal(normalizeEventType('zebra_convention'), 'other'); assert.equal(normalizeEventType(''), 'other'); assert.equal(normalizeEventType(' '), 'other'); assert.equal(normalizeEventType(null), 'other'); assert.equal(normalizeEventType(undefined), 'other'); assert.equal(normalizeEventType(42), 'other'); assert.ok(EVENT_FAMILY_NAMES.includes('other')); assert.ok(EVENT_FAMILY_NAMES.length <= 15, `taxonomy grew to ${EVENT_FAMILY_NAMES.length} families`); }); test('every allowed horizon lands in one of three buckets', () => { assert.equal(horizonBucket(1), 'short'); assert.equal(horizonBucket(5), 'short'); assert.equal(horizonBucket(10), 'medium'); assert.equal(horizonBucket(20), 'medium'); assert.equal(horizonBucket(30), 'long'); assert.equal(horizonBucket(60), 'long'); assert.equal(horizonBucket(90), 'long'); assert.equal(horizonBucket(null), 'unknown'); assert.equal(horizonBucket('nope'), 'unknown'); }); test('cohort key is versioned, coarse and stable, and the legacy key is still available', () => { assert.equal( cohortKey({ direction: 'positive', eventType: 'earnings_beat', horizonDays: 10 }), 'v2|unknown|earnings|medium|positive' ); // different raw event text, same family and horizon bucket -> same cohort assert.equal( cohortKey({ direction: 'positive', eventType: 'quarterly results miss', horizonDays: 20 }), cohortKey({ direction: 'positive', eventType: 'earnings_beat', horizonDays: 10 }) ); assert.notEqual( cohortKey({ direction: 'negative', eventType: 'earnings_beat', horizonDays: 10 }), cohortKey({ direction: 'positive', eventType: 'earnings_beat', horizonDays: 10 }) ); assert.equal( legacyCohortKey({ direction: 'positive', eventType: 'earnings_beat', horizonDays: 10 }), 'unknown|earnings_beat|10|positive' ); }); test('the coarse taxonomy actually collapses a realistic spread of free text', () => { const raw = [ 'earnings_beat', 'earnings_miss', 'q2_earnings', 'revenue_growth', 'margin_expansion', 'guidance_raise', 'guidance_cut', 'outlook_downgrade', 'profit_warning', 'supply_constraint', 'chip_shortage', 'production_halt', 'capacity_expansion', 'analyst_upgrade', 'price_target_raise', 'ceo_departure', 'board_shakeup', 'antitrust_probe', 'export_controls', 'tariff_announcement', ]; const families = new Set(raw.map(normalizeEventType)); assert.ok(families.size <= 8, `expected heavy collapse, got ${families.size} families`); }); test('calibrateOutcomes reports instrument diversity and concentration', () => { const rows = [ { excess_return: 0.02, direction_correct: 1, instrument: 'NVDA' }, { excess_return: 0.01, direction_correct: 1, instrument: 'nvda' }, { excess_return: -0.01, direction_correct: 0, instrument: 'NVDA' }, { excess_return: 0.03, direction_correct: 1, instrument: 'AMD' }, ]; const result = calibrateOutcomes(rows); assert.equal(result.sampleSize, 4); assert.equal(result.distinctInstruments, 2); assert.equal(result.topInstrumentShare, 0.75); const empty = calibrateOutcomes([]); assert.equal(empty.distinctInstruments, 0); assert.equal(empty.topInstrumentShare, null); const unlabelled = calibrateOutcomes([{ excess_return: 0.01, direction_correct: 1 }]); assert.equal(unlabelled.distinctInstruments, 0); }); test('the diversification gate blocks single ticker cohorts however large they are', () => { const base = { direction: 'positive', probability: 0.8, expectedExcessReturn: 0.02, lowerReturn: -0.01 }; // 300 samples, one name: this is the NVDA case, and it must not qualify const concentrated = decide({ ...base, sampleSize: 300, distinctInstruments: 1, topInstrumentShare: 1 }); assert.equal(concentrated.action, 'ABSTAIN'); assert.match(concentrated.rationale, /diversity/); // enough names but still dominated by one of them const dominated = decide({ ...base, sampleSize: 300, distinctInstruments: 9, topInstrumentShare: 0.82 }); assert.equal(dominated.action, 'ABSTAIN'); assert.match(dominated.rationale, /dominated/); // pre-diversification snapshots carry no count, unknown is not adequate const unknown = decide({ ...base, sampleSize: 300, distinctInstruments: null, topInstrumentShare: null }); assert.equal(unknown.action, 'ABSTAIN'); assert.match(unknown.rationale, /unknown/); const qualified = decide({ ...base, sampleSize: 40, distinctInstruments: 9, topInstrumentShare: 0.3 }); assert.equal(qualified.action, 'BUY'); }); test('both evidence thresholds are overridable and default conservatively', () => { assert.equal(DEFAULT_POLICY_RULES.minSampleSize, 30); assert.equal(DEFAULT_POLICY_RULES.minDistinctInstruments, 5); assert.equal(DEFAULT_POLICY_RULES.maxInstrumentConcentration, 0.5); const input = { direction: 'positive', probability: 0.8, expectedExcessReturn: 0.02, lowerReturn: -0.01, sampleSize: 12, distinctInstruments: 3, topInstrumentShare: 0.4, }; assert.equal(decide(input).action, 'ABSTAIN'); assert.equal(decide(input, { minSampleSize: 10, minDistinctInstruments: 2 }).action, 'BUY'); assert.equal(decide(input, { minSampleSize: 10, minDistinctInstruments: 2, maxInstrumentConcentration: 0.3 }).action, 'ABSTAIN'); }); test('sample size gate still runs before the diversity gate', () => { const result = decide({ direction: 'positive', probability: 0.9, expectedExcessReturn: 0.05, sampleSize: 2, distinctInstruments: 40, topInstrumentShare: 0.1, }); assert.equal(result.action, 'ABSTAIN'); assert.match(result.rationale, /insufficient calibration sample/); }); test('a missing concentration share cannot sneak past the cap as a zero', () => { const base = { direction: 'positive', probability: 0.8, expectedExcessReturn: 0.02, lowerReturn: -0.01, sampleSize: 300, distinctInstruments: 40, }; // Number(null) is 0, which used to slide straight under the cap even though we // had no idea what the real concentration was. for (const share of [null, undefined]) { const verdict = decide({ ...base, topInstrumentShare: share }); assert.equal(verdict.action, 'ABSTAIN'); assert.match(verdict.rationale, /concentration unknown/); } // a genuinely broad cohort still gets through, we havent just bolted it shut const broad = decide({ ...base, topInstrumentShare: 0.12 }); assert.equal(broad.action, 'BUY'); });