import { YahooFinanceClient } from '../clients/YahooFinanceClient'; import { REGIME } from '../config/constants'; import type { MarketContext, Logger, BenchmarkProviderOptions } from '../types'; export class BenchmarkProvider { private static readonly TTL_MS = 60 * 60 * 1000; private static readonly DEFAULTS: MarketContext = { sp500Price: 5000, riskFreeRate: 4.5, vixLevel: 20, rateRegime: 'HIGH', volatilityRegime: 'NORMAL', benchmarks: { marketPE: 22, techPE: 30, reitYield: 3.5, igSpread: 1.0 }, }; private static rateRegime(rate: number): MarketContext['rateRegime'] { return rate < 2 ? REGIME.LOW : rate <= 5 ? REGIME.NORMAL : REGIME.HIGH; } private static volRegime(vix: number): MarketContext['volatilityRegime'] { return vix < 15 ? REGIME.LOW : vix <= 25 ? REGIME.NORMAL : REGIME.HIGH; } private static pe(summary: any): number | null { return summary?.summaryDetail?.trailingPE ?? summary?.defaultKeyStatistics?.forwardPE ?? null; } private client: YahooFinanceClient; private cache: { data: MarketContext | null; expiresAt: number }; private logger: Logger; constructor({ logger }: BenchmarkProviderOptions = {}) { this.client = new YahooFinanceClient(); this.cache = { data: null, expiresAt: 0 }; this.logger = logger ?? (console as unknown as Logger); } async getMarketContext(): Promise { if (this.cache.data && Date.now() < this.cache.expiresAt) return this.cache.data; try { const [sp500, tn10y, vix, spy, xlk, xlre, lqd] = await Promise.all([ this.client.fetchSummary('^GSPC'), this.client.fetchSummary('^TNX'), this.client.fetchSummary('^VIX'), this.client.fetchSummary('SPY'), this.client.fetchSummary('XLK'), this.client.fetchSummary('XLRE'), this.client.fetchSummary('LQD'), ]); const riskFreeRate = (sp500 as any)?.price?.regularMarketPrice !== undefined ? ((tn10y as any)?.price?.regularMarketPrice ?? 0) : 0; const sp500Price = (sp500 as any)?.price?.regularMarketPrice ?? 0; const vixLevel = (vix as any)?.price?.regularMarketPrice ?? 0; if (!sp500Price || !riskFreeRate) throw new Error('Invalid market data (zero values)'); const lqdYield = ((lqd as any)?.summaryDetail?.trailingAnnualDividendYield ?? 0) * 100; const context: MarketContext = { sp500Price, riskFreeRate, vixLevel, rateRegime: BenchmarkProvider.rateRegime(riskFreeRate), volatilityRegime: BenchmarkProvider.volRegime(vixLevel), benchmarks: { marketPE: BenchmarkProvider.pe(spy) ?? 22, techPE: BenchmarkProvider.pe(xlk) ?? 30, reitYield: ((xlre as any)?.summaryDetail?.trailingAnnualDividendYield ?? 0.035) * 100, igSpread: Math.max(0.1, lqdYield - riskFreeRate), }, }; this.cache = { data: context, expiresAt: Date.now() + BenchmarkProvider.TTL_MS }; return context; } catch (err) { this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message); return this.cache.data ?? BenchmarkProvider.DEFAULTS; } } }