Files
2026-10-22 15:43:17 +08:00

72 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Segmented, year-end cash-flow DCF. Rates are decimals; cash amounts are 亿元, shares 亿股.
export function calculateDCF(p) {
const finite = (value, label) => {
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${label}须为有效数字`);
};
if (!['fcff', 'fcfe'].includes(p.basis)) throw new Error('请选择现金流口径');
for (const [key, label] of Object.entries({fcf: '基期自由现金流', shares: '当前总股本', rate: '折现率', terminal: '永续增长率', margin: '安全边际'})) finite(p[key], label);
if (p.fcf <= 0) throw new Error('本模型要求基期可持续自由现金流大于 0');
if (p.shares <= 0) throw new Error('请填写该公司的当前总股本(亿股),且须大于 0');
if (!Number.isInteger(p.year) || p.year < 1900 || p.year > 2200) throw new Error('基期年份须为 1900—2200 的整数');
if (p.rate <= -1 || p.terminal <= -1 || p.terminal >= p.rate) throw new Error('永续增长率须小于折现率,且两者均须大于 −100%');
if (p.margin < 0 || p.margin >= 1) throw new Error('安全边际须大于等于 0% 且小于 100%');
if (!Array.isArray(p.stages) || p.stages.length < 1 || p.stages.length > 5) throw new Error('请设置 1—5 个增长阶段');
let years = 0;
for (const stage of p.stages) {
if (!Number.isInteger(stage.years) || stage.years < 1) throw new Error('每阶段年数须为正整数');
finite(stage.growth, '阶段增长率');
if (stage.growth <= -1) throw new Error('阶段增长率须大于 −100%');
years += stage.years;
}
if (years > 30) throw new Error('显式预测期合计最多 30 年');
let adjustment = 0;
if (p.basis === 'fcff') {
for (const [key, label] of Object.entries({debt: '净债务', assets: '非经营资产净额', minority: '少数股东权益'})) finite(p[key], label);
adjustment = -p.debt + p.assets - p.minority;
}
let cash = p.fcf, t = 0, forecastPV = 0;
const rows = [];
p.stages.forEach((stage, i) => {
for (let y = 0; y < stage.years; y++) {
t++;
cash *= 1 + stage.growth;
const factor = (1 + p.rate) ** -t;
const presentValue = cash * factor;
forecastPV += presentValue;
rows.push({year: p.year + t, stage: i + 1, growth: stage.growth, cash, factor, presentValue});
}
});
const terminalCash = cash * (1 + p.terminal);
const terminalValue = terminalCash / (p.rate - p.terminal);
const terminalPV = terminalValue / (1 + p.rate) ** years;
const operatingValue = forecastPV + terminalPV;
const equityValue = operatingValue + adjustment;
const perShare = equityValue / p.shares;
const entryPrice = perShare * (1 - p.margin);
const terminalWeight = terminalPV / operatingValue;
const result = {years, rows, terminalCash, terminalValue, forecastPV, terminalPV, operatingValue,
adjustment, equityValue, perShare, entryPrice, terminalWeight};
if (![terminalCash, terminalValue, forecastPV, terminalPV, operatingValue, equityValue, perShare,
entryPrice, terminalWeight, ...rows.flatMap(row => [row.cash, row.factor, row.presentValue])].every(Number.isFinite)) {
throw new Error('参数超出有效计算范围,请缩小参数');
}
return result;
}
// The price bands are user-defined discounts/premiums around this DCF scenario.
export function calculateDCFPriceRange({perShare, margin, premium}) {
if (!Number.isFinite(perShare) || perShare <= 0) throw new Error('无正的每股股权价值,无法划分股价区间');
if (!Number.isFinite(margin) || margin < 0 || margin >= 1) throw new Error('低估折价须大于等于 0% 且小于 100%');
if (!Number.isFinite(premium) || premium < 0 || premium > 10) throw new Error('高估溢价阈值须为 0%—1000%');
const low = perShare * (1 - margin);
const high = perShare * (1 + premium);
if (![low, high].every(Number.isFinite)) throw new Error('区间超出有效数值范围');
return {low, high, perShare};
}
export function classifyDCFPrice(price, range) {
if (!Number.isFinite(price) || price <= 0) throw new Error('当前股价须为大于 0 的有效数字');
const zone = price < range.low ? '低估' : price > range.high ? '高估' : '合理';
return {zone, deviation: price / range.perShare - 1};
}