import test from 'node:test'; import assert from 'node:assert/strict'; import { StockScorer } from '../server/domains/screener/scorers/StockScorer.js'; import { ScoringConfig } from '../server/domains/shared/scoring/ScoringConfig.js'; import type { StockMetrics } from '../server/domains/shared/types/models.model.js'; const DEFAULT_RULES = { gates: ScoringConfig.base.gates.STOCK, weights: ScoringConfig.base.weights.STOCK, thresholds: ScoringConfig.base.thresholds.STOCK, }; test('StockScorer', async (t) => { await t.test('rejects stock with high debt-to-equity ratio', () => { const metrics: StockMetrics = { peRatio: 15, pegRatio: 1.0, debtToEquity: 2.5, // Exceeds 1.5 gate quickRatio: 1.0, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('D/E')); }); await t.test('rejects stock with low quick ratio', () => { const metrics: StockMetrics = { peRatio: 15, pegRatio: 1.0, debtToEquity: 1.0, quickRatio: 0.5, // Below 0.8 gate returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('Quick')); }); await t.test('rejects stock with high P/E ratio', () => { const metrics: StockMetrics = { peRatio: 25, // Exceeds 15 gate pegRatio: 1.0, debtToEquity: 1.0, quickRatio: 1.0, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('P/E')); }); await t.test('rejects stock with high PEG ratio', () => { const metrics: StockMetrics = { peRatio: 15, pegRatio: 1.5, // Exceeds 1.0 gate debtToEquity: 1.0, quickRatio: 1.0, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('PEG')); }); await t.test('scores high-quality stock positively', () => { const metrics: StockMetrics = { peRatio: 12, // Below gate pegRatio: 0.8, // Below gate debtToEquity: 0.5, quickRatio: 1.2, returnOnEquity: 25, // High ROE operatingMargin: 20, netProfitMargin: 15, revenueGrowth: 10, fcfYield: 5, priceToBook: 2.0, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.notEqual(result.label, '🔴 REJECT'); // Should have positive score assert.ok(result.audit?.passedGates); }); await t.test('handles null/undefined metrics gracefully', () => { const metrics: StockMetrics = { peRatio: 15, pegRatio: 1.0, debtToEquity: null, quickRatio: 1.0, returnOnEquity: 20, operatingMargin: null, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); // Should not crash, null values are skipped in gate checks assert.ok(result); }); await t.test('passes all quality gates for strong stock', () => { const metrics: StockMetrics = { peRatio: 10, pegRatio: 0.9, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 30, operatingMargin: 25, netProfitMargin: 18, revenueGrowth: 8, fcfYield: 6, priceToBook: 3.0, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit?.passedGates); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('includes audit trail of gate checks', () => { const metrics: StockMetrics = { peRatio: 25, pegRatio: 1.0, debtToEquity: 1.0, quickRatio: 1.0, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 5, fcfYield: 3, priceToBook: 1.5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit); if (!result.audit?.passedGates) { assert.ok(Array.isArray(result.audit?.failures)); } }); await t.test('scores ROE as primary quality factor', () => { const metricsLowRoe: StockMetrics = { peRatio: 10, pegRatio: 0.9, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 5, // Low ROE operatingMargin: 25, netProfitMargin: 18, revenueGrowth: 8, fcfYield: 6, priceToBook: 3.0, } as any; const metricsHighRoe: StockMetrics = { peRatio: 10, pegRatio: 0.9, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 40, // High ROE operatingMargin: 25, netProfitMargin: 18, revenueGrowth: 8, fcfYield: 6, priceToBook: 3.0, } as any; const resultLow = StockScorer.score(metricsLowRoe, DEFAULT_RULES); const resultHigh = StockScorer.score(metricsHighRoe, DEFAULT_RULES); // Both should pass gates, but high ROE should score better assert.ok(resultLow.audit?.passedGates); assert.ok(resultHigh.audit?.passedGates); }); await t.test('rejects stock with multiple gate failures', () => { const metrics: StockMetrics = { peRatio: 25, // High pegRatio: 1.5, // High debtToEquity: 2.5, // High quickRatio: 0.5, // Low returnOnEquity: 5, operatingMargin: 5, netProfitMargin: 2, revenueGrowth: -5, fcfYield: -1, priceToBook: 0.5, } as any; const result = StockScorer.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('handles edge case of zero metrics', () => { const metrics: StockMetrics = { peRatio: 0, pegRatio: 0, debtToEquity: 0, quickRatio: 0, returnOnEquity: 0, operatingMargin: 0, netProfitMargin: 0, revenueGrowth: 0, fcfYield: 0, priceToBook: 0, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); // Zero quick ratio is a real value and fails the liquidity gate; // zero P/E, PEG, P/B are impossible values and are treated as missing. assert.ok(result); assert.equal(result.label, '🔴 REJECT'); assert.ok(result.scoreSummary.includes('Quick')); }); await t.test('treats zero revenue growth as a real (stagnant) value', () => { const metrics: StockMetrics = { peRatio: 12, pegRatio: 0.8, debtToEquity: 0.5, quickRatio: 1.2, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 0, // stagnant — must be scored, not skipped fcfYield: 5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit?.passedGates); // 0% growth is below revMed (5) → scores -1, same as slightly negative growth assert.equal(result.audit?.breakdown?.revenue, -1); }); await t.test('treats zero debt-to-equity as debt-free, not missing', () => { const metrics: StockMetrics = { peRatio: 12, pegRatio: 0.8, debtToEquity: 0, // debt-free — should pass the gate, not be skipped quickRatio: 1.2, returnOnEquity: 20, operatingMargin: 15, netProfitMargin: 10, revenueGrowth: 8, fcfYield: 5, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit?.passedGates); assert.notEqual(result.label, '🔴 REJECT'); }); await t.test('flags insufficient data instead of plain HOLD', () => { const metrics: StockMetrics = { currentPrice: 50 } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.equal(result.label, '🟡 HOLD (No Data)'); assert.equal(result.audit?.coverage?.active, 0); }); await t.test('returns structured tier and numeric score (P0.3)', () => { const strong: StockMetrics = { peRatio: 12, pegRatio: 0.7, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 30, operatingMargin: 25, netProfitMargin: 18, revenueGrowth: 12, fcfYield: 6, } as any; const pass = StockScorer.score(strong, DEFAULT_RULES); assert.equal(pass.tier, 'PASS'); assert.ok(typeof pass.score === 'number' && pass.score >= 4); const gated: StockMetrics = { ...strong, peRatio: 40 } as any; const reject = StockScorer.score(gated, DEFAULT_RULES); assert.equal(reject.tier, 'REJECT'); assert.equal(reject.score, null); const noData = StockScorer.score({ currentPrice: 50 } as any, DEFAULT_RULES); assert.equal(noData.tier, 'HOLD'); assert.equal(noData.score, 0); }); await t.test('reports factor coverage in audit', () => { const metrics: StockMetrics = { peRatio: 12, pegRatio: 0.8, quickRatio: 1.2, returnOnEquity: 20, currentPrice: 50, } as any; const result = StockScorer.score(metrics, DEFAULT_RULES); assert.ok(result.audit?.coverage); assert.ok(result.audit.coverage.active >= 1); assert.ok(result.audit.coverage.active <= result.audit.coverage.total); }); await t.test('scores based on configured thresholds', () => { const metricsLowMargin: StockMetrics = { peRatio: 10, pegRatio: 0.9, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 20, operatingMargin: 5, // Below medium threshold netProfitMargin: 18, revenueGrowth: 8, fcfYield: 6, priceToBook: 3.0, } as any; const metricsHighMargin: StockMetrics = { peRatio: 10, pegRatio: 0.9, debtToEquity: 0.3, quickRatio: 1.5, returnOnEquity: 20, operatingMargin: 25, // Above high threshold netProfitMargin: 18, revenueGrowth: 8, fcfYield: 6, priceToBook: 3.0, } as any; const resultLow = StockScorer.score(metricsLowMargin, DEFAULT_RULES); const resultHigh = StockScorer.score(metricsHighMargin, DEFAULT_RULES); // Both should pass gates assert.ok(resultLow.audit?.passedGates); assert.ok(resultHigh.audit?.passedGates); }); });