Files
smart-fin-chart/js/tools/stock-return-math.js
T
2026-10-22 15:43:17 +08:00

72 lines
4.5 KiB
JavaScript

/* 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 };