收益率曲线构建全流程(中国市场实例)¶

本 Notebook 演示如何按照业界最佳实践标准,通过 Python 从零构建收益率曲线。

学习目标:

  • 理解自举(Bootstrapping)过程;
  • 区分单曲线与多曲线逻辑(OIS vs Shibor);
  • 掌握插值、平滑、以及参数化模型(NSS / Smith–Wilson);
  • 进行无套利检测与 DV01 敏感度分析。

📦 1. 准备环境与数据¶

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d, CubicSpline
from scipy.optimize import curve_fit
plt.style.use('seaborn-v0_8')

数据结构说明¶

模拟中国银行间市场利率:

  • 存款/FR007–OIS: 短端;
  • Shibor–IRS: 中长期;
  • 国债收益率: 长端验证。
In [2]:
deposits = pd.DataFrame({
    'Tenor':[7/365,1/12,0.25],
    'Rate':[0.018,0.019,0.021]
})
swaps = pd.DataFrame({
    'Tenor':[1,2,3],
    'SwapRate':[0.024,0.0265,0.029]
})
ois_quotes = pd.DataFrame({
    'Tenor':[0.5,1,2],
    'Rate':[0.019,0.020,0.021]
})
govbond = pd.DataFrame({
    "Instrument":["GovBond 5Y"],
    "Tenor":[5.0],
    "Rate":[0.025],
    "Category":["GovBond"]
})
deposits, swaps, ois_quotes, govbond
Out[2]:
(      Tenor   Rate
 0  0.019178  0.018
 1  0.083333  0.019
 2  0.250000  0.021,
    Tenor  SwapRate
 0      1    0.0240
 1      2    0.0265
 2      3    0.0290,
    Tenor   Rate
 0    0.5  0.019
 1    1.0  0.020
 2    2.0  0.021,
    Instrument  Tenor   Rate Category
 0  GovBond 5Y    5.0  0.025  GovBond)
In [3]:
deposits_df = deposits.copy()
deposits_df["Instrument"] = ["Deposit 7D","Deposit 1M","Shibor 3M"]
deposits_df["Category"] = ["Deposit","Deposit","Shibor"]

swaps_df = swaps.rename(columns={"SwapRate":"Rate"})
swaps_df["Instrument"] = ["IRS 1Y","IRS 2Y","IRS 3Y"]
swaps_df["Category"] = "IRS"

ois_df = ois_quotes.copy()
ois_df["Instrument"] = ["OIS 6M","OIS 1Y","OIS 2Y"]
ois_df["Category"] = "OIS"

govbond_df = govbond.copy()
govbond_df["Instrument"] ="GovBond 5Y",
govbond_df["Category"] = "GovBond"

# govbond_df = pd.DataFrame({
#     "Instrument":["GovBond 5Y"],
#     "Tenor":[5.0],
#     "Rate":[0.025],
#     "Category":["GovBond"]
# })

# 合并
market = pd.concat([deposits_df, ois_df, swaps_df, govbond_df], ignore_index=True)
market
Out[3]:
Tenor Rate Instrument Category
0 0.019178 0.0180 Deposit 7D Deposit
1 0.083333 0.0190 Deposit 1M Deposit
2 0.250000 0.0210 Shibor 3M Shibor
3 0.500000 0.0190 OIS 6M OIS
4 1.000000 0.0200 OIS 1Y OIS
5 2.000000 0.0210 OIS 2Y OIS
6 1.000000 0.0240 IRS 1Y IRS
7 2.000000 0.0265 IRS 2Y IRS
8 3.000000 0.0290 IRS 3Y IRS
9 5.000000 0.0250 GovBond 5Y GovBond
In [4]:
# 正确显示中文字符
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['SimHei']      # 使用黑体(SimHei)显示中文
matplotlib.rcParams['axes.unicode_minus'] = False        # 解决负号“‑”显示为方块问题
plt.figure(figsize=(7,4))
for cat,grp in market.groupby('Category'):
    plt.scatter(grp['Tenor'], grp['Rate'], label=cat)
plt.legend();plt.xlabel('期限(年)');plt.ylabel('年化利率(%)');plt.title('中国市场主要利率数据(示例)');
No description has been provided for this image

💬 练习思考:

  • 你能区分这些利率的资金来源吗?
  • 哪些可以看作“风险更低”的贴现基准?

🧩 2. 单曲线自举 (Shibor 3M)¶

根据短端存款和 Shibor 互换利率平价公式: $$ K \sum_{i=1}^{N} \alpha_i P(0,T_i) = 1 - P(0,T_N) $$ 其中 $\alpha$ 为付息间隔 (0.25)。

In [5]:
# Step1: 短端贴现因子
deposits['DF'] = 1 / (1 + deposits['Rate'] * deposits['Tenor'])
P = dict(zip(deposits['Tenor'], deposits['DF']))
P
Out[5]:
{0.019178082191780823: 0.999654913646248,
 0.08333333333333333: 0.9984191696480573,
 0.25: 0.9947774185525988}
In [6]:
# Step2: 逐步自举 IRS 合约贴现因子
def bootstrap_swaps(P, swaps, alpha=0.25):
    for _, row in swaps.iterrows():
        K, Tn = row['SwapRate'], row['Tenor']
        N = int(Tn / alpha)
        known = sum(alpha * P[t] for t in P if t < Tn)
        P[Tn] = (1 - K * known) / (1 + K * alpha)
    return P

P = bootstrap_swaps(P, swaps)
sorted(P.items())
Out[6]:
[(0.019178082191780823, 0.999654913646248),
 (0.08333333333333333, 0.9984191696480573),
 (0.25, 0.9947774185525988),
 (1.0, 0.9761857763309331),
 (2.0, 0.9672967868193934),
 (3.0, 0.9572713606639565)]
In [7]:
# Step3: 即期利率计算与绘图
T = np.array(sorted(P.keys()))
R = -np.log(np.array(list(P.values())))/T
# R = -np.log(np.array([P[t] for t in T])) / T
plt.plot(T, R*100, 'o-', label='Shibor 单曲线')
plt.xlabel('期限年'); plt.ylabel('即期利率(%)'); plt.legend(); plt.grid(True)
plt.title('单曲线自举结果'); plt.show()
No description has been provided for this image

💬 问题
若将 IRS 报价 2.90 % 改为 3.10 %,曲线斜率将如何变化?为什么?

🌊 3. 多曲线体系 (OIS 贴现 + Shibor 预测)¶

现代框架:

  • 折现曲线基于 OIS (FR007);
  • 预测曲线基于 Shibor 浮息。
In [8]:
# OIS贴现曲线
P_ois = {T:1/(1+R*T) for T,R in zip(ois_quotes['Tenor'], ois_quotes['Rate'])}
P_ois
Out[8]:
{0.5: 0.9905894006934125, 1.0: 0.9803921568627451, 2.0: 0.9596928982725528}
In [9]:
# Shibor预测曲线 (简化迭代法)
alpha = 0.25
P_d = P_ois.copy()
for T,K in zip(swaps['Tenor'], swaps['SwapRate']):
    N = int(T/alpha)
    known = sum(alpha*P_d.get(round(i*alpha,2),1) for i in range(1,N))
    P_d[T] = (1 - K*known)/(1+K*alpha)

T_nodes = np.array(sorted(P_d.keys()))
L = (1/np.array(list(P_d.values())) - 1)/T_nodes
plt.plot(T_nodes, L*100, 's--', label='Shibor预测曲线')
plt.plot(ois_quotes['Tenor'], ois_quotes['Rate']*100, 'o-', label='OIS贴现曲线')
plt.xlabel('期限(年)'); plt.ylabel('利率(%)'); plt.legend(); plt.title('多曲线结构对比'); plt.show()
No description has been provided for this image

💬 对比分析

  • 哪一种估值更“风险中性”?
  • 如果互换被抵押时,应采用哪条曲线贴现?

🔧 4. 插值与平滑 (Hagan–West / Cubic / ln P)¶

比较不同插值方法下的曲线平滑与无套利特性。

方法一:ln P 线性插值¶

💡 思想¶

贴现因子 $P(0,T)$ 代表 1 元资金未来的折现。
若假设在相邻期限间的瞬时远期率 $f(t) = -\frac{d \ln P(t)}{dt}$ 恒定,即可得到: $$ \ln P(0,T) = \ln P(0,T_i) + \frac{T - T_i}{T_{i+1} - T_i} [\ln P(0,T_{i+1}) - \ln P(0,T_i)] $$

🔑 关键公式¶

  • 在区间 $[T_i, T_{i+1}]$:
$$ f(t) = \text{常数} = \frac{-\ln P(0,T_{i+1}) + \ln P(0,T_i)}{T_{i+1}-T_i} $$
  • 相应的贴现因子:
$$ P(0,T) = P(0,T_i) \exp[-f(t)\,(T-T_i)] $$

✅ 特点¶

  • 保证 $P(T)$ 单调递减 → 无套利;
  • 简单、快速、常用于 OIS 曲线。
  • 缺点是远期率不连续,曲线形状略显“折线感”。

方法二:Cubic Spline(三次样条插值)¶

💡 思想¶

用分段三次多项式确保一次、二次导数连续,得到平滑曲线。
通常对即期率 $R(T)$ 插值:

$$ R(T) = a_i + b_i(T - T_i) + c_i(T - T_i)^2 + d_i(T - T_i)^3, \quad T \in [T_i, T_{i+1}] $$

通过端点 $R(T_i), R(T_{i+1})$ 及导数连续性条件求得系数。

对应贴现因子为: $$ P(0,T) = \exp(-R(T) \, T) $$

⚠️ 风险¶

  • 尽管 $R(T)$ 光滑,但 $f(T)$(远期率)可能出现负值 → 可能违反无套利条件。
  • 实践中通常对 $\ln P$ 而非 $R$ 做 Spline,可减少风险。

方法三:Hagan–West 无套利平滑¶

该方法由 Hagan 和 West 在 2006 年《Interpolation Methods for Curve Construction》中提出,
目的就是在保持平滑性同时防止出现负远期率。
目前 ISDA、EIOPA 等监管标准均推荐使用这一类思路。

💡 思想核心¶

对每个区间 ([T_i, T_{i+1}]),令瞬时远期率 $f(t)$ 采用三次多项式形式:
保证在两端匹配前一段和后一段的远期率及导数,且约束不为负。

$$ f(t) = a_i + b_i (t-T_i) + c_i (t-T_i)^2 + d_i (t-T_i)^3 $$

再由定义 $P(0,T) = \exp[-\int_0^T f(u)\,du]$ 计算贴现因子。

通过边界条件 $f(T_i)=f_i, \ f'(T_i)=f'_i$ 得到系数。
若 $f(t)<0$ 出现,需调整导数估计使其回归非负(即“monotone preserving cubic interpolation”,单调保持样条)。

In [10]:
# 数据
T2 = np.array([0.25, 0.5, 1, 2, 3, 5])
R2 = np.array([0.021, 0.022, 0.024, 0.0265, 0.029, 0.025])

# lnP插值
lnP = -R2*T2
f_lnP = interp1d(T2, lnP, kind='linear', fill_value='extrapolate')

# Cubic Spline
cs = CubicSpline(T2, R2, bc_type='natural')

# 绘制对比
grid = np.linspace(0.1,5,100)
plt.plot(T2, R2*100, 'o', label='样本点')
plt.plot(grid, -f_lnP(grid)/grid*100, '--', label='lnP线性')
plt.plot(grid, cs(grid)*100, '-', label='Cubic Spline')
plt.xlabel('期限(年)'); plt.ylabel('利率%'); plt.legend(); plt.title('无套利插值比较'); plt.show()
No description has been provided for this image

💬 思考:

  • 哪种插值形态的远端趋向最合理?
  • 如果国债 10Y 利率为 2.8 %,哪种方法更容易外推?

🧠 5. 参数化建模 (NSS 与 Smith–Wilson)¶

Nelson–Siegel (NS) 模型概念¶

💡思想¶

NS 模型用三个参数化「因子」描述整个期限结构:

  • $\beta_0$:长期利率(Level);
  • $\beta_1$:短期与长期的差(Slope);
  • $\beta_2$:曲率(Curvature);
  • $\tau_1$:决定峰值所在期限的“衰减常数”。

🧮 公式¶

即期利率 $R(t)$: $$ R(t) = \beta_0 + \beta_1 \frac{1 - e^{-t / \tau_1}}{t / \tau_1} + \beta_2\left[\frac{1 - e^{-t / \tau_1}}{t / \tau_1} - e^{-t / \tau_1}\right] $$

或称 Nelson–Siegel 3-parameter form。

📈 特性¶

  • 当 $t \to 0$:$R(0) = \beta_0 + \beta_1$
  • 当 $t \to \infty$:$R(\infty) = \beta_0$
  • $\beta_2$ 决定曲线中段隆起。

Nelson–Siegel–Svensson (NSS) 模型¶

💡思想¶

NSS 是 NS 的扩展版,更灵活地拟合波动形态。
在 NS 基础上再增加一组曲率参数 $\beta_3, \tau_2$。

🧮 公式¶

$$ \begin{aligned} R(t) &= \beta_0 + \beta_1 \frac{1 - e^{-t / \tau_1}}{t / \tau_1} + \beta_2 \left[\frac{1 - e^{-t / \tau_1}}{t / \tau_1} - e^{-t / \tau_1}\right] \\ &\quad + \beta_3 \left[\frac{1 - e^{-t / \tau_2}}{t / \tau_2} - e^{-t / \tau_2}\right] \end{aligned} $$

当 $\beta_3 = 0$ 或 $\tau_2 = \tau_1$,NSS 即退化为 NS。

📊 解释维度¶

参数 含义 影响
$\beta_0$ 长期均衡水平 决定长期利率收敛值
$\beta_1$ 坡度 控制短端斜率
$\beta_2$ 中段曲率 调整中期峰值
$\beta_3$ 次曲率 再现多波峰结构
$\tau_1, \tau_2$ 时间尺度 控制拐点位置

☑️ 优点¶

  • 用寥寥几个参数即可精准拟合;
  • 光滑、可解析导数;
  • 拟合后可快速计算 DV01、长期外推。

⚠️ 缺点¶

  • 并非严格「无套利」;
  • 参数初值敏感,最小二乘优化可能陷局部极值。

Smith–Wilson (SW) 模型¶

💡思想来源¶

由英国精算师协会 (2012) 推荐给 EIOPA ,用于 Solvency II 框架下的贴现率外推。
目标:在保证贴现因子单调递减、渐近收敛到终极远期利率 (UFR) 的前提下,用最小二乘意义的“边缘平滑”确定整条曲线。

相比 NSS(拟合利率曲线),SW 是直接拟合贴现因子 $P(t)$。

🧮 核心方程¶

设:

  • 已知市场贴现因子 $P^{\text{mkt}}(t_i)$,$i=1,...,N$;
  • 未知目标函数为: $$ P(t) = e^{-UFR \cdot t} + \sum_{i=1}^{N} W(t, t_i) \, \zeta_i $$ 其中 $W(t, t_i)$ 称为 Wilson 核函数。

🔑 Wilson 核¶

$$ W(t, t_i) = e^{-UFR (t + t_i)} \left[ \alpha \min(t, t_i) - e^{-\alpha \max(t, t_i)} \sinh(\alpha \min(t, t_i)) \right] $$

其中:

  • $\alpha > 0$:控制收敛速度(较大 (\alpha),更快向 UFR 收敛);
  • $UFR$:长期终极远期率(通常 3.5% 左右,由监管设定)。

通过使曲线在市场观测点处最小化误差: $$ P(t_i) = P^{\text{mkt}}(t_i) $$ 即可线性求得 $\zeta_i$ 系数。

📈 长期性质¶

  • 当 $t \to \infty$:
    $$ f(t) \to UFR $$ 即远期利率趋向于监管终极水平,利率曲线平滑外推。

  • 自动保证 $P(t)$ 单调递减(即无套利)。

⚙️ 实务应用¶

模型 使用场景 目的
NSS 商业银行、固定收益投资、国债曲线 拟合市场数据(灵活形态)
Smith–Wilson 保险公司、监管贴现曲线 长期外推,收敛至 UFR
In [11]:
# NSS模型拟合
def NSS(t,b0,b1,b2,b3,tau1,tau2):
    term1=(1-np.exp(-t/tau1))/(t/tau1)
    term2=term1-np.exp(-t/tau1)
    term3=(1-np.exp(-t/tau2))/(t/tau2)-np.exp(-t/tau2)
    return b0+b1*term1+b2*term2+b3*term3

popt,_=curve_fit(NSS,T2,R2,p0=[0.02,-0.01,0.01,0.005,1,3])
popt
c:\Users\wukek\anaconda3\lib\site-packages\scipy\optimize\_minpack_py.py:1010: OptimizeWarning: Covariance of the parameters could not be estimated
  warnings.warn('Covariance of the parameters could not be estimated',
Out[11]:
array([ -1.85263945,   1.87189641,   2.00836947,   3.60424422,
        17.92262821, 797.78081835])
In [12]:
# Smith–Wilson外推简化版
def smith_wilson(t,UEP=0.02,alpha=0.1):
    return UEP + (R2[-1]-UEP)*np.exp(-alpha*(t-T2[-1]))

grid_long = np.linspace(0.1,30,300)
plt.plot(T2, R2*100,'o',label='市场点')
plt.plot(grid_long, NSS(grid_long,*popt)*100,'--',label='NSS拟合')
plt.plot(grid_long, smith_wilson(grid_long)*100,':',label='Smith–Wilson外推')
plt.legend(); plt.xlabel('期限(年)'); plt.ylabel('利率%'); plt.title('参数化模型比较'); plt.show()
No description has been provided for this image

⚙️ 6. 无套利检测与版本保存¶

In [13]:
def check_arbitrage(P):
    T = np.array(sorted(P.keys()))
    DF = np.array([P[t] for t in T])
    fwd = np.diff(-np.log(DF))/np.diff(T)
    if np.any(fwd<0): print('⚠️ 检测到负远期,需修正!')
    else: print('✅ 无负远期,曲线平稳')

check_arbitrage(P)
✅ 无负远期,曲线平稳
In [14]:
import json, datetime
version={'date':str(datetime.date.today()),'DF':P}
with open(f'curve_{version["date"]}.json','w') as f: json.dump(version,f,indent=2)
print('曲线已保存为版本:',version['date'])
曲线已保存为版本: 2025-10-22

📈 7. DV01 与 Key-Rate 敏感度¶

In [15]:
T = np.array(sorted(P.keys()))
R = -np.log(np.array(list(P.values())))/T

def bond_price(y, c=0.029, T=3, freq=1):
    y = float(np.squeeze(y))  # 强制标量
    times = np.arange(1/freq, T+1/freq, 1/freq)
    cf = np.repeat(c/freq, len(times)); cf[-1] += 1
    disc = np.exp(-y * times)
    return np.sum(cf * disc)

y0 = float(np.squeeze(R[T == 3]))
dy = 0.0001
P0 = bond_price(y0)
P1 = bond_price(y0 + dy)
DV01 = (P0 - P1) / dy
print(f"DV01 = {DV01:.4f} per 1bp")
DV01 = 3.0396 per 1bp

Key-Rate 敏感度(示意)¶

In [16]:
key_rates=[1,3]
def key_rate_shift(R,shift,k):
    R2=R.copy()
    if k in key_rates:
        i=np.where(T==k)[0][0]; R2[i]+=shift
    return R2

for k in key_rates:
    R_shift=key_rate_shift(R,0.0001,k)
    y3=R_shift[T==3][0]
    print(f'KeyRate({k}Y): ΔP = {(bond_price(y0)-bond_price(y3))/0.0001:.4f}')
KeyRate(1Y): ΔP = 0.0000
KeyRate(3Y): ΔP = 3.0396

✅ 总结¶

  • 我们从市场数据出发,构建了 单曲线 与 多曲线;
  • 使用 插值和平滑 方法实现了连续曲线;
  • 采用 NSS 与 Smith–Wilson 模型实现参数化外推;
  • 最后进行了 无套利检测 与 DV01/Key‑Rate 敏感度分析。