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
+91
View File
@@ -0,0 +1,91 @@
// Investor Flow — NasdaqAdapter (DESIGN.md §3a Module 2 + §5 yfinance policy).
// SourceFetch for NASDAQ short-interest data via the Nasdaq Data Link API.
// Per-symbol JSON endpoint, no API key required (public, rate-limited).
// Covers NASDAQ-listed stocks only; NYSE/other tickers return an error.
import type { CacheKey, TtlClass, Provenance, SourceKind } from '../cache/CacheRepository.ts';
import { parseCacheKey } from '../cache/CacheRepository.ts';
import type { SourceFetch, FetchResult } from './SourceAdapter.ts';
const UA = `InvestorFlow/${process.env.APP_VERSION ?? 'dev'} (+https://github.com/anomalyco/investor-flow)`;
const NASDAQ_SHORTINTEREST_TTL_MS = 12 * 60 * 60_000; // 12h (NASDAQ publishes bi-monthly)
interface NasdaqShortInterestRow {
settlementDate: string;
interest: string;
avgDailyShareVolume: string;
daysToCover: number;
}
interface NasdaqShortInterestResponse {
data?: {
symbol: string;
shortInterestTable?: {
headers: Array<{ key: string }>;
rows: NasdaqShortInterestRow[];
};
};
status?: { rCode: number };
message?: string | null;
}
export class NasdaqAdapter implements SourceFetch {
readonly sourceKind = 'nasdaq' as SourceKind;
async fetchOne(key: CacheKey): Promise<FetchResult> {
const { kind, id } = parseCacheKey(key);
const fetchedAt = new Date().toISOString();
if (kind !== 'nasdaqShortinterest') {
throw new Error(`NasdaqAdapter: unknown kind '${kind}'`);
}
const res = await fetch(
`https://api.nasdaq.com/api/quote/${encodeURIComponent(id)}/short-interest?assetclass=stocks`,
{
headers: {
'User-Agent': UA,
Accept: 'application/json',
},
}
);
if (!res.ok) {
throw new Error(`NASDAQ API returned ${res.status}`);
}
const body = (await res.json()) as NasdaqShortInterestResponse;
if (!body.data?.shortInterestTable?.rows?.length) {
throw new Error(body.message ?? 'No short-interest data available');
}
const rows = body.data.shortInterestTable.rows as NasdaqShortInterestRow[];
const latest = rows[0];
const history = rows.map((r) => ({
settlementDate: r.settlementDate,
sharesShort: parseShares(r.interest),
avgDailyVolume: parseShares(r.avgDailyShareVolume),
daysToCover: r.daysToCover,
}));
return {
value: {
sharesShort: parseShares(latest.interest),
avgDailyShareVolume: parseShares(latest.avgDailyShareVolume),
daysToCover: latest.daysToCover,
settlementDate: latest.settlementDate,
history,
source: 'nasdaq',
},
ttlClass: 'short_interest' as TtlClass,
provenance: { fetchedAt, sourceKind: 'nasdaq', rawSourceId: id },
};
}
}
function parseShares(raw: string): number | null {
if (!raw) return null;
const cleaned = raw.replace(/,/g, '');
const n = Number(cleaned);
return Number.isNaN(n) || n < 0 ? null : n;
}