第 6 章 · 03 CDaR 条件回撤 本节摘要:半方差盯单日下行,CVaR 盯单日尾部,但都没考虑「价值持续走弱的时间维度」——投资者对「连跌半年账户腰斩」的痛感远大于「单日暴跌后立刻反弹」。CDaR(Conditional Drawdown at Risk,条件回撤风险)直接对齐「回撤」这一最直观的痛感指标:它衡量「最严重的 (1-β) 比例时段内的平均回撤」。本节讲清 类:基于 Chekhlov-Rockafellar-Uryasev (2005) 的凸化,把回撤序列的尾部期望写成线性规划,引入 α(回撤阈值)、u(累计收益)、z(超出)三组辅助变量求解。读完本节,你能用 CDaR 做优化,并说清它与 CVaR 的关键差异。 内容来源:原项目源码 、文档 ,汉化并套用体系化模板。
本节摘要:半方差盯单日下行,CVaR 盯单日尾部,但都没考虑「价值持续走弱的时间维度」——投资者对「连跌半年账户腰斩」的痛感远大于「单日暴跌后立刻反弹」。CDaR(Conditional Drawdown at Risk,条件回撤风险)直接对齐「回撤」这一最直观的痛感指标:它衡量「最严重的 (1-β) 比例时段内的平均回撤」。本节讲清
EfficientCDaR类:基于 Chekhlov-Rockafellar-Uryasev (2005) 的凸化,把回撤序列的尾部期望写成线性规划,引入 α(回撤阈值)、u(累计收益)、z(超出)三组辅助变量求解。读完本节,你能用 CDaR 做优化,并说清它与 CVaR 的关键差异。
内容来源:原项目源码
pypfopt/efficient_frontier/efficient_cdar.py、文档docs/GeneralEfficientFrontier.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
Drawdown(回撤):从历史峰值到当前值的相对跌幅,衡量「账面从最高点滑落了多少」。
D(w, r, t) = max_{τ<t}(wᵀr(τ)) - wᵀr(t) = 历史累计峰值 - 当前累计值
它和单日损失有本质差别:
| 视角 | 度量 | 直觉 |
|---|---|---|
| 单日 | 收益 rₜ 的损失 | 「今天跌了 3%」 |
| 累计 | 回撤 D(w,r,t) | 「相比历史最高,我账户已腰斩」 |
💡 核心心法:CVaR 把每个时点当成独立样本,只看「单日最惨」;CDaR 把回撤作为带时间结构的序列,衡量「持续走弱期」。一个资产可能单日 VaR/CVaR 都小,但持续阴跌几个月,回撤巨大——只有 CDaR 能捕捉这种痛感。
类比 CVaR,CDaR 是回撤序列的尾部期望:
CDaR(w, β) = (1/(1-β)) ∫_{D(w,r,t) ≥ α} D(w,r,t) p(r(t)) dr(t)
Chekhlov-Rockafellar-Uryasev (2005) 证明:同 CVaR 一样,可以用凸函数 + 线性规划求解。
from pypfopt import expected_returns, EfficientCDaR mu = expected_returns.mean_historical_return(df) returns = expected_returns.returns_from_prices(df) ecd = EfficientCDaR( expected_returns=mu, returns=returns, beta=0.95, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None, )
源码 __init__ 创建了比 CVaR 多一个变量 z:
self._alpha = cp.Variable() # 回撤阈值 (DaR) self._u = cp.Variable(len(self.returns) + 1) # 累计净值序列(多 1 个初值) self._z = cp.Variable(len(self.returns)) # 回撤超出量
核心约束 _add_cdar_constraints 把回撤的递推关系写出来:
def _add_cdar_constraints(self) -> None: self.add_constraint(lambda _: self._z >= self._u[1:] - self._alpha) # z = max(DaR 超出) self.add_constraint( lambda w: self._u[1:] >= self._u[:-1] - self.returns.values @ w # u = 累计净值递推 ) self.add_constraint(lambda _: self._u[0] == 0) # 初值 self.add_constraint(lambda _: self._z >= 0) self.add_constraint(lambda _: self._u[1:] >= 0)
u 序列递推地模拟「累计净值」:u[t+1] = max(u[t] - rₜ·w, ...),这是「running max」的线性化。然后 z 把所有超过 α 的回撤抓出来,目标函数最小化 α + 平均 z。
| 方法 | 含义 | 数学形式 |
|---|---|---|
min_cdar() |
最小化 CDaR | min α + (1/((1-β)T))·Σz |
efficient_risk(target_cdar) |
给定 CDaR 上限下最大化收益 | max μᵀw, s.t. CDaR ≤ target |
efficient_return(target_return) |
给定收益下最小化 CDaR | min CDaR, s.t. μᵀw ≥ target |
父类的不可用方法:
def min_volatility(self): raise NotImplementedError("Please use min_cdar instead.") def max_sharpe(self, risk_free_rate=0.0): raise NotImplementedError("Method not available in EfficientCDaR.") def max_quadratic_utility(self, risk_aversion=1, market_neutral=False): raise NotImplementedError("Method not available in EfficientCDaR.")
portfolio_performance 返回 (期望收益, CDaR):
mu, cdar_val = ecd.portfolio_performance(verbose=True) # Expected annual return: 18.0% # Conditional Drawdown at Risk: 12.34%
| 对比维度 | CVaR | CDaR |
|---|---|---|
| 输入 | 单期收益 | 累计收益序列 |
| 关注 | 单日尾部损失 | 累计回撤尾部 |
| β=1 退化 | VaR(单日最大损失) | 最大回撤 |
| 时间维度 | 无(每个时点独立) | 有(依赖历史峰值) |
| 辅助变量 | α, u 两组 | α, u, z 三组 |
| 投资者直觉 | 风控部门 | 基金投资人 |
| 适合场景 | 短期、监管 | 长期、对回撤敏感的资金 |
💡 β=1 = 最大回撤:如果你想直接最小化最大回撤,把 beta 设为接近 1(但
0 ≤ β < 1校验不让你设 1,可用 0.99 近似)。这是 CDaR 最直觉的入口。
from pypfopt import expected_returns, EfficientCDaR df = ... mu = expected_returns.mean_historical_return(df) returns = expected_returns.returns_from_prices(df) ecd = EfficientCDaR(mu, returns, beta=0.95) ecd.min_cdar() weights = ecd.clean_weights() print(weights) ecd.portfolio_performance(verbose=True)
⚠️ 样本要求更高:CDaR 要观察「峰值-当前」的累计模式,对样本长度的要求比 CVaR 更严——必须包含至少一个完整的「涨-跌-涨」周期,否则回撤估计完全失真。建议至少 5 年日线数据。
z 辅助变量(共 α、u、z 三组),通过 u 序列模拟累计净值,递推地构建回撤。min_cdar、efficient_risk(target_cdar)、efficient_return(target_return);输出 (收益, CDaR)。至此第 6 章三种通用风险度量讲完。下一章我们转到「收益端」——Black-Litterman 模型,从市场市值反推「市场隐含收益」作为更稳健的先验。