Files
2026-06-13 15:16:57 +02:00

276 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}