import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; export const revalidate = 0; // Partitioned active asset universes const US_TICKERS = [ 'AAPL', 'MSFT', 'NVDA', 'TSLA', 'AMD', 'SMCI', 'NFLX', 'AMZN', 'GOOGL', 'META', 'WMT', 'JNJ', 'PG', 'MRK', 'PLTR', 'BABA', 'CVX', 'XOM', 'BAC', 'JPM' ]; const EU_TICKERS = [ 'ASML', 'SAP', 'MC.PA', 'OR.PA', 'NESN', 'NOVOB', 'SHEL', 'BP', 'HSBC', 'ALV.DE', 'VOW3.DE', 'BMW.DE', 'SIE.DE', 'DTE.DE', 'MBG.DE', 'BAS.DE', 'SAN.MC', 'BBVA.MC' ]; const CRYPTO_TICKERS = [ 'BTC-USD', 'ETH-USD', 'SOL-USD', 'ADA-USD', 'XRP-USD', 'DOGE-USD', 'DOT-USD', 'LINK-USD', 'LTC-USD', 'AVAX-USD', 'BNB-USD', 'TRX-USD', 'NEAR-USD' ]; // Fallback database for fundamental overlay metrics to ensure absolute robustness const MOCK_FUNDAMENTALS: Record = { // US Markets 'AAPL': { marketCap: 3000e9, trailingPE: 30.5, forwardPE: 26.8, peg: 1.5, priceToBook: 40.8, dividendYield: 0.005 }, 'MSFT': { marketCap: 3200e9, trailingPE: 35.2, forwardPE: 30.1, peg: 1.8, priceToBook: 12.4, dividendYield: 0.007 }, 'NVDA': { marketCap: 2800e9, trailingPE: 65.4, forwardPE: 32.5, peg: 0.9, priceToBook: 45.2, dividendYield: 0.0002 }, 'TSLA': { marketCap: 600e9, trailingPE: 55.0, forwardPE: 42.0, peg: 2.1, priceToBook: 8.5, dividendYield: 0.0 }, 'AMD': { marketCap: 250e9, trailingPE: 45.2, forwardPE: 28.5, peg: 1.4, priceToBook: 4.8, dividendYield: 0.0 }, 'SMCI': { marketCap: 25e9, trailingPE: 22.4, forwardPE: 15.2, peg: 0.6, priceToBook: 6.2, dividendYield: 0.0 }, 'NFLX': { marketCap: 280e9, trailingPE: 38.5, forwardPE: 29.2, peg: 1.3, priceToBook: 11.5, dividendYield: 0.0 }, 'AMZN': { marketCap: 1800e9, trailingPE: 40.2, forwardPE: 32.1, peg: 1.2, priceToBook: 8.2, dividendYield: 0.0 }, 'GOOGL':{ marketCap: 2100e9, trailingPE: 25.4, forwardPE: 21.2, peg: 1.1, priceToBook: 6.8, dividendYield: 0.0 }, 'META': { marketCap: 1200e9, trailingPE: 28.1, forwardPE: 22.4, peg: 1.0, priceToBook: 7.5, dividendYield: 0.0 }, 'WMT': { marketCap: 500e9, trailingPE: 26.5, forwardPE: 23.1, peg: 2.5, priceToBook: 5.4, dividendYield: 0.014 }, 'JNJ': { marketCap: 380e9, trailingPE: 15.4, forwardPE: 14.2, peg: 2.8, priceToBook: 5.1, dividendYield: 0.032 }, 'PG': { marketCap: 390e9, trailingPE: 24.2, forwardPE: 22.1, peg: 3.1, priceToBook: 7.2, dividendYield: 0.025 }, 'MRK': { marketCap: 300e9, trailingPE: 16.8, forwardPE: 14.5, peg: 1.9, priceToBook: 4.9, dividendYield: 0.028 }, 'PLTR': { marketCap: 85e9, trailingPE: 80.2, forwardPE: 55.4, peg: 1.7, priceToBook: 14.2, dividendYield: 0.0 }, 'BABA': { marketCap: 180e9, trailingPE: 9.5, forwardPE: 8.2, peg: 0.8, priceToBook: 1.1, dividendYield: 0.012 }, 'CVX': { marketCap: 290e9, trailingPE: 12.1, forwardPE: 11.2, peg: 2.0, priceToBook: 1.7, dividendYield: 0.042 }, 'XOM': { marketCap: 480e9, trailingPE: 13.4, forwardPE: 12.2, peg: 1.8, priceToBook: 2.1, dividendYield: 0.037 }, 'BAC': { marketCap: 310e9, trailingPE: 11.5, forwardPE: 10.4, peg: 1.5, priceToBook: 1.0, dividendYield: 0.024 }, 'JPM': { marketCap: 550e9, trailingPE: 12.4, forwardPE: 11.5, peg: 1.6, priceToBook: 1.6, dividendYield: 0.022 }, // EU Markets 'ASML': { marketCap: 350e9, trailingPE: 42.1, forwardPE: 33.4, peg: 1.6, priceToBook: 22.4, dividendYield: 0.009 }, 'SAP': { marketCap: 220e9, trailingPE: 34.5, forwardPE: 28.1, peg: 1.5, priceToBook: 8.4, dividendYield: 0.011 }, 'MC.PA': { marketCap: 410e9, trailingPE: 24.2, forwardPE: 21.5, peg: 2.0, priceToBook: 6.9, dividendYield: 0.018 }, 'OR.PA': { marketCap: 240e9, trailingPE: 32.1, forwardPE: 29.4, peg: 2.4, priceToBook: 7.8, dividendYield: 0.016 }, 'NESN': { marketCap: 280e9, trailingPE: 20.4, forwardPE: 18.5, peg: 2.8, priceToBook: 5.6, dividendYield: 0.031 }, 'NOVOB': { marketCap: 520e9, trailingPE: 38.2, forwardPE: 31.0, peg: 1.4, priceToBook: 32.1, dividendYield: 0.008 }, 'SHEL': { marketCap: 210e9, trailingPE: 8.5, forwardPE: 7.8, peg: 1.2, priceToBook: 1.1, dividendYield: 0.041 }, 'BP': { marketCap: 105e9, trailingPE: 7.2, forwardPE: 6.5, peg: 1.0, priceToBook: 0.9, dividendYield: 0.049 }, 'HSBC': { marketCap: 140e9, trailingPE: 6.8, forwardPE: 6.2, peg: 1.3, priceToBook: 0.7, dividendYield: 0.062 }, 'ALV.DE': { marketCap: 95e9, trailingPE: 10.4, forwardPE: 9.2, peg: 1.1, priceToBook: 1.2, dividendYield: 0.048 }, 'VOW3.DE':{ marketCap: 60e9, trailingPE: 4.1, forwardPE: 3.8, peg: 0.5, priceToBook: 0.4, dividendYield: 0.075 }, 'BMW.DE': { marketCap: 62e9, trailingPE: 5.2, forwardPE: 4.8, peg: 0.7, priceToBook: 0.6, dividendYield: 0.068 }, 'SIE.DE': { marketCap: 135e9, trailingPE: 15.2, forwardPE: 13.1, peg: 1.3, priceToBook: 2.4, dividendYield: 0.028 }, 'DTE.DE': { marketCap: 115e9, trailingPE: 13.8, forwardPE: 12.1, peg: 1.4, priceToBook: 1.8, dividendYield: 0.031 }, 'MBG.DE': { marketCap: 70e9, trailingPE: 5.5, forwardPE: 5.1, peg: 0.8, priceToBook: 0.8, dividendYield: 0.072 }, 'BAS.DE': { marketCap: 45e9, trailingPE: 12.4, forwardPE: 10.8, peg: 1.9, priceToBook: 1.1, dividendYield: 0.065 }, 'SAN.MC': { marketCap: 75e9, trailingPE: 6.2, forwardPE: 5.8, peg: 0.9, priceToBook: 0.7, dividendYield: 0.045 }, 'BBVA.MC':{ marketCap: 58e9, trailingPE: 6.5, forwardPE: 6.0, peg: 0.8, priceToBook: 0.8, dividendYield: 0.051 }, // Crypto Assets 'BTC-USD': { marketCap: 1300e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'ETH-USD': { marketCap: 420e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'SOL-USD': { marketCap: 75e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'ADA-USD': { marketCap: 18e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'XRP-USD': { marketCap: 32e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'DOGE-USD': { marketCap: 22e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'DOT-USD': { marketCap: 7e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'LINK-USD': { marketCap: 9e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'LTC-USD': { marketCap: 6e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'AVAX-USD': { marketCap: 12e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'BNB-USD': { marketCap: 90e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'TRX-USD': { marketCap: 10e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }, 'NEAR-USD': { marketCap: 6e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 } }; // Calculate standard 14-day Welles Wilder RSI function calculateRSI14(prices: number[]): number { if (prices.length < 15) return 50; let gains = 0; let losses = 0; for (let i = 1; i <= 14; i++) { const diff = prices[i] - prices[i - 1]; if (diff > 0) { gains += diff; } else { losses -= diff; } } let avgGain = gains / 14; let avgLoss = losses / 14; for (let i = 15; i < prices.length; i++) { const diff = prices[i] - prices[i - 1]; const gain = diff > 0 ? diff : 0; const loss = diff < 0 ? -diff : 0; avgGain = (avgGain * 13 + gain) / 14; avgLoss = (avgLoss * 13 + loss) / 14; } if (avgLoss === 0) return 100; const rs = avgGain / avgLoss; return 100 - 100 / (1 + rs); } // Fetch fundamental data from FMP with safe fallback async function fetchFMPFundamentalData(ticker: string, apiKey: string) { if (ticker.includes('-USD') || ticker.includes('BTC') || ticker.includes('ETH')) { const mock = MOCK_FUNDAMENTALS[ticker] || { marketCap: 0, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }; return { ...mock, dividendYield: mock.dividendYield * 100 }; } try { const profileUrl = `https://financialmodelingprep.com/stable/profile?symbol=${ticker}&apikey=${apiKey}`; const ratiosUrl = `https://financialmodelingprep.com/stable/ratios-ttm?symbol=${ticker}&apikey=${apiKey}`; const [profRes, ratRes] = await Promise.all([ fetch(profileUrl, { signal: AbortSignal.timeout(3000) }), fetch(ratiosUrl, { signal: AbortSignal.timeout(3000) }) ]); if (!profRes.ok || !ratRes.ok) { throw new Error(`FMP request failed for ${ticker}`); } const profData = await profRes.json(); const ratData = await ratRes.json(); const profile = profData?.[0] || {}; const ratios = ratData?.[0] || {}; const marketCap = profile.marketCap || MOCK_FUNDAMENTALS[ticker]?.marketCap || 0; const trailingPE = ratios.priceToEarningsRatioTTM || MOCK_FUNDAMENTALS[ticker]?.trailingPE || 0; const peg = ratios.priceToEarningsGrowthRatioTTM || MOCK_FUNDAMENTALS[ticker]?.peg || 0; const priceToBook = ratios.priceToBookRatioTTM || MOCK_FUNDAMENTALS[ticker]?.priceToBook || 0; const dividendYield = ratios.dividendYieldTTM || MOCK_FUNDAMENTALS[ticker]?.dividendYield || 0; // Invert Forward PE mathematically based on implied PEG growth let forwardPE = null; if (trailingPE && peg && peg > 0) { const growth = trailingPE / (peg * 100); forwardPE = trailingPE / (1 + growth); } else if (trailingPE) { forwardPE = trailingPE * 0.91; } else { forwardPE = MOCK_FUNDAMENTALS[ticker]?.forwardPE || 0; } return { marketCap, trailingPE: Number(trailingPE.toFixed(2)), forwardPE: Number(forwardPE.toFixed(2)), peg: Number(peg.toFixed(2)), priceToBook: Number(priceToBook.toFixed(2)), dividendYield: Number((dividendYield * 100).toFixed(2)) // convert to percentage (e.g. 0.015 -> 1.50) }; } catch (err) { console.warn(`Error fetching FMP data for ${ticker}, using fallback:`, err); const mock = MOCK_FUNDAMENTALS[ticker] || { marketCap: 0, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }; return { ...mock, dividendYield: mock.dividendYield * 100 }; } } export async function GET(request: Request) { const { searchParams } = new URL(request.url); const tickerQuery = searchParams.get('ticker'); const tickersQuery = searchParams.get('tickers'); const mode = searchParams.get('mode') || 'day_crash'; const region = searchParams.get('region') || 'us'; const fmpApiKey = process.env.FMP_API_KEY || 'U6lOXaOFPye7oc1D235kyAqJeQaiTAWc'; let tickers: string[] = []; try { if (tickerQuery) { tickers = [tickerQuery.trim().toUpperCase()]; } else if (tickersQuery && tickersQuery !== 'top_losers') { tickers = tickersQuery.split(',').map(t => t.trim().toUpperCase()); } else { // Partition by region if (region === 'eu') { tickers = EU_TICKERS; } else if (region === 'crypto') { tickers = CRYPTO_TICKERS; } else { tickers = US_TICKERS; } } } catch (err: any) { console.error('Ticker resolution error:', err.message); } if (tickers.length === 0) { tickers = US_TICKERS; } // Fetch Yahoo Finance 1y charts in parallel const rawResults = await Promise.all( tickers.map(async (ticker) => { try { const response = await fetch( `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?range=1y&interval=1d`, { cache: 'no-store', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } } ); if (!response.ok) { throw new Error(`Failed to fetch chart data for ${ticker}`); } const data = await response.json(); const result = data.chart?.result?.[0]; if (!result) { throw new Error(`No chart data found for ${ticker}`); } const closePrices = result.indicators?.quote?.[0]?.close || []; const validPrices = closePrices.filter((p: any): p is number => typeof p === 'number' && p > 0); if (validPrices.length === 0) { throw new Error(`No valid closing prices found for ${ticker}`); } const currentPrice = validPrices[validPrices.length - 1]; const prevPrice = validPrices.length >= 2 ? validPrices[validPrices.length - 2] : currentPrice; // Mode 1: single day change const dayChange = (currentPrice - prevPrice) / prevPrice; // Mode 2: 50-day moving average drop const slice50 = validPrices.slice(-50); const sma50 = slice50.reduce((a: number, b: number) => a + b, 0) / slice50.length; const maDeviation = (currentPrice - sma50) / sma50; // Mode 3: 52-week distance const peak52w = Math.max(...validPrices); const dist52w = (currentPrice - peak52w) / peak52w; // Mode 4: 14-day RSI const rsi14 = calculateRSI14(validPrices); // Daily returns for local GJR-GARCH calculations const returns = []; for (let i = 1; i < validPrices.length; i++) { returns.push((validPrices[i] - validPrices[i - 1]) / validPrices[i - 1]); } // We use the 90d peak drop as the default 'priceChange' compatibility variable const slice90 = validPrices.slice(-90); const peak90 = Math.max(...slice90); const priceChange = (currentPrice - peak90) / peak90; return { ticker, name: result.meta?.longName || result.meta?.shortName || `${ticker} Corp.`, currentPrice, peakPrice: peak90, priceChange, dayChange, maDeviation, dist52w, rsi14, returns: returns.slice(-90) // return last 90 days of returns to keep payload slim }; } catch (err: any) { console.error(`Error fetching ticker ${ticker}:`, err.message); return { ticker, error: err.message || 'Unknown error' }; } }) ); // Filter out invalid tickers const validResults = rawResults.filter((r) => !r.error) as Array<{ ticker: string; name: string; currentPrice: number; peakPrice: number; priceChange: number; dayChange: number; maDeviation: number; dist52w: number; rsi14: number; returns: number[]; }>; // Rank results based on the requested scan mode let sortedResults = [...validResults]; if (mode === 'ma_drop') { sortedResults.sort((a, b) => a.maDeviation - b.maDeviation); } else if (mode === '52w_dist') { sortedResults.sort((a, b) => a.dist52w - b.dist52w); } else if (mode === 'rsi_oversold') { sortedResults.sort((a, b) => a.rsi14 - b.rsi14); } else { // Default: day_crash (sort by dayChange ascending, most negative first) sortedResults.sort((a, b) => a.dayChange - b.dayChange); } // Identify the top 15 outlier tickers to apply FMP overlay const top15Tickers = new Set(sortedResults.slice(0, 15).map(r => r.ticker)); // Overlay FMP fundamental details const results = await Promise.all( sortedResults.map(async (res) => { // Pull live data if in top 15, otherwise load direct mock fallback if (top15Tickers.has(res.ticker)) { const fundamentals = await fetchFMPFundamentalData(res.ticker, fmpApiKey); return { ...res, ...fundamentals }; } else { const mock = MOCK_FUNDAMENTALS[res.ticker] || { marketCap: 0, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 }; return { ...res, ...mock, dividendYield: mock.dividendYield * 100 }; } }) ); const response = NextResponse.json({ results }); response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate'); return response; }