import test from 'node:test'; import assert from 'node:assert/strict'; import { BondScorer } from '../server/domains/screener/scorers/BondScorer.js'; import { ScoringConfig } from '../server/domains/shared/scoring/ScoringConfig.js'; import type { BondMetrics } from '../server/domains/shared/types/models.model.js'; const DEFAULT_RULES = { gates: ScoringConfig.base.gates.BOND, weights: ScoringConfig.base.weights.BOND, thresholds: ScoringConfig.base.thresholds.BOND, }; test('BondScorer', async (t) => { await t.test('rejects bond with low credit rating', () => { const metrics: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'BB', // Below BBB (gate is BBB = 7) creditRatingNumeric: 5, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('Credit')); }); await t.test('accepts bond with BBB credit rating', () => { const metrics: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'BBB', creditRatingNumeric: 7, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('accepts bond with A credit rating', () => { const metrics: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'A', creditRatingNumeric: 9, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('scores high-yield bond positively', () => { const metrics: BondMetrics = { ytm: 7.5, // High yield duration: 5, creditRating: 'A', creditRatingNumeric: 9, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); assert.ok(result.audit?.passedGates); }); await t.test('penalizes long-duration bond', () => { const metricsShort: BondMetrics = { ytm: 5.5, duration: 3, // Short creditRating: 'A', creditRatingNumeric: 9, }; const metricsLong: BondMetrics = { ytm: 5.5, duration: 10, // Long creditRating: 'A', creditRatingNumeric: 9, }; const resultShort = BondScorer.score(metricsShort, DEFAULT_RULES); const resultLong = BondScorer.score(metricsLong, DEFAULT_RULES); // Both should pass gates assert.ok(resultShort.audit?.passedGates); assert.ok(resultLong.audit?.passedGates); }); await t.test('handles null/undefined metrics gracefully', () => { const metrics: BondMetrics = { ytm: null, duration: 5, creditRating: null, creditRatingNumeric: null, }; const result = BondScorer.score(metrics, DEFAULT_RULES); // Should not crash assert.ok(result); }); await t.test('includes audit trail of gate checks', () => { const metrics: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'BB', creditRatingNumeric: 5, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit); if (!result.audit?.passedGates) { assert.ok(Array.isArray(result.audit?.failures)); } }); await t.test('accepts high-quality bond (AAA rated)', () => { const metrics: BondMetrics = { ytm: 4.0, // Lower yield (less risk) duration: 7, creditRating: 'AAA', creditRatingNumeric: 10, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit?.passedGates); }); await t.test('scores spread relative to risk-free rate', () => { const metricsWideSpread: BondMetrics = { ytm: 7.5, // Wide spread from ~4.5% risk-free duration: 5, creditRating: 'BBB', creditRatingNumeric: 7, }; const metricsTightSpread: BondMetrics = { ytm: 4.8, // Tight spread duration: 5, creditRating: 'BBB', creditRatingNumeric: 7, }; const resultWide = BondScorer.score(metricsWideSpread, DEFAULT_RULES); const resultTight = BondScorer.score(metricsTightSpread, DEFAULT_RULES); // Both should pass gates assert.ok(resultWide.audit?.passedGates); assert.ok(resultTight.audit?.passedGates); }); await t.test('rejects bond with very long duration', () => { const metrics: BondMetrics = { ytm: 5.5, duration: 20, // Much longer than default gate creditRating: 'A', creditRatingNumeric: 9, }; const result = BondScorer.score(metrics, DEFAULT_RULES); // May fail duration gate if threshold is enforced assert.ok(result); }); await t.test('handles extreme yield scenarios', () => { const metricsVeryHigh: BondMetrics = { ytm: 15.0, // Very high (distressed) duration: 5, creditRating: 'B', creditRatingNumeric: 4, }; const metricsVeryLow: BondMetrics = { ytm: 2.0, // Very low (low risk) duration: 5, creditRating: 'AAA', creditRatingNumeric: 10, }; const resultHigh = BondScorer.score(metricsVeryHigh, DEFAULT_RULES); const resultLow = BondScorer.score(metricsVeryLow, DEFAULT_RULES); // High yield bond likely fails credit gate assert.ok(resultHigh); // Low yield AAA bond should pass assert.ok(resultLow.audit?.passedGates); }); await t.test('scores based on credit rating thresholds', () => { const metricsJustAboveGate: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'BBB', creditRatingNumeric: 7, // Exactly at gate }; const metricsWellAboveGate: BondMetrics = { ytm: 5.5, duration: 5, creditRating: 'AA', creditRatingNumeric: 9, // Well above gate }; const resultAt = BondScorer.score(metricsJustAboveGate, DEFAULT_RULES); const resultAbove = BondScorer.score(metricsWellAboveGate, DEFAULT_RULES); // Both should pass assert.ok(resultAt.audit?.passedGates); assert.ok(resultAbove.audit?.passedGates); }); await t.test('handles negative YTM (unlikely but possible)', () => { const metrics: BondMetrics = { ytm: -0.5, // Negative yield (Swiss bonds in past) duration: 2, creditRating: 'AAA', creditRatingNumeric: 10, }; const result = BondScorer.score(metrics, DEFAULT_RULES); // Should handle gracefully assert.ok(result); }); await t.test('investment-grade bond scores well', () => { const metrics: BondMetrics = { ytm: 5.0, duration: 5, creditRating: 'A', creditRatingNumeric: 8, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); assert.ok(result.audit?.passedGates); }); await t.test('speculative-grade bond rejected', () => { const metrics: BondMetrics = { ytm: 8.5, duration: 5, creditRating: 'CCC', creditRatingNumeric: 3, }; const result = BondScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); }); });