refactor: restructure to clean architecture
fix: restore ScoringConfig improvements lost in refactor commit docs: rewrite README and CLAUDE.md to reflect current architecture code-format code fixes
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { StockScorer } from '../src/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')));
|
||||
});
|
||||
Reference in New Issue
Block a user