phase-10.5: screener enhancements

This commit is contained in:
saikiranvella
2026-06-11 19:18:19 -04:00
parent bac00ab5d5
commit e953822bab
51 changed files with 3745 additions and 36 deletions
@@ -1,5 +1,5 @@
import YahooFinance from 'yahoo-finance2';
import type { YahooNewsItem, YahooSearchOptions, YahooFinanceLib } from '../types';
import type { YahooNewsItem, YahooSearchOptions, YahooFinanceLib, PricePoint } from '../types';
import { YAHOO_MODULES } from '../config/constants';
export class YahooFinanceClient {
@@ -49,4 +49,71 @@ export class YahooFinanceClient {
const { news = [] } = await this.lib.search(query, opts);
return news;
}
/**
* Top holdings of an ETF (ticker symbols, largest weight first).
* Used for sector drill-down. Returns [] on any failure.
*/
async fetchTopHoldings(etf: string, limit = 10): Promise<string[]> {
try {
const result = await this.lib.quoteSummary(
YahooFinanceClient.normalise(etf),
{ modules: ['topHoldings'] },
{ validateResult: false },
);
const holdings = (result?.topHoldings?.holdings ?? []) as Array<{ symbol?: string }>;
return holdings
.map((h) => h.symbol)
.filter((s): s is string => Boolean(s))
.slice(0, limit)
.map((s) => s.toUpperCase());
} catch {
return [];
}
}
/** Chart range presets — Robinhood/Yahoo-style. Intraday for short ranges. */
static readonly CHART_RANGES: Record<string, { days: number; interval: string }> = {
'1d': { days: 1, interval: '5m' },
'5d': { days: 5, interval: '30m' },
'1mo': { days: 30, interval: '1d' },
'3mo': { days: 91, interval: '1d' },
'6mo': { days: 182, interval: '1d' },
ytd: { days: 0, interval: '1d' }, // days computed dynamically (Jan 1 → now)
'1y': { days: 365, interval: '1d' },
'5y': { days: 1826, interval: '1wk' }, // weekly bars keep ~260 points
};
/**
* Closing prices for a named range (ticker modal chart). Intraday ranges
* keep the full timestamp; daily ranges keep the date only.
* Returns [] on any failure — the chart is a nice-to-have, never a blocker.
*/
async fetchCloses(ticker: string, range = '6mo'): Promise<PricePoint[]> {
const preset = YahooFinanceClient.CHART_RANGES[range] ?? YahooFinanceClient.CHART_RANGES['6mo'];
try {
const period1 =
range === 'ytd'
? new Date(Date.UTC(new Date().getUTCFullYear(), 0, 1))
: new Date(Date.now() - preset.days * 24 * 60 * 60 * 1000);
const result = await this.lib.chart(
YahooFinanceClient.normalise(ticker),
{ period1, interval: preset.interval },
{ validateResult: false },
);
const quotes = (result?.quotes ?? []) as Array<{ date?: string | Date; close?: number }>;
const intraday = preset.interval !== '1d';
return quotes
.filter((q) => q.close != null && q.date != null)
.map((q) => {
const iso = new Date(q.date as string | Date).toISOString();
return {
date: intraday ? iso : iso.slice(0, 10),
close: +(q.close as number).toFixed(2),
};
});
} catch {
return [];
}
}
}