CH.09 · 財金程式設計互動教材

債券評價與利率風險分析工具

第一套完整金融專業分析工具:從現金流折現算出債券價格,用二分法反推 YTM,計算存續期間與凸性,估計利率變動時的價格衝擊。

殖利率 ↑ → 價格 ↓

本章學習目標

  • 建立債券現金流量表,用折現公式計算合理價格
  • 判斷平價、溢價、折價,理解殖利率與價格的反向關係
  • 用二分法反推到期殖利率(YTM)
  • 計算麥考利存續期間、修正存續期間與凸性

01債券的六大基本要素

前八章完成了金融網站的基礎建設。從本章開始,正式進入金融專業工具的開發。債券是固定收益市場的重要工具:政府、銀行與企業發行債券取得資金,投資人依約收取利息及到期本金。債券看似固定,但市場價格會隨利率、信用風險、剩餘期限及流動性變動。

例如,投資人持有票面利率 3% 的十年期債券。若市場殖利率升至 5%,原債券票息相對偏低,市場價格通常會下跌。這就是本章要解決的問題:如何計算債券的合理價格、存續期間,以及利率變動時的價格估計。

要素說明
面額(Face Value)到期時發行機構應償還的本金,例如 100,000 元。所有票息與折現均以面額為基礎。
票面利率(Coupon Rate)決定每期票息的利率,通常在發行時固定。不等於市場殖利率,也不直接決定市場價格。
票息(Coupon)面額 100,000、票面利率 4%、每半年付息一次:每期票息 = 100,000 × 4% ÷ 2 = 2,000 元。
市場殖利率(Market Yield)投資人目前要求的報酬率,是計算債券現值的折現率,會隨時間變動。
到期日剩餘到期期間越長,固定利率債券通常對市場利率變動越敏感。

付息頻率(m)——影響每期票息、每期殖利率與總期數三個變數,計算時必須同步調整:

m付息方式每期票息公式總期數常見用途
1每年付息一次面額×票面利率到期年數一般公司債、政府公債(主流)
2每半年付息一次面額×票面利率÷2到期年數×2美國國債(慣用)
0零息債券無票息,到期一次還本到期年數(1期)節省利息支出的融資工具

02票面利率、當期殖利率與到期殖利率

殖利率種類公式限制
票面利率
Coupon Rate
年度票息 ÷ 面額⚠️ 不考慮市場價格,固定不變,與市場殖利率無關
當期殖利率
Current Yield
年度票息 ÷ 市場價格⚠️ 不考慮到期本金損益,忽略折/溢價損益
到期殖利率
YTM
使全部現金流現值 = 市場價格的折現率⚠️ 假設持有至到期、不違約、票息再投資;需用數值方法(二分法)求解

YTM 的六項假設(初學者必知)

YTM 假設實際可能不同的情況
持有至到期投資人可能在到期前賣出
發行機構不違約信用風險無法完全排除
每期票息按相同殖利率再投資實際再投資利率可能不同
沒有交易成本實際交易有手續費
i

YTM 是標準化報酬指標,不代表投資人實際報酬一定等於該數值。工具頁面下方應加入此說明。

03債券現金流量表

一般附息債券(年付息,3 年)——面額 100,000 元、票面利率 4%、每年付息一次:

年度票息本金總現金流
14,00004,000
24,00004,000
34,000100,000104,000

半年付息債券(5 年,m=2)——總期數 = 5×2 = 10 期,每期票息 = 100,000×4%÷2 = 2,000 元。最後一期(第 10 期)現金流 = 2,000 + 100,000 = 102,000 元。

i

零息債券:面額 100,000 元、殖利率 4%、5 年到期,無票息,到期時一次支付本金。零息債券的麥考利存續期間恰好等於到期年數,因為全部現金流集中在到期日——這是存續期間的一個重要極端案例。

04債券定價公式

P = Σ [C / (1+y/m)ᵗ] + F / (1+y/m)ᴺ

P 為債券價格(求解目標);C 為每期票息(= 面額 × 票面利率 ÷ m);F 為面額;y 為年化市場殖利率(小數);m 為每年付息次數;t 為期數;N 為總期數(= 到期年數 × m)。

定價案例計算——面額 100,000 元、票面利率 3%、市場殖利率 4%、到期 5 年、年付息一次。年度票息 = 100,000 × 3% = 3,000 元。因為市場殖利率 4% 高於票面利率 3%,所以債券價格低於面額,屬於折價債券(實際計算約 95,548 元)。

!

輸入 y 時,程式應除以 100 轉成小數(4% 輸入 4,程式需 y = 4/100 = 0.04)。這是本章最常見的計算錯誤來源。

05平價、溢價與折價債券

類型條件直覺解釋
平價 Par
價格 ≈ 面額
票面利率 = 市場殖利率投資人要求的報酬正好等於票息,不需折扣或溢價
溢價 Premium
價格 > 面額
票面利率 > 市場殖利率票息高於市場要求,投資人願意多付,形成溢價
折價 Discount
價格 < 面額
票面利率 < 市場殖利率票息低於市場要求,必須降價才能吸引投資人
i

直覺理解:市場殖利率上升,代表新發行的債券提供更高收益。舊債券固定票息相對吸引力下降,價格必須下跌(折價),使實際殖利率提升,才能與新債券競爭。

06利率與債券價格的關係

市場殖利率變化債券價格原因
殖利率 ↑ 上升價格 ↓ 下降折現率提高,未來固定現金流現值降低
殖利率 ↓ 下降價格 ↑ 上升折現率降低,未來固定現金流現值提高
i

非對稱性(凸性的直覺):殖利率下降 1% 帶來的價格上漲,通常大於殖利率上升 1% 造成的價格下跌。這種非對稱性來自價格-殖利率曲線的凸性,對投資人有利。

07麥考利存續期間

麥考利存續期間是以各期現金流現值為權重,計算債券現金流回收時間的加權平均。單位為年,不是期數。

規律原因
零息債券存續期間 = 到期年數全部現金流集中在到期日,加權平均 = 到期年數
附息債券存續期間 < 到期年數票息分散在每一期,早期現金流拉低加權平均
票面利率越高 → 存續期間越短更多現金流提早回收,時間加權降低
到期年限越長 → 存續期間越長更多現金流集中在較遠的未來

08修正存續期間

修正存續期間(Modified Duration)是麥考利存續期間除以 (1 + 每期殖利率),用於估計殖利率小幅變動時的債券價格百分比變化。

ΔP/P ≈ −D_mod × Δy

假設修正存續期間 = 6,市場殖利率上升 0.5%(Δy = 0.005):ΔP/P ≈ −6 × 0.005 = −3%。若原價格 100,000 元,估計新價格 ≈ 97,000 元。

!

負號的意義:殖利率上升(Δy > 0)→ 價格下降(ΔP < 0)。Δy 必須使用小數格式,例如 1% 應輸入 0.01,而非 1。

09凸性(Convexity)

存續期間使用直線近似,當殖利率變動較大時(如超過 1%)誤差明顯。凸性捕捉了曲線的彎曲程度,可改善估計。

方法公式優點限制
純存續期間估計ΔP/P ≈ −D_mod × Δy計算簡單利率變動大時誤差明顯(線性假設)
加入凸性估計ΔP/P ≈ −D_mod × Δy + ½ × C × (Δy)²改善曲線誤差,精度較高需要凸性計算,公式較複雜
完整重新定價代入新殖利率直接用公式計算結果最精確需完整計算,但計算機成本低
i

一般無選擇權的固定利率債券具有正凸性(Positive Convexity):殖利率下降時,價格上漲幅度較存續期間估計更大;殖利率上升時,價格下跌幅度較存續期間估計更小。這對債券投資人有利。可贖回債券(Callable Bond)在殖利率大幅下降時可能出現負凸性,不能直接套用本章公式。

10到期殖利率(YTM)的數值求解:二分法

多期附息債券的定價公式包含多個不同次方,無法像單期公式一樣直接代數求解 y,必須使用數值方法。

步驟說明
① 設定搜尋區間lower_bound = -0.95(最低殖利率);upper_bound = 10.0(最高殖利率)
② 取中間值mid_yield = (lower_bound + upper_bound) / 2
③ 計算理論價格用 mid_yield 代入定價公式,計算 price(mid_yield)
④ 與市場價格比較價格偏高→殖利率偏低→lower=mid;價格偏低→殖利率偏高→upper=mid
⑤ 判斷是否收斂若 |upper - lower| < tolerance(如 1e-10)→ 回傳結果
⑥ 重複直到收斂重複步驟②–⑤,最多 1000 次迭代,通常幾十次即可
i

二分法的前提:在搜尋區間內,bond price 是殖利率的嚴格遞減函數。若現金流出現多次正負號改變(如浮動利率或特殊結構),可能需要其他方法。

11Python 債券分析模組

程式區塊python/bond_analysis.py(核心函數)
from dataclasses import dataclass
import math

@dataclass(frozen=True)
class BondInput:
    face_value: float
    coupon_rate: float          # 小數,3% → 0.03
    market_yield: float          # 小數
    years_to_maturity: float
    payments_per_year: int = 1

def calculate_bond_price(bond: BondInput) -> float:
    y_per = bond.market_yield / bond.payments_per_year
    N = int(round(bond.years_to_maturity * bond.payments_per_year))
    coupon = bond.face_value * bond.coupon_rate / bond.payments_per_year
    price = 0.0
    for t in range(1, N + 1):
        principal = bond.face_value if t == N else 0.0
        price += (coupon + principal) / (1 + y_per) ** t
    return price

def calculate_macaulay_duration(bond: BondInput) -> float:
    price = calculate_bond_price(bond)
    y_per = bond.market_yield / bond.payments_per_year
    N = int(round(bond.years_to_maturity * bond.payments_per_year))
    coupon = bond.face_value * bond.coupon_rate / bond.payments_per_year
    weighted = 0.0
    for t in range(1, N + 1):
        principal = bond.face_value if t == N else 0.0
        pv = (coupon + principal) / (1 + y_per) ** t
        weighted += (t / bond.payments_per_year) * pv
    return weighted / price

def calculate_modified_duration(bond: BondInput) -> float:
    return calculate_macaulay_duration(bond) / (1 + bond.market_yield / bond.payments_per_year)
程式區塊YTM 二分法求解
def calculate_ytm(face_value, coupon_rate, market_price, years_to_maturity,
                   payments_per_year=1, lower_bound=-0.95, upper_bound=10.0,
                   tolerance=1e-10, max_iterations=1000) -> float:
    def price_diff(annual_yield):
        test_bond = BondInput(face_value, coupon_rate, annual_yield,
                               years_to_maturity, payments_per_year)
        return calculate_bond_price(test_bond) - market_price

    lo, hi = lower_bound, upper_bound
    for _ in range(max_iterations):
        mid = (lo + hi) / 2
        diff = price_diff(mid)
        if abs(hi - lo) < tolerance:
            return mid
        # 殖利率上升→價格下降:理論價格 > 市場價格 → 殖利率偏低 → 提高 lo
        if diff > 0:
            lo = mid
        else:
            hi = mid
    return (lo + hi) / 2

12Streamlit 債券分析工具

import streamlit as st
from bond_analysis import BondInput, analyze_bond, estimate_price_change

st.set_page_config(page_title='債券評價與利率風險', page_icon='📈', layout='wide')
st.title('債券評價與利率風險分析')

with st.sidebar:
    face_value = st.number_input('面額', min_value=1.0, value=100000.0, step=1000.0)
    coupon_rate_pct = st.number_input('票面利率(%)', min_value=0.0, value=3.0, step=0.1)
    market_yield_pct = st.number_input('市場殖利率(%)', min_value=-90.0, value=4.0, step=0.1)
    years_to_maturity = st.number_input('到期年數', min_value=0.25, value=5.0, step=0.5)
    payments_per_year = st.selectbox('每年付息次數', [1, 2, 4, 12], index=0)
    yield_change_bps = st.number_input('殖利率變動(基點)', value=100, step=25)

bond = BondInput(face_value, coupon_rate_pct/100, market_yield_pct/100,
                  years_to_maturity, payments_per_year)
result = analyze_bond(bond)

c1, c2, c3, c4 = st.columns(4)
c1.metric('債券價格', f"{result['price']:,.2f} 元")
c2.metric('價格狀態', result['price_type'])
c3.metric('修正存續期間', f"{result['modified_duration']:.4f}")
c4.metric('凸性', f"{result['convexity']:.4f}")

13JavaScript 前台債券計算器

前台 HTML + JavaScript 版本可直接整合至 Cloudflare Pages 靜態網站,不需 Python 環境。完整程式碼可以到「示範分頁」看實際運作結果,也是本章挑戰任務要動手修改的對象。

// ── 債券定價 ─────────────────────────────────────
function calculateBondPrice(faceValue, couponRate, marketYield, years, frequency) {
  const N = Math.round(years * frequency);
  const periodicCpn = faceValue * couponRate / frequency;   // 每期票息
  const periodicYld = marketYield / frequency;                // 每期殖利率
  let price = 0;
  for (let t = 1; t <= N; t++) {
    const principal = t === N ? faceValue : 0;    // 最後一期才還本
    const cashFlow = periodicCpn + principal;
    price += cashFlow / Math.pow(1 + periodicYld, t);   // 折現
  }
  return price;
}

// ── 麥考利存續期間 ───────────────────────────────
function calculateMacaulayDuration(faceValue, couponRate, marketYield, years, frequency) {
  const N = Math.round(years * frequency);
  const periodicCpn = faceValue * couponRate / frequency;
  const periodicYld = marketYield / frequency;
  const price = calculateBondPrice(faceValue, couponRate, marketYield, years, frequency);
  let weightedValue = 0;
  for (let t = 1; t <= N; t++) {
    const principal = t === N ? faceValue : 0;
    const cashFlow = periodicCpn + principal;
    const pv = cashFlow / Math.pow(1 + periodicYld, t);
    const timeInYears = t / frequency;   // 轉換為年
    weightedValue += timeInYears * pv;
  }
  return weightedValue / price;
}

// ── 修正存續期間 ─────────────────────────────────
function calculateModifiedDuration(macaulayDuration, marketYield, frequency) {
  return macaulayDuration / (1 + marketYield / frequency);
}

// ── 凸性 ─────────────────────────────────────────
function calculateBondConvexity(faceValue, couponRate, marketYield, years, frequency) {
  const N = Math.round(years * frequency);
  const periodicCpn = faceValue * couponRate / frequency;
  const periodicYld = marketYield / frequency;
  const price = calculateBondPrice(faceValue, couponRate, marketYield, years, frequency);
  let total = 0;
  for (let t = 1; t <= N; t++) {
    const principal = t === N ? faceValue : 0;
    const cashFlow = periodicCpn + principal;
    total += cashFlow * t * (t + 1) / Math.pow(1 + periodicYld, t + 2);
  }
  return total / (price * frequency * frequency);
}

14操作實務與七項測試案例

測試輸入條件預期結果
平價面額10萬、票面3%、殖利率3%、5年價格≈100,000元、顯示平價
折價面額10萬、票面3%、殖利率5%、5年價格<100,000元、顯示折價
溢價面額10萬、票面5%、殖利率3%、5年價格>100,000元、顯示溢價
零息面額10萬、票面0%、殖利率4%、5年價格≈82,193元;存續期間≈5年
半年付息面額10萬、票面4%、殖利率4%、5年、m=2每期票息2,000元;總期數10;價格≈面額
殖利率情境原殖利率4%、變動+100基點(+1%)新殖利率5%;債券價格應下降
YTM反推用殖利率4%算出價格,再反推YTM反推結果應接近4%(誤差<0.001%)

15基點(Basis Point,bps)換算

基點是利率變動的常用單位,1 基點 = 0.01% = 0.0001。

基點百分比小數用途範例
25 bps0.25%0.0025小幅升息情境
100 bps1.00%0.0100一碼升息(常見壓力測試)
200 bps2.00%0.0200大幅升息情境
// ✗ 錯誤:100 基點以為是 100%
yieldChange = 100;              // 這是 100%!

// ✓ 正確:100 基點 = 1% = 0.01
const yieldChange = 100 / 10000;   // = 0.01

16實務案例

(一)中央銀行升息

投資人持有面額 100 萬元、票面利率 2%、十年期、市場殖利率 2% 的平價債券。若市場殖利率升至 3%,固定票息相對偏低,市場價格下跌。長期債券受利率影響比短期更大,存續期間越長,利率敏感度越高。

(二)高票面利率不代表高報酬

某債券票面利率 6%,但市場價格 115 元(溢價),剩餘期間短。投資人雖每年收到較高票息,但到期只收回 100 元面額(資本損失 15 元)。應觀察 YTM 而非票面利率。

(三)折價債券的完整報酬

面額 100 元、票面利率 2%、市場價格 90 元。投資人報酬來源有二:每期票息收入,以及 90 元升至 100 元的到期價差(資本利得)。因此 YTM 通常高於票面利率與當期殖利率。

(四)存續期間估計誤差

某債券修正存續期間 = 8,殖利率上升 0.1% 時,估計價格變動 −0.8%,誤差通常很小。但若殖利率一次上升 3%,存續期間線性估計誤差可能超過 1%,建議加入凸性修正或使用完整重新定價。

(五)可贖回債券不適用本章公式

當利率大幅下降時,發行機構可能提前贖回高票息債券,價格上漲空間受限,可能出現負凸性。可贖回債券需要期權定價方法(OAS),不能直接套用本章的固定現金流假設。

17常見錯誤與除錯

錯誤類型症狀正確做法
百分比未除以 100票面利率輸入 3 被當成 300%,價格嚴重失真couponRate = input% / 100
半年付息未調整殖利率用年殖利率直接折現,價格偏高periodicYield = annualYield / m
最後一期忘記加面額債券價格遠低於合理值最後一期 cashFlow = coupon + faceValue
存續期間用期數不用年數存續期間顯示 10(應為 5 年)timeInYears = period / frequency
基點換算錯誤100 基點當成 100%(應為 1%)yieldChange = bps / 10000
YTM 搜尋上限不足二分法無法收斂,顯示錯誤擴大 upper_bound,或檢查價格是否合理

18模型限制與風險揭露

本章債券定價模型的標準假設:現金流確定、不發生違約、市場殖利率可作為折現率、票息按時支付、沒有稅負與交易成本、可持有至到期、沒有提前贖回或賣回條款。

實際債券還可能面臨:信用風險(發行機構違約)、流動性風險、再投資風險、通膨風險、匯率風險、提前贖回風險、法律與契約風險。

!

網站工具下方應明確標示:「本工具為固定利率債券的簡化評價模型,未納入信用違約、流動性、稅負、交易成本及選擇權條款,結果僅供金融教育與情境分析,不構成任何投資建議。」

19債券工具的網站整合

區塊內容
工具簡介工具名稱、一句話說明用途
輸入欄位面額、票面利率、市場殖利率、到期年數、付息頻率、情境基點
主要結果債券價格、價格狀態、修正存續期間、凸性
現金流表逐期展開:期數、票息、本金、現值
價格曲線殖利率 0.5%–10% 的價格-殖利率曲線圖
風險揭露模型假設、限制說明

20關鍵字

債券固定收益面額 票面利率票息付息頻率 市場殖利率當期殖利率到期殖利率 YTM 債券定價公式平價溢價 折價現金流量表折現因子 基點(bps)利率風險麥考利存續期間 修正存續期間凸性正凸性 負凸性二分法信用利差 再投資風險零息債券可贖回債券 dataclassBondInput情境分析

21實作練習

(一)基礎操作
  • 計算面額 100,000 元、票面利率 3%、殖利率 4%、五年期的債券價格,手動驗算第一期折現值
  • 將殖利率改為 3%,確認價格是否接近面額(平價)
  • 將票面利率改為 0%(零息),計算價格並確認麥考利存續期間是否等於到期年數
  • 將付息頻率改為每半年一次,確認期數變為 10、每期票息減半
  • 計算修正存續期間,用公式手動估算殖利率上升 1% 後的價格變動率
(二)功能修改
  • 增加總票息收入欄位(= 每期票息 × 總期數)
  • 增加到期總現金流(= 面額 + 總票息)
  • 在 Streamlit 工具新增平價水平線
  • 增加不同到期年限的存續期間比較(期限 1/5/10/20 年)
  • 在 JavaScript 版本加入 GA4 事件追蹤(bond_calculation 事件)
(三)進階挑戰
  • 加入應計利息計算,區分淨價與全價
  • 使用到期日與交割日計算剩餘期間
  • 建立多檔債券存續期間比較表
  • 加入信用利差情境分析(無風險利率 + 信用利差 = 總殖利率)
  • 將工具整合至第八章 Streamlit 後台

JavaScript 前台版本可以在瀏覽器中真的執行,所以本章挑戰任務(04 分頁)延續第一、二、三、五、七章的即時執行模式:修改真正會執行的程式碼,系統會即時告訴你哪裡出錯。

本章結語——本章完成了第一套完整金融專業分析工具:債券評價與利率風險分析器。從面額、票面利率、付息頻率與市場殖利率建立現金流,折現後計算合理價格;判斷平價、溢價、折價;用二分法反推 YTM;計算麥考利存續期間、修正存續期間與凸性;比較三種利率情境估計方式。

工具同時提供 Python 模組、Streamlit 互動工具與 JavaScript 前台計算器,三個版本共用相同的金融公式,確保計算結果一致。

下一章進入股票報酬、基本面與選擇權分析工具。讀者將建立股票報酬比較器、基本面指標分析、移動平均、選擇權到期損益圖,以及 Black–Scholes 理論價格與希臘字母(Delta/Gamma/Theta/Vega)敏感度指標。

債券評價與利率風險分析(教材原始範例)

輸入面額、票面利率、市場殖利率、到期年數、付息頻率與情境基點,點擊按鈕即時計算債券價格、平溢折價狀態、麥考利存續期間、修正存續期間、凸性,以及殖利率情境變動後的重新定價。這就是課本第十三節的 bond-calculator.html/bond-calculator.js 完全對應的成品,等一下的挑戰任務就是在這份程式碼上動手修改。

  bond-calculator.html — 在瀏覽器中執行

觀念測驗

共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。

0 / 15 已作答

挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯

債券計算器是純前端 JavaScript,可以在瀏覽器中真的執行。下面三個練習都是「真的會執行」的程式碼,修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行,並用白話文告訴你哪裡不對。

任務一:功能修改

對應教材「二十一、實作練習(二)功能修改」

難度:入門
  • 在結果區新增顯示「年度票息」(面額 × 票面利率)
js/bond-calculator.js — 可直接編輯
提示(卡住了再打開)
handleBondCalculation() 函式最後(resEl.classList.remove("hidden") 之前),加入一行把計算結果寫進指定的空欄位。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。

任務二:抓出隱藏的 id 錯誤

對應教材「十七、常見錯誤與除錯」

難度:除錯
  • 這份程式碼交接自另一位同學,執行後完全沒有反應(或沒有正確顯示結果)
  • 找出問題並修正,讓計算器恢復正常運作
js/bond-calculator.js — 藏有 1 個 bug
提示(卡住了再打開)
F12 → Console 如果出現「Cannot read properties of null」,代表 document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。

任務三:抓出百分比/基點換算錯誤

對應教材「十七、常見錯誤與除錯」— 百分比未除以100 / 基點換算錯誤

難度:除錯
  • 這份程式碼可以正常執行、不會報錯,但算出來的數字不合理
  • 面額 100,000、票面利率 3%、殖利率 4%、5 年期,正確價格應約為 $95,548
  • 找出問題所在並修正
js/bond-calculator.js — 數字不合理,但沒有紅字錯誤
提示(卡住了再打開)
這是「跑得動、但答案錯」的類型,找不到紅字錯誤時,回頭檢查每一個百分比轉換與基點換算,尤其是 couponRate 跟 yieldChange 是怎麼算出來的。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種換算錯誤。