phase-2: extract shared utils
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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,
|
||||
});
|
||||
Reference in New Issue
Block a user