'use client'; 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; sharesShortPriorMonth: number | null; sharesPercentSharesOut: number | null; shortRatio: number | null; shortPercentOfFloat: number | null; 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; } function formatNumber(n: number | null): string { if (n === null || n === undefined) return '—'; if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`; return String(n); } function formatPct(n: number | null): string { if (n === null || n === undefined) return '—'; return `${(n * 100).toFixed(2)}%`; } export function ShortInterestPanel({ symbol }: { symbol: string | null }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [retryKey, setRetryKey] = useState(0); useEffect(() => { if (!symbol) return; setLoading(true); setError(null); let cancelled = false; api.institutional.shortInterest(symbol).then((res) => { if (cancelled) return; setData(res); }).catch((e: Error) => { if (cancelled) return; setError(e.message); }).finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [symbol, retryKey]); if (!symbol) return null; if (loading) return
Loading short interest...
; if (error) return (
Short interest: {error}
); if (!data) return
No short interest data available for {symbol}.
; 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 (

Short Interest

{dateLabel && (

{dateLabel}{data.stale ? ' — stale' : ''} ({sourceLabel})

)} {data.sharesShort !== null && (
Shares Short
{formatNumber(data.sharesShort)}
% of Float
{formatPct(data.sharesPercentSharesOut)}
Short Ratio
{data.shortRatio !== null ? `${data.shortRatio.toFixed(1)}d` : '—'}
{data.daysToCover !== null && (
Days to Cover
{data.daysToCover.toFixed(2)}d
)} {data.sharesShortPriorMonth !== null && data.floatShares ? (
% Float (prior)
{formatPct(data.sharesShortPriorMonth / data.floatShares)}
) : null} {data.floatShares !== null && (
Float Shares
{formatNumber(data.floatShares)}
)} {data.sharesOutstanding !== null && (
Shares Out
{formatNumber(data.sharesOutstanding)}
)}
)} {historySlice.length > 0 && (

6-month history (NASDAQ source)

{historySlice.map((h, i) => ( ))}
DateShortDays
{h.settlementDate} {formatNumber(h.sharesShort)} {h.daysToCover.toFixed(2)}
)}

Reconciled from NASDAQ (days-to-cover, history) + Yahoo Finance (all tickers). Educational display only.

); }