4. 息票剥离法(Bootstrap):原理、算法步骤、Python实现、插值方法

说到收益率曲线构建,我个人最常用的方法就是息票剥离法,也就是Bootstrap。这名字听起来挺唬人,其实说白了就是「把靴子上的鞋带提起来」——从短端到长端,一步步把零息利率给「提」出来。

我记得刚入行那会儿,第一次手动算Bootstrap,算到第三期就卡住了。后来才发现,这方法的核心就一句话:用已知的零息利率,去解未知的零息利率

4.1 原理:为什么需要Bootstrap?

市场上我们能直接看到的,大多是附息债券的价格。比如一个3年期国债,每年付息一次,最后还本。这种债券的价格里,包含了多个现金流的折现信息。但问题是——每个现金流的折现率都不一样。

你想想看,第1年的现金流,应该用1年期零息利率折现;第2年的用2年期零息利率;第3年的用3年期零息利率。可我们一开始只知道1年期零息利率(因为有1年期零息债),2年期和3年期的都不知道。

Bootstrap的思路就是:从短端开始,一步步往前推

核心思想:对于附息债券,其价格等于所有未来现金流按对应期限零息利率折现之和。已知短端利率,即可反推长端利率。

4.2 算法步骤:手把手拆解

我习惯把Bootstrap拆成四个步骤,这样不容易乱:

  1. 整理数据:收集各期限附息债券的市价、票面利率、付息频率。
  2. 确定短端:如果有1年期以内的零息债(如国库券),直接用它们的收益率作为对应期限的零息利率。
  3. 逐期求解:从最短的附息债开始,用已知利率解未知利率。
  4. 插值填充:对于缺失期限(比如1.5年、2.3年),用插值法补齐。

举个例子,假设我们有这样一组数据:

期限(年) 债券类型 票面利率 市价(面值100)
0.5 零息债 0% 98.0
1.0 零息债 0% 96.0
1.5 附息债 4% 99.5
2.0 附息债 5% 101.0

第一步,先算零息债的利率:

  • 0.5年期:98.0 = 100 / (1 + r₀.₅/2) → r₀.₅ ≈ 4.08%
  • 1.0年期:96.0 = 100 / (1 + r₁.₀/2)² → r₁.₀ ≈ 4.17%

第二步,解1.5年期附息债。它有三个现金流:0.5年付2元,1.0年付2元,1.5年付102元。

公式是:99.5 = 2/(1+r₀.₅/2) + 2/(1+r₁.₀/2)² + 102/(1+r₁.₅/2)³

把已知的r₀.₅和r₁.₀代进去,解方程就能得到r₁.₅。嗯,这里要注意,这是个一元方程,用数值方法(比如牛顿法)就能解。

避坑指南:我曾经在解这个方程时,直接用Excel的Goal Seek,结果因为初始值设得不对,算出来的利率是负的。建议初始值设成前一期利率+0.1%,这样收敛快。

4.3 Python实现:从零到一

下面是我常用的Bootstrap实现代码。我习惯用scipy.optimize里的fsolve来解方程,比手写牛顿法省事多了。

import numpy as np
from scipy.optimize import fsolve

def bootstrap_yield_curve(bonds):
    """
    bonds: list of dict, 每个债券包含:
        - 'maturity': 期限(年)
        - 'coupon': 票面利率(小数)
        - 'price': 市价(面值100)
        - 'freq': 付息频率(次/年)
    返回: dict, 期限->零息利率
    """
    # 按期限排序
    bonds_sorted = sorted(bonds, key=lambda x: x['maturity'])
    
    # 存储已知利率
    rates = {}
    
    for bond in bonds_sorted:
        T = bond['maturity']
        c = bond['coupon']
        P = bond['price']
        freq = bond.get('freq', 2)  # 默认半年付息
        
        # 如果是零息债,直接算
        if c == 0:
            r = (100 / P) ** (1 / T) - 1
            rates[T] = r
            continue
        
        # 附息债:用fsolve解方程
        def equation(r):
            # 计算所有现金流的折现和
            pv = 0
            # 生成付息时间点
            n_periods = int(T * freq)
            for i in range(1, n_periods + 1):
                t = i / freq
                cf = 100 * c / freq
                if i == n_periods:
                    cf += 100  # 最后一期还本
                # 如果该时间点的利率已知,直接用
                if t in rates:
                    pv += cf / (1 + rates[t] / freq) ** (t * freq)
                else:
                    pv += cf / (1 + r / freq) ** (t * freq)
            return pv - P
        
        # 初始值设为前一期利率+0.1%
        prev_rate = list(rates.values())[-1] if rates else 0.03
        r_solution = fsolve(equation, prev_rate + 0.001)[0]
        rates[T] = r_solution
    
    return rates

# 示例数据
bonds_data = [
    {'maturity': 0.5, 'coupon': 0.0, 'price': 98.0, 'freq': 2},
    {'maturity': 1.0, 'coupon': 0.0, 'price': 96.0, 'freq': 2},
    {'maturity': 1.5, 'coupon': 0.04, 'price': 99.5, 'freq': 2},
    {'maturity': 2.0, 'coupon': 0.05, 'price': 101.0, 'freq': 2},
]

result = bootstrap_yield_curve(bonds_data)
for T, r in sorted(result.items()):
    print(f"{T}年: {r*100:.2f}%")

注意:实际市场中,不同债券的付息频率可能不同(半年付、年付、季度付)。代码里我默认了半年付息,但你可以通过freq参数灵活调整。我曾经遇到过一个奇葩债券,每月付息,当时代码没处理好,算出来的利率曲线像心电图一样抖。

4.4 插值方法:让曲线更平滑

Bootstrap算出来的利率,只在有债券的期限上有值。但实际应用中,我们需要任意期限的利率。比如要定价一个1.3年期的衍生品,就得靠插值了。

我个人最常用的两种插值方法:

4.4.1 线性插值

最简单,也最直观。在两个已知点之间画一条直线。

def linear_interpolate(rates, target_T):
    """
    rates: dict, 期限->利率
    target_T: 目标期限
    """
    sorted_T = sorted(rates.keys())
    
    # 边界处理
    if target_T <= sorted_T[0]:
        return rates[sorted_T[0]]
    if target_T >= sorted_T[-1]:
        return rates[sorted_T[-1]]
    
    # 找到左右两个点
    for i in range(len(sorted_T) - 1):
        if sorted_T[i] <= target_T <= sorted_T[i+1]:
            T1, T2 = sorted_T[i], sorted_T[i+1]
            r1, r2 = rates[T1], rates[T2]
            # 线性插值
            r = r1 + (r2 - r1) * (target_T - T1) / (T2 - T1)
            return r

4.4.2 三次样条插值

线性插值有个问题——在已知点处导数不连续,曲线看起来「有棱有角」。三次样条能保证一阶、二阶导数连续,曲线更平滑。

from scipy.interpolate import CubicSpline

def cubic_spline_interpolate(rates, target_T):
    """
    使用三次样条插值
    """
    T_array = np.array(sorted(rates.keys()))
    r_array = np.array([rates[T] for T in T_array])
    
    # 创建样条函数
    cs = CubicSpline(T_array, r_array, bc_type='natural')
    
    return cs(target_T)

经验之谈:我做过一个对比测试,用线性插值和三次样条分别构建曲线,然后定价一个5年期利率互换。结果两者差了2个基点。对于交易台来说,2个基点可能就是一笔不小的盈亏。所以,插值方法的选择,直接影响定价精度

4.5 知识体系总览

下面这张图,是我自己总结的Bootstrap知识框架。每次带新人时,我都会先让他们看这张图,心里有个全局概念。

息票剥离法(Bootstrap)知识体系 Bootstrap核心 原理:逐期求解 算法步骤 Python实现 插值方法 已知短端 → 推长端 附息债 = 现金流折现和 1. 整理数据 2. 确定短端零息利率 3. 逐期解方程 4. 插值填充缺失期限 fsolve解方程 支持不同付息频率 边界条件处理 线性插值(简单) 三次样条(平滑) 导数连续 vs 不连续 应用:债券定价、利率衍生品估值、风险管理 插值方法直接影响定价精度,建议根据业务需求选择

说实话,Bootstrap这个方法,看起来简单,但实际用起来坑不少。比如市场数据不完整、不同债券的流动性差异、节假日调整等等。我建议你先把上面的代码跑一遍,理解核心逻辑,然后再去处理真实市场数据。

嗯,今天就先聊到这儿。记住一句话:Bootstrap不是终点,而是起点——有了零息利率曲线,后面才能做各种利率产品的定价和对冲。


专注资料整理