第 4 章 · 01 EfficientFrontier


文档摘要

第 4 章 · 01 EfficientFrontier 本节摘要:本章是全教程的优化核心,而本节讲这一层的入口—— 类。它是 PyPortfolioOpt 把「均值-方差优化」封装成的统一容器:你把期望收益 μ 与协方差 Σ 喂给它,它就给你提供五个求解方法( 、 、 、 、 )。本节聚焦类本身的构造、与四层架构的对应关系、 约束边界、继承自 的 / 扩展点,以及 / / 等通用工具方法。理解了本节,后续四节只是逐一调用它的不同方法。 内容来源:原项目源码 、 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 构造优化器,理解构造参数。 解释它如何对应四层架构的第四层。 用 设置权重上下界(只做多 / 多空)。 调用 、 扩展优化问题。 用 、 、 等工具方法。

第 4 章 · 01 EfficientFrontier

本节摘要:本章是全教程的优化核心,而本节讲这一层的入口——EfficientFrontier 类。它是 PyPortfolioOpt 把「均值-方差优化」封装成的统一容器:你把期望收益 μ 与协方差 Σ 喂给它,它就给你提供五个求解方法(max_sharpemin_volatilityefficient_riskefficient_returnmax_quadratic_utility)。本节聚焦类本身的构造、与四层架构的对应关系、weight_bounds 约束边界、继承自 BaseConvexOptimizeradd_constraint/add_objective 扩展点,以及 clean_weights/portfolio_performance/set_weights 等通用工具方法。理解了本节,后续四节只是逐一调用它的不同方法。

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

学习目标

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

  1. EfficientFrontier(mu, S) 构造优化器,理解构造参数。
  2. 解释它如何对应四层架构的第四层。
  3. weight_bounds 设置权重上下界(只做多 / 多空)。
  4. 调用 add_constraintadd_objective 扩展优化问题。
  5. clean_weightsportfolio_performanceset_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, )

构造函数的核心职责:

  1. 校验并存储 μ 与 Σ:把 pd.Series/list/np.ndarray 统一转成 ndarray 内部表示(_validate_expected_returns_validate_cov_matrix)。
  2. 提取 ticker 标签:优先从 expected_returns.index,其次从 cov_matrix.columns,都没有就用整数标签。
  3. 形状一致性检查:cov_matrix.shape != (num_assets, num_assets)ValueError
  4. 把 weight_bounds 转成 cvxpy 约束:委托给父类的 _map_bounds_to_constraints

2.1 关键参数详解

参数 类型 默认 含义
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_sharpeefficient_return 等需要 μ 的方法会抛错。

三、weight_bounds:只做多 vs 多空

weight_bounds 是构造函数里最重要的参数,控制权重的可行域:

3.1 只做多(默认)

ef = EfficientFrontier(mu, S) # 等价于 weight_bounds=(0, 1)

每只资产权重 ∈ [0, 1],且和为 1。这是教科书默认——不能做空。

3.2 多空

ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))

允许负权重(做空),但每只资产权重 ∈ [-1, 1]。这是「允许做空」的基本设置。

3.3 持仓上限

ef = EfficientFrontier(mu, S, weight_bounds=(0, 0.1)) # 单资产最多 10%

3.4 每资产独立边界

bounds = [(0, 0.3), (0.05, 0.4), ...] # 长度 = 资产数 ef = EfficientFrontier(mu, S, weight_bounds=bounds)

3.5 无界

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 >= lowerw <= 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

五、扩展点:add_constraint 与 add_objective

继承自 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 章的核心思路。

六、工具方法(继承自 BaseOptimizer)

这些方法所有优化器共享,务必熟悉:

6.1 clean_weights

ef.clean_weights(cutoff=1e-4, rounding=5)

把绝对值小于 cutoff 的权重置零,其余四舍五入到 rounding 位。让结果可读,方便后续离散分配。

6.2 portfolio_performance

exp_return, volatility, sharpe = ef.portfolio_performance(verbose=True, risk_free_rate=0.0)

求解后查看表现。verbose=True 打印三行:预期年化收益、年化波动率、夏普比率。注意:这是样本内估计,与优化用的是同一份 μ、Σ。

6.3 set_weights

ef.set_weights({"GOOG": 0.05, "AAPL": 0.1, ...})

从用户给的 dict 反向设置 self.weights——用于在不重新求解的情况下评估某个外部权重组合的表现。

6.4 save_weights_to_file

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 章三行代码背后的完整骨架——本节把每一格都讲清了。

本节要点回顾

  1. 入口:EfficientFrontier 是均值-方差优化的统一容器,继承自 BaseConvexOptimizerBaseOptimizer
  2. 构造:EfficientFrontier(mu, S, weight_bounds=(0,1), solver=None, verbose=False, solver_options=None);Σ 必须半正定,μ 可为 None(只优化方差)。
  3. weight_bounds:控制可行域——(0,1) 只做多(默认)、(-1,1) 多空、(0,0.1) 持仓上限、(None,None) 无界(转 ±1)。
  4. 五个方法:max_sharpemin_volatilityefficient_riskefficient_returnmax_quadratic_utility;一个实例只解一次,改目标要重建。
  5. 扩展点:add_constraintadd_objective(第 5 章主轴);约束与目标可互换。
  6. 工具方法:clean_weightsportfolio_performanceset_weightssave_weights_to_file

下一节,我们详解第一个、也是最常用的目标——max_sharpe,看它如何把「最大化夏普」这个非凸问题转化为凸问题。


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