第 4 章 · 03 minvolatility 本节摘要:本节详解 ——求全局最小方差组合(Global Minimum Variance, GMV)。它不依赖期望收益 μ,只在约束下最小化组合方差 ,因此是「最保守」的均值-方差组合。本节拆解它的源码实现:目标函数是 ,加上「权重和为 1」的约束即可,相比 maxsharpe 不需要变量替换,因此与额外目标完全兼容。重点讲一个反直觉的实证发现——大量研究(Kritzman et al. 2010 等)表明,GMV 组合样本外常常跑赢 maxsharpe 组合,因为后者依赖极不稳定的 μ。这使得 在「没有可靠收益预测」时成为更稳的默认选择。本节还讲它与「长短最小方差」的搭配。
本节摘要:本节详解
EfficientFrontier.min_volatility——求全局最小方差组合(Global Minimum Variance, GMV)。它不依赖期望收益 μ,只在约束下最小化组合方差wᵀ Σ w,因此是「最保守」的均值-方差组合。本节拆解它的源码实现:目标函数是objective_functions.portfolio_variance,加上「权重和为 1」的约束即可,相比 max_sharpe 不需要变量替换,因此与额外目标完全兼容。重点讲一个反直觉的实证发现——大量研究(Kritzman et al. 2010 等)表明,GMV 组合样本外常常跑赢 max_sharpe 组合,因为后者依赖极不稳定的 μ。这使得min_volatility在「没有可靠收益预测」时成为更稳的默认选择。本节还讲它与「长短最小方差」的搭配。
内容来源:原项目源码
pypfopt/efficient_frontier/efficient_frontier.py、pypfopt/objective_functions.py、文档docs/MeanVariance.rst、docs/UserGuide.rst、cookbook/2-Mean-Variance-Optimisation.ipynb,汉化并套用体系化模板。
阅读完本节,你应当能够:
min_volatility() 求全局最小方差组合。max_sharpe 在样本外表现上的差异。EfficientFrontier(None, S) + min_volatility 跑「纯风险优化」。在「收益-风险」平面上,有效前沿是一条向右上的曲线。这条曲线上最左边的点(风险最小的点)就是全局最小方差组合(GMV)——它在所有可行组合里方差最小。
minimize wᵀ Σ w (组合方差) subject to wᵀ 1 = 1 (权重和为 1) weight_bounds (边界约束)
注意:目标里没有 μ。GMV 完全由协方差矩阵 Σ 决定,与期望收益无关。这是它与 max_sharpe 最大的区别。
💡 核心心法:GMV 是「不求最高收益,只求最稳」的组合。它放弃了对 μ 的依赖——而 μ 恰恰是均值-方差优化里最难估、最不稳定的输入。这种「主动放弃不可靠输入」的设计,让 GMV 在样本外往往表现更稳。
打开 efficient_frontier.py,定位 min_volatility(源码 195-211 行):
def min_volatility(self): """ Minimise volatility. Returns OrderedDict of asset weights. """ self._objective = objective_functions.portfolio_variance( self._w, self.cov_matrix ) for obj in self._additional_objectives: self._objective += obj self.add_constraint(lambda w: cp.sum(w) == 1) return self._solve_cvxpy_opt_problem()
极其简洁:
objective_functions.portfolio_variance(w, Σ)——查 objective_functions.py,它就是 cp.quad_form(w, Σ, assume_PSD=True),即 wᵀ Σ w。add_objective 加的所有项 += 到主目标上。这是 min_volatility 与额外目标完全兼容的关键(对比 max_sharpe 的变量替换会破坏这种兼容)。cp.sum(w) == 1(权重和为 1)。注意 weight_bounds 已经在构造时由 _map_bounds_to_constraints 转成 w >= lower 和 w <= upper 加入约束列表了。_solve_cvxpy_opt_problem,cvxpy 用默认求解器解这个标准凸二次规划。💡 为何这么简单:相比
max_sharpe的变量替换凸化,min_volatility本身就是标准凸二次规划——目标wᵀ Σ w是凸的(Σ 半正定),约束都是线性的。无需任何数学技巧,直接交给 cvxpy。
portfolio_variance 目标函数打开 objective_functions.py,看 portfolio_variance(源码 64-81 行):
def portfolio_variance(w, cov_matrix): variance = cp.quad_form(w, cov_matrix, assume_PSD=True) return _objective_value(w, variance)
cp.quad_form(w, Σ, assume_PSD=True) 是 cvxpy 的二次型原子函数,计算 wᵀ Σ w。assume_PSD=True 跳过 PSD 检查以加速(Σ 已经由 risk_models 保证 PSD)。
_objective_value(w, obj) 是个辅助函数:
w 是 np.ndarray(已求解):返回 obj.value(标量)。w 是 cp.Variable(未求解):返回 obj(cvxpy 表达式)。这种「同一个函数,既能算具体值、又能构造优化表达式」的设计,让目标函数既可在优化器内部用,也可在外部独立计算某个权重组的方差。
这是一个反直觉但被反复验证的实证发现。docs/UserGuide.rst 明确写:
有大量研究表明,最小方差组合(
ef.min_volatility())样本外持续跑赢最大夏普组合——即便用夏普比率来衡量——因为预测期望收益太困难。
docs/RiskModels.rst 也引用 Kritzman, Page & Turkington (2010) 的研究:
最小方差组合(不提供期望收益直接优化)实际上样本外表现好得多。
直觉解释:
| 维度 | max_sharpe | min_volatility |
|---|---|---|
| 依赖 μ | 是(放大 μ 错误) | 否 |
| 在前沿上的位置 | 切点(最尖端,对输入敏感) | 最左端(最稳) |
| μ 估计噪声的影响 | 极大(切点漂移) | 无 |
| 样本外稳健性 | 差 | 好 |
max_sharpe 在找前沿的「最尖端」——那里对 μ、Σ 的微小变化极度敏感,输入稍有偏差,切点位置剧烈漂移。而 GMV 在前沿的「最左端」——它只依赖 Σ(相对 μ 更可估),位置稳定。
💡 作者的隐藏建议:UserGuide 的「改进表现」一节明确推荐:若回测不理想,试着「放弃期望收益,只用 min_volatility,或用 HRP」。这是经验之谈,不是教科书教条。
from pypfopt import EfficientFrontier, risk_models S = risk_models.CovarianceShrinkage(df).ledoit_wolf() ef = EfficientFrontier(None, S) # μ=None,只优化方差 ef.min_volatility() weights = ef.clean_weights() ef.portfolio_performance(verbose=True)
输出:
Annual volatility: 18.5% # 注意没有 expected return 与 sharpe
因为 μ=None,portfolio_performance 只输出波动率,不算收益与夏普。
cookbook 2 的招牌示例——允许做空的 GMV:
S = risk_models.CovarianceShrinkage(prices).ledoit_wolf() ef = EfficientFrontier(None, S, weight_bounds=(None, None)) ef.min_volatility() weights = ef.clean_weights() ef.portfolio_performance(verbose=True)
weight_bounds=(None, None) 等价于 (-1, 1),允许任意多空。这是「研究显示样本外好」的那类组合。
由于 min_volatility 与额外目标完全兼容,可直接加 L2 正则:
from pypfopt import objective_functions ef = EfficientFrontier(None, S) ef.add_objective(objective_functions.L2_reg, gamma=1) ef.min_volatility()
这会减少零权重,让组合更分散(详见第 5 章第 02 节)。
ef = EfficientFrontier(None, S) ef.add_sector_constraints(sector_mapper, sector_lower, sector_upper) ef.min_volatility()
无需担心 max_sharpe 那种「变量替换破坏额外目标」的问题。
| 维度 | min_volatility | max_sharpe |
|---|---|---|
| 目标 | 最小化 wᵀ Σ w |
最大化 (wᵀ μ - R_f)/σ |
| 需要 μ | 否 | 是 |
| 在前沿上位置 | 最左端(GMV) | 切点 |
| 凸化技巧 | 无(本就是凸 QP) | 变量替换 |
| 与额外目标兼容 | 完全兼容 | 不兼容(警告) |
| 输入敏感度 | 低(只依赖 Σ) | 高(μ 与 Σ 都敏感) |
| 样本外稳健性 | 强 | 弱 |
| 推荐度 | 实证更稳 | 直觉更美 |
💡 务实选择:如果你没有自研的收益预测模型(多数散户与机构都没有稳定可盈利的 α),
min_volatility几乎总是比max_sharpe更好的默认。它会牺牲一些样本内「理论最优」,换样本外的稳健性。
wᵀ Σ w,得全局最小方差组合(GMV),位于有效前沿最左端。EfficientFrontier(None, S),只优化方差;portfolio_performance 只输出波动率。portfolio_variance + 约束 sum(w)==1,本就是凸 QP,无需凸化技巧。min_volatility 或 HRP,而非 max_sharpe。下一节,我们看
efficient_risk与efficient_return——给定目标波动求最大收益、给定目标收益求最小风险,二者描出整条有效前沿。