Files

349 lines
9.6 KiB
TypeScript

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<Response> {
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<string, WhaleHoldingRaw>();
rawPrevious.forEach(h => {
if (h.symbol) {
prevHoldingsMap.set(h.symbol, h);
}
});
const processedSymbols = new Set<string>();
// 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;
}
}