由 CDS 利差反推累计违约概率:信用三角近似
CDS-Implied Cumulative Default Probability via the Credit-Triangle Approximation
开始编码实现 solution(spread_bps: float, recovery_rate: float, horizon_years: float) -> float。某信贷台每天跑一个脚本,从挂出的 CDS 平价利差反推暗含的累计违约概率,把结果写进信用看板的"暗含 PD"那一列。给定平价 CDS 利差 spread_bps(基点)、假设回收率 recovery_rate、视野 horizon_years(年),按标准信用三角扁平 hazard 近似,返回该视野下的累计违约概率。
公式三步:bps 转十进制 s = spread_bps / 10000.0;信用三角反推 hazard lambda = s / (1.0 - recovery_rate)(CDS 平价利差每年补偿期望损失 lambda * (1 - R),反解得 lambda = s / (1 - R));视野累计违约概率 cum_pd = 1 - exp(-lambda * horizon_years)(连续时间扁平 hazard 下存活概率为 exp(-lambda*T),故累计违约为 1 - exp(-lambda*T))。
示例
solution(150.0, 0.4, 5.0) 返回 0.11750309741540454。逐步:s = 150 / 10000 = 0.0150(年化 1.5% 的利差,不是 1.5 也不是 0.0015)。然后 lambda = 0.015 / (1 - 0.4) = 0.015 / 0.6 = 0.025(每年的 hazard,乘以 LGD 0.6 复现 0.015 年化利差)。最后 cum_pd = 1 - exp(-0.025 * 5) = 1 - exp(-0.125) = 1 - 0.882496... ≈ 0.11750。看板读数直觉:150 bp 的 5y CDS、按 40% 回收率假设,市场暗含未来五年的累计违约概率约为 11.75%。
三个易错点
第一,bps 到十进制:1 bp = 1/10000,不是 1/100。把 bps 当作百分比会把 hazard 放大 100x,给一个普通信用名义算出接近 1.0 的累计违约概率(明显荒谬)。务必除以 10000.0。
第二,信用三角的方向:正确是 lambda = s / (1 - R),分母是违约损失率(LGD)。常见错写有 lambda = s * (1 - R)(乘代替除)和 lambda = s / R(除以回收率而非 LGD)。三种公式的量纲一致,但只有一个对。记忆口诀:利差每年补偿期望损失,期望损失等于 PD * LGD = lambda * (1 - R),反解 lambda 就要除以 (1 - R)。
第三,累计违约概率的合成形式:扁平 hazard lambda 在视野 T 上累计违约概率为 1 - exp(-lambda * T)(连续时间)。线性近似 lambda * T 仅当 lambda * T << 1 时准;扭曲名义(利差 5000 bp、R=0.4、T=5y)下 lambda * T = 4.17,线性给出 4.17——非法概率(>1),而正确指数形式给 1 - exp(-4.17) ≈ 0.9846。离散式 1 - (1 - lambda)**T 也错(它回答的是不同的问题——离散年度伯努利,不是连续 hazard),在 lambda * T = 0.83 时偏差 ~3%。请使用 math.exp 与 1 - exp(-lambda*T)。
哨兵
若 recovery_rate == 1.0,信用三角的分母为 0:任何 lambda 都给出零期望损失利差,spread 无法标定 hazard。返回 float('nan')(且必须先检测,再做除法——让 Python 抛 ZeroDivisionError 违反合约)。检查顺序很重要:R == 1.0 哨兵优先于其他捷径。特别地,solution(0.0, 1.0, 5.0) 必须返回 NaN,不是 0.0——先按 spread_bps == 0 短路的作者会答错这条对抗用例。
若 spread_bps == 0.0(且 R != 1.0),则 lambda = 0、cum_pd = 1 - exp(0) = 0.0。指数形式天然处理;除 R == 1.0 哨兵外无需特判。
若 horizon_years 极小(例如一天 1/365),累计 PD 大约等于 lambda * T(1 - exp(-x) 在小 x 处的线性化),但仍按 1 - exp(-lambda*T) 计算以保持一致;若 horizon_years 极大(例如扭曲名义的 30 年),累计 PD 饱和到接近 1.0,指数形式自然处理。
stubs/stub.py 提供函数骨架。
实践背景
报价或对冲单名 CDS 的信贷交易台,从经纪人单与做市商屏幕上读取平价利差,需要一个快速读数:这一利差暗示了几个标准视野(1y、3y、5y、10y)的累计违约概率是多少?信用三角近似 lambda = s / (1 - R) 是每本固收信用教材都会教的"快速读数"公式;配上扁平 hazard 累计形式 1 - exp(-lambda*T),就给出该 desk 早间看板上的暗含 PD 期限结构那一列,紧挨着现券到期收益率反推 PD 与评级机构一年期 PD。从一组 CDS 期限(1y、3y、5y、7y、10y)做更精细的逐段扁平 hazard bootstrap 是后续问题;单期限的信用三角读数是正确的起点,也正是 solution(...) 计算的内容。
约束条件
- 0.0 <= spread_bps <= 10000.0(基点;10000 bp = 100% 利差,扭曲名义区间)
- 0.0 <= recovery_rate <= 1.0
- 0.0 < horizon_years <= 30.0
- 输出:float ∈ [0.0, 1.0],或 float('nan') 当且仅当 recovery_rate == 1.0;rel_tol=1e-9、abs_tol=1e-9;NaN 与 NaN 视为相等
样例
Case 1 · statement-example: 150bp spread, R=0.4, T=5y -> ~0.1175
输入: [150,0.4,5]
期望: 0.11750309741540454
s = 150/10000 = 0.015;lambda = 0.015/(1-0.4) = 0.025;cum_pd = 1 - exp(-0.025*5) = 1 - exp(-0.125) ≈ 0.11750。
Case 2 · typical: 100bp HY-style 5y benchmark
输入: [100,0.4,5]
期望: 0.07995558537067671
s=0.01;lambda=0.01/0.6≈0.01667;cum_pd = 1 - exp(-0.0833...) ≈ 0.07996。注意:写错把 100bp 当 1.0(相当于 100%)会得到接近 1 的结果。
Case 3 · typical: 200bp 5y, R=0.4
输入: [200,0.4,5]
期望: 0.15351827510938587
s=0.02;lambda=0.02/0.6≈0.03333;cum_pd = 1 - exp(-0.16667) ≈ 0.15352。
Case 4 · typical: 50bp IG name 5y, R=0.4
输入: [50,0.4,5]
期望: 0.040810542890861834
s=0.005;lambda≈0.00833;cum_pd ≈ 0.04081。投资级名义的典型量级。
Case 5 · boundary: spread_bps=0 -> cum_pd=0 exactly
输入: [0,0.4,5]
期望: 0
s=0 -> lambda=0 -> cum_pd = 1 - exp(0) = 0。
Case 6 · boundary: R=0.0 no-recovery -> lambda equals s
输入: [100,0,5]
期望: 0.048770575499285984
R=0 时 lambda = s/1 = s = 0.01;cum_pd = 1 - exp(-0.05) ≈ 0.04877。
Case 7 · large: 5000bp (50%) distressed 5y
输入: [5000,0.4,5]
期望: 0.9844961464009907
lambda=0.5/0.6≈0.833;lambda*T≈4.17;cum_pd ≈ 0.98450。注意:错用线性公式 lambda*T 会给出 4.17(>1,非法概率)。
Case 8 · adversarial: R=1.0 sentinel returns NaN (not ZeroDivisionError)
输入: [150,1,5]
期望: "NaN"
R=1 时 LGD=0,从 spread 反推不出 lambda(除以零);按合约返回 NaN,不能让 Python 抛 ZeroDivisionError。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: 150bp spread, R=0.4, T=5y -> ~0.1175
输入: [150,0.4,5]
期望: 0.11750309741540454
s = 150/10000 = 0.015;lambda = 0.015/(1-0.4) = 0.025;cum_pd = 1 - exp(-0.025*5) = 1 - exp(-0.125) ≈ 0.11750。
Case 2 · typical: 100bp HY-style 5y benchmark
输入: [100,0.4,5]
期望: 0.07995558537067671
s=0.01;lambda=0.01/0.6≈0.01667;cum_pd = 1 - exp(-0.0833...) ≈ 0.07996。注意:写错把 100bp 当 1.0(相当于 100%)会得到接近 1 的结果。
Case 3 · typical: 200bp 5y, R=0.4
输入: [200,0.4,5]
期望: 0.15351827510938587
s=0.02;lambda=0.02/0.6≈0.03333;cum_pd = 1 - exp(-0.16667) ≈ 0.15352。
Case 4 · typical: 50bp IG name 5y, R=0.4
输入: [50,0.4,5]
期望: 0.040810542890861834
s=0.005;lambda≈0.00833;cum_pd ≈ 0.04081。投资级名义的典型量级。
Case 5 · boundary: spread_bps=0 -> cum_pd=0 exactly
输入: [0,0.4,5]
期望: 0
s=0 -> lambda=0 -> cum_pd = 1 - exp(0) = 0。
Case 6 · boundary: R=0.0 no-recovery -> lambda equals s
输入: [100,0,5]
期望: 0.048770575499285984
R=0 时 lambda = s/1 = s = 0.01;cum_pd = 1 - exp(-0.05) ≈ 0.04877。
Case 7 · large: 5000bp (50%) distressed 5y
输入: [5000,0.4,5]
期望: 0.9844961464009907
lambda=0.5/0.6≈0.833;lambda*T≈4.17;cum_pd ≈ 0.98450。注意:错用线性公式 lambda*T 会给出 4.17(>1,非法概率)。
Case 8 · adversarial: R=1.0 sentinel returns NaN (not ZeroDivisionError)
输入: [150,1,5]
期望: "NaN"
R=1 时 LGD=0,从 spread 反推不出 lambda(除以零);按合约返回 NaN,不能让 Python 抛 ZeroDivisionError。