第二章:收益率曲线构建

收益率曲线这东西,说白了就是债券市场的“温度计”。我做了这么多年固收交易,每天开盘第一件事就是看曲线形态变了没有。但你知道吗?市场上能直接看到的收益率其实很少,大部分都是“算出来”的。

这一章,我们就来聊聊怎么从零开始构建一条靠谱的收益率曲线。

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 知识体系总览

为了让你对整个流程有个全局认识,我画了一张图:

收益率曲线构建流程 市场数据输入 国债价格、票息、期限 Bootstrapping 剥离零息收益率 插值方法 线性 / 三次样条 线性插值 简单快速,有折角 三次样条插值 平滑连续,可能抖动 完整收益率曲线 任意期限收益率 图:收益率曲线构建流程——从市场数据到完整曲线

2.6 常见问题与避坑

做曲线构建这么多年,我总结了几条血泪教训:

  1. 数据质量是第一位的:如果输入的价格有误,再好的模型也白搭。我每次跑之前都会手动检查几个关键点。
  2. 注意付息频率:中国国债是一年付息一次,但有些企业债是半年付息。Bootstrapping时一定要区分。
  3. 长端插值要小心:10年到30年之间数据点很少,三次样条容易“放飞自我”。我建议加一些约束条件。
  4. 不要过度拟合:有时候曲线看起来特别平滑,但实际交易中根本用不上。保持简单,够用就好。

我的小技巧:每次构建完曲线,我会拿它去给几个活跃券定价。如果定价偏差超过1个bp,我就知道哪里出问题了。这比看任何统计指标都管用。

好了,这一章的内容就到这里。收益率曲线构建是固收交易的基础,也是很多高级策略的起点。把这一步做扎实了,后面学什么都会轻松很多。


专注资料整理