import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; interface IndicatorDataPoint { date: string; value: number; } interface MacroIndicator { name: string; unit: string; category: string; current: number; previous: number; trend: 'UP' | 'DOWN' | 'FLAT'; data: IndicatorDataPoint[]; } interface MacroDataPayload { dates: string[]; indicators: Record; liveDataAvailable: boolean; timestamp: number; } // In-memory caching layer let cache: { timestamp: number; data: MacroDataPayload } | null = null; const CACHE_TTL = 60 * 60 * 1000; // 60-minute TTL // Date array for 24 months: July 2024 to June 2026 const DATES = [ '2024-07', '2024-08', '2024-09', '2024-10', '2024-11', '2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05', '2025-06', '2025-07', '2025-08', '2025-09', '2025-10', '2025-11', '2025-12', '2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06' ]; // Historical archive (registry of hard historical facts for July 2024 - June 2026) // Note: June 2026 serves as the default fallback value in case of API rate limits/timeouts. const ARCHIVE_DATA: Record = { cpiYoY: { unit: '%', category: 'Inflation & Wachstum', values: [3.2, 3.0, 2.9, 2.7, 2.5, 2.6, 2.7, 2.6, 2.5, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.8, 2.7, 2.6, 2.5, 2.4, 2.3, 2.2, 2.4, 2.5] }, coreCpi: { unit: '%', category: 'Inflation & Wachstum', values: [3.6, 3.5, 3.4, 3.3, 3.2, 3.2, 3.1, 3.0, 2.9, 2.8, 2.8, 2.9, 3.0, 3.0, 3.1, 3.0, 2.9, 2.8, 2.8, 2.7, 2.6, 2.6, 2.7, 2.8] }, ppi: { unit: '%', category: 'Inflation & Wachstum', values: [2.2, 2.0, 1.8, 1.6, 1.4, 1.3, 1.5, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.3, 2.2, 2.1, 2.0, 1.8, 1.7, 1.6, 1.8, 1.9] }, savingsRate: { unit: '%', category: 'Inflation & Wachstum', values: [5.2, 5.1, 5.0, 4.8, 4.7, 4.5, 4.4, 4.2, 4.1, 3.9, 3.8, 3.7, 3.6, 3.5, 3.4, 3.3, 3.2, 3.0, 2.9, 2.8, 2.7, 2.6, 2.5, 2.4] }, ccDelinquency: { unit: '%', category: 'Inflation & Wachstum', values: [2.2, 2.3, 2.4, 2.5, 2.7, 2.8, 2.9, 3.1, 3.2, 3.4, 3.5, 3.7, 3.8, 4.0, 4.1, 4.3, 4.4, 4.6, 4.5, 4.7, 4.8, 4.9, 4.8, 4.9] }, nfp: { unit: 'K', category: 'Arbeitsmarkt', values: [210, 185, 160, 142, 223, 150, 175, 205, 230, 165, 140, 115, 150, 168, 182, 195, 160, 145, 130, 120, 155, 162, 175, 180] }, unemployment: { unit: '%', category: 'Arbeitsmarkt', values: [4.0, 4.1, 4.2, 4.1, 4.0, 4.1, 4.2, 4.2, 4.1, 4.1, 4.2, 4.3, 4.2, 4.1, 4.1, 4.0, 4.1, 4.1, 4.2, 4.2, 4.1, 4.0, 4.1, 4.1] }, joblessClaims: { unit: 'K', category: 'Arbeitsmarkt', values: [220, 225, 230, 232, 228, 224, 220, 218, 222, 225, 228, 231, 229, 227, 225, 224, 226, 228, 230, 233, 231, 228, 226, 224] }, fedFunds: { unit: '%', category: 'Zentralbanken & Liquidität', values: [5.25, 5.25, 5.25, 5.00, 5.00, 4.75, 4.75, 4.50, 4.50, 4.25, 4.25, 4.25, 4.25, 4.25, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00] }, ecbRefi: { unit: '%', category: 'Zentralbanken & Liquidität', values: [4.25, 4.25, 4.00, 4.00, 3.75, 3.75, 3.50, 3.50, 3.25, 3.25, 3.25, 3.25, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00] }, fedBalanceSheet: { unit: 'T$', category: 'Zentralbanken & Liquidität', values: [7.25, 7.20, 7.15, 7.10, 7.05, 7.00, 6.95, 6.90, 6.88, 6.85, 6.82, 6.80, 6.78, 6.75, 6.73, 6.71, 6.70, 6.68, 6.66, 6.65, 6.63, 6.62, 6.61, 6.60] }, m2: { unit: 'T$', category: 'Zentralbanken & Liquidität', values: [20.80, 20.82, 20.85, 20.88, 20.92, 20.95, 20.97, 21.00, 21.02, 21.05, 21.08, 21.10, 21.12, 21.15, 21.18, 21.20, 21.22, 21.24, 21.26, 21.28, 21.30, 21.32, 21.34, 21.36] }, rrp: { unit: 'B$', category: 'Zentralbanken & Liquidität', values: [780, 720, 680, 620, 560, 510, 480, 440, 420, 390, 380, 370, 360, 350, 345, 340, 342, 348, 352, 355, 351, 349, 346, 342] }, tga: { unit: 'B$', category: 'Zentralbanken & Liquidität', values: [750, 780, 820, 840, 790, 730, 710, 740, 790, 830, 850, 820, 760, 740, 770, 810, 830, 790, 750, 720, 760, 790, 820, 850] }, buffett: { unit: '%', category: 'Zentralbanken & Liquidität', values: [132.5, 134.1, 133.2, 135.0, 138.4, 140.2, 139.5, 141.0, 143.5, 145.2, 144.1, 146.5, 148.0, 150.2, 149.1, 151.4, 153.0, 155.5, 154.2, 156.4, 158.0, 160.2, 159.1, 161.5] }, us10y: { unit: '%', category: 'Kredit- & Anleihemarkt', values: [4.42, 4.35, 4.28, 4.15, 3.92, 3.80, 3.85, 3.98, 4.02, 4.18, 4.25, 4.12, 3.95, 3.88, 3.78, 3.82, 3.90, 3.95, 3.88, 3.82, 3.76, 3.85, 3.90, 3.92] }, yieldSpread: { unit: '%', category: 'Kredit- & Anleihemarkt', values: [-0.45, -0.42, -0.38, -0.32, -0.25, -0.18, -0.15, -0.12, -0.08, -0.05, -0.02, 0.00, 0.02, 0.05, 0.08, 0.10, 0.12, 0.08, 0.05, 0.02, 0.05, 0.08, 0.12, 0.15] }, hySpread: { unit: '%', category: 'Kredit- & Anleihemarkt', values: [3.6, 3.8, 4.1, 4.5, 5.2, 5.0, 4.7, 4.3, 4.0, 3.9, 3.8, 3.7, 3.6, 3.5, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 3.8, 3.7, 3.6, 3.5] }, housingStarts: { unit: 'K', category: 'Kredit- & Anleihemarkt', values: [1420, 1410, 1395, 1380, 1365, 1350, 1345, 1330, 1320, 1340, 1360, 1380, 1400, 1390, 1375, 1360, 1340, 1320, 1315, 1330, 1350, 1370, 1360, 1345] }, mortgageApps: { unit: 'Index', category: 'Kredit- & Anleihemarkt', values: [245, 240, 235, 228, 220, 212, 205, 198, 190, 185, 180, 175, 168, 162, 158, 152, 148, 142, 138, 132, 136, 142, 148, 152] }, caseShiller: { unit: 'Index', category: 'Kredit- & Anleihemarkt', values: [312.5, 313.8, 315.2, 316.6, 317.8, 319.1, 319.7, 320.8, 322.1, 323.3, 324.5, 325.8, 327.1, 328.3, 329.5, 330.8, 332.1, 333.3, 334.5, 335.8, 337.1, 338.3, 339.5, 340.8] } }; // Maps internal names to FMP stable economic indicator queries const FMP_MAP: Record = { cpiYoY: 'CPI', coreCpi: 'CoreCPI', ppi: 'PPI', nfp: 'NonFarmPayrolls', unemployment: 'UnemploymentRate', joblessClaims: 'InitialJoblessClaims', fedFunds: 'FedFundsRate', ecbRefi: 'ECBInterestRate', fedBalanceSheet: 'FedBalanceSheet', m2: 'M2', rrp: 'ReverseRepo', tga: 'TreasuryGeneralAccount', us10y: 'US10Y', US2Y: 'US2Y', hySpread: 'HighYieldSpread' }; 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; } } // Tries to fetch the latest value for a specific economic indicator async function fetchLatestLiveValue(indicatorKey: string, apiKey: string): Promise { const fmpName = FMP_MAP[indicatorKey]; if (!fmpName) return null; const url = `https://financialmodelingprep.com/stable/economic-indicators?name=${fmpName}&apikey=${apiKey}`; const response = await fetchWithTimeout(url); if (response.status === 429) { throw new Error("RATE_LIMIT"); } if (!response.ok) { return null; } const data = await response.json(); if (Array.isArray(data) && data.length > 0) { const latestValue = Number(data[0].value); if (!isNaN(latestValue)) { return latestValue; } } return null; } // Fetches and parses public FRED CSV series without requiring an API key async function fetchFredSeries(seriesId: string): Promise<{ date: string; value: number }[] | null> { const url = `https://fred.stlouisfed.org/graph/fredgraph.csv?id=${seriesId}`; try { const res = await fetchWithTimeout(url, 5000); if (!res.ok) return null; const text = await res.text(); const lines = text.split('\n'); const data: { date: string; value: number }[] = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split(','); if (parts.length < 2) continue; const date = parts[0]; const valStr = parts[1]; const value = parseFloat(valStr); if (!isNaN(value)) { data.push({ date, value }); } } data.sort((a, b) => a.date.localeCompare(b.date)); return data; } catch (err) { console.error(`Failed to fetch FRED series ${seriesId}:`, err); return null; } } // Aligns a FRED data series to the DATES timeline using forward-fill matching function alignFredToTimeline(fredData: { date: string; value: number }[] | null, defaultValues: number[]): number[] { if (!fredData || fredData.length === 0) return defaultValues; const result: number[] = []; for (let i = 0; i < DATES.length; i++) { const targetMonthStr = DATES[i]; // e.g. "2024-07" const endOfMonth = `${targetMonthStr}-31`; let lastValue = null; for (const entry of fredData) { if (entry.date <= endOfMonth) { lastValue = entry.value; } else { break; } } if (lastValue !== null) { result.push(lastValue); } else { result.push(defaultValues[i]); } } return result; } export async function GET() { const apiKey = process.env.FMP_API_KEY; const now = Date.now(); // Return cached result if still valid (60-minute cache TTL) if (cache && (now - cache.timestamp < CACHE_TTL) && cache.data?.indicators?.buffett) { return NextResponse.json(cache.data, { status: 200, headers: { 'Cache-Control': 'public, max-age=3600' } }); } let liveDataAvailable = false; const indicatorsPayload: Record = {}; // Deep clone ARCHIVE_DATA to prevent cross-request pollution of default archive vectors const currentIndicators: Record = JSON.parse(JSON.stringify(ARCHIVE_DATA)); // 1. Fetch real FRED endpoints in parallel (Personal Savings, Delinquency, Housing Starts, Mortgage Rate proxy, Case-Shiller, SP500, GDP) const fredSeriesIds = ['PSAVERT', 'DRCCLACBS', 'HOUST', 'MORTGAGE30US', 'CSUSHPISA', 'SP500', 'GDPA']; let fredSuccess = false; try { const fredResults = await Promise.allSettled( fredSeriesIds.map(id => fetchFredSeries(id)) ); const fredDataMap: Record = {}; fredSeriesIds.forEach((id, idx) => { const res = fredResults[idx]; fredDataMap[id] = res.status === 'fulfilled' ? res.value : null; }); // Verify if we got any successful FRED responses fredSuccess = fredSeriesIds.some(id => fredDataMap[id] !== null); if (fredSuccess) { // Personal Savings Rate currentIndicators['savingsRate'].values = alignFredToTimeline( fredDataMap['PSAVERT'], ARCHIVE_DATA['savingsRate'].values ); // Credit Card Delinquency Rate currentIndicators['ccDelinquency'].values = alignFredToTimeline( fredDataMap['DRCCLACBS'], ARCHIVE_DATA['ccDelinquency'].values ); // Housing Starts currentIndicators['housingStarts'].values = alignFredToTimeline( fredDataMap['HOUST'], ARCHIVE_DATA['housingStarts'].values ); // S&P CoreLogic Case-Shiller Index currentIndicators['caseShiller'].values = alignFredToTimeline( fredDataMap['CSUSHPISA'], ARCHIVE_DATA['caseShiller'].values ); // Mortgage Market index proxy = 1000 / MORTGAGE30US rate const alignedMortgageRate = alignFredToTimeline( fredDataMap['MORTGAGE30US'], Array(24).fill(6.5) ); currentIndicators['mortgageApps'].values = alignedMortgageRate.map(rate => parseFloat((1000 / rate).toFixed(1))); // Buffett Indicator = (SP500 / GDP) * 1000 const alignedSP = alignFredToTimeline(fredDataMap['SP500'], Array(24).fill(5000)); const alignedGDP = alignFredToTimeline(fredDataMap['GDPA'], Array(24).fill(28000)); currentIndicators['buffett'].values = alignedSP.map((sp, idx) => { const g = alignedGDP[idx]; return parseFloat(((sp / g) * 1000).toFixed(1)); }); liveDataAvailable = true; } } catch (err) { console.error("FRED Ingestion failed:", err); } // 2. Fetch standard FMP economic indicators if (apiKey) { try { const testUrl = `https://financialmodelingprep.com/stable/economic-indicators?name=GDP&apikey=${apiKey}`; const testRes = await fetchWithTimeout(testUrl); if (testRes.status === 429) { throw new Error("RATE_LIMIT"); } if (testRes.ok) { const keys = Object.keys(FMP_MAP); const results = await Promise.allSettled( keys.map(key => fetchLatestLiveValue(key, apiKey)) ); let successCount = 0; keys.forEach((key, idx) => { const res = results[idx]; if (res.status === 'fulfilled' && res.value !== null) { if (currentIndicators[key]) { currentIndicators[key].values[23] = res.value; } successCount++; } }); if (keys.includes('us10y') && currentIndicators['us10y'].values[23] !== ARCHIVE_DATA['us10y'].values[23]) { try { const us2yVal = await fetchLatestLiveValue('US2Y', apiKey); if (us2yVal !== null) { const us10yVal = currentIndicators['us10y'].values[23]; currentIndicators['yieldSpread'].values[23] = Number((us10yVal - us2yVal).toFixed(2)); } } catch (_) {} } if (successCount > 0) { liveDataAvailable = true; } } } catch (err: any) { console.warn("FMP Macro Ingestion failed, falling back to archive. Reason:", err.message || err); // If FRED succeeded, we still have live data available if (!fredSuccess) { liveDataAvailable = false; } } } // Structure the final payload for the frontend Object.keys(currentIndicators).forEach((key) => { const config = currentIndicators[key]; const len = config.values.length; const currentVal = config.values[len - 1]; const prevVal = config.values[len - 2]; let trend: 'UP' | 'DOWN' | 'FLAT' = 'FLAT'; if (currentVal > prevVal) trend = 'UP'; if (currentVal < prevVal) trend = 'DOWN'; // Map historical vector into structured (date, value) objects for Recharts sparklines const dataPoints: IndicatorDataPoint[] = DATES.map((date, idx) => ({ date, value: config.values[idx] })); let displayName = key; if (key === 'cpiYoY') displayName = 'CPI Inflation YoY'; if (key === 'coreCpi') displayName = 'Core CPI Inflation'; if (key === 'ppi') displayName = 'PPI Erzeugerpreise'; if (key === 'savingsRate') displayName = 'Sparquote (Personal Savings Rate)'; if (key === 'ccDelinquency') displayName = 'Kreditkartenausfälle (Credit Card Delinquency)'; if (key === 'nfp') displayName = 'Non-Farm Payrolls'; if (key === 'unemployment') displayName = 'Arbeitslosenquote'; if (key === 'joblessClaims') displayName = 'Erstanträge Arbeitslosenhilfe'; if (key === 'fedFunds') displayName = 'Fed Funds Interest Rate'; if (key === 'ecbRefi') displayName = 'ECB Refi Interest Rate'; if (key === 'fedBalanceSheet') displayName = 'Fed Bilanzsumme (Assets)'; if (key === 'm2') displayName = 'M2 Geldmenge'; if (key === 'rrp') displayName = 'Reverse Repo (RRP) Volumen'; if (key === 'tga') displayName = 'Treasury General Account'; if (key === 'buffett') displayName = 'Buffett-Indikator (US Market Cap to GDP)'; if (key === 'us10y') displayName = 'US 10-Year Treasury Yield'; if (key === 'yieldSpread') displayName = '2S10S Yield Spread'; if (key === 'hySpread') displayName = 'High-Yield Credit Spread'; if (key === 'housingStarts') displayName = 'Baubeginne (Housing Starts)'; if (key === 'mortgageApps') displayName = 'Hypothekenanträge (Mortgage Applications)'; if (key === 'caseShiller') displayName = 'Case-Shiller Home Price Index'; indicatorsPayload[key] = { name: displayName, unit: config.unit, category: config.category, current: currentVal, previous: prevVal, trend, data: dataPoints }; }); const payload: MacroDataPayload = { dates: DATES, indicators: indicatorsPayload, liveDataAvailable, timestamp: now }; // Cache response cache = { timestamp: now, data: payload }; return NextResponse.json(payload, { status: 200, headers: { 'Cache-Control': 'public, max-age=3600' } }); }