276 lines
14 KiB
TypeScript
276 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import WhaleMathModal from './WhaleMathModal';
|
||
import WhaleBlueprintModal from './WhaleBlueprintModal';
|
||
import {
|
||
Compass, ArrowUpRight, ArrowDownRight, Minus, BookOpen, AlertCircle, RefreshCw, Layers, DollarSign, Calendar, TrendingUp
|
||
} from 'lucide-react';
|
||
|
||
interface WhaleProfile {
|
||
name: string;
|
||
cik: string;
|
||
aum: number;
|
||
holdingsCount: number;
|
||
topSector: string;
|
||
filingDate: string;
|
||
}
|
||
|
||
interface PositionDelta {
|
||
manager: string;
|
||
symbol: string;
|
||
name: string;
|
||
currentWeight: number;
|
||
prevWeight: number;
|
||
vocDelta: number;
|
||
shares: number;
|
||
value: number;
|
||
}
|
||
|
||
export default function WhaleScreener() {
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [profiles, setProfiles] = useState<WhaleProfile[]>([]);
|
||
const [positions, setPositions] = useState<PositionDelta[]>([]);
|
||
const [isShieldActive, setIsShieldActive] = useState(false);
|
||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false);
|
||
const [refreshKey, setRefreshKey] = useState(0);
|
||
|
||
useEffect(() => {
|
||
const fetchWhaleData = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await fetch('/api/whale/screener');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
setProfiles(data.whales || []);
|
||
setPositions(data.positions || []);
|
||
setIsShieldActive(!!data.isShieldActive);
|
||
} else {
|
||
setError('Failed to fetch institutional whale screener data.');
|
||
}
|
||
} catch (err) {
|
||
console.error('Fetch whale data error:', err);
|
||
setError('Network error loading Whale Satellite Screener data.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchWhaleData();
|
||
}, [refreshKey]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-8 text-slate-100 shadow-xl min-h-[450px] flex flex-col items-center justify-center space-y-4">
|
||
<div className="w-10 h-10 rounded-full border-2 border-blue-400 border-t-transparent animate-spin" />
|
||
<div className="text-slate-400 text-sm font-mono animate-pulse">Extracting SEC Form 13F filing dates & holding logs...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl min-h-[400px] flex items-center justify-center">
|
||
<div className="text-rose-400 font-semibold flex items-center gap-2">
|
||
<AlertCircle className="w-5 h-5" /> {error}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
|
||
{/* ⚠️ Dynamic Rate-Limit Fallback Banner */}
|
||
{isShieldActive && (
|
||
<div className="bg-amber-955/40 border border-amber-850 text-amber-400 text-xs rounded-xl p-4 flex items-center gap-3 shadow-[0_0_15px_rgba(245,158,11,0.12)]">
|
||
<AlertCircle className="w-5 h-5 text-amber-400 shrink-0" />
|
||
<div className="flex-1">
|
||
<span className="font-bold font-mono uppercase tracking-wider block mb-0.5">[🔒 Offline-Shield Active - Cached Archive Ingested]</span>
|
||
Iterative development protection is active (`DEV_MODE`). The system is rendering high-fidelity baseline 13F deltas to maintain zero outbound FMP API calls.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* HEADER PANEL */}
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
|
||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-3xl -z-10" />
|
||
|
||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||
<div className="space-y-1">
|
||
<span className="text-blue-400 text-xs font-semibold uppercase tracking-wider">Whale Satellite-Screener</span>
|
||
<h2 className="text-2xl font-extrabold text-white flex flex-wrap items-center gap-2">
|
||
<Compass className="text-blue-400 w-6 h-6" />
|
||
<span>Whale Institutional Screener</span>
|
||
{isShieldActive ? (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)] ml-2 animate-pulse">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||
DEV ARCHIVE ACTIVE (0 CALLS)
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] ml-2">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping" />
|
||
LIVE API ENDPOINT (FMP CORPO)
|
||
</span>
|
||
)}
|
||
</h2>
|
||
<p className="text-xs text-slate-400">
|
||
Tracks smart money allocation pivots by analyzing quarterly CIK portfolio shifts. Calculates the conviction velocity of boutique value and small-cap managers.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 w-full md:w-auto justify-end">
|
||
<button
|
||
onClick={() => setRefreshKey(prev => prev + 1)}
|
||
className="flex items-center justify-center p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all text-slate-400 hover:text-slate-200 h-11 w-11 cursor-pointer"
|
||
title="Refresh holdings"
|
||
>
|
||
<RefreshCw className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setIsMathModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-blue-400 w-full md:w-auto justify-center h-11 cursor-pointer"
|
||
>
|
||
<BookOpen className="w-4 h-4" />
|
||
<span>📖 Quantitative Handbook</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setIsBlueprintModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-blue-400 w-full md:w-auto justify-center h-11 cursor-pointer animate-pulse-slow"
|
||
>
|
||
<Compass className="w-4 h-4" />
|
||
<span>⚙️ Operational Blueprint</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2-COLUMN WORKSTATION LAYOUT */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
|
||
{/* LEFT COLUMN: WHALE PROFILE CARDS */}
|
||
<div className="space-y-6 lg:col-span-1">
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-5 text-slate-100 shadow-xl space-y-4">
|
||
<h3 className="font-bold text-white text-sm border-b border-slate-800 pb-2 flex items-center gap-2">
|
||
<Layers className="text-blue-400 w-4 h-4" /> Tracked Institutional Managers
|
||
</h3>
|
||
|
||
<div className="space-y-4">
|
||
{profiles.map((profile) => (
|
||
<div key={profile.cik} className="bg-slate-950/50 border border-slate-850 rounded-xl p-4 space-y-3 relative overflow-hidden group">
|
||
<div className="absolute top-0 right-0 w-16 h-16 bg-blue-500/5 rounded-full blur-xl group-hover:bg-blue-500/10 transition-colors pointer-events-none" />
|
||
|
||
<div>
|
||
<h4 className="font-bold text-slate-200 text-xs tracking-tight">{profile.name}</h4>
|
||
<span className="text-[9px] text-slate-500 font-mono">CIK: {profile.cik}</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-2.5 pt-2 border-t border-slate-900 text-xs">
|
||
<div>
|
||
<span className="text-[9px] text-slate-500 uppercase font-mono block">Estimated AUM</span>
|
||
<span className="font-semibold text-slate-350 font-mono">
|
||
${profile.aum >= 1e9
|
||
? `${(profile.aum / 1e9).toFixed(2)}B`
|
||
: `${(profile.aum / 1e6).toFixed(1)}M`}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-[9px] text-slate-500 uppercase font-mono block">Top Sector Focus</span>
|
||
<span className="font-semibold text-slate-350">{profile.topSector}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center text-[10px] pt-1 font-mono text-slate-500">
|
||
<span className="flex items-center gap-1"><DollarSign className="w-3 h-3 text-slate-600" /> Holdings: {profile.holdingsCount}</span>
|
||
<span className="flex items-center gap-1"><Calendar className="w-3 h-3 text-slate-600" /> Filing: {profile.filingDate}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* RIGHT COLUMN: HIGH-CONVICTION BUY/SELL TABLE */}
|
||
<div className="lg:col-span-2 space-y-6">
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
|
||
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
|
||
<div>
|
||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||
<TrendingUp className="text-blue-400 w-4 h-4" /> High-Conviction Portfolio Shifts
|
||
</h3>
|
||
<p className="text-[10px] text-slate-500 font-mono">Position weights delta compared to prior 13F report</p>
|
||
</div>
|
||
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-950 border border-slate-800 text-slate-300">
|
||
{positions.length} Positions
|
||
</span>
|
||
</div>
|
||
|
||
<div className="overflow-x-auto scrollbar-thin">
|
||
<table className="w-full text-left text-xs border-collapse min-w-[600px]">
|
||
<thead>
|
||
<tr className="border-b border-slate-900 text-slate-550 font-mono text-[9px] uppercase tracking-wider">
|
||
<th className="py-2.5 px-3">Manager</th>
|
||
<th className="py-2.5 px-3">Ticker</th>
|
||
<th className="py-2.5 px-3">Company Name</th>
|
||
<th className="py-2.5 px-3 text-right">Prev Weight</th>
|
||
<th className="py-2.5 px-3 text-right">Current Weight</th>
|
||
<th className="py-2.5 px-3 text-center">VoC Delta</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-900/60">
|
||
{positions.map((pos, idx) => {
|
||
const isPositive = pos.vocDelta > 0;
|
||
const isZero = pos.vocDelta === 0;
|
||
|
||
const deltaColor = isPositive
|
||
? 'text-emerald-400 bg-emerald-500/10 border-emerald-500/25'
|
||
: (isZero ? 'text-slate-400 bg-slate-900 border-slate-800' : 'text-rose-400 bg-rose-500/10 border-rose-500/25');
|
||
|
||
const deltaIcon = isPositive
|
||
? <ArrowUpRight className="w-3 h-3 inline-block" />
|
||
: (isZero ? <Minus className="w-3 h-3 inline-block" /> : <ArrowDownRight className="w-3 h-3 inline-block" />);
|
||
|
||
return (
|
||
<tr key={`${pos.manager}_${pos.symbol}_${idx}`} className="hover:bg-slate-900/20 transition-colors">
|
||
<td className="py-3 px-3 font-semibold text-slate-350 max-w-[150px] truncate" title={pos.manager}>
|
||
{pos.manager.split(' (')[0]}
|
||
</td>
|
||
<td className="py-3 px-3 font-mono font-bold text-slate-100">
|
||
{pos.symbol}
|
||
</td>
|
||
<td className="py-3 px-3 text-slate-400 truncate max-w-[180px]" title={pos.name}>
|
||
{pos.name}
|
||
</td>
|
||
<td className="py-3 px-3 text-right font-mono text-slate-500">
|
||
{pos.prevWeight.toFixed(2)}%
|
||
</td>
|
||
<td className="py-3 px-3 text-right font-mono text-slate-300">
|
||
{pos.currentWeight.toFixed(2)}%
|
||
</td>
|
||
<td className="py-3 px-3 text-center">
|
||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded border text-[10px] font-bold font-mono ${deltaColor}`}>
|
||
{deltaIcon}
|
||
{isPositive ? '+' : ''}{pos.vocDelta.toFixed(2)}%
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<WhaleMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||
<WhaleBlueprintModal isOpen={isBlueprintModalOpen} onClose={() => setIsBlueprintModalOpen(false)} />
|
||
</div>
|
||
);
|
||
}
|