拓冰建站拓冰建站
首页 / 资讯中心 / 正文

静态CoVaR计算全解析:分位数回归与Python实现

做金融风控的人对VaR肯定不会陌生。但单家机构的VaR管不了“你出事我也跟着出事”这层风险。CoVaR这个指标就是专门干这个的它衡量的是当某一家机构陷入尾部风险时另一家机构的条件风险价值。计算CoVaR的标准方法是分位数回归尤其静态CoVaR逻辑清晰、计算量小、可解释性强非常适合做截面风险溢出分析和压力测试的底稿。这篇文章我从头梳理静态CoVaR的计算逻辑给出一个可以直接复现的Python案例把分位数回归、VaR估计、ΔCoVaR的计算代码全部贴出来并附上我在实操中踩过的坑。适合金融风控、量化研究、宏观审慎监管相关岗位的同学参考。1. CoVaR是什么从单家风险到系统性风险1.1 传统VaR的局限性只看自己没看别人VaRValue at Risk风险价值是金融风险管理中最基础的指标之一。它回答的问题是在给定置信水平下某家机构或某个资产组合在未来一段时间内的最大可能损失是多少。比如某只股票日收益率在5%分位数上是-3.2%那它的5% VaR就是-3.2%。这个数字意味着有95%的概率单日亏损不会超过3.2%。但问题在于VaR是一个“孤立”的指标。它只刻画了单一机构自身的风险完全没有考虑这家机构出问题时会不会把其他机构也拖下水。2008年金融危机给全行业上了一课雷曼兄弟自己的VaR看起来也许还在“可控范围”内但它的倒闭引发了整个金融体系的连锁反应。这种“你爆我也爆”的联动风险传统VaR测不了。1.2 CoVaR如何捕捉风险溢出效应CoVaR的全称是Conditional Value at Risk条件风险价值。由Adrian和Brunnermeier在2016年的经典论文中提出。它的核心思想是当机构i处于尾部风险状态收益率为VaR时机构j的条件VaR是多少这个“条件”是关键。CoVaR把单家机构的风险放到了“传染网络”里去看。如果CoVaR_q(j|i)显著低于机构j自身的VaR_q说明机构i的困境会显著放大机构j的风险。两者差距越大风险溢出越强。更精确一点的指标是ΔCoVaR它用机构i处于尾部风险时的机构j的条件VaR减去机构i处于正常状态中位数水平时机构j的条件VaR。这个差值把“因为i陷入困境而额外增加的j的风险”单独剥离出来是衡量系统性风险溢出的核心量化指标。1.3 为什么静态CoVaR依然值得掌握CoVaR有静态和动态两种计算思路。动态CoVaR比如基于DCC-GARCH或者时变参数模型可以让CoVaR随宏观环境变化逐点估计静态CoVaR则是用全样本做一次分位数回归估计出平均意义上的风险溢出水平。静态方法看起来“笨”但实践中非常实用计算逻辑简单透明监管沟通和论文复现都方便。对样本量的要求比动态模型低数据量少也能算。适合做横截面比较比如同一时点哪些机构是系统重要性机构。作为压力测试的补充指标静态CoVaR可以给出“平均尾部依赖强度”的参照为进一步做动态分析打基础。所以不要觉得静态方法过时了。它反而是理解CoVaR逻辑最好的入口也是很多实证研究的起点。2. 分位数回归CoVaR的标准估计引擎2.1 分位数回归与OLS的本质区别普通最小二乘回归OLS估计的是因变量Y在给定自变量X时的条件均值E(Y|X)。它关注的是“平均水平”。分位数回归Quantile Regression恰恰相反它估计的是Y在给定X时的条件分位数比如5%分位数、50%分位数、95%分位数。这个区别在风险度量场景下是致命的。金融收益率分布常常是尖峰厚尾的。均值附近的“平均影响”和尾部区域的“极端影响”可能是完全不同的逻辑。我们做风险管理关心的恰恰是尾部而不是均值。OLS把尾部信息平均掉之后等于把最危险的信息给抹平了。分位数回归不假设误差服从正态分布对异常值和厚尾分布更稳健。这正是它成为CoVaR标准估计方法的原因。2.2 检查函数与目标函数分位数回归如何工作分位数回归的估计原理是这样的。对于分位数τ定义检查函数check functionρ_τ(u) u * (τ - I(u 0))其中I是指示函数当u 0时I取1否则取0。这个函数的特点是对负残差和正残差的惩罚不对称从而可以“卡”住特定分位数。分位数回归的目标函数是最小化min_β Σ ρ_τ(y_i - x_iβ)展开来看这个目标函数等价于min_β [ τ * Σ_{y_i ≥ x_iβ} (y_i - x_iβ) (1-τ) * Σ_{y_i x_iβ} (x_iβ - y_i) ]也就是对正残差加权τ负残差加权(1-τ)。当τ较大比如0.95时回归线会更贴近上尾的数据点当τ较小比如0.05时回归线会贴近下尾的数据点。因为这个目标函数在残差为0处不可导所以无法用普通的梯度下降或最小二乘法求解。statsmodels库内部使用线性规划方法单纯形法或内点法来求解Python中直接调用sm.QuantReg即可R里有quantreg包。2.3 两步建模框架从VaR到ΔCoVaR的推导静态CoVaR的估计遵循Adrian-Brunnermeier论文中的两步框架。第一步估计机构i的自身VaR。用分位数回归把机构i的收益率X_i对滞后状态变量M回归X_t^i α^i γ^i M_{t-1} ε_t^i在τ分位下机构i的VaR就是VaR_τ^i α^i γ^i (M的均值)这里把M取样本均值得到的是样本期内的静态VaR。严格来说M是时变的所以VaR也是时变的但静态版本取均值得到的是一个平均意义上的尾部风险水平。第二步建立机构j对机构i的条件分位数回归X_t^j α^(j|i) β^(j|i) X_t^i γ^(j|i) M_{t-1} ε_t^j在τ分位下代入X_t^i VaR_τ^i得到CoVaR_τ^(j|i) α^(j|i) β^(j|i) * VaR_τ^i γ^(j|i) * (M的均值)再计算正常状态下的条件风险。取τ 0.5先估计中位数回归得到机构i的正常水平VaR_50%^i代入同样的公式CoVaR_50%^(j|i) α^(j|i) β^(j|i) * VaR_50%^i γ^(j|i) * (M的均值)最终ΔCoVaR_τ^(j|i) CoVaR_τ^(j|i) - CoVaR_50%^(j|i) β^(j|i) * (VaR_τ^i - VaR_50%^i)注意这个公式最后化简得特别干净ΔCoVaR只和两个量有关——条件回归中机构i对机构j的系数β以及机构i自身尾部风险与中位数水平的差距。系数β反映了尾部依赖强度β的绝对值越大说明机构i的困境对机构j的溢出效应越强。2.4 状态变量的选择与处理状态变量M的作用是控制宏观环境对收益率的影响避免把宏观冲击误判为机构间的风险溢出。Adrian-Brunnermeier原文中使用了一组市场状态变量包括VIX波动率指数、期限利差10年期国债-3个月国库券、信用利差Baa级企业债-Aaa级企业债、三个月Libor与隔夜指数互换利率的利差、标普500收益率、房地产超额收益率。国内做类似研究时通常做如下调整类别国外常用国内常用市场波动VIX50ETF期权隐含波动率或GARCH(1,1)估计波动率期限利差10Y国债-3M国库券10Y国债-1Y国债信用利差Baa-Aaa企业债利差AA级企业债-国债利差或中短期票据利差流动性利差3M Libor-OISSHIBOR 3M-回购定盘利率FR007市场收益标普500沪深300或中证全指房地产房价指数国房景气指数或百城住宅价格指数实操中状态变量要滞后一期。因为回归的解释逻辑是用t-1期的宏观状态和t期机构i的收益率来解释t期机构j的收益率。状态变量滞后一期可以部分缓解内生性问题这也是原论文的标准做法。3. 案例准备数据、样本与预处理细节3.1 案例设定银行和券商的风险联动我选一个在国内比较有代表性的组合来做案例招商银行代表股份制银行和中信证券代表头部券商。逻辑上券商对银行的尾部风险溢出在A股市场非常典型。银行如果出现流动性风险或资产质量恶化会迅速传导至券商的信用业务和自营投资反过来券商如果暴跌也会通过股权质押、两融业务向银行传导。这一对组合的尾部依赖关系值得量化。样本区间我选2018年1月1日到2023年12月31日正好覆盖了2018年去杠杆、2020年疫情冲击、2022年市场调整等多个宏观波动期。数据频率用日度收益率。3.2 收益率计算与数据对齐先从数据源获取收盘价序列。建议使用前复权收盘价避免分红送转带来的价格跳空。收益率采用对数收益率r_t ln(P_t / P_{t-1})对数收益率的好处是时间上可加且近似正态性比简单收益率更好在分位数回归中数值上也更平稳。由于银行、券商、指数和宏观变量的交易日可能不完全一致必须做一次严格的数据对齐df pd.concat([cmb, citics, hs300, term_spread, credit_spread, shibor], axis1, keys[cmb, citics, hs300, term, credit, shibor]) df df.dropna().copy()用dropna把某个交易日缺失的观测整行剔除虽然会损失少量样本但能保证所有回归的样本区间完全一致避免因为数据口径不同产生偏差。3.3 状态变量构造与滞后处理我用四个状态变量市场收益率沪深300指数对数收益率控制市场整体涨跌。期限利差变动10年期国债到期收益率减去1年期国债到期收益率取日度变化值。信用利差变动AA级企业债到期收益率减去国债到期收益率取日度变化值。货币市场流动性SHIBOR 3M的日度变化值。状态变量统一做滞后一期处理state_vars [hs300, term, credit, shibor] for var in state_vars: df[var _lag] df[var].shift(1) df df.dropna()这样最后进入回归模型的状态变量就是带_lag后缀的滞后变量保证信息流的时间顺序是“用昨天的宏观状态预测今天”。4. Python代码实现从零搭建静态CoVaR计算管线4.1 环境与依赖配置只需要四个库pandas、numpy、statsmodels、matplotlib。statsmodels是分位数回归的核心其他都是辅助。import numpy as np import pandas as pd import statsmodels.api as sm import matplotlib.pyplot as pltstatsmodels的QuantReg是现成的不需要自己写优化算法。但有一点必须提醒QuantReg对数据框格式有一定要求直接传numpy数组和传DataFrame的处理方式不太一样后续坑的部分我会专门讲。4.2 核心代码分位数回归与CoVaR计算函数先把单次分位数回归封装成函数def quantile_reg(y, X, q): X sm.add_constant(X) model sm.QuantReg(y, X) res model.fit(qq, max_iter1000) return res然后写计算VaR和CoVaR的函数。VaR的估计也走分位数回归路线def estimate_var(returns_i, state_vars, q): X state_vars.copy() res quantile_reg(returns_i, X, q) state_mean X.mean().values var_q res.params[const] np.dot(res.params[X.columns].values, state_mean) return var_q, res注意这里state_mean是样本均值对应静态VaR的定义。如果你想要时变VaR把这一行改成逐点计算即可。再写CoVaR核心计算函数def estimate_covar(returns_i, returns_j, state_vars, q): # 第一步估计机构i在q分位下的自身VaR var_i_q, res_i estimate_var(returns_i, state_vars, q) var_i_med, res_i_med estimate_var(returns_i, state_vars, 0.5) # 第二步机构j对机构i的条件分位数回归 X state_vars.copy() X[x_i] returns_i res quantile_reg(returns_j, X, q) res_med quantile_reg(returns_j, X, 0.5) # 第三步计算CoVaR状态变量取均值 state_mean state_vars.mean().values base res.params[const] np.dot(res.params[state_vars.columns].values, state_mean) covar_q base res.params[x_i] * var_i_q covar_med res_med.params[const] np.dot(res_med.params[state_vars.columns].values, state_mean) res_med.params[x_i] * var_i_med d_covar covar_q - covar_med return { var_i_q: var_i_q, var_i_med: var_i_med, covar_q_j_given_i: covar_q, covar_med_j_given_i: covar_med, d_covar: d_covar, beta: res.params[x_i], reg_res: res, reg_res_med: res_med }这段代码的逻辑和前面推导的公式严格对应。核心就是三步先算机构i自己的VaR再做条件分位数回归最后用回归系数和VaR代入公式得到CoVaR。4.3 完整案例代码与示例输出把上面的函数串起来跑完整案例。我用模拟数据演示完整的代码流程因为真实数据涉及数据源权限不方便在博客里直接贴完整文件但逻辑完全一致。你把你的真实收益率数据填进df对应的列就行。# 读取并处理数据的伪代码请替换为你的实际数据源 # df pd.read_excel(your_data.xlsx, index_col0) # 确保df包含 cmb, citics, hs300_lag, term_lag, credit_lag, shibor_lag 这些列 # 以模拟数据演示 np.random.seed(42) n 1200 df_sim pd.DataFrame({ cmb: np.random.normal(0.0002, 0.012, n), citics: np.random.normal(0.0001, 0.018, n), hs300_lag: np.random.normal(0.0003, 0.010, n), term_lag: np.random.normal(0, 0.0005, n), credit_lag: np.random.normal(0, 0.0003, n), shibor_lag: np.random.normal(0, 0.0002, n), }) df_sim[cmb] 0.5 * df_sim[hs300_lag] 0.3 * df_sim[cmb].shift(1).fillna(0) np.random.normal(0, 0.01, n) df_sim[citics] 0.8 * df_sim[hs300_lag] 0.5 * df_sim[cmb] np.random.normal(0, 0.015, n) df_sim df_sim.dropna().reset_index(dropTrue) # 定义列名 state_cols [hs300_lag, term_lag, credit_lag, shibor_lag] target_col citics # 机构j被影响方 cond_col cmb # 机构i触发方 # 计算 q 0.05 时的静态CoVaR result estimate_covar(df_sim[cond_col], df_sim[target_col], df_sim[state_cols], q0.05) # 输出结果 print( 静态CoVaR计算与Delta CoVaR ) print(pd.Series(result))示例输出大致长这样var_i_q -0.0187 var_i_med 0.0012 covar_q_j_given_i -0.0453 covar_med_j_given_i -0.0098 d_covar -0.0355 beta 0.5730需要注意这个模拟输出只是演示格式真实数据下β的符号和大小会根据金融机构的实际关联度变化。但计算逻辑是完全一致、可以直接替换数据的。4.4 可视化CoVaR与VaR的对比算完之后画个图把机构j的无条件VaR、条件CoVaR和ΔCoVaR放在一起对比能直观看出溢出效应的大小var_j_q, _ estimate_var(df_sim[target_col], df_sim[state_cols], 0.05) fig, ax plt.subplots(figsize(10, 6)) bars ax.bar([VaR_j(5%), CoVaR(5%|i), ΔCoVaR(5%)], [var_j_q, result[covar_q_j_given_i], result[d_covar]], color[#4C72B0, #DD8452, #C44E52]) ax.axhline(0, colorblack, linewidth0.8) ax.set_ylabel(Return) ax.set_title(Conditional Value at Risk Comparison) for bar in bars: height bar.get_height() ax.annotate(f{height:.4f}, xy(bar.get_x() bar.get_width()/2, height), xytext(0, 3), textcoordsoffset points, hacenter) plt.tight_layout() plt.show()这个图对汇报场景特别有用。领导/合作方只要看到“CoVaR比自身VaR还低一截”立刻就能理解风险溢出的含义比纯数字解释高效得多。5. 结果解读系数、CoVaR与ΔCoVaR怎么用5.1 分位数回归系数揭示了什么分位数回归输出的β系数是解读的核心。β 0.573的含义是当机构i的收益率变动1个百分点时机构j在5%分位处的条件收益率变动0.573个百分点。这里有个细节值得注意β是在5%分位点的尾部依赖系数它跟OLS估计的β可能差别很大。OLS回归估计的是“平均联动”而分位数回归在5%分位点估计的是“极端下跌时的联动”。尾部依赖系数β_tail通常大于均值依赖系数这才是CoVaR能识别出系统性风险的原因。实操中建议同时跑多个分位点1%、5%、10%、50%、90%、95%、99%画出“β随分位点变化”的曲线。如果两端的β明显高于中间说明机构间存在“非对称尾部依赖”即它们在极端行情下的联动更强。这在监测系统性风险时是一个非常重要的信号。5.2 CoVaR与ΔCoVaR的经济含义CoVaR_5%(j|i) -4.53%这句话的准确解读是当机构i处于5%分位数的风险水平即它正在经历尾部损失时机构j的收益率在5%概率下将低于-4.53%。注意这不是说机构j明天的期望亏损是4.53%而是说在“i已经出事”这个条件下j的尾部风险显著放大。ΔCoVaR_5% -3.55%的解读更直接由于机构i从正常状态跌入尾部风险状态机构j的5%条件VaR额外增加了3.55个百分点的潜在损失。这个3.55个百分点就是纯粹由机构i引发的溢出效应。如果把它乘上机构j的杠杆率或者资产规模就可以粗略估算系统性损失的传导量级。5.3 稳健性检验与置信区间讨论分位数回归的系数和CoVaR都是估计值有抽样误差。严谨的做法是报告置信区间。statsmodels的QuantReg结果自带系数协方差矩阵可以用bootstrap方法获取置信区间res.bootstrap(nsim1000, sizelen(df_sim)) ci res.conf_int(alpha0.05)Adrian-Brunnermeier论文中的标准做法就是在稳健性检验里跑不同的分位点、不同的状态变量组合、不同的样本区间查看ΔCoVaR符号和大小是否稳定。我自己的实操习惯是换分位点q 0.01、0.05、0.10都跑一遍看β是否单调变化。换状态变量去掉某个状态变量看结果是否稳健避免结果被单一宏观因子驱动。换样本区间比如排除2020年疫情期间的数据测试结果是否由极少数极端日驱动。Bootstrap置信区间用非参数bootstrap算ΔCoVaR的95%置信区间看是否显著异于0。如果以上检验都通过这个CoVaR结果才算是可信的。6. 实操中的坑与排查技巧实录6.1 statsmodels QuantReg的常见陷阱sm.QuantReg这个API有几个容易踩的坑。第一不加常数项的问题。很多时候你会忘记加sm.add_constant(X)结果截距被强制为0回归的残差结构完全变了CoVaR整体偏移。这是最常见的一个错误。第二数据框的列名问题。如果X是DataFrame且里面有非数值类型或NaN模型拟合时会抛错。务必在建模前做一次df.info()检查列类型并用dropna()清洗。第三fit(q0.05, max_iter1000)的收敛问题。有时候默认迭代次数不够模型会报Linear programming failed to converge错误。把max_iter调大或者换fit_method参数试其他求解器。res model.fit(q0.05, max_iter5000)第四样本量太小时分位数回归容易不稳定。分位数回归的求解依赖数据点的排序样本量少于几百时结果波动很大。日度数据一般都够但如果是月度数据尽量保证样本期在5年以上。6.2 数据频率与样本量对结果的影响用日度数据和月度数据算出来的CoVaR量级差别会很大。日度收益率的波动比月度小所以日度5%分位点的VaR通常在-3%到-5%区间月度5%分位点可能到-10%甚至更低。这不是bug只是频率不同导致的自然差异。关键是不能混用状态变量如果用的日度收益率也必须用日度保持一致。我在一个项目里遇到过同事把日度收益率和月度宏观利差放一起跑结果CoVaR完全失真后来排查半天才发现是频率不匹配。6.3 极端值导致的分位数回归系数异常金融收益率序列里偶尔会出现极端值比如2020年美股多次熔断、2024年初A股流动性冲击。这些极端值会让分位数回归结果变得很不稳定。我曾经在算券商对银行的ΔCoVaR时发现β突然变成负值后来逐步排查发现是样本里有一天券商因为配股除权导致价格异常下跌而银行当天没什么波动这一对异常值把尾部回归的斜率拉歪了。解决办法有两个对收益率做缩尾处理winsorize比如把上下1%的极端值替换为1%和99%分位数。剔除异常日并重新跑回归进行对比确认β是否反转。def winsorize_series(s, lower0.01, upper0.99): lo, hi s.quantile([lower, upper]) return s.clip(lo, hi)但要小心做风险管理的人天然不愿意剔除极端值因为极端值本身就是尾部风险的信息。我在实践中采取的策略是主回归用原始数据稳健性检验里换缩尾数据。两者方向一致结论才算扎实。6.4 从静态到动态后续扩展方向静态CoVaR是基础版跑通之后建议往两个方向扩展。一是滚动窗口法。用过去250个交易日为窗口逐日滚动估计CoVaR可以得到一条ΔCoVaR时间序列。这个方法的优点是理解起来直观缺点是对突发性风险的反应有滞后。二是引入更多状态变量的时变参数模型。比如状态变量降低法State Variable Approach直接让CoVaR随M_t变化不用滚动窗口也能逐点估计。也可以用分位数回归与DCC-GARCH结合但复杂度会指数级上升计算时间和调试成本都比较高。我的建议是静态CoVaR是地基先在它上面跑通逻辑确保每一步推导和代码都没问题再上滚动窗口或者动态模型。很多团队一上来直接整DCC-GARCH结果代码跑出一个完全不合理的结果连自己都解释不了就是因为基础逻辑没吃透。最后再分享一个实操中的体会CoVaR计算本身不算难难的永远是数据质量的把控和结果的经济解释。你花一晚上写的代码可能十分钟就跑完了剩下大把时间全在清洗数据、核对口径、反复验证。我最早把CoVaR代码跑通的时候也激动了一下但后来真正在报告里写出“某券商对某银行的尾部风险溢出为-3.5个百分点”这句话之前做了整整一周的敏感性测试。做风险研究慢就是快这个指标最终是要拿来做决策参考的每一步都得经得起推敲。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门