Compare commits
1 Commits
feat-catl
...
feat-valuer
| Author | SHA1 | Date | |
|---|---|---|---|
| 273177a89b |
@@ -188,18 +188,16 @@ A:编辑 `js/theme.js` 的 `PALETTE` 对象,所有图表自动同步。
|
|||||||
|
|
||||||
单独运行估值检查:`npm run test:valuation`;`npm test` 同时运行图表回归与估值检查。
|
单独运行估值检查:`npm run test:valuation`;`npm test` 同时运行图表回归与估值检查。
|
||||||
|
|
||||||
「DCF 估值」Tab(`#/valuation-tools/dcf`)采用分段工作台,可设置 1—5 个增长阶段,每段输入年数与现金流年增长率,总预测期不超过 30 年。支持 FCFF/WACC 与 FCFE/股权资本成本两种口径、永续增长终值、股权价值调整和逐年折现明细。
|
「DCF 估值」Tab(`#/valuation-tools/dcf`)采用独立的现金流工作台,可设置 1—5 个增长阶段,每段输入年数与现金流年增长率,总预测期不超过 30 年。支持 FCFF/WACC 与 FCFE/股权资本成本两种口径、永续增长终值、股权价值调整和逐年折现明细。
|
||||||
|
|
||||||
总股本在基础参数区单独填写,单位为亿股;每股价值 = 股权价值(亿元)÷ 当前总股本(亿股)。每家公司的股本、股本日期、现金流和增长假设独立保存在当前浏览器,也可新增公司。真实公司初次使用时,现金流与股本留空,需自行核实后填写;教学示例的 10 亿股仅为假设。当前模型使用人民币金额与预测期内不变的股本。
|
总股本在基础参数区单独填写,单位为亿股;每股价值 = 股权价值(亿元)÷ 当前总股本(亿股)。每家公司的股本、股本日期、现金流和增长假设独立保存在当前浏览器,也可新增公司。真实公司初次使用时,现金流与股本留空,需自行核实后填写;教学示例的 10 亿股仅为假设。当前模型使用人民币金额与预测期内不变的股本。
|
||||||
|
|
||||||
公司以按钮形式展示,点击即可切换;当前股价、股价日期和区间阈值也按公司自动保存到 LocalStorage,兼容原有 DCF 本地记录。股价需要手动输入,不自动请求行情。未填或填错股价不影响已完成的 DCF 计算,也不会保留上次股价标记。
|
公司以按钮形式展示,点击即可切换;当前股价、股价日期和区间阈值也按公司自动保存到 LocalStorage,兼容原有 DCF 本地记录。股价需要手动输入,不自动请求行情。未填或填错股价不影响已完成的 DCF 计算,也不会保留上次股价标记。
|
||||||
|
|
||||||
ECharts 区间图以 DCF 每股价值为基准,默认低于其 80% 为低估、高于其 120% 为高估,两端边界均包含在合理区间内。「区间设置」可分别修改低估折价(同时作为安全边际)与高估溢价阈值。这是当前假设下的自定义价格分区,不是独立的投资结论;无正股权价值时不显示区间。
|
ECharts 区间图以 DCF 每股价值为基准,默认低于其 80% 为低估、高于其 120% 为高估,两端边界均包含在合理区间内。股价旁直接展示低估折价(同时作为安全边际)与高估溢价阈值,可随时修改。安全边际价、低估价格及折价使用绿色,溢价与高估价格使用红色。这是当前假设下的自定义价格分区,不是独立的投资结论;无正股权价值时不显示区间。
|
||||||
|
|
||||||
「目标收益交叉验证」共用当前公司的股价、股本、基期和 DCF 参考价。输入独立的归母净利润及预计利润增速后,可比较现价、DCF 含安全边际参考价两种买入情景的所需利润 CAGR 与预计 IRR,并查看悲观/基准/乐观退出 PE 情景。计算复用独立「目标收益反推增长率」工具的数学引擎。
|
首屏顶部并列展示 DCF 每股内在价值、安全边际参考价、当前股价及相对内在价值的折溢价。下方左侧编辑基础参数、分段增长率和股权调整,右侧查看现金流现值、终值与股权价值拆解。逐年现金流图表始终独立展示,计算明细可折叠展开;小屏幕自动改为上下排列。
|
||||||
|
|
||||||
首屏顶部并列展示当前股价、按现价计算的预计年化 IRR、达到目标收益所需的利润年增速;后两项采用基准退出 PE,与下方现价情景结果同步。修改参数或切换公司后即时更新,参数不完整或无效时清除旧值。
|
DCF 页只计算现金流估值。预计 IRR、利润增长门槛和退出 PE 情景使用单独的「收益反推」Tab。现有公司的 DCF 记录沿用原有 LocalStorage 键,旧版额外字段不会参与 DCF 计算。
|
||||||
|
|
||||||
FCFE 模式可选择以用户提供的基期现金转化率(FCFE ÷ 归母净利润)估算利润;该值会明确标注为估算,不作为独立财报验证。FCFF 模式要求单独填写归母净利润。真实公司的利润、转化率和预计利润增速默认留空,新增验证参数仍按公司保存在原有 LocalStorage 记录中。
|
标题栏右侧的问号打开「估值概念说明」,包含 FCFF、FCFE、口径选择、折现率、永续终值、安全边际与区间说明,支持目录定位和键盘关闭。
|
||||||
|
|
||||||
双买入价格表按各自实际买入价反推增速及计算 IRR,不额外叠加安全边际。「共同参考买入上限」取 DCF 参考价和目标收益模型参考价的较低值,两边各应用一次安全边际。分红按独立路径计算;交叉验证中的股本变化仅影响收益模型,DCF 仍采用固定股本假设。验证参数缺失或无效时只隐藏验证结果,不影响有效的 DCF 结果。
|
|
||||||
|
|||||||
+198
-868
File diff suppressed because it is too large
Load Diff
@@ -1,99 +1,71 @@
|
|||||||
<section class="dcf-calculator return-calculator" aria-label="DCF 估值计算器">
|
<section class="dcf-calculator" aria-label="DCF 估值计算器">
|
||||||
<header class="df-header"><div><div class="df-eyebrow">VALUATION WORKBENCH <span>/ 估值研究</span></div><h2>DCF 估值工作台</h2><p class="df-subtitle">从现金流价值出发,用盈利增长检验回报。</p></div><div class="df-header-actions"><div class="df-header-mark"><span class="df-live-dot"></span> 参数调整 · 即时试算</div><button class="df-help-trigger" type="button" data-help-open aria-label="打开估值概念说明" aria-haspopup="dialog" title="估值概念说明"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.5 9a2.5 2.5 0 0 1 5 .3c0 1.7-2.5 2-2.5 3.7"></path><circle cx="12" cy="16.5" r=".8" fill="currentColor" stroke="none"></circle></svg></button></div></header>
|
<header class="df-header">
|
||||||
<div class="df-profile-bar">
|
<div><div class="df-eyebrow">VALUATION WORKBENCH</div><h2>DCF 估值工作台</h2><p class="df-subtitle">从未来自由现金流,衡量今天的企业价值。</p></div>
|
||||||
<div class="df-company-buttons" data-company-buttons role="group" aria-label="切换估值公司"></div>
|
<div class="df-header-actions"><span class="df-header-mark">参数调整 · 即时试算</span><button class="df-help-trigger" type="button" data-help-open aria-label="打开估值概念说明" aria-haspopup="dialog" title="估值概念说明"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.5 9a2.5 2.5 0 0 1 5 .3c0 1.7-2.5 2-2.5 3.7"></path><circle cx="12" cy="16.5" r=".8" fill="currentColor" stroke="none"></circle></svg></button></div>
|
||||||
<button class="export-button" type="button" data-new-company>+ 新增公司</button>
|
</header>
|
||||||
<span class="text-small text-muted df-save-status" data-save-status role="status">每家公司的参数独立保存在本机浏览器</span>
|
<div class="df-profile-bar">
|
||||||
</div>
|
<div class="df-company-buttons" data-company-buttons role="group" aria-label="切换估值公司"></div>
|
||||||
<form class="df-new-company" data-company-form hidden>
|
<button type="button" data-new-company>+ 新增公司</button>
|
||||||
<label>公司名称<input class="form-control" data-company-name maxlength="60" placeholder="输入公司名称" required></label>
|
<span class="df-save-status" data-save-status role="status">每家公司的参数独立保存在本机浏览器</span>
|
||||||
<button class="export-button" type="submit">添加</button>
|
</div>
|
||||||
<button class="export-button" type="button" data-cancel-company>取消</button>
|
<form class="df-new-company" data-company-form hidden>
|
||||||
<span class="text-small text-destructive" data-company-error role="alert"></span>
|
<label>公司名称<input data-company-name maxlength="60" placeholder="输入公司名称" required></label>
|
||||||
</form>
|
<button type="submit">添加</button><button type="button" data-cancel-company>取消</button>
|
||||||
<div class="df-company-empty" data-company-empty role="status" hidden>还没有估值公司,添加一家公司开始估值。</div>
|
<span class="df-company-error" data-company-error role="alert"></span>
|
||||||
<nav class="df-jumpnav" aria-label="估值工作台导航"><button type="button" data-jump="overview">01 估值总览</button><button type="button" data-jump="parameters">02 DCF 参数</button><button type="button" data-jump="crosscheck">03 双模型校验</button><button type="button" data-jump="detail">04 计算明细</button></nav>
|
</form>
|
||||||
<div class="df-overview" data-section="overview" tabindex="-1"><div class="df-output" aria-live="polite"><div class="df-hero"><div class="df-muted">DCF 每股内在价值</div><div class="df-number" data-price>—</div><div class="df-muted">人民币/股 · 当前现金流假设</div><div class="df-value-note"><span data-margin-label>20%</span> 安全边际参考价 <b data-buy>—</b> <span class="df-muted">元/股</span><div class="df-muted" data-shares-note></div></div></div>
|
<div class="df-company-empty" data-company-empty role="status" hidden>还没有估值公司,添加一家公司开始估值。</div>
|
||||||
</div><div class="df-top-comparison" aria-live="polite" aria-label="现价与收益门槛对比">
|
<section class="df-overview" aria-label="DCF 估值总览" aria-live="polite">
|
||||||
<div><h3>当前股价</h3>
|
<div class="df-stat df-stat-main"><h3>DCF 每股内在价值</h3><div class="df-stat-value" data-price>—</div><p class="df-muted">元/股 · <span data-basis-caption>FCFF 口径</span></p></div>
|
||||||
<div class="df-price-readout"><span class="df-market-price" data-market-current>—</span><span class="df-muted">元/股</span><span class="df-zone-badge" data-market-badge hidden></span></div>
|
<div class="df-stat"><h3>安全边际参考价</h3><div class="df-stat-value" data-buy>—</div><p class="df-muted" data-discount-note>按设定的安全边际折价</p></div>
|
||||||
<div class="df-muted" data-deviation>填写当前股价后查看所处区间</div>
|
<div class="df-stat"><h3>当前股价</h3><div class="df-stat-value" data-market-current>—</div><p class="df-muted">元/股 · 手动输入</p></div>
|
||||||
</div>
|
<div class="df-stat df-stat-position"><h3>现价相对内在价值</h3><div class="df-stat-value" data-deviation>—</div><p class="df-muted" data-price-position>填写当前股价后查看所处区间</p></div>
|
||||||
<div class="df-top-return"><h3>预计年化 IRR</h3>
|
</section>
|
||||||
<div class="df-top-return-value" data-top-irr>—</div>
|
<section class="df-market" aria-label="当前股价与估值区间">
|
||||||
<div class="df-muted" data-top-irr-detail>补全交叉验证参数后显示</div>
|
<div class="df-market-head"><div><h3>现价在估值的什么位置?</h3><p class="df-muted">以当前 DCF 假设为基准的价格区间</p></div><span class="df-zone-badge" data-market-badge hidden></span></div>
|
||||||
</div>
|
<div class="df-price-controls">
|
||||||
<div class="df-top-return"><h3>目标收益所需利润年增速(含边际)</h3>
|
<div class="df-price-fields"><label>当前股价(人民币)<input data-field="marketPrice" type="number" min="0" step="any" placeholder="手动输入当前股价"></label><label>股价日期(选填)<input data-field="quoteDate" type="date"></label></div>
|
||||||
<div class="df-top-return-value" data-top-growth>—</div>
|
<div class="df-fields df-interval-fields" role="group" aria-label="估值区间阈值"><label>低估折价/安全边际(%)<input data-field="margin" type="number" min="0" max="99.99" step="any"></label><label>高估溢价阈值(%)<input data-field="premium" type="number" min="0" max="1000" step="any"></label></div>
|
||||||
<div class="df-muted" data-top-growth-detail>按现价与基准退出 PE 反推</div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
<section class="df-decision" aria-label="双模型结论" aria-live="polite"><div><span class="df-eyebrow">双模型共同参考上限</span><strong data-overview-ceiling>—</strong><span class="df-muted">元/股</span></div><p data-overview-verdict>填写当前股价及利润假设,比较现金流价值与目标收益门槛。</p><button type="button" data-jump="crosscheck">查看校验 ↓</button></section>
|
|
||||||
<section class="df-market" aria-label="当前股价与估值区间">
|
|
||||||
<div class="df-market-head">
|
|
||||||
<div><div class="df-eyebrow">PRICE POSITION</div><h3>现价在估值的什么位置?</h3><div class="df-muted">以当前 DCF 假设为基准的价格区间</div></div>
|
|
||||||
<div class="df-price-fields">
|
|
||||||
<label>当前股价(人民币)<input data-field="marketPrice" type="number" min="0" step="any" placeholder="手动输入当前股价"></label>
|
|
||||||
<label>股价日期(选填)<input data-field="quoteDate" type="date"></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="df-price-chart" data-market-chart role="img" aria-label="当前股价与 DCF 估值区间" hidden></div>
|
||||||
|
<div class="df-zone-keys" data-zone-keys hidden><div class="df-zone-key"><span><i class="df-zone-dot"></i>低估区间</span><strong data-zone-low>—</strong></div><div class="df-zone-key"><span><i class="df-zone-dot"></i>合理区间</span><strong data-zone-fair>—</strong></div><div class="df-zone-key"><span><i class="df-zone-dot"></i>高估区间</span><strong data-zone-high>—</strong></div></div>
|
||||||
|
<p class="df-muted df-market-message" data-market-message>请补全现金流与股本后查看估值区间</p>
|
||||||
|
<div class="df-error" data-market-error role="alert" hidden></div>
|
||||||
|
<p class="df-muted df-interval-note">区间由当前 DCF 假设与折溢价阈值划分,不自动获取行情。</p>
|
||||||
|
</section>
|
||||||
|
<div class="df-layout">
|
||||||
|
<section class="df-form" aria-label="DCF 参数">
|
||||||
|
<div class="df-section-head"><h3>估值假设</h3><span class="df-muted" data-context>教学示例 · 人民币</span></div>
|
||||||
|
<section class="df-parameter-group"><h4><span>01</span>基础参数</h4><div class="df-fields">
|
||||||
|
<label>现金流口径<select data-field="basis"><option value="fcff">企业自由现金流 FCFF</option><option value="fcfe">股权自由现金流 FCFE</option></select></label>
|
||||||
|
<label>基期自由现金流(亿元)<input data-field="fcf" type="number" step="any"></label>
|
||||||
|
<label>当前总股本(亿股,必填)<input data-field="shares" type="number" min="0" step="any" required placeholder="填写该公司总股本"></label>
|
||||||
|
<label>股本数据日期(选填)<input data-field="shareDate" type="date"></label>
|
||||||
|
<label>现金流基期年份<input data-field="year" type="number" min="1900" max="2200" step="1"></label>
|
||||||
|
<label><span data-rate-label>折现率 WACC(%)</span><input data-field="rate" type="number" step="any"></label>
|
||||||
|
</div><p class="df-muted" data-basis-note>先折现企业现金流,再调整为股权价值。</p></section>
|
||||||
|
<section class="df-parameter-group"><div class="df-section-head"><h4><span>02</span>分段增长率</h4><button type="button" class="df-quiet-button" data-add>+ 添加阶段</button></div><div class="df-stages" data-stages></div><p class="df-total df-muted" data-duration></p></section>
|
||||||
|
<section class="df-parameter-group"><h4><span>03</span>永续经营与股权调整</h4><div class="df-fields">
|
||||||
|
<label>永续增长率(%)<input data-field="terminal" type="number" step="any"></label>
|
||||||
|
<label data-debt-field>净债务(亿元,可为负)<input data-field="debt" type="number" step="any"></label>
|
||||||
|
<label data-assets-field>其他非经营资产净额(亿元)<input data-field="assets" type="number" step="any"></label>
|
||||||
|
<label data-minority-field>少数股东权益(亿元)<input data-field="minority" type="number" step="any"></label>
|
||||||
|
</div><p class="df-muted">所有金额使用人民币,股本在预测期内保持不变。净债务与非经营资产中的超额现金避免重复计入。</p></section>
|
||||||
|
</section>
|
||||||
|
<section class="df-results" aria-label="DCF 价值拆解">
|
||||||
|
<div class="df-section-head"><h3>价值从哪里来?</h3><span class="df-muted">亿元</span></div>
|
||||||
|
<div class="df-result-line"><span>预测期现金流现值</span><b data-pv>—</b></div><div class="df-result-line"><span>永续终值现值</span><b data-tv>—</b></div><div class="df-result-line df-sum"><span data-value-label>企业经营价值</span><b data-ev>—</b></div>
|
||||||
|
<div data-adjustment><div class="df-result-line"><span>− 净债务</span><b data-debt-value>—</b></div><div class="df-result-line"><span>+ 非经营资产净额</span><b data-assets-value>—</b></div><div class="df-result-line"><span>− 少数股东权益</span><b data-minority-value>—</b></div></div>
|
||||||
|
<div class="df-result-line df-equity"><span>股权价值</span><b data-equity>—</b></div><p class="df-muted df-shares-note" data-shares-note>补全总股本后换算每股价值</p>
|
||||||
|
<div class="df-value-per-share"><span>每股内在价值</span><strong><span data-value-per-share>—</span><small> 元/股</small></strong></div>
|
||||||
|
<div class="df-composition" aria-hidden="true"><span class="df-segment-a" data-composition-forecast></span><span class="df-segment-b" data-composition-terminal></span></div><div class="df-legend"><span>预测期 <b data-forecast-share>—</b></span><span>永续终值 <b data-terminal-share>—</b></span></div><p class="df-terminal-note" data-terminal-note>补全有效参数后查看价值构成</p>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="df-price-chart" data-market-chart role="img" aria-label="当前股价与 DCF 估值区间" hidden></div>
|
<div class="df-error" role="alert" data-error hidden></div><div class="df-warning" data-warning hidden></div>
|
||||||
<div class="df-zone-keys" data-zone-keys hidden>
|
<section class="df-cashflow-section" data-cashflow-section aria-label="逐年现金流与现值图表" hidden>
|
||||||
<div class="df-zone-key"><span><i class="df-zone-dot"></i>低估区间</span><strong data-zone-low>—</strong></div>
|
<div class="df-section-head"><div><h3>逐年现金流与折现现值</h3><p class="df-muted" data-cashflow-caption></p></div><span class="df-muted">单位:亿元</span></div>
|
||||||
<div class="df-zone-key"><span><i class="df-zone-dot"></i>合理区间</span><strong data-zone-fair>—</strong></div>
|
<div class="df-cashflow-chart" data-cashflow-chart role="img" aria-label="逐年自由现金流与折现现值对比"></div><div class="df-error" data-cashflow-error role="alert" hidden></div>
|
||||||
<div class="df-zone-key"><span><i class="df-zone-dot"></i>高估区间</span><strong data-zone-high>—</strong></div>
|
<p class="df-muted">浅棕为当年自由现金流,深棕为折算到基期的现值;仅展示显式预测期,不含永续终值。悬停或轻触柱形可查看阶段、增长率及折现因子。</p>
|
||||||
</div>
|
<details class="df-table-section" data-detail><summary>逐年现金流与折现明细 <span class="df-muted" data-table-summary></span></summary><div class="df-table-wrap"><table aria-label="逐年现金流折现"><thead><tr><th>年份/阶段</th><th>增长率</th><th>自由现金流(亿元)</th><th>折现因子</th><th>现值(亿元)</th></tr></thead><tbody data-rows></tbody></table></div></details>
|
||||||
<div class="df-muted df-market-message" data-market-message>请补全现金流与股本后查看估值区间</div>
|
</section>
|
||||||
<div class="df-error" data-market-error role="alert" hidden></div>
|
<footer class="df-foot"><span>人民币口径 · 年末现金流 · 永续增长法</span><span data-model-note></span></footer>
|
||||||
<div class="df-interval-rule">
|
|
||||||
<span class="df-muted">区间依据当前 DCF 假设与折溢价阈值划分,不自动获取行情</span>
|
|
||||||
<details><summary>区间设置</summary>
|
|
||||||
<div class="df-fields df-interval-fields">
|
|
||||||
<label>低估折价/安全边际(%)<input data-field="margin" type="number" min="0" max="99.99" value="20" step="any"></label>
|
|
||||||
<label>高估溢价阈值(%)<input data-field="premium" type="number" min="0" max="1000" value="20" step="any"></label>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="df-layout">
|
|
||||||
<div class="df-form" data-section="parameters" tabindex="-1"><div class="df-column-heading"><span>01</span><div><h3>现金流估值</h3><p>企业能产生多少可分配现金流</p></div><b>DCF</b></div>
|
|
||||||
<section><div class="df-section-head"><h3>基础参数</h3><span class="df-muted"><span data-context>教学示例 · 人民币</span></span></div>
|
|
||||||
<div class="df-fields">
|
|
||||||
<label>现金流口径<select class="cursor-interaction" data-field="basis"><option value="fcff">企业自由现金流 FCFF</option><option value="fcfe">股权自由现金流 FCFE</option></select></label>
|
|
||||||
<label>基期自由现金流(亿元)<input data-field="fcf" type="number" value="100" step="any"></label>
|
|
||||||
<label class="df-share-field">当前总股本(亿股,必填)<input data-field="shares" type="number" min="0" step="any" required placeholder="填写该公司总股本"><small class="df-muted">用于每股价值换算,1 亿股 = 100,000,000 股</small></label>
|
|
||||||
<label>股本数据日期(选填)<input data-field="shareDate" type="date"></label>
|
|
||||||
<label>现金流基期年份<input data-field="year" type="number" value="2026" step="1"></label>
|
|
||||||
<label><span data-rate-label>折现率 WACC(%)</span><input data-field="rate" type="number" value="10" step="0.5"></label>
|
|
||||||
</div></section>
|
|
||||||
<section><div class="df-section-head"><h3>分段增长率</h3><button class="cursor-interaction" type="button" data-add>+ 添加阶段</button></div>
|
|
||||||
<div class="df-stages" data-stages></div><div class="df-total df-muted" data-duration></div>
|
|
||||||
</section>
|
|
||||||
<section><div class="df-section-head"><h3>永续经营与股权换算</h3></div>
|
|
||||||
<div class="df-fields">
|
|
||||||
<label>永续增长率(%)<input data-field="terminal" type="number" value="2" step="0.5"></label>
|
|
||||||
|
|
||||||
<label data-debt-field>净债务(亿元,可为负)<input data-field="debt" type="number" value="100" step="any"></label>
|
|
||||||
<label data-assets-field>其他非经营资产净额(亿元)<input data-field="assets" type="number" value="0" step="any"></label>
|
|
||||||
<label data-minority-field>少数股东权益(亿元)<input data-field="minority" type="number" value="0" step="any"></label>
|
|
||||||
|
|
||||||
</div></section>
|
|
||||||
<div class="df-results"><h3>价值拆解 <span class="df-muted">/亿元</span></h3><div class="df-result-line"><span>预测期现金流现值</span><b data-pv>—</b></div><div class="df-result-line"><span>永续终值现值</span><b data-tv>—</b></div><div class="df-result-line df-sum"><span data-value-label>企业经营价值</span><b data-ev>—</b></div><div class="df-result-line" data-adjustment><span>− 净债务 + 非经营资产 − 少数股东权益</span><b data-bridge>—</b></div><div class="df-result-line df-sum"><span>股权价值</span><b data-equity>—</b></div><div class="df-composition" aria-hidden="true"><span class="df-segment-a" data-composition-forecast></span><span class="df-segment-b" data-composition-terminal></span></div><div class="df-legend"><span>预测期现金流</span><span>永续终值</span></div><div class="df-muted" data-terminal-share></div></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="df-analysis" data-section="crosscheck" tabindex="-1" data-crosscheck-slot></div>
|
|
||||||
</div>
|
|
||||||
<div class="df-error" role="alert" data-error hidden></div>
|
|
||||||
<div class="df-warning" data-warning hidden></div>
|
|
||||||
|
|
||||||
<details class="df-table-section" data-detail data-section="detail" tabindex="-1"><summary class="cursor-interaction">逐年现金流与折现明细 <span class="df-muted" data-table-summary></span></summary><div class="df-table-wrap"><table aria-label="逐年现金流折现"><thead><tr><th>年份/阶段</th><th>增长率</th><th>自由现金流(亿元)</th><th>折现因子</th><th>现值(亿元)</th></tr></thead><tbody data-rows></tbody></table></div></details>
|
|
||||||
<section class="df-cashflow-section" data-cashflow-section aria-label="逐年现金流与现值图表" hidden>
|
|
||||||
<div class="df-section-head"><div><h3>逐年现金流与折现现值</h3><p class="df-muted" data-cashflow-caption></p></div><span class="df-muted">单位:亿元</span></div>
|
|
||||||
<div class="df-cashflow-chart" data-cashflow-chart role="img" aria-label="逐年自由现金流与折现现值对比"></div>
|
|
||||||
<div class="df-error" data-cashflow-error role="alert" hidden></div>
|
|
||||||
<p class="df-muted">浅棕为当年自由现金流,深棕为折算到基期的现值;仅展示显式预测期,不含永续终值。悬停或轻触柱形可查看年份、增长阶段及折现因子。</p>
|
|
||||||
</section>
|
|
||||||
<footer class="df-foot"><span>假设示例 · 年末现金流 · 永续增长法</span><span data-model-note></span></footer>
|
|
||||||
<p class="df-muted df-note">净利润不能直接代替自由现金流。FCFF 按 WACC 折现后调整为股权价值,FCFE 按股权资本成本折现。非经营资产不得与净债务中的超额现金重复计入。所有金额使用人民币,股本在预测期内保持不变,终值采用持续经营假设。</p>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+37
-69
@@ -3,22 +3,16 @@ import '../../css/dcf.css';
|
|||||||
import { calculateDCF, calculateDCFPriceRange, classifyDCFPrice } from './dcf-math.js';
|
import { calculateDCF, calculateDCFPriceRange, classifyDCFPrice } from './dcf-math.js';
|
||||||
import { buildDCFPriceOption } from './dcf-price-chart.js';
|
import { buildDCFPriceOption } from './dcf-price-chart.js';
|
||||||
import { buildDCFCashflowOption } from './dcf-cashflow-chart.js';
|
import { buildDCFCashflowOption } from './dcf-cashflow-chart.js';
|
||||||
import crosscheckTemplate from './dcf-crosscheck.html?raw';
|
|
||||||
import { createDCFCrosscheck } from './dcf-crosscheck.js';
|
|
||||||
import { createDCFHelp } from './dcf-help.js';
|
import { createDCFHelp } from './dcf-help.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'smart-fin-chart:dcf:v1';
|
const STORAGE_KEY = 'smart-fin-chart:dcf:v1';
|
||||||
let memory;
|
let memory;
|
||||||
const defaults = (example = false) => ({basis: 'fcff', fcf: '', year: '2026', shares: '', shareDate: '',
|
const defaults = () => ({basis: 'fcff', fcf: '', year: '2026', shares: '', shareDate: '',
|
||||||
rate: '10', terminal: '2', debt: '0', assets: '0', minority: '0', margin: '20',
|
rate: '10', terminal: '2', debt: '0', assets: '0', minority: '0', margin: '20',
|
||||||
premium: '20', marketPrice: '', quoteDate: '',
|
premium: '20', marketPrice: '', quoteDate: ''});
|
||||||
crossProfitMode: 'manual', crossProfit: example ? '125' : '', crossConversion: example ? '80' : '',
|
|
||||||
crossProfitGrowth: example ? '8' : '', crossTarget: '13', crossYears: '5',
|
|
||||||
crossPeLow: '9', crossPe: '12', crossPeHigh: '15',
|
|
||||||
crossDividend: '0', crossDividendGrowth: '0', crossDilution: '0'});
|
|
||||||
const initialStages = () => [{years: '3', growth: '12'}, {years: '4', growth: '8'}, {years: '3', growth: '4'}];
|
const initialStages = () => [{years: '3', growth: '12'}, {years: '4', growth: '8'}, {years: '3', growth: '4'}];
|
||||||
const profile = (name, example = false) => ({name,
|
const profile = (name, example = false) => ({name,
|
||||||
inputs: {...defaults(example), ...(example ? {fcf: '100', shares: '10', debt: '100', marketPrice: '138'} : {})}, stages: initialStages(), detailOpen: false});
|
inputs: {...defaults(), ...(example ? {fcf: '100', shares: '10', debt: '100', marketPrice: '138'} : {})}, stages: initialStages(), detailOpen: false});
|
||||||
const fmt = value => new Intl.NumberFormat('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
|
const fmt = value => new Intl.NumberFormat('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
|
||||||
|
|
||||||
function readStore() {
|
function readStore() {
|
||||||
@@ -40,32 +34,7 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
const root = container.querySelector('.dcf-calculator');
|
const root = container.querySelector('.dcf-calculator');
|
||||||
const disposeHelp = createDCFHelp(root);
|
const disposeHelp = createDCFHelp(root);
|
||||||
const q = name => root.querySelector(`[data-${name}]`);
|
const q = name => root.querySelector(`[data-${name}]`);
|
||||||
q('crosscheck-slot').innerHTML = crosscheckTemplate;
|
|
||||||
const fields = Object.fromEntries([...root.querySelectorAll('[data-field]')].map(field => [field.dataset.field, field]));
|
const fields = Object.fromEntries([...root.querySelectorAll('[data-field]')].map(field => [field.dataset.field, field]));
|
||||||
const crosscheck = createDCFCrosscheck(q('crosscheck-slot').querySelector('.df-crosscheck'), fields, (data) => {
|
|
||||||
if (!data) {
|
|
||||||
q('overview-ceiling').textContent = '—';
|
|
||||||
q('overview-verdict').textContent = '填写有效的股价及利润假设,比较现金流价值与目标收益门槛。';
|
|
||||||
q('overview-verdict').closest('.df-decision').removeAttribute('data-ok');
|
|
||||||
q('top-irr').textContent = '—';
|
|
||||||
q('top-growth').textContent = '—';
|
|
||||||
q('top-irr-detail').textContent = '补全交叉验证参数后显示';
|
|
||||||
q('top-growth-detail').textContent = '按现价与基准退出 PE 反推';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {result, assumptions} = data;
|
|
||||||
const current = result.rows[0].base;
|
|
||||||
q('overview-ceiling').textContent = fmt(result.jointCeiling);
|
|
||||||
const decision = q('overview-verdict').closest('.df-decision');
|
|
||||||
decision.dataset.ok = String(assumptions.price <= result.jointCeiling + 1e-10);
|
|
||||||
q('overview-verdict').textContent = assumptions.price <= result.jointCeiling + 1e-10
|
|
||||||
? '现价低于共同参考上限,两套价格上限均留有设定的安全边际。'
|
|
||||||
: '现价高于共同参考上限,需复核现金流、利润增长与退出 PE 假设。';
|
|
||||||
q('top-irr').textContent = `${fmt(current.irr * 100)}%`;
|
|
||||||
q('top-growth').textContent = current.requiredGrowth === null ? '无需最低增速' : `${fmt(current.requiredGrowth * 100)}%`;
|
|
||||||
q('top-irr-detail').textContent = `${current.meetsTarget ? '达到' : '未达'}目标 ${fmt(assumptions.target * 100)}% · 持有 ${assumptions.years} 年`;
|
|
||||||
q('top-growth-detail').textContent = `含安全边际 ${fmt(Number(fields.margin.value))}% · 无边际 ${current.requiredGrowthWithoutMargin === null ? '无需最低增速' : fmt(current.requiredGrowthWithoutMargin * 100) + '%'} · 退出 PE ${fmt(assumptions.pe)} 倍`;
|
|
||||||
});
|
|
||||||
const store = readStore();
|
const store = readStore();
|
||||||
store.deletedProfileIds = Array.isArray(store.deletedProfileIds) ? store.deletedProfileIds.filter(id => typeof id === 'string') : [];
|
store.deletedProfileIds = Array.isArray(store.deletedProfileIds) ? store.deletedProfileIds.filter(id => typeof id === 'string') : [];
|
||||||
const addPreset = (id, name, example = false) => {
|
const addPreset = (id, name, example = false) => {
|
||||||
@@ -82,15 +51,14 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
let cashflowRows;
|
let cashflowRows;
|
||||||
let resizeFrame;
|
let resizeFrame;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let lastDCFInput;
|
|
||||||
memory = store;
|
memory = store;
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
if (active) {
|
if (active) {
|
||||||
active.inputs = Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.value]));
|
// Retain legacy saved fields without using them in this DCF-only interface.
|
||||||
active.stages = stages.map(stage => ({...stage}));
|
active.inputs = {...active.inputs, ...Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.value]))};
|
||||||
active.detailOpen = q('detail').open;
|
active.stages = stages.map(stage => ({...stage}));
|
||||||
active.crossAdvancedOpen = q('cross-advanced').open;
|
active.detailOpen = q('detail').open;
|
||||||
}
|
}
|
||||||
memory = store;
|
memory = store;
|
||||||
try {
|
try {
|
||||||
@@ -102,7 +70,7 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteCompany(item) {
|
function deleteCompany(item) {
|
||||||
if (!window.confirm(`删除「${item.name}」?\n该公司保存在本机的 DCF 与收益校验参数将一并删除,无法撤销。`)) return;
|
if (!window.confirm(`删除「${item.name}」?\n该公司保存在本机的估值参数将一并删除,无法撤销。`)) return;
|
||||||
save();
|
save();
|
||||||
const index = store.profiles.findIndex(profile => profile.id === item.id);
|
const index = store.profiles.findIndex(profile => profile.id === item.id);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
@@ -191,7 +159,9 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('market-error').hidden = true;
|
q('market-error').hidden = true;
|
||||||
const price = Number(fields.marketPrice.value);
|
const price = Number(fields.marketPrice.value);
|
||||||
q('market-current').textContent = fields.marketPrice.value.trim() && Number.isFinite(price) && price > 0 ? fmt(price) : '—';
|
q('market-current').textContent = fields.marketPrice.value.trim() && Number.isFinite(price) && price > 0 ? fmt(price) : '—';
|
||||||
q('deviation').textContent = '待补全有效的估值参数';
|
q('deviation').textContent = '—';
|
||||||
|
delete q('deviation').dataset.priceRelation;
|
||||||
|
q('price-position').textContent = '待补全有效的估值参数';
|
||||||
q('market-message').textContent = message;
|
q('market-message').textContent = message;
|
||||||
q('market-message').hidden = false;
|
q('market-message').hidden = false;
|
||||||
}
|
}
|
||||||
@@ -210,7 +180,7 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('zone-high').textContent = `> ${fmt(range.high)} 元`;
|
q('zone-high').textContent = `> ${fmt(range.high)} 元`;
|
||||||
q('zone-keys').hidden = false;
|
q('zone-keys').hidden = false;
|
||||||
q('market-message').hidden = true;
|
q('market-message').hidden = true;
|
||||||
q('deviation').textContent = '填写当前股价后查看所处区间';
|
q('price-position').textContent = '填写当前股价后查看所处区间';
|
||||||
let price;
|
let price;
|
||||||
if (fields.marketPrice.value.trim()) {
|
if (fields.marketPrice.value.trim()) {
|
||||||
const entered = Number(fields.marketPrice.value);
|
const entered = Number(fields.marketPrice.value);
|
||||||
@@ -221,7 +191,9 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('market-badge').textContent = `${position.zone}区间`;
|
q('market-badge').textContent = `${position.zone}区间`;
|
||||||
q('market-badge').dataset.zone = position.zone;
|
q('market-badge').dataset.zone = position.zone;
|
||||||
q('market-badge').hidden = false;
|
q('market-badge').hidden = false;
|
||||||
q('deviation').textContent = `相对 DCF 每股价值${position.deviation <= 0 ? '折价' : '溢价'} ${fmt(Math.abs(position.deviation) * 100)}%`;
|
q('deviation').textContent = `${position.deviation <= 0 ? '折价' : '溢价'} ${fmt(Math.abs(position.deviation) * 100)}%`;
|
||||||
|
q('deviation').dataset.priceRelation = position.deviation <= 0 ? 'discount' : 'premium';
|
||||||
|
q('price-position').textContent = `处于${position.zone}区间`;
|
||||||
q('market-message').hidden = true;
|
q('market-message').hidden = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
q('market-error').textContent = error.message;
|
q('market-error').textContent = error.message;
|
||||||
@@ -311,6 +283,8 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('rate-label').textContent = fcfe ? '股权资本成本(%)' : '折现率 WACC(%)';
|
q('rate-label').textContent = fcfe ? '股权资本成本(%)' : '折现率 WACC(%)';
|
||||||
for (const name of ['debt-field', 'assets-field', 'minority-field', 'adjustment']) q(name).hidden = fcfe;
|
for (const name of ['debt-field', 'assets-field', 'minority-field', 'adjustment']) q(name).hidden = fcfe;
|
||||||
q('value-label').textContent = fcfe ? '现金流股权价值' : '企业经营价值';
|
q('value-label').textContent = fcfe ? '现金流股权价值' : '企业经营价值';
|
||||||
|
q('basis-caption').textContent = `${fcfe ? 'FCFE' : 'FCFF'} 口径`;
|
||||||
|
q('basis-note').textContent = fcfe ? '直接折现股东现金流,不再重复扣减净债务。' : '先折现企业现金流,再调整为股权价值。';
|
||||||
q('model-note').textContent = fcfe ? 'FCFE 按股权资本成本折现,不再扣减净债务' : 'FCFF 按 WACC 折现,再调整为股权价值';
|
q('model-note').textContent = fcfe ? 'FCFE 按股权资本成本折现,不再扣减净债务' : 'FCFF 按 WACC 折现,再调整为股权价值';
|
||||||
let first = 1;
|
let first = 1;
|
||||||
stages.forEach((stage, index) => {
|
stages.forEach((stage, index) => {
|
||||||
@@ -326,13 +300,17 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
const r = calculateDCF(p);
|
const r = calculateDCF(p);
|
||||||
q('duration').textContent = `合计 ${r.years} 年 · ${p.year + 1}—${p.year + r.years} 年,各阶段逐年复利衔接`;
|
q('duration').textContent = `合计 ${r.years} 年 · ${p.year + 1}—${p.year + r.years} 年,各阶段逐年复利衔接`;
|
||||||
q('table-summary').textContent = ` · ${r.years} 年`;
|
q('table-summary').textContent = ` · ${r.years} 年`;
|
||||||
const values = {pv: r.forecastPV, tv: r.terminalPV, ev: r.operatingValue, equity: r.equityValue, bridge: r.adjustment};
|
const values = {pv: r.forecastPV, tv: r.terminalPV, ev: r.operatingValue, equity: r.equityValue,
|
||||||
|
'debt-value': p.debt, 'assets-value': p.assets, 'minority-value': p.minority};
|
||||||
for (const [name, value] of Object.entries(values)) q(name).textContent = fmt(value);
|
for (const [name, value] of Object.entries(values)) q(name).textContent = fmt(value);
|
||||||
q('price').textContent = r.equityValue > 0 ? fmt(r.perShare) : '无正股权价值';
|
q('price').textContent = r.equityValue > 0 ? fmt(r.perShare) : '无正股权价值';
|
||||||
|
q('value-per-share').textContent = r.equityValue > 0 ? fmt(r.perShare) : '—';
|
||||||
q('buy').textContent = r.equityValue > 0 ? fmt(r.entryPrice) : '—';
|
q('buy').textContent = r.equityValue > 0 ? fmt(r.entryPrice) : '—';
|
||||||
q('margin-label').textContent = fmt(p.margin * 100) + '%';
|
q('discount-note').textContent = `内在价值打 ${fmt((1 - p.margin) * 10)} 折`;
|
||||||
q('shares-note').textContent = `按 ${fmt(p.shares)} 亿股换算${fields.shareDate.value ? ` · 股本日期 ${fields.shareDate.value}` : ''}`;
|
q('shares-note').textContent = `按 ${fmt(p.shares)} 亿股换算${fields.shareDate.value ? ` · 股本日期 ${fields.shareDate.value}` : ''}`;
|
||||||
q('terminal-share').textContent = `永续终值占比 ${fmt(r.terminalWeight * 100)}%`;
|
q('forecast-share').textContent = `${fmt((1 - r.terminalWeight) * 100)}%`;
|
||||||
|
q('terminal-share').textContent = `${fmt(r.terminalWeight * 100)}%`;
|
||||||
|
q('terminal-note').textContent = `永续终值占经营现金流现值的 ${fmt(r.terminalWeight * 100)}%,结果对折现率和永续增长假设敏感。`;
|
||||||
q('rows').replaceChildren(...r.rows.map(row => {
|
q('rows').replaceChildren(...r.rows.map(row => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
for (const value of [`${row.year} · 阶段 ${row.stage}`, `${fmt(row.growth * 100)}%`, fmt(row.cash), row.factor.toFixed(4), fmt(row.presentValue)]) {
|
for (const value of [`${row.year} · 阶段 ${row.stage}`, `${fmt(row.growth * 100)}%`, fmt(row.cash), row.factor.toFixed(4), fmt(row.presentValue)]) {
|
||||||
@@ -352,13 +330,9 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('cashflow-caption').textContent = `${p.year + 1}—${p.year + r.years} 年 · ${r.years} 年预测期 · 现值合计 ${fmt(r.forecastPV)} 亿元`;
|
q('cashflow-caption').textContent = `${p.year + 1}—${p.year + r.years} 年 · ${r.years} 年预测期 · 现值合计 ${fmt(r.forecastPV)} 亿元`;
|
||||||
q('cashflow-chart').setAttribute('aria-label', `${r.years} 年自由现金流与折现现值对比,${r.rows.map(row => `${row.year} 年现金流 ${fmt(row.cash)}、现值 ${fmt(row.presentValue)} 亿元`).join(';')}`);
|
q('cashflow-chart').setAttribute('aria-label', `${r.years} 年自由现金流与折现现值对比,${r.rows.map(row => `${row.year} 年现金流 ${fmt(row.cash)}、现值 ${fmt(row.presentValue)} 亿元`).join(';')}`);
|
||||||
drawCashflowChart();
|
drawCashflowChart();
|
||||||
root.querySelector('.df-output').hidden = false;
|
|
||||||
root.querySelector('.df-results').hidden = false;
|
|
||||||
q('composition-forecast').style.width = `${Math.max(0, Math.min(100, (1 - r.terminalWeight) * 100))}%`;
|
q('composition-forecast').style.width = `${Math.max(0, Math.min(100, (1 - r.terminalWeight) * 100))}%`;
|
||||||
q('composition-terminal').style.width = `${Math.max(0, Math.min(100, r.terminalWeight * 100))}%`;
|
q('composition-terminal').style.width = `${Math.max(0, Math.min(100, r.terminalWeight * 100))}%`;
|
||||||
updateMarket(p, r);
|
updateMarket(p, r);
|
||||||
lastDCFInput = p;
|
|
||||||
crosscheck.update(p);
|
|
||||||
root.dataset.status = 'ready';
|
root.dataset.status = 'ready';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
q('error').textContent = error.message;
|
q('error').textContent = error.message;
|
||||||
@@ -367,12 +341,14 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
cashflowRows = undefined;
|
cashflowRows = undefined;
|
||||||
cashflowChart?.clear();
|
cashflowChart?.clear();
|
||||||
q('cashflow-section').hidden = true;
|
q('cashflow-section').hidden = true;
|
||||||
root.querySelector('.df-output').hidden = true;
|
for (const name of ['price', 'buy', 'value-per-share', 'pv', 'tv', 'ev', 'equity', 'debt-value', 'assets-value', 'minority-value', 'forecast-share', 'terminal-share']) q(name).textContent = '—';
|
||||||
root.querySelector('.df-results').hidden = true;
|
q('shares-note').textContent = '补全总股本后换算每股价值';
|
||||||
|
q('discount-note').textContent = '按设定的安全边际折价';
|
||||||
|
q('terminal-note').textContent = '补全有效参数后查看价值构成';
|
||||||
|
q('composition-forecast').style.width = '0%';
|
||||||
|
q('composition-terminal').style.width = '0%';
|
||||||
q('duration').textContent = '请补全有效参数后查看结果';
|
q('duration').textContent = '请补全有效参数后查看结果';
|
||||||
clearMarket('请补全有效的现金流、股本与折现参数后查看估值区间');
|
clearMarket('请补全有效的现金流、股本与折现参数后查看估值区间');
|
||||||
lastDCFInput = undefined;
|
|
||||||
crosscheck.update(undefined);
|
|
||||||
root.dataset.status = 'invalid';
|
root.dataset.status = 'invalid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +358,7 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('company-empty').hidden = Boolean(active);
|
q('company-empty').hidden = Boolean(active);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
stages = [];
|
stages = [];
|
||||||
lastDCFInput = chartData = cashflowRows = undefined;
|
chartData = cashflowRows = undefined;
|
||||||
chart?.clear();
|
chart?.clear();
|
||||||
cashflowChart?.clear();
|
cashflowChart?.clear();
|
||||||
root.dataset.status = 'empty';
|
root.dataset.status = 'empty';
|
||||||
@@ -391,11 +367,10 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
renderCompanies();
|
renderCompanies();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const values = {...defaults(active.id === 'example'), ...active.inputs};
|
const values = {...defaults(), ...active.inputs};
|
||||||
for (const [key, field] of Object.entries(fields)) field.value = values[key];
|
for (const [key, field] of Object.entries(fields)) field.value = values[key];
|
||||||
stages = active.stages.map(stage => ({years: String(stage.years ?? ''), growth: String(stage.growth ?? '')}));
|
stages = active.stages.map(stage => ({years: String(stage.years ?? ''), growth: String(stage.growth ?? '')}));
|
||||||
q('detail').open = active.detailOpen !== false;
|
q('detail').open = active.detailOpen !== false;
|
||||||
q('cross-advanced').open = Boolean(active.crossAdvancedOpen);
|
|
||||||
q('context').textContent = active.id === 'example' ? '教学示例 · 非实时数据 · 人民币' : `${active.name} · 用户手动输入 · 人民币`;
|
q('context').textContent = active.id === 'example' ? '教学示例 · 非实时数据 · 人民币' : `${active.name} · 用户手动输入 · 人民币`;
|
||||||
renderCompanies();
|
renderCompanies();
|
||||||
renderStages();
|
renderStages();
|
||||||
@@ -407,13 +382,12 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
stages.push({years: '3', growth: '3'});
|
stages.push({years: '3', growth: '3'});
|
||||||
renderStages(); update(); save();
|
renderStages(); update(); save();
|
||||||
});
|
});
|
||||||
for (const [name, field] of Object.entries(fields)) field.addEventListener('input', () => {
|
for (const field of Object.values(fields)) {
|
||||||
if (name.startsWith('cross')) crosscheck.update(lastDCFInput);
|
const onChange = () => { update(); save(); };
|
||||||
else update();
|
field.addEventListener('input', onChange);
|
||||||
save();
|
if (field.tagName === 'SELECT') field.addEventListener('change', onChange);
|
||||||
});
|
}
|
||||||
q('detail').addEventListener('toggle', () => { if (root.isConnected) save(); });
|
q('detail').addEventListener('toggle', () => { if (root.isConnected) save(); });
|
||||||
q('cross-advanced').addEventListener('toggle', () => { if (root.isConnected) save(); });
|
|
||||||
q('new-company').addEventListener('click', () => {
|
q('new-company').addEventListener('click', () => {
|
||||||
q('company-form').hidden = false;
|
q('company-form').hidden = false;
|
||||||
q('company-error').textContent = '';
|
q('company-error').textContent = '';
|
||||||
@@ -433,12 +407,6 @@ export function renderDCFCalculator(container, companies = []) {
|
|||||||
q('company-name').value = '';
|
q('company-name').value = '';
|
||||||
applyProfile(); save();
|
applyProfile(); save();
|
||||||
});
|
});
|
||||||
root.querySelectorAll('[data-jump]').forEach(button => button.addEventListener('click', () => {
|
|
||||||
const target = root.querySelector(`[data-section="${button.dataset.jump}"]`);
|
|
||||||
if (target === q('detail')) target.open = true;
|
|
||||||
target.scrollIntoView({behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth', block: 'start'});
|
|
||||||
target.focus({preventScroll: true});
|
|
||||||
}));
|
|
||||||
applyProfile();
|
applyProfile();
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
cancelAnimationFrame(resizeFrame);
|
cancelAnimationFrame(resizeFrame);
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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};
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<section class="df-crosscheck" aria-label="目标收益交叉验证">
|
|
||||||
<div class="df-column-heading"><span>02</span><div><h3>目标收益交叉验证</h3><p>以当前价格买入,需要怎样的盈利增长</p></div><b>IRR</b></div>
|
|
||||||
<div class="df-cross-shared df-muted" data-cross-shared>共用当前公司的股价、股本、基期及 DCF 参考价</div>
|
|
||||||
<details class="df-cross-inputs" open><summary>利润与收益假设 <span class="df-muted">独立于 DCF 现金流增速</span></summary><div class="df-cross-grid">
|
|
||||||
<div>
|
|
||||||
<label>基期归母净利润来源
|
|
||||||
<select data-field="crossProfitMode"><option value="manual">手动填写归母净利润</option><option value="estimate">共用 FCFE + 现金转化率估算</option></select>
|
|
||||||
</label>
|
|
||||||
<div class="df-cross-bridge" data-cross-bridge hidden>
|
|
||||||
<div class="df-muted">已关联基础参数:<span data-cross-fcf>—</span> 亿元 FCFE</div>
|
|
||||||
<label>基期现金转化率(FCFE ÷ 归母净利润,%)<input data-field="crossConversion" type="number" min="0" step="any" placeholder="填写基期转化率假设"></label>
|
|
||||||
<div class="df-cross-derived"><span>基期归母净利润(估算)</span><b data-cross-derived>—</b></div>
|
|
||||||
<div class="df-muted">基期转化率为用户假设;100% 表示假设基期 FCFE 等于归母利润。</div>
|
|
||||||
</div>
|
|
||||||
<label class="df-cross-profit" data-cross-manual>基期归母净利润(亿元)<input data-field="crossProfit" type="number" min="0" step="any" placeholder="填写归母净利润"></label>
|
|
||||||
<div class="df-muted df-cross-source-note" data-cross-source-note></div>
|
|
||||||
<label class="df-cross-growth">预计归母净利润年增长率(%)<input data-field="crossProfitGrowth" type="number" min="-99.99" max="1000" step="any" placeholder="独立填写利润增速假设"></label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="df-fields">
|
|
||||||
<label>目标年化收益率(%)<input data-field="crossTarget" type="number" min="-99.99" max="1000" step="any"></label>
|
|
||||||
<label>持有年限<input data-field="crossYears" type="number" min="1" max="30" step="1"></label>
|
|
||||||
</div>
|
|
||||||
<div class="df-cross-pe">
|
|
||||||
<label>悲观退出 PE(倍)<input data-field="crossPeLow" type="number" min="0" step="any"></label>
|
|
||||||
<label>基准退出 PE(倍)<input data-field="crossPe" type="number" min="0" step="any"></label>
|
|
||||||
<label>乐观退出 PE(倍)<input data-field="crossPeHigh" type="number" min="0" step="any"></label>
|
|
||||||
</div>
|
|
||||||
<div class="df-muted df-cross-pe-note">退出 PE 为情景假设,不是未来市场定价承诺。目标收益率不会改写 DCF 折现率。</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<details class="df-cross-advanced" data-cross-advanced>
|
|
||||||
<summary>分红与股本变化 <span class="df-muted" data-cross-advanced-caption></span></summary>
|
|
||||||
<div class="df-fields">
|
|
||||||
<label>首年每股现金股息(元)<input data-field="crossDividend" type="number" min="0" step="any"></label>
|
|
||||||
<label>每股股息年增长率(%)<input data-field="crossDividendGrowth" type="number" min="-99.99" max="1000" step="any"></label>
|
|
||||||
<label>股本年变化率(%)<input data-field="crossDilution" type="number" min="-99.99" max="1000" step="any"></label>
|
|
||||||
</div>
|
|
||||||
<div class="df-muted df-cross-pe-note">每年末收到股息,末年同时收到股息与退出款;股息路径独立于利润,不计再投资与税费。DCF 按固定股本计算,本区股本变化仅用于收益验证。</div>
|
|
||||||
</details>
|
|
||||||
</details>
|
|
||||||
<div class="df-error" data-cross-error role="alert" hidden></div>
|
|
||||||
<div class="df-cross-results" data-cross-results aria-live="polite" hidden>
|
|
||||||
<section class="df-cross-joint" aria-label="共同参考买入上限">
|
|
||||||
<div class="df-cross-joint-grid">
|
|
||||||
<div><span>DCF 参考价</span><b data-cross-joint-dcf></b></div>
|
|
||||||
<div><span>目标收益参考价(含边际)</span><b data-cross-joint-return></b><small class="df-muted" data-cross-return-base></small></div>
|
|
||||||
<div><span>共同参考买入上限</span><b data-cross-joint-price></b></div>
|
|
||||||
</div>
|
|
||||||
<div class="df-muted" data-cross-joint-note></div>
|
|
||||||
</section>
|
|
||||||
<section class="df-scenarios" aria-label="现价买入的三种退出情景"><div class="df-section-head"><h3>三种退出情景</h3><span class="df-muted">现价买入 · 预计年化 IRR</span></div><div class="df-scenario-grid" data-cross-cards></div></section>
|
|
||||||
<section class="df-cross-sensitivity" aria-label="退出 PE 情景验证">
|
|
||||||
<div class="df-section-head"><h3>退出 PE 情景验证</h3><span class="df-muted">表中为预计年化 IRR</span></div>
|
|
||||||
<div class="df-table-wrap"><table aria-label="退出 PE 敏感性"><thead><tr><th>买入情景</th><th data-cross-head-low></th><th data-cross-head-base></th><th data-cross-head-high></th></tr></thead><tbody data-cross-scenario-rows></tbody></table></div>
|
|
||||||
</section>
|
|
||||||
<details class="df-cross-prices"><summary>展开双价格验证与条件拆解</summary>
|
|
||||||
<div class="df-section-head"><h3>两种买入价格,分别验证</h3><span class="df-muted" data-cross-caption></span></div>
|
|
||||||
<div class="df-table-wrap"><table aria-label="双买入价格交叉验证"><thead><tr><th>买入情景</th><th>买入价(元)</th><th>所需利润年增速</th><th>预计年化 IRR</th><th>目标回报条件</th></tr></thead><tbody data-cross-price-rows></tbody></table></div>
|
|
||||||
<div class="df-muted df-cross-table-note" data-cross-growth-note></div>
|
|
||||||
<div class="df-cross-checks">
|
|
||||||
<div><span class="df-muted">现金流估值条件 · 按现价</span><strong data-cross-dcf-condition></strong><p class="df-muted" data-cross-dcf-gap></p></div>
|
|
||||||
<div><span class="df-muted">盈利与安全边际门槛 · 基准 PE · 按现价</span><strong data-cross-growth-condition></strong><p class="df-muted" data-cross-growth-gap></p></div>
|
|
||||||
</div>
|
|
||||||
<div class="df-cross-verdict" data-cross-verdict></div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { calculateDCFCrosscheck } from './dcf-crosscheck-math.js';
|
|
||||||
|
|
||||||
const fmt = value => new Intl.NumberFormat('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
|
|
||||||
const pct = value => `${fmt(value * 100)}%`;
|
|
||||||
const cell = (text, ok) => {
|
|
||||||
const td = document.createElement('td');
|
|
||||||
if (ok === undefined) td.textContent = text;
|
|
||||||
else {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.className = 'df-cross-tag';
|
|
||||||
span.dataset.ok = String(ok);
|
|
||||||
span.textContent = text;
|
|
||||||
td.append(span);
|
|
||||||
}
|
|
||||||
return td;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createDCFCrosscheck(root, fields, onResult = () => {}) {
|
|
||||||
const q = name => root.querySelector(`[data-cross-${name}]`);
|
|
||||||
const number = (name, label) => {
|
|
||||||
if (!fields[name].value.trim()) throw new Error(`请填写${label}`);
|
|
||||||
const value = Number(fields[name].value);
|
|
||||||
if (!Number.isFinite(value)) throw new Error(`${label}须为有效数字`);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
function reset(message) {
|
|
||||||
q('results').hidden = true;
|
|
||||||
q('error').textContent = message;
|
|
||||||
q('error').hidden = false;
|
|
||||||
q('derived').textContent = '—';
|
|
||||||
root.dataset.status = 'invalid';
|
|
||||||
onResult(undefined);
|
|
||||||
}
|
|
||||||
function update(dcfInput) {
|
|
||||||
const fcff = fields.basis.value !== 'fcfe';
|
|
||||||
fields.crossProfitMode.querySelector('[value="estimate"]').disabled = fcff;
|
|
||||||
if (fcff && fields.crossProfitMode.value === 'estimate') fields.crossProfitMode.value = 'manual';
|
|
||||||
const estimated = fields.crossProfitMode.value === 'estimate';
|
|
||||||
q('bridge').hidden = !estimated;
|
|
||||||
q('manual').hidden = estimated;
|
|
||||||
q('source-note').textContent = fcff ? 'FCFF 包含债权人现金流,归母净利润需独立输入。'
|
|
||||||
: estimated ? '采用基期转化率假设估算利润,非独立财报验证。' : '归母净利润独立输入,不随基期现金流自动变动。';
|
|
||||||
q('fcf').textContent = dcfInput ? fmt(dcfInput.fcf) : '—';
|
|
||||||
if (!dcfInput) {
|
|
||||||
q('shared').textContent = '共用当前公司的股价、股本和基期,请先补全有效的 DCF 参数。';
|
|
||||||
reset('完成 DCF 参数后,可继续填写利润与目标收益假设进行验证。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
q('shared').textContent = `已共用当前公司 · ${dcfInput.year} 年基期 · ${fmt(dcfInput.shares)} 亿股 · 人民币口径`;
|
|
||||||
try {
|
|
||||||
const assumptions = {
|
|
||||||
price: number('marketPrice', '当前股价(上方股价区)'),
|
|
||||||
profitMode: fields.crossProfitMode.value,
|
|
||||||
profit: estimated ? undefined : number('crossProfit', '基期归母净利润'),
|
|
||||||
conversion: estimated ? number('crossConversion', '基期现金转化率') / 100 : undefined,
|
|
||||||
profitGrowth: number('crossProfitGrowth', '预计利润年增长率') / 100,
|
|
||||||
target: number('crossTarget', '目标年化收益率') / 100,
|
|
||||||
years: number('crossYears', '持有年限'),
|
|
||||||
peLow: number('crossPeLow', '悲观退出 PE'), pe: number('crossPe', '基准退出 PE'), peHigh: number('crossPeHigh', '乐观退出 PE'),
|
|
||||||
dividend: number('crossDividend', '首年股息'),
|
|
||||||
dividendGrowth: number('crossDividendGrowth', '股息增长率') / 100,
|
|
||||||
dilution: number('crossDilution', '股本年变化率') / 100,
|
|
||||||
};
|
|
||||||
const result = calculateDCFCrosscheck(dcfInput, assumptions);
|
|
||||||
const current = result.rows[0].base;
|
|
||||||
const growthText = growth => growth === null ? '无需最低增速' : pct(growth);
|
|
||||||
q('derived').textContent = `${fmt(result.profit)} 亿元`;
|
|
||||||
q('caption').textContent = `基准退出 PE ${fmt(assumptions.pe)} 倍 · 持有 ${assumptions.years} 年`;
|
|
||||||
q('advanced-caption').textContent = ` · 首年股息 ${fmt(assumptions.dividend)} 元 · 股本年变化 ${pct(assumptions.dilution)}`;
|
|
||||||
q('price-rows').replaceChildren(...result.rows.map(row => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.append(cell(row.name), cell(fmt(row.price)), cell(`${growthText(row.base.requiredGrowth)}(含边际);无边际 ${growthText(row.base.requiredGrowthWithoutMargin)}`),
|
|
||||||
cell(pct(row.base.irr)), cell(row.base.meetsTarget ? '达到目标' : '未达目标', row.base.meetsTarget));
|
|
||||||
return tr;
|
|
||||||
}));
|
|
||||||
q('dcf-condition').textContent = result.dcfCondition ? '现价满足 DCF 安全边际要求' : '现价未满足 DCF 安全边际要求';
|
|
||||||
q('dcf-gap').textContent = `相对 DCF 每股价值${result.dcfDiscount >= 0 ? '折价' : '溢价'} ${pct(Math.abs(result.dcfDiscount))}`;
|
|
||||||
q('growth-condition').textContent = current.requiredGrowth === null ? '股息现值已覆盖含边际要求'
|
|
||||||
: `预计利润年增 ${pct(assumptions.profitGrowth)},含边际门槛 ${pct(current.requiredGrowth)}`;
|
|
||||||
q('growth-gap').textContent = result.growthGap === null ? '当前分红假设下,无最低退出盈利要求。'
|
|
||||||
: `${result.growthGap >= 0 ? '高于' : '低于'}门槛 ${fmt(Math.abs(result.growthGap) * 100)} 个百分点`;
|
|
||||||
q('verdict').textContent = result.dcfCondition
|
|
||||||
? result.returnCondition ? '当前两套模型条件均满足;结果仍取决于现金流、利润和退出 PE 假设。'
|
|
||||||
: 'DCF 有安全边际,但当前利润增长与退出 PE 假设尚未同时满足目标回报和设定的安全边际。'
|
|
||||||
: result.returnCondition ? '收益情景满足目标回报和设定的安全边际,但现价尚未满足 DCF 安全边际要求,需复核两边假设。'
|
|
||||||
: '现价尚未同时满足 DCF 与目标收益模型的安全边际条件。';
|
|
||||||
q('joint-dcf').textContent = fmt(result.dcf.entryPrice);
|
|
||||||
q('joint-return').textContent = fmt(result.returnCeiling);
|
|
||||||
q('return-base').textContent = `折扣前 ${fmt(current.maxPrice)} 元/股`;
|
|
||||||
q('joint-price').textContent = fmt(result.jointCeiling);
|
|
||||||
q('joint-note').textContent = `两套参考价各应用一次 ${pct(dcfInput.margin)} 安全边际后取较低值;单位:元/股。`;
|
|
||||||
q('growth-note').textContent = `所需增速计入 ${pct(dcfInput.margin)} 安全边际,与“收益反推”的主结果口径相同,同时列示无边际对照。预计 IRR 按实际买入价、股息与退出款计算,不因安全边际设置而改变。`;
|
|
||||||
for (const [name, value] of [['low', assumptions.peLow], ['base', assumptions.pe], ['high', assumptions.peHigh]]) {
|
|
||||||
q(`head-${name}`).textContent = `${{low: '悲观', base: '基准', high: '乐观'}[name]} · ${fmt(value)} 倍`;
|
|
||||||
}
|
|
||||||
q('cards').replaceChildren(...currentScenarios());
|
|
||||||
function currentScenarios() {
|
|
||||||
return result.rows[0].scenarios.map((scenario, index) => {
|
|
||||||
const card = document.createElement('article');
|
|
||||||
card.className = 'df-scenario-card';
|
|
||||||
card.dataset.base = String(index === 1);
|
|
||||||
card.dataset.ok = String(scenario.meetsTarget);
|
|
||||||
const caption = document.createElement('span');
|
|
||||||
caption.textContent = `${['悲观', '基准', '乐观'][index]} · PE ${fmt(scenario.pe)}`;
|
|
||||||
const value = document.createElement('strong');
|
|
||||||
value.textContent = pct(scenario.irr);
|
|
||||||
const growth = document.createElement('small');
|
|
||||||
growth.textContent = `含边际所需年增 ${growthText(scenario.requiredGrowth)};无边际 ${growthText(scenario.requiredGrowthWithoutMargin)}`;
|
|
||||||
const status = document.createElement('span');
|
|
||||||
status.className = 'df-scenario-status';
|
|
||||||
status.textContent = scenario.meetsTarget ? '✓ 达到目标收益' : '未达目标收益';
|
|
||||||
card.append(caption, value, growth, status);
|
|
||||||
return card;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
q('scenario-rows').replaceChildren(...result.rows.map(row => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.append(cell(row.name), ...row.scenarios.map(scenario => cell(pct(scenario.irr))));
|
|
||||||
return tr;
|
|
||||||
}));
|
|
||||||
q('results').hidden = false;
|
|
||||||
q('error').hidden = true;
|
|
||||||
root.dataset.status = 'ready';
|
|
||||||
onResult({result, assumptions});
|
|
||||||
} catch (error) {
|
|
||||||
reset(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {update};
|
|
||||||
}
|
|
||||||
+16
-8
@@ -7,7 +7,8 @@
|
|||||||
<button type="button" data-help-jump="fcff">FCFF 企业现金流</button>
|
<button type="button" data-help-jump="fcff">FCFF 企业现金流</button>
|
||||||
<button type="button" data-help-jump="fcfe">FCFE 股东现金流</button>
|
<button type="button" data-help-jump="fcfe">FCFE 股东现金流</button>
|
||||||
<button type="button" data-help-jump="comparison">区别与选择</button>
|
<button type="button" data-help-jump="comparison">区别与选择</button>
|
||||||
<button type="button" data-help-jump="profit">与净利润的关系</button>
|
<button type="button" data-help-jump="discount">折现率与终值</button>
|
||||||
|
<button type="button" data-help-jump="margin">安全边际与区间</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="df-help-body">
|
<div class="df-help-body">
|
||||||
<p class="df-help-lead">FCFF 看企业能为<strong>股东和债权人整体</strong>创造多少现金;FCFE 看处理完债务融资后,能留给<strong>股东</strong>多少现金。</p>
|
<p class="df-help-lead">FCFF 看企业能为<strong>股东和债权人整体</strong>创造多少现金;FCFE 看处理完债务融资后,能留给<strong>股东</strong>多少现金。</p>
|
||||||
@@ -36,13 +37,20 @@
|
|||||||
<p>根据手上数据的实际定义选择。切换下拉选项不会自动把 FCFF 转换为 FCFE,也不会自动换算折现率;现金流金额、预测增长率与折现率都要使用匹配的假设。</p>
|
<p>根据手上数据的实际定义选择。切换下拉选项不会自动把 FCFF 转换为 FCFE,也不会自动换算折现率;现金流金额、预测增长率与折现率都要使用匹配的假设。</p>
|
||||||
<p>财报中的“经营活动现金流净额”还没有扣除资本开支,不能直接当作自由现金流。“经营现金流减资本开支”也需核对利息分类与借款处理,才能确定其口径。</p>
|
<p>财报中的“经营活动现金流净额”还没有扣除资本开支,不能直接当作自由现金流。“经营现金流减资本开支”也需核对利息分类与借款处理,才能确定其口径。</p>
|
||||||
</section>
|
</section>
|
||||||
<section data-help-section="profit" tabindex="-1">
|
<section data-help-section="discount" tabindex="-1">
|
||||||
<h3>为什么还要输入归母净利润?</h3>
|
<h3>折现率与永续终值</h3>
|
||||||
<p>DCF 使用自由现金流;目标收益交叉验证通过利润、股本和退出 PE 推算回报。利润和现金流会因折旧、资本开支、应收账款与存货占款等产生差异。</p>
|
<p>折现率将未来现金流换算为今天的价值。FCFF 对应 WACC;FCFE 对应股权资本成本。同样的现金流假设下,折现率越高,现值越低。</p>
|
||||||
<div class="df-help-formula">现金转化率 = FCFE ÷ 归母净利润<br>估算归母净利润 = FCFE ÷ 现金转化率</div>
|
<p>分段增长率用于显式预测期,合计最多 30 年。永续增长率用于预测期之后持续经营的现金流,须小于所用折现率。</p>
|
||||||
<p>例如 FCFE 为 80 亿元、现金转化率假设为 80%,估算利润为 100 亿元。该比例是用户假设;填 100% 表示假设两者相等。</p>
|
<div class="df-help-formula">预测期末终值 = 下一年自由现金流 ÷(折现率 − 永续增长率)</div>
|
||||||
<p><strong>本页的选择限制:</strong>“共用 FCFE + 现金转化率估算”仅在基础参数选择 FCFE 时启用。FCFF 模式需手动输入归母净利润。使用独立财报利润更有利于交叉验证;共用现金流估算的利润会依赖同一套假设。</p>
|
<p>终值还需折现回基期。“价值从哪里来?”展示预测期与永续终值的现值及占比;终值占比较高时,应更谨慎地核对折现率与永续增长率。</p>
|
||||||
<p><strong>与“收益反推”对照:</strong>目标收益参考价和所需利润增速均计入本页设置的安全边际。两页对照时,需同时保持买入价、利润、股本、持有年限、退出 PE、目标收益率、安全边际、分红与股本变化假设一致。预计 IRR 始终按实际买入价计算。</p>
|
<p>自由现金流会受到资本开支、营运资金及融资活动等影响,不能直接用归母净利润替代。</p>
|
||||||
|
</section>
|
||||||
|
<section data-help-section="margin" tabindex="-1">
|
||||||
|
<h3>安全边际与现价区间</h3>
|
||||||
|
<div class="df-help-formula">安全边际参考价 = DCF 每股内在价值 ×(1 − 安全边际)</div>
|
||||||
|
<p>例如 DCF 每股价值为 100 元,安全边际设为 20%,参考价为 80 元。页面的区间设置同时用该折价作为低估区间上界;默认高估区间从 DCF 每股价值的 120% 以上开始。</p>
|
||||||
|
<p>低估上界与高估下界均计入合理区间。现价相对内在价值的折溢价以 DCF 每股价值为基准计算,不以安全边际参考价为基准。</p>
|
||||||
|
<p>这些区间由当前现金流假设和用户设定的阈值决定,不自动获取行情,也不代表未来股价预测。</p>
|
||||||
</section>
|
</section>
|
||||||
<p class="df-help-source">参考:<a href="https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/free-cash-flow-valuation" target="_blank" rel="noopener noreferrer">CFA Institute · Free Cash Flow Valuation(英文)</a></p>
|
<p class="df-help-source">参考:<a href="https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/free-cash-flow-valuation" target="_blank" rel="noopener noreferrer">CFA Institute · Free Cash Flow Valuation(英文)</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "node test/regress.mjs && npm run test:valuation",
|
"test": "node test/regress.mjs && npm run test:valuation",
|
||||||
"test:valuation": "node test/stock_return_valuation.mjs && node test/dcf_valuation.mjs && node test/dcf_crosscheck.mjs",
|
"test:valuation": "node test/stock_return_valuation.mjs && node test/dcf_valuation.mjs",
|
||||||
"test:param": "node test/sweet_spot_param.mjs"
|
"test:param": "node test/sweet_spot_param.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import { calculateDCFCrosscheck } from '../js/tools/dcf-crosscheck-math.js';
|
|
||||||
import { calculate as calculateReturn } from '../js/tools/stock-return-math.js';
|
|
||||||
const dcf = {basis: 'fcfe', fcf: 100, year: 2026, rate: .1, terminal: .02, shares: 10,
|
|
||||||
debt: 100, assets: 0, minority: 0, margin: .2,
|
|
||||||
stages: [{years: 3, growth: .12}, {years: 4, growth: .08}, {years: 3, growth: .04}]};
|
|
||||||
const assumptions = {price: 138, profitMode: 'manual', profit: 125, conversion: .8,
|
|
||||||
profitGrowth: .08, target: .13, years: 5, peLow: 9, pe: 12, peHigh: 15,
|
|
||||||
dividend: 0, dividendGrowth: 0, dilution: 0};
|
|
||||||
const near = (a, b) => assert.ok(Math.abs(a - b) <= 1e-8 * Math.max(1, Math.abs(b)), `${a} ≠ ${b}`);
|
|
||||||
const result = calculateDCFCrosscheck(dcf, assumptions);
|
|
||||||
assert.equal(result.dcf.perShare.toFixed(2), '203.46');
|
|
||||||
assert.equal(result.rows.length, 2);
|
|
||||||
for (const row of result.rows) {
|
|
||||||
for (const scenario of row.scenarios) {
|
|
||||||
const standalone = calculateReturn({base_year: dcf.year, base_profit_100m: assumptions.profit,
|
|
||||||
shares_100m: dcf.shares, price: row.price, years: 5, exit_pe: scenario.pe, growth_rate: .08,
|
|
||||||
target_return: .13, annual_dividend_per_share: 0, dividend_growth_rate: 0, annual_dilution_rate: 0, safety_margin: dcf.margin});
|
|
||||||
near(scenario.irr, standalone.holding_period_irr);
|
|
||||||
near(scenario.requiredGrowth, standalone.required_growth_with_safety_margin.required_growth_rate);
|
|
||||||
near(scenario.requiredGrowthWithoutMargin, standalone.required_growth.required_growth_rate);
|
|
||||||
near(scenario.maxPrice, standalone.max_purchase_price_at_target_return);
|
|
||||||
near(scenario.maxPriceWithMargin, standalone.max_purchase_price_with_safety_margin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.equal(result.dcfCondition, true);
|
|
||||||
assert.equal(result.returnCondition, false);
|
|
||||||
near(result.returnCeiling, 125 / 10 * 1.08 ** 5 * 12 / 1.13 ** 5 * .8);
|
|
||||||
near(result.jointCeiling, Math.min(result.dcf.entryPrice, result.returnCeiling));
|
|
||||||
// Safety margin changes the growth hurdle and price ceiling, while actual IRR stays unchanged.
|
|
||||||
const noMargin = calculateDCFCrosscheck({...dcf, margin: 0}, assumptions);
|
|
||||||
near(result.returnCeiling, noMargin.returnCeiling * .8);
|
|
||||||
near(result.rows[0].base.irr, noMargin.rows[0].base.irr);
|
|
||||||
near(result.rows[0].base.requiredGrowthWithoutMargin, noMargin.rows[0].base.requiredGrowth);
|
|
||||||
assert.ok(result.rows[0].base.requiredGrowth > noMargin.rows[0].base.requiredGrowth);
|
|
||||||
const estimated = calculateDCFCrosscheck(dcf, {...assumptions, profitMode: 'estimate', profit: NaN});
|
|
||||||
near(estimated.profit, 125);
|
|
||||||
near(estimated.rows[0].base.irr, result.rows[0].base.irr);
|
|
||||||
const changedCash = calculateDCFCrosscheck({...dcf, fcf: 120}, assumptions);
|
|
||||||
near(changedCash.profit, 125);
|
|
||||||
near(changedCash.rows[0].base.irr, result.rows[0].base.irr);
|
|
||||||
assert.notEqual(changedCash.dcf.perShare, result.dcf.perShare);
|
|
||||||
near(calculateDCFCrosscheck({...dcf, fcf: 120}, {...assumptions, profitMode: 'estimate'}).profit, 150);
|
|
||||||
const distribution = calculateDCFCrosscheck(dcf, {...assumptions, dividend: 100});
|
|
||||||
assert.equal(distribution.rows[0].base.requiredGrowth, null);
|
|
||||||
assert.equal(distribution.growthGap, null);
|
|
||||||
assert.equal(calculateDCFCrosscheck(dcf, {...assumptions, profitGrowth: .2}).returnCondition, true);
|
|
||||||
// A scenario can reach the actual IRR target yet fail the requested price safety margin.
|
|
||||||
const targetOnly = calculateDCFCrosscheck(dcf, {...assumptions, price: 110});
|
|
||||||
assert.equal(targetOnly.rows[0].base.meetsTarget, true);
|
|
||||||
assert.equal(targetOnly.returnCondition, false);
|
|
||||||
// End-to-end round trips: DCF forward price -> standalone reverse growth -> DCF price.
|
|
||||||
// Exercise zero/nonzero margins, payouts, dilution, negative growth and long holdings.
|
|
||||||
for (const patch of [{}, {dividend: 2, dividendGrowth: .03, dilution: .02},
|
|
||||||
{profitGrowth: -.08, dilution: -.03, years: 3}, {years: 30}, {target: -.1}]) {
|
|
||||||
for (const margin of [0, .2, .5]) {
|
|
||||||
const a = {...assumptions, ...patch};
|
|
||||||
const p = {...dcf, margin};
|
|
||||||
const cross = calculateDCFCrosscheck(p, a);
|
|
||||||
const input = {base_year: p.year, base_profit_100m: a.profit, shares_100m: p.shares,
|
|
||||||
price: cross.returnCeiling, years: a.years, exit_pe: a.pe, growth_rate: a.profitGrowth,
|
|
||||||
target_return: a.target, annual_dividend_per_share: a.dividend,
|
|
||||||
dividend_growth_rate: a.dividendGrowth, annual_dilution_rate: a.dilution, safety_margin: margin};
|
|
||||||
const reversed = calculateReturn(input);
|
|
||||||
near(reversed.required_growth_with_safety_margin.required_growth_rate, a.profitGrowth);
|
|
||||||
near(reversed.max_purchase_price_with_safety_margin, cross.returnCeiling);
|
|
||||||
const atCeiling = calculateDCFCrosscheck(p, {...a, price: cross.returnCeiling});
|
|
||||||
near(atCeiling.rows[0].base.requiredGrowth, a.profitGrowth);
|
|
||||||
assert.equal(atCeiling.returnCondition, true);
|
|
||||||
const fromReverse = calculateDCFCrosscheck(p, {...a, profitGrowth: reversed.required_growth_with_safety_margin.required_growth_rate});
|
|
||||||
near(fromReverse.returnCeiling, cross.returnCeiling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Dividends may cover the actual price but not the price buffered by the safety margin.
|
|
||||||
const dividendCovered = calculateDCFCrosscheck(dcf, {...assumptions, dividend: 40});
|
|
||||||
assert.equal(dividendCovered.rows[0].base.requiredGrowthWithoutMargin, null);
|
|
||||||
assert.notEqual(dividendCovered.rows[0].base.requiredGrowth, null);
|
|
||||||
const diluted = calculateDCFCrosscheck(dcf, {...assumptions, dilution: .02, dividend: 2, dividendGrowth: .03});
|
|
||||||
assert.ok(diluted.rows.every(row => row.scenarios.every(scenario => Number.isFinite(scenario.irr))));
|
|
||||||
assert.throws(() => calculateDCFCrosscheck({...dcf, basis: 'fcff'}, {...assumptions, profitMode: 'estimate'}));
|
|
||||||
assert.throws(() => calculateDCFCrosscheck({...dcf, basis: 'fcff', debt: 1e9}, assumptions));
|
|
||||||
for (const patch of [{profit: 0}, {profitMode: 'invalid'}, {price: 0}, {profitGrowth: -1}, {years: 0},
|
|
||||||
{years: 3.5}, {target: -1}, {peLow: 15}, {peHigh: 10}, {pe: 0}, {dividend: -1}, {dilution: -1}]) {
|
|
||||||
assert.throws(() => calculateDCFCrosscheck(dcf, {...assumptions, ...patch}));
|
|
||||||
}
|
|
||||||
for (const conversion of [0, -1, Infinity, NaN]) {
|
|
||||||
assert.throws(() => calculateDCFCrosscheck(dcf, {...assumptions, profitMode: 'estimate', conversion}));
|
|
||||||
}
|
|
||||||
console.log('✓ DCF 与收益反推的含边际增速一致、价格/增速往返、实际 IRR、利润来源及边界校验');
|
|
||||||
Reference in New Issue
Block a user