import { calculateDCF } from './dcf-math.js'; import { calculate as calculateReturn } from './stock-return-math.js'; // Cash amounts: 亿元; shares: 亿股; prices/dividends: 元/股; rates: decimals. export function calculateDCFCrosscheck(dcfInput, assumptions) { const dcf = calculateDCF(dcfInput); if (dcf.perShare <= 0) throw new Error('无正的 DCF 每股股权价值,无法进行双价格验证'); if (!Number.isFinite(assumptions.price) || assumptions.price <= 0) throw new Error('请在股价区填写大于 0 的当前股价'); if (!['manual', 'estimate'].includes(assumptions.profitMode)) throw new Error('请选择归母净利润来源'); let profit; if (assumptions.profitMode === 'estimate') { if (dcfInput.basis !== 'fcfe') throw new Error('FCFF 口径不能直接估算归母净利润,请手动填写'); if (!Number.isFinite(assumptions.conversion) || assumptions.conversion <= 0) throw new Error('基期现金转化率须大于 0'); profit = dcfInput.fcf / assumptions.conversion; } else profit = assumptions.profit; if (!Number.isFinite(profit) || profit <= 0) throw new Error('基期归母净利润须大于 0'); for (const pe of [assumptions.peLow, assumptions.pe, assumptions.peHigh]) { if (!Number.isFinite(pe) || pe <= 0) throw new Error('悲观、基准和乐观退出 PE 均须大于 0'); } if (assumptions.peLow > assumptions.pe || assumptions.pe > assumptions.peHigh) throw new Error('退出 PE 请按悲观 ≤ 基准 ≤ 乐观填写'); const model = {base_year: dcfInput.year, base_profit_100m: profit, shares_100m: dcfInput.shares, years: assumptions.years, growth_rate: assumptions.profitGrowth, target_return: assumptions.target, annual_dividend_per_share: assumptions.dividend, dividend_growth_rate: assumptions.dividendGrowth, annual_dilution_rate: assumptions.dilution, // Use the standalone calculator's safety-margin convention for both price and growth. // Actual IRR still uses the actual buy price, without a synthetic price adjustment. safety_margin: dcfInput.margin}; const scenario = (price, exitPe) => { const result = calculateReturn({...model, price, exit_pe: exitPe}); return {pe: exitPe, irr: result.holding_period_irr, requiredGrowth: result.required_growth_with_safety_margin.required_growth_rate, requiredGrowthWithoutMargin: result.required_growth.required_growth_rate, maxPrice: result.max_purchase_price_at_target_return, maxPriceWithMargin: result.max_purchase_price_with_safety_margin, meetsSafetyMargin: result.meets_safety_margin, meetsTarget: result.holding_period_irr + 1e-10 >= assumptions.target}; }; const rows = [ {name: '当前股价买入', price: assumptions.price}, {name: 'DCF 参考价买入', price: dcf.entryPrice}, ].map(row => ({...row, scenarios: [assumptions.peLow, assumptions.pe, assumptions.peHigh].map(pe => scenario(row.price, pe))})); rows.forEach(row => { row.base = row.scenarios[1]; }); const returnCeiling = rows[0].base.maxPriceWithMargin; const jointCeiling = Math.min(dcf.entryPrice, returnCeiling); const dcfCondition = assumptions.price <= dcf.entryPrice + 1e-10; const returnCondition = rows[0].base.meetsSafetyMargin; return {profit, estimated: assumptions.profitMode === 'estimate', dcf, rows, returnCeiling, jointCeiling, dcfCondition, returnCondition, dcfDiscount: 1 - assumptions.price / dcf.perShare, growthGap: rows[0].base.requiredGrowth === null ? null : assumptions.profitGrowth - rows[0].base.requiredGrowth}; }