Files
investment-sandbox/app/api/insider/route.ts

489 lines
18 KiB
TypeScript

import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
const MOCK_EXECUTIVES = [
{
id: 'mock_exec_1',
ticker: 'PLTR',
insiderName: 'Karp Alexander C.',
relation: 'CEO / Director',
type: 'SELL',
shares: 250000,
value: 9250000,
date: '2026-06-10',
insight: 'Verkauf durch CEO/CFO. Häufig automatisiert (10b5-1 Plan) zur Portfoliodiversifikation.'
},
{
id: 'mock_exec_2',
ticker: 'NVDA',
insiderName: 'Huang Jen Hsun',
relation: 'CEO / President',
type: 'SELL',
shares: 120000,
value: 15400000,
date: '2026-06-09',
insight: 'Regulärer Verkauf im Rahmen eines vorab festgelegten 10b5-1 Handelsplans.'
},
{
id: 'mock_exec_3',
ticker: 'AAPL',
insiderName: 'Maestri Luca',
relation: 'CFO / Senior VP',
type: 'BUY',
shares: 15000,
value: 2850000,
date: '2026-06-08',
insight: 'Starkes Conviction-Signal: CFO kauft eigene Aktien aus freien Mitteln.'
},
{
id: 'mock_exec_4',
ticker: 'TSLA',
insiderName: 'Taneja Vaibhav',
relation: 'Chief Accounting Officer',
type: 'BUY',
shares: 5000,
value: 950000,
date: '2026-06-07',
insight: 'Opportunistischer Conviction-Kauf mit positivem Signal für den Markt.'
}
];
const MOCK_CONGRESS = [
{
id: 'mock_cong_1',
ticker: 'MSFT',
representative: 'Nancy Pelosi',
chamber: 'HOUSE',
type: 'BUY',
valueRange: '$1,000,001 - $5,000,000',
transactionDate: '2026-05-20',
filingDate: '2026-06-05',
lagDays: 16,
insight: 'Politisches Conviction-Signal (Nancy Pelosi). Möglicher Informationsvorsprung.'
},
{
id: 'mock_cong_2',
ticker: 'NVIDIA',
representative: 'Tommy Tuberville',
chamber: 'SENATE',
type: 'SELL',
valueRange: '$100,001 - $250,000',
transactionDate: '2026-05-15',
filingDate: '2026-06-02',
lagDays: 18,
insight: 'Taktische Reduzierung der Position im Rahmen von Compliance-Richtlinien.'
},
{
id: 'mock_cong_3',
ticker: 'LMT',
representative: 'Mark Green',
chamber: 'HOUSE',
type: 'BUY',
valueRange: '$50,001 - $100,000',
transactionDate: '2026-05-18',
filingDate: '2026-06-01',
lagDays: 14,
insight: 'Akkumulation im Rüstungssektor. Zeitliche Nähe zu Haushaltsbeschlüssen.'
}
];
const MOCK_WHALES = [
{
id: 'mock_whale_1',
ticker: 'AMZN',
institution: 'Scion Asset Management (Michael Burry)',
type: 'BUY',
sharesTraded: 50000,
sharesHeld: 150000,
filingDate: '2026-05-15',
estimatedValue: 9150000,
insight: 'Institutionelle Akkumulation durch Scion Asset Management. Aufbau einer strategischen Position.'
},
{
id: 'mock_whale_2',
ticker: 'GOOGL',
institution: 'Akre Capital Management',
type: 'BUY',
sharesTraded: 250000,
sharesHeld: 1250000,
filingDate: '2026-05-15',
estimatedValue: 43250000,
insight: 'Aufbau/Verstärkung einer langfristigen Kernposition durch Akre Capital.'
},
{
id: 'mock_whale_3',
ticker: 'TSLA',
institution: 'Renaissance Technologies LLC',
type: 'SELL',
sharesTraded: 450000,
sharesHeld: 2100000,
filingDate: '2026-05-14',
estimatedValue: 85500000,
insight: 'Taktische Gewinnmitnahme / Quantitative Portfolio-Rebalancierung.'
}
];
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;
const isDevMode = process.env.DEV_MODE === 'true';
if (isDevMode) {
let mockResults: any[] = [];
if (type === 'executives') mockResults = MOCK_EXECUTIVES;
else if (type === 'congress') mockResults = MOCK_CONGRESS;
else if (type === 'whales') mockResults = MOCK_WHALES;
const res = NextResponse.json(
{ results: mockResults, liveDataAvailable: false, isShieldActive: true },
{ status: 200 }
);
res.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate');
res.headers.set('X-Shield-Active', 'true');
return res;
}
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;
}
}