← 返回编程题库
coding-credit-el-stress-pd-lgd-shocks中等免费版2000ms未尝试

信用预期损失:PD 与 LGD 乘性冲击下的压力测试

Credit Expected Loss under Multiplicative PD and LGD Stress Shocks

开始编码

实现 solution(pd: list[float], lgd: list[float], ead: list[float], pd_shock_multiplier: float, lgd_shock_multiplier: float) -> list[float]。某行的信用压力仪表盘需要在监管"严苛但合理"的逆境情景下、把每个债务人的 PD 同时乘以 pd_shock_multiplier、把每个债务人的 LGD 同时乘以 lgd_shock_multiplier,并报告每日基线与受冲击两个预期损失。返回二元组 [base_el, stressed_el](美元)。交易台对外报告的"压力损失"由调用方计算为 stressed_el - base_el

基线预期损失为逐笔加总:base_el = sum_i pd[i] * lgd[i] * ead[i]。受冲击预期损失逐笔施加冲击并在 1.0 处封顶——因为概率与 LGD 比率不能超过 1.0:

pd_stressed[i]  = min(1.0, pd[i]  * pd_shock_multiplier)
lgd_stressed[i] = min(1.0, lgd[i] * lgd_shock_multiplier)
stressed_el     = sum_i pd_stressed[i] * lgd_stressed[i] * ead[i]

测试集会戳的五个正确性陷阱。(1) 1.0 处封顶:先乘后写 min(1.0, ...)。漏掉封顶会让单笔受冲击贡献超过 ead[i],stressed_el 可能超过总 EAD——这不可能,因为损失被敞口上界界定。(2) 冲击是乘法不是加法1.5 表示上涨 50%(乘以 1.5),不是"加 1.5 到利率上"。(3) 两个冲击独立作用:PD 冲击只作用 PD,LGD 冲击只作用 LGD,EAD 不变。(4) 逐债务人,不是聚合不要base_el 乘以 pd_shock * lgd_shock——这忽略了逐笔封顶,只要任何一个债务人触顶就错。(5) 顺序:封顶的是乘积,不是输入——写 min(1.0, pd[i]*shock) 而不是 min(1.0, pd[i])*shock

solution([0.02, 0.05, 0.10], [0.40, 0.50, 0.60], [1000.0, 2000.0, 5000.0], 1.5, 1.2) 返回 [358.0, 644.4]

基线步骤。逐笔贡献:债务人 0 0.02*0.40*1000.0 = 8.0;债务人 1 0.05*0.50*2000.0 = 50.0;债务人 2 0.10*0.60*5000.0 = 300.0;求和得 base_el = 358.0

受冲击步骤。先乘冲击,再在 1.0 处封顶。债务人 0:pd_s = min(1.0, 0.02*1.5) = 0.03(不触顶),lgd_s = min(1.0, 0.40*1.2) = 0.48(不触顶),贡献 0.03*0.48*1000.0 = 14.4。债务人 1:pd_s = min(1.0, 0.05*1.5) = 0.075lgd_s = min(1.0, 0.50*1.2) = 0.60,贡献 0.075*0.60*2000.0 = 90.0。债务人 2:pd_s = min(1.0, 0.10*1.5) = 0.15lgd_s = min(1.0, 0.60*1.2) = 0.72,贡献 0.15*0.72*5000.0 = 540.0。求和:stressed_el = 14.4 + 90.0 + 540.0 = 644.4。函数返回 [358.0, 644.4]。交易台对外报告的"压力损失"是 644.4 - 358.0 = 286.4 美元的额外资本要求。

封顶在 1.0 的算例。pd = [0.7]lgd = [0.5]ead = [1000.0]pd_shock = 1.5lgd_shock = 1.0:基线 0.7*0.5*1000 = 350.0。受冲击:pd_s = min(1.0, 0.7*1.5) = min(1.0, 1.05) = 1.0(封顶生效);lgd_s = 0.5;受冲击贡献 1.0*0.5*1000.0 = 500.0。注意 stressed_el = 500.0 <= ead[0] = 1000.0,正是封顶在起作用——若漏掉封顶会得到 1.05*0.5*1000 = 525.0,本例下尚未越过 EAD,但当两个冲击同时把高利率债务人推过 1.0 时(如 pd_shock = lgd_shock = 2.0 作用在已经接近 1 的利率上),不封顶很快就会产出超过 EAD 的荒唐数字。

逐债务人封顶的算例(聚合缩放陷阱)。pd = [0.05, 0.9]lgd = [0.5, 0.5]ead = [1000.0, 1000.0]pd_shock = 2.0lgd_shock = 1.0:基线 0.05*0.5*1000 + 0.9*0.5*1000 = 25 + 450 = 475逐债务人受冲击:债务人 0 pd_s = min(1.0, 0.05*2.0) = 0.1(不触顶),贡献 0.1*0.5*1000 = 50;债务人 1 pd_s = min(1.0, 0.9*2.0) = min(1.0, 1.8) = 1.0触顶),贡献 1.0*0.5*1000 = 500stressed_el = 50 + 500 = 550。错把 base 乘以 pd_shock * lgd_shock = 2.0 给出 475*2.0 = 950,错了 400——正是因为债务人 1 的受冲击 PD 在 1 处饱和,而不是 1.8。逐笔封顶不能穿过聚合求和。

需要明确的细节:len(pd) = 0(且 len(lgd) = len(ead) = 0)返回 [0.0, 0.0]——空求和都是 0;函数不抛异常。pd_shock = lgd_shock = 1.0 返回 [base_el, base_el](无冲击就是恒等)。pd_shock = 0.0lgd_shock = 0.0 返回 [base_el, 0.0],因为每个受冲击因子都被乘 0。当所有 pd[i] = 1.0 且所有 lgd[i] = 1.0 时,受冲击因子全部封顶在 1.0,故 stressed_el = sum_i ead[i] = base_el

期望算法 O(N) 时间、O(1) 辅助空间:一遍扫数组,累加两个标量。无需物化受冲击 PD/LGD 数组。

实现细节由 stubs/stub.py 提供。

实践背景

监管(以及内部风险委员会)要求银行对信用资产组合做压力测试,最简单的一类便是乘性冲击:「PD 全线上涨 50%、LGD 全线上涨 20%、且同时发生」——这是一种监管口径的「严苛但合理」的逆境情景,直接对应 CCAR / EBA 压力测试报表。solution(...) 输出每日基线 EL 与受冲击 EL 这一对数字,交易台再以 stressed_el - base_el 上报「压力情景下需追加的资本」。在 1.0 处的封顶尤为关键:概率与 LGD 比率取值在 [0, 1],当乘性冲击把高 PD 债务人推过 1.0 时必须饱和在 1.0;漏掉封顶会产出超过总敞口的「损失」——损失不能超过敞口,这种数字一旦落到审计上便是红线。其他四个陷阱(加法 vs 乘法、双重冲击同一因子、用聚合倍率代替逐债务人计算、封顶错位)是真实风控团队代码评审里、初级量化把规格搬到生产时最常见的几类错。

约束条件

  • 0 <= len(pd) == len(lgd) == len(ead) <= 200
  • 0.0 <= pd[i] <= 1.0(是概率而不是百分数)
  • 0.0 <= lgd[i] <= 1.0(是比率而不是百分数)
  • 0.0 <= ead[i] <= 1e9(美元,非负)
  • 0.0 <= pd_shock_multiplier <= 10.0 且 0.0 <= lgd_shock_multiplier <= 10.0(乘性比率,不是加性常数)
  • 输出是长度 2 的 list[float]:[base_el, stressed_el];都非负;按 rel_tol=1e-9、abs_tol=1e-9 比对

样例

Case 1 · statement-example: 3 obligors, pd_shock=1.5 lgd_shock=1.2, no clamp fires

输入: [[0.02,0.05,0.1],[0.4,0.5,0.6],[1000,2000,5000],1.5,1.2]

期望: [358,644.4000000000001]

基线 = 8 + 50 + 300 = 358.0。受冲击 = 14.4 + 90.0 + 540.0 = 644.4。无封顶触发。

Case 2 · visible: empty portfolio returns [0.0, 0.0] (not crash)

输入: [[],[],[],1.5,1.2]

期望: [0,0]

空数组。两个空求和都是 0.0。返回 [0.0, 0.0]。

Case 3 · visible: pd_shock=1.0 lgd_shock=1.0 (identity) - stressed_el == base_el

输入: [[0.05,0.1,0.2],[0.4,0.6,0.8],[1000,2000,3000],1,1]

期望: [620.0000000000001,620.0000000000001]

恒等冲击。base_el = stressed_el = 20 + 120 + 480 = 620。

Case 4 · visible: clamp at 1 fires - pd*shock=1.05 saturates to 1.0

输入: [[0.7],[0.5],[1000],1.5,1]

期望: [350,500]

pd*shock=1.05 封顶到 1.0;受冲击 = 1.0*0.5*1000 = 500.0 <= ead=1000。

Case 5 · visible: per-obligor clamp - aggregate scaling gives wrong answer

输入: [[0.05,0.9],[0.5,0.5],[1000,1000],2,1]

期望: [475,550]

债务人 0 不触顶(0.1),债务人 1 封顶到 1.0。受冲击 = 50 + 500 = 550。错误聚合缩放给 475*2 = 950。

Case 6 · typical: single obligor pd=0.10 lgd=0.50 ead=10000, shocks (1.5, 1.2)

输入: [[0.1],[0.5],[10000],1.5,1.2]

期望: [500,900.0000000000001]

基线 = 500。受冲击 pd_s=0.15 lgd_s=0.60,贡献 900。

Case 7 · typical: pd_shock=0.0 - stressed_el=0 regardless of lgd_shock

输入: [[0.05,0.1],[0.5,0.6],[1000,2000],0,1.5]

期望: [145,0]

pd_shock=0 把每个受冲击 PD 归零;stressed_el = 0.0。基线 = 25 + 120 = 145。

Case 8 · typical: lgd_shock=0.0 - stressed_el=0

输入: [[0.05,0.1],[0.5,0.6],[1000,2000],1.5,0]

期望: [145,0]

lgd_shock=0 把每个受冲击 LGD 归零;stressed_el=0。

Case 9 · boundary: all pd=1 all lgd=1 with shocks > 1 - stressed = sum(ead)

输入: [[1,1,1],[1,1,1],[100,200,300],2,3]

期望: [600,600]

所有因子封顶在 1。基线 = 受冲击 = 100+200+300 = 600。

Case 10 · typical: mixed clamp - obligor 0 clamps in PD only, obligor 1 in LGD only

输入: [[0.8,0.1],[0.2,0.9],[5000,4000],1.5,1.5]

期望: [1160.0000000000002,2100.0000000000005]

债务人 0:pd_s=1.0(封顶),lgd_s=0.30。债务人 1:pd_s=0.15,lgd_s=1.0(封顶)。

最近提交

还没有提交记录。

编码区

实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。

加载编辑器...
计时0:00

默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。

Case 1 · statement-example: 3 obligors, pd_shock=1.5 lgd_shock=1.2, no clamp fires

输入: [[0.02,0.05,0.1],[0.4,0.5,0.6],[1000,2000,5000],1.5,1.2]

期望: [358,644.4000000000001]

基线 = 8 + 50 + 300 = 358.0。受冲击 = 14.4 + 90.0 + 540.0 = 644.4。无封顶触发。

Case 2 · visible: empty portfolio returns [0.0, 0.0] (not crash)

输入: [[],[],[],1.5,1.2]

期望: [0,0]

空数组。两个空求和都是 0.0。返回 [0.0, 0.0]。

Case 3 · visible: pd_shock=1.0 lgd_shock=1.0 (identity) - stressed_el == base_el

输入: [[0.05,0.1,0.2],[0.4,0.6,0.8],[1000,2000,3000],1,1]

期望: [620.0000000000001,620.0000000000001]

恒等冲击。base_el = stressed_el = 20 + 120 + 480 = 620。

Case 4 · visible: clamp at 1 fires - pd*shock=1.05 saturates to 1.0

输入: [[0.7],[0.5],[1000],1.5,1]

期望: [350,500]

pd*shock=1.05 封顶到 1.0;受冲击 = 1.0*0.5*1000 = 500.0 <= ead=1000。

Case 5 · visible: per-obligor clamp - aggregate scaling gives wrong answer

输入: [[0.05,0.9],[0.5,0.5],[1000,1000],2,1]

期望: [475,550]

债务人 0 不触顶(0.1),债务人 1 封顶到 1.0。受冲击 = 50 + 500 = 550。错误聚合缩放给 475*2 = 950。

Case 6 · typical: single obligor pd=0.10 lgd=0.50 ead=10000, shocks (1.5, 1.2)

输入: [[0.1],[0.5],[10000],1.5,1.2]

期望: [500,900.0000000000001]

基线 = 500。受冲击 pd_s=0.15 lgd_s=0.60,贡献 900。

Case 7 · typical: pd_shock=0.0 - stressed_el=0 regardless of lgd_shock

输入: [[0.05,0.1],[0.5,0.6],[1000,2000],0,1.5]

期望: [145,0]

pd_shock=0 把每个受冲击 PD 归零;stressed_el = 0.0。基线 = 25 + 120 = 145。

Case 8 · typical: lgd_shock=0.0 - stressed_el=0

输入: [[0.05,0.1],[0.5,0.6],[1000,2000],1.5,0]

期望: [145,0]

lgd_shock=0 把每个受冲击 LGD 归零;stressed_el=0。

Case 9 · boundary: all pd=1 all lgd=1 with shocks > 1 - stressed = sum(ead)

输入: [[1,1,1],[1,1,1],[100,200,300],2,3]

期望: [600,600]

所有因子封顶在 1。基线 = 受冲击 = 100+200+300 = 600。

Case 10 · typical: mixed clamp - obligor 0 clamps in PD only, obligor 1 in LGD only

输入: [[0.8,0.1],[0.2,0.9],[5000,4000],1.5,1.5]

期望: [1160.0000000000002,2100.0000000000005]

债务人 0:pd_s=1.0(封顶),lgd_s=0.30。债务人 1:pd_s=0.15,lgd_s=1.0(封顶)。