import type { AssetType } from '../types.js'; // Shape of the raw Yahoo Finance summary payload (loosely typed — fields vary by asset) type YahooSummary = Record>; interface MappedData { type: AssetType; ticker: string; [key: string]: unknown; } export const mapToStandardFormat = (ticker: string, summary: YahooSummary): MappedData => { const quoteType = summary.price?.quoteType as string | undefined; const category = ((summary.assetProfile?.category as string) || '').toLowerCase(); const yieldVal = (summary.summaryDetail?.trailingAnnualDividendYield as number) ?? 0; const isBond = category.includes('bond') || category.includes('fixed income') || category.includes('treasury') || (quoteType === 'ETF' && yieldVal > 0.02 && category === ''); if (quoteType === 'ETF') { return isBond ? { type: 'BOND', ticker, ...mapBondData(summary) } : { type: 'ETF', ticker, ...mapEtfData(summary) }; } return { type: 'STOCK', ticker, ...mapStockData(summary) }; }; const mapStockData = (summary: YahooSummary) => { const fd = (summary.financialData ?? {}) as Record; const ks = (summary.defaultKeyStatistics ?? {}) as Record; const sd = (summary.summaryDetail ?? {}) as Record; const pr = (summary.price ?? {}) as Record; const currentPrice = pr.regularMarketPrice ?? 0; const sharesOutstanding = ks.sharesOutstanding ?? 0; const operatingCashflow = fd.operatingCashflow ?? 0; const freeCashflow = fd.freeCashflow ?? 0; // P/FFO proxy — used for REIT scoring const pFFO = operatingCashflow != null && operatingCashflow > 0 && sharesOutstanding != null && sharesOutstanding > 0 ? (currentPrice as number) / (operatingCashflow / sharesOutstanding) : null; // FCF yield — negative FCF preserved so cash-burning companies fail the gate const fcfYield = freeCashflow !== 0 && sharesOutstanding != null && sharesOutstanding > 0 && currentPrice != null && currentPrice > 0 ? ((freeCashflow as number) / (sharesOutstanding as number) / (currentPrice as number)) * 100 : null; // PEG: prefer Yahoo's value, fall back to trailingPE / earningsGrowth const yahoosPEG = ks.pegRatio ?? null; const trailingPE = sd.trailingPE ?? null; const earningsGrowth = fd.earningsGrowth != null ? (fd.earningsGrowth as number) * 100 : null; const computedPEG = trailingPE != null && earningsGrowth != null && earningsGrowth > 0 ? +((trailingPE as number) / earningsGrowth).toFixed(2) : null; const pegRatio = yahoosPEG ?? computedPEG; // Quick ratio — fall back to currentRatio when missing const quickRatio = fd.quickRatio ?? fd.currentRatio ?? null; return { peRatio: trailingPE ?? ks.forwardPE, trailingPE, pegRatio, priceToBook: ks.priceToBook ?? null, evToEbitda: ks.enterpriseToEbitda ?? null, netProfitMargin: fd.profitMargins != null ? (fd.profitMargins as number) * 100 : null, operatingMargin: fd.operatingMargins != null ? (fd.operatingMargins as number) * 100 : null, returnOnEquity: fd.returnOnEquity != null ? (fd.returnOnEquity as number) * 100 : null, revenueGrowth: fd.revenueGrowth != null ? (fd.revenueGrowth as number) * 100 : null, earningsGrowth, debtToEquity: fd.debtToEquity != null ? (fd.debtToEquity as number) / 100 : null, quickRatio, fcfYield, pFFO, dividendYield: sd.trailingAnnualDividendYield != null ? (sd.trailingAnnualDividendYield as number) * 100 : null, beta: sd.beta ?? null, week52High: sd.fiftyTwoWeekHigh ?? null, week52Low: sd.fiftyTwoWeekLow ?? null, currentPrice, assetProfile: summary.assetProfile || {}, }; }; const mapEtfData = (summary: YahooSummary) => ({ expenseRatio: ((summary.summaryDetail?.expenseRatio as number) ?? 0) * 100, totalAssets: (summary.summaryDetail?.totalAssets as number) ?? 0, yield: ((summary.summaryDetail?.trailingAnnualDividendYield as number) ?? 0) * 100, fiveYearReturn: ((summary.defaultKeyStatistics?.fiveYearAverageReturn as number) ?? 0) * 100, volume: (summary.summaryDetail?.averageVolume as number) ?? (summary.price?.averageVolume as number) ?? 0, currentPrice: (summary.price?.regularMarketPrice as number) ?? 0, }); const inferCreditRating = (category: string | undefined): string => { 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'; }; const inferDuration = (category: string | undefined): number => { 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; }; const mapBondData = (summary: YahooSummary) => ({ yieldToMaturity: ((summary.summaryDetail?.yield as number) ?? 0) * 100, duration: inferDuration(summary.assetProfile?.category as string), creditRating: inferCreditRating(summary.assetProfile?.category as string), currentPrice: (summary.price?.regularMarketPrice as number) ?? 0, });