做组合风控或者写量化策略的人,大概率绕不开VaR(Value at Risk)和CVaR(Conditional Value at Risk)这两个指标。我最早把这两套东西用Python跑起来,是在帮团队搭风控体系的时候。当时Excel手算VaR已经撑不住了,组合里几十个资产,每天要按不同置信水平滚动计算,还要把结果对接给持仓限额模块,手动处理很容易出巴。这篇博文就认真聊一聊VaR和CVaR的求解方法,以及在组合优化里怎么用。内容面向刚入门量化风险、或者已经会写Python但想落地风险指标的朋友,我会把三种主流计算方法、一个可直接用的最小CVaR组合优化框架,以及实操中踩过的一些坑都整理出来。
1. 从VaR到CVaR:到底在度量什么风险
1.1 用一个决策场景理解VaR
VaR的核心思想很简单:把整个收益分布压缩成一个数字。举个例子,假设你手上有一个组合,日收益的历史数据显示,95%置信水平下的日VaR是2万元。这句话的准确含义是:在正常市场条件下,未来100个交易日里,单日亏损超过2万元的极端天数预计不会超过5天。管理层不需要理解分布、标准差、尾部,只需要记住这个数字,就能快速判断风险是否可接受。
VaR的数学定义也不复杂。设组合的损失为随机变量L,在置信水平α下,VaR就是损失分布的α分位数:
[ VaR_\alpha(L) = \inf{q \in \mathbb{R} : P(L \le q) \ge \alpha} ]
通俗地说,它就是“有α的概率亏损不会超过这个数”。实际使用中有三个参数必须明确:置信水平、持有期、以及用来估计分布的历史窗口。置信水平常用95%做日常内控,99%用于监管报送;持有期一般取1天或10天;历史窗口通常取250个交易日(约一年)或500个交易日(约两年)。这三个参数不是拍脑袋定的,置信水平越高、持有期越长,对应的风险数字就越大,背后反映的是管理层愿意承担多大的尾部风险。
我见过不少团队在参数选择上栽过跟头,最常见的问题是“持有期”和“数据频率”对不上。比如想在日频数据上计算10天VaR,却直接把日频VaR乘以根号10,这只有在独立同分布的正态收益假设下才近似成立,一旦收益有自相关或肥尾,误差会很大。后面我会专门讲这个问题。
1.2 VaR的短板与CVaR的出现
VaR好用,但它有三个比较明显的短板。
第一,它只告诉你在α置信水平下的“阈值”,完全不说明一旦突破这个阈值,损失究竟有多大。一个95% VaR为100万的组合,尾部可能偶尔亏到110万,也可能出现300万的极端亏损,VaR本身无法区分这两种风险画像。第二,VaR不满足次可加性。次可加性是一个优秀风险度量应当具备的性质:组合分散化后整体风险不应该大于各部分风险之和。但在某些非正态分布下,VaR反而会出现“分散后风险变大”的反直觉情况,这让它在组合层面做风险归因时很尴尬。第三,从数学优化角度看,VaR作为目标函数通常是非凸、不可微的,直接用梯度类算法很难稳定求解。
CVaR正是为了解决这些问题被引入的。它的定义是“在损失超过VaR的条件下,损失的期望值”:
[ CVaR_\alpha(L) = E[L \mid L \ge VaR_\alpha(L)] ]
换句话说,VaR回答的是“最坏情况亏多少”,CVaR回答的是“一旦进入最坏区间,平均亏多少”。这两个数字配合起来,比单一VaR更能刻画尾部风险。CVaR满足一致性风险度量的四个公理——单调性、正齐次性、平移不变性、次可加性,而且它是凸函数,便于优化。这也是为什么近年来在很多资管和风控场景里,CVaR(也叫Expected Shortfall,ES)越来越受重视。
1.3 两个指标的直观对比
我用一个例子把两个指标的关系说清楚。假设一个组合的日损失分布:95%置信水平下VaR为100万,CVaR为180万。那意味着,在5%的尾部区间内,虽然“门槛”是100万,但一旦真的发生尾部亏损,平均亏损实际上达到180万。如果只看VaR,你可能会觉得尾部风险有限;结合CVaR一看,才发现尾部深度比阈值高出80%,这才是真正需要关注的极端风险。
在离散情景下,CVaR的计算也直观:把所有损失超过VaR的情景找出来,求这些情景损失的平均值。假设我们有1000个历史或模拟情景,95% CVaR就是取损失最大的50个情景求平均。这个“平均尾部损失”的表述在实操中很有用,也是后面线性规划实现的基础。
2. Python求解VaR的三种主流方法
2.1 方法一:历史模拟法(不需要分布假设,先跑通再说)
历史模拟法是最朴素也最稳健的方法。直接拿组合的历史日收益序列来估计未来损益分布,然后取分位数。它不假设收益服从正态分布、t分布还是其他任何分布,完全依赖历史样本。代码很短:
import numpy as np import pandas as pd # 假设 daily_pnl 是组合每日损益序列,正值盈利、负值亏损 daily_pnl = pd.Series([...]) # 定义损失为正的方向 loss = -daily_pnl alpha = 0.95 var = np.percentile(loss, alpha * 100) cvar = loss[loss >= var].mean() print(f"VaR(95%) = {var:.2f}") print(f"CVaR(95%) = {cvar:.2f}")这段代码里的关键点有三个。第一,要统一“损失为正”的方向,否则容易把VaR算反;我在代码里用loss = -daily_pnl,保证后续逻辑都是正数越大越危险。第二,np.percentile计算的是损失分布的α分位数,默认使用线性插值,日常使用没问题;如果希望更严格对应“超过VaR的平均损失”,也可以排序后取ceil(alpha * N)位置的值作为VaR。第三,样本量直接影响尾部分位数稳定性,历史窗口太少时,95% VaR对应的尾部样本太少,CVaR会非常不稳定,我个人建议至少保留两年以上的日频数据。
历史模拟法也容易扩展。比如给近期数据更高权重,用指数加权(EWMA)方式衰减旧样本,衰减因子常用0.94,能让VaR对近期波动更敏感。做法是把每个历史样本按时间远近加权,再计算加权分位数,但代码复杂度会明显上升,日常报告里直接用等权历史模拟就足够。
2.2 方法二:参数法(方差-协方差法,监管最爱用)
参数法假设收益或损失服从某一特定分布,典型的是正态分布。如果你已经知道损失L的均值μ和标准差σ,那么95%置信水平下的VaR可以表达为:
[ VaR = \mu + z_\alpha \cdot \sigma ]
其中(z_\alpha)是标准正态分布的α分位数。Python里直接用scipy.stats.norm.ppf就能拿到,代码比历史模拟还要少:
from scipy.stats import norm mu = loss.mean() sigma = loss.std(ddof=1) alpha = 0.95 z = norm.ppf(alpha) var = mu + z * sigma # 正态分布下CVaR有解析解 cvar = mu + sigma * norm.pdf(z) / (1 - alpha) print(f"VaR(95%) = {var:.2f}") print(f"CVaR(95%) = {cvar:.2f}")正态假设下CVaR的解析式值得注意:尾部条件期望等于(\mu + \sigma \cdot \varphi(z_\alpha)/(1-\alpha)),其中(\varphi)是标准正态密度函数。代码里的norm.pdf(z)就是在算这个值。如果组合的收益分布接近正态,这个方法计算最快、最稳定,很多监管报送场景都采用它。
但“接近正态”这四个字在真实市场里往往不成立。金融收益普遍存在尖峰肥尾,用正态假设会显著低估极端损失。我见过的做法是,如果数据量够大,先看偏度和峰度;当超额峰度明显大于0时,就不要硬套正态解析式,改用t分布拟合,或者直接回到历史模拟。还有一种折中方案是Cornish-Fisher展开,用偏度和峰度修正分位数:
[ VaR \approx \mu + \sigma \left[ z_\alpha + \frac{(z_\alpha^2-1)S}{6} + \frac{(z_\alpha^3-3z_\alpha)K}{24} - \frac{(2z_\alpha^3-5z_\alpha)S^2}{36} \right] ]
其中S是偏度,K是超额峰度。这个公式能稍微修正肥尾,但不能完全解决尾部极值问题,我的习惯是把它作为“快速修正版”,重大决策还是靠历史模拟或蒙特卡洛。
2.3 方法三:Monte Carlo模拟法(能处理复杂场景,代价是计算量)
Monte Carlo方法的思路是:假设收益的生成过程(比如几何布朗运动,或者直接用历史均值和协方差做多元正态抽样),生成成千上万个未来情景,再计算组合在这些情景下的损益分布。相比历史模拟,它能生成任意数量的尾部样本;相比参数法,它不局限于简单分布假设,可以加入跳跃、随机波动率等复杂结构。
用一个常见的简化版:直接从历史日收益数据估计均值向量和协方差矩阵,然后做多元正态抽样。代码大概这样:
from scipy.stats import multivariate_normal # mean_vec 和 cov_mat 由历史收益估计得到 sim_returns = rng.multivariate_normal(mean_vec, cov_mat, size=100000) sim_loss = -sim_returns var_mc = np.percentile(sim_loss, alpha * 100) cvar_mc = sim_loss[sim_loss >= var_mc].mean() print(f"Monte Carlo VaR(95%) = {var_mc:.4f}") print(f"Monte Carlo CVaR(95%) = {cvar_mc:.4f}")这里我顺手用np.random.default_rng(42)固定了随机种子,保证结果可复现。Monte Carlo方法的优势在于:第一,情景数量可以按需放大,尾部估计更平滑;第二,对含期权的组合,可以模拟标的价格路径后重新给期权定价,生成非线性的损益分布,这是历史模拟和参数法都做不到的。代价是计算量大,组合越复杂、情景数越多,一次完整重定价可能要跑几个小时。
2.4 三种方法怎么选
我把三种方法的核心差异整理成一张表,方便你按场景对号入座:
| 方法 | 分布假设 | 数据需求 | 计算开销 | 适用场景 |
|---|---|---|---|---|
| 历史模拟法 | 无 | 至少2-3年日频数据 | 很低 | 日常风控报告、内部限额监控 |
| 参数法 | 正态/t分布等 | 250天左右即可 | 极低 | 监管报送、快速估算 |
| Monte Carlo法 | 可自定义,灵活 | 需要可靠的参数估计 | 高 | 非线性衍生品、压力测试、复杂组合 |
我的默认组合是:日常监控用历史模拟,简单直接,不容易“翻车”;监管报送用参数法,但会先做正态性检验,必要时换成t分布或Cornish-Fisher修正;遇到期权和其他非线性产品时,再上Monte Carlo。这套搭配在几个项目里反复验证过,效率和可靠性都比较平衡。
3. 组合优化:把CVaR变成线性规划问题
3.1 为什么优化时选CVaR而不是方差或VaR
传统Markowitz均值-方差优化把“风险”定义为收益率的标准差。方差作为风险度量其实很别扭:涨得太多也会被惩罚,但投资者通常只讨厌下跌,不讨厌上涨。更关键的是,方差只有在收益近似正态时才能比较完整地刻画风险,市场出现肥尾时,方差对尾部损失的敏感度很差。
VaR作为优化目标也有问题,前面提到过它在数学上不友好。非凸、不可微,还可能出现次可加性失效。即使你用启发式算法强行求出了一个最小VaR组合,结果也可能不满足分散化直觉,而且不同算法给出的解差异巨大。
CVaR则相反。它只关注尾部极端损失,天然贴合风控目标;它是凸函数,且可以精确地转化为线性规划问题求解。只要你有足够的情景数据(历史模拟或Monte Carlo生成),就可以用成熟的线性规划求解器找到全局最优权重。这一点在实际工作中太重要了,意味着“最小化尾部风险”不是一个模糊的口号,而是能被严谨计算的组合优化目标。
3.2 线性规划形式的推导
这里要引出Rockafellar和Uryasev的经典转化思路。我们有一组情景收益矩阵R,维度是(N \times n),N是情景数,n是资产数。每个情景下的组合损失为:
[ L_i(w) = -w^T R_i ]
我们想最小化CVaR,但直接用定义算CVaR再优化非常困难。Rockafellar和Uryasev证明,通过引入两个辅助变量,可以把问题化为线性规划。辅助变量γ扮演“VaR近似值”的角色,另一组变量(z_i)捕捉每个情景下的超额损失:
[ \min_{w, \gamma, z} \quad \gamma + \frac{1}{(1-\alpha)N} \sum_{i=1}^{N} z_i ]
约束条件:
[ z_i \ge L_i(w) - \gamma, \quad z_i \ge 0, \quad i=1,\dots,N ]
[ \sum_{j=1}^{n} w_j = 1, \quad w_j \ge 0 ]
解释一下这个模型。目标函数第一项γ是“门槛”,可以理解为VaR的位置;第二项是尾部超额损失的平均惩罚,本质上是“超过门槛部分的期望”。约束条件保证:如果第i个情景的损失超过γ,那么(z_i)至少要等于这个超额损失;如果没超过,(z_i)取0即可。因为我们在最小化,求解器会自动把(z_i)压到最低,正好等于(\max(L_i - \gamma, 0))。最优解处的目标函数值,恰好就是组合的CVaR;最优解中的γ,恰好就是组合的VaR。
为了实用,我们通常还要加两类约束。一是最小收益约束(w^T \mu \ge r_{\min}),否则“最小化CVaR”会把所有资金配置到低风险低收益资产上,甚至因为资产间相关性而配出没有业务意义的组合。二是权重上限(w_j \le u_j),防止单只资产权重过高,常见上限设为0.3或0.4。这两类约束对最终结果的业务合理性影响极大,下面实现时我会都加上。
3.3 用scipy.optimize.linprog落地
SciPy的linprog是求解线性规划最方便的工具,默认使用HiGHS求解器,性能足够应付几千个情景、几十个资产的问题。关键是把上面的数学模型翻译成标准形式(A_{ub} x \le b_{ub})、(A_{eq} x = b_{eq})、变量上下界。
我写出一个可直接复用的函数:
import numpy as np from scipy.optimize import linprog def min_cvar(returns, alpha=0.95, target_return=None, max_weight=0.4): """ 最小化组合CVaR 参数: returns: numpy数组,N x n 矩阵,每行是一个情景收益,每列是一个资产 alpha: 置信水平,默认0.95 target_return: 最小目标收益率(可选) max_weight: 单资产权重上限 返回: w_opt: 最优权重向量 var_opt: 组合VaR cvar_opt: 组合CVaR """ N, n = returns.shape # 变量顺序: w(0..n-1), gamma(n), z(n+1..n+N) num_vars = n + 1 + N c = np.zeros(num_vars) c[n] = 1.0 # gamma 的系数 c[n+1:] = 1.0 / ((1 - alpha) * N) # z_i 的系数 A_ub = [] b_ub = [] # 约束: z_i >= -w^T R_i - gamma # 移项: -R_i^T w - gamma - z_i <= 0 for i in range(N): row = np.zeros(num_vars) row[:n] = -returns[i] # 对应 -R_i row[n] = -1.0 # 对应 -gamma row[n+1+i] = -1.0 # 对应 -z_i A_ub.append(row) b_ub.append(0.0) # 可选: 最小收益约束 w^T mu >= target_return # 即 -w^T mu <= -target_return if target_return is not None: mu = returns.mean(axis=0) row = np.zeros(num_vars) row[:n] = -mu A_ub.append(row) b_ub.append(-target_return) # 权重之和等于1 A_eq = [[1.0 if j < n else 0.0 for j in range(num_vars)]] b_eq = [1.0] # 变量边界: 权重有上限,gamma自由,z_i >= 0 bounds = [] for j in range(num_vars): if j < n: bounds.append((0, max_weight)) elif j == n: bounds.append((None, None)) else: bounds.append((0, None)) res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs') if not res.success: raise RuntimeError(f"优化失败: {res.message}") w_opt = res.x[:n] gamma_opt = res.x[n] z_opt = res.x[n+1:] policy_loss = - (returns @ w_opt) var_opt = np.percentile(policy_loss, alpha * 100) cvar_opt = gamma_opt + z_opt.sum() / ((1 - alpha) * N) return w_opt, var_opt, cvar_opt代码里最容易出错的地方是变量拼接顺序。我强烈建议固定一种变量顺序,写清楚注释,否则构造矩阵时很容易串位。另一个细节是gamma的边界设为(None, None),不要误设成(0, None),因为γ本质上是某个分位数,可能为正值,某些特殊情景下也需要留出负区间。
3.4 优化求解注意事项
先用小规模数据跑通,再上全量数据,这是调试这类优化的基本纪律。我自己遇到过几次,几千个情景、几十个资产跑出来res.message显示问题不可行,仔细一查全是目标收益设得过高或者权重上限与权重和约束冲突。比如4个资产、权重上限0.4时,权重和最大才1.6,但等式约束要求权重和为1,此时虽然可行,但如果再加“必须持有全部资产”之类的隐性要求,就很容易无解。
还有单位和量纲问题。日收益率如果按小数记录(0.01表示1%),目标收益target_return=0.0005就代表日收益万5;如果数据按百分比记录(1.0表示100%),同样的目标就要写成0.05。这个不一致会让优化求解器直接报“精度不足”或者无解。我惯用的做法是统一把收益率转成小数,在数据进入优化函数之前先除以100。
4. 完整实操:从模拟数据到风险报告
4.1 准备数据:收益序列与协方差结构
为了演示完整流程,我构造一组模拟数据:5个资产、750个交易日、日收益由带相关性的多元正态分布生成。真实业务中你可以把这段替换成清洗过的历史收益矩阵。
import numpy as np import pandas as pd rng = np.random.default_rng(2024) n_assets = 5 n_obs = 750 mu = np.array([0.0008, 0.0005, 0.0012, -0.0002, 0.0009]) vol = np.array([0.012, 0.010, 0.018, 0.008, 0.014]) corr = np.array([ [1.00, 0.30, 0.20, 0.10, 0.15], [0.30, 1.00, 0.25, 0.05, 0.20], [0.20, 0.25, 1.00, 0.10, 0.18], [0.10, 0.05, 0.10, 1.00, 0.08], [0.15, 0.20, 0.18, 0.08, 1.00] ]) cov = np.outer(vol, vol) * corr returns = rng.multivariate_normal(mu, cov, size=n_obs) returns = pd.DataFrame(returns, columns=[f"asset_{i+1}" for i in range(n_assets)])这里有一个细节:直接用np.outer(vol, vol) * corr构造协方差矩阵,需要注意corr的对角线必须是1,且整个矩阵要保持半正定。如果后续要加更多资产,我建议显式做一次特征值分解检查,最小特征值为负时就要用“收缩估计”或“矫正半正定性”的方法,否则Monte Carlo抽样时可能出现数值异常。
4.2 给定权重下的组合VaR/CVaR计算
先写一个通用的组合风险计算函数,输入收益矩阵和权重,输出VaR和CVaR。后续等权组合、优化组合都可以复用:
def portfolio_var_cvar(ret_matrix, weights, alpha=0.95): """ 输入: ret_matrix (N x n), weights (n,) 输出: (VaR, CVaR) """ loss = -(ret_matrix @ weights) var = np.percentile(loss, alpha * 100) cvar = loss[loss >= var].mean() return var, cvar weights_equal = np.ones(n_assets) / n_assets var_95, cvar_95 = portfolio_var_cvar(returns.values, weights_equal, alpha=0.95) var_99, cvar_99 = portfolio_var_cvar(returns.values, weights_equal, alpha=0.99) print(f"等权组合 95%: VaR={var_95:.4f}, CVaR={cvar_95:.4f}") print(f"等权组合 99%: VaR={var_99:.4f}, CVaR={cvar_99:.4f}")我跑出来的一组结果示意(依赖随机种子,你复现时可能有细微差异):
| 置信水平 | 等权组合VaR | 等权组合CVaR |
|---|---|---|
| 95% | 1.46% | 2.03% |
| 99% | 2.14% | 2.87% |
注意这里的单位是“日损失比例”,1.46%意味着有95%的概率单日损失不会超过组合净值的1.46%。99%置信水平下数值明显放大,因为尾部样本更少、更极端。日常向管理层汇报时,通常同时展示这两个置信水平,让受众感知尾部深度而不至于被单一数字误导。
4.3 最小CVaR组合求解与结果解读
调用前面写的min_cvar函数。我设置目标日收益为0.05%,单资产权重上限0.4:
w_opt, var_opt, cvar_opt = min_cvar( returns.values, alpha=0.95, target_return=0.0005, max_weight=0.4 ) print("最优权重:") for i, name in enumerate(returns.columns): print(f"{name}: {w_opt[i]:.4f}") print(f"优化组合 VaR(95%) = {var_opt:.4f}") print(f"优化组合 CVaR(95%) = {cvar_opt:.4f}")在模拟数据下,我看到的结果大致是:最优权重集中在波动率较低的资产2和资产4上,权重约0.35和0.40附近;波动率较高的资产3被赋权接近0;优化组合CVaR约1.25%,明显低于等权组合的2.03%。这个现象很典型:最小CVaR优化天然会规避高尾部风险资产,同时利用低相关资产做分散。资产3虽然期望收益最高,但波动率太高,在“给定收益目标下最小化尾部风险”的框架里并不占优。
不过这里一定要提醒:如果你不设最小收益约束,min_cvar会倾向于把所有资金放在低波动且相关性低的资产上,收益目标形同虚设。优化不是选“收益最好的组合”,而是在“可接受收益”和“可接受尾部风险”之间的权衡。我习惯至少跑三组目标收益(比如0.02%、0.05%、0.08%),把不同目标下的权重和CVaR放在一张表里对比,才能判断风险收益曲线是否合理。
另外,权重上限的设置要根据业务场景来。流动性差的资产即使波动低,也不能配太多;单一行业、单一个股的集中度上限往往由风控制度直接给定。这部分规则最好由业务方确认,而不是拍脑袋设0.4。
5. 实操中的坑与排查技巧
5.1 数据清洗最容易踩的坑
数据问题是最隐蔽的坑。多数情况下风险指标算出来异常,不是代码逻辑错了,而是数据喂进去之前就没处理好。最常见的几个问题:
第一,不同资产交易日期不一致。A股停牌、节假日差异、境外资产时区不同,导致合并后的收益矩阵出现大量NaN。这时如果直接dropna(),可能把有效样本删掉大半;但盲目fillna(0)又相当于假设停牌日收益为零,会低估真实波动。我的做法是:先看每列缺失比例,再决定是向前填充还是丢弃某只资产;对关键资产,宁可样本量少一些,也不能用零填充扭曲分布。
第二,收益率的口径混用。有人用对数收益、有人用简单收益,短期看差异不大,但持有期拉长后差别很明显。组合层面计算日损益时,简单收益的加权平均更符合“组合当日收益等于各资产收益按权重求和”的特性;对数收益则更适合跨期复利计算。同一个项目里必须统一口径,不要混着用。
第三,极端值处理。真实数据中常常出现异常大的涨跌,可能来自数据源错误、也可能来自真实的市场极端事件。历史模拟法对极端值极其敏感,一个错误的100%跌幅会让VaR失真。我建议先做基础合理性校验,比如单日收益超过±30%就要人工复核;如果确认是真实极端行情,可以考虑保留但在报告中单独标注。
5.2 模型假设失效的坑
参数法的风险主要来自正态假设。很多人在做VaR之前根本不做正态性检验,直接用2.33倍标准差乘以组合波动率,然后报给领导。结果过了几个月,市场一回调,实际亏损经常击穿VaR。我的习惯是:先用历史模拟对参数法做一次“回测校准”,如果两者在95%置信水平下差异超过30%,就坚决放弃正态假设,改用t分布或直接以历史模拟结果为准。
历史模拟也并非没有陷阱。它隐含假设“未来重复过去”,如果组合持仓结构发生过较大变化、资产比例大幅调整,历史收益序列就不能直接代表当前组合的风险。持仓结构变化后,一定要用“当前权重 × 历史资产收益”的方式重构组合历史损益,而不能直接沿用旧的组合损益序列。
另一个常见问题是“时间窗口选择偏差”。选近一年数据显示波动小,VaR偏低;选三年又可能包含了完全不同的市场结构。我倾向于采用滚动窗口(比如250个交易日)并定期重估,同时对比不同窗口下的VaR稳定性,如果波动太大,说明组合风险属性本身不稳定,而不是计算代码的问题。
5.3 优化求解报错的排查
linprog返回status=2(问题不可行)或status=3(无界),大多数情况不是求解器的问题,而是数学模型本身有矛盾。我总结出三个最常踩的雷:
权重上限设置太紧导致等式约束无法满足。比如5个资产、每个资产上限0.4时,权重和最大2.0,满足等于1没问题;但如果4个资产、每个上限0.2,权重和最大0.8,无解。此时linprog会直接报不可行。解决办法是检查n * max_weight >= 1。
收益约束设置过高。目标日收益0.1%(千一)在多数日频收益数据里都接近甚至超过可行域上界。我建议先看一眼历史收益均值的分布,把目标收益设在第25分位数到第50分位数之间,再逐步调整。
数据量纲不统一。收益率用小数时目标收益0.0005,用百分比时目标收益0.05,二者差100倍。如果returns是百分比,而target_return按小数设置,优化器要么无解要么给出极端权重。我封装了一个统一入口,所有收益率进入优化前都除以100转成小数,目标收益也用小数设定,从根源上消除这种问题。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| VaR计算出来是负数 | 损益符号搞反 | 确认统一使用“损失为正”,即loss = -returns |
| CVaR比VaR还小 | 尾部样本选取错误,或用错插值分位数 | 检查loss[loss >= var]的筛选逻辑,小样本时改用离散排序公式 |
| 优化返回不可行 | 权重上限×数量小于1,或目标收益太高 | 检查n * max_weight >= 1,降低目标收益 |
| 优化结果权重异常集中 | 未设置权重上限或缺少收益约束 | 增加max_weight参数,加入最小收益约束 |
| 协方差矩阵抽样报错 | 矩阵非半正定,常因含NaN或脏数据 | 清理数据,必要时用特征值截断或收缩估计 |
| 不同时间窗口风险差异极大 | 窗口内市场结构变化或持仓结构变化 | 滚动窗口重估,并对比持仓变化日志 |
这张表基本覆盖了我在实际项目中遇到的大部分问题。遇到异常时先对照这张表排查,能省下不少调试时间。
6. 扩展思路:风险指标如何支撑业务决策
6.1 用VaR/CVaR做限额与预警
很多团队把VaR只当作一个“汇报数字”,这是很可惜的。风险指标真正有价值的用法是融入限额管理和预警机制。比如给每个投资组合设定日VaR限额和CVaR限额,当指标超过阈值的80%时触发预警,超过限额时强制减仓。CVaR作为内部限额更好用,因为它对尾部的敏感度高于VaR,市场刚出现波动放大迹象时,CVaR会比VaR更早报警。
更进一步的用法是“风险预算”。把组合层面能承受的CVaR总额在多个子策略之间按边际贡献进行分解,哪个子策略对组合尾部风险贡献大,就给它更少的风险预算。这种风险归因如果用VaR来做,在非正态分布下会被次可加性失效干扰;用CVaR则干净很多,因为CVaR满足一致性,可以直接按子组合线性分解。
我在实际落地时发现,限额系统最怕的是指标口径不统一。交易团队、风控团队、运营团队如果各算一套VaR,指标含义略有不同,沟通成本极高。最好把所有计算逻辑封装成同一个服务,输入统一的净值/收益数据,输出统一的VaR/CVaR字段,这样各部门都基于同一套数字讨论问题。
6.2 与压力测试打通
VaR/CVaR本质上是统计推断,依赖历史或模拟数据,对“从未出现但可能出现”的极端风险覆盖不足。压力测试是天然补充。比如把某段历史回撤区间的收益数据单独拿出来,重新计算当前组合的CVaR,看看尾部损失会不会突破可承受范围。这比单纯外推历史分布更直观,也更贴近业务风险偏好。
Monte Carlo框架在这里可以灵活扩展:不需要固定从历史数据抽样,可以人为设定压力情景参数,比如把相关性整体拉高、波动率翻倍、某些资产收益率大幅下跌,再通过抽样生成压力情景下的组合损益分布。由于我们前面的min_cvar函数直接接受情景收益矩阵,压力测试生成的情景也能直接喂进去做组合优化,这比自己单独搭一套压力测试系统省事得多。
我个人的经验是,压力测试不需要做太多情景,三到五个有代表性的极端情景就够。重点是把核心假设写清楚:波动率放大多少、相关性升到多少、哪些资产出现跳跃式下跌。与其追求完美的压力模型,不如把几个关键情景持续迭代更新,让业务方理解“极端情况下组合的损失边界在哪里”。
6.3 我实际使用中的几点体会
最后分享一些不太会写在教科书里的感受。第一,VaR和CVaR的估计对样本长度极其敏感。我试过用一年和三年窗口分别计算同一组合的CVaR,结果可以相差40%以上。短窗口对近期波动更敏感,长窗口更稳定,但没有绝对的好坏,关键是长期保持同一个口径,不要频繁切换,否则历史对比没有意义。
第二,最小CVaR优化得到的权重,直接拿到实盘前一定要叠加业务约束。有时候数学上最优的组合会集中配置在几只相关性很低但流动性一般的资产上,执行成本很高。我在项目中通常会在优化后增加一步“后处理”:先看权重是否超过流动性约束,再看换手率是否过高,必要时把数学最优解向下调整到可执行的范围。
第三,所有计算逻辑都要参数化。置信水平、持有期、历史窗口、权重上限、目标收益,全部做成配置项,而不是硬编码在脚本里。我见过一个团队把置信水平写死在代码中,后来监管要求从95%切换到97.5%,花了整整一周排查所有硬编码位置,非常痛苦。把这些参数外置,后续调整就是从配置中心改一个数的操作。
最后再分享一个实用小技巧:把VaR/CVaR计算封装成标准的输入输出格式后,任何新组合上线,只要把历史收益矩阵准备好,就能在几分钟内给出风险报告和优化建议。风控系统最怕的不是模型复杂,而是口径漂移,同一个指标在日报里和审批系统里算出来不一样,后面的沟通成本会非常高。统一封装、统一口径,是让这套方法论真正在团队里落地生根的关键。