第 3 章 · 04 CVaR 条件风险价值手写实现(对应 QS040)


文档摘要

第 3 章 · 04 CVaR 条件风险价值手写实现(对应 QS040) 速查摘要:本节不调任何风险库,从零手写 VaR 和 CVaR,把"尾部风险度量"的本质看清。数据是 OEX(S&P 100 原 90 只成分,2014-2016)。流程: 算收益 → 减均值去均值化 → 随机权重 → 手写 用 算 95% 分位点 → 先取 VaR,再对所有小于 VaR 的收益求 (尾部期望损失) → 直方图标注 VaR 实线、CVaR 虚线对比。手写一遍,你才真正理解"VaR 是分位点、CVaR 是超过分位点的损失均值"。

第 3 章 · 04 CVaR 条件风险价值手写实现(对应 QS040)

速查摘要:本节不调任何风险库,从零手写 VaR 和 CVaR,把"尾部风险度量"的本质看清。数据是 OEX(S&P 100 原 90 只成分,2014-2016)。流程:Close.pct_change(fill_method=None) 算收益 → 减均值去均值化 → 随机权重 scale(x)=x/sum(|x|) → 手写 value_at_risknp.percentile(portfolio_returns, 100*(1-alpha)) 算 95% 分位点 → cvar 先取 VaR,再对所有小于 VaR 的收益求 nanmean(尾部期望损失) → 直方图标注 VaR 实线、CVaR 虚线对比。手写一遍,你才真正理解"VaR 是分位点、CVaR 是超过分位点的损失均值"。

涉及脚本:原项目 QS040-cvar/01_cvar.py(约 104 行)

⚠️ 注意:OEX 这个列表里有些 ticker 在 2014-2016 已退市或更名(如 EMCBRK-B 写法),yfinance 可能取不到或返回 NaN。脚本靠 fillna(0.0)nanmean 容错。本节的重点是理解算法本质,不是回测严谨性——如果想跑出干净结果,建议换成现役 S&P 100 成分。

学习目标

阅读完本节,你应当能够:

  1. np.percentile 手写历史 VaR,说清 95% VaR 的分位点含义。
  2. 用"VaR 阈值 + 尾部均值"手写 CVaR,解释为什么 CVaR 比 VaR 更不乐观。
  3. 解释 CVaR 满足次可加性(子可加)而 VaR 不满足,以及 Basel III 偏好 CVaR 的原因。
  4. dot(weights) 算组合收益、用 iloc[-lookback_days:] 取回看窗口。

工具与原理

VaR(Value at Risk,风险价值)

VaR 回答一个问题:"在 95% 置信水平下,这个组合未来一段时间内最多亏多少?" 数学定义:

VaR_α = -inf{x : P(loss > x) ≤ 1 - α}

用历史模拟法,就是把历史收益排序,取 (1-α) 分位点。95% VaR 就是第 5 百分位(最差的 5% 边界)。脚本里 np.percentile(portfolio_returns, 100*(1-alpha))alpha=0.95 就是取第 5 百分位。

VaR 的关键缺陷:它只告诉你"阈值在哪",不告诉你"超过阈值之后有多惨"。一个组合可能在 5% 尾部亏 6%,另一个可能在尾部亏 60%,但两者 95% VaR 完全相同。这是 VaR 致命的盲区——它对尾部形状视而不见。

CVaR(Conditional VaR,条件风险价值,又称 ES/Expected Shortfall)

CVaR 弥补了 VaR 的盲区,它问:"如果跌破了 VaR,平均会亏多少?"

CVaR_α = -E[loss | loss > VaR_α]

即"超过 VaR 阈值的那部分损失的期望"。脚本实现:

var_pct_loss = var / value_invested np.nanmean(portfolio_returns[portfolio_returns < var_pct_loss])

portfolio_returns[portfolio_returns < var_pct_loss] 取出所有比 VaR 阈值更差的收益(尾部),nanmean 求平均——这就是 CVaR 的全部秘密。它天然包含了尾部形状信息:尾部越肥,CVaR 比 VaR 大得越多。

次可加性:为什么 Basel III 放弃 VaR

次可加性(subadditivity)指"组合的风险 ≤ 各部分风险之和",即分散化应该降低风险:

ρ(A + B) ≤ ρ(A) + ρ(B)

VaR 不满足次可加性:两个资产单独算的 VaR 加起来,可能比组合的 VaR 还小——这在金融上荒谬(分散持仓反而显得更危险)。原因在于 VaR 是分位点,分位点之间没有线性关系。

CVaR 满足次可加性(Rockafellar & Uryasev 2000 证明),作为一致性风险度量(Coherent Risk Measure),它在数学上"行为正确"。这就是为什么巴塞尔协议 III(Basel III)在市场风险监管(BRTF 2019 起的 FRTB)里把 VaR 换成预期尾部损失 ES(即 CVaR)——监管宁愿要一个数学性质好的度量。

脚本精读

1. 90 只 OEX 成分 + 去均值

oex = ['MMM','T','ABBV','ABT','ACN','ALL','GOOGL','GOOG','MO','AMZN','AXP','AIG', 'AMGN','AAPL','BAC','BRK-B','BIIB','BLK','BA','BMY','CVS','COF','CAT','CVX', 'CSCO','C','KO','CL','CMCSA','COP','DHR','DUK','DD','EMC','EMR','EXC','XOM', 'META','FDX','F','GD','GE','GM','GILD','GS','HAL','HD','HON','INTC','IBM','JPM', 'JNJ','KMI','LLY','LMT','LOW','MA','MCD','MDT','MRK','MET','MSFT','MS','NKE', 'NEE','OXY','ORCL','PYPL','PEP','PFE','PM','PG','QCOM','SLB','SPG','SO','SBUX', 'TGT','TXN','BK','USB','UNP','UPS','UNH','VZ','V','WMT','WBA','DIS','WFC'] data = yf.download(oex, start='2014-01-01', end='2016-04-04') returns = data.Close.pct_change(fill_method=None) returns = returns - returns.mean(skipna=True)
  • pct_change(fill_method=None):显式禁用前向填充。新版 pandas 默认 fill_method='ffp'(用前值填缺失),在金融数据里会掩盖真实的停牌/退市,应该关掉。
  • returns - returns.mean(skipna=True):去均值化。组合层面的"风险"只关心波动、不关心漂移,减掉均值把所有股票放在"零漂移"基准上比较。注意这是用全程均值减,严格回测应该用滚动均值,本脚本简化处理。

2. 随机权重 + scale

def scale(x): return x / np.sum(np.abs(x)) weights = scale(np.random.random(num_stocks))

scale 把权重归一化到绝对值之和为 1(支持做空时用绝对值,本脚本全正即等价于 x/sum(x))。随机生成只是演示——真实场景应该用前几节学的优化器出权重,这里为了聚焦 VaR/CVaR 算法本身。

3. 手写 VaR

def value_at_risk(value_invested, returns, weights, alpha=0.95, lookback_days=500): returns = returns.fillna(0.0) portfolio_returns = returns.iloc[-lookback_days:].dot(weights) return np.percentile(portfolio_returns, 100 * (1 - alpha)) * value_invested

三步:填 0取回看窗口取分位点

  • returns.iloc[-lookback_days:].dot(weights):最后 500 天的收益矩阵(500×90)点乘权重向量(90,),得到 500 个组合日收益。这是"用历史权重组合出组合收益"的标准写法。
  • np.percentile(portfolio_returns, 100*(1-alpha)):当 alpha=0.95,取第 5 百分位——这是 95% VaR 的"分位点定义"。
  • * value_invested:从收益率转回金额(本脚本 value_invested=100_000,即 10 万美元)。

注意 VaR 是负数(因为是左尾亏损)。* value_invested 后,VaR 表示"亏损金额",负值代表亏多少。

4. 手写 CVaR

def cvar(value_invested, returns, weights, alpha=0.95, lookback_days=500): var = value_at_risk(value_invested, returns, weights, alpha, lookback_days=lookback_days) returns = returns.fillna(0.0) portfolio_returns = returns.iloc[-lookback_days:].dot(weights) var_pct_loss = var / value_invested return np.nanmean(portfolio_returns[portfolio_returns < var_pct_loss]) * value_invested

四步:先算 VaR重算组合收益(代码冗余但清晰) → 把 VaR 转回百分比阈值取所有比阈值更差的收益求 nanmean

  • portfolio_returns < var_pct_loss:布尔掩码,挑出"比 VaR 阈值更差"的尾部收益。
  • nanmean:用 nanmean 而不是 mean,跳过 NaN(90 只里总有几只停牌产生 NaN,普通 mean 会报错或返回 NaN)。

CVaR 的本质就这一句:"超过 VaR 的损失,平均多深"

5. 直方图双线对比

portfolio_returns = returns.fillna(0.0).iloc[-lookback_days:].dot(weights) portfolio_VaR = value_at_risk(value_invested, returns, weights) portfolio_VaR_return = portfolio_VaR / value_invested portfolio_CVaR = cvar(value_invested, returns, weights) portfolio_CVaR_return = portfolio_CVaR / value_invested plt.hist(portfolio_returns[portfolio_returns > portfolio_VaR_return], bins=20) plt.hist(portfolio_returns[portfolio_returns < portfolio_VaR_return], bins=10) plt.axvline(portfolio_VaR_return, color='red', linestyle='solid') plt.axvline(portfolio_CVaR_return, color='red', linestyle='dashed')
  • 两段直方图:VaR 右边(正常区,20 桶)和 VaR 左边(尾部,10 桶)分开画,尾部用了更少桶是因为样本少。
  • VaR 实线:正常收益和尾部的分界,即第 5 百分位。
  • CVaR 虚线:在 VaR 左边更深处——这是"尾部均值",永远 ≤ VaR(亏损更大)。

直观对比:VaR 实线和 CVaR 虚线的距离 = 尾部肥瘦程度。距离大说明尾部肥(黑天鹅风险高),距离小说明尾部薄(亏损集中在阈值附近,可控)。

关键技巧

  1. fill_method=None 是金融数据卫生。pandas 2.x 起 pct_change 默认会前向填充,会把停牌/退市的缺失偷偷填上前值,导致你算出的收益"虚假平滑"。任何对真实缺失敏感的指标(VaR/CVaR 尤其)都应显式 fill_method=None,再决定怎么处理 NaN。

  2. dot(weights) 是组合收益的标准写法。收益矩阵(T×n)点乘权重(n,)→ 组合收益(T,),远比 for 循环逐资产加权快。这也是为什么权重通常存成 numpy 数组而非列表。

  3. iloc[-lookback_days:] 是回看窗口。500 天约 2 年。窗口太短分位点估计噪声大、太长又把"过时"的波动算进来。FRTB 标准是最近 1 年(约 250 天)的历史窗口,本脚本用 500 是经验值。

  4. nanmean 而非 mean。90 只成分总有停牌、退市、合并产生的 NaN。用 mean 会得到 NaN、用 np.nanmean 自动跳过。同理 nanstd/nansum 都是金融代码常客。

  5. CVaR 的监管优势是次可加性。Basel III / FRTB 用 CVaR(称 ES,Expected Shortfall)替代 VaR,正是看中它的次可加性——分散化在 CVaR 框架下永远降低风险,符合金融直觉。VaR 不满足,这意味着按 VaR 算资本充足率,可能出现"拆成两个子公司算反而资本要求更低"的荒谬结果。

  6. 去均值化的取舍。脚本用 returns - returns.mean(skipna=True),这把全程均值减掉。严格做"当日 VaR"应该用滚动均值(当日之前 N 天的均值),否则有前视偏差——用未来数据估计均值去减当日收益。本脚本为了简化用了全程均值,演示算法本质,生产环境要改滚动。

💡 速查要点:VaR 用 np.percentile(returns, 100*(1-alpha)) 取分位点,CVaR 用 nanmean(returns[returns < var_pct]) 取尾部均值。一个分位点、一个尾部期望,差出尾部肥瘦。组合收益用 dot(weights),回看用 iloc[-lookback_days:],缺值用 nanmean 跳过。CVaR 满足次可加性所以 Basel III 用它。

本节要点

  1. VaR 是给定置信水平(如 95%)的最大损失分位点,np.percentile(returns, 5) 即可;只知阈值不知尾部多惨,是它的盲区。
  2. CVaR(ES) 是超过 VaR 的尾部损失均值,nanmean(returns[returns < var_pct]),比 VaR 更不乐观、对尾部形状敏感。
  3. 次可加性:CVaR 满足(分散化降低风险),VaR 不满足——所以 Basel III/FRTB 监管改用 CVaR。
  4. 手写要点:dot(weights) 算组合收益、iloc[-lookback_days:] 回看窗口、fill_method=None 不前向填充、nanmean 容错。
  5. 直方图双线:VaR 实线是分界、CVaR 虚线更深(尾部均值),两线距离反映尾部肥瘦。

下一节,我们用数值积分求 Kelly 凯利公式的最优杠杆——最大化长期复利增长,看看为什么"理论最优 f"可能大于 1(需要加杠杆)。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U