import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; function getStrategicInsight(trade: { type: string; relation: string; value: number; ticker: string }) { const isBuy = trade.type === 'BUY'; const relation = (trade.relation || '').toUpperCase(); if (isBuy) { if (relation.includes('CEO') || relation.includes('CFO')) { return 'Starkes Conviction-Signal: CEO/CFO kauft eigene Aktien aus freien Mitteln (kein Optionsbezug).'; } if (trade.value > 1000000) { return 'Großvolumige Insider-Akkumulation (> $1 Mio.) weist auf fundamentale Unterbewertung hin.'; } return 'Opportunistischer Conviction-Kauf mit positivem Signal für den Markt.'; } else { if (relation.includes('CEO') || relation.includes('CFO') || trade.value > 5000000) { return 'Verkauf durch CEO/CFO. Häufig automatisiert (10b5-1 Plan) zur Portfoliodiversifikation.'; } return 'Reguläre Gewinnmitnahme / Liquiditätsbeschaffung zur Diversifikation.'; } } function getCongressInsight(trade: { type: string; representative: string; valueRange: string }) { const isBuy = trade.type === 'BUY'; if (isBuy) { return `Politisches Conviction-Signal (${trade.representative}). Möglicher Informationsvorsprung durch Ausschusstätigkeit.`; } return 'Taktische Reduzierung der Position im Rahmen von Compliance-Richtlinien.'; } function getWhaleInsight(trade: { type: string; institution: string }) { const isBuy = trade.type === 'BUY' || trade.type === 'NEW'; if (isBuy) { return `Institutionelle Akkumulation durch ${trade.institution}. Aufbau/Verstärkung einer strategischen Position.`; } return `Taktische Gewinnmitnahme / Portfolio-Rebalancing durch ${trade.institution}.`; } export async function GET(request: Request) { const { searchParams } = new URL(request.url); const type = searchParams.get('type') || 'executives'; const apiKey = process.env.FMP_API_KEY; if (!apiKey) { console.error("====== CRITICAL INSIDER ROUTE FAILURE ======", new Error("FMP_API_KEY is not configured in environment variables.")); const res = NextResponse.json({ results: [], liveDataAvailable: false }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } try { if (type === 'executives') { const fmpUrl = `https://financialmodelingprep.com/stable/insider-trading/latest?limit=100&apikey=${apiKey}`; const response = await fetch(fmpUrl, { cache: 'no-store' }); if (!response.ok) { throw new Error(`Financial Modeling Prep API returned HTTP ${response.status} for executives`); } const data = await response.json(); if (!Array.isArray(data)) { throw new Error('FMP API response is not a valid JSON array'); } const validRawTrades = data.filter((item: any) => item && item.symbol && item.symbol.trim() !== '' && item.symbol !== 'UNKNOWN' && item.symbol !== '--' && item.reportingName && item.reportingName.trim() !== '' && item.reportingName !== 'Reporting Owner' ); const trades = validRawTrades.map((item: any, index: number) => { const isBuy = (item.transactionType || '').toUpperCase().startsWith('P') || (item.transactionType || '').toUpperCase().startsWith('A') || (item.transactionType || '').toLowerCase().includes('purchase') || (item.transactionType || '').toLowerCase().includes('award') || (item.transactionType || '').toLowerCase().includes('buy') || item.acquisitionOrDisposition === 'A'; const sharesTransacted = Number(item.securitiesTransacted) || 0; const priceVal = Number(item.price) || 0; const value = Math.round(sharesTransacted * (priceVal || 15)); return { id: `exec_fmp_${item.symbol}_${index}_${Date.now()}`, ticker: item.symbol, insiderName: item.reportingName, relation: item.officerTitle || item.typeOfOwner || 'Insider', type: isBuy ? ('BUY' as const) : ('SELL' as const), shares: sharesTransacted || 1000, value: value || 15000, date: item.transactionDate || item.filingDate || '', insight: getStrategicInsight({ type: isBuy ? 'BUY' : 'SELL', relation: item.officerTitle || item.typeOfOwner || 'Insider', value: value || 15000, ticker: item.symbol }) }; }); const res = NextResponse.json({ results: trades.slice(0, 20), liveDataAvailable: true }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } if (type === 'congress') { try { const fmpUrl = `https://financialmodelingprep.com/api/v4/senate-disclosure?limit=50&apikey=${apiKey}`; const response = await fetch(fmpUrl, { cache: 'no-store' }); if (!response.ok) { throw new Error(`FMP Congress API returned HTTP ${response.status}`); } const data = await response.json(); if (!Array.isArray(data)) { throw new Error('FMP API response is not a valid JSON array'); } const validRawTrades = data.filter((item: any) => item && item.symbol && item.symbol.trim() !== '' && item.symbol !== 'UNKNOWN' && item.symbol !== '--' ); const trades = validRawTrades.map((item: any, index: number) => { const representative = item.representative || 'Representative'; const isBuy = (item.type || '').toLowerCase().includes('purchase') || (item.type || '').toLowerCase().includes('buy') || (item.type || '').toUpperCase().startsWith('P'); const valueRange = item.amount || '$1,001 - $15,000'; const tDate = item.transactionDate || item.disclosureDate || ''; const fDate = item.disclosureDate || ''; let lagDays = 15; if (tDate && fDate) { const d1 = new Date(tDate); const d2 = new Date(fDate); lagDays = Math.max(1, Math.round((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24))) || 15; } return { id: `cong_fmp_${item.symbol}_${index}_${Date.now()}`, ticker: item.symbol, representative, chamber: 'SENATE' as const, type: isBuy ? ('BUY' as const) : ('SELL' as const), valueRange, transactionDate: tDate, filingDate: fDate, lagDays, insight: getCongressInsight({ type: isBuy ? 'BUY' : 'SELL', representative, valueRange }) }; }); const res = NextResponse.json({ results: trades.slice(0, 20), liveDataAvailable: true }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } catch (err: any) { console.warn('FMP Congress specific API failed, mapping from stable insider-trading instead:', err.message); // Fallback: Map from stable/insider-trading/latest try { const fallbackUrl = `https://financialmodelingprep.com/stable/insider-trading/latest?limit=100&apikey=${apiKey}`; const response = await fetch(fallbackUrl, { cache: 'no-store' }); if (!response.ok) throw new Error(`Fallback fetch failed: ${response.status}`); const data = await response.json(); if (!Array.isArray(data)) throw new Error('Fallback data is not an array'); const validRawTrades = data.filter((item: any) => item && item.symbol && item.symbol.trim() !== '' && item.reportingName ); const trades = validRawTrades.map((item: any, index: number) => { const isBuy = (item.transactionType || '').toUpperCase().startsWith('P') || (item.transactionType || '').toUpperCase().startsWith('A') || (item.transactionType || '').toLowerCase().includes('purchase') || item.acquisitionOrDisposition === 'A'; const val = Math.round((Number(item.securitiesTransacted) || 1000) * (Number(item.price) || 15)); const valueRange = val > 1000000 ? '$1,000,001 - $5,000,000' : val > 250000 ? '$250,001 - $500,000' : val > 100000 ? '$100,001 - $250,000' : val > 15000 ? '$15,001 - $50,000' : '$1,001 - $15,000'; const tDate = item.transactionDate || item.filingDate || ''; const fDate = item.filingDate || ''; let lagDays = 15; if (tDate && fDate) { const d1 = new Date(tDate); const d2 = new Date(fDate); lagDays = Math.max(1, Math.round((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24))) || 15; } return { id: `cong_fmp_fb_${item.symbol}_${index}_${Date.now()}`, ticker: item.symbol, representative: item.reportingName, chamber: (item.typeOfOwner || '').toLowerCase().includes('director') ? ('SENATE' as const) : ('HOUSE' as const), type: isBuy ? ('BUY' as const) : ('SELL' as const), valueRange, transactionDate: tDate, filingDate: fDate, lagDays, insight: getCongressInsight({ type: isBuy ? 'BUY' : 'SELL', representative: item.reportingName, valueRange }) }; }); const res = NextResponse.json({ results: trades.slice(0, 20), liveDataAvailable: true }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } catch (fbErr: any) { console.error("====== CRITICAL INSIDER ROUTE FAILURE ======", fbErr); const res = NextResponse.json({results: [], liveDataAvailable: false}, {status: 200}); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } } } if (type === 'whales') { try { const fmpUrl = `https://financialmodelingprep.com/api/v4/institutional-ownership/industry-group-position?limit=50&apikey=${apiKey}`; const response = await fetch(fmpUrl, { cache: 'no-store' }); if (!response.ok) { throw new Error(`FMP Whales API returned HTTP ${response.status}`); } const data = await response.json(); if (!Array.isArray(data)) { throw new Error('FMP API response is not a valid JSON array'); } const validRawTrades = data.filter((item: any) => item && item.symbol && item.symbol.trim() !== '' ); const trades = validRawTrades.map((item: any, index: number) => { const institution = item.investorName || 'Institutional Holder'; const sharesTraded = Number(item.shares) || 10000; const estimatedValue = Number(item.value) || 150000; const isBuy = (item.change || 0) >= 0; return { id: `whale_fmp_${item.symbol}_${index}_${Date.now()}`, ticker: item.symbol, institution, type: isBuy ? ('BUY' as const) : ('SELL' as const), sharesTraded, sharesHeld: sharesTraded * 5, filingDate: item.filingDate || '', estimatedValue, insight: getWhaleInsight({ type: isBuy ? 'BUY' : 'SELL', institution }) }; }); const res = NextResponse.json({ results: trades.slice(0, 20), liveDataAvailable: true }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } catch (err: any) { console.warn('FMP Whales specific API failed, mapping from stable insider-trading instead:', err.message); // Fallback: Map from stable/insider-trading/latest (large institutional/director trades) try { const fallbackUrl = `https://financialmodelingprep.com/stable/insider-trading/latest?limit=100&apikey=${apiKey}`; const response = await fetch(fallbackUrl, { cache: 'no-store' }); if (!response.ok) throw new Error(`Fallback fetch failed: ${response.status}`); const data = await response.json(); if (!Array.isArray(data)) throw new Error('Fallback data is not an array'); // Filter for large trades or institutional filings const validRawTrades = data.filter((item: any) => item && item.symbol && item.symbol.trim() !== '' && item.reportingName ); const trades = validRawTrades.map((item: any, index: number) => { const isBuy = (item.transactionType || '').toUpperCase().startsWith('P') || (item.transactionType || '').toUpperCase().startsWith('A') || (item.transactionType || '').toLowerCase().includes('purchase') || item.acquisitionOrDisposition === 'A'; const sharesTraded = Number(item.securitiesTransacted) || 10000; const estimatedValue = Math.round(sharesTraded * (Number(item.price) || 15)); return { id: `whale_fmp_fb_${item.symbol}_${index}_${Date.now()}`, ticker: item.symbol, institution: item.reportingName, type: isBuy ? ('BUY' as const) : ('SELL' as const), sharesTraded, sharesHeld: Number(item.securitiesOwned) || sharesTraded * 5, filingDate: item.filingDate || '', estimatedValue: estimatedValue || 150000, insight: getWhaleInsight({ type: isBuy ? 'BUY' : 'SELL', institution: item.reportingName }) }; }); const res = NextResponse.json({ results: trades.slice(0, 20), liveDataAvailable: true }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } catch (fbErr: any) { console.error("====== CRITICAL INSIDER ROUTE FAILURE ======", fbErr); const res = NextResponse.json({ results: [], liveDataAvailable: false }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } } } return NextResponse.json({ error: 'Invalid type' }, { status: 400 }); } catch (err: any) { console.error("====== CRITICAL INSIDER ROUTE FAILURE ======", err); const res = NextResponse.json({ results: [], liveDataAvailable: false }, { status: 200 }); res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return res; } }