/* Mirrors scripts/valuation.py. All amounts reaching the engine use one currency. */ function num(x, name, positive) { if (typeof x !== 'number' || !Number.isFinite(x) || (positive && x <= 0)) throw new Error(name + '必须为' + (positive ? '正数' : '有限数值')); return x; } function validate(p) { ['price', 'base_profit_100m', 'shares_100m', 'exit_pe'].forEach(k => num(p[k], k, true)); if (!Number.isInteger(p.years) || p.years < 1 || p.years > 30) throw new Error('持有年限须为1—30年的整数'); if (!Number.isInteger(p.base_year) || p.base_year < 1900 || p.base_year > 2200) throw new Error('基期年份须为1900—2200的整数'); ['growth_rate', 'target_return', 'dividend_growth_rate', 'annual_dilution_rate'].forEach(k => { const x = num(p[k], k, false); if (x <= -1 || x > 10) throw new Error('增长率和收益率须大于−100%且不超过1000%'); }); if (num(p.annual_dividend_per_share, '首年股息', false) < 0) throw new Error('首年股息不能为负'); const margin = num(p.safety_margin === undefined ? 0 : p.safety_margin, '安全边际', false); if (margin < 0 || margin >= 1) throw new Error('安全边际须大于等于0%且小于100%'); } function schedule(p, growth) { return Array.from({length: p.years}, (_, i) => { const t = i + 1, profit = p.base_profit_100m * (1 + growth) ** t; const shares = p.shares_100m * (1 + p.annual_dilution_rate) ** t; return {year: p.base_year + t, profit_100m: profit, shares_100m: shares, eps: profit / shares, dividend_per_share: p.annual_dividend_per_share * (1 + p.dividend_growth_rate) ** i}; }); } function pv(rate, dividends, exit) { return dividends.reduce((s, d, i) => s + d / (1 + rate) ** (i + 1), 0) + exit / (1 + rate) ** dividends.length; } function irr(price, dividends, exit) { let low = -.999999999, high = 10; if (pv(high, dividends, exit) > price) throw new Error('回报率超过模型上限1000%'); for (let i = 0; i < 150; i++) { const m = (low + high) / 2; if (pv(m, dividends, exit) > price) low = m; else high = m; } return (low + high) / 2; } function calculate(p) { validate(p); const rows = schedule(p, p.growth_rate), last = rows[rows.length - 1]; const dividends = rows.map(x => x.dividend_per_share), exit = last.eps * p.exit_pe; const pvDiv = dividends.reduce((s, d, i) => s + d / (1 + p.target_return) ** (i + 1), 0); function reverseForPrice(price) { const requiredExit = (price - pvDiv) * (1 + p.target_return) ** p.years; const requiredProfit = requiredExit * last.shares_100m / p.exit_pe; return requiredExit <= 0 ? {required_growth_rate: null, message: '股息现值已覆盖所需现值,无最低退出盈利要求。'} : { required_growth_rate: (requiredProfit / p.base_profit_100m) ** (1 / p.years) - 1, required_terminal_profit_100m: requiredProfit, required_terminal_eps: requiredProfit / last.shares_100m, required_exit_price: requiredExit, pv_dividends_per_share: pvDiv }; } const margin = p.safety_margin === undefined ? 0 : p.safety_margin; const reverse = reverseForPrice(p.price), buffered = reverseForPrice(p.price / (1-margin)); const maxPrice = pv(p.target_return, dividends, exit), marginPrice = maxPrice * (1-margin); const result = {base_eps: p.base_profit_100m / p.shares_100m, base_pe: p.price * p.shares_100m / p.base_profit_100m, future_years: rows, exit_price: exit, holding_period_irr: irr(p.price, dividends, exit), required_growth: reverse, required_growth_with_safety_margin: buffered, max_purchase_price_at_target_return: maxPrice, max_purchase_price_with_safety_margin: marginPrice, margin_of_safety_at_purchase: 1-p.price/maxPrice, meets_safety_margin: p.price <= marginPrice + 1e-12*maxPrice}; [result.base_pe, result.exit_price, result.holding_period_irr, result.max_purchase_price_at_target_return, ...rows.flatMap(x => [x.profit_100m, x.shares_100m, x.eps, x.dividend_per_share])].forEach(x => { if (!Number.isFinite(x)) throw new Error('计算超出有效数值范围,请缩小参数'); }); for (const r of [reverse, buffered]) { if (r.required_growth_rate !== null && ![r.required_growth_rate, r.required_terminal_profit_100m, r.required_exit_price, r.required_terminal_eps].every(Number.isFinite)) throw new Error('无法反推增长率,请缩小参数'); } return result; } export { calculate, validate, schedule, pv, irr };