import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; interface WhaleFilingDate { date: string; } interface WhaleHoldingRaw { symbol: string; securityName: string; shares: number; value: number; date: string; } 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; } // target tracking CIK registry const TARGET_WHALES = [ { name: 'Scion Asset Management (Michael Burry)', cik: '0001649339', sector: 'Consumer Cyclical' }, { name: 'Akre Capital Management (Chuck Akre)', cik: '0001483348', sector: 'Financial Services' }, { name: 'Mairs & Power Small Cap Fund', cik: '0001099684', sector: 'Industrials' } ]; // Offline high-fidelity mock data baseline const MOCK_WHALE_PROFILES: WhaleProfile[] = [ { name: 'Scion Asset Management (Michael Burry)', cik: '0001649339', aum: 118420000, holdingsCount: 16, topSector: 'Consumer Cyclical', filingDate: '2026-05-15' }, { name: 'Akre Capital Management (Chuck Akre)', cik: '0001483348', aum: 13420800000, holdingsCount: 22, topSector: 'Financial Services', filingDate: '2026-05-15' }, { name: 'Mairs & Power Small Cap Fund', cik: '0001099684', aum: 928400000, holdingsCount: 38, topSector: 'Industrials', filingDate: '2026-05-14' } ]; const MOCK_WHALE_POSITIONS: PositionDelta[] = [ { manager: 'Scion Asset Management (Michael Burry)', symbol: 'JD', name: 'JD.com Inc. ADR', currentWeight: 10.45, prevWeight: 5.21, vocDelta: 5.24, shares: 280000, value: 12373000 }, { manager: 'Scion Asset Management (Michael Burry)', symbol: 'BABA', name: 'Alibaba Group Holding Ltd.', currentWeight: 9.82, prevWeight: 6.12, vocDelta: 3.70, shares: 135000, value: 11629000 }, { manager: 'Mairs & Power Small Cap Fund', symbol: 'TNC', name: 'Tennant Company', currentWeight: 5.42, prevWeight: 2.15, vocDelta: 3.27, shares: 480000, value: 50320000 }, { manager: 'Akre Capital Management (Chuck Akre)', symbol: 'MA', name: 'Mastercard Inc.', currentWeight: 16.85, prevWeight: 13.80, vocDelta: 3.05, shares: 5100000, value: 2261470000 }, { manager: 'Scion Asset Management (Michael Burry)', symbol: 'BIDU', name: 'Baidu Inc. ADR', currentWeight: 5.82, prevWeight: 3.55, vocDelta: 2.27, shares: 65000, value: 6891000 }, { manager: 'Akre Capital Management (Chuck Akre)', symbol: 'V', name: 'Visa Inc.', currentWeight: 14.12, prevWeight: 12.15, vocDelta: 1.97, shares: 6800000, value: 1895020000 }, { manager: 'Mairs & Power Small Cap Fund', symbol: 'HURC', name: 'Hurco Companies Inc.', currentWeight: 3.85, prevWeight: 2.10, vocDelta: 1.75, shares: 1400000, value: 35742000 }, { manager: 'Akre Capital Management (Chuck Akre)', symbol: 'KMX', name: 'CarMax Inc.', currentWeight: 7.95, prevWeight: 6.80, vocDelta: 1.15, shares: 11200000, value: 1066980000 }, { manager: 'Scion Asset Management (Michael Burry)', symbol: 'BP', name: 'BP plc ADR', currentWeight: 3.25, prevWeight: 4.55, vocDelta: -1.30, shares: 105000, value: 3848000 }, { manager: 'Mairs & Power Small Cap Fund', symbol: 'CSGP', name: 'CoStar Group Inc.', currentWeight: 1.12, prevWeight: 2.85, vocDelta: -1.73, shares: 150000, value: 10398000 }, { manager: 'Scion Asset Management (Michael Burry)', symbol: 'GOOGL', name: 'Alphabet Inc.', currentWeight: 0.00, prevWeight: 5.12, vocDelta: -5.12, shares: 0, value: 0 } ]; async function fetchWithTimeout(url: string, timeoutMs = 4000): Promise { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal, cache: 'no-store' }); clearTimeout(id); return response; } catch (err) { clearTimeout(id); throw err; } } export async function GET() { const isDevMode = process.env.DEV_MODE === 'true'; const apiKey = process.env.FMP_API_KEY || 'U6lOXaOFPye7oc1D235kyAqJeQaiTAWc'; const now = Date.now(); // 1. DEV_MODE Interception check if (isDevMode) { const response = NextResponse.json({ whales: MOCK_WHALE_PROFILES, positions: MOCK_WHALE_POSITIONS, isShieldActive: true, timestamp: now }); response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); response.headers.set('X-Shield-Active', 'true'); return response; } // 2. Live FMP Ingestion try { const whaleProfiles: WhaleProfile[] = []; const positionsList: PositionDelta[] = []; for (const target of TARGET_WHALES) { // Step A: Fetch Filing Dates const datesUrl = `https://financialmodelingprep.com/api/v3/form-thirteen-date/${target.cik}?apikey=${apiKey}`; const datesRes = await fetchWithTimeout(datesUrl); if (!datesRes.ok) { throw new Error(`Failed to fetch dates for CIK ${target.cik}`); } const dates: WhaleFilingDate[] = await datesRes.json(); if (!Array.isArray(dates) || dates.length < 2) { // Fallback to mock profiles/positions for this specific whale if insufficient historical filings exist const mockProfile = MOCK_WHALE_PROFILES.find(p => p.cik === target.cik); if (mockProfile) { whaleProfiles.push(mockProfile); MOCK_WHALE_POSITIONS.filter(pos => pos.manager.includes(target.name)).forEach(pos => { positionsList.push(pos); }); } continue; } const dateCurrent = dates[0].date; const datePrevious = dates[1].date; // Step B: Fetch Holdings for Current & Previous quarters const currentUrl = `https://financialmodelingprep.com/api/v3/form-thirteen/${target.cik}?date=${dateCurrent}&apikey=${apiKey}`; const prevUrl = `https://financialmodelingprep.com/api/v3/form-thirteen/${target.cik}?date=${datePrevious}&apikey=${apiKey}`; const [currRes, prevRes] = await Promise.all([ fetchWithTimeout(currentUrl), fetchWithTimeout(prevUrl) ]); if (!currRes.ok || !prevRes.ok) { throw new Error(`Failed to fetch holdings data for CIK ${target.cik}`); } const rawCurrent: WhaleHoldingRaw[] = await currRes.json(); const rawPrevious: WhaleHoldingRaw[] = await prevRes.json(); // Step C: Compute total filing values (AUM) const currentAum = rawCurrent.reduce((acc, item) => acc + (item.value || 0), 0); const prevAum = rawPrevious.reduce((acc, item) => acc + (item.value || 0), 0); whaleProfiles.push({ name: target.name, cik: target.cik, aum: currentAum, holdingsCount: rawCurrent.length, topSector: target.sector, filingDate: dateCurrent }); // Step D: Calculate delta weights const prevHoldingsMap = new Map(); rawPrevious.forEach(h => { if (h.symbol) { prevHoldingsMap.set(h.symbol, h); } }); const processedSymbols = new Set(); // Process current holdings rawCurrent.forEach(curr => { if (!curr.symbol) return; processedSymbols.add(curr.symbol); const prev = prevHoldingsMap.get(curr.symbol); const currentWeight = currentAum > 0 ? (curr.value / currentAum) * 100 : 0; const prevWeight = prev && prevAum > 0 ? (prev.value / prevAum) * 100 : 0; const vocDelta = currentWeight - prevWeight; positionsList.push({ manager: target.name, symbol: curr.symbol, name: curr.securityName || `${curr.symbol} Corp.`, currentWeight: parseFloat(currentWeight.toFixed(2)), prevWeight: parseFloat(prevWeight.toFixed(2)), vocDelta: parseFloat(vocDelta.toFixed(2)), shares: curr.shares, value: curr.value }); }); // Process closed out positions (held in previous quarter, but not current) rawPrevious.forEach(prev => { if (!prev.symbol || processedSymbols.has(prev.symbol)) return; const prevWeight = prevAum > 0 ? (prev.value / prevAum) * 100 : 0; const vocDelta = 0 - prevWeight; positionsList.push({ manager: target.name, symbol: prev.symbol, name: prev.securityName || `${prev.symbol} Corp.`, currentWeight: 0, prevWeight: parseFloat(prevWeight.toFixed(2)), vocDelta: parseFloat(vocDelta.toFixed(2)), shares: 0, value: 0 }); }); } // Sort positions by delta weight descending positionsList.sort((a, b) => b.vocDelta - a.vocDelta); return NextResponse.json({ whales: whaleProfiles, positions: positionsList, isShieldActive: false, timestamp: now }); } catch (err: any) { console.error("FMP Ingestion for Whale Screener failed, falling back to mock archive. Reason:", err.message || err); // Fallback response on error const response = NextResponse.json({ whales: MOCK_WHALE_PROFILES, positions: MOCK_WHALE_POSITIONS, isShieldActive: true, timestamp: now }); response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return response; } }