feat: Phase 2 NASDAQ adapter for days-to-cover + 24mo history

- Add nasdaq SourceKind + nasdaqShortinterest KindHandler (kv_cache with nasdaq: prefix)
- Create NasdaqAdapter: calls api.nasdaq.com/quote/SYM/short-interest, parses settlementDate/sharesShort/daysToCover/avgVolume, returns 24mo history; handles non-NASDAq gracefully
- Register NasdaqAdapter in server index.ts adapters map
- Update shortInterest procedure: reads both yfinance + nasdaq caches in parallel, reconciles (NASDAQ for daysToCover/sharesShort, Yahoo for %float/shortRatio and NYSE fallback)
- Update ShortInterestPanel: display daysToCover, settlement date, source badges, 6-month history table, updated disclaimer
- Update trpc.ts client type for merged response shape
This commit is contained in:
Investor Flow Build
2026-07-25 12:59:43 -04:00
parent d53c3a1cab
commit 76f60dc766
6 changed files with 205 additions and 33 deletions
+42 -21
View File
@@ -1900,27 +1900,48 @@ const institutionalRouter = router({
return { symbol, ratings: result.ratings, consensus: result.consensus };
}),
/** Short interest (shares short, % float, short ratio) from yahoo-finance2 defaultKeyStatistics. */
shortInterest: publicProcedure
.input(z.object({ symbol: z.string().min(1) }))
.query(async ({ ctx, input }) => {
const symbol = input.symbol.toUpperCase();
const k = `yfinance:shortinterest:${symbol}`;
const entry = await ctx.cache.get<unknown>(k);
const value = entry.value as Record<string, unknown> | null;
return {
symbol,
sharesShort: value ? (value.sharesShort as number | null) : null,
sharesShortPriorMonth: value ? (value.sharesShortPriorMonth as number | null) : null,
sharesPercentSharesOut: value ? (value.sharesPercentSharesOut as number | null) : null,
shortRatio: value ? (value.shortRatio as number | null) : null,
shortPercentOfFloat: value ? (value.shortPercentOfFloat as number | null) : null,
dateShortInterest: value ? (value.dateShortInterest as string | null) : null,
floatShares: value ? (value.floatShares as number | null) : null,
sharesOutstanding: value ? (value.sharesOutstanding as number | null) : null,
stale: entry.isStale,
provenance: entry.provenance,
};
/** Short interest reconciled across Yahoo Finance + NASDAQ API.
* Yahoo covers all US-listed tickers (basic: shares short, % float, short ratio).
* NASDAQ covers NASDAQ-listed only but adds days-to-cover + 24mo history.
* Reconciliation: prefer NASDAQ's sharesShort/daysToCover when available (richer),
* fall back to Yahoo for NYSE/other tickers. */
shortInterest: publicProcedure
.input(z.object({ symbol: z.string().min(1) }))
.query(async ({ ctx, input }) => {
const symbol = input.symbol.toUpperCase();
const [yfEntry, ndqEntry] = await Promise.all([
ctx.cache.get<Record<string, unknown>>(`yfinance:shortinterest:${symbol}`),
ctx.cache.get<Record<string, unknown>>(`nasdaq:nasdaqShortinterest:${symbol}`),
]);
const yf = yfEntry.value;
const ndq = ndqEntry.value;
// Prefer NASDAQ's sharesShort + daysToCover when available (NASDAQ-listed);
// otherwise fall back to Yahoo (covers NYSE/other tickers).
const sharesShort = ndq?.sharesShort ?? yf?.sharesShort ?? null;
const daysToCover = ndq?.daysToCover ?? null;
const settlementDate = ndq?.settlementDate ?? null;
const history = ndq?.history ?? null;
return {
symbol,
sharesShort,
sharesShortPriorMonth: yf?.sharesShortPriorMonth ?? null,
sharesPercentSharesOut: yf?.sharesPercentSharesOut ?? null,
shortRatio: yf?.shortRatio ?? null,
shortPercentOfFloat: yf?.shortPercentOfFloat ?? null,
dateShortInterest: yf?.dateShortInterest ?? null,
floatShares: yf?.floatShares ?? null,
sharesOutstanding: yf?.sharesOutstanding ?? null,
daysToCover,
settlementDate,
history,
sources: {
yfinance: { available: !!yf, stale: yfEntry.isStale },
nasdaq: { available: !!ndq, stale: ndqEntry.isStale },
},
stale: yfEntry.isStale && ndqEntry.isStale,
};
}),
});