# 甚麼是 DCF 現金流折現估值模型

# DCF(Discounted Cash Flow,現金流折現)

在理解任何 DCF 公式之前,先問自己一個問題:你現在拿到 $100,和三年後拿到 $100,哪個比較有價值?
答案顯然是「現在的 $100 更有價值」,因為你可以把它拿去投資、賺取報酬。這就是「錢的時間價值(Time Value of Money)」—— 未來的錢,比現在的錢不值錢。

DCF 的本質,就是把未來每一年的現金流,換算成「今天的價值」,然後加總起來,得到這間公司目前值多少錢。

# 為甚麼要把 未來每一年的現金流,換算成「今天的價值」

  1. 假設我要買一台印鈔機

這台機器每年都會自動印出鈔票給你。你會願意付多少錢買它?

這台機器未來能印出多少錢,決定了你現在應該付多少錢買它。

一間公司,本質上就是這台印鈔機。它每年產生的 FCF(自由現金流),就是它每年「印出來的鈔票」。所以「這間公司今天值多少錢」,答案就是:它未來所有能產生的現金流的總和。

  1. 那為什麼不直接把未來的錢全部加起來就好?

假設這台機器未來 10 年,每年都印出 $100。你可能會說:「那這台機器值 $1,000,因為 100 × 10 = 1,000。」
但這個算法有一個根本性的錯誤:第 10 年的 $100,跟今天的 $100,根本不是同一件事。
為什麼?因為今天的 $100,你可以拿去投資,10 年後可能變成 $260(假設年化報酬 10%)。反過來說,10 年後的 $100,換算成今天的購買力,其實只值大約 $38。

  1. 折現,就是「統一單位」的過程

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 如何影響最終估值?

  1. 是每年的折現計算:PV = FCF ÷ (1 + WACC)ⁿ。WACC 越高,遠年的現金流被打的折扣越重。Year 10 的現金流在 WACC = 10% 時,只剩下原來的 38.6%;但在 WACC = 13% 時,只剩下 29.5%。

  2. 是終值的計算: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>