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
+51 -10
View File
@@ -2,6 +2,13 @@
import { useState, useEffect, useMemo } from 'react';
import { api } from '@/lib/trpc';
interface HistoryEntry {
settlementDate: string;
sharesShort: number | null;
avgDailyVolume: number | null;
daysToCover: number;
}
interface ShortInterestData {
symbol: string;
sharesShort: number | null;
@@ -12,8 +19,11 @@ interface ShortInterestData {
dateShortInterest: string | null;
floatShares: number | null;
sharesOutstanding: number | null;
daysToCover: number | null;
settlementDate: string | null;
history: HistoryEntry[] | null;
sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean } };
stale: boolean;
provenance: { fetchedAt: string; sourceKind: string; rawSourceId?: string } | null;
}
function formatNumber(n: number | null): string {
@@ -62,34 +72,65 @@ export function ShortInterestPanel({ symbol }: { symbol: string | null }) {
);
if (!data) return <div className="text-xs text-fg-muted py-2">No short interest data available for {symbol}.</div>;
const dateLabel = data.dateShortInterest
? `as of ${data.dateShortInterest}`
: data.provenance?.fetchedAt
? `fetched ${data.provenance.fetchedAt.slice(0, 10)}`
const dateLabel = data.settlementDate
? `Settlement: ${data.settlementDate}`
: data.dateShortInterest
? `Yahoo: ${data.dateShortInterest}`
: null;
const sourceTags = [] as string[];
if (data.sources.nasdaq.available) sourceTags.push('NASDAQ');
if (data.sources.yfinance.available) sourceTags.push('Yahoo');
const sourceLabel = sourceTags.length ? sourceTags.join(' + ') : '—';
const historySlice = data.history ? data.history.slice(0, 6) : [];
return (
<div className="rounded-lg bg-surface-sunken border border-line p-4 space-y-3">
<h4 className="text-sm font-semibold text-fg">Short Interest</h4>
{dateLabel && (
<p className="text-[10px] text-fg-muted">{dateLabel}{data.stale ? ' — stale' : ''}</p>
<p className="text-[10px] text-fg-muted">{dateLabel}{data.stale ? ' — stale' : ''} ({sourceLabel})</p>
)}
{data.sharesShort !== null && (
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
<div className="flex justify-between"><dt className="text-fg-muted">Shares Short</dt><dd className="text-fg font-mono">{formatNumber(data.sharesShort)}</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">% of Float</dt><dd className="text-fg font-mono">{formatPct(data.sharesPercentSharesOut)}</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">Short Ratio</dt><dd className="text-fg font-mono">{data.shortRatio !== null ? `${data.shortRatio.toFixed(1)}d` : '—'}</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">% of Float (prior mo)</dt><dd className="text-fg font-mono">{formatPct(data.sharesShortPriorMonth !== null && data.floatShares ? data.sharesShortPriorMonth / data.floatShares : null)}</dd></div>
{data.daysToCover !== null && (
<div className="flex justify-between"><dt className="text-fg-muted">Days to Cover</dt><dd className="text-fg font-mono">{data.daysToCover.toFixed(2)}d</dd></div>
)}
{data.sharesShortPriorMonth !== null && data.floatShares ? (
<div className="flex justify-between"><dt className="text-fg-muted">% Float (prior)</dt><dd className="text-fg font-mono">{formatPct(data.sharesShortPriorMonth / data.floatShares)}</dd></div>
) : null}
{data.floatShares !== null && (
<div className="flex justify-between"><dt className="text-fg-muted">Float Shares</dt><dd className="text-fg font-mono">{formatNumber(data.floatShares)}</dd></div>
)}
{data.sharesOutstanding !== null && (
<div className="flex justify-between"><dt className="text-fg-muted">Shares Outstanding</dt><dd className="text-fg font-mono">{formatNumber(data.sharesOutstanding)}</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">Shares Out</dt><dd className="text-fg font-mono">{formatNumber(data.sharesOutstanding)}</dd></div>
)}
</dl>
)}
<p className="text-[10px] text-fg-muted">
Short interest from Yahoo Finance (derived from exchange filings). Educational display only — not investment advice.
{historySlice.length > 0 && (
<div className="mt-2">
<p className="text-[10px] text-fg-muted mb-1">6-month history (NASDAQ source)</p>
<div className="overflow-x-auto">
<table className="w-full text-[10px] text-fg-muted">
<thead><tr><th className="text-left py-0.5">Date</th><th className="text-right py-0.5">Short</th><th className="text-right py-0.5">Days</th></tr></thead>
<tbody>
{historySlice.map((h, i) => (
<tr key={i} className="border-b border-line/40">
<td className="py-0.5">{h.settlementDate}</td>
<td className="text-right font-mono">{formatNumber(h.sharesShort)}</td>
<td className="text-right font-mono">{h.daysToCover.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
<p className="text-[10px] text-fg-muted mt-1">
Reconciled from NASDAQ (days-to-cover, history) + Yahoo Finance (all tickers). Educational display only.
</p>
</div>
);