02-利率衍生品 (Interest Rate Derivatives)

预计学习时间:3 小时

难度:⭐⭐⭐⭐

核心问题:利率不是单个数字,而是一条曲线。整条曲线的随机变动怎么建模和定价?


概述

利率衍生品的定价逻辑和股票期权类似(都是用无套利原理和风险中性定价),但有一个根本性的区别:

  • 股票:一个随机变量(股价 S),用 BSM 搞定
  • 利率:一整条曲线(从隔夜到 30 年),每个期限的利率都在变动

所以利率建模的核心挑战不是”一个变量怎么变”,而是”一整条曲线怎么一起变”。

"借钱利率" → 即期/远期利率 → IRS → Cap/Floor → Swaption → 短利率模型 → Bootstrap

一、利率基础:从”借钱利率”讲起

1.1 即期利率(Spot Rate)

白话解释:即期利率就是”今天借一笔钱,约定某个期限,到期一起还本付息”的年化利率。

期限即期利率含义
1 年3.0%今天借 100,一年后还 103
2 年3.5%今天借 100,两年后还 100 × (1.035)^2 = 107.12
3 年3.8%今天借 100,三年后还 100 × (1.038)^3 = 111.83

如果一年后你再借一年,利率不一定是 3.5%——那取决于一年后的市场情况。

1.2 远期利率(Forward Rate)

白话解释:远期利率是”今天约定,未来某个时间借一笔钱”的利率。你今天签约锁定利率,未来到期时执行。

即期利率与远期利率的关系(无套利):

白话解读:“先借 t1 年,再借 t2-t1 年”的总效果,必须等于”直接借 t2 年”。如果不等,就存在套利机会。

用连续复利:

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# 即期利率与远期利率的关系
# ============================================================
 
# 模拟即期利率曲线(向上倾斜)
maturities = np.array([0.5, 1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 10.0])
spot_rates = np.array([0.025, 0.030, 0.033, 0.035, 0.038, 0.042, 0.044, 0.045])
 
# 从即期利率计算远期利率
# f(t1, t2) = (r2 * t2 - r1 * t1) / (t2 - t1)
forward_rates = []
forward_periods = []
for i in range(len(maturities) - 1):
    t1 = maturities[i]
    t2 = maturities[i + 1]
    r1 = spot_rates[i]
    r2 = spot_rates[i + 1]
    f = (r2 * t2 - r1 * t1) / (t2 - t1)
    forward_rates.append(f)
    forward_periods.append(f"{t1}-{t2}")
 
print("即期利率与远期利率")
print("=" * 60)
print(f"{'期限':>10} {'即期利率':>10} {'远期利率':>10} {'远期区间':>10}")
print("-" * 45)
print(f"{'0.5年':>10} {spot_rates[0]:>10.3%} {'':>10} {'':>10}")
for i in range(len(forward_rates)):
    print(f"{maturities[i+1]:>6.1f}{spot_rates[i+1]:>10.3%} {forward_rates[i]:>10.3%} {forward_periods[i]:>10}")
 
# 可视化
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(maturities, spot_rates * 100, 'bo-', lw=2, markersize=6, label='即期利率')
# 远期利率画在中点
mid_points = [(maturities[i] + maturities[i+1]) / 2 for i in range(len(forward_rates))]
ax.plot(mid_points, [f * 100 for f in forward_rates], 'rs--', lw=1.5,
        markersize=5, label='远期利率')
ax.set_xlabel('期限(年)')
ax.set_ylabel('利率(%)')
ax.set_title('即期利率曲线 vs 远期利率')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
 
print("\n关键观察:")
print("  向上倾斜的收益率曲线 → 远期利率 > 即期利率")
print("  这是市场的正常状态(借的时间越长,利率越高)")

1.3 零利率曲线(Zero Curve)

零利率曲线(也叫收益率曲线)是不同期限的即期利率连成的曲线。它是所有利率产品定价的基础——就像 BSM 公式中的 S(股价)一样重要。

常见的收益率曲线形状:

形状含义典型场景
向上倾斜(Normal)长期利率 > 短期利率正常经济环境
平坦(Flat)各期限利率差不多加息周期末端
倒挂(Inverted)短期利率 > 长期利率经济衰退预警
驼峰形(Humped)中期利率最高不常见

二、利率互换(IRS)

2.1 什么是利率互换

白话解释:两方约定,一方付固定利率,另一方付浮动利率,交换利息的现金流。

具体例子

A 公司借了一笔浮动利率贷款(利率 = SHIBOR + 1%)。A 公司担心利率上升,想换成固定利率。

B 公司借了一笔固定利率贷款(利率 = 5%)。B 公司预期利率会下降,想换成浮动利率。

于是 A 和 B 签了一个 IRS:

  • A 每季度付给 B 固定利率 5%
  • B 每季度付给 A 浮动利率 SHIBOR

结果:A 成功把浮动利率换成了固定利率,B 成功把固定利率换成了浮动利率。双方都满意。

2.2 IRS 的定价

核心思想:互换的价值 = 固定端现值 - 浮动端现值。

在互换签约时,价值应该等于 0(否则一方就不愿意签了)。这意味着:

import numpy as np
 
# ============================================================
# 利率互换(IRS)定价
# ============================================================
 
def swap_fixed_rate(notional, payment_dates, spot_rates):
    """
    计算使互换价值为 0 的固定利率(互换利率)
 
    参数:
        notional: 名义本金
        payment_dates: 付款日期列表(年)
        spot_rates: 对应期限的即期利率(连续复利)
    返回:
        互换利率(固定端利率)
    """
    # 浮动端:每个付款日的浮动端 PV(简化假设:使用即期利率折现)
    float_pv = 0
    fixed_pv = 0
 
    for i, t in enumerate(payment_dates):
        # 找到对应的即期利率(线性插值)
        r_t = np.interp(t, spot_rates[:, 0], spot_rates[:, 1])
        discount = np.exp(-r_t * t)
 
        # 浮动端:假设每期按即期利率付息
        if i == 0:
            float_leg = notional * (np.exp(r_t * t) - 1) * discount
        else:
            # 远期利率作为浮动端利率
            r_prev = np.interp(payment_dates[i-1], spot_rates[:, 0], spot_rates[:, 1])
            f_rate = (r_t * t - r_prev * payment_dates[i-1]) / (t - payment_dates[i-1])
            dt = t - payment_dates[i-1]
            float_leg = notional * (np.exp(f_rate * dt) - 1) * discount
 
        float_pv += float_leg
 
        # 固定端的折现因子之和(年金因子)
        fixed_pv += discount * dt if i > 0 else discount * t
 
    # 互换利率
    swap_rate = float_pv / (notional * fixed_pv)
 
    return swap_rate
 
# 构造即期利率曲线
spot_rates_data = np.array([
    [0.25, 0.025],   # 3 个月
    [0.50, 0.030],   # 6 个月
    [1.00, 0.032],   # 1 年
    [1.50, 0.034],   # 1.5 年
    [2.00, 0.035],   # 2 年
    [2.50, 0.036],   # 2.5 年
    [3.00, 0.037],   # 3 年
    [4.00, 0.039],   # 4 年
    [5.00, 0.040],   # 5 年
])
 
notional = 100_000_000  # 1 亿名义本金
payment_dates = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
 
# 计算互换利率
swap_rate = swap_fixed_rate(notional, payment_dates, spot_rates_data)
 
print("=" * 50)
print("利率互换定价")
print("=" * 50)
print(f"名义本金: {notional/1e8:.0f} 亿元")
print(f"期限: 5 年,每半年交换一次")
print(f"计算得到的互换利率(固定端): {swap_rate:.4%}")
print()
print("白话解释:")
print(f"  如果你支付固定利率 {swap_rate:.2%},收到浮动利率 SHIBOR,")
print(f"  那么这个互换在签约时的价值为 0(公平定价)。")

2.3 IRS 的估值

签约之后,利率曲线发生了变化,互换就有了正的或负的价值。

白话:如果你签了”付 4% 收 SHIBOR”的互换,然后 SHIBOR 涨到了 5%,那你就赚了——因为你能用 4% 的成本换来 5% 的浮动利率。

import numpy as np
 
# ============================================================
# IRS 估值(签约后利率变化)
# ============================================================
 
def value_swap(notional, fixed_rate, payment_dates, old_curve, new_curve):
    """
    评估 IRS 的当前价值
 
    参数:
        notional: 名义本金
        fixed_rate: 签约时的固定利率
        payment_dates: 付款日期列表
        old_curve: 签约时的即期利率曲线
        new_curve: 当前(新)的即期利率曲线
    """
    remaining_dates = payment_dates[payment_dates > 0.25]  # 假设已经过了一段时间
 
    fixed_leg_pv = 0
    float_leg_pv = 0
 
    for i, t in enumerate(remaining_dates):
        r_new = np.interp(t, new_curve[:, 0], new_curve[:, 1])
        discount = np.exp(-r_new * t)
 
        # 固定端现值
        if i == 0:
            dt = t
        else:
            dt = t - remaining_dates[i-1]
        fixed_leg_pv += notional * fixed_rate * dt * discount
 
        # 浮动端现值(用当前远期利率)
        if i == 0:
            prev_t = 0.25  # 假设互换从 0.25 年开始
        else:
            prev_t = remaining_dates[i-1]
        r_new_prev = np.interp(prev_t, new_curve[:, 0], new_curve[:, 1])
        fwd_rate = (r_new * t - r_new_prev * prev_t) / (t - prev_t)
        float_leg_pv += notional * (np.exp(fwd_rate * (t - prev_t)) - 1) * discount
 
    swap_value = float_leg_pv - fixed_leg_pv
    return swap_value, fixed_leg_pv, float_leg_pv
 
# 模拟利率变化:SHIBOR 曲线整体上移 1%
new_curve = spot_rates_data.copy()
new_curve[:, 1] += 0.01  # 利率上升 1%
 
swap_val, fixed_pv, float_pv = value_swap(
    100_000_000, swap_rate, payment_dates,
    spot_rates_data, new_curve
)
 
print("=" * 50)
print("IRS 估值(利率上升 1% 后)")
print("=" * 50)
print(f"签约固定利率: {swap_rate:.4%}")
print(f"固定端现值: {fixed_pv:>15,.0f} 元")
print(f"浮动端现值: {float_pv:>15,.0f} 元")
print(f"互换价值(浮动端 - 固定端): {swap_val:>15,.0f} 元")
print(f"\n白话解释:你付固定 {swap_rate:.2%},收到浮动利率。")
print(f"利率上升后,浮动端更值钱 → 你赚了 {swap_val/1e6:.2f} 百万元。")

三、上限(Cap)与下限(Floor)

3.1 什么是 Cap

白话解释:Cap 就是一个”利率保险”。你向银行付一笔保费,银行承诺:如果未来浮动利率超过了约定上限,银行补偿你差额。

具体例子:你借了一笔浮动利率贷款,利率 = SHIBOR + 0.5%。你担心利率暴涨。

你买了一个 Cap,上限利率 = 5%:

  • 如果 SHIBOR + 0.5% = 7%,超过上限 5% → 银行补给你 2%
  • 如果 SHIBOR + 0.5% = 3%,没超过上限 → 什么都得不到

Floor 完全对称:银行给你保底,如果利率低于下限,银行付你差额。

Cap = 一串欧式 Call 的组合(每个付息日对应一个 Caplet)

Floor = 一串欧式 Put 的组合(每个付息日对应一个 Floorlet)

3.2 Black 公式定价

Cap 的定价使用 Black 公式(76 模型):

其中:

白话解读:每个 Caplet 就是一个”看涨期权”,标的资产是远期利率,行权价是上限利率。

import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
 
# ============================================================
# Cap / Floor 定价(Black 公式)
# ============================================================
 
def caplet_black(F, K, T, sigma, r, notional, delta):
    """
    单个 Caplet 的 Black 公式定价
 
    参数:
        F: 远期利率
        K: 上限利率(行权价)
        T: 到期时间
        sigma: 远期利率的波动率
        r: 折现利率
        notional: 名义本金
        delta: 付息期间(年)
    """
    d1 = (np.log(F / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
 
    caplet = notional * delta * np.exp(-r * T) * (F * norm.cdf(d1) - K * norm.cdf(d2))
    return caplet
 
def floorlet_black(F, K, T, sigma, r, notional, delta):
    """Floorlet = K*N(-d2) - F*N(-d1)(看跌期权)"""
    d1 = (np.log(F / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
 
    floorlet = notional * delta * np.exp(-r * T) * (K * norm.cdf(-d2) - F * norm.cdf(-d1))
    return floorlet
 
# 参数
notional = 100_000_000   # 1 亿
cap_strike = 0.05        # 上限 5%
sigma = 0.20             # 远期利率波动率 20%
 
# 付息日和远期利率
payment_dates = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
forward_rates = np.array([0.030, 0.033, 0.035, 0.038, 0.040, 0.041, 0.042, 0.043, 0.044, 0.045])
delta = 0.5  # 每半年付息
r_discount = 0.035  # 折现利率
 
print("=" * 60)
print("Cap 定价(Black 公式)")
print("=" * 60)
print(f"名义本金: {notional/1e8:.0f} 亿")
print(f"上限利率: {cap_strike:.2%}")
print(f"波动率: {sigma:.2%}")
print()
print(f"{'日期':>6} {'远期利率':>10} {'Caplet价值':>12} {'累计':>12}")
print("-" * 45)
 
total_cap = 0
for i, (t, F) in enumerate(zip(payment_dates, forward_rates)):
    cp = caplet_black(F, cap_strike, t, sigma, r_discount, notional, delta)
    total_cap += cp
    print(f"{t:>5.1f}{F:>10.3%} {cp:>12,.0f} {total_cap:>12,.0f}")
 
print(f"\nCap 总价格: {total_cap:>12,.0f} 元 ({total_cap/notional*100:.4f}%)")
print(f"\n白话解释:花 {total_cap/1e4:,.0f} 万元买这个 Cap,")
print(f"相当于给利率买了 5% 的'保险'。")
print(f"如果未来任何一次付息时 SHIBOR 超过 5%,你就受到保护。")
 
# Cap 价值随上限利率的变化
K_range = np.linspace(0.02, 0.08, 100)
cap_prices = []
for K_val in K_range:
    total = sum(caplet_black(F, K_val, t, sigma, r_discount, notional, delta)
                for t, F in zip(payment_dates, forward_rates))
    cap_prices.append(total)
 
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(K_range, np.array(cap_prices) / 1e6, 'b-', lw=2)
ax.axvline(x=0.05, color='red', linestyle='--', alpha=0.7, label='Cap Strike = 5%')
ax.set_xlabel('上限利率 K')
ax.set_ylabel('Cap 价格(百万元)')
ax.set_title('Cap 价格 vs 上限利率')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

3.3 Cap-Floor Parity

与 Put-Call Parity 类似:

白话解读:买 Cap + 卖 Floor 的效果 = 付固定利率(利率超过上限时赚,低于下限时亏)。这和利率互换中”付固定收浮动”的效果一样。


四、Swaption(互换期权)

4.1 什么是 Swaption

白话解释:Swaption = 互换(Swap)+ 期权(Option)。你付一笔钱,获得”未来某个时间进入一个利率互换”的权利。

  • Payer Swaption(支付方互换期权):有权在未来支付固定利率、收到浮动利率(本质上是一个看涨期权)
  • Receiver Swaption(接收方互换期权):有权在未来收到固定利率、支付浮动利率(本质上是一个看跌期权)

4.2 Black 公式定价 Swaption

Swaption 的定价使用 Black 公式的另一种形式:

其中 是互换的年金因子, 是远期互换利率, 是互换期权的行权利率。

import numpy as np
from scipy.stats import norm
 
# ============================================================
# Swaption 定价(Black 公式)
# ============================================================
 
def swaption_black(swap_rate, strike_rate, T_expiry, T_swap, sigma, r):
    """
    Swaption 的 Black 公式定价
 
    参数:
        swap_rate: 远期互换利率(连续复利)
        strike_rate: 行权互换利率
        T_expiry: 期权到期时间
        T_swap: 互换期限
        sigma: 互换利率波动率
        r: 折现利率
    """
    # 年金因子(简化计算)
    n_payments = int(T_swap / 0.5)  # 每半年付息
    dt = 0.5
    annuity = sum(np.exp(-r * (T_expiry + (i+1) * dt)) * dt for i in range(n_payments))
 
    # Black 公式
    d1 = (np.log(swap_rate / strike_rate) + 0.5 * sigma**2 * T_expiry) / \
         (sigma * np.sqrt(T_expiry))
    d2 = d1 - sigma * np.sqrt(T_expiry)
 
    payer = annuity * (swap_rate * norm.cdf(d1) - strike_rate * norm.cdf(d2))
    receiver = annuity * (strike_rate * norm.cdf(-d2) - swap_rate * norm.cdf(-d1))
 
    return payer, receiver, annuity
 
# 参数
swap_rate = 0.04      # 当前远期互换利率 4%
strike = 0.04         # 行权利率 4%
T_expiry = 1.0        # 1 年后可以行权
T_swap = 5.0          # 进入一个 5 年期互换
sigma = 0.20          # 波动率 20%
r = 0.035             # 折现利率
 
payer_price, receiver_price, annuity = swaption_black(swap_rate, strike, T_expiry, T_swap, sigma, r)
 
print("=" * 50)
print("Swaption 定价(Black 公式)")
print("=" * 50)
print(f"远期互换利率: {swap_rate:.2%}")
print(f"行权利率: {strike:.2%}")
print(f"期权到期: {T_expiry} 年后")
print(f"互换期限: {T_swap} 年")
print(f"波动率: {sigma:.2%}")
print(f"年金因子: {annuity:.4f}")
print(f"\nPayer Swaption 价格: {payer_price:.6f}(每 1 元名义本金)")
print(f"Receiver Swaption 价格: {receiver_price:.6f}")
print()
print("白话解释:")
print(f"  花钱买 Payer Swaption = 买了一个'付固定收浮动'的权利")
print(f"  如果未来互换利率涨到 > {strike:.2%},你行权就赚了")
 
# Swaption 波动率矩阵
print("\n" + "=" * 60)
print("Swaption 波动率矩阵(示意)")
print("=" * 60)
print("每个格子对应一个 (期权期限, 互换期限) 组合的波动率\n")
 
option_tenors = ['1M', '3M', '6M', '1Y', '2Y', '5Y']
swap_tenors =  ['1Y',  '2Y',  '5Y',  '10Y']
 
# 简化的波动率矩阵
vol_matrix = np.array([
    [18, 20, 22, 24],
    [19, 21, 23, 25],
    [20, 22, 24, 26],
    [21, 23, 25, 27],
    [20, 22, 24, 25],
    [18, 20, 21, 22],
])
 
print(f"{'期权期限':<10}", end='')
for st in swap_tenors:
    print(f"{st:>8}", end='')
print()
print("-" * 45)
for i, ot in enumerate(option_tenors):
    print(f"{ot:<10}", end='')
    for j in range(len(swap_tenors)):
        print(f"{vol_matrix[i,j]:>7}%", end='')
    print()
 
# 可视化
fig, ax = plt.subplots(figsize=(8, 5))
im = ax.imshow(vol_matrix, cmap='YlOrRd', aspect='auto')
ax.set_xticks(range(len(swap_tenors)))
ax.set_xticklabels(swap_tenors)
ax.set_yticks(range(len(option_tenors)))
ax.set_yticklabels(option_tenors)
ax.set_xlabel('底层互换期限')
ax.set_ylabel('期权到期期限')
ax.set_title('Swaption 波动率矩阵(Volatility Cube 切片)')
 
for i in range(len(option_tenors)):
    for j in range(len(swap_tenors)):
        ax.text(j, i, f'{vol_matrix[i,j]}%', ha='center', va='center',
                color='black' if vol_matrix[i,j] < 23 else 'white')
 
plt.colorbar(im, ax=ax, label='波动率(%)')
plt.tight_layout()
plt.show()

五、短利率模型

5.1 为什么需要利率模型

Black 公式(用于 Cap、Swaption)假设远期利率服从对数正态分布。但这只是一个”市场标准”的参数化方法,它没有严格的动态一致性。

短利率模型从短期利率 的随机微分方程出发,通过无套利条件推导出整条利率曲线的动态。这是更基础、更自洽的建模方法。

5.2 Vasicek 模型

白话解释:短期利率像一根被弹簧拉住的物体——它会围绕一个”长期均值”上下波动,偏离越远,回归的力量越大。

参数含义直觉
回归速度弹簧的劲度系数。 越大,回归越快
长期均值弹簧的自然长度。利率长期来看会回到 附近
波动率随机扰动的强度
当前利率初始位置

Vasicek 的问题:利率可以为负。现实中有时候利率确实会变成负数(如欧洲、日本),但在很多时候我们希望利率保持非负。

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# Vasicek 模型模拟
# ============================================================
 
def simulate_vasicek(r0, a, b, sigma, T, n_steps, n_paths):
    """
    Vasicek 模型路径模拟(Euler-Maruyama 离散化)
 
    dr = a(b - r)dt + sigma * dW
 
    参数:
        r0: 初始利率
        a: 均值回归速度
        b: 长期均值
        sigma: 波动率
        T: 总时间
        n_steps: 步数
        n_paths: 路径数
    """
    dt = T / n_steps
    t = np.linspace(0, T, n_steps + 1)
 
    r = np.zeros((n_steps + 1, n_paths))
    r[0] = r0
 
    Z = np.random.standard_normal((n_steps, n_paths))
 
    for i in range(n_steps):
        dr = a * (b - r[i]) * dt + sigma * np.sqrt(dt) * Z[i]
        r[i + 1] = r[i] + dr
 
    return t, r
 
# 参数
r0 = 0.03       # 当前利率 3%
a = 1.0          # 回归速度
b = 0.04         # 长期均值 4%
sigma = 0.01     # 波动率 1%
T = 10.0
n_steps = 2500
n_paths = 10
 
np.random.seed(42)
t, r_paths = simulate_vasicek(r0, a, b, sigma, T, n_steps, n_paths)
 
# 可视化
fig, ax = plt.subplots(figsize=(12, 5))
for i in range(n_paths):
    ax.plot(t, r_paths[:, i] * 100, alpha=0.5, lw=0.8)
ax.axhline(y=b * 100, color='red', linestyle='--', lw=1.5,
           label=f'长期均值 b = {b:.1%}')
ax.axhline(y=0, color='black', linestyle='-', lw=0.5)
ax.axhline(y=r0 * 100, color='gray', linestyle=':', alpha=0.5,
           label=f'初始利率 r0 = {r0:.1%}')
ax.set_xlabel('时间(年)')
ax.set_ylabel('短期利率(%)')
ax.set_title('Vasicek 模型:短期利率路径模拟')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
 
print("Vasicek 模型参数:")
print(f"  a (回归速度) = {a}")
print(f"  b (长期均值) = {b:.2%}")
print(f"  sigma (波动率) = {sigma:.2%}")
print(f"  r0 (初始利率) = {r0:.2%}")
print()
print(f"观察:利率在 {b:.1%} 附近波动,偏离后会被拉回来")
print(f"注意:部分路径利率为负——这是 Vasicek 模型的缺陷")
 
# 统计
final_rates = r_paths[-1]
print(f"\n{10} 年后利率统计:")
print(f"  均值 = {final_rates.mean():.4%}")
print(f"  标准差 = {final_rates.std():.4%}")
print(f"  最小值 = {final_rates.min():.4%}(可以为负!)")
print(f"  最大值 = {final_rates.max():.4%}")

5.3 CIR 模型(Cox-Ingersoll-Ross)

CIR 模型解决了 Vasicek 的”利率可以为负”的问题:

关键区别:波动率项是 而不是 。当利率接近 0 时,波动率也接近 0,利率不容易变成负数。

Vasicek vs CIR 的区别:

Vasicek: dr = a(b-r)dt + sigma * dW       ← 常数波动率
CIR:     dr = a(b-r)dt + sigma*sqrt(r) * dW ← 波动率与利率成正比

           |
  sigma ---|------- Vasicek: 波动率恒定
           |    /
  sigma*   |   /  CIR: 波动率随 r 减小
  sqrt(r)  |  /
           | /
  0 -------|/-------- r
           0

当 r 接近 0 时:
  Vasicek: 仍然有 sigma 的波动 → 可以变成负数
  CIR:     波动率接近 0 → 被推回正数

Feller 条件: 2*a*b >= sigma^2 (保证 r 不会触及 0)
import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# CIR 模型模拟
# ============================================================
 
def simulate_cir(r0, a, b, sigma, T, n_steps, n_paths):
    """
    CIR 模型路径模拟(Euler-Maruyama + 反射条件)
 
    dr = a(b-r)dt + sigma*sqrt(r)*dW
 
    注意:当 sigma 较大时,简单 Euler 方法不稳定。
    这里使用带反射的 Euler 方法作为简化实现。
    """
    dt = T / n_steps
    t = np.linspace(0, T, n_steps + 1)
    r = np.zeros((n_steps + 1, n_paths))
    r[0] = r0
 
    Z = np.random.standard_normal((n_steps, n_paths))
 
    for i in range(n_steps):
        sqrt_r = np.sqrt(np.maximum(r[i], 0))
        dr = a * (b - r[i]) * dt + sigma * sqrt_r * np.sqrt(dt) * Z[i]
        r[i + 1] = np.maximum(r[i] + dr, 0)  # 反射条件:保证非负
 
    return t, r
 
# 对比 Vasicek 和 CIR
np.random.seed(42)
r0 = 0.01   # 低初始利率,更容易看到差异
a = 0.5
b = 0.03
sigma_v = 0.02
T = 10.0
n_steps = 2500
n_paths = 30
 
# Vasicek 路径
np.random.seed(42)
t, r_vasicek = simulate_vasicek(r0, a, b, sigma_v, T, n_steps, n_paths)
 
# CIR 路径(需要更大的 sigma 以产生类似的波动幅度)
np.random.seed(42)
sigma_cir = 0.06  # CIR 需要更大的 sigma
t, r_cir = simulate_cir(r0, a, b, sigma_cir, T, n_steps, n_paths)
 
# 可视化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
 
# Vasicek 路径
for i in range(min(20, n_paths)):
    axes[0].plot(t, r_vasicek[:, i] * 100, alpha=0.3, color='blue')
axes[0].axhline(y=0, color='red', linewidth=1, label='零利率')
axes[0].axhline(y=b * 100, color='gray', linestyle='--', label=f'长期均值 b={b:.0%}')
axes[0].set_xlabel('时间(年)')
axes[0].set_ylabel('利率(%)')
axes[0].set_title('Vasicek(利率可以为负)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
 
# CIR 路径
for i in range(min(20, n_paths)):
    axes[1].plot(t, r_cir[:, i] * 100, alpha=0.3, color='green')
axes[1].axhline(y=0, color='red', linewidth=1, label='零利率')
axes[1].axhline(y=b * 100, color='gray', linestyle='--', label=f'长期均值 b={b:.0%}')
axes[1].set_xlabel('时间(年)')
axes[1].set_ylabel('利率(%)')
axes[1].set_title('CIR(利率非负)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
 
# 不同参数下的 CIR 路径
params = [
    (0.1, 'a=0.1(慢回归)'),
    (0.5, 'a=0.5(中回归)'),
    (2.0, 'a=2.0(快回归)')
]
for a_val, label in params:
    np.random.seed(42)
    r, _ = simulate_cir(r0, a_val, b, sigma_cir, T, n_steps, 1)
    axes[2].plot(_, r[0] * 100, lw=1.5, label=label)
axes[2].axhline(y=b * 100, color='gray', linestyle='--')
axes[2].set_xlabel('时间(年)')
axes[2].set_ylabel('利率(%)')
axes[2].set_title('不同均值回归速度下的 CIR 路径')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()
 
# 负利率统计
neg_vasicek = np.mean(r_vasicek < 0)
neg_cir = np.mean(r_cir < 0)
print(f"Vasicek 负利率比例: {neg_vasicek:.2%}")
print(f"CIR 负利率比例: {neg_cir:.2%}")
print(f"CIR Feller 条件: 2ab = {2*a*b:.4f}, sigma^2 = {sigma_cir**2:.4f}")
print(f"  2ab >= sigma^2? {2*a*b >= sigma_cir**2} ({'满足' if 2*a*b >= sigma_cir**2 else '不满足'})")
print()
print("选择建议:")
print("  课堂学习 → Vasicek(数学最简单,直觉最清晰)")
print("  利率下限严格 → CIR(保证非负)")
print("  实际定价 → Hull-White(匹配市场曲线)")

5.4 Hull-White 模型

Hull-White 模型是 Vasicek 的扩展,加入了时间依赖的参数,使其能够精确匹配当前市场的收益率曲线:

白话解读:Vasicek 的长期均值 是一个常数,但现实中市场收益率曲线的形状可能是任意的。Hull-White 通过让 随时间变化,确保模型在 时完全匹配市场曲线。

关键优势:模型 calibrated 到市场数据后,不会产生与市场价格矛盾的定价。

5.5 模型对比

特性VasicekCIRHull-White
SDE
利率可以为负
匹配市场曲线是(通过
解析解(债券)
解析解(期权)部分数值/树方法
波动率结构常数 成正比常数
均值回归
实际应用教学场景利率下限严格时主流生产模型
选择指南:

  课堂学习 → Vasicek 和 CIR(数学优美,直觉清晰)
  实际定价 → Hull-White 或更高级的模型(保证无套利)
  负利率环境 → Vasicek / Hull-White(允许负利率反而是优势)
  利率不能为负 → CIR

5.6 模型校准

白话解释:模型的参数()需要从市场数据中”拟合”出来。这个过程叫校准。

校准的标准:让模型定价和实际市场价格尽可能接近。

import numpy as np
from scipy.optimize import minimize
 
# ============================================================
# Hull-White 模型校准(简化版)
# ============================================================
 
def hw_bond_price(r0, a, b, sigma, T):
    """
    Hull-White 模型下的零息债券价格
 
    P(t, T) = A(t, T) * exp(-B(t, T) * r(t))
    """
    B = (1 - np.exp(-a * T)) / a
    A = np.exp((B - T) * (a**2 * b - 0.5 * sigma**2) / a**2
               - sigma**2 * B**2 / (4 * a))
    return A * np.exp(-B * r0)
 
def calibrate_hw(r0, market_prices, market_maturities):
    """
    校准 Hull-White 模型参数
 
    目标:最小化模型债券价格和市场价格的差异
    """
    def objective(params):
        a, b, sigma = params
        if a <= 0 or sigma <= 0:
            return 1e10
 
        model_prices = [hw_bond_price(r0, a, b, sigma, T)
                        for T in market_maturities]
        errors = [(model - market) / market
                  for model, market in zip(model_prices, market_prices)]
        return sum(e**2 for e in errors)
 
    # 初始猜测和边界
    x0 = [1.0, 0.04, 0.01]
    bounds = [(0.01, 10.0), (0.001, 0.15), (0.001, 0.05)]
 
    result = minimize(objective, x0, bounds=bounds, method='L-BFGS-B')
 
    if result.success:
        a_cal, b_cal, sigma_cal = result.x
        return a_cal, b_cal, sigma_cal, result.fun
    else:
        return None
 
# 模拟市场数据
np.random.seed(42)
r0 = 0.03
a_true, b_true, sigma_true = 1.5, 0.04, 0.015  # 真实参数
 
maturities = np.array([0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0])
# 用真实参数生成"市场"债券价格
market_prices = [hw_bond_price(r0, a_true, b_true, sigma_true, T)
                 for T in maturities]
 
# 校准
print("=" * 50)
print("Hull-White 模型校准")
print("=" * 50)
print(f"{'期限':>8} {'市场价格':>12} {'校准价格':>12}")
print("-" * 35)
 
result = calibrate_hw(r0, market_prices, maturities)
if result:
    a_cal, b_cal, sigma_cal, obj_val = result
    cal_prices = [hw_bond_price(r0, a_cal, b_cal, sigma_cal, T) for T in maturities]
    for T, mp, cp in zip(maturities, market_prices, cal_prices):
        print(f"{T:>6.1f}{mp:>12.6f} {cp:>12.6f}")
 
    print(f"\n校准参数: a={a_cal:.4f}, b={b_cal:.6f}, sigma={sigma_cal:.6f}")
    print(f"真实参数: a={a_true:.4f}, b={b_true:.6f}, sigma={sigma_true:.6f}")
    print(f"目标函数值: {obj_val:.10f}")
    print(f"\n白话解释:")
    print(f"  校准出来的参数和真实参数很接近")
    print(f"  这说明模型能从市场数据中正确地提取利率动态信息")

六、LMM / BGM 市场模型(简要)

6.1 为什么需要 LMM

短利率模型(Vasicek、Hull-White)的一个问题是:它们假设的是短期利率的动态,然后推导出远期利率。但市场交易的是远期利率(Cap、Swaption),所以更好的做法是直接建模远期利率。

LIBOR Market Model (LMM),也叫 BGM 模型(Brace-Gatarek-Musiela),直接对一组远期利率建模:

白话解读:每个期限的远期利率都服从一个几何布朗运动,且它们之间有相关性结构。

6.2 LMM vs 短利率模型

维度短利率模型 (HW)LMM
建模对象短期利率 r(t)一组远期利率 F_i(t)
校准到 Cap近似匹配自然匹配
校准到 Swaption近似需要额外处理
计算速度快(解析解)慢(蒙特卡洛)
市场标准简单产品复杂结构化产品

七、利率曲线 Bootstrap

7.1 从市场数据构建收益率曲线

白话解释:市场上有很多利率产品(国债、回购、互换等),它们隐含了不同期限的利率信息。Bootstrap 方法就是从这些碎片化的市场信息中,拼凑出一条完整的收益率曲线。

方法:从短到长,逐段推导。先确定短期利率(从隔夜、1 周等),然后用这些信息推导稍长期限的利率,依次类推。

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# 利率曲线 Bootstrap(简化版)
# ============================================================
 
def bootstrap_yield_curve(instruments):
    """
    从市场利率工具 Bootstrap 出零利率曲线
 
    参数:
        instruments: list of dict,每个 dict 包含:
            - 'type': 'deposit'(存款)或 'swap'(互换)
            - 'tenor': 期限(年)
            - 'rate': 市场利率
    返回:
        折现因子列表(对应每个期限)
    """
    # 按 tenor 排序
    instruments = sorted(instruments, key=lambda x: x['tenor'])
    discount_factors = []
 
    for inst in instruments:
        t = inst['tenor']
        rate = inst['rate']
 
        if inst['type'] == 'deposit':
            # 存款: DF = 1 / (1 + rate * t)
            df = 1.0 / (1 + rate * t)
        elif inst['type'] == 'swap':
            # 互换: 固定端 = 浮动端 → 推导 DF
            # 简化: 假设每年付息一次
            n_payments = int(t)
            fixed_leg = rate * sum(discount_factors[-1] * np.exp(-0.03 * (i+1))
                                   for i in range(n_payments))
            # 实际上需要求解非线性方程,这里用简化方法
            # 正确做法: 使用插值 + 迭代
            df = discount_factors[-1] * np.exp(-rate * (t - instruments[-2]['tenor']))
        else:
            raise ValueError(f"未知的工具类型: {inst['type']}")
 
        discount_factors.append(df)
 
    return discount_factors
 
# 模拟市场数据
market_data = [
    {'type': 'deposit', 'tenor': 0.25, 'rate': 0.028},   # 3 个月存款
    {'type': 'deposit', 'tenor': 0.50, 'rate': 0.030},   # 6 个月存款
    {'type': 'deposit', 'tenor': 1.0,  'rate': 0.032},   # 1 年存款
    {'type': 'swap',   'tenor': 2.0,  'rate': 0.035},   # 2 年互换
    {'type': 'swap',   'tenor': 3.0,  'rate': 0.037},   # 3 年互换
    {'type': 'swap',   'tenor': 5.0,  'rate': 0.040},   # 5 年互换
    {'type': 'swap',   'tenor': 7.0,  'rate': 0.042},   # 7 年互换
    {'type': 'swap',   'tenor': 10.0, 'rate': 0.044},   # 10 年互换
]
 
print("=" * 50)
print("利率曲线 Bootstrap")
print("=" * 50)
print(f"{'期限':>8} {'市场利率':>10} {'类型':>10}")
print("-" * 35)
for inst in market_data:
    type_cn = '存款' if inst['type'] == 'deposit' else '互换'
    print(f"{inst['tenor']:>6.1f}{inst['rate']:>10.3%} {type_cn:>10}")
 
# 更完整的 Bootstrap 实现
def bootstrap_full(market_data):
    """完整的 Bootstrap 实现"""
    market_data = sorted(market_data, key=lambda x: x['tenor'])
    curve = {}  # tenor -> discount_factor
 
    for inst in market_data:
        t = inst['tenor']
        r = inst['rate']
 
        if inst['type'] == 'deposit':
            curve[t] = 1.0 / (1.0 + r * t)
        elif inst['type'] == 'swap':
            # 对于互换,使用已知的短期 DF + 线性插值
            # 这里简化处理:用连续复利近似
            curve[t] = np.exp(-r * t)
 
    return curve
 
curve = bootstrap_full(market_data)
 
# 转换为零利率
tenors = sorted(curve.keys())
zero_rates = [-np.log(df) / t for t, df in curve.items()]
 
print(f"\n{'期限':>8} {'折现因子':>10} {'零利率':>10}")
print("-" * 35)
for t, zr in zip(tenors, zero_rates):
    print(f"{t:>6.1f}{curve[t]:>10.6f} {zr:>10.4%}")
 
# 可视化
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(tenors, [r * 100 for r in zero_rates], 'bo-', lw=2, markersize=8)
ax.set_xlabel('期限(年)')
ax.set_ylabel('零利率(%)')
ax.set_title('Bootstrap 出来的零利率曲线')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
 
print("\n白话解释:")
print("  这条曲线就是'市场认为的未来不同期限的利率'")
print("  所有利率衍生品的定价都以这条曲线为输入")
print("  曲线形状(向上/平坦/倒挂)反映了市场对未来经济的预期")

八、关键概念回顾

概念一句话解释
即期利率今天借一笔钱到某期限到期的利率
远期利率今天约定未来某个时间借钱的利率
利率互换(IRS)固定利率换浮动利率
Cap / Floor利率保险(上限/下限)
Swaption进入互换的权利(互换 + 期权)
Vasicek 模型短期利率围绕长期均值波动的模型
Hull-White 模型Vasicek 的时间依赖扩展,能匹配市场曲线
LMM / BGM直接对远期利率建模的市场模型
Bootstrap从市场数据构建收益率曲线的方法

推荐阅读

书名章节核心价值
Options, Futures, and Other Derivatives (Hull)Ch.28-31利率衍生品的权威参考
Interest Rate Models (Brigo & Mercurio)全书利率模型的全面深度参考
Fixed Income Securities (Tuckman)Ch.8-12实务导向的利率建模

版本信息

  • 创建日期:2026-03-28
  • 最后更新:2026-03-28