import type { FinancialCadence, FinancialStatementPeriod, RatioRow, StandardizedFinancialRow } from '@/lib/types'; type StatementRowMap = { income: StandardizedFinancialRow[]; balance: StandardizedFinancialRow[]; cashFlow: StandardizedFinancialRow[]; }; type RatioDefinition = { key: string; label: string; category: string; order: number; unit: RatioRow['unit']; denominatorKey: string | null; }; const RATIO_DEFINITIONS: RatioDefinition[] = [ { key: 'gross_margin', label: 'Gross Margin', category: 'margins', order: 10, unit: 'percent', denominatorKey: 'revenue' }, { key: 'operating_margin', label: 'Operating Margin', category: 'margins', order: 20, unit: 'percent', denominatorKey: 'revenue' }, { key: 'ebitda_margin', label: 'EBITDA Margin', category: 'margins', order: 30, unit: 'percent', denominatorKey: 'revenue' }, { key: 'net_margin', label: 'Net Margin', category: 'margins', order: 40, unit: 'percent', denominatorKey: 'revenue' }, { key: 'fcf_margin', label: 'FCF Margin', category: 'margins', order: 50, unit: 'percent', denominatorKey: 'revenue' }, { key: 'roa', label: 'ROA', category: 'returns', order: 60, unit: 'percent', denominatorKey: 'total_assets' }, { key: 'roe', label: 'ROE', category: 'returns', order: 70, unit: 'percent', denominatorKey: 'total_equity' }, { key: 'roic', label: 'ROIC', category: 'returns', order: 80, unit: 'percent', denominatorKey: 'average_invested_capital' }, { key: 'roce', label: 'ROCE', category: 'returns', order: 90, unit: 'percent', denominatorKey: 'average_capital_employed' }, { key: 'debt_to_equity', label: 'Debt to Equity', category: 'financial_health', order: 100, unit: 'ratio', denominatorKey: 'total_equity' }, { key: 'net_debt_to_ebitda', label: 'Net Debt to EBITDA', category: 'financial_health', order: 110, unit: 'ratio', denominatorKey: 'ebitda' }, { key: 'cash_to_debt', label: 'Cash to Debt', category: 'financial_health', order: 120, unit: 'ratio', denominatorKey: 'total_debt' }, { key: 'current_ratio', label: 'Current Ratio', category: 'financial_health', order: 130, unit: 'ratio', denominatorKey: 'current_liabilities' }, { key: 'revenue_per_share', label: 'Revenue per Share', category: 'per_share', order: 140, unit: 'currency', denominatorKey: 'diluted_shares' }, { key: 'fcf_per_share', label: 'FCF per Share', category: 'per_share', order: 150, unit: 'currency', denominatorKey: 'diluted_shares' }, { key: 'book_value_per_share', label: 'Book Value per Share', category: 'per_share', order: 160, unit: 'currency', denominatorKey: 'diluted_shares' }, { key: 'revenue_yoy', label: 'Revenue YoY', category: 'growth', order: 170, unit: 'percent', denominatorKey: 'revenue' }, { key: 'net_income_yoy', label: 'Net Income YoY', category: 'growth', order: 180, unit: 'percent', denominatorKey: 'net_income' }, { key: 'eps_yoy', label: 'EPS YoY', category: 'growth', order: 190, unit: 'percent', denominatorKey: 'diluted_eps' }, { key: 'fcf_yoy', label: 'FCF YoY', category: 'growth', order: 200, unit: 'percent', denominatorKey: 'free_cash_flow' }, { key: '3y_revenue_cagr', label: '3Y Revenue CAGR', category: 'growth', order: 210, unit: 'percent', denominatorKey: 'revenue' }, { key: '5y_revenue_cagr', label: '5Y Revenue CAGR', category: 'growth', order: 220, unit: 'percent', denominatorKey: 'revenue' }, { key: '3y_eps_cagr', label: '3Y EPS CAGR', category: 'growth', order: 230, unit: 'percent', denominatorKey: 'diluted_eps' }, { key: '5y_eps_cagr', label: '5Y EPS CAGR', category: 'growth', order: 240, unit: 'percent', denominatorKey: 'diluted_eps' }, { key: 'market_cap', label: 'Market Cap', category: 'valuation', order: 250, unit: 'currency', denominatorKey: null }, { key: 'enterprise_value', label: 'Enterprise Value', category: 'valuation', order: 260, unit: 'currency', denominatorKey: null }, { key: 'price_to_earnings', label: 'Price to Earnings', category: 'valuation', order: 270, unit: 'ratio', denominatorKey: 'diluted_eps' }, { key: 'price_to_fcf', label: 'Price to FCF', category: 'valuation', order: 280, unit: 'ratio', denominatorKey: 'free_cash_flow' }, { key: 'price_to_book', label: 'Price to Book', category: 'valuation', order: 290, unit: 'ratio', denominatorKey: 'total_equity' }, { key: 'ev_to_sales', label: 'EV to Sales', category: 'valuation', order: 300, unit: 'ratio', denominatorKey: 'revenue' }, { key: 'ev_to_ebitda', label: 'EV to EBITDA', category: 'valuation', order: 310, unit: 'ratio', denominatorKey: 'ebitda' }, { key: 'ev_to_fcf', label: 'EV to FCF', category: 'valuation', order: 320, unit: 'ratio', denominatorKey: 'free_cash_flow' } ]; function valueFor(row: StandardizedFinancialRow | undefined, periodId: string) { return row?.values[periodId] ?? null; } function divideValues(left: number | null, right: number | null) { if (left === null || right === null || right === 0) { return null; } return left / right; } function averageValues(left: number | null, right: number | null) { if (left === null || right === null) { return null; } return (left + right) / 2; } function sumValues(values: Array) { if (values.some((value) => value === null)) { return null; } return values.reduce((sum, value) => sum + (value ?? 0), 0); } function subtractValues(left: number | null, right: number | null) { if (left === null || right === null) { return null; } return left - right; } function buildRowMap(rows: StatementRowMap) { return new Map([ ...rows.income.map((row) => [row.key, row] as const), ...rows.balance.map((row) => [row.key, row] as const), ...rows.cashFlow.map((row) => [row.key, row] as const) ]); } function collectSourceRowData(rowsByKey: Map, keys: string[]) { const sourceConcepts = new Set(); const sourceRowKeys = new Set(); const sourceFactIds = new Set(); let hasDimensions = false; for (const key of keys) { const row = rowsByKey.get(key); if (!row) { continue; } hasDimensions = hasDimensions || row.hasDimensions; for (const concept of row.sourceConcepts) { sourceConcepts.add(concept); } for (const sourceRowKey of row.sourceRowKeys) { sourceRowKeys.add(sourceRowKey); } for (const sourceFactId of row.sourceFactIds) { sourceFactIds.add(sourceFactId); } } return { sourceConcepts: [...sourceConcepts].sort((left, right) => left.localeCompare(right)), sourceRowKeys: [...sourceRowKeys].sort((left, right) => left.localeCompare(right)), sourceFactIds: [...sourceFactIds].sort((left, right) => left - right), hasDimensions }; } function previousPeriodId(periods: FinancialStatementPeriod[], periodId: string) { const index = periods.findIndex((period) => period.id === periodId); return index > 0 ? periods[index - 1]?.id ?? null : null; } function cagr(current: number | null, previous: number | null, years: number) { if (current === null || previous === null || previous <= 0 || years <= 0) { return null; } return Math.pow(current / previous, 1 / years) - 1; } export function buildRatioRows(input: { periods: FinancialStatementPeriod[]; cadence: FinancialCadence; rows: StatementRowMap; pricesByPeriodId: Record; }) { const periods = [...input.periods].sort((left, right) => { return Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate); }); const rowsByKey = buildRowMap(input.rows); const valuesByKey = new Map>(); const setValue = (key: string, periodId: string, value: number | null) => { const existing = valuesByKey.get(key); if (existing) { existing[periodId] = value; } else { valuesByKey.set(key, { [periodId]: value }); } }; for (const period of periods) { const periodId = period.id; const priorId = previousPeriodId(periods, periodId); const revenue = valueFor(rowsByKey.get('revenue'), periodId); const grossProfit = valueFor(rowsByKey.get('gross_profit'), periodId); const operatingIncome = valueFor(rowsByKey.get('operating_income'), periodId); const ebitda = valueFor(rowsByKey.get('ebitda'), periodId); const netIncome = valueFor(rowsByKey.get('net_income'), periodId); const freeCashFlow = valueFor(rowsByKey.get('free_cash_flow'), periodId); const totalAssets = valueFor(rowsByKey.get('total_assets'), periodId); const priorAssets = priorId ? valueFor(rowsByKey.get('total_assets'), priorId) : null; const totalEquity = valueFor(rowsByKey.get('total_equity'), periodId); const priorEquity = priorId ? valueFor(rowsByKey.get('total_equity'), priorId) : null; const totalDebt = valueFor(rowsByKey.get('total_debt'), periodId); const cash = valueFor(rowsByKey.get('cash_and_equivalents'), periodId); const shortTermInvestments = valueFor(rowsByKey.get('short_term_investments'), periodId); const currentAssets = valueFor(rowsByKey.get('current_assets'), periodId); const currentLiabilities = valueFor(rowsByKey.get('current_liabilities'), periodId); const dilutedShares = valueFor(rowsByKey.get('diluted_shares'), periodId); const dilutedEps = valueFor(rowsByKey.get('diluted_eps'), periodId); const effectiveTaxRate = valueFor(rowsByKey.get('effective_tax_rate'), periodId); const pretaxIncome = valueFor(rowsByKey.get('pretax_income'), periodId); const incomeTaxExpense = valueFor(rowsByKey.get('income_tax_expense'), periodId); const priorRevenue = priorId ? valueFor(rowsByKey.get('revenue'), priorId) : null; const priorNetIncome = priorId ? valueFor(rowsByKey.get('net_income'), priorId) : null; const priorDilutedEps = priorId ? valueFor(rowsByKey.get('diluted_eps'), priorId) : null; const priorFcf = priorId ? valueFor(rowsByKey.get('free_cash_flow'), priorId) : null; const price = input.pricesByPeriodId[periodId] ?? null; const fallbackTaxRate = divideValues(incomeTaxExpense, pretaxIncome); const nopat = operatingIncome === null ? null : (effectiveTaxRate ?? fallbackTaxRate) === null ? null : operatingIncome * (1 - ((effectiveTaxRate ?? fallbackTaxRate) ?? 0)); const investedCapital = subtractValues(sumValues([totalDebt, totalEquity]), sumValues([cash, shortTermInvestments])); const priorInvestedCapital = priorId ? subtractValues( sumValues([ valueFor(rowsByKey.get('total_debt'), priorId), valueFor(rowsByKey.get('total_equity'), priorId) ]), sumValues([ valueFor(rowsByKey.get('cash_and_equivalents'), priorId), valueFor(rowsByKey.get('short_term_investments'), priorId) ]) ) : null; const averageInvestedCapital = averageValues(investedCapital, priorInvestedCapital); const capitalEmployed = subtractValues(totalAssets, currentLiabilities); const priorCapitalEmployed = priorId ? subtractValues(valueFor(rowsByKey.get('total_assets'), priorId), valueFor(rowsByKey.get('current_liabilities'), priorId)) : null; const averageCapitalEmployed = averageValues(capitalEmployed, priorCapitalEmployed); const marketCap = price === null || dilutedShares === null ? null : price * dilutedShares; const enterpriseValue = marketCap === null ? null : subtractValues(sumValues([marketCap, totalDebt]), sumValues([cash, shortTermInvestments])); setValue('gross_margin', periodId, divideValues(grossProfit, revenue)); setValue('operating_margin', periodId, divideValues(operatingIncome, revenue)); setValue('ebitda_margin', periodId, divideValues(ebitda, revenue)); setValue('net_margin', periodId, divideValues(netIncome, revenue)); setValue('fcf_margin', periodId, divideValues(freeCashFlow, revenue)); setValue('roa', periodId, divideValues(netIncome, averageValues(totalAssets, priorAssets))); setValue('roe', periodId, divideValues(netIncome, averageValues(totalEquity, priorEquity))); setValue('roic', periodId, divideValues(nopat, averageInvestedCapital)); setValue('roce', periodId, divideValues(operatingIncome, averageCapitalEmployed)); setValue('debt_to_equity', periodId, divideValues(totalDebt, totalEquity)); setValue('net_debt_to_ebitda', periodId, divideValues(subtractValues(totalDebt, sumValues([cash, shortTermInvestments])), ebitda)); setValue('cash_to_debt', periodId, divideValues(sumValues([cash, shortTermInvestments]), totalDebt)); setValue('current_ratio', periodId, divideValues(currentAssets, currentLiabilities)); setValue('revenue_per_share', periodId, divideValues(revenue, dilutedShares)); setValue('fcf_per_share', periodId, divideValues(freeCashFlow, dilutedShares)); setValue('book_value_per_share', periodId, divideValues(totalEquity, dilutedShares)); setValue('revenue_yoy', periodId, priorId ? divideValues(subtractValues(revenue, priorRevenue), priorRevenue) : null); setValue('net_income_yoy', periodId, priorId ? divideValues(subtractValues(netIncome, priorNetIncome), priorNetIncome) : null); setValue('eps_yoy', periodId, priorId ? divideValues(subtractValues(dilutedEps, priorDilutedEps), priorDilutedEps) : null); setValue('fcf_yoy', periodId, priorId ? divideValues(subtractValues(freeCashFlow, priorFcf), priorFcf) : null); setValue('market_cap', periodId, marketCap); setValue('enterprise_value', periodId, enterpriseValue); setValue('price_to_earnings', periodId, divideValues(price, dilutedEps)); setValue('price_to_fcf', periodId, divideValues(marketCap, freeCashFlow)); setValue('price_to_book', periodId, divideValues(marketCap, totalEquity)); setValue('ev_to_sales', periodId, divideValues(enterpriseValue, revenue)); setValue('ev_to_ebitda', periodId, divideValues(enterpriseValue, ebitda)); setValue('ev_to_fcf', periodId, divideValues(enterpriseValue, freeCashFlow)); } if (input.cadence === 'annual') { for (let index = 0; index < periods.length; index += 1) { const period = periods[index]; const periodId = period.id; const revenue = valueFor(rowsByKey.get('revenue'), periodId); const eps = valueFor(rowsByKey.get('diluted_eps'), periodId); setValue('3y_revenue_cagr', periodId, index >= 3 ? cagr(revenue, valueFor(rowsByKey.get('revenue'), periods[index - 3]?.id ?? ''), 3) : null); setValue('5y_revenue_cagr', periodId, index >= 5 ? cagr(revenue, valueFor(rowsByKey.get('revenue'), periods[index - 5]?.id ?? ''), 5) : null); setValue('3y_eps_cagr', periodId, index >= 3 ? cagr(eps, valueFor(rowsByKey.get('diluted_eps'), periods[index - 3]?.id ?? ''), 3) : null); setValue('5y_eps_cagr', periodId, index >= 5 ? cagr(eps, valueFor(rowsByKey.get('diluted_eps'), periods[index - 5]?.id ?? ''), 5) : null); } } return RATIO_DEFINITIONS.map((definition) => { const dependencyKeys = (() => { switch (definition.key) { case 'gross_margin': return ['gross_profit', 'revenue']; case 'operating_margin': return ['operating_income', 'revenue']; case 'ebitda_margin': return ['ebitda', 'revenue']; case 'net_margin': return ['net_income', 'revenue']; case 'fcf_margin': return ['free_cash_flow', 'revenue']; case 'roa': return ['net_income', 'total_assets']; case 'roe': return ['net_income', 'total_equity']; case 'roic': return ['operating_income', 'effective_tax_rate', 'income_tax_expense', 'pretax_income', 'total_debt', 'total_equity', 'cash_and_equivalents', 'short_term_investments']; case 'roce': return ['operating_income', 'total_assets', 'current_liabilities']; case 'debt_to_equity': return ['total_debt', 'total_equity']; case 'net_debt_to_ebitda': return ['total_debt', 'cash_and_equivalents', 'short_term_investments', 'ebitda']; case 'cash_to_debt': return ['cash_and_equivalents', 'short_term_investments', 'total_debt']; case 'current_ratio': return ['current_assets', 'current_liabilities']; case 'revenue_per_share': return ['revenue', 'diluted_shares']; case 'fcf_per_share': return ['free_cash_flow', 'diluted_shares']; case 'book_value_per_share': return ['total_equity', 'diluted_shares']; case 'revenue_yoy': case '3y_revenue_cagr': case '5y_revenue_cagr': return ['revenue']; case 'net_income_yoy': return ['net_income']; case 'eps_yoy': case '3y_eps_cagr': case '5y_eps_cagr': return ['diluted_eps']; case 'fcf_yoy': return ['free_cash_flow']; case 'market_cap': return ['diluted_shares']; case 'enterprise_value': return ['diluted_shares', 'total_debt', 'cash_and_equivalents', 'short_term_investments']; case 'price_to_earnings': return ['diluted_eps']; case 'price_to_fcf': return ['diluted_shares', 'free_cash_flow']; case 'price_to_book': return ['diluted_shares', 'total_equity']; case 'ev_to_sales': return ['diluted_shares', 'total_debt', 'cash_and_equivalents', 'short_term_investments', 'revenue']; case 'ev_to_ebitda': return ['diluted_shares', 'total_debt', 'cash_and_equivalents', 'short_term_investments', 'ebitda']; case 'ev_to_fcf': return ['diluted_shares', 'total_debt', 'cash_and_equivalents', 'short_term_investments', 'free_cash_flow']; default: return []; } })(); const sources = collectSourceRowData(rowsByKey, dependencyKeys); return { key: definition.key, label: definition.label, category: definition.category, order: definition.order, unit: definition.unit, values: valuesByKey.get(definition.key) ?? {}, sourceConcepts: sources.sourceConcepts, sourceRowKeys: sources.sourceRowKeys, sourceFactIds: sources.sourceFactIds, formulaKey: definition.key, hasDimensions: false, resolvedSourceRowKeys: Object.fromEntries(periods.map((period) => [period.id, null])), denominatorKey: definition.denominatorKey } satisfies RatioRow; }).filter((row) => { if (row.key.includes('_cagr')) { return Object.values(row.values).some((value) => value !== null); } return true; }); } export const RATIO_CATEGORY_ORDER = [ 'margins', 'returns', 'financial_health', 'per_share', 'growth', 'valuation' ] as const;