feat: 增加 DCF 等估值工具

This commit is contained in:
R524809
2026-10-22 15:43:17 +08:00
parent f646ee6718
commit a46de853a0
22 changed files with 2645 additions and 3 deletions
+71
View File
@@ -0,0 +1,71 @@
// 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};
}