Closes #010 - Implement Whale Satellite Screener: VoC weighting and SEC 13F filing integration

This commit is contained in:
Antigravity Agent
2026-06-12 21:06:37 +02:00
parent 6e4dd29d1d
commit 00e88eedb2
6 changed files with 779 additions and 2 deletions

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { BookOpen, X, ShieldAlert, DollarSign, BarChart2, TrendingUp } from 'lucide-react';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
interface WhaleMathModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function WhaleMathModal({ isOpen, onClose }: WhaleMathModalProps) {
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-955/90 backdrop-blur-md p-4 sm:p-6 md:p-8">
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden shadow-2xl relative text-slate-350">
{/* Modal Header */}
<div className="flex justify-between items-center px-6 py-4 bg-slate-950/50 border-b border-slate-800/80">
<div>
<h2 className="text-base font-bold bg-gradient-to-r from-blue-400 to-teal-400 bg-clip-text text-transparent flex items-center gap-2">
<BookOpen className="w-5 h-5 text-blue-400" /> English Quantitative Whale Screener & Conviction Handbook
</h2>
<p className="text-[10px] text-slate-500 font-mono">13F Institutional Filing Tracking & Velocity of Conviction Matrix</p>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-200 bg-slate-950/50 border border-slate-800 hover:border-slate-700 p-2 rounded-xl transition-all cursor-pointer flex items-center justify-center"
aria-label="Close modal"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Modal Body */}
<div className="flex-1 overflow-y-auto p-6 sm:p-8 space-y-8 text-slate-300 scrollbar-thin">
{/* Executive Overview */}
<div className="bg-slate-955/30 rounded-2xl p-5 border border-slate-850 space-y-2">
<h3 className="text-sm font-bold text-slate-100 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-blue-400" /> Executive Overview
</h3>
<p className="text-xs leading-relaxed text-slate-400">
The Whale Satellite Screener isolates high-conviction adjustments in portfolios managed by elite long-term value and small-cap investment firms (such as Michael Burry's Scion Asset Management and Chuck Akre's Akre Capital). By calculating the quarter-over-quarter relative portfolio weight adjustments, the system tracks the directional commitment ("Velocity of Conviction") of smart capital while adjusting for reporting lags and sector clustering.
</p>
</div>
{/* Section 1: The 45-Day 13F Reporting Lag */}
<div className="space-y-3">
<h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider font-mono flex items-center gap-1.5">
<BarChart2 className="w-3.5 h-3.5" /> 1. The 13F Reporting Lag Constraint
</h4>
<p className="text-xs leading-relaxed text-slate-400">
Institutional investment managers with over $100 million in Assets Under Management (AUM) are legally mandated by the SEC to submit Form 13F within 45 days after the end of each calendar quarter. This lag presents a structural challenge for quantitative models, as holdings data represents historical positions:
</p>
<div className="bg-slate-955/40 p-5 rounded-2xl border border-slate-850 my-2 text-xs space-y-3 text-slate-400 leading-relaxed">
<div>
<strong className="text-blue-300 block mb-1">I. Retrospective Analysis:</strong>
Positions disclosed on day \(T + 45\) reflect the portfolio state at day \(T\). As a result, short-term momentum models cannot directly trade 13F filings.
</div>
<div>
<strong className="text-blue-300 block mb-1">II. Informational Asymmetry:</strong>
Because holdings are backward-looking, the screener is optimized for long-term fundamental tracking (holding periods exceeding 12 months) where the 45-day lag does not dilute the underlying investment thesis.
</div>
</div>
</div>
{/* Section 2: Velocity of Conviction Formula */}
<div className="space-y-3">
<h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider font-mono flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5" /> 2. Velocity of Conviction (VoC) Weighting
</h4>
<p className="text-xs leading-relaxed text-slate-400">
Rather than tracking the absolute number of shares bought or sold, the screener measures changes in the relative portfolio weight of each asset. This normalizes for AUM changes caused by overall market movements:
</p>
<div className="bg-slate-955/40 p-5 rounded-2xl border border-slate-850 my-2 space-y-4">
<div>
<p className="text-xs text-slate-300 mb-2 font-semibold">Velocity of Conviction (VoC) Delta:</p>
<BlockMath math="\text{VoC}_{i} = w_{i, t} - w_{i, t-1}" />
<p className="text-xs text-slate-300 my-2 font-semibold">Asset Portfolio Weight Calculation:</p>
<BlockMath math="w_{i, t} = \frac{V_{i, t}}{\sum_{j} V_{j, t}} \times 100" />
<p className="text-[10px] text-slate-500 mt-3 font-mono leading-relaxed">
Where:
<br />
- <InlineMath math="w_{i, t}" /> is the portfolio weight of asset \(i\) at the end of the current quarter \(t\) (expressed as a percentage).
<br />
- <InlineMath math="w_{i, t-1}" /> is the portfolio weight of asset \(i\) at the end of the previous quarter \(t-1\).
<br />
- <InlineMath math="V_{i, t}" /> is the market value of the position in asset \(i\) as reported in the 13F filing for quarter \(t\).
<br />
- <InlineMath math="\sum_{j} V_{j, t}" /> represents the total market value of all reported equity holdings in the filing for quarter \(t\) (AUM proxy).
</p>
</div>
<p className="text-xs leading-relaxed text-slate-400">
<strong className="text-blue-300">Strategic Rationale:</strong> A positive VoC delta {"\\(\\text{VoC}_i > 0\\)"} indicates that the manager has actively allocated capital to the asset relative to other holdings, suggesting high-conviction buying. Conversely, a negative delta {"\\(\\text{VoC}_i < 0\\)"} indicates relative allocation decreases, showing active selling or profit-taking.
</p>
</div>
</div>
{/* Section 3: Institutional Clustering */}
<div className="space-y-3">
<h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider font-mono flex items-center gap-1.5">
<DollarSign className="w-3.5 h-3.5" /> 3. Institutional Clustering
</h4>
<p className="text-xs leading-relaxed text-slate-400">
Institutional clustering occurs when multiple high-conviction managers establish positions in the same security during the same period. The screener tracks cross-ownership patterns to detect when elite managers converge on specific investment clusters, which historically serves as a strong signal of structural undervaluation.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,264 @@
'use client';
import React, { useState, useEffect } from 'react';
import WhaleMathModal from './WhaleMathModal';
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 [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-ARCHIV AKTIV (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 ENDPUNKT (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-955/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>📖 Modulerklärung</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)} />
</div>
);
}