phase-6: typescript introduction

This commit is contained in:
Kazuma
2026-06-04 22:16:48 -04:00
parent 16bd95aa85
commit 69d13c3dbe
69 changed files with 2323 additions and 1036 deletions
+87
View File
@@ -0,0 +1,87 @@
import { YahooClient } from './YahooClient.js';
import { REGIME } from '../config/constants.js';
import type { MarketContext, Logger } from '../types.js';
const TTL_MS = 60 * 60 * 1000;
const DEFAULTS: MarketContext = {
sp500Price: 5000,
riskFreeRate: 4.5,
vixLevel: 20,
rateRegime: 'HIGH',
volatilityRegime: 'NORMAL',
benchmarks: { marketPE: 22, techPE: 30, reitYield: 3.5, igSpread: 1.0 },
};
const rateRegime = (rate: number): MarketContext['rateRegime'] =>
rate < 2 ? REGIME.LOW : rate <= 5 ? REGIME.NORMAL : REGIME.HIGH;
const volRegime = (vix: number): MarketContext['volatilityRegime'] =>
vix < 15 ? REGIME.LOW : vix <= 25 ? REGIME.NORMAL : REGIME.HIGH;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pe = (summary: any): number | null =>
summary?.summaryDetail?.trailingPE ?? summary?.defaultKeyStatistics?.forwardPE ?? null;
interface BenchmarkProviderOptions {
logger?: Logger;
}
export class BenchmarkProvider {
private client: YahooClient;
private cache: { data: MarketContext | null; expiresAt: number };
private logger: Logger;
constructor({ logger }: BenchmarkProviderOptions = {}) {
this.client = new YahooClient();
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: rateRegime(riskFreeRate),
volatilityRegime: volRegime(vixLevel),
benchmarks: {
marketPE: pe(spy) ?? 22,
techPE: 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() + TTL_MS };
return context;
} catch (err) {
this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message);
return this.cache.data ?? DEFAULTS;
}
}
}