第 4 章 · 01 EfficientFrontier 本节摘要:本章是全教程的优化核心,而本节讲这一层的入口—— 类。它是 PyPortfolioOpt 把「均值-方差优化」封装成的统一容器:你把期望收益 μ 与协方差 Σ 喂给它,它就给你提供五个求解方法( 、 、 、 、 )。本节聚焦类本身的构造、与四层架构的对应关系、 约束边界、继承自 的 / 扩展点,以及 / / 等通用工具方法。理解了本节,后续四节只是逐一调用它的不同方法。 内容来源:原项目源码 、 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 构造优化器,理解构造参数。 解释它如何对应四层架构的第四层。 用 设置权重上下界(只做多 / 多空)。 调用 、 扩展优化问题。 用 、 、 等工具方法。
本节摘要:本章是全教程的优化核心,而本节讲这一层的入口——
EfficientFrontier类。它是 PyPortfolioOpt 把「均值-方差优化」封装成的统一容器:你把期望收益 μ 与协方差 Σ 喂给它,它就给你提供五个求解方法(max_sharpe、min_volatility、efficient_risk、efficient_return、max_quadratic_utility)。本节聚焦类本身的构造、与四层架构的对应关系、weight_bounds约束边界、继承自BaseConvexOptimizer的add_constraint/add_objective扩展点,以及clean_weights/portfolio_performance/set_weights等通用工具方法。理解了本节,后续四节只是逐一调用它的不同方法。
内容来源:原项目源码
pypfopt/efficient_frontier/efficient_frontier.py、pypfopt/base/_base_optimizer.py、文档docs/MeanVariance.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
EfficientFrontier(mu, S) 构造优化器,理解构造参数。weight_bounds 设置权重上下界(只做多 / 多空)。add_constraint、add_objective 扩展优化问题。clean_weights、portfolio_performance、set_weights 等工具方法。回顾第 1 章的四层架构:EfficientFrontier 是**第四层(优化器)**的主入口。它接受前两层的输出(μ、Σ),按第三层(目标函数)定义的目标求解。
具体到类的设计,EfficientFrontier 继承自 BaseConvexOptimizer(在 pypfopt/base/_base_optimizer.py),后者又继承自 BaseOptimizer。继承链:
BaseOptimizer (通用:权重清理、存盘) ↑ BaseConvexOptimizer (cvxpy 求解:add_constraint / add_objective) ↑ EfficientFrontier (均值-方差:五个具体目标方法)
这意味着 EfficientFrontier 既有「均值-方差专用的五个方法」,又继承了「所有 cvxpy 优化器共享的扩展点与工具方法」。
打开 efficient_frontier.py,定位 __init__(源码 57-123 行):
class EfficientFrontier(BaseConvexOptimizer): def __init__( self, expected_returns, cov_matrix, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None, ): self.cov_matrix = self._validate_cov_matrix(cov_matrix) self.expected_returns = self._validate_expected_returns(expected_returns) ... super().__init__( len(tickers), tickers, weight_bounds, solver=solver, verbose=verbose, solver_options=solver_options, )
构造函数的核心职责:
pd.Series/list/np.ndarray 统一转成 ndarray 内部表示(_validate_expected_returns、_validate_cov_matrix)。expected_returns.index,其次从 cov_matrix.columns,都没有就用整数标签。cov_matrix.shape != (num_assets, num_assets) 抛 ValueError。_map_bounds_to_constraints。| 参数 | 类型 | 默认 | 含义 |
|---|---|---|---|
expected_returns |
pd.Series / list / ndarray | — | 期望收益;可传 None(只优化方差时) |
cov_matrix |
pd.DataFrame / ndarray | — | 协方差矩阵;必须半正定 |
weight_bounds |
tuple 或 list of tuples | (0, 1) |
权重上下界 |
solver |
str | None(cvxpy 默认) |
求解器,如 "ECOS"、"SCS" |
verbose |
bool | False |
是否打印求解过程 |
solver_options |
dict | None |
求解器参数 |
⚠️ Σ 必须半正定:
_validate_cov_matrix不检查 PSD(只查类型),但优化器求解时若 Σ 非 PSD 会失败。risk_models的所有函数都已内置 PSD 修正(fix_nonpositive_semidefinite),但若你喂自己的 Σ,务必先确保 PSD。
💡 μ 可以为 None:cookbook 2 的「长/短最小方差」示例就传
None:
ef = EfficientFrontier(None, S, weight_bounds=(None, None)) ef.min_volatility()
只优化方差时不需要 μ。但 max_sharpe、efficient_return 等需要 μ 的方法会抛错。
weight_bounds 是构造函数里最重要的参数,控制权重的可行域:
ef = EfficientFrontier(mu, S) # 等价于 weight_bounds=(0, 1)
每只资产权重 ∈ [0, 1],且和为 1。这是教科书默认——不能做空。
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
允许负权重(做空),但每只资产权重 ∈ [-1, 1]。这是「允许做空」的基本设置。
ef = EfficientFrontier(mu, S, weight_bounds=(0, 0.1)) # 单资产最多 10%
bounds = [(0, 0.3), (0.05, 0.4), ...] # 长度 = 资产数 ef = EfficientFrontier(mu, S, weight_bounds=bounds)
ef = EfficientFrontier(mu, S, weight_bounds=(None, None))
None 自动转 ±1(查源码 _map_bounds_to_constraints:lower=None→-1, upper=None→1)。cookbook 2 的多空最小方差就用这个。
源码里 _map_bounds_to_constraints 把 bounds 转成两条 cvxpy 约束 w >= lower 与 w <= upper,加入约束列表。
构造完后,EfficientFrontier 提供五个求解方法,每个对应一个目标函数。本节只列概览,后续四节逐一详讲:
| 方法 | 目标 | 用途 | 详节 |
|---|---|---|---|
max_sharpe(risk_free_rate=0.0) |
最大化夏普比率 | 切线组合,最常用 | 第 02 节 |
min_volatility() |
最小化方差 | 全局最小方差,最保守 | 第 03 节 |
efficient_risk(target_volatility, market_neutral=False) |
目标波动下最大化收益 | 给定风险求收益 | 第 04 节 |
efficient_return(target_return, market_neutral=False) |
目标收益下最小化风险 | Markowitz 原版 | 第 04 节 |
max_quadratic_utility(risk_aversion=1, market_neutral=False) |
最大化二次效用 | 风险厌恶视角 | 第 05 节 |
所有方法返回 OrderedDict {ticker: weight},并把权重存到 self.weights。
⚠️ 一个实例只解一次:
BaseConvexOptimizer._solve_cvxpy_opt_problem在第二次调用时会检查 objective/constraints 是否被改动,改了就抛InstantiationError。这意味着改目标或约束必须新建实例。比如先ef.max_sharpe()再ef.min_volatility()会报错——要重建ef。
继承自 BaseConvexOptimizer,这两个方法是第 5 章的主轴。本节先认识签名:
def add_constraint(self, new_constraint): """约束必须是 cvxpy 的 DCP 形式(线性等式或凸不等式)""" ... self._constraints.append(new_constraint(self._w)) def add_objective(self, new_objective, **kwargs): """目标必须是凸的,用 cvxpy 原子函数构建""" ... self._additional_objectives.append(new_objective(self._w, **kwargs))
示例(详见第 5 章):
from pypfopt import objective_functions # 加 L2 正则 ef.add_objective(objective_functions.L2_reg, gamma=1) # 加单资产上限约束 amzn_index = ef.tickers.index("AMZN") ef.add_constraint(lambda w: w[amzn_index] <= 0.10)
💡 约束 vs 目标的边界模糊:从优化角度,「最小化风险 + 收益≥目标」与「最大化收益 + 风险≤目标」是同一问题的两种写法。PyPortfolioOpt 因此把目标函数设计成「既可加进 objective,也可当 constraint」。这是第 4、5 章的核心思路。
这些方法所有优化器共享,务必熟悉:
ef.clean_weights(cutoff=1e-4, rounding=5)
把绝对值小于 cutoff 的权重置零,其余四舍五入到 rounding 位。让结果可读,方便后续离散分配。
exp_return, volatility, sharpe = ef.portfolio_performance(verbose=True, risk_free_rate=0.0)
求解后查看表现。verbose=True 打印三行:预期年化收益、年化波动率、夏普比率。注意:这是样本内估计,与优化用的是同一份 μ、Σ。
ef.set_weights({"GOOG": 0.05, "AAPL": 0.1, ...})
从用户给的 dict 反向设置 self.weights——用于在不重新求解的情况下评估某个外部权重组合的表现。
ef.save_weights_to_file("weights.csv") # 或 .json / .txt
存盘,支持 csv(pandas)、json、txt(str(dict))三种格式。
把本节学的串起来:
import pandas as pd from pypfopt import EfficientFrontier, risk_models, expected_returns df = pd.read_csv("tests/resources/stock_prices.csv", parse_dates=True, index_col="date") mu = expected_returns.capm_return(df) S = risk_models.CovarianceShrinkage(df).ledoit_wolf() # 构造 + 求解 ef = EfficientFrontier(mu, S, weight_bounds=(0, 1)) weights = ef.max_sharpe() # 清理 + 查看 cleaned = ef.clean_weights() print(cleaned) ef.portfolio_performance(verbose=True)
输出示例:
{'GOOG': 0.01269, 'AAPL': 0.09202, 'FB': 0.19856, ...} Expected annual return: 33.0% Annual volatility: 21.7% Sharpe Ratio: 1.43
这就是第 1 章三行代码背后的完整骨架——本节把每一格都讲清了。
EfficientFrontier 是均值-方差优化的统一容器,继承自 BaseConvexOptimizer → BaseOptimizer。EfficientFrontier(mu, S, weight_bounds=(0,1), solver=None, verbose=False, solver_options=None);Σ 必须半正定,μ 可为 None(只优化方差)。(0,1) 只做多(默认)、(-1,1) 多空、(0,0.1) 持仓上限、(None,None) 无界(转 ±1)。max_sharpe、min_volatility、efficient_risk、efficient_return、max_quadratic_utility;一个实例只解一次,改目标要重建。add_constraint、add_objective(第 5 章主轴);约束与目标可互换。clean_weights、portfolio_performance、set_weights、save_weights_to_file。下一节,我们详解第一个、也是最常用的目标——
max_sharpe,看它如何把「最大化夏普」这个非凸问题转化为凸问题。