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 剥息法:逐期限求解
剥息法的逻辑是这样的:
- 从最短期限开始(比如6个月),假设该期限内λ为常数,用该期限的CDS报价反解出λ₁。
- 然后处理下一个期限(比如1年)。此时,0-6个月的λ已经知道了,我们假设6个月-1年之间λ为常数λ₂,用1年期CDS报价反解λ₂。
- 以此类推,逐段求解,直到覆盖所有期限。
这样做的好处是:每个期限的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报价到生存概率曲线的完整流程:
4.7 实际项目中的注意事项
做信用曲线构建,有几个点我特别想强调:
- 回收率假设很敏感:不同交易对手、不同优先级,回收率差异很大。我一般会同时算40%和20%两套曲线,看价差影响。
- 插值方法要选对:生存概率曲线本身应该是单调递减的。用线性插值可能会出负概率,我习惯用log-linear插值,保证单调性。
- 流动性调整:有些期限的CDS报价可能很久没更新了。遇到这种情况,我会给一个流动性折扣,或者直接用相邻期限的报价做平滑。
我曾经遇到过:某次做市时,一个客户要的是7.5年期的CDS报价。我直接用5年和10年的报价线性插值,结果算出来的价格和实际成交价差了20%。后来发现,是因为中间有个公司评级调整事件,导致5-10年段的信用曲线形状发生了扭曲。从那以后,我每次都会先检查一下曲线形状是否合理。
4.8 小结
信用曲线构建,说白了就是把市场报价翻译成数学上可用的生存概率。剥息法是最经典的做法,但实际中要根据市场情况做调整。
嗯,这一章的内容就到这里。代码虽然简化了,但核心逻辑是通用的。你可以在自己的项目里试试看,从最简单的两三个期限开始,慢慢加复杂度。
公众号:蓝海资料掘金营,微信deep3321