UI enhancemnts
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { BenchmarkProvider } from '../server/domains/shared/services/BenchmarkProvider.js';
|
||||
|
||||
// P0.5 — rate-regime hysteresis: the 10Y must cross a boundary by ±0.25%
|
||||
// before the regime flips, so a rate hovering at the threshold can't toggle
|
||||
// INFLATED gates between back-to-back requests.
|
||||
test('BenchmarkProvider.resolveRateRegime', async (t) => {
|
||||
await t.test('no previous regime → raw thresholds apply', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(1.5, null), 'LOW');
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(4.5, null), 'NORMAL');
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(5.1, null), 'HIGH');
|
||||
});
|
||||
|
||||
await t.test('NORMAL holds until 10Y clears 5.25%', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(5.1, 'NORMAL'), 'NORMAL'); // damped
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(5.25, 'NORMAL'), 'NORMAL'); // boundary
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(5.3, 'NORMAL'), 'HIGH'); // crossed
|
||||
});
|
||||
|
||||
await t.test('HIGH holds until 10Y drops below 4.75%', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(4.9, 'HIGH'), 'HIGH'); // damped
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(4.75, 'HIGH'), 'HIGH'); // boundary
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(4.7, 'HIGH'), 'NORMAL'); // crossed
|
||||
});
|
||||
|
||||
await t.test('LOW/NORMAL boundary at 2% gets the same damping', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(1.9, 'NORMAL'), 'NORMAL'); // damped
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(1.7, 'NORMAL'), 'LOW'); // crossed
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(2.1, 'LOW'), 'LOW'); // damped
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(2.3, 'LOW'), 'NORMAL'); // crossed
|
||||
});
|
||||
|
||||
await t.test('no change when raw regime equals previous', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(4.5, 'NORMAL'), 'NORMAL');
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(6.0, 'HIGH'), 'HIGH');
|
||||
});
|
||||
|
||||
await t.test('double jump (LOW→HIGH) is not damped', () => {
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(5.4, 'LOW'), 'HIGH');
|
||||
assert.equal(BenchmarkProvider.resolveRateRegime(1.2, 'HIGH'), 'LOW');
|
||||
});
|
||||
});
|
||||
@@ -255,6 +255,49 @@ test('EtfScorer', async (t) => {
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('does not reject ETF when Yahoo data is missing (null)', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.05,
|
||||
yield: 1.8,
|
||||
volume: null, // Yahoo did not return averageVolume
|
||||
fiveYearReturn: null, // Yahoo did not return fiveYearAverageReturn
|
||||
totalAssets: null,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
// Missing data skips gates — must NOT auto-fail as 0 < gate
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
assert.ok(result.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('still enforces expense gate when other data is missing', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.8, // above 0.2 gate
|
||||
yield: null,
|
||||
volume: null,
|
||||
fiveYearReturn: null,
|
||||
totalAssets: null,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
assert.ok(result.scoreSummary.includes('Expense ratio'));
|
||||
});
|
||||
|
||||
await t.test('labels all-null metrics as No Data instead of Neutral', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: null,
|
||||
yield: null,
|
||||
volume: null,
|
||||
fiveYearReturn: null,
|
||||
totalAssets: null,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🟡 Neutral (No Data)');
|
||||
assert.equal(result.audit?.coverage?.active, 0);
|
||||
});
|
||||
|
||||
await t.test('handles negative 5-year return', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { SignalSnapshotRepository } from '../server/domains/shared/persistence/SignalSnapshotRepository.js';
|
||||
import { MockDatabaseConnection } from './helpers/mockDb.js';
|
||||
import type { DatabaseConnection } from '../server/domains/shared/db/index.js';
|
||||
import type { ScoreResult } from '../server/domains/shared/types/index.js';
|
||||
|
||||
const passResult: ScoreResult = {
|
||||
label: '🟢 BUY (High Conviction)',
|
||||
tier: 'PASS',
|
||||
score: 9,
|
||||
scoreSummary: 'Score: 9',
|
||||
audit: { passedGates: true, breakdown: { roe: 3 }, coverage: { active: 6, total: 11 } },
|
||||
};
|
||||
|
||||
const rejectResult: ScoreResult = {
|
||||
label: '🔴 REJECT',
|
||||
tier: 'REJECT',
|
||||
score: null,
|
||||
scoreSummary: 'Gate failed: P/E 40 > 15',
|
||||
audit: { passedGates: false, failures: ['P/E 40 > 15'] },
|
||||
};
|
||||
|
||||
function repo(): SignalSnapshotRepository {
|
||||
return new SignalSnapshotRepository(
|
||||
new MockDatabaseConnection() as unknown as DatabaseConnection,
|
||||
);
|
||||
}
|
||||
|
||||
test('SignalSnapshotRepository', async (t) => {
|
||||
await t.test('record() builds a valid UPSERT (16 params, no throw)', () => {
|
||||
// QueryBuilder validates placeholder count — a param mismatch throws here.
|
||||
assert.doesNotThrow(() =>
|
||||
repo().record({
|
||||
ticker: 'aapl',
|
||||
assetType: 'STOCK',
|
||||
price: 189.5,
|
||||
signal: '✅ Strong Buy',
|
||||
fundamental: passResult,
|
||||
inflated: passResult,
|
||||
rateRegime: 'NORMAL',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('record() tolerates gate-failed results (null score)', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
repo().record({
|
||||
ticker: 'XYZ',
|
||||
assetType: 'STOCK',
|
||||
price: null,
|
||||
signal: '❌ Avoid',
|
||||
fundamental: rejectResult,
|
||||
inflated: rejectResult,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('recordBatch() returns count written', () => {
|
||||
const n = repo().recordBatch([
|
||||
{
|
||||
ticker: 'AAPL',
|
||||
assetType: 'STOCK',
|
||||
price: 189.5,
|
||||
signal: '✅ Strong Buy',
|
||||
fundamental: passResult,
|
||||
inflated: passResult,
|
||||
},
|
||||
{
|
||||
ticker: 'MSFT',
|
||||
assetType: 'STOCK',
|
||||
price: 425.3,
|
||||
signal: '🔄 Neutral',
|
||||
fundamental: passResult,
|
||||
inflated: passResult,
|
||||
},
|
||||
]);
|
||||
assert.equal(n, 2);
|
||||
});
|
||||
|
||||
await t.test('read methods build valid queries', () => {
|
||||
const r = repo();
|
||||
assert.deepEqual(r.history('aapl'), []);
|
||||
assert.deepEqual(r.byDate('2026-06-09'), []);
|
||||
assert.deepEqual(r.latestBefore('2026-06-09'), []);
|
||||
});
|
||||
});
|
||||
@@ -238,8 +238,97 @@ test('StockScorer', async (t) => {
|
||||
} as any;
|
||||
|
||||
const result = StockScorer.score(metrics, DEFAULT_RULES);
|
||||
// Should handle gracefully (zero is falsy, treated as null)
|
||||
// Zero quick ratio is a real value and fails the liquidity gate;
|
||||
// zero P/E, PEG, P/B are impossible values and are treated as missing.
|
||||
assert.ok(result);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
assert.ok(result.scoreSummary.includes('Quick'));
|
||||
});
|
||||
|
||||
await t.test('treats zero revenue growth as a real (stagnant) value', () => {
|
||||
const metrics: StockMetrics = {
|
||||
peRatio: 12,
|
||||
pegRatio: 0.8,
|
||||
debtToEquity: 0.5,
|
||||
quickRatio: 1.2,
|
||||
returnOnEquity: 20,
|
||||
operatingMargin: 15,
|
||||
netProfitMargin: 10,
|
||||
revenueGrowth: 0, // stagnant — must be scored, not skipped
|
||||
fcfYield: 5,
|
||||
} as any;
|
||||
|
||||
const result = StockScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.ok(result.audit?.passedGates);
|
||||
// 0% growth is below revMed (5) → scores -1, same as slightly negative growth
|
||||
assert.equal(result.audit?.breakdown?.revenue, -1);
|
||||
});
|
||||
|
||||
await t.test('treats zero debt-to-equity as debt-free, not missing', () => {
|
||||
const metrics: StockMetrics = {
|
||||
peRatio: 12,
|
||||
pegRatio: 0.8,
|
||||
debtToEquity: 0, // debt-free — should pass the gate, not be skipped
|
||||
quickRatio: 1.2,
|
||||
returnOnEquity: 20,
|
||||
operatingMargin: 15,
|
||||
netProfitMargin: 10,
|
||||
revenueGrowth: 8,
|
||||
fcfYield: 5,
|
||||
} as any;
|
||||
|
||||
const result = StockScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.ok(result.audit?.passedGates);
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('flags insufficient data instead of plain HOLD', () => {
|
||||
const metrics: StockMetrics = { currentPrice: 50 } as any;
|
||||
|
||||
const result = StockScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🟡 HOLD (No Data)');
|
||||
assert.equal(result.audit?.coverage?.active, 0);
|
||||
});
|
||||
|
||||
await t.test('returns structured tier and numeric score (P0.3)', () => {
|
||||
const strong: StockMetrics = {
|
||||
peRatio: 12,
|
||||
pegRatio: 0.7,
|
||||
debtToEquity: 0.3,
|
||||
quickRatio: 1.5,
|
||||
returnOnEquity: 30,
|
||||
operatingMargin: 25,
|
||||
netProfitMargin: 18,
|
||||
revenueGrowth: 12,
|
||||
fcfYield: 6,
|
||||
} as any;
|
||||
const pass = StockScorer.score(strong, DEFAULT_RULES);
|
||||
assert.equal(pass.tier, 'PASS');
|
||||
assert.ok(typeof pass.score === 'number' && pass.score >= 4);
|
||||
|
||||
const gated: StockMetrics = { ...strong, peRatio: 40 } as any;
|
||||
const reject = StockScorer.score(gated, DEFAULT_RULES);
|
||||
assert.equal(reject.tier, 'REJECT');
|
||||
assert.equal(reject.score, null);
|
||||
|
||||
const noData = StockScorer.score({ currentPrice: 50 } as any, DEFAULT_RULES);
|
||||
assert.equal(noData.tier, 'HOLD');
|
||||
assert.equal(noData.score, 0);
|
||||
});
|
||||
|
||||
await t.test('reports factor coverage in audit', () => {
|
||||
const metrics: StockMetrics = {
|
||||
peRatio: 12,
|
||||
pegRatio: 0.8,
|
||||
quickRatio: 1.2,
|
||||
returnOnEquity: 20,
|
||||
currentPrice: 50,
|
||||
} as any;
|
||||
|
||||
const result = StockScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.ok(result.audit?.coverage);
|
||||
assert.ok(result.audit.coverage.active >= 1);
|
||||
assert.ok(result.audit.coverage.active <= result.audit.coverage.total);
|
||||
});
|
||||
|
||||
await t.test('scores based on configured thresholds', () => {
|
||||
|
||||
Reference in New Issue
Block a user