76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
"use client";
|
|||
|
|
// Two-tier "who holds this" strip (M22 Symbol Search Index, ADR-0011).
|
||
|
|
//
|
||
|
|
// Tier 1 — tracked funds (M21 `fund_position_records`): each fund that holds the
|
||
|
|
// symbol, with its weight in the fund's disclosed book and the record source/as-of.
|
||
|
|
// Tier 2 — the count of distinct 13F filers that reported the symbol (M4 cache).
|
||
|
|
//
|
||
|
|
// Facts only (ADR-0010 / ADR-0007): never "you should follow this fund".
|
||
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { api, type FundHoldersResult } from "@/lib/trpc";
|
||
|
|
import { Badge } from "@/components/ui";
|
||
|
|
|
||
|
|
export function FundHoldingsStrip({ symbol }: { symbol: string | null }) {
|
||
|
|
const [data, setData] = useState<FundHoldersResult | null>(null);
|
||
|
|
const [showInstitutions, setShowInstitutions] = useState(false);
|
||
|
|
const [error, setError] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let cancelled = false;
|
||
|
|
setData(null);
|
||
|
|
setShowInstitutions(false);
|
||
|
|
setError(false);
|
||
|
|
if (!symbol) return;
|
||
|
|
api.symbols
|
||
|
|
.holders({ symbol })
|
||
|
|
.then((d) => { if (!cancelled) setData(d); })
|
||
|
|
.catch(() => { if (!cancelled) setError(true); });
|
||
|
|
return () => { cancelled = true; };
|
||
|
|
}, [symbol]);
|
||
|
|
|
||
|
|
if (error) return null; // silent — strip is auxiliary
|
||
|
|
if (!data) return null;
|
||
|
|
if (data.tracked.length === 0 && data.institutionCount === 0) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section className="rounded-xl border border-line-strong bg-surface-raised p-4">
|
||
|
|
<h3 className="text-sm font-semibold text-fg mb-2">Who holds {symbol}</h3>
|
||
|
|
|
||
|
|
{data.tracked.length > 0 && (
|
||
|
|
<ul className="space-y-1.5">
|
||
|
|
{data.tracked.map((t) => (
|
||
|
|
<li key={t.fundId} className="flex items-center gap-2 text-sm">
|
||
|
|
<span className="font-medium text-fg">{t.fundName}</span>
|
||
|
|
{t.weightPct != null && (
|
||
|
|
<span className="text-xs text-fg-muted tnum">{t.weightPct.toFixed(1)}% of book</span>
|
||
|
|
)}
|
||
|
|
<Badge tone={t.source === "13f" ? "neutral" : "warn"}>
|
||
|
|
{t.source === "13f" ? "SEC 13F" : t.source}
|
||
|
|
</Badge>
|
||
|
|
<span className="text-[10px] text-fg-faint">as of {t.asOf}</span>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{data.institutionCount > 0 && (
|
||
|
|
<div className="mt-2 border-t border-line pt-2">
|
||
|
|
<button
|
||
|
|
onClick={() => setShowInstitutions((v) => !v)}
|
||
|
|
className="text-xs text-fg-muted hover:text-fg transition-colors flex items-center gap-1"
|
||
|
|
>
|
||
|
|
<span>{data.institutionCount} other institution{data.institutionCount === 1 ? "" : "s"} report holding this (13F)</span>
|
||
|
|
<span className="text-fg-faint">{showInstitutions ? "▾" : "▸"}</span>
|
||
|
|
</button>
|
||
|
|
{showInstitutions && (
|
||
|
|
<p className="mt-1 text-[10px] text-fg-faint">
|
||
|
|
From the shared 13F filings cache (M4 institutional module). This is every
|
||
|
|
distinct filer that reported the symbol, passive and active alike.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|