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); // Should handle gracefully (zero is falsy, treated as null) assert.ok(result); }); 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); }); });