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
+22 -3
View File
@@ -12,7 +12,8 @@ export type CacheKey = string; // `${SourceKind}:${kind}:${id}` e.g. 'yfinance:q
export type TtlClass =
| 'live_quote' | 'intraday' | 'daily_permanent' | 'options_snapshot'
| 'filing_immutable' | 'quarterly_immutable' | 'thread_7d'
| 'macro_event' | 'regime_classification' | 'llm_summary' | 'symbol_meta';
| 'macro_event' | 'regime_classification' | 'llm_summary' | 'symbol_meta'
| 'short_interest';
export interface Provenance { fetchedAt: string; sourceKind: SourceKind; rawSourceId?: string; }
@@ -38,6 +39,7 @@ export const TTL_MS: Record<TtlClass, number> = {
regime_classification: 24 * 60 * 60_000,
llm_summary: Number.POSITIVE_INFINITY, // cached forever by prompt-hash
symbol_meta: 7 * 24 * 60 * 60_000, // weekly (slow-changing sector/industry)
short_interest: 24 * 60 * 60_000, // refreshed twice/month per source
};
/** Parse 'source:kind:id...' into { source, kind, id } (id may contain colons). */
@@ -280,11 +282,24 @@ const lintHoldersHandler: KindHandler = {
const lintInsidersHandler: KindHandler = {
ttlClass: 'daily_permanent',
read() { return null; }, // never read — work happens in DB tables directly
read() { return null; },
write(d, key, _value, provenance) {
d.prepare('INSERT OR REPLACE INTO kv_cache (key,value,observed_at) VALUES (?,?,?)').run(`sec-lint-insiders:${key}`, JSON.stringify({ ok: true }), provenance.fetchedAt);
},
isStale() { return false; }, // never stale once written
isStale() { return false; },
};
const shortInterestHandler: KindHandler = {
ttlClass: 'short_interest',
read(d, id) {
const r = d.prepare('SELECT value, observed_at FROM kv_cache WHERE key=?').get(`yfinance:shortinterest:${id}`) as { value: string; observed_at: string } | undefined;
if (!r) return null;
try { return { value: JSON.parse(r.value), stalenessTs: r.observed_at }; } catch { return null; }
},
write(d, id, value, provenance) {
d.prepare('INSERT OR REPLACE INTO kv_cache (key, value, observed_at) VALUES (?,?,?)').run(`yfinance:shortinterest:${id}`, JSON.stringify(value), provenance.fetchedAt);
},
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.short_interest; },
};
const HANDLERS = new Map<string, KindHandler>([
@@ -298,6 +313,7 @@ const HANDLERS = new Map<string, KindHandler>([
['fetch', fetchHandler],
['holders', lintHoldersHandler],
['insiders', lintInsidersHandler],
['shortinterest', shortInterestHandler],
]);
export interface CacheRepository {
@@ -405,6 +421,9 @@ export class CacheRepositoryImpl implements CacheRepository {
case 'expiry_dates':
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`options_expiry:${id}`);
break;
case 'shortinterest':
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`yfinance:shortinterest:${id}`);
break;
default: {
// Unknown/wildcard kind (e.g. `x:cashtag:*`): best-effort delete from kv_cache via LIKE.
const like = key.endsWith(':*') ? `${key.slice(0, -1)}%` : key;