82 lines
2.4 KiB
JavaScript
82 lines
2.4 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { StockScorer } from '../server/screener/scorers/StockScorer.js';
|
|
|
|
const baseRules = {
|
|
gates: { maxDebtToEquity: 3.0, minQuickRatio: 0.5, maxPERatio: 20, maxPegGate: 1.5 },
|
|
weights: { margin: 2, opMargin: 2, roe: 3, peg: 2, revenue: 2, fcf: 2 },
|
|
thresholds: {
|
|
marginHigh: 20,
|
|
marginMed: 10,
|
|
opMarginHigh: 20,
|
|
opMarginMed: 10,
|
|
roeHigh: 20,
|
|
roeMed: 10,
|
|
pegHigh: 1.0,
|
|
pegMed: 1.5,
|
|
revHigh: 15,
|
|
revMed: 5,
|
|
fcfHigh: 5,
|
|
fcfMed: 2,
|
|
},
|
|
};
|
|
|
|
const pass = {
|
|
peRatio: 15,
|
|
pegRatio: 1.2,
|
|
debtToEquity: 1.0,
|
|
quickRatio: 1.0,
|
|
returnOnEquity: 22,
|
|
operatingMargin: 25,
|
|
netProfitMargin: 18,
|
|
revenueGrowth: 16,
|
|
fcfYield: 6,
|
|
};
|
|
|
|
test('rejects on high D/E', () => {
|
|
const result = StockScorer.score({ ...pass, debtToEquity: 4.0 }, baseRules);
|
|
assert.equal(result.label, '🔴 REJECT');
|
|
assert(result.scoreSummary.includes('D/E'));
|
|
});
|
|
|
|
test('rejects on high P/E', () => {
|
|
const result = StockScorer.score({ ...pass, peRatio: 25 }, baseRules);
|
|
assert.equal(result.label, '🔴 REJECT');
|
|
assert(result.scoreSummary.includes('P/E'));
|
|
});
|
|
|
|
test('rejects on high PEG', () => {
|
|
const result = StockScorer.score({ ...pass, pegRatio: 2.0 }, baseRules);
|
|
assert.equal(result.label, '🔴 REJECT');
|
|
});
|
|
|
|
test('skips gate when metric is null (missing data)', () => {
|
|
const result = StockScorer.score({ ...pass, pegRatio: null, peRatio: null }, baseRules);
|
|
assert.notEqual(result.label, '🔴 REJECT');
|
|
});
|
|
|
|
test('high-conviction BUY on strong metrics', () => {
|
|
const result = StockScorer.score(pass, baseRules);
|
|
assert.equal(result.label, '🟢 BUY (High Conviction)');
|
|
});
|
|
|
|
test('audit breakdown contains scored factors', () => {
|
|
const result = StockScorer.score(pass, baseRules);
|
|
assert(result.audit.passedGates);
|
|
assert(result.audit.breakdown.roe != null);
|
|
assert(result.audit.breakdown.margin != null);
|
|
});
|
|
|
|
test('beta > 1.5 surfaces as risk flag', () => {
|
|
const result = StockScorer.score({ ...pass, beta: 2.0 }, baseRules);
|
|
assert(result.audit.riskFlags?.some((f) => f.includes('High volatility')));
|
|
});
|
|
|
|
test('near 52-week high surfaces as risk flag', () => {
|
|
const result = StockScorer.score(
|
|
{ ...pass, week52High: 200, week52Low: 100, currentPrice: 195 },
|
|
baseRules,
|
|
);
|
|
assert(result.audit.riskFlags?.some((f) => f.includes('52-week high')));
|
|
});
|