import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BondScorer } from '../src/screener/scorers/BondScorer.js'; // ytm is stored as a percentage value (e.g. 6.5 = 6.5%), matching how DataMapper outputs it. // BondScorer._sanitize divides by 100 to convert to decimal before spread calculation. const rules = { gates: { minCreditRating: 7 }, weights: { yieldSpread: 3, duration: 2 }, thresholds: { minSpread: 1.0, maxDuration: 10 }, }; const ctx = { riskFreeRate: 4.5 }; test('rejects bond below investment-grade floor', () => { const result = BondScorer.score( { ytm: 8.0, duration: 5, creditRating: 'BB', creditRatingNumeric: 6 }, rules, ctx, ); assert.equal(result.label, '🔴 Avoid'); assert(result.scoreSummary.includes('Gate failed')); }); test('attractive for wide spread and short duration', () => { // ytm=6.5%, riskFree=4.5% → spreadPct=(0.065-0.045)*100=2.0% >= minSpread 1.0% const result = BondScorer.score( { ytm: 6.5, duration: 4, creditRating: 'AA', creditRatingNumeric: 9 }, rules, ctx, ); assert.equal(result.label, '🟢 Attractive'); }); test('spread calculation: ytm% → decimal, subtract riskFreeRate/100, back to %', () => { const result = BondScorer.score( { ytm: 6.5, duration: 5, creditRating: 'AAA', creditRatingNumeric: 10 }, rules, ctx, ); assert.equal(result.audit.breakdown.spread, rules.weights.yieldSpread); }); test('fails spread when yield barely above risk-free', () => { // ytm=4.7%, riskFree=4.5% → spreadPct=0.2% < minSpread 1.0% const result = BondScorer.score( { ytm: 4.7, duration: 5, creditRating: 'AAA', creditRatingNumeric: 10 }, rules, ctx, ); assert.equal(result.audit.breakdown.spread, -2); }); test('penalises long duration', () => { const result = BondScorer.score( { ytm: 6.5, duration: 15, creditRating: 'AA', creditRatingNumeric: 9 }, rules, ctx, ); assert.equal(result.audit.breakdown.duration, -1); });