export const mapToStandardFormat = (ticker, summary) => { const quoteType = summary.price?.quoteType; const category = (summary.assetProfile?.category || '').toLowerCase(); const yieldVal = summary.summaryDetail?.trailingAnnualDividendYield ?? 0; // Logic to determine type const isBond = category.includes('bond') || category.includes('fixed income') || category.includes('treasury') || (quoteType === 'ETF' && yieldVal > 0.02 && category === ''); // Heuristic fallback if (quoteType === 'ETF') { return isBond ? { type: 'BOND', ticker, ...mapBondData(summary), } : { type: 'ETF', ticker, ...mapEtfData(summary), }; } // Default to STOCK (covers 'EQUITY' or missing types) return { type: 'STOCK', ticker, ...mapStockData(summary), }; }; const mapStockData = (summary) => { const fd = summary.financialData ?? {}; const ks = summary.defaultKeyStatistics ?? {}; const sd = summary.summaryDetail ?? {}; const pr = summary.price ?? {}; const currentPrice = pr.regularMarketPrice ?? 0; const sharesOutstanding = ks.sharesOutstanding ?? 0; const operatingCashflow = fd.operatingCashflow ?? 0; const freeCashflow = fd.freeCashflow ?? 0; // P/FFO proxy (price / operating cash flow per share) — used for REIT scoring const pFFO = operatingCashflow > 0 && sharesOutstanding > 0 ? currentPrice / (operatingCashflow / sharesOutstanding) : null; // FCF yield = free cash flow per share / price. // Negative FCF is preserved (not nulled) — a company burning cash should fail the gate, // not be silently skipped as "no data". const fcfYield = freeCashflow !== 0 && sharesOutstanding > 0 && currentPrice > 0 ? (freeCashflow / sharesOutstanding / currentPrice) * 100 : null; // PEG computation: use Yahoo's value first; fall back to trailingPE / earningsGrowth // earningsGrowth from Yahoo is a decimal (e.g. 0.15 = 15%), convert to whole number first const yahoosPEG = ks.pegRatio ?? null; const trailingPE = sd.trailingPE ?? null; const earningsGrowth = fd.earningsGrowth != null ? fd.earningsGrowth * 100 : null; // now in % const computedPEG = trailingPE != null && earningsGrowth > 0 ? +(trailingPE / earningsGrowth).toFixed(2) : null; const pegRatio = yahoosPEG ?? computedPEG; // prefer Yahoo's, fall back to computed // Quick ratio — fall back to currentRatio when quickRatio is missing const quickRatio = fd.quickRatio ?? fd.currentRatio ?? null; return { // Valuation — trailing PE is the audited number; forward PE is an analyst estimate // (historically 10-15% optimistic). Use trailing as primary for fundamental mode. peRatio: trailingPE ?? ks.forwardPE, trailingPE, pegRatio, priceToBook: ks.priceToBook ?? null, evToEbitda: ks.enterpriseToEbitda ?? null, // Profitability netProfitMargin: fd.profitMargins != null ? fd.profitMargins * 100 : null, operatingMargin: fd.operatingMargins != null ? fd.operatingMargins * 100 : null, returnOnEquity: fd.returnOnEquity != null ? fd.returnOnEquity * 100 : null, // Growth revenueGrowth: fd.revenueGrowth != null ? fd.revenueGrowth * 100 : null, earningsGrowth, // Financial health debtToEquity: fd.debtToEquity != null ? fd.debtToEquity / 100 : null, quickRatio, // Cash flow fcfYield, pFFO, // Income dividendYield: sd.trailingAnnualDividendYield != null ? sd.trailingAnnualDividendYield * 100 : null, // Risk & momentum beta: sd.beta ?? null, week52High: sd.fiftyTwoWeekHigh ?? null, week52Low: sd.fiftyTwoWeekLow ?? null, currentPrice, assetProfile: summary.assetProfile || {}, }; }; const mapEtfData = (summary) => ({ expenseRatio: (summary.summaryDetail?.expenseRatio ?? 0) * 100, totalAssets: summary.summaryDetail?.totalAssets ?? 0, yield: (summary.summaryDetail?.trailingAnnualDividendYield ?? 0) * 100, // fiveYearAverageReturn is annualised total return — valid proxy for performance vs peers. fiveYearReturn: (summary.defaultKeyStatistics?.fiveYearAverageReturn ?? 0) * 100, // averageVolume from summaryDetail is average daily trading volume — used for liquidity gate. volume: summary.summaryDetail?.averageVolume ?? summary.price?.averageVolume ?? 0, currentPrice: summary.price?.regularMarketPrice ?? 0, }); /** * Infer credit rating from ETF category string (Yahoo Finance doesn't expose * bond credit ratings directly). Defaults to BBB (investment grade) when unknown. */ const inferCreditRating = (category) => { const cat = (category || '').toLowerCase(); if (cat.includes('government') || cat.includes('treasury')) return 'AAA'; if (cat.includes('muni')) return 'AA'; if (cat.includes('high yield') || cat.includes('junk')) return 'BB'; if (cat.includes('corporate') || cat.includes('investment grade')) return 'A'; return 'BBB'; // conservative default }; // Infers approximate effective duration (years) from bond ETF category name. // Buckets match standard industry classifications (short < 3y, intermediate 3-7y, long > 10y). const inferDuration = (category) => { const cat = (category || '').toLowerCase(); if (cat.includes('short') || cat.includes('ultrashort') || cat.includes('1-3')) return 2; if (cat.includes('intermediate') || cat.includes('3-7') || cat.includes('3-10')) return 5; if (cat.includes('long') || cat.includes('10+') || cat.includes('20+')) return 18; if (cat.includes('target maturity') || cat.includes('defined maturity')) return 4; return 6; // conservative default — typical aggregate bond fund duration }; const mapBondData = (summary) => ({ yieldToMaturity: (summary.summaryDetail?.yield ?? 0) * 100, // KNOWN LIMITATION: Yahoo Finance does not expose effective duration via the modules // we fetch (assetProfile, financialData, defaultKeyStatistics, price, summaryDetail). // The `fundProfile` module has duration for some funds but requires a separate fetch. // We use the ETF category name to infer a rough duration bucket as a proxy. duration: inferDuration(summary.assetProfile?.category), creditRating: inferCreditRating(summary.assetProfile?.category), currentPrice: summary.price?.regularMarketPrice ?? 0, });