phase-8f: persistant cache locally
This commit is contained in:
@@ -11,10 +11,16 @@ export class YahooFinanceClient {
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalise ticker before hitting Yahoo: BRK.B → BRK-B */
|
||||
private static normalise(ticker: string): string {
|
||||
return ticker.toUpperCase().replace(/\./g, '-');
|
||||
}
|
||||
|
||||
async fetchSummary(ticker: string, retries = 3, backoff = 1000): Promise<any> {
|
||||
const normalised = YahooFinanceClient.normalise(ticker);
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
try {
|
||||
return await this.lib.quoteSummary(ticker, { modules: YAHOO_MODULES });
|
||||
return await this.lib.quoteSummary(normalised, { modules: YAHOO_MODULES });
|
||||
} catch (error) {
|
||||
if (attempt === retries - 1) throw error;
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, backoff * (attempt + 1)));
|
||||
@@ -24,7 +30,9 @@ export class YahooFinanceClient {
|
||||
|
||||
async fetchCalendarEvents(ticker: string): Promise<any | null> {
|
||||
try {
|
||||
const result = await this.lib.quoteSummary(ticker, { modules: ['calendarEvents'] });
|
||||
const result = await this.lib.quoteSummary(YahooFinanceClient.normalise(ticker), {
|
||||
modules: ['calendarEvents'],
|
||||
});
|
||||
return result.calendarEvents ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -13,10 +13,6 @@ export class FinanceController {
|
||||
private readonly advisor: PortfolioAdvisor,
|
||||
) {}
|
||||
|
||||
private static normalizeYahoo(ticker: string): string {
|
||||
return ticker.toUpperCase().replace(/\./g, '-');
|
||||
}
|
||||
|
||||
register(app: FastifyInstance): void {
|
||||
app.get('/api/finance/portfolio', this.portfolio.bind(this));
|
||||
app.post('/api/finance/holdings', { schema: holdingSchema }, this.addHolding.bind(this));
|
||||
@@ -38,7 +34,7 @@ export class FinanceController {
|
||||
|
||||
const screenable = holdings
|
||||
.filter((h) => (h.type ?? 'stock') !== 'crypto')
|
||||
.map((h) => FinanceController.normalizeYahoo(h.ticker));
|
||||
.map((h) => h.ticker.toUpperCase());
|
||||
|
||||
const results =
|
||||
screenable.length > 0
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { YahooFinanceClient } from '../clients/YahooFinanceClient';
|
||||
import { REGIME } from '../config/constants';
|
||||
import type { MarketContext, Logger, BenchmarkProviderOptions } from '../types';
|
||||
|
||||
interface CacheFile {
|
||||
data: MarketContext;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export class BenchmarkProvider {
|
||||
private static readonly TTL_MS = 60 * 60 * 1000;
|
||||
private static readonly CACHE_PATH = '.benchmark-cache.json';
|
||||
|
||||
private static readonly DEFAULTS: MarketContext = {
|
||||
sp500Price: 5000,
|
||||
@@ -32,10 +39,33 @@ export class BenchmarkProvider {
|
||||
private readonly client: YahooFinanceClient,
|
||||
{ logger }: BenchmarkProviderOptions = {},
|
||||
) {
|
||||
this.cache = { data: null, expiresAt: 0 };
|
||||
this.cache = this.loadDiskCache();
|
||||
this.logger = logger ?? (console as unknown as Logger);
|
||||
}
|
||||
|
||||
private loadDiskCache(): { data: MarketContext | null; expiresAt: number } {
|
||||
try {
|
||||
if (!existsSync(BenchmarkProvider.CACHE_PATH)) return { data: null, expiresAt: 0 };
|
||||
const file = JSON.parse(readFileSync(BenchmarkProvider.CACHE_PATH, 'utf8')) as CacheFile;
|
||||
if (Date.now() < file.expiresAt) return { data: file.data, expiresAt: file.expiresAt };
|
||||
} catch {
|
||||
// corrupt or missing — ignore
|
||||
}
|
||||
return { data: null, expiresAt: 0 };
|
||||
}
|
||||
|
||||
private saveDiskCache(data: MarketContext, expiresAt: number): void {
|
||||
try {
|
||||
writeFileSync(
|
||||
BenchmarkProvider.CACHE_PATH,
|
||||
JSON.stringify({ data, expiresAt } satisfies CacheFile, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
} catch {
|
||||
// non-fatal — in-memory cache still works
|
||||
}
|
||||
}
|
||||
|
||||
async getMarketContext(): Promise<MarketContext> {
|
||||
if (this.cache.data && Date.now() < this.cache.expiresAt) return this.cache.data;
|
||||
|
||||
@@ -75,7 +105,9 @@ export class BenchmarkProvider {
|
||||
},
|
||||
};
|
||||
|
||||
this.cache = { data: context, expiresAt: Date.now() + BenchmarkProvider.TTL_MS };
|
||||
const expiresAt = Date.now() + BenchmarkProvider.TTL_MS;
|
||||
this.cache = { data: context, expiresAt };
|
||||
this.saveDiskCache(context, expiresAt);
|
||||
return context;
|
||||
} catch (err) {
|
||||
this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message);
|
||||
|
||||
Reference in New Issue
Block a user