第 1 章 · 03 四层架构总览 本节摘要:本节是全教程的「地图」,把 PyPortfolioOpt 的所有功能抽象成一张贯穿全书的「四层可替换架构」——收益模型、风险模型、目标函数、优化器。前两层负责「估输入」(给出来自历史数据的 μ 与 Σ),后两层负责「定目标 + 求解」(把 μ、Σ 喂进凸优化问题求出 w)。这四层互不耦合:任意一层都可以独立替换,例如把样本协方差换成 Ledoit-Wolf 收缩、把 maxsharpe 换成 minvolatility、甚至把整个方差目标换成 CVaR,框架照常运转。理解这张地图,后续 9 章就只是「逐层深入填充细节」。本节还讲清为何这种 scikit-learn 风格的模块化设计是 PyPortfolioOpt 的核心竞争力。
本节摘要:本节是全教程的「地图」,把 PyPortfolioOpt 的所有功能抽象成一张贯穿全书的「四层可替换架构」——收益模型、风险模型、目标函数、优化器。前两层负责「估输入」(给出来自历史数据的 μ 与 Σ),后两层负责「定目标 + 求解」(把 μ、Σ 喂进凸优化问题求出 w)。这四层互不耦合:任意一层都可以独立替换,例如把样本协方差换成 Ledoit-Wolf 收缩、把 max_sharpe 换成 min_volatility、甚至把整个方差目标换成 CVaR,框架照常运转。理解这张地图,后续 9 章就只是「逐层深入填充细节」。本节还讲清为何这种 scikit-learn 风格的模块化设计是 PyPortfolioOpt 的核心竞争力。
内容来源:原项目源码
README.md、docs/UserGuide.rst、docs/index.rst概念流程图,汉化并套用体系化模板。
阅读完本节,你应当能够:
回顾上一节的三行代码:
mu = expected_returns.mean_historical_return(df) # 收益 S = risk_models.sample_cov(df) # 风险 ef = EfficientFrontier(mu, S) ef.max_sharpe() # 目标 + 优化器
这三行其实暗含四个角色,只是最后一步把「目标」和「优化器」揉在一起了。把它们拆开,就是 PyPortfolioOpt 全部功能围绕的四个抽象层:
| 层 | 模块 | 输入 | 输出 | 职责 |
|---|---|---|---|---|
| ① 收益模型 | pypfopt.expected_returns |
价格 DataFrame | μ 向量 | 估每只资产的期望收益 |
| ② 风险模型 | pypfopt.risk_models |
价格 DataFrame | Σ 矩阵 | 估协方差/相关性 |
| ③ 目标函数 | pypfopt.objective_functions |
w(+μ/Σ) | 凸表达式 | 定义「什么是最优」 |
| ④ 优化器 | pypfopt.efficient_frontier 等 |
μ、Σ、目标 | w 字典 | 求解凸问题 |
💡 核心心法:前两层是「估输入」——把不可知的未来收益与风险,用历史数据估出来;后两层是「求最优」——把估出来的 μ、Σ 喂进凸优化问题,求出权重。PyPortfolioOpt 的卖点不是某层做得最好,而是让你自由换层:你可以用自研因子模型估 μ,再喂进它的优化器;也可以保留它的 μ、Σ,换上自己的目标函数。
定义:把价格序列转成「每只资产未来一期期望收益」的估计。模块是 pypfopt.expected_returns,提供三种内置实现:
mean_historical_return:历史均值(算术/几何)。ema_historical_return:指数加权,对近期更敏感。capm_return:资本资产定价模型,基于市场 beta。按约定,这些函数输出年化期望收益(默认 frequency=252,即一年交易日数)。统一后,后续优化器不必关心时间频率。
⚠️ 作者反复警告:供给错误收益比不给收益更糟。若你没有自研预测模型,文档建议干脆放弃 μ,只跑
min_volatility()或用 HRP。第 7 章 Black-Litterman 会给出更稳的替代方案。详见第 2 章。
定义:估协方差矩阵 Σ,它编码了每只资产的波动率以及它们两两之间的联动程度。模块是 pypfopt.risk_models,提供:
sample_cov:样本协方差,教科书默认,但噪声大。semicovariance:只惩罚下行偏差。exp_cov:指数加权,对近期联动更敏感。CovarianceShrinkage(Ledoit-Wolf / OAS):收缩估计,生产推荐。min_cov_determinant:MCD,抗异常值。💡 生产默认值:作者在 UserGuide 里点名——
risk_models.CovarianceShrinkage(df).ledoit_wolf()是默认推荐,而非sample_cov。原因是样本协方差在「资产数接近样本数」时严重失真,收缩能显著降噪。详见第 3 章。
定义:把「最优」用数学语言写下来——一个关于权重 w 的函数,求它的最小(或最大)就是优化目标。模块是 pypfopt.objective_functions,内置:
| 函数 | 数学形式 | 用途 |
|---|---|---|
portfolio_variance |
wᵀ Σ w |
最小方差 |
portfolio_return |
wᵀ μ |
最大收益 |
sharpe_ratio |
(wᵀ μ - R_f) / σ |
最大夏普 |
quadratic_utility |
wᵀ μ - ½ δ wᵀ Σ w |
二次效用 |
L2_reg |
γ wᵀ w |
正则,减少零权重 |
transaction_cost |
k‖w - w_prev‖₁ |
换手成本 |
ex_ante_tracking_error |
(w-w_b)ᵀ Σ (w-w_b) |
跟踪误差 |
💡 目标与约束可互换:从优化角度看,「最小化风险 + 收益≥目标」与「最大化收益 + 风险≤目标」是同一类问题——风险与收益的角色互换。PyPortfolioOpt 因此把目标函数设计成「既能当目标,也能当约束」。这是第 4、5 章的核心思路。
定义:把目标函数 + 约束喂给凸优化求解器(cvxpy 后端),解出 w。主入口是 EfficientFrontier 类,提供五个内置方法:
除 EfficientFrontier 外,还有针对不同风险度量的优化器(第 6 章详讲):
EfficientSemivariance:半方差,只惩罚下行。EfficientCVaR:条件风险价值,关注尾部。EfficientCDaR:条件回撤,关注亏损期。HRP:层次风险平价,绕开 Σ 估计。CLA:临界线算法,马科维茨原版。所有优化器都继承自 BaseOptimizer/BaseConvexOptimizer,共享 add_constraint、add_objective、clean_weights、portfolio_performance 等接口。这是 scikit-learn 风格「统一 API」的体现。
README 把「易替换组件」列为头号设计原则。其工程价值体现在三个场景:
EfficientFrontier;无需 fork 库。sample_cov 起步,数据足够后切到 Ledoit-Wolf,再切到 Black-Litterman——其余三行代码不变。# 同一优化器,换不同风险模型 mu = expected_returns.capm_return(df) S1 = risk_models.sample_cov(df) S2 = risk_models.CovarianceShrinkage(df).ledoit_wolf() w1 = EfficientFrontier(mu, S1).max_sharpe() w2 = EfficientFrontier(mu, S2).max_sharpe() # 只换 S
这种「接口稳定、实现可换」的设计,让 PyPortfolioOpt 既适合教学(每层都简单),也适合生产(每层都能定制)。
把四层串起来,一条端到端工作流如下:
后续章节就是逐节填充这张图的每一格:
expected_returns)、风险模型(risk_models)、目标函数(objective_functions)、优化器(EfficientFrontier 等)。BaseOptimizer,共享 add_constraint/clean_weights 等方法,scikit-learn 风格。min_volatility 更稳。下一章,我们深入第一层——收益模型,从最朴素的
mean_historical_return开始。