# 甚麼是 DCF 現金流折現估值模型
# DCF(Discounted Cash Flow,現金流折現)
在理解任何 DCF 公式之前,先問自己一個問題:你現在拿到 $100,和三年後拿到 $100,哪個比較有價值?
答案顯然是「現在的 $100 更有價值」,因為你可以把它拿去投資、賺取報酬。這就是「錢的時間價值(Time Value of Money)」—— 未來的錢,比現在的錢不值錢。
DCF 的本質,就是把未來每一年的現金流,換算成「今天的價值」,然後加總起來,得到這間公司目前值多少錢。
# 為甚麼要把 未來每一年的現金流,換算成「今天的價值」
- 假設我要買一台印鈔機
這台機器每年都會自動印出鈔票給你。你會願意付多少錢買它?
這台機器未來能印出多少錢,決定了你現在應該付多少錢買它。
一間公司,本質上就是這台印鈔機。它每年產生的 FCF(自由現金流),就是它每年「印出來的鈔票」。所以「這間公司今天值多少錢」,答案就是:它未來所有能產生的現金流的總和。
- 那為什麼不直接把未來的錢全部加起來就好?
假設這台機器未來 10 年,每年都印出 $100。你可能會說:「那這台機器值 $1,000,因為 100 × 10 = 1,000。」
但這個算法有一個根本性的錯誤:第 10 年的 $100,跟今天的 $100,根本不是同一件事。
為什麼?因為今天的 $100,你可以拿去投資,10 年後可能變成 $260(假設年化報酬 10%)。反過來說,10 年後的 $100,換算成今天的購買力,其實只值大約 $38。
- 折現,就是「統一單位」的過程
DCF 的折現步驟,本質上是在做單位換算,把所有「不同時間點的錢」,全部換算成「今天的錢」這個統一的單位,這樣才能合法地相加。
換算完之後,你得到的總和,就是這間公司以今天的購買力衡量,總共能為你創造多少價值。而這個數字,就是這間公司今天「應該值多少錢」。
# 關鍵名詞解釋
# FCF(Free Cash Flow,自由現金流)
# 定義
FCF 是公司在支付所有營運成本和資本支出(蓋工廠、買設備)之後,真正「自由」可以動用的現金。
# FCF 與 淨利 的差別
# 淨利
淨利(Net Income)= 營收 − 各種成本與費用 − 利息 − 稅
聽起來很合理,但問題藏在「各種費用」的細節裡。會計上有一個重要的原則叫做應計制(Accrual Basis),意思是:收入和費用,不是以「錢有沒有真的進出口袋」來認列,而是以「這筆交易有沒有發生」來認列。
這就造成了淨利和真實現金流之間的裂縫。
假設你開了一間軟體公司,12 月簽了一個大合約,客戶答應明年 3 月付你 $1,000 萬。在會計上,這筆收入今年就可以認列,所以今年的淨利看起來很漂亮。但你的口袋裡呢?一毛都沒有。
另一個方向也成立:你今年花了 $5,000 萬蓋了一座新工廠,但會計上不會讓你今年一次認列這 $5,000 萬的費用,而是把它「攤提(Depreciation)」到未來 20 年,每年認列 $250 萬。所以今年的淨利看起來沒有少太多,但你的現金帳戶已經少了 $5,000 萬。
這就是淨利最大的問題:它是一個「被調整過」的數字,反映的是會計規則下的獲利,而不是公司真正口袋裡有多少錢。
# FCF(Free Cash Flow,自由現金流)
FCF = 營業現金流(Operating Cash Flow) − 資本支出(CapEx)
營業現金流已經把「應收帳款還沒收到的錢」、「折舊這種非現金費用加回來」等等全部調整掉了,反映的是真實的現金進出。然後再扣掉資本支出(買設備、蓋廠房等維持或擴張業務的花費),剩下來的才是真正「自由」的現金 —— 公司可以拿來還債、發股息、回購股票,或者繼續投資成長。
# 兩者的核心差異
造成差距的主要原因有三個。第一是折舊與攤銷(D&A):這是帳面上的費用,但公司根本沒有真的付出這筆現金,所以在計算現金流時要加回來。第二是營運資本的變動(Working Capital Changes):比如應收帳款增加,代表你「記帳的收入」比「實際收到的現金」多,這部分要從淨利中扣掉。第三是資本支出(CapEx):這是真實付出去的大筆現金,但淨利只反映每年攤提的一小部分,所以淨利會高估真實的現金狀況。
# 為什麼 DCF 用 FCF 而不是淨利?
回到 DCF 估值的核心邏輯:我們是在問「這間公司未來能為我產生多少真實的錢」。既然如此,當然要用反映真實現金的 FCF,而不是可以被會計手法影響的淨利。
有一句華爾街流傳的話很傳神:"Revenue is vanity, profit is sanity, but cash is reality."(營收是虛榮,利潤是理智,現金才是現實。)FCF 就是那個「現實」。
# WACC(Weighted Average Cost of Capital,加權平均資本成本)
WACC = (股本比例 × 股東要求報酬率) + (債務比例 × 借款利率 × (1 − 稅率))
WACC 是企業為了取得資金所付出的平均代價。
越高代表機會成本越高,反之亦然。
任何一間公司要運作,都需要資金。這些資金來自兩個地方:一是向股東募集的股本,二是向銀行或債券市場借來的債務。這兩種資金的提供者,都對公司有報酬率的要求。
股東承擔了最大的風險(公司倒了就虧光),所以他們要求的報酬率很高,可能是 12–15%。債權人相對安全(有抵押品、優先受償),所以要求的利率比較低,可能是 4–6%。
WACC 就是把這兩種資金的成本,依照各自在公司資本結構中的比例加權平均,得到一個「這間公司整體的資金成本」。
# WACC 如何影響最終估值?
-
是每年的折現計算:PV = FCF ÷ (1 + WACC)ⁿ。WACC 越高,遠年的現金流被打的折扣越重。Year 10 的現金流在 WACC = 10% 時,只剩下原來的 38.6%;但在 WACC = 13% 時,只剩下 29.5%。
-
是終值的計算:Gordon 公式的分母是 (WACC − gTerm),WACC 越大,分母越大,終值越小。
# gTerm (Terminal Growth Rate,永續成長率)
在 DCF 模型裡,我們預測了 10 年的 FCF。但公司不會在第 10 年就消失,它還會繼續營運下去 —— 理論上是永遠。那第 10 年之後無限長的未來,要怎麼估值?
這就是 ** 終值(Terminal Value)** 存在的原因,而 gTerm 就是用來計算終值的那個「永久成長率」。它的意思是:我們假設這間公司從第 10 年開始,每年以一個固定的速度永遠成長下去。
# FCF 基期設定
# 直接輸入 FCF
這個方式直接把你填入的 TTM FCF(過去 12 個月實際產生的自由現金流)當作計算起點,然後從這個數字開始,依照你設定的成長率往後推算 10 年。
適合使用的情境是:這間公司已經相對成熟,FCF 是真實且穩定的,你認為「現在的 FCF」就是一個合理的出發點。
# 營收 × FCF Margin
這個方式不直接用 TTM FCF,而是用「TTM 營收 × 你預期的 FCF Margin」來重新計算一個「理想化的基期 FCF」。
舉個例子,公司目前 TTM 營收是 $122,000M,但實際 TTM FCF 只有 $10,000M,FCF Margin 大約 8%。然而你研究後認為這間公司未來成熟後可以達到 25% 的 FCF Margin,那基期 FCF 就會被設定為 $122,000M × 25% = $30,500M,而不是實際的 $10,000M。
# 什麼時候該選哪一種?
關鍵在於對這間公司現況的判斷。如果公司目前的 FCF 已經能代表它真實的獲利能力,就用直接輸入。但如果公司正處於大量投資擴張的階段,FCF 被壓得很低甚至是負的,但你相信等它度過投資期之後利潤率會大幅提升,這時候直接用 TTM FCF 當起點就會嚴重低估公司價值,用 FCF Margin 法來設定一個「未來正常化的起點」就更合理。
簡單說,直接輸入 FCF 是「現在的公司值多少」,FCF Margin 法是「成熟後的公司值多少」。兩種方法本身沒有對錯,差別在於你對這間公司所處階段的判斷。
# 兩階段成長模型
真實世界的公司不可能永遠以同一個速度成長。一間科技公司早期可能每年成長 30%,但成熟後就趨緩到 10%,最終接近整體 GDP 成長率。因此這個模型用「兩階段」來模擬:
for (let y = 1; y <= 10; y++) { | |
const rate = y <= 5 ? g1 : g2; // 前 5 年用 g1,後 5 年用 g2 | |
fcf = fcf * (1 + rate); | |
fcfs.push(fcf); | |
pvs.push(fcf / Math.pow(1 + wacc, y)); // 折現 | |
} |
每一年的 FCF 預測公式是:
FCF (年) = 前一年 FCF × (1 + 成長率)
例如,如果 Year 0 的 FCF 是 $30,380M,第一階段成長率 20%,那麼 Year 1 的 FCF 就是 $30,380 × 1.20 = $36,456M。
# 相關公式
# 折現公式:把未來的錢拉回現在
每一年預測出來的 FCF,都要用以下公式折現回「今天的現值(Present Value, PV)」:
PV (第 n 年) = FCF (第 n 年) ÷ (1 + WACC)ⁿ
# 終值(Terminal Value)
# 方法一:Gordon 增長模型(永久年金公式)
假設公司在第 10 年之後,以一個固定的「永久成長率(Terminal g)」永遠成長下去:
終值 = FCF (Year 10) × (1 + gTerm) ÷ (WACC − gTerm)
# 方法二:退出乘數法(Exit Multiple)
這個方法更直觀:假設 10 年後你把這間公司賣掉,買家願意出的價格是「當年 FCF 的 N 倍」:
終值 = FCF (Year 10) × 退出乘數
這個 N 倍(例如 20×)代表市場願意為這間公司未來的獲利能力付出的溢價,可以參考同行業上市公司的估值倍數來設定。
# 企業價值(EV, Enterprise Value)
所有的折現現金流加起來,得到的是企業價值(EV, Enterprise Value)—— 這是整間公司(含債務)的價值:
EV = Σ PV (Year 1–10) + 終值現值
但股東真正擁有的,是扣掉債務、加回現金之後的股權價值(Equity Value):
股權價值 = EV + 現金 − 總負債
最後再除以流通股數,就得到每股的合理估值:
合理股價 = 股權價值 ÷ 流通股數
# 安全邊際(Margin of Safety)
這邊其實就是在計算,現在的價格與我們算出的合理股價相差多少百分比。
安全邊際 = (合理股價 − 當前市價) ÷ 當前市價 × 100%
這個概念源自 Benjamin Graham 的價值投資思想。如果合理股價遠高於市價(安全邊際 > 15%),代表這支股票被低估,有買入的空間;反之則被高估。「安全邊際」這個名字很有意思 —— 即使你的 DCF 假設稍有偏差,只要低買,你還是有緩衝空間。
# 敏感性分析:承認你的不確定性
DCF 的輸出高度依賴你輸入的假設(尤其是 WACC 和 gTerm)。敏感性矩陣的作法是:固定其他條件,系統性地改變 WACC 和 gTerm,看看在不同的假設組合下,合理股價會落在什麼範圍,在圖中的最下面顯示了這個矩陣。
# 程式碼
需要的所有參數
- TTM 營收(過去 12 個月總營收,單位:百萬)
- TTM 自由現金流(過去 12 個月自由現金流,單位:百萬)
- 現金及短期投資(資產負債表上現金部位,單位:百萬)
- 總負債(含長短期借款,單位:百萬)
- 流通股數(已發行普通股,單位:百萬股)
- 當前股價(市場現價,單位:$)
- FCF Margin 目標(預計達到的自由現金流利潤率,單位:%)
- 第一階段成長率 g1(Year 1–5 年化成長率,單位:%)
- 第二階段成長率 g2(Year 6–10 年化成長率,單位:%)
- 永久成長率 gTerm(第 10 年後的永久成長率,單位:%)
- WACC(加權平均資本成本,單位:%)
- 退出 EV/FCF 乘數(Year 10 FCF 的市場估值倍數)
<!DOCTYPE html> | |
<html lang="zh-TW"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>DCF 現金流折現估值模型</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script> | |
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--bg: #0a0c0f; | |
--surface: #111418; | |
--surface2: #161b22; | |
--border: #21262d; | |
--border2: #30363d; | |
--gold: #d4a853; | |
--gold-dim: #8a6b30; | |
--green: #3fb950; | |
--red: #f85149; | |
--blue: #58a6ff; | |
--text: #e6edf3; | |
--text-dim: #8b949e; | |
--text-muted: #484f58; | |
--accent: #d4a853; | |
} | |
* { box-sizing: border-box; margin: 0; padding: 0; } | |
body { | |
background: var(--bg); | |
color: var(--text); | |
font-family: 'JetBrains Mono', monospace; | |
font-size: 13px; | |
min-height: 100vh; | |
} | |
/* ─── Header ─── */ | |
header { | |
border-bottom: 1px solid var(--border); | |
padding: 24px 40px; | |
display: flex; | |
align-items: baseline; | |
gap: 20px; | |
background: var(--surface); | |
} | |
header h1 { | |
font-family: 'Syne', sans-serif; | |
font-size: 22px; | |
font-weight: 800; | |
letter-spacing: -0.5px; | |
color: var(--gold); | |
} | |
header span { | |
color: var(--text-dim); | |
font-size: 11px; | |
letter-spacing: 2px; | |
text-transform: uppercase; | |
} | |
/* ─── Layout ─── */ | |
.wrapper { | |
display: grid; | |
grid-template-columns: 340px 1fr; | |
min-height: calc(100vh - 73px); | |
overflow: hidden; | |
} | |
/* ─── Left Panel ─── */ | |
.left-panel { | |
border-right: 1px solid var(--border); | |
padding: 28px 24px; | |
display: flex; | |
flex-direction: column; | |
gap: 24px; | |
overflow-y: auto; | |
background: var(--surface); | |
} | |
.section-title { | |
font-family: 'Syne', sans-serif; | |
font-size: 10px; | |
font-weight: 700; | |
letter-spacing: 3px; | |
text-transform: uppercase; | |
color: var(--gold); | |
padding-bottom: 10px; | |
border-bottom: 1px solid var(--border); | |
margin-bottom: 14px; | |
} | |
.field-group { display: flex; flex-direction: column; gap: 12px; } | |
.field { | |
display: grid; | |
grid-template-columns: 1fr auto; | |
align-items: start; | |
gap: 8px; | |
} | |
.field-info { display: flex; flex-direction: column; gap: 3px; } | |
.field-label { | |
font-size: 12px; | |
font-weight: 500; | |
color: var(--text); | |
} | |
.field-hint { | |
font-size: 10px; | |
color: var(--text-muted); | |
line-height: 1.5; | |
} | |
.field-input-wrap { | |
position: relative; | |
display: flex; | |
align-items: center; | |
} | |
.field-input-wrap .unit { | |
position: absolute; | |
right: 8px; | |
color: var(--text-muted); | |
font-size: 11px; | |
pointer-events: none; | |
} | |
input[type="number"], input[type="text"] { | |
background: var(--bg); | |
border: 1px solid var(--border2); | |
color: var(--text); | |
font-family: 'JetBrains Mono', monospace; | |
font-size: 13px; | |
padding: 7px 28px 7px 10px; | |
width: 110px; | |
border-radius: 4px; | |
outline: none; | |
transition: border-color 0.15s; | |
text-align: right; | |
} | |
input[type="number"]:focus, input[type="text"]:focus { | |
border-color: var(--gold); | |
} | |
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; } | |
.method-toggle { | |
display: flex; | |
gap: 6px; | |
margin-bottom: 4px; | |
} | |
.method-btn { | |
flex: 1; | |
padding: 6px; | |
border: 1px solid var(--border2); | |
background: var(--bg); | |
color: var(--text-dim); | |
font-family: 'JetBrains Mono', monospace; | |
font-size: 11px; | |
cursor: pointer; | |
border-radius: 4px; | |
transition: all 0.15s; | |
} | |
.method-btn.active { | |
border-color: var(--gold); | |
background: rgba(212,168,83,0.1); | |
color: var(--gold); | |
} | |
.calc-btn { | |
width: 100%; | |
padding: 12px; | |
background: var(--gold); | |
color: #0a0c0f; | |
font-family: 'Syne', sans-serif; | |
font-size: 13px; | |
font-weight: 700; | |
letter-spacing: 2px; | |
text-transform: uppercase; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
transition: all 0.2s; | |
} | |
.calc-btn:hover { background: #e8bb66; transform: translateY(-1px); } | |
/* ─── Right Panel ─── */ | |
.right-panel { | |
padding: 28px 32px; | |
display: flex; | |
flex-direction: column; | |
gap: 28px; | |
overflow-y: auto; | |
overflow-x: hidden; | |
min-width: 0; | |
} | |
/* ─── Result Cards ─── */ | |
.cards { | |
display: grid; | |
grid-template-columns: repeat(4, 1fr); | |
gap: 12px; | |
} | |
.card { | |
background: var(--surface2); | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
padding: 16px; | |
display: flex; | |
flex-direction: column; | |
gap: 6px; | |
} | |
.card.highlight { | |
border-color: var(--gold-dim); | |
background: rgba(212,168,83,0.05); | |
} | |
.card-label { | |
font-size: 10px; | |
color: var(--text-muted); | |
letter-spacing: 1.5px; | |
text-transform: uppercase; | |
} | |
.card-value { | |
font-family: 'Syne', sans-serif; | |
font-size: 26px; | |
font-weight: 700; | |
color: var(--text); | |
} | |
.card-sub { font-size: 11px; color: var(--text-dim); } | |
.card-value.green { color: var(--green); } | |
.card-value.red { color: var(--red); } | |
.card-value.gold { color: var(--gold); } | |
/* ─── Signal Badge ─── */ | |
.signal-badge { | |
display: inline-flex; | |
align-items: center; | |
gap: 8px; | |
padding: 6px 14px; | |
border-radius: 20px; | |
font-family: 'Syne', sans-serif; | |
font-size: 13px; | |
font-weight: 700; | |
letter-spacing: 2px; | |
} | |
.signal-badge.buy { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); } | |
.signal-badge.sell { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid rgba(248,81,73,0.3); } | |
.signal-badge.neutral { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid rgba(88,166,255,0.3); } | |
/* ─── Charts Grid ─── */ | |
.charts-grid { | |
display: grid; | |
grid-template-columns: 3fr 2fr; | |
gap: 16px; | |
} | |
.chart-card { | |
background: var(--surface2); | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
padding: 20px; | |
min-width: 0; | |
} | |
.chart-card.wide { grid-column: 1 / -1; } | |
.chart-title { | |
font-family: 'Syne', sans-serif; | |
font-size: 11px; | |
font-weight: 700; | |
letter-spacing: 2px; | |
text-transform: uppercase; | |
color: var(--text-dim); | |
margin-bottom: 16px; | |
} | |
.chart-wrap { | |
position: relative; | |
height: 220px; | |
width: 100%; | |
} | |
.chart-wrap.tall { height: 260px; } | |
.chart-wrap.pie-wrap { | |
height: 240px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.chart-wrap.pie-wrap canvas { | |
max-width: 100%; | |
max-height: 100%; | |
} | |
/* ─── Projection Table ─── */ | |
.table-card { | |
background: var(--surface2); | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
overflow: hidden; | |
} | |
.table-header { | |
padding: 16px 20px; | |
border-bottom: 1px solid var(--border); | |
font-family: 'Syne', sans-serif; | |
font-size: 11px; | |
font-weight: 700; | |
letter-spacing: 2px; | |
text-transform: uppercase; | |
color: var(--text-dim); | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th { | |
text-align: right; | |
padding: 10px 14px; | |
font-size: 10px; | |
color: var(--text-muted); | |
letter-spacing: 1px; | |
border-bottom: 1px solid var(--border); | |
white-space: nowrap; | |
} | |
th:first-child { text-align: left; } | |
td { | |
text-align: right; | |
padding: 9px 14px; | |
font-size: 12px; | |
border-bottom: 1px solid var(--border); | |
color: var(--text-dim); | |
} | |
td:first-child { | |
text-align: left; | |
color: var(--text-muted); | |
font-size: 11px; | |
} | |
tr:last-child td { border-bottom: none; } | |
tr:hover td { background: rgba(255,255,255,0.02); } | |
.td-highlight { color: var(--gold) !important; font-weight: 600; } | |
/* ─── Sensitivity Matrix ─── */ | |
.sensitivity-grid { | |
overflow-x: auto; | |
} | |
.sens-table { | |
border-collapse: collapse; | |
min-width: 100%; | |
} | |
.sens-table th, .sens-table td { | |
padding: 8px 12px; | |
border: 1px solid var(--border); | |
text-align: center; | |
font-size: 11px; | |
} | |
.sens-table th { color: var(--gold); background: rgba(212,168,83,0.07); } | |
.sens-table td { min-width: 70px; } | |
.sens-low { background: rgba(248,81,73,0.15); color: var(--red); } | |
.sens-mid { background: rgba(88,166,255,0.1); color: var(--blue); } | |
.sens-high { background: rgba(63,185,80,0.15); color: var(--green); } | |
.sens-current { border: 2px solid var(--gold) !important; color: var(--gold) !important; font-weight: 600; } | |
/* ─── Value Bridge ─── */ | |
.bridge-row { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
padding: 8px 0; | |
border-bottom: 1px solid var(--border); | |
} | |
.bridge-row:last-child { border-bottom: none; } | |
.bridge-label { color: var(--text-dim); font-size: 12px; } | |
.bridge-val { font-size: 13px; font-weight: 600; } | |
.bridge-val.pos { color: var(--green); } | |
.bridge-val.neg { color: var(--red); } | |
.bridge-val.gold { color: var(--gold); } | |
.bridge-divider { | |
border-top: 1px solid var(--border2); | |
margin: 4px 0; | |
} | |
/* ─── Responsive ─── */ | |
@media (max-width: 1100px) { | |
.wrapper { grid-template-columns: 1fr; } | |
.left-panel { border-right: none; border-bottom: 1px solid var(--border); } | |
.cards { grid-template-columns: repeat(2, 1fr); } | |
.charts-grid { grid-template-columns: 1fr; } | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>DCF 現金流折現估值模型</h1> | |
<span>Discounted Cash Flow Valuation</span> | |
</header> | |
<div class="wrapper"> | |
<!-- ─── LEFT PANEL: Inputs ─── --> | |
<div class="left-panel"> | |
<!-- 公司基本資料 --> | |
<div> | |
<div class="section-title">公司基本資料</div> | |
<div class="field-group"> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">TTM 營收</div> | |
<div class="field-hint">過去 12 個月總營收(百萬)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="ttm_revenue" value="3000000" step="1"> | |
<span class="unit">M</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">TTM 自由現金流</div> | |
<div class="field-hint">過去 12 個月自由現金流(百萬)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="ttm_fcf" value="688083" step="1"> | |
<span class="unit">M</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">現金及短期投資</div> | |
<div class="field-hint">資產負債表上現金部位(百萬)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="cash" value="2400000" step="1"> | |
<span class="unit">M</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">總負債</div> | |
<div class="field-hint">含長短期借款(百萬)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="debt" value="2368362" step="1"> | |
<span class="unit">M</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">流通股數</div> | |
<div class="field-hint">已發行普通股(百萬股)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="shares" value="24211" step="1"> | |
<span class="unit">M</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">當前股價</div> | |
<div class="field-hint">市場現價,用於計算安全邊際</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="price" value="1865" step="0.01"> | |
<span class="unit">$</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- FCF 估算方式 --> | |
<div> | |
<div class="section-title">FCF 基期設定</div> | |
<div class="method-toggle"> | |
<button class="method-btn active" id="btn_direct" onclick="setMethod('direct')">直接輸入 FCF</button> | |
<button class="method-btn" id="btn_margin" onclick="setMethod('margin')">營收 × FCF Margin</button> | |
</div> | |
<div id="method_direct" class="field-group"> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">基期 FCF(已自動帶入)</div> | |
<div class="field-hint">使用上方 TTM FCF 作為 Year 0</div> | |
</div> | |
</div> | |
</div> | |
<div id="method_margin" class="field-group" style="display:none"> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">FCF Margin(目標)</div> | |
<div class="field-hint">預計達到的自由現金流利潤率</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="fcf_margin" value="30" step="0.5"> | |
<span class="unit">%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 成長率假設 --> | |
<div> | |
<div class="section-title">成長率假設(兩階段)</div> | |
<div class="field-group"> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">第一階段成長率(Y1–5)</div> | |
<div class="field-hint">近 5 年高速成長預期,參考歷史成長率</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="g1" value="25" step="0.5"> | |
<span class="unit">%</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">第二階段成長率(Y6–10)</div> | |
<div class="field-hint">成長趨於穩定,通常低於第一階段</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="g2" value="20" step="0.5"> | |
<span class="unit">%</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">永久成長率(Terminal g)</div> | |
<div class="field-hint">永遠維持的長期成長率,通常≈GDP成長率(2–3%)</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="gterm" value="4" step="0.1"> | |
<span class="unit">%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 折現率 --> | |
<div> | |
<div class="section-title">折現率(WACC)</div> | |
<div class="field-group"> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">WACC</div> | |
<div class="field-hint">加權平均資本成本,用於折現所有未來現金流。通常 8–12%</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="wacc" value="10.5" step="0.5"> | |
<span class="unit">%</span> | |
</div> | |
</div> | |
<div class="field"> | |
<div class="field-info"> | |
<div class="field-label">終值計算方式</div> | |
<div class="field-hint">—</div> | |
</div> | |
</div> | |
<div class="method-toggle"> | |
<button class="method-btn active" id="btn_gordon" onclick="setTVMethod('gordon')">Gordon 增長模型</button> | |
<button class="method-btn" id="btn_exit" onclick="setTVMethod('exit')">退出乘數法</button> | |
</div> | |
<div id="tv_exit_field" class="field" style="display:none"> | |
<div class="field-info"> | |
<div class="field-label">退出 EV/FCF 乘數</div> | |
<div class="field-hint">Year 10 FCF × 此乘數 = 終值</div> | |
</div> | |
<div class="field-input-wrap"> | |
<input type="number" id="exit_mult" value="30" step="1"> | |
<span class="unit">×</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<button class="calc-btn" onclick="calculate()">▶ 計算估值</button> | |
</div> | |
<!-- ─── RIGHT PANEL: Results ─── --> | |
<div class="right-panel"> | |
<!-- Key Metrics Cards --> | |
<div class="cards" id="result-cards"> | |
<div class="card highlight"> | |
<div class="card-label">合理股票價值</div> | |
<div class="card-value gold" id="card-fair">—</div> | |
<div class="card-sub">Per Share (DCF)</div> | |
</div> | |
<div class="card"> | |
<div class="card-label">當前市價</div> | |
<div class="card-value" id="card-price">—</div> | |
<div class="card-sub">Current Market Price</div> | |
</div> | |
<div class="card"> | |
<div class="card-label">安全邊際</div> | |
<div class="card-value" id="card-margin">—</div> | |
<div class="card-sub" id="card-signal-wrap">—</div> | |
</div> | |
<div class="card"> | |
<div class="card-label">企業價值 (EV)</div> | |
<div class="card-value" id="card-ev">—</div> | |
<div class="card-sub">Enterprise Value (M)</div> | |
</div> | |
</div> | |
<!-- Value Bridge + FCF Chart --> | |
<div class="charts-grid"> | |
<div class="chart-card"> | |
<div class="chart-title">價值組成分析</div> | |
<div id="bridge-rows"> | |
<div style="color:var(--text-muted);font-size:12px">請先點擊「計算估值」</div> | |
</div> | |
</div> | |
<div class="chart-card"> | |
<div class="chart-title">價值構成比例</div> | |
<div class="chart-wrap pie-wrap"> | |
<canvas id="pieChart"></canvas> | |
</div> | |
</div> | |
<div class="chart-card wide"> | |
<div class="chart-title">年度 FCF 預測(折現前 vs 折現後)</div> | |
<div class="chart-wrap tall"> | |
<canvas id="fcfChart"></canvas> | |
</div> | |
</div> | |
</div> | |
<!-- Projection Table --> | |
<div class="table-card"> | |
<div class="table-header">10 年 FCF 預測明細</div> | |
<div style="overflow-x:auto"> | |
<table id="proj-table"> | |
<thead> | |
<tr> | |
<th style="text-align:left">項目</th> | |
<th>Year 1</th><th>Year 2</th><th>Year 3</th><th>Year 4</th><th>Year 5</th> | |
<th>Year 6</th><th>Year 7</th><th>Year 8</th><th>Year 9</th><th>Year 10</th> | |
</tr> | |
</thead> | |
<tbody id="proj-body"> | |
<tr><td colspan="11" style="text-align:center;padding:20px;color:var(--text-muted)">請先點擊「計算估值」</td></tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<!-- Sensitivity Matrix --> | |
<div class="chart-card"> | |
<div class="chart-title">敏感性分析 — 合理股價(WACC × 永久成長率)</div> | |
<div class="sensitivity-grid"> | |
<table class="sens-table" id="sens-table"> | |
<tbody> | |
<tr><td colspan="8" style="text-align:center;color:var(--text-muted);padding:20px">請先點擊「計算估值」</td></tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// ─── State ─── | |
let fcfMethod = 'direct'; | |
let tvMethod = 'gordon'; | |
let pieChart = null; | |
let fcfChartInst = null; | |
// ─── Method Toggles ─── | |
function setMethod(m) { | |
fcfMethod = m; | |
document.getElementById('btn_direct').classList.toggle('active', m==='direct'); | |
document.getElementById('btn_margin').classList.toggle('active', m==='margin'); | |
document.getElementById('method_direct').style.display = m==='direct' ? '' : 'none'; | |
document.getElementById('method_margin').style.display = m==='margin' ? '' : 'none'; | |
} | |
function setTVMethod(m) { | |
tvMethod = m; | |
document.getElementById('btn_gordon').classList.toggle('active', m==='gordon'); | |
document.getElementById('btn_exit').classList.toggle('active', m==='exit'); | |
document.getElementById('tv_exit_field').style.display = m==='exit' ? '' : 'none'; | |
} | |
// ─── Helpers ─── | |
const fmtMoney = v => v >= 1e6 ? (v/1e6).toFixed(2)+'T' : v >= 1e3 ? (v/1e3).toFixed(1)+'B' : v.toFixed(1)+'M'; | |
const fmtPct = v => (v>=0?'+':'')+v.toFixed(1)+'%'; | |
const fmtUSD = v => '$'+v.toFixed(2); | |
const g = id => parseFloat(document.getElementById(id).value) || 0; | |
// ─── Core DCF Calculation ─── | |
function dcfCalc(ttmFcf, g1, g2, gTerm, wacc, tvMult) { | |
const fcfs = []; | |
const pvs = []; | |
let fcf = ttmFcf; | |
for (let y = 1; y <= 10; y++) { | |
const rate = y <= 5 ? g1 : g2; | |
fcf = fcf * (1 + rate); | |
fcfs.push(fcf); | |
pvs.push(fcf / Math.pow(1 + wacc, y)); | |
} | |
let terminalValue, pvTerminal; | |
if (tvMethod === 'gordon') { | |
terminalValue = fcfs[9] * (1 + gTerm) / (wacc - gTerm); | |
} else { | |
terminalValue = fcfs[9] * tvMult; | |
} | |
pvTerminal = terminalValue / Math.pow(1 + wacc, 10); | |
const pvFcfSum = pvs.reduce((a,b)=>a+b, 0); | |
return { fcfs, pvs, pvFcfSum, terminalValue, pvTerminal }; | |
} | |
// ─── Main Calculate ─── | |
function calculate() { | |
const ttmRev = g('ttm_revenue'); | |
const ttmFcf = g('ttm_fcf'); | |
const cash = g('cash'); | |
const debt = g('debt'); | |
const shares = g('shares'); | |
const price = g('price'); | |
const g1 = g('g1') / 100; | |
const g2 = g('g2') / 100; | |
const gTerm = g('gterm') / 100; | |
const wacc = g('wacc') / 100; | |
const exitMult = g('exit_mult'); | |
const fcfMargin= g('fcf_margin') / 100; | |
// Base FCF | |
let baseFcf = fcfMethod === 'direct' ? ttmFcf : ttmRev * fcfMargin; | |
// Validate | |
if (wacc <= gTerm && tvMethod === 'gordon') { | |
alert('⚠️ WACC 必須大於永久成長率(Gordon 模型要求)'); | |
return; | |
} | |
const { fcfs, pvs, pvFcfSum, terminalValue, pvTerminal } = dcfCalc(baseFcf, g1, g2, gTerm, wacc, exitMult); | |
const ev = pvFcfSum + pvTerminal; | |
const equityValue = ev + cash - debt; | |
const fairPrice = equityValue / shares; | |
const mos = (fairPrice - price) / price * 100; | |
// ── Cards ── | |
document.getElementById('card-fair').textContent = fmtUSD(fairPrice); | |
document.getElementById('card-price').textContent = fmtUSD(price); | |
const mosEl = document.getElementById('card-margin'); | |
mosEl.textContent = fmtPct(mos); | |
mosEl.className = 'card-value ' + (mos > 15 ? 'green' : mos < -15 ? 'red' : 'blue'); | |
const sig = mos > 15 ? 'buy' : mos < -15 ? 'sell' : 'neutral'; | |
const sigText = mos > 15 ? '▲ BUY' : mos < -15 ? '▼ SELL' : '◉ HOLD'; | |
document.getElementById('card-signal-wrap').innerHTML = | |
`<span class="signal-badge ${sig}">${sigText}</span>`; | |
document.getElementById('card-ev').textContent = fmtMoney(ev); | |
// ── Bridge ── | |
const stage1PV = pvs.slice(0,5).reduce((a,b)=>a+b,0); | |
const stage2PV = pvs.slice(5).reduce((a,b)=>a+b,0); | |
document.getElementById('bridge-rows').innerHTML = ` | |
<div class="bridge-row"> | |
<div class="bridge-label">PV of FCF(Y1–5)</div> | |
<div class="bridge-val pos">+${fmtMoney(stage1PV)}</div> | |
</div> | |
<div class="bridge-row"> | |
<div class="bridge-label">PV of FCF(Y6–10)</div> | |
<div class="bridge-val pos">+${fmtMoney(stage2PV)}</div> | |
</div> | |
<div class="bridge-row"> | |
<div class="bridge-label">PV of 終值(Terminal Value)</div> | |
<div class="bridge-val pos">+${fmtMoney(pvTerminal)}</div> | |
</div> | |
<div class="bridge-divider"></div> | |
<div class="bridge-row"> | |
<div class="bridge-label">企業價值 (EV)</div> | |
<div class="bridge-val gold">${fmtMoney(ev)}</div> | |
</div> | |
<div class="bridge-row"> | |
<div class="bridge-label">(+) 現金及短期投資</div> | |
<div class="bridge-val pos">+${fmtMoney(cash)}</div> | |
</div> | |
<div class="bridge-row"> | |
<div class="bridge-label">(−) 總負債</div> | |
<div class="bridge-val neg">−${fmtMoney(debt)}</div> | |
</div> | |
<div class="bridge-divider"></div> | |
<div class="bridge-row"> | |
<div class="bridge-label">股權價值</div> | |
<div class="bridge-val gold">${fmtMoney(equityValue)}</div> | |
</div> | |
<div class="bridge-row"> | |
<div class="bridge-label">÷ 流通股數</div> | |
<div class="bridge-val">${shares.toFixed(2)}M 股</div> | |
</div> | |
<div class="bridge-divider"></div> | |
<div class="bridge-row"> | |
<div class="bridge-label"><strong>合理股價</strong></div> | |
<div class="bridge-val gold" style="font-size:16px">${fmtUSD(fairPrice)}</div> | |
</div> | |
`; | |
// ── Pie Chart ── | |
const pieData = [stage1PV, stage2PV, pvTerminal]; | |
if (pieChart) pieChart.destroy(); | |
pieChart = new Chart(document.getElementById('pieChart'), { | |
type: 'doughnut', | |
data: { | |
labels: ['FCF Y1–5', 'FCF Y6–10', '終值'], | |
datasets: [{ | |
data: pieData, | |
backgroundColor: ['#3fb950', '#58a6ff', '#d4a853'], | |
borderWidth: 0, | |
hoverOffset: 4 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: true, | |
cutout: '62%', | |
layout: { padding: 8 }, | |
plugins: { | |
legend: { position:'bottom', labels:{ color:'#8b949e', font:{family:'JetBrains Mono',size:11}, padding:14, boxWidth:12 } }, | |
tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${fmtMoney(ctx.raw)} (${(ctx.raw/ev*100).toFixed(1)}%)` } } | |
} | |
} | |
}); | |
// ── FCF Bar Chart ── | |
const labels = ['Y1','Y2','Y3','Y4','Y5','Y6','Y7','Y8','Y9','Y10']; | |
if (fcfChartInst) fcfChartInst.destroy(); | |
fcfChartInst = new Chart(document.getElementById('fcfChart'), { | |
type: 'bar', | |
data: { | |
labels, | |
datasets: [ | |
{ label: '未折現 FCF', data: fcfs, backgroundColor: 'rgba(88,166,255,0.25)', borderColor: '#58a6ff', borderWidth: 1.5, borderRadius: 3 }, | |
{ label: '折現後 PV', data: pvs, backgroundColor: 'rgba(63,185,80,0.3)', borderColor: '#3fb950', borderWidth: 1.5, borderRadius: 3 } | |
] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
plugins: { | |
legend: { labels:{ color:'#8b949e', font:{family:'JetBrains Mono',size:11} } }, | |
tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${fmtMoney(ctx.raw)}` } } | |
}, | |
scales: { | |
x: { ticks:{ color:'#8b949e', font:{size:11}, maxRotation: 0 }, grid:{ color:'rgba(255,255,255,0.04)' } }, | |
y: { ticks:{ color:'#8b949e', font:{size:11}, callback: v=>fmtMoney(v) }, grid:{ color:'rgba(255,255,255,0.06)' } } | |
} | |
} | |
}); | |
// ── Projection Table ── | |
const fcfMarginArr = fcfs.map(f => f / ttmRev * 100); | |
const discountFactor = fcfs.map((_,i) => (1/Math.pow(1+wacc, i+1))); | |
const rows = [ | |
{ label:'FCF(百萬)', vals: fcfs, fmt: v=>'$'+v.toFixed(0), cls: 'td-highlight' }, | |
{ label:'折現係數', vals: discountFactor, fmt: v=>v.toFixed(4) }, | |
{ label:'PV(折現後)', vals: pvs, fmt: v=>'$'+v.toFixed(0) }, | |
{ label:'成長率', vals: Array.from({length:10},(_,i)=>i<5?g1*100:g2*100), fmt: v=>v.toFixed(1)+'%' }, | |
{ label:'FCF/Rev Margin', vals: fcfMarginArr, fmt: v=>v.toFixed(1)+'%' }, | |
]; | |
document.getElementById('proj-body').innerHTML = rows.map(r => | |
`<tr><td>${r.label}</td>${r.vals.map(v=>`<td class="${r.cls||''}">${r.fmt(v)}</td>`).join('')}</tr>` | |
).join('') + `<tr> | |
<td style="color:var(--gold)">終值 PV(Year 10)</td> | |
<td colspan="9"></td> | |
<td class="td-highlight">${fmtMoney(pvTerminal)}</td> | |
</tr>`; | |
// ── Sensitivity Matrix ── | |
const waccRange = [0.07, 0.08, 0.09, 0.10, 0.11, 0.12, 0.13]; | |
const gtermRange = [0.01, 0.015, 0.02, 0.025, 0.03, 0.035]; | |
let html = '<thead><tr><th>WACC↓ / gTerm→</th>'; | |
gtermRange.forEach(gt => html += `<th>${(gt*100).toFixed(1)}%</th>`); | |
html += '</tr></thead><tbody>'; | |
waccRange.forEach(w => { | |
html += `<tr><th>${(w*100).toFixed(0)}%</th>`; | |
gtermRange.forEach(gt => { | |
if (w <= gt) { html += `<td style="color:var(--text-muted)">N/A</td>`; return; } | |
const r = dcfCalc(baseFcf, g1, g2, gt, w, exitMult); | |
const fp = (r.pvFcfSum + r.pvTerminal + cash - debt) / shares; | |
const mosV = (fp - price) / price * 100; | |
const isCurrentW = Math.abs(w - wacc) < 0.005; | |
const isCurrentG = Math.abs(gt - gTerm) < 0.002; | |
const cls = isCurrentW && isCurrentG ? 'sens-current' : mosV > 20 ? 'sens-high' : mosV < -10 ? 'sens-low' : 'sens-mid'; | |
html += `<td class="${cls}">${fmtUSD(fp)}</td>`; | |
}); | |
html += '</tr>'; | |
}); | |
html += '</tbody>'; | |
document.getElementById('sens-table').innerHTML = html; | |
} | |
// Auto-calculate on load | |
window.onload = () => calculate(); | |
</script> | |
</body> | |
</html> |