637 lines
27 KiB
TypeScript
637 lines
27 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
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<string, {
|
|
marketCap: number;
|
|
trailingPE: number;
|
|
forwardPE: number;
|
|
peg: number;
|
|
priceToBook: number;
|
|
dividendYield: number;
|
|
}> = {
|
|
// 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);
|
|
}
|
|
|
|
function getSimulatedSloan(ticker: string): { sloanRatio: number; sloanRegime: 'SAFE' | 'ANOMALY' } {
|
|
let hash = 0;
|
|
for (let i = 0; i < ticker.length; i++) {
|
|
hash = ticker.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
const sloanRatio = parseFloat(((hash % 170) / 10).toFixed(2)); // range: 0.0 to 17.0
|
|
const sloanRegime = (sloanRatio > 10 || sloanRatio < -10) ? ('ANOMALY' as const) : ('SAFE' as const);
|
|
return { sloanRatio, sloanRegime };
|
|
}
|
|
|
|
async function fetchFmpSloanRatio(ticker: string, apiKey: string): Promise<{ sloanRatio: number; sloanRegime: 'SAFE' | 'ANOMALY' }> {
|
|
if (ticker.includes('-USD') || ticker.includes('BTC') || ticker.includes('ETH')) {
|
|
return getSimulatedSloan(ticker);
|
|
}
|
|
try {
|
|
const incUrl = `https://financialmodelingprep.com/api/v3/income-statement/${ticker}?period=quarter&limit=1&apikey=${apiKey}`;
|
|
const balUrl = `https://financialmodelingprep.com/api/v3/balance-sheet-statement/${ticker}?period=quarter&limit=1&apikey=${apiKey}`;
|
|
const cfUrl = `https://financialmodelingprep.com/api/v3/cash-flow-statement/${ticker}?period=quarter&limit=1&apikey=${apiKey}`;
|
|
|
|
const [incRes, balRes, cfRes] = await Promise.all([
|
|
fetch(incUrl, { signal: AbortSignal.timeout(3000) }),
|
|
fetch(balUrl, { signal: AbortSignal.timeout(3000) }),
|
|
fetch(cfUrl, { signal: AbortSignal.timeout(3000) })
|
|
]);
|
|
|
|
if (incRes.ok && balRes.ok && cfRes.ok) {
|
|
const incData = await incRes.json();
|
|
const balData = await balRes.json();
|
|
const cfData = await cfRes.json();
|
|
|
|
const inc = incData?.[0] || {};
|
|
const bal = balData?.[0] || {};
|
|
const cf = cfData?.[0] || {};
|
|
|
|
const netIncome = inc.netIncome || 0;
|
|
const cfo = cf.netCashProvidedByOperatingActivities || cf.operatingCashFlow || 0;
|
|
const cfi = cf.netCashUsedForInvestingActivites || cf.netCashUsedForInvestingActivities || cf.investingCashFlow || 0;
|
|
const totalAssets = bal.totalAssets || 0;
|
|
|
|
const accruals = netIncome - (cfo + cfi);
|
|
const sloanRatio = totalAssets > 0 ? (accruals / totalAssets) * 100 : 0;
|
|
const sloanRegime = (sloanRatio > 10 || sloanRatio < -10) ? ('ANOMALY' as const) : ('SAFE' as const);
|
|
|
|
return {
|
|
sloanRatio: Number(sloanRatio.toFixed(2)),
|
|
sloanRegime
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Error fetching FMP Sloan data for ${ticker}:`, err);
|
|
}
|
|
|
|
return getSimulatedSloan(ticker);
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
}
|
|
|
|
function getMockFundamentals(ticker: string): {
|
|
marketCap: number;
|
|
trailingPE: number;
|
|
forwardPE: number;
|
|
peg: number;
|
|
priceToBook: number;
|
|
dividendYield: number;
|
|
} {
|
|
if (MOCK_FUNDAMENTALS[ticker]) {
|
|
return MOCK_FUNDAMENTALS[ticker];
|
|
}
|
|
if (ticker.endsWith('-USD')) {
|
|
return { marketCap: 5e9, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 };
|
|
}
|
|
let hash = 0;
|
|
for (let i = 0; i < ticker.length; i++) {
|
|
hash = ticker.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
const seedCap = Math.abs(hash % 18) + 1;
|
|
const marketCap = seedCap * 100 * 1000000;
|
|
const trailingPE = 10 + Math.abs(hash % 30);
|
|
const forwardPE = trailingPE * 0.82;
|
|
const peg = 0.5 + (Math.abs(hash % 15) / 10);
|
|
const priceToBook = 1.0 + (Math.abs(hash % 40) / 10);
|
|
const dividendYield = (Math.abs(hash % 6) / 2) / 100;
|
|
return {
|
|
marketCap,
|
|
trailingPE,
|
|
forwardPE,
|
|
peg,
|
|
priceToBook,
|
|
dividendYield
|
|
};
|
|
}
|
|
|
|
async function fetchBinanceFundingRate(symbol: string): Promise<number> {
|
|
const symbolMap: Record<string, string> = {
|
|
'BTC-USD': 'BTCUSDT',
|
|
'ETH-USD': 'ETHUSDT',
|
|
'SOL-USD': 'SOLUSDT',
|
|
'BTC': 'BTCUSDT',
|
|
'ETH': 'ETHUSDT',
|
|
'SOL': 'SOLUSDT'
|
|
};
|
|
const binanceSymbol = symbolMap[symbol] || `${symbol.replace('-USD', '')}USDT`;
|
|
try {
|
|
const res = await fetch(`https://fapi.binance.com/fapi/v1/fundingRate?symbol=${binanceSymbol}&limit=1`, {
|
|
cache: 'no-store',
|
|
signal: AbortSignal.timeout(2000)
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data && data[0]) {
|
|
return parseFloat(data[0].fundingRate) * 100; // convert to % (e.g. 0.0001 -> 0.01%)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to fetch Binance funding rate for ${symbol}:`, err);
|
|
}
|
|
// Default fallbacks matching original stats
|
|
return symbol.includes('BTC') ? -0.015 : symbol.includes('ETH') ? 0.045 : 0.082;
|
|
}
|
|
|
|
async function fetchBinanceFuturesArbitrageData(symbol: string): Promise<{ fundingRate: number; futuresPrice: number }> {
|
|
const symbolMap: Record<string, string> = {
|
|
'BTC-USD': 'BTCUSDT',
|
|
'ETH-USD': 'ETHUSDT',
|
|
'SOL-USD': 'SOLUSDT',
|
|
'BTC': 'BTCUSDT',
|
|
'ETH': 'ETHUSDT',
|
|
'SOL': 'SOLUSDT'
|
|
};
|
|
const binanceSymbol = symbolMap[symbol] || `${symbol.replace('-USD', '')}USDT`;
|
|
let fundingRate = symbol.includes('BTC') ? -0.015 : symbol.includes('ETH') ? 0.045 : 0.082;
|
|
let futuresPrice = 0;
|
|
|
|
try {
|
|
const res = await fetch(`https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${binanceSymbol}`, {
|
|
cache: 'no-store',
|
|
signal: AbortSignal.timeout(2000)
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data) {
|
|
if (data.lastFundingRate !== undefined) {
|
|
fundingRate = parseFloat(data.lastFundingRate) * 100; // convert to % (e.g. 0.0001 -> 0.01%)
|
|
}
|
|
if (data.markPrice !== undefined) {
|
|
futuresPrice = parseFloat(data.markPrice);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to fetch Binance premium index for ${symbol}:`, err);
|
|
}
|
|
return { fundingRate, futuresPrice };
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
const isDevMode = process.env.DEV_MODE === 'true';
|
|
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 or read from local CSV
|
|
const rawResults = await Promise.all(
|
|
tickers.map(async (ticker) => {
|
|
try {
|
|
if (ticker === 'BTC-USD') {
|
|
const csvPath = path.join(process.cwd(), 'backend', 'data', 'BTC-USD.csv');
|
|
if (fs.existsSync(csvPath)) {
|
|
const content = fs.readFileSync(csvPath, 'utf8');
|
|
const lines = content.trim().split('\n');
|
|
if (lines.length >= 2) {
|
|
const lastLine = lines[lines.length - 1];
|
|
const columns = lastLine.split(',');
|
|
const currentPrice = parseFloat(columns[4]);
|
|
|
|
const prevLine = lines[lines.length - 2];
|
|
const prevColumns = prevLine.split(',');
|
|
const prevClose = parseFloat(prevColumns[4]);
|
|
const dayChange = (currentPrice - prevClose) / prevClose;
|
|
|
|
// Extract valid prices from the CSV file
|
|
const validPrices = lines.slice(1).map(l => {
|
|
const parts = l.split(',');
|
|
return parseFloat(parts[4]);
|
|
}).filter(p => typeof p === 'number' && p > 0);
|
|
|
|
const slice50 = validPrices.slice(-50);
|
|
const sma50 = slice50.reduce((a: number, b: number) => a + b, 0) / slice50.length;
|
|
const maDeviation = (currentPrice - sma50) / sma50;
|
|
|
|
const peak52w = Math.max(...validPrices);
|
|
const dist52w = (currentPrice - peak52w) / peak52w;
|
|
|
|
const rsi14 = calculateRSI14(validPrices);
|
|
|
|
const returns = [];
|
|
for (let i = 1; i < validPrices.length; i++) {
|
|
returns.push((validPrices[i] - validPrices[i - 1]) / validPrices[i - 1]);
|
|
}
|
|
|
|
const slice90 = validPrices.slice(-90);
|
|
const peak90 = Math.max(...slice90);
|
|
const priceChange = (currentPrice - peak90) / peak90;
|
|
|
|
let futuresPrice = 0;
|
|
let fundingRate = -0.015;
|
|
const binanceData = await fetchBinanceFuturesArbitrageData('BTC-USD');
|
|
fundingRate = binanceData.fundingRate;
|
|
futuresPrice = binanceData.futuresPrice;
|
|
if (futuresPrice === 0) {
|
|
futuresPrice = currentPrice * 1.0008; // Fallback 0.08% premium
|
|
}
|
|
const basisSpread = futuresPrice - currentPrice;
|
|
const fundingDec = fundingRate / 100;
|
|
const basisApy = (Math.pow(1 + fundingDec, 1095) - 1) * 100;
|
|
|
|
return {
|
|
ticker,
|
|
name: 'Bitcoin USD (Local CSV)',
|
|
currentPrice,
|
|
peakPrice: peak90,
|
|
priceChange,
|
|
dayChange,
|
|
maDeviation,
|
|
dist52w,
|
|
rsi14,
|
|
returns: returns.slice(-90),
|
|
futuresPrice,
|
|
basisSpread,
|
|
basisApy,
|
|
fundingRate
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
let futuresPrice = 0;
|
|
let fundingRate = ticker.includes('BTC') ? -0.015 : ticker.includes('ETH') ? 0.045 : 0.082;
|
|
let basisSpread = 0;
|
|
let basisApy = 0;
|
|
|
|
if (ticker === 'BTC-USD' || ticker === 'ETH-USD' || ticker === 'SOL-USD') {
|
|
const binanceData = await fetchBinanceFuturesArbitrageData(ticker);
|
|
fundingRate = binanceData.fundingRate;
|
|
futuresPrice = binanceData.futuresPrice;
|
|
if (futuresPrice === 0) {
|
|
const premiumPercent = ticker.includes('BTC') ? 0.0008 : ticker.includes('ETH') ? 0.0012 : 0.0018;
|
|
futuresPrice = currentPrice * (1 + premiumPercent);
|
|
}
|
|
basisSpread = futuresPrice - currentPrice;
|
|
const fundingDec = fundingRate / 100;
|
|
basisApy = (Math.pow(1 + fundingDec, 1095) - 1) * 100;
|
|
}
|
|
|
|
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
|
|
futuresPrice,
|
|
basisSpread,
|
|
basisApy,
|
|
fundingRate
|
|
};
|
|
} 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[];
|
|
futuresPrice?: number;
|
|
basisSpread?: number;
|
|
basisApy?: number;
|
|
fundingRate?: 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 & Crypto futures indicators
|
|
const results = await Promise.all(
|
|
sortedResults.map(async (res) => {
|
|
const isCrypto = res.ticker.includes('-USD') || res.ticker.includes('BTC') || res.ticker.includes('ETH') || res.ticker.includes('SOL');
|
|
|
|
let cryptoDetails = {};
|
|
if (isCrypto) {
|
|
const fundingRate = res.fundingRate !== undefined ? res.fundingRate : await fetchBinanceFundingRate(res.ticker);
|
|
const cleanTicker = res.ticker.replace('-USD', '');
|
|
cryptoDetails = {
|
|
fundingRate,
|
|
openInterestChange: cleanTicker === 'BTC' ? 8.2 : cleanTicker === 'ETH' ? -3.5 : 14.5,
|
|
longShortRatio: cleanTicker === 'BTC' ? 0.92 : cleanTicker === 'ETH' ? 1.34 : 1.62,
|
|
whaleInflow: cleanTicker === 'BTC' ? 480 : cleanTicker === 'ETH' ? -120 : 1250,
|
|
exchangeReserves: cleanTicker === 'BTC' ? -1.4 : cleanTicker === 'ETH' ? 0.8 : -2.8,
|
|
futuresPrice: res.futuresPrice,
|
|
basisSpread: res.basisSpread,
|
|
basisApy: res.basisApy
|
|
};
|
|
}
|
|
|
|
// Pull live data if in top 15, otherwise load direct mock fallback
|
|
if (top15Tickers.has(res.ticker)) {
|
|
const fundamentals = isDevMode
|
|
? { ...getMockFundamentals(res.ticker), dividendYield: getMockFundamentals(res.ticker).dividendYield * 100 }
|
|
: await fetchFMPFundamentalData(res.ticker, fmpApiKey);
|
|
|
|
const sloan = isDevMode
|
|
? getSimulatedSloan(res.ticker)
|
|
: await fetchFmpSloanRatio(res.ticker, fmpApiKey);
|
|
|
|
return { ...res, ...fundamentals, ...sloan, ...cryptoDetails };
|
|
} else {
|
|
const mock = MOCK_FUNDAMENTALS[res.ticker] || { marketCap: 0, trailingPE: 0, forwardPE: 0, peg: 0, priceToBook: 0, dividendYield: 0 };
|
|
const sloan = getSimulatedSloan(res.ticker);
|
|
return {
|
|
...res,
|
|
...mock,
|
|
dividendYield: mock.dividendYield * 100,
|
|
...sloan,
|
|
...cryptoDetails
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
const response = NextResponse.json({
|
|
results,
|
|
isShieldActive: isDevMode
|
|
});
|
|
response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate');
|
|
if (isDevMode) {
|
|
response.headers.set('X-Shield-Active', 'true');
|
|
}
|
|
return response;
|
|
}
|