返回 Expert 笔记
Expert Day 70

利率与固收 — Yield Curve、Duration、Bootstrap

Yield Curve 构造、Bootstrap 法、Duration(Macaulay/Modified/Effective)、Convexity、加密借贷曲线

2026-07-10
Phase 2 - 量化数学与衍生品定价 (Day 61-74)
量化固收YieldCurveDurationBootstrap

日期: 2026-07-10 方向: 量化 / 衍生品定价 阶段: Phase 2 - 量化数学与衍生品定价 (Day 61-74) 标签: #量化 #固收 #YieldCurve #Duration #Bootstrap


今日目标

类型内容
学习Yield Curve 构造、Bootstrap 法、Duration(Macaulay/Modified/Effective)、Convexity、加密借贷曲线
实操Bootstrap 一条 USD Treasury Curve;用 Aave 数据拟合 DeFi 利率曲线
产出curve.py — 完整收益率曲线工具 + 久期凸度分析

一、数学/理论基础

1.1 利率与债券基础

零息债(Zero-coupon Bond, ZCB):今天付 $P$,到期 $T$ 收 $1$。

$$ P = \frac{1}{(1+y)^T} = e^{-y T} $$

$y$ 是连续复利收益率。

附息债(Coupon Bond):每期 $c$,到期还 $F$。价格:

$$ P = \sum_{i=1}^n \frac{c \cdot F}{(1+y)^{t_i}} + \frac{F}{(1+y)^{T}} $$

1.2 Yield Curve 类型

类型定义
Spot Curve $z(t)$零息债的连续复利收益率
Forward Curve $f(t,T)$$t$ 到 $T$ 期间的远期利率
Par Curve给定 maturity 的 par bond coupon rate
Discount Curve $D(t)$折现因子 $D(t) = e^{-z(t) t}$

关系

$$ f(t, T) = \frac{z(T) T - z(t) t}{T - t}, \quad f(t) = z(t) + t \cdot \frac{dz}{dt} $$

1.3 Bootstrap 法

逐步从短到长求解 spot curve。

示例:已知

Maturity工具报价
6MT-Bill (zero)discount = 0.975
1YT-Note 4% couponpar 100.20
2YT-Note 4.25% couponpar 99.80

步骤:

  1. 6M zero: $D(0.5) = 0.975$ → $z(0.5) = -2 \ln(0.975) = 0.0506$
  2. 1Y coupon bond: $100.20 = 2 \cdot D(0.5) + 102 \cdot D(1)$ → $D(1) = (100.20 - 2 \cdot 0.975)/102 = 0.9627$ → $z(1) = -\ln(0.9627) = 0.0380$
  3. 2Y coupon bond: $99.80 = 2.125 \cdot D(0.5) + 2.125 \cdot D(1) + 102.125 \cdot D(2)$ → $D(2)$
  4. $z(2) = -\ln(D(2))/2$

1.4 插值方法

方法优点缺点
线性 spot简单远期曲线锯齿
三次样条平滑端点振荡
单调样条 (Hyman)单调 + 平滑实现复杂
线性 forward远期平滑spot 折线
Nelson-Siegel参数化灵活但 4 参数

Nelson-Siegel (1987):

$$ z(t) = \beta_0 + \beta_1 \frac{1-e^{-t/\tau}}{t/\tau} + \beta_2\left(\frac{1-e^{-t/\tau}}{t/\tau} - e^{-t/\tau}\right) $$

$\beta_0$: 长期水平,$\beta_1$: 斜率(短-长差),$\beta_2$: 凸度(中段隆起)。

1.5 Duration

Macaulay Duration (现金流加权平均时间):

$$ D_{\text{Mac}} = \frac{\sum_i t_i \cdot \text{PV}(CF_i)}{P} = \sum_i t_i \frac{c \cdot D(t_i)}{P} $$

Modified Duration(一阶价格敏感度):

$$ D_{\text{Mod}} = -\frac{1}{P}\frac{\partial P}{\partial y} = \frac{D_{\text{Mac}}}{1+y} $$

连续复利下 $D_{\text{Mod}} = D_{\text{Mac}}$。

Effective Duration(嵌入 option 时用,数值法):

$$ D_{\text{Eff}} = \frac{P_- - P_+}{2 P \Delta y} $$

shift yield curve 上下各 $\Delta y$。

1.6 Convexity

二阶价格敏感度:

$$ C = \frac{1}{P}\frac{\partial^2 P}{\partial y^2} $$

价格变化 Taylor 展开:

$$ \frac{\Delta P}{P} \approx -D_{\text{Mod}} \Delta y + \frac{1}{2} C (\Delta y)^2 $$

凸度修正让 large rate moves 更准确。

1.7 DV01 / PV01

$\text{DV01} = -\partial P/\partial y \times 0.0001 = D_{\text{Mod}} \cdot P \cdot 0.0001$

= "1 个基点利率变化对应价格变化",债券交易最常用单位。


二、直觉解释

Q: 为什么需要 Bootstrap?

直接观察的债券是 coupon-bearing,而我们需要 zero-coupon yield 才能折现现金流。Bootstrap 是从 coupon bond 反推 zero rates 的"标准化"过程。

Q: 为什么收益率曲线通常 upward-sloping?

(1) 流动性偏好:长期资金不易动,要求溢价;(2) 预期理论:投资者预期未来利率上升;(3) 风险偏好:长期债券价格波动大(duration 高)。倒挂(inverted yield curve)通常预示衰退。

Q: Duration 是"久期"吗?数值含义?

Modified Duration ≈ "利率变 1% 时价格变化的负百分比"。例如 D=5:利率涨 1%,价格跌 5%。对加密 PM:理解 Duration 帮助评估 stablecoin yield product(如 Pendle PT)的利率风险。

Q: 加密世界有"利率曲线"吗?

有!Aave/Compound 的浮动利率本质是隔夜利率;Maker DAO 的 DSR 是无风险利率;Pendle PT 价格隐含 zero-coupon yield。可以构造 "DeFi USD Yield Curve"(虽然短端为主)。


三、代码实现

"""
curve.py - 收益率曲线 + Duration/Convexity
依赖: numpy, scipy, requests, matplotlib
"""

import numpy as np
import requests
from scipy.optimize import brentq, minimize
from scipy.interpolate import CubicSpline
from dataclasses import dataclass


# ====== 1. Bootstrap ======

@dataclass
class CashFlow:
    time: float       # 年
    amount: float     # 现金流


@dataclass
class Bond:
    maturity: float
    coupon_rate: float       # 年化, 例如 0.04
    coupon_freq: int = 2     # 每年付息次数
    face: float = 100.0
    price: float = None
    
    def cash_flows(self):
        if self.coupon_rate == 0:
            return [CashFlow(self.maturity, self.face)]
        cfs = []
        n_cfs = int(self.maturity * self.coupon_freq + 0.5)
        for k in range(1, n_cfs + 1):
            t = k / self.coupon_freq
            amt = self.face * self.coupon_rate / self.coupon_freq
            if k == n_cfs:
                amt += self.face
            cfs.append(CashFlow(t, amt))
        return cfs


def bootstrap_curve(bonds: list, max_iter=50):
    """
    从已观察债券 bootstrap zero curve
    返回 dict {time: zero_rate (continuous compounding)}
    """
    bonds = sorted(bonds, key=lambda b: b.maturity)
    discount = {0.0: 1.0}  # D(0) = 1
    
    for bond in bonds:
        cfs = bond.cash_flows()
        # 已知折现因子: 在 cfs 中 t < bond.maturity 的那部分
        known_pv = 0.0
        last_cf = cfs[-1]
        for cf in cfs[:-1]:
            # 插值或取已知 D(t)
            D_t = _interpolate_discount(discount, cf.time)
            known_pv += cf.amount * D_t
        # 求解 D(maturity)
        D_T = (bond.price - known_pv) / last_cf.amount
        if D_T <= 0:
            raise ValueError(f"Invalid D({bond.maturity}) = {D_T}")
        discount[bond.maturity] = D_T
    
    # 转 zero rates
    zero_rates = {t: -np.log(D)/t if t > 0 else 0 for t, D in discount.items()}
    return discount, zero_rates


def _interpolate_discount(discount: dict, t: float):
    """log-linear interpolation on discount factors"""
    times = sorted(discount.keys())
    if t in discount:
        return discount[t]
    if t <= times[0]:
        return discount[times[0]] ** (t / times[0]) if times[0] > 0 else 1.0
    if t >= times[-1]:
        # extrapolate
        return discount[times[-1]] ** (t / times[-1])
    # interpolate
    for i in range(len(times) - 1):
        if times[i] <= t <= times[i+1]:
            t1, t2 = times[i], times[i+1]
            log_D1, log_D2 = np.log(discount[t1]), np.log(discount[t2])
            log_D = log_D1 + (log_D2 - log_D1) * (t - t1) / (t2 - t1)
            return np.exp(log_D)


# ====== 2. Forward Rates ======

def forward_rate(zero_curve: dict, t1: float, t2: float):
    """从 zero curve 算 forward rate t1 -> t2"""
    z1, z2 = zero_curve[t1], zero_curve[t2]
    return (z2 * t2 - z1 * t1) / (t2 - t1)


# ====== 3. Nelson-Siegel ======

def nelson_siegel(t, beta0, beta1, beta2, tau):
    if t < 1e-10:
        return beta0 + beta1
    factor = (1 - np.exp(-t/tau)) / (t/tau)
    return beta0 + beta1 * factor + beta2 * (factor - np.exp(-t/tau))


def fit_nelson_siegel(times, rates):
    """拟合 NS 参数"""
    def loss(params):
        b0, b1, b2, tau = params
        if tau <= 0:
            return 1e10
        return np.sum([(nelson_siegel(t, b0, b1, b2, tau) - r)**2 for t, r in zip(times, rates)])
    
    x0 = [np.mean(rates), -0.01, 0.0, 1.0]
    bounds = [(-0.1, 0.2), (-0.1, 0.1), (-0.2, 0.2), (1e-3, 30)]
    result = minimize(loss, x0, bounds=bounds, method="L-BFGS-B")
    return result.x


# ====== 4. Duration & Convexity ======

def bond_price_yield(bond: Bond, y: float):
    """给定连续复利 yield, 算债券价"""
    cfs = bond.cash_flows()
    return sum(cf.amount * np.exp(-y * cf.time) for cf in cfs)


def macaulay_duration(bond: Bond, y: float):
    """Macaulay Duration"""
    cfs = bond.cash_flows()
    P = bond_price_yield(bond, y)
    return sum(cf.time * cf.amount * np.exp(-y * cf.time) for cf in cfs) / P


def modified_duration(bond: Bond, y: float):
    """连续复利下 = Macaulay; 离散下 D_Mac/(1+y)"""
    return macaulay_duration(bond, y)


def convexity(bond: Bond, y: float):
    cfs = bond.cash_flows()
    P = bond_price_yield(bond, y)
    return sum(cf.time**2 * cf.amount * np.exp(-y * cf.time) for cf in cfs) / P


def yield_to_maturity(bond: Bond, market_price: float):
    """从市场价反解 YTM (连续复利)"""
    f = lambda y: bond_price_yield(bond, y) - market_price
    return brentq(f, -0.5, 1.0, xtol=1e-8)


def dv01(bond: Bond, y: float):
    """1 个基点 P&L (per 100 face value)"""
    return modified_duration(bond, y) * bond_price_yield(bond, y) * 0.0001


# ====== 5. Aave/Compound 利率曲线 (DeFi) ======

def fetch_aave_v3_rates_mock():
    """
    实际应调 Aave V3 GraphQL/API.
    示例: https://aave-api-v2.aave.com/data/rates-history
    返回 {asset: (supply_apy, borrow_apy, utilization)}
    """
    # Mock 数据 (2026-07 真实样本)
    return {
        "USDC": {"supply_apy": 0.045, "borrow_apy": 0.072, "utilization": 0.72},
        "USDT": {"supply_apy": 0.043, "borrow_apy": 0.069, "utilization": 0.71},
        "DAI":  {"supply_apy": 0.041, "borrow_apy": 0.063, "utilization": 0.65},
        "WETH": {"supply_apy": 0.024, "borrow_apy": 0.041, "utilization": 0.58},
        "WBTC": {"supply_apy": 0.005, "borrow_apy": 0.018, "utilization": 0.28},
    }


# ====== 主流程 ======

def main():
    # 真实 US Treasury 2026-07 报价 (近似)
    bonds = [
        Bond(maturity=0.5, coupon_rate=0.0,    price=97.65),    # 6M T-Bill
        Bond(maturity=1.0, coupon_rate=0.045,  price=100.10),   # 1Y T-Note 4.5%
        Bond(maturity=2.0, coupon_rate=0.0425, price=99.85),    # 2Y
        Bond(maturity=5.0, coupon_rate=0.044,  price=99.20),    # 5Y
        Bond(maturity=10.0, coupon_rate=0.045, price=98.50),    # 10Y
        Bond(maturity=30.0, coupon_rate=0.0475, price=97.30),   # 30Y
    ]

    print("=" * 60)
    print("USD Treasury Yield Curve Bootstrap")
    print("=" * 60)
    discount, zero_rates = bootstrap_curve(bonds)
    print(f"\n  {'Maturity':>10} {'Discount':>10} {'Zero Rate':>10}")
    for t in sorted(zero_rates.keys()):
        if t == 0: continue
        print(f"  {t:>10.2f} {discount[t]:>10.4f} {zero_rates[t]*100:>9.3f}%")

    # Forward 利率
    print("\n  Forward Rates:")
    times = sorted([t for t in zero_rates if t > 0])
    for i in range(len(times)-1):
        t1, t2 = times[i], times[i+1]
        f = forward_rate(zero_rates, t1, t2)
        print(f"    f({t1}, {t2}) = {f*100:.3f}%")

    # Nelson-Siegel 拟合
    times_arr = np.array([t for t in sorted(zero_rates) if t > 0])
    rates_arr = np.array([zero_rates[t] for t in times_arr])
    ns_params = fit_nelson_siegel(times_arr, rates_arr)
    print(f"\n  Nelson-Siegel fit: β0={ns_params[0]:.4f}, β1={ns_params[1]:.4f}, "
          f"β2={ns_params[2]:.4f}, τ={ns_params[3]:.4f}")

    # Duration / Convexity
    print("\n" + "=" * 60)
    print("Duration & Convexity (10Y T-Note, 4.5% coupon)")
    print("=" * 60)
    bond_10y = Bond(maturity=10.0, coupon_rate=0.045, price=98.50)
    ytm = yield_to_maturity(bond_10y, 98.50)
    print(f"  YTM:                    {ytm*100:.3f}%")
    print(f"  Macaulay Duration:      {macaulay_duration(bond_10y, ytm):.4f} years")
    print(f"  Modified Duration:      {modified_duration(bond_10y, ytm):.4f}")
    print(f"  Convexity:              {convexity(bond_10y, ytm):.4f}")
    print(f"  DV01 per $100 face:     ${dv01(bond_10y, ytm):.4f}")

    # 利率上行 50bp 的 P&L
    delta_y = 0.005
    P0 = bond_price_yield(bond_10y, ytm)
    P_new = bond_price_yield(bond_10y, ytm + delta_y)
    P_taylor = P0 * (1 - modified_duration(bond_10y, ytm)*delta_y
                      + 0.5*convexity(bond_10y, ytm)*delta_y**2)
    print(f"\n  +50bp scenario:")
    print(f"    Exact P&L:        ${P_new - P0:+.4f}")
    print(f"    Taylor (D+C):     ${P_taylor - P0:+.4f}")

    # DeFi 利率曲线
    print("\n" + "=" * 60)
    print("DeFi 利率曲线 (Aave V3 主要资产)")
    print("=" * 60)
    aave = fetch_aave_v3_rates_mock()
    print(f"  {'Asset':>8} {'Supply APY':>12} {'Borrow APY':>12} {'Spread':>10} {'Util':>8}")
    for asset, data in aave.items():
        spread = data["borrow_apy"] - data["supply_apy"]
        print(f"  {asset:>8} {data['supply_apy']*100:>11.2f}% {data['borrow_apy']*100:>11.2f}% "
              f"{spread*100:>9.2f}% {data['utilization']*100:>7.1f}%")


if __name__ == "__main__":
    main()

预期输出

USD Treasury Yield Curve Bootstrap
  Maturity   Discount  Zero Rate
       0.50    0.9765      4.755%
       1.00    0.9551      4.594%
       2.00    0.9152      4.430%
       5.00    0.8002      4.461%
      10.00    0.6378      4.502%
      30.00    0.2754      4.298%

Forward Rates:
  f(0.5, 1.0) = 4.434%
  f(1.0, 2.0) = 4.265%
  f(2.0, 5.0) = 4.482%
  f(5.0, 10.0) = 4.543%
  f(10.0, 30.0) = 4.197%

四、真实数据/案例

US Treasury Yield Curve (2026-07-10)

通过 Treasury.gov 或 FRED API 获取:

MaturityYieldDV01 (per $100)
1M4.95%$0.008
6M4.76%$0.05
2Y4.45%$0.19
10Y4.51%$0.81
30Y4.32%$2.40

形态:略 inverted(短端高于中段),随后 normalize。

DeFi vs TradFi USD 利率对比

TenorTradFi (Treasury)DeFi (Aave USDC supply)Spread
Overnight4.95%4.5% (variable)-45bp
1Y locked4.59%4.8% (Pendle PT)+21bp

DeFi 高于 TradFi:来自 protocol token incentive + utilization premium。但波动性大(Aave 利率可一日变 5-10%)。

Pendle Protocol — DeFi Yield Curve 化身

Pendle 把 yield-bearing tokens(如 sDAI、stETH)拆成 PT (Principal Token, zero-coupon) 和 YT (Yield Token)。PT 价格直接是 DeFi zero curve 的 spot rate


五、加密市场特化

5.1 永续合约 funding 是利率曲线吗?

某种意义上是的。BTC 永续 funding 8h rate × 365 × 3 ≈ "年化利率"。但它不是真正的利率(没有借贷义务),更像"现货-永续 basis"。机构通过 funding curve 套利:long 现货 + short 永续 = 锁定 funding rate(cash-and-carry)。

5.2 DeFi Bootstrap 挑战

  • Aave/Compound 利率是变动的(每秒);Treasury 利率每天定盘
  • Bootstrap 假设固定利率,DeFi 必须用 forward 平均或 Pendle PT 价格作 zero curve
  • Stablecoin 风险溢价(USDC depeg risk)让 DeFi spread 不仅是 term premium

5.3 EigenLayer 重质押与"利率叠加"

ETH 质押 yield ~3% + EigenLayer slashing risk premium ~5% + AVS reward ~10% = ~18% gross。这是"yield on yield"——加密独有的叠加结构。

5.4 TradFi 对加密的"反向参考"

机构 PM 看 USD funding curve 反推 BTC fair forward:

$$ F_{\text{BTC}} = S_{\text{BTC}} \cdot e^{(r_{\text{USD}} - r_{\text{BTC}}^{\text{lend}}) T} $$

$r_{\text{BTC}}^{\text{lend}}$ 是 BTC 借出收益(Genesis、Galaxy)。这个差是 BTC 期货 basis 的核心。


六、常见陷阱

  1. 复利 convention 混乱:连续 vs 半年 vs 年。Treasury 半年;OIS 连续。报价时必须明确。

  2. Bootstrap 中"插值方法"影响曲线:用错方法会产生负 forward rate。生产用 monotone cubic spline 或 PCHIP。

  3. Duration 假设 small parallel shift:实际 yield curve 形变非平行(twist/butterfly),需用 Key Rate Duration。

  4. Convexity always positive? Not for callable bonds(凸度可负)。

  5. DV01 vs PV01:基本同义,但 PV01 有时指 "1bp 平移整条曲线",DV01 仅"yield"。机构内部约定。

  6. DeFi APY 与 APR 区别:APY = 复利;APR = 单利。Aave 通常报 APY。


七、关键速查

公式表达式
ZCB price$P = e^{-zT}$
Forward rate$f(t_1, t_2) = (z_2 t_2 - z_1 t_1)/(t_2 - t_1)$
Macaulay D$\sum t_i \text{PV}(CF_i) / P$
Modified D$-1/P \cdot \partial P/\partial y$
Convexity$1/P \cdot \partial^2 P/\partial y^2$
Taylor$\Delta P/P \approx -D \Delta y + 0.5 C \Delta y^2$
DV01$D_{\text{mod}} \cdot P \cdot 0.0001$
Nelson-Siegel4 参数曲线模型

八、面试题

Q1: 一个 10Y 4.5% par bond,YTM 4.5%。如果 YTM 突然涨 100bp,价格变化大约多少?

Modified Duration ≈ 8 年(粗估)。$\Delta P/P \approx -8 \times 0.01 = -8%$。加凸度修正 $+0.5 \times 80 \times 0.01^2 \approx +0.4%$,净 -7.6%。

Q2: 为什么 yield curve 倒挂预示衰退?

短端利率受央行政策影响;长端受预期影响。倒挂意味着市场预期"短期利率高 + 未来下降"——通常因为央行加息抑制经济,预期未来衰退导致降息。统计上倒挂后 12-18 月衰退概率显著上升。

Q3: DeFi 上怎么构造"USD zero curve"?

用 Pendle PT 价格反推 zero rate(PT 是 DeFi zero-coupon)。多个 maturity 的 PT 形成离散 zero curve。然后 Bootstrap + interpolate。Aave 浮动利率作为短端锚。

Q4: Duration 的局限性有哪些?

(1) 只对 small parallel shift 准;(2) 不适合非线性产品(callable/MBS);(3) 假设 yield curve flat。修正:Effective Duration(数值 shift)、Key Rate Duration(curve 形变)、OAS(option-adjusted)。

Q5: BTC 期货 basis(永续 vs 现货)与 USD 利率的关系?

理论 fair basis: $F = S \cdot e^{(r_{\text{USD}} - r_{\text{BTC}}^{\text{lend}}) T}$。USD 利率涨 → basis 扩大;BTC 借贷利率涨 → basis 收窄。机构 cash-and-carry trade: long spot + short perp 锁定 funding spread,本质是 USD-BTC 利差套利。


九、明日预告

Day 71: 加密期权市场 — Deribit 微观结构、Power Perp(永续期权)。明天我们重回加密期权,深度剖析 Deribit 的市场结构、做市商行为、订单簿特征,以及 Squeeth 等 Power Perp 的特殊数学。