UI enhancemnts

This commit is contained in:
Kazuma
2026-06-09 19:34:31 -04:00
parent fbadd7fb6e
commit 5655cde6bf
55 changed files with 6226 additions and 465 deletions
@@ -12,19 +12,52 @@ export class BenchmarkProvider {
private static readonly TTL_MS = 60 * 60 * 1000;
private static readonly CACHE_PATH = '.benchmark-cache.json';
// NOTE: regimes must stay consistent with rateRegime()/volRegime() below —
// 4.5% ⇒ NORMAL (25%), VIX 20 ⇒ NORMAL (1525).
private static readonly DEFAULTS: MarketContext = {
sp500Price: 5000,
riskFreeRate: 4.5,
vixLevel: 20,
rateRegime: 'HIGH',
rateRegime: 'NORMAL',
volatilityRegime: 'NORMAL',
benchmarks: { marketPE: 22, techPE: 30, reitYield: 3.5, igSpread: 1.0 },
};
/** Hysteresis band: the 10Y must cross a regime boundary by this much to flip. */
private static readonly REGIME_HYSTERESIS = 0.25;
private static rateRegime(rate: number): MarketContext['rateRegime'] {
return rate < 2 ? REGIME.LOW : rate <= 5 ? REGIME.NORMAL : REGIME.HIGH;
}
/**
* Rate regime with hysteresis (PRODUCT.md P0.5).
*
* The raw thresholds (2% / 5%) flip the INFLATED scoring gates between
* back-to-back requests when the 10Y hovers near a boundary. With a known
* previous regime, the rate must cross the boundary by ±0.25% before the
* regime switches. A two-step jump (LOW→HIGH) applies immediately.
* Public static for direct unit testing.
*/
static resolveRateRegime(
rate: number,
previous: MarketContext['rateRegime'] | null,
): MarketContext['rateRegime'] {
const raw = BenchmarkProvider.rateRegime(rate);
if (!previous || raw === previous) return raw;
const h = BenchmarkProvider.REGIME_HYSTERESIS;
if (previous === REGIME.NORMAL && raw === REGIME.HIGH)
return rate > 5 + h ? REGIME.HIGH : REGIME.NORMAL;
if (previous === REGIME.HIGH && raw === REGIME.NORMAL)
return rate < 5 - h ? REGIME.NORMAL : REGIME.HIGH;
if (previous === REGIME.NORMAL && raw === REGIME.LOW)
return rate < 2 - h ? REGIME.LOW : REGIME.NORMAL;
if (previous === REGIME.LOW && raw === REGIME.NORMAL)
return rate > 2 + h ? REGIME.NORMAL : REGIME.LOW;
return raw; // LOW↔HIGH double jump — no damping
}
private static volRegime(vix: number): MarketContext['volatilityRegime'] {
return vix < 15 ? REGIME.LOW : vix <= 25 ? REGIME.NORMAL : REGIME.HIGH;
}
@@ -34,6 +67,8 @@ export class BenchmarkProvider {
}
private cache: { data: MarketContext | null; expiresAt: number };
private logger: Logger;
/** Last known rate regime — survives cache expiry so hysteresis has memory. */
private lastRegime: MarketContext['rateRegime'] | null = null;
constructor(
private readonly client: YahooFinanceClient,
@@ -47,6 +82,8 @@ export class BenchmarkProvider {
try {
if (!existsSync(BenchmarkProvider.CACHE_PATH)) return { data: null, expiresAt: 0 };
const file = JSON.parse(readFileSync(BenchmarkProvider.CACHE_PATH, 'utf8')) as CacheFile;
// Even an expired cache remembers the previous regime for hysteresis
this.lastRegime = file.data?.rateRegime ?? null;
if (Date.now() < file.expiresAt) return { data: file.data, expiresAt: file.expiresAt };
} catch {
// corrupt or missing — ignore
@@ -95,7 +132,7 @@ export class BenchmarkProvider {
sp500Price,
riskFreeRate,
vixLevel,
rateRegime: BenchmarkProvider.rateRegime(riskFreeRate),
rateRegime: BenchmarkProvider.resolveRateRegime(riskFreeRate, this.lastRegime),
volatilityRegime: BenchmarkProvider.volRegime(vixLevel),
benchmarks: {
marketPE: BenchmarkProvider.pe(spy) ?? 22,
@@ -107,6 +144,7 @@ export class BenchmarkProvider {
const expiresAt = Date.now() + BenchmarkProvider.TTL_MS;
this.cache = { data: context, expiresAt };
this.lastRegime = context.rateRegime;
this.saveDiskCache(context, expiresAt);
return context;
} catch (err) {