import test from 'node:test'; import assert from 'node:assert/strict'; import { EtfScorer } from '../server/domains/screener/scorers/EtfScorer.js'; import { ScoringConfig } from '../server/domains/shared/scoring/ScoringConfig.js'; import type { EtfMetrics } from '../server/domains/shared/types/models.model.js'; const DEFAULT_RULES = { gates: ScoringConfig.base.gates.ETF, weights: ScoringConfig.base.weights.ETF, thresholds: ScoringConfig.base.thresholds.ETF, }; test('EtfScorer', async (t) => { await t.test('rejects ETF with high expense ratio', () => { const metrics: EtfMetrics = { expenseRatio: 0.8, // Exceeds 0.2% gate yield: 2.5, volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('Expense ratio')); }); await t.test('accepts ETF with low expense ratio', () => { const metrics: EtfMetrics = { expenseRatio: 0.05, // Well below 0.2% gate yield: 2.5, volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('rejects ETF with zero expense ratio (data issue)', () => { const metrics: EtfMetrics = { expenseRatio: 0, // Zero treated as missing/invalid yield: 2.5, volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); // Zero is treated as null/missing data assert.ok(result); }); await t.test('rejects ETF with low 5-year return', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 5000000, fiveYearReturn: 5, // Below 8% gate totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('5-year return')); }); await t.test('accepts ETF with strong 5-year return', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 5000000, fiveYearReturn: 12, // Above 8% gate totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('rejects ETF with insufficient volume', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 500000, // Below 1M gate fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('Volume')); }); await t.test('accepts ETF with strong volume', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 10000000, // Well above 1M gate fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('scores high-quality ETF positively', () => { const metrics: EtfMetrics = { expenseRatio: 0.05, // Low yield: 3.0, // Decent volume: 20000000, // High fiveYearReturn: 15, // Strong totalAssets: 50e9, // Large }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); assert.ok(result.audit?.passedGates); }); await t.test('handles null/undefined metrics gracefully', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: null, volume: 5000000, fiveYearReturn: 10, totalAssets: null, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); // Should not crash, null values are skipped assert.ok(result); }); await t.test('includes audit trail of gate checks', () => { const metrics: EtfMetrics = { expenseRatio: 0.8, yield: 2.5, volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit); if (!result.audit?.passedGates) { assert.ok(Array.isArray(result.audit?.failures)); } }); await t.test('scores yield as secondary factor', () => { const metricsLowYield: EtfMetrics = { expenseRatio: 0.1, yield: 0.5, // Low yield volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const metricsHighYield: EtfMetrics = { expenseRatio: 0.1, yield: 4.0, // High yield volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const resultLow = EtfScorer.score(metricsLowYield, DEFAULT_RULES); const resultHigh = EtfScorer.score(metricsHighYield, DEFAULT_RULES); // Both should pass gates assert.ok(resultLow.audit?.passedGates); assert.ok(resultHigh.audit?.passedGates); }); await t.test('rejects ETF with multiple gate failures', () => { const metrics: EtfMetrics = { expenseRatio: 1.0, // High yield: 0.5, volume: 100000, // Low fiveYearReturn: 3, // Low totalAssets: 100e6, // Small }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); // Should have multiple failures if (!result.audit?.passedGates && result.audit?.failures) { assert.ok(result.audit.failures.length > 1); } }); await t.test('calculates score based on thresholds', () => { const metricsLowReturn: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 5000000, fiveYearReturn: 8.5, // Just above gate totalAssets: 5e9, }; const metricsHighReturn: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 5000000, fiveYearReturn: 15, // Well above gate totalAssets: 5e9, }; const resultLow = EtfScorer.score(metricsLowReturn, DEFAULT_RULES); const resultHigh = EtfScorer.score(metricsHighReturn, DEFAULT_RULES); // Both should pass assert.ok(resultLow.audit?.passedGates); assert.ok(resultHigh.audit?.passedGates); }); await t.test('penalizes low-volume ETF', () => { const metricsLowVolume: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 2000000, // Low but above gate fiveYearReturn: 10, totalAssets: 5e9, }; const metricsHighVolume: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 50000000, // High volume fiveYearReturn: 10, totalAssets: 5e9, }; const resultLow = EtfScorer.score(metricsLowVolume, DEFAULT_RULES); const resultHigh = EtfScorer.score(metricsHighVolume, DEFAULT_RULES); // Both pass gates, but high volume should score higher assert.ok(resultLow.audit?.passedGates); assert.ok(resultHigh.audit?.passedGates); }); await t.test('handles extremely high expense ratio', () => { const metrics: EtfMetrics = { expenseRatio: 5.0, // 5%! yield: 2.5, volume: 5000000, fiveYearReturn: 10, totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); }); await t.test('handles negative 5-year return', () => { const metrics: EtfMetrics = { expenseRatio: 0.1, yield: 2.5, volume: 5000000, fiveYearReturn: -5, // Negative return totalAssets: 5e9, }; const result = EtfScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); }); });