第二章:收益率曲线构建
收益率曲线这东西,说白了就是债券市场的“温度计”。我做了这么多年固收交易,每天开盘第一件事就是看曲线形态变了没有。但你知道吗?市场上能直接看到的收益率其实很少,大部分都是“算出来”的。
这一章,我们就来聊聊怎么从零开始构建一条靠谱的收益率曲线。
2.1 为什么需要构建曲线?
你打开Wind或者Bloomberg,能看到国债收益率对吧?但那些只是关键期限的点。比如1年、2年、5年、10年。那3年、7年、15年呢?没有现成的数据。
更麻烦的是,有些债券不是标准期限的。比如一个4.7年的老券,它的收益率怎么算?
所以我们需要两件事:
- Bootstrapping:从附息债券价格里“剥离”出零息收益率
- 插值:把离散的点连成一条连续的曲线
核心思想:收益率曲线不是画出来的,是算出来的。每一步都有坑,我们一个一个填。
2.2 Bootstrapping方法
Bootstrapping,中文叫“自助法”或“剥离法”。名字挺唬人,其实逻辑很简单。
假设你有一堆国债,每个债券都有不同的期限和票息。你想知道的是:如果我现在借出去1年、2年、3年……分别能拿到多少利息?
但问题是,附息债券中间有现金流。比如一个3年期国债,每年付息一次。那它的价格里包含了第1年、第2年、第3年的现金流。我们得一层层剥开。
2.2.1 基本原理
我举个例子你就明白了。
假设市场上有三只零息国债:
| 期限 | 价格(面值100) | 收益率 |
|---|---|---|
| 1年 | 97.5 | 2.56% |
| 2年 | 94.8 | 2.71% |
| 3年 | 91.9 | 2.85% |
零息债券好办,直接用公式:
收益率 = (面值 / 价格)^(1/期限) - 1
但现实世界哪有这么多零息债?大部分都是附息债。这时候就要用Bootstrapping了。
2.2.2 附息债券的剥离过程
假设有一只3年期国债,票面利率3%,每年付息一次,当前价格100.5元。
第一步:先找到1年期零息收益率。假设是2.5%。
第二步:用1年期收益率折现第一笔现金流(3元利息)。
第三步:剩下的现金流用2年期收益率折现……但2年期收益率还不知道?
嗯,这就是Bootstrapping的递归本质。你得从短端往长端一步步推。
我的经验:做Bootstrapping时,我习惯从最短期限开始。先算1年,再算2年,一步步往上搭。就像盖房子,地基不稳后面全歪。
2.2.3 Python实现
import numpy as np
from scipy.optimize import brentq
# 示例数据:债券列表
bonds = [
{'maturity': 1.0, 'coupon': 0.0, 'price': 97.5}, # 零息债
{'maturity': 2.0, 'coupon': 0.025, 'price': 99.8}, # 附息债
{'maturity': 3.0, 'coupon': 0.03, 'price': 100.5}, # 附息债
]
def bootstrap_yields(bonds):
yields = {}
for bond in bonds:
t = bond['maturity']
c = bond['coupon']
p = bond['price']
# 如果是零息债
if c == 0:
y = (100 / p) ** (1/t) - 1
yields[t] = y
continue
# 附息债:用已知收益率折现前面的现金流
def price_diff(y):
pv = 0
# 计算所有付息日
for year in range(1, int(t) + 1):
cf = 100 * c
if year == int(t):
cf += 100 # 最后一期还本
# 用已知收益率折现
if year in yields:
pv += cf / (1 + yields[year]) ** year
else:
pv += cf / (1 + y) ** year
return pv - p
y_est = brentq(price_diff, 0.001, 0.10)
yields[t] = y_est
return yields
result = bootstrap_yields(bonds)
for t, y in result.items():
print(f"{t}年: {y*100:.2f}%")
我曾经踩过的坑:Bootstrapping时,如果债券的付息频率不是一年一次(比如半年付息),一定要调整折现因子。我刚开始做的时候忘了这茬,算出来的曲线全是歪的。后来花了整整一天debug……
2.3 插值方法
Bootstrapping做完,你手里只有几个离散的点。比如1年、2年、3年、5年、10年。但交易员需要的是任意期限的收益率。比如一个4.5年的债券,怎么定价?
这时候就需要插值了。
2.3.1 线性插值
最简单的方法。两点之间画条直线。
def linear_interp(x, x1, y1, x2, y2):
return y1 + (y2 - y1) * (x - x1) / (x2 - x1)
# 示例:已知2年2.7%,5年3.0%,求3年
y_3 = linear_interp(3, 2, 0.027, 5, 0.030)
print(f"3年收益率: {y_3*100:.2f}%")
线性插值的好处是简单、稳定。但缺点也很明显——曲线不够平滑。在期限交界处会有“折角”,这在金融模型里不太好看。
我个人习惯:做快速估值时用线性插值就够了。但如果是做定价模型或者风险管理,我建议用更平滑的方法。
2.3.2 三次样条插值
三次样条插值,说白了就是每两个点之间用一个三次多项式连接,并且保证连接处的一阶导数和二阶导数连续。
这样画出来的曲线非常平滑,没有折角。
from scipy.interpolate import CubicSpline
import numpy as np
# 已知数据点
maturities = np.array([1, 2, 3, 5, 7, 10])
yields = np.array([0.025, 0.027, 0.0285, 0.030, 0.031, 0.032])
# 构建三次样条
cs = CubicSpline(maturities, yields, bc_type='natural')
# 预测4年收益率
y_4 = cs(4)
print(f"4年收益率: {y_4*100:.2f}%")
# 预测6年收益率
y_6 = cs(6)
print(f"6年收益率: {y_6*100:.2f}%")
三次样条有个小问题:在数据点稀疏的地方,曲线可能会“抖动”。尤其是长端,如果只有10年和30年两个点,中间可能会翘起来或者凹下去。
避坑指南:我曾经用三次样条插值做30年国债定价,结果在15-20年段曲线突然下弯,导致一个组合的久期对冲完全失效。后来我加了一个“单调性约束”,才把问题解决。
2.4 构建实战:完整流程
好了,理论讲完了。我们来走一遍完整的流程。
2.4.1 数据准备
假设我们从市场拿到了以下国债数据:
| 债券代码 | 期限(年) | 票面利率 | 价格 |
|---|---|---|---|
| B001 | 1 | 0% | 97.5 |
| B002 | 2 | 2.5% | 99.8 |
| B003 | 3 | 3.0% | 100.5 |
| B004 | 5 | 3.2% | 101.2 |
| B005 | 10 | 3.5% | 102.0 |
2.4.2 完整代码
import numpy as np
from scipy.optimize import brentq
from scipy.interpolate import CubicSpline
# 第一步:Bootstrapping
bonds_data = [
(1.0, 0.0, 97.5),
(2.0, 0.025, 99.8),
(3.0, 0.03, 100.5),
(5.0, 0.032, 101.2),
(10.0, 0.035, 102.0),
]
known_yields = {}
def bootstrap_step(maturity, coupon, price):
# 如果是零息债
if coupon == 0:
y = (100 / price) ** (1/maturity) - 1
known_yields[maturity] = y
return y
# 附息债
def price_diff(y):
pv = 0
for year in range(1, int(maturity) + 1):
cf = 100 * coupon
if year == int(maturity):
cf += 100
if year in known_yields:
pv += cf / (1 + known_yields[year]) ** year
else:
pv += cf / (1 + y) ** year
return pv - price
y = brentq(price_diff, 0.001, 0.10)
known_yields[maturity] = y
return y
for mat, coup, pri in bonds_data:
y = bootstrap_step(mat, coup, pri)
print(f"{mat}年零息收益率: {y*100:.2f}%")
# 第二步:三次样条插值
maturities = np.array(list(known_yields.keys()))
yields = np.array(list(known_yields.values()))
cs = CubicSpline(maturities, yields, bc_type='natural')
# 生成连续曲线
target_maturities = np.arange(0.5, 10.5, 0.5)
interpolated_yields = cs(target_maturities)
print("\n插值结果:")
for t, y in zip(target_maturities, interpolated_yields):
print(f"{t:.1f}年: {y*100:.2f}%")
2.4.3 可视化曲线
代码跑完,我们来看看曲线长什么样。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(maturities, yields, 'ro', label='Bootstrapped Points', markersize=8)
plt.plot(target_maturities, interpolated_yields, 'b-', label='Cubic Spline', linewidth=2)
plt.xlabel('Maturity (Years)')
plt.ylabel('Yield (%)')
plt.title('Yield Curve Construction')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
关键点:红色点是Bootstrapping算出来的零息收益率,蓝色线是三次样条插值后的连续曲线。注意看,曲线在数据点处是平滑连接的,没有折角。
2.5 知识体系总览
为了让你对整个流程有个全局认识,我画了一张图:
2.6 常见问题与避坑
做曲线构建这么多年,我总结了几条血泪教训:
- 数据质量是第一位的:如果输入的价格有误,再好的模型也白搭。我每次跑之前都会手动检查几个关键点。
- 注意付息频率:中国国债是一年付息一次,但有些企业债是半年付息。Bootstrapping时一定要区分。
- 长端插值要小心:10年到30年之间数据点很少,三次样条容易“放飞自我”。我建议加一些约束条件。
- 不要过度拟合:有时候曲线看起来特别平滑,但实际交易中根本用不上。保持简单,够用就好。
我的小技巧:每次构建完曲线,我会拿它去给几个活跃券定价。如果定价偏差超过1个bp,我就知道哪里出问题了。这比看任何统计指标都管用。
好了,这一章的内容就到这里。收益率曲线构建是固收交易的基础,也是很多高级策略的起点。把这一步做扎实了,后面学什么都会轻松很多。