feat: Phase 3 FINRA bulk adapter + Phase 4 three-way merge with discrepancy flagging
Phase 3 — FINRA bulk short-interest ingest: - Add finra_short_interest table to schema (per-symbol, per-settlement-date, per-exchange, with short/exempt/total volume, avg daily vol, days to cover) - Create FinraIngestService: downloads FINRA consolidated pipe-delimited file from configurable base URL, parses Market|Symbol|ShortVolume|ShortExemptVolume|TotalVolume, stores in finra_short_interest table - Create FinraBulkAdapter: SourceFetch that calls downloadAndIngestFinra, registers as finra-bulk source kind with finraShortinterest cache handler - finraShortinterest handler reads latest settlement row per symbol from finra_short_interest table (no per-symbol kv_cache write; data is bulk-ingested) - Register in index.ts adapter map + HANDLERS + del case Phase 4 — three-way merge with discrepancy detection: - shortInterest tRPC procedure now reads all 3 caches (yfinance, nasdaq, finra-bulk) in parallel - Reconciliation hierarchy: FINRA (shares short) > NASDAQ > Yahoo - daysToCover: NASDAQ (specific) > FINRA (computed) > Yahoo (short ratio fallback) - settlementDate: FINRA > NASDAQ > Yahoo - Discrepancy detection: compares sharesShort across available sources, flags >10% difference with discrepancyPct + discrepancyBetween - Updated ShortInterestPanel: FINRA source badge, discrepancy warning banner, three-source disclaimer - Updated trpc.ts client type for new shape
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '@/lib/trpc';
|
||||
|
||||
interface HistoryEntry {
|
||||
@@ -22,7 +22,9 @@ interface ShortInterestData {
|
||||
daysToCover: number | null;
|
||||
settlementDate: string | null;
|
||||
history: HistoryEntry[] | null;
|
||||
sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean } };
|
||||
discrepancyPct: number | null;
|
||||
discrepancyBetween: string | null;
|
||||
sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean }; finra: { available: boolean; stale: boolean } };
|
||||
stale: boolean;
|
||||
}
|
||||
|
||||
@@ -80,6 +82,7 @@ export function ShortInterestPanel({ symbol }: { symbol: string | null }) {
|
||||
|
||||
const sourceTags = [] as string[];
|
||||
if (data.sources.nasdaq.available) sourceTags.push('NASDAQ');
|
||||
if (data.sources.finra.available) sourceTags.push('FINRA');
|
||||
if (data.sources.yfinance.available) sourceTags.push('Yahoo');
|
||||
const sourceLabel = sourceTags.length ? sourceTags.join(' + ') : '—';
|
||||
|
||||
@@ -91,6 +94,11 @@ export function ShortInterestPanel({ symbol }: { symbol: string | null }) {
|
||||
{dateLabel && (
|
||||
<p className="text-[10px] text-fg-muted">{dateLabel}{data.stale ? ' — stale' : ''} ({sourceLabel})</p>
|
||||
)}
|
||||
{data.discrepancyPct !== null && (
|
||||
<div className="bg-warning/10 border border-warning/30 rounded px-2 py-1 text-[10px] text-warning">
|
||||
{data.discrepancyPct}% discrepancy in shares short between sources ({data.discrepancyBetween})
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
@@ -130,7 +138,7 @@ export function ShortInterestPanel({ symbol }: { symbol: string | null }) {
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[10px] text-fg-muted mt-1">
|
||||
Reconciled from NASDAQ (days-to-cover, history) + Yahoo Finance (all tickers). Educational display only.
|
||||
Reconciled across FINRA (regulatory, shares short), NASDAQ (days-to-cover, history), and Yahoo Finance (% float, universal coverage). Educational display only.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user