phase-7: code restructure
This commit is contained in:
committed by
saikiranvella
parent
c160e65bd6
commit
357b0c0f6e
@@ -0,0 +1,124 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { StockScorer } from '../server/scorers/StockScorer';
|
||||
import type { StockMetrics } from '../server/types';
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
// Minimal fixture — tests exercise specific fields; unused metrics are null.
|
||||
const nullMetrics: Omit<
|
||||
StockMetrics,
|
||||
| 'sector'
|
||||
| 'capCategory'
|
||||
| 'growthCategory'
|
||||
| 'currentPrice'
|
||||
| 'peRatio'
|
||||
| 'pegRatio'
|
||||
| 'debtToEquity'
|
||||
| 'quickRatio'
|
||||
| 'returnOnEquity'
|
||||
| 'operatingMargin'
|
||||
| 'netProfitMargin'
|
||||
| 'revenueGrowth'
|
||||
| 'fcfYield'
|
||||
> = {
|
||||
priceToBook: null,
|
||||
grossMargin: null,
|
||||
earningsGrowth: null,
|
||||
pFFO: null,
|
||||
dividendYield: null,
|
||||
beta: null,
|
||||
week52High: null,
|
||||
week52Low: null,
|
||||
week52Change: null,
|
||||
week52FromHigh: null,
|
||||
week52FromLow: null,
|
||||
marketCap: null,
|
||||
analystRating: null,
|
||||
analystTargetPrice: null,
|
||||
analystUpside: null,
|
||||
numberOfAnalysts: null,
|
||||
dcfIntrinsicValue: null,
|
||||
dcfMarginOfSafety: null,
|
||||
};
|
||||
|
||||
const pass: StockMetrics = {
|
||||
...nullMetrics,
|
||||
sector: 'GENERAL',
|
||||
capCategory: 'Large Cap',
|
||||
growthCategory: 'Growth',
|
||||
currentPrice: 150,
|
||||
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