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:
+22
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user