4. 信用曲线构建:从CDS报价到生存概率曲线的转换

做信用衍生品做市,最基础也最绕不开的一步,就是构建信用曲线。

说白了,就是把市场上那些零散的CDS报价,变成一条连续的、能用来给任何期限的信用产品定价的生存概率曲线。这个过程,我习惯叫它「剥息法」——跟债券里剥息曲线一个道理,但逻辑上更贴近违约风险。

4.1 为什么需要信用曲线?

你想想看,市场上交易的CDS,通常只有几个标准期限:6个月、1年、2年、3年、5年、7年、10年。但实际做市时,客户要的可能是个4.5年的CDS,或者一个7.25年的。怎么办?

你不能说「不好意思,没有这个期限的报价」。你得自己算出来。

信用曲线就是干这个的。它把离散的报价点,变成一条连续的曲线。有了它,任何期限的违约概率、信用利差,你都能插值得到。

核心逻辑链条:

CDS报价 → 违约强度 → 生存概率 → 信用曲线

4.2 从CDS报价到违约强度

先看一个最简单的例子。

假设某公司1年期CDS报价为100个基点(bps)。这意味着什么?

嗯,这里要注意:CDS的保费是分期支付的,但为了讲清楚核心逻辑,我们先忽略 accrued premium 和违约时间点的问题,用连续时间模型来理解。

在连续时间框架下,CDS的保费腿(premium leg)和赔付腿(protection leg)要相等。公式长这样:

保费腿 = s * ∫₀ᵀ e^(-rt) * S(t) dt
赔付腿 = (1-R) * ∫₀ᵀ e^(-rt) * (-dS(t))

其中:

  • s = CDS报价(bps)
  • r = 无风险利率
  • S(t) = t时刻的生存概率
  • R = 回收率(通常假设40%)

如果假设违约强度λ是常数,那么生存概率就是 S(t) = e^(-λt)。代入上面两个积分,化简后得到:

s = (1-R) * λ

所以:

λ = s / (1-R)

举个例子:s=100bps=0.01,R=40%,那么 λ = 0.01 / 0.6 ≈ 0.01667。也就是说,该公司的年化违约强度约为1.667%。

我个人习惯:在实际项目中,我一般先用这个简单公式算个初值,再用数值方法精确求解。因为真实市场里,CDS的保费是季度支付的,而且违约可能发生在任何时间点,这些细节都会影响结果。

4.3 从违约强度到生存概率曲线

有了违约强度λ,生存概率就很简单了:

S(t) = e^(-λt)

但问题是,不同期限的CDS报价,对应的λ是不一样的。1年期和5年期的λ通常不同。这就引出了「剥息法」的核心思路。

4.4 剥息法:逐期限求解

剥息法的逻辑是这样的:

  1. 从最短期限开始(比如6个月),假设该期限内λ为常数,用该期限的CDS报价反解出λ₁。
  2. 然后处理下一个期限(比如1年)。此时,0-6个月的λ已经知道了,我们假设6个月-1年之间λ为常数λ₂,用1年期CDS报价反解λ₂。
  3. 以此类推,逐段求解,直到覆盖所有期限。

这样做的好处是:每个期限的CDS报价只影响该段及之后的曲线,不会「污染」前面的结果。

我曾经踩过一个坑:有次做某个高收益债的曲线,5年期CDS报价异常低,明显是流动性不足导致的。如果直接用剥息法,会把5年之后的曲线拉得很低。后来我加了平滑约束,才把曲线修回来。所以,剥息法虽然简单,但遇到异常报价时一定要做预处理。

4.5 代码实现:从CDS报价到生存概率

下面是一个简化的Python实现。假设我们有三个期限的CDS报价:1年、3年、5年。

import numpy as np
from scipy.optimize import brentq

# CDS报价(bps)
cds_quotes = {
    1: 100,   # 1年
    3: 150,   # 3年
    5: 180    # 5年
}

# 无风险利率(连续复利)
r = 0.03
# 回收率
R = 0.4

# 存储结果:每个时间段的违约强度
lambdas = []
time_points = sorted(cds_quotes.keys())

def survival_prob(t, lambdas, time_points):
    """计算t时刻的生存概率"""
    sp = 1.0
    prev_t = 0
    for i, tp in enumerate(time_points):
        if t <= tp:
            sp *= np.exp(-lambdas[i] * (t - prev_t))
            break
        else:
            sp *= np.exp(-lambdas[i] * (tp - prev_t))
            prev_t = tp
    return sp

def cds_price(s, T, lambdas, time_points, r, R):
    """计算CDS价格(保费腿 - 赔付腿)"""
    # 简化:假设保费连续支付,违约发生在到期时
    # 实际中要用数值积分
    n_steps = 100
    dt = T / n_steps
    premium_leg = 0
    protection_leg = 0
    for i in range(n_steps):
        t = (i + 0.5) * dt
        sp = survival_prob(t, lambdas, time_points)
        premium_leg += s * np.exp(-r * t) * sp * dt
        # 违约概率密度
        if t <= time_points[len(lambdas)-1]:
            idx = np.searchsorted(time_points, t)
            lambda_t = lambdas[min(idx, len(lambdas)-1)]
            protection_leg += (1-R) * np.exp(-r * t) * sp * lambda_t * dt
    return premium_leg - protection_leg

# 剥息法逐段求解
for i, T in enumerate(time_points):
    def f(s):
        return cds_price(s, T, lambdas + [0], time_points[:i+1], r, R) - 0
    # 用brentq求解使CDS价格为0的违约强度
    # 注意:这里s是违约强度,不是CDS报价
    # 实际中需要嵌套求解
    # 为简化,直接用公式近似
    lambda_est = cds_quotes[T] / 10000 / (1-R)
    lambdas.append(lambda_est)

print("各时间段违约强度:")
for i, T in enumerate(time_points):
    print(f"{T}年: λ = {lambdas[i]:.6f}")

# 计算生存概率曲线
print("\n生存概率:")
for t in [0.5, 1, 2, 3, 4, 5]:
    sp = survival_prob(t, lambdas, time_points)
    print(f"t={t}年: S(t) = {sp:.4f}")

我建议:实际生产环境中,不要用这种简化的数值积分。应该用更精确的解析公式,或者至少用高斯求积法。上面的代码只是为了展示核心逻辑。

4.6 信用曲线可视化

下面这张图展示了从CDS报价到生存概率曲线的完整流程:

信用曲线构建流程 CDS报价 1年/3年/5年等 剥息法 违约强度 λ(t) 分段常数 积分 生存概率 S(t) 连续曲线 插值 信用曲线 任意期限定价 插值/拟合 输入:离散CDS报价 → 输出:连续生存概率曲线 关键参数:回收率R、无风险利率r、报价期限结构

4.7 实际项目中的注意事项

做信用曲线构建,有几个点我特别想强调:

  • 回收率假设很敏感:不同交易对手、不同优先级,回收率差异很大。我一般会同时算40%和20%两套曲线,看价差影响。
  • 插值方法要选对:生存概率曲线本身应该是单调递减的。用线性插值可能会出负概率,我习惯用log-linear插值,保证单调性。
  • 流动性调整:有些期限的CDS报价可能很久没更新了。遇到这种情况,我会给一个流动性折扣,或者直接用相邻期限的报价做平滑。

我曾经遇到过:某次做市时,一个客户要的是7.5年期的CDS报价。我直接用5年和10年的报价线性插值,结果算出来的价格和实际成交价差了20%。后来发现,是因为中间有个公司评级调整事件,导致5-10年段的信用曲线形状发生了扭曲。从那以后,我每次都会先检查一下曲线形状是否合理。

4.8 小结

信用曲线构建,说白了就是把市场报价翻译成数学上可用的生存概率。剥息法是最经典的做法,但实际中要根据市场情况做调整。

嗯,这一章的内容就到这里。代码虽然简化了,但核心逻辑是通用的。你可以在自己的项目里试试看,从最简单的两三个期限开始,慢慢加复杂度。


公众号:蓝海资料掘金营,微信deep3321