71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { RuleMerger } from '../server/services/RuleMerger';
|
|
import { SCORE_MODE } from '../server/config/constants';
|
|
import type { MarketContext } from '../server/types';
|
|
|
|
const ctx = {
|
|
benchmarks: { marketPE: 25, techPE: 32, reitYield: 3.8, igSpread: 1.2 },
|
|
} as Partial<MarketContext>;
|
|
|
|
test('FUNDAMENTAL mode returns Graham-style P/E gate', () => {
|
|
const rules = RuleMerger.getRulesForAsset(
|
|
'STOCK',
|
|
{ sector: 'GENERAL' },
|
|
ctx as MarketContext,
|
|
SCORE_MODE.FUNDAMENTAL,
|
|
);
|
|
assert.equal(rules.gates.maxPERatio, 15); // updated: Graham's real rule is 15x
|
|
assert.equal(rules.gates.maxPegGate, 1.0); // updated: Lynch PEG standard
|
|
});
|
|
|
|
test('INFLATED mode loosens P/E gate from live SPY data', () => {
|
|
const rules = RuleMerger.getRulesForAsset(
|
|
'STOCK',
|
|
{ sector: 'GENERAL' },
|
|
ctx as MarketContext,
|
|
SCORE_MODE.INFLATED,
|
|
);
|
|
assert.equal(rules.gates.maxPERatio, Math.round(25 * 1.5)); // 37
|
|
assert(rules.gates.maxPERatio > 15, 'Inflated P/E should exceed fundamental 15x');
|
|
});
|
|
|
|
test('INFLATED tech P/E gate uses XLK benchmark', () => {
|
|
const rules = RuleMerger.getRulesForAsset(
|
|
'STOCK',
|
|
{ sector: 'TECHNOLOGY' },
|
|
ctx as MarketContext,
|
|
SCORE_MODE.INFLATED,
|
|
);
|
|
assert.equal(rules.gates.maxPERatio, Math.round(32 * 1.3)); // 42
|
|
});
|
|
|
|
test('Sector override applied before inflated overrides', () => {
|
|
const rules = RuleMerger.getRulesForAsset(
|
|
'STOCK',
|
|
{ sector: 'REIT' },
|
|
ctx as MarketContext,
|
|
SCORE_MODE.FUNDAMENTAL,
|
|
);
|
|
assert.equal(rules.gates.maxPERatio, 9999);
|
|
assert.equal(rules.weights.yield, 5);
|
|
assert.equal(rules.weights.margin, 0);
|
|
});
|
|
|
|
test('SECTOR_OVERRIDE is deleted from returned rules', () => {
|
|
const rules = RuleMerger.getRulesForAsset(
|
|
'STOCK',
|
|
{ sector: 'GENERAL' },
|
|
ctx as MarketContext,
|
|
SCORE_MODE.FUNDAMENTAL,
|
|
) as unknown as Record<string, unknown>;
|
|
assert.equal(rules.SECTOR_OVERRIDE, undefined);
|
|
});
|
|
|
|
test('throws for unknown asset type', () => {
|
|
assert.throws(
|
|
() => RuleMerger.getRulesForAsset('CRYPTO' as never, {}, ctx as MarketContext),
|
|
/No rules configured/,
|
|
);
|
|
});
|