feat: short interest panel Phase 1 Yahoo Finance

- Add short_interest TtlClass (24h) to CacheRepository.ts with shortInterestHandler KindHandler (kv_cache JSON pattern), register in HANDLERS and del switch case
- Add case 'shortinterest' to YFinanceAdapter.fetchOne using quoteSummary(defaultKeyStatistics): parses sharesShort, shortPercentOfFloat, shortRatio, dateShortInterest, sharesShortPriorMonth, floatShares, sharesOutstanding
- Add institutional.shortInterest tRPC procedure (cache-read pattern, returns stale flag + provenance)
- Add api.institutional.shortInterest to frontend trpc client
- Create ShortInterestPanel.tsx: mirrors AnalystRatings pattern with loading/error/retry states, formatted shares/ratio/percentage display with source date
- Mount ShortInterestPanel on overview page after AnalystRatings
This commit is contained in:
Investor Flow Build
2026-07-25 12:09:21 -04:00
parent 351c104eba
commit 605dc7f3d1
6 changed files with 176 additions and 6 deletions
@@ -45,6 +45,34 @@ export class YFinanceAdapter implements SourceFetch {
const raw = await yf.quoteSummary(id, { modules: ['summaryProfile'] });
return { value: parseSymbol(raw, id), ttlClass: 'symbol_meta', provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: id } };
}
case 'shortinterest': {
const raw = await yf.quoteSummary(id, { modules: ['defaultKeyStatistics'] });
const stats = (raw as Record<string, unknown>)?.defaultKeyStatistics as Record<string, unknown> | undefined;
if (!stats) throw new Error('No defaultKeyStatistics module available');
const parseNum = (v: unknown): number | null => {
if (v === null || v === undefined) return null;
if (typeof v === 'number') return v;
const s = String(v).replace(/[,]/g, '');
const n = parseFloat(s);
return Number.isNaN(n) ? null : n;
};
return {
value: {
sharesShort: parseNum(stats.sharesShort),
sharesShortPriorMonth: parseNum(stats.sharesShortPriorMonth),
sharesShortPreviousMonthDate: stats.sharesShortPreviousMonthDate ? String(stats.sharesShortPreviousMonthDate).slice(0, 10) : null,
dateShortInterest: stats.dateShortInterest ? String(stats.dateShortInterest).slice(0, 10) : null,
sharesPercentSharesOut: parseNum(stats.sharesPercentSharesOut),
shortRatio: parseNum(stats.shortRatio),
shortPercentOfFloat: parseNum(stats.shortPercentOfFloat),
floatShares: parseNum(stats.floatShares),
sharesOutstanding: parseNum(stats.sharesOutstanding),
sourceTickerKind: (stats as Record<string, unknown>).optionMethod?.toString() ?? null,
},
ttlClass: 'short_interest',
provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: id },
};
}
default: throw new Error(`YFinanceAdapter: unknown kind '${kind}'`);
}
}