86 lines
3.1 KiB
TypeScript
86 lines
3.1 KiB
TypeScript
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 cache: { data: MarketContext | null; expiresAt: number };
|
|
private logger: Logger;
|
|
|
|
constructor(
|
|
private readonly client: YahooFinanceClient,
|
|
{ logger }: BenchmarkProviderOptions = {},
|
|
) {
|
|
this.cache = { data: null, expiresAt: 0 };
|
|
this.logger = logger ?? (console as unknown as Logger);
|
|
}
|
|
|
|
async getMarketContext(): Promise<MarketContext> {
|
|
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;
|
|
}
|
|
}
|
|
}
|