第 6 章 · 01 半方差优化


文档摘要

第 6 章 · 01 半方差优化 本节摘要:均值-方差优化把「涨」和「跌」一视同仁地惩罚,但投资者真正怕的是跌,而不是涨。半方差(Semivariance)只惩罚低于目标收益的「下行波动」,更贴合真实风险偏好。本节讲清 类:它继承自 ,但不再喂协方差矩阵,而是喂历史收益 DataFrame,内部用 cvxpy 引入 个变量求解一个凸的均值-半方差规划。配套指标从 Sharpe 换成 Sortino。读完本节,你能用半方差替换方差出权重,并说清它与均值-方差在「下行偏差」上的本质差异。 内容来源:原项目源码 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 EfficientSemivariance 替换 做半方差优化。

第 6 章 · 01 半方差优化

本节摘要:均值-方差优化把「涨」和「跌」一视同仁地惩罚,但投资者真正怕的是跌,而不是涨。半方差(Semivariance)只惩罚低于目标收益的「下行波动」,更贴合真实风险偏好。本节讲清 EfficientSemivariance 类:它继承自 EfficientFrontier,但不再喂协方差矩阵,而是喂历史收益 DataFrame,内部用 cvxpy 引入 2T+N 个变量求解一个凸的均值-半方差规划。配套指标从 Sharpe 换成 Sortino。读完本节,你能用半方差替换方差出权重,并说清它与均值-方差在「下行偏差」上的本质差异。

内容来源:原项目源码 pypfopt/efficient_frontier/efficient_semivariance.py、文档 docs/GeneralEfficientFrontier.rst,汉化并套用体系化模板。

学习目标

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

  1. EfficientSemivariance 替换 EfficientFrontier 做半方差优化。
  2. 说清 下行风险(downside) 与对称方差的区别。
  3. 解释 semideviation、Sortino 这两个核心指标。
  4. 区分 min_semivariance / efficient_risk / efficient_return 三个目标方法。
  5. 知道为什么半方差必须喂 历史收益 DataFrame 而非协方差。

一、为什么需要半方差:下行才是真风险

经典马科维茨模型用方差 wᵀΣw 度量风险,把「向上偏离」也当成坏事。但真实世界里:

  • 涨得多是好事,不该被惩罚。
  • 跌得多才是坏事,投资者真正怕的是亏。
  • 收益分布偏斜厚尾,上下并不对称。

半方差只统计「低于某个基准 benchmark 的偏差」,数学定义是:

semivariance = (1/T) Σ max(benchmark - rₜ, 0)² × frequency semideviation = √semivariance

对应到 PyPortfolioOpt 的源码逻辑(portfolio_performance 中):

portfolio_returns = self.returns @ self.weights drops = np.fmin(portfolio_returns - self.benchmark, 0) # 只留低于 benchmark 的部分 semivariance = np.sum(np.square(drops)) / self._T * self.frequency semi_deviation = np.sqrt(semivariance)

💡 核心心法:方差 = 全体偏离的平方均值;半方差 = 仅负偏离的平方均值。半方差是把方差中「正向」那一半置零,因此永远 ≤ 方差。它更精确地刻画了「最坏情况下的波动」。

二、EfficientSemivariance 的构造:喂收益而非协方差

EfficientSemivariance 继承自 EfficientFrontier,但构造函数签名变了:不要协方差矩阵,要历史收益。

from pypfopt import expected_returns, EfficientSemivariance mu = expected_returns.mean_historical_return(df) returns = expected_returns.returns_from_prices(df) # 关键:历史日收益 DataFrame es = EfficientSemivariance( expected_returns=mu, returns=returns, frequency=252, benchmark=0, # 区分上下行的阈值,默认 0(日级),要与 returns 同频率 weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None, )

源码 __init__ 干了两件关键事:

super().__init__( expected_returns=expected_returns, cov_matrix=np.zeros((returns.shape[1],) * 2), # 占位用的假协方差 weight_bounds=weight_bounds, ... ) self.returns = self._validate_returns(returns) self.benchmark = benchmark self.frequency = frequency self._T = self.returns.shape[0] # 记录样本天数 T

⚠️ 重要差异:它给父类塞了个全零的假协方差矩阵——父类的协方差通路被绕开,真正算风险用的是 self.returns(T×N 的历史收益)。所以你不必、也不应该自己提供协方差。

benchmark 是区分「上下行」的收益阈值,默认 0,意思是「日收益 < 0 就算下行」。它必须与 returns 同频率——日收益就配日级 benchmark,年化收益就配年化 benchmark。

三、四个目标方法:min_semivariance 等

由于风险度量换了,父类的 min_volatility / max_sharpe 在子类里直接抛 NotImplementedError,你得改用半方差版的方法:

def min_volatility(self): raise NotImplementedError("Please use min_semivariance instead.") def max_sharpe(self, risk_free_rate=0.0): raise NotImplementedError("Method not available in EfficientSemivariance")

下表对应四个可用方法:

方法 含义 对应 EfficientFrontier
min_semivariance() 最小化组合半方差 min_volatility()
max_quadratic_utility(risk_aversion) 最大化下行二次效用 max_quadratic_utility()
efficient_risk(target_semideviation) 给定半偏差下最大化收益 efficient_risk()
efficient_return(target_return) 给定收益下最小化半偏差 efficient_return()

min_semivariance 的内部数学

源码用「正负偏差分解」技巧把半方差写成凸规划。引入两个非负变量 p(正向超出)和 n(负向超出):

p = cp.Variable(self._T, nonneg=True) n = cp.Variable(self._T, nonneg=True) self._objective = cp.sum(cp.square(n)) # 只惩罚负向 B = (self.returns.values - self.benchmark) / np.sqrt(self._T) self.add_constraint(lambda w: B @ w - p + n == 0) # 每日的正负分解 self._make_weight_sum_constraint(market_neutral)

物理意义:对每个历史日 t,B[t] @ w = p[t] - n[t],要么正向要么负向,半方差就是 Σ n²,只惩罚负的部分。

💡 变量数翻倍:半方差优化变量数是 2T + N(T 个 p、T 个 n、N 个权重)。50 个资产 + 3 年日线 ≈ 1500 个变量。规模一上来,solver 容易报错,保持问题小而干净

四、portfolio_performance:Sortino 取代 Sharpe

优化完调 portfolio_performance(verbose=True),输出三件套:

mu, semi_deviation, sortino_ratio = es.portfolio_performance(verbose=True) # Expected annual return: 20.0% # Annual semi-deviation: 9.1% # Sortino Ratio: 2.18

Sortino 是 Sharpe 的「下行版」,分母从标准差换成 semideviation:

Sortino = (μ - R_f) / semideviation
指标 分子 分母 适用
Sharpe 超额收益 全体标准差 收益对称正态
Sortino 超额收益 下行 semideviation 收益偏斜、只怕跌

五、完整示例:最小半方差换 20% 收益

from pypfopt import expected_returns, EfficientSemivariance df = ... # 你的价格 DataFrame mu = expected_returns.mean_historical_return(df) hist_rets = expected_returns.returns_from_prices(df) es = EfficientSemivariance(mu, hist_rets) es.efficient_return(target_return=0.20) # 给定 20% 年化,最小化半方差 weights = es.clean_weights() print(weights) es.portfolio_performance(verbose=True)

输出类似:

Expected annual return: 20.0% Annual semi-deviation: 9.1% Sortino Ratio: 2.18

六、与均值-方差的差异速查

对比维度 EfficientFrontier(方差) EfficientSemivariance(半方差)
输入 协方差矩阵 历史收益 DataFrame
风险定义 全体偏离 仅下行偏离
变量数 N 2T + N(规模大)
比率指标 Sharpe Sortino
求解难度 容易 较难,易触发 solver 错误
适用场景 收益对称、追求稳定 关注下行、怕大跌

⚠️ 常见坑:benchmark 必须与 returns 同频率。你用日级 returns 却传年化 benchmark 0.05,等于把所有日子都判成下行,结果毫无意义。

本节要点回顾

  1. 半方差只惩罚低于 benchmark 的下行波动,比方差更贴近真实风险偏好;semideviation 永远 ≤ 标准差。
  2. EfficientSemivariance 继承 EfficientFrontier,构造时要喂历史收益而非协方差;父类的协方差被假零矩阵绕开。
  3. 方法替换:min_volatilitymin_semivariancemax_sharpe不可用、efficient_risk/return 仍可用但参数变为 semideviation。
  4. 变量规模:2T+N,样本大或资产多时容易报 solver 错误,保持问题最小化。
  5. 指标换成 Sortino:分母是 semideviation,portfolio_performance 返回 (期望收益, 半偏差, Sortino)
  6. benchmark 同频率:日收益配日级阈值,年化收益配年化阈值,默认 0 表示「日收益为负即下行」。

下一节,我们看 CVaR 条件风险价值——直接刻画「最坏那 5% 日子里的平均损失」,把目光从「整体波动」推向「尾部极端」。


发布者: 作者: 灏天文库 转发
评论区 (0)
U