用于 CVA 聚合的存活加权期望敞口剖面
Survival-Weighted Expected-Exposure Profile for CVA Aggregation
开始编码实现 solution(ee_profile: list[float], marginal_pd: list[float]) -> list[float]。某衍生品交易对手的 CVA 聚合器需要按"交易对手在该期开始时还活着"为条件的 per-period 敞口——只有当交易对手进入第 t 期时还活着,银行才会在第 t 期蒙受违约损失。给定 per-period 无条件期望敞口(EE)剖面和 per-period 边际违约概率期限结构(条件于存活的 hazard),返回喂给 CVA 求和的存活加权 EE 剖面。
形式上,定义运行存活概率
Q[-1] := 1.0 (满初始存活)
Q[t] := Q[t-1] * (1 - marginal_pd[t]) for t = 0, 1, ..., T-1以及存活加权 EE
weighted_ee[t] := ee_profile[t] * Q[t-1] for t = 0, 1, ..., T-1注意:weighted_ee[t] 用的是 Q[t-1](到第 t-1 期末/第 t 期开始时的累计存活),不是 Q[t]。返回长度 T 的列表 weighted_ee。
这个剖面是从原始 EE/PD 曲线走到一份合理存活加权的 CVA 美元费用过程的"后半段"计算。一旦拿到 weighted_ee,标准 CVA 聚合即为
CVA = LGD * sum_t (weighted_ee[t] * marginal_pd[t] * DF[t])(姊妹题 coding-unilateral-cva-independence 在已加权 EE 剖面与无条件边际 PD 曲线下完成美元 CVA 聚合;本题完成存活加权这一步)。
算例
考虑 ee_profile = [1_000_000, 900_000, 800_000, 700_000, 600_000]、marginal_pd = [0.02, 0.018, 0.016, 0.014, 0.012]。逐期维护 Q:
t=0:Q = 1.0,weighted_ee[0] = 1,000,000 * 1.0 = 1,000,000。更新Q = 1.0 * (1 - 0.02) = 0.98。t=1:weighted_ee[1] = 900,000 * 0.98 = 882,000。更新Q = 0.98 * (1 - 0.018) = 0.96236。t=2:weighted_ee[2] = 800,000 * 0.96236 = 769,888。更新Q = 0.96236 * (1 - 0.016) = 0.94696...。t=3:weighted_ee[3] = 700,000 * 0.94696... ≈ 662,873.57。更新Q ≈ 0.93371...。t=4:weighted_ee[4] = 600,000 * 0.93371... ≈ 560,222.86。
所以 solution(...) 返回 [1_000_000, 882_000, 769_888, 662_873.568, 560_222.861...]。
五个易错点
第一个易错点是循环内 **Q[t-1] 与 Q[t] 的顺序**。weighted_ee[t] 用的是第 t 期起始时刻的存活概率—— Q[t-1],到 t-1 期末为止的累计存活——不是第 t 期默认更新后的 Q[t]。具体地,循环必须 (1) 先用当前 Q 计算 weighted_ee[t] = ee_profile[t] * Q,然后 (2) 才更新 Q *= (1 - marginal_pd[t])。把顺序反过来(先更新 Q 再加权)会让每一期都被多乘一个 (1 - mp[t]) 因子。在算例中,错序写法会得到 weighted_ee[0] = 1,000,000 * 0.98 = 980,000 而不是 1,000,000——第一期就错,且偏差逐期累计放大。
第二个易错点是 **Q[-1] = 1 的初值**。在 t=0 时存活权重是 Q[-1] = 1.0(满初始存活——按假设交易对手在交易开始时还活着),因此 weighted_ee[0] = ee_profile[0] * 1.0 = ee_profile[0]。把 Q = 1 - marginal_pd[0](提前一步)作初值会让第 0 期被低估 (1 - mp[0]) 倍。干净的实现以 Q = 1.0 作为初值,先计算再更新——初值陷阱自动消失。
第三个易错点是乘性而非加性的存活演化。条件存活递推式是 Q[t] = Q[t-1] * (1 - marginal_pd[t]),不是 Q[t] = Q[t-1] - marginal_pd[t]。把每期存活率 (1 - mp[t]) 乘进 Q 才能让 Q 解释为 prod_{s<=t} (1 - mp[s]) = P(过完第 t 期后还活着)。加性减法在累计 mp 超过 1 时(per-period bound mp[t] <= 1 是允许的)会让 Q 变负,即便没变负也不对应任何概率意义上的存活路径。以 mp = [0.3, 0.3, 0.3, 0.3] 为例,正确 Q 序列是 [0.7, 0.49, 0.343, 0.2401];而加性减法会得到 [0.7, 0.4, 0.1, -0.2](出现负值!)——只有乘性形式正确。
第四个易错点是 **marginal_pd[t] = 1 是吸收边界**。若任意 mp[t] = 1.0,运行 Q 将在更新步骤坍塌为 0,且所有 k >= 1 的 weighted_ee[t+k] 都为 0。但 weighted_ee[t] 本身是在更新之前计算的,仍使用未坍塌的 Q[t-1](通常非 0)。参考实现通过乘性更新 Q * (1 - 1.0) = 0 自然处理——无需特例。但要小心:在计算当期贡献之前提前 if mp[t] == 1.0: break,会错误地把当期也置零。
第五点是 CVA 聚合恒等式——一个有用的自检。sum_t (weighted_ee[t] * marginal_pd[t]) 等于期限 T 内的期望违约时刻敞口(即 LGD=1、所有 DF=1 时的 CVA)。等价地,姊妹题 coding-unilateral-cva-independence 在 LGD=1 且 DF[t]=1 时给出的 CVA 与该和相等。规范作者可以用这个恒等式独立交叉验证存活加权剖面。
边界
T == 0:空输出[](无任何期)。- 所有
marginal_pd[t] == 0.0:Q始终为1.0,weighted_ee == ee_profile逐元素相等(视野内零违约风险的交易对手)。 marginal_pd = [1.0, *, *, ...]:weighted_ee[0] = ee_profile[0];weighted_ee[t] = 0fort >= 1(交易对手在第 0 期违约——Q自第 1 期起坍塌为 0)。- 所有
ee_profile[t] == 0.0:无论marginal_pd取值,weighted_ee全为 0(无敞口可加权)。 T == 1:weighted_ee = [ee_profile[0]](用Q[-1] = 1;marginal_pd[0]不影响唯一输出值——只对超出区间的期才会起作用)。
本函数不抛异常——调用方保证两个列表长度一致、所有输入在声明范围内。
期望算法:单遍 O(T)——维护单个运行存活概率 Q(初值 1.0);在每一期 t:先计算 weighted_ee[t] = ee_profile[t] * Q,然后更新 Q *= (1 - marginal_pd[t])。这个顺序就是易错点 #1 的全部内容;反过来每一期都会少加权。T <= 60 足够小,浮点累加顺序在 1e-9 容差下无关紧要。
实现细节由 stubs/stub.py 提供。
实践背景
衍生品交易台的 CVA 系统把每个交易对手交易的信用费用算成"交易对手违约时未来预期损失的现值"。标准流程分三步:(1) 模拟或计算 per-period 无条件期望敞口剖面 ee_profile[t](不发生违约时的期望正向重置价值);(2) 做本题 solution(...) 完成的存活加权——把每期敞口按"在该期开始时还活着"为条件加权;(3) 聚合 LGD * sum_t (weighted_ee[t] * marginal_pd[t] * DF[t]) 得到美元 CVA。第 (2) 步在数学上是必需的,因为第 t 期违约只会在交易对手进入该期时还活着的情形下造成损失——银行不会再损失给已不存在的交易对手。跳过存活加权(即把原始 ee_profile 直接代入 CVA 求和)会高估 CVA,期限越长、违约率越高,偏差越大。本题做存活加权剖面构造;姊妹题 coding-unilateral-cva-independence 做美元 CVA 聚合;coding-cumulative-default-prob-from-marginal-hazard-rates 产出累计 PD 期限结构(相关但不同的量);coding-marginal-pd-from-cumulative-term-structure 是反向的期限结构分解;coding-portfolio-expected-loss-by-bucket 是单期 EL(无时间剖面、无存活加权)。存活加权 EE 剖面也是 IFRS 9 衍生品头寸 stage-2/3 终身 ECL 计算的一个构件,亦是融资成本 (FVA)、资本 (KVA) 等同类(hazard-conditioned 现金流结构)调整的基础。
约束条件
- 0 <= T == len(ee_profile) == len(marginal_pd) <= 60
- 0.0 <= ee_profile[t] <= 1e9(per-period 无条件期望敞口、美元、非负)
- 0.0 <= marginal_pd[t] <= 1.0(per-period **边际**违约概率——给定存活到 0..t-1 期之后、第 t 期违约的概率)
- 输出为长度 T 的 `list[float]`,非负,按 rel_tol=1e-9、abs_tol=1e-9 比对
- 函数名为 `solution`,复杂度 O(T) 时间、O(T) 空间
样例
Case 1 · statement-example: T=5 swap-like profile
输入: [[1000000,900000,800000,700000,600000],[0.02,0.018,0.016,0.014,0.012]]
期望: [1000000,882000,769888,662873.568,560222.8611839999]
T=5:Q[-1]=1,weighted_ee[0]=1e6;Q[0]=0.98,weighted_ee[1]=900000*0.98=882000;以此类推。
Case 2 · typical: T=1 single period, weighted_ee=[ee[0]]
输入: [[1000000],[0.05]]
期望: [1000000]
T=1:Q[-1]=1,weighted_ee[0]=ee[0]*1=1e6(输出与输入 EE 同样、与 PD 无关)。
Case 3 · boundary: T=0 empty deal returns []
输入: [[],[]]
期望: []
T=0:空合约、空输出 []。
Case 4 · boundary: all marginal_pd=0 (riskless) returns ee_profile unchanged
输入: [[100,200,300],[0,0,0]]
期望: [100,200,300]
所有 mp=0:Q 始终为 1,weighted_ee==ee_profile。
Case 5 · boundary: marginal_pd[0]=1 absorbs (Q drops to 0 from t=1)
输入: [[100,200,300],[1,0.5,0.5]]
期望: [100,0,0]
mp[0]=1:weighted_ee[0]=ee[0]*Q[-1]=100*1=100;之后 Q=0,weighted_ee[t]=0 for t>=1。
Case 6 · adversarial: marginal_pd[1]=1 absorbs mid-stream (Q drops at t=1->t=2)
输入: [[100,200,300,400],[0.1,1,0.5,0.5]]
期望: [100,180,0,0]
mp[1]=1:t=0,1 正常计算(weighted_ee[0]=100、weighted_ee[1]=200*0.9=180),update 后 Q=0,t>=2 weighted_ee=0。
Case 7 · boundary: all ee_profile=0 returns all zeros
输入: [[0,0,0],[0.1,0.2,0.3]]
期望: [0,0,0]
所有 ee=0:weighted_ee 全 0、与 PD 无关。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: T=5 swap-like profile
输入: [[1000000,900000,800000,700000,600000],[0.02,0.018,0.016,0.014,0.012]]
期望: [1000000,882000,769888,662873.568,560222.8611839999]
T=5:Q[-1]=1,weighted_ee[0]=1e6;Q[0]=0.98,weighted_ee[1]=900000*0.98=882000;以此类推。
Case 2 · typical: T=1 single period, weighted_ee=[ee[0]]
输入: [[1000000],[0.05]]
期望: [1000000]
T=1:Q[-1]=1,weighted_ee[0]=ee[0]*1=1e6(输出与输入 EE 同样、与 PD 无关)。
Case 3 · boundary: T=0 empty deal returns []
输入: [[],[]]
期望: []
T=0:空合约、空输出 []。
Case 4 · boundary: all marginal_pd=0 (riskless) returns ee_profile unchanged
输入: [[100,200,300],[0,0,0]]
期望: [100,200,300]
所有 mp=0:Q 始终为 1,weighted_ee==ee_profile。
Case 5 · boundary: marginal_pd[0]=1 absorbs (Q drops to 0 from t=1)
输入: [[100,200,300],[1,0.5,0.5]]
期望: [100,0,0]
mp[0]=1:weighted_ee[0]=ee[0]*Q[-1]=100*1=100;之后 Q=0,weighted_ee[t]=0 for t>=1。
Case 6 · adversarial: marginal_pd[1]=1 absorbs mid-stream (Q drops at t=1->t=2)
输入: [[100,200,300,400],[0.1,1,0.5,0.5]]
期望: [100,180,0,0]
mp[1]=1:t=0,1 正常计算(weighted_ee[0]=100、weighted_ee[1]=200*0.9=180),update 后 Q=0,t>=2 weighted_ee=0。
Case 7 · boundary: all ee_profile=0 returns all zeros
输入: [[0,0,0],[0.1,0.2,0.3]]
期望: [0,0,0]
所有 ee=0:weighted_ee 全 0、与 PD 无关。