第 4 章 · 04 efficientrisk 与 efficientreturn 本节摘要:本节把两个对称的方法一起讲—— 与 。它们是马科维茨 1952 原论文的核心:前者「在目标波动率下最大化收益」,后者「在目标收益下最小化风险」,从两个方向逼近同一条有效前沿。扫一遍不同的目标值,每个都解出一个最优点,把这些点连起来就是前沿曲线本身。本节拆解两者的源码: 把方差约束写成 ,而 把收益约束写成 ;两者都支持 市场中性模式;还讲清它们各自的边界检查(全局最小波动率、最大可能收益)以及作者关于「不合理目标会静默失败」的警告。 内容来源:原项目源码 、 、文档 、 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 区分 与 的目标与约束互换关系。 用两者描出整条有效前沿。
本节摘要:本节把两个对称的方法一起讲——
efficient_risk与efficient_return。它们是马科维茨 1952 原论文的核心:前者「在目标波动率下最大化收益」,后者「在目标收益下最小化风险」,从两个方向逼近同一条有效前沿。扫一遍不同的目标值,每个都解出一个最优点,把这些点连起来就是前沿曲线本身。本节拆解两者的源码:efficient_risk把方差约束写成variance <= target_variance,而efficient_return把收益约束写成ret >= target_return_par;两者都支持market_neutral市场中性模式;还讲清它们各自的边界检查(全局最小波动率、最大可能收益)以及作者关于「不合理目标会静默失败」的警告。
内容来源:原项目源码
pypfopt/efficient_frontier/efficient_frontier.py、pypfopt/objective_functions.py、文档docs/MeanVariance.rst、docs/UserGuide.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
efficient_risk 与 efficient_return 的目标与约束互换关系。market_neutral=True 求市场中性组合。回顾有效前沿的定义:所有满足「给定风险下收益最高」或「给定收益下风险最低」的组合的集合。这两个定义等价——它们描出同一条曲线。efficient_risk 与 efficient_return 就是从这两个方向求解:
| 方法 | 目标 | 约束 | 直觉 |
|---|---|---|---|
efficient_risk(target_volatility) |
最大化收益 | 波动率 ≤ 目标 | 「我能承受 X% 波动,求最大收益」 |
efficient_return(target_return) |
最小化方差 | 收益 ≥ 目标 | 「我要至少 X% 收益,求最稳」 |
扫不同的目标值,每个解出一个前沿上的点,连起来就是前沿:
for target in [0.15, 0.18, 0.21, 0.24, 0.27]: ef = EfficientFrontier(mu, S) ef.efficient_risk(target_volatility=target) # 记录 (volatility, return)
打开 efficient_frontier.py,定位 efficient_risk(源码 359-416 行)。核心片段:
def efficient_risk(self, target_volatility, market_neutral=False): if not isinstance(target_volatility, (float, int)) or target_volatility < 0: raise ValueError("target_volatility should be a positive float") global_min_volatility = np.sqrt(1 / np.sum(np.linalg.pinv(self.cov_matrix))) if target_volatility < global_min_volatility: raise ValueError( "The minimum volatility is {:.3f}. Please use a higher target_volatility".format( global_min_volatility ) ) # 目标:最大化收益(即最小化负收益) self._objective = objective_functions.portfolio_return(self._w, self.expected_returns) variance = objective_functions.portfolio_variance(self._w, self.cov_matrix) for obj in self._additional_objectives: self._objective += obj # 约束:方差 ≤ 目标方差 target_variance = cp.Parameter( name="target_variance", value=target_volatility**2, nonneg=True ) self.add_constraint(lambda _: variance <= target_variance) self._make_weight_sum_constraint(market_neutral) return self._solve_cvxpy_opt_problem()
关键点:
target_volatility 必须正数。np.sqrt(1/np.sum(np.linalg.pinv(Σ))) 计算——这是 Σ 的伪逆的元素和的倒数的平方根,对应 GMV 组合的波动率。低于它无解。portfolio_return(默认 negative=True,即最小化负收益 = 最大化收益)。Parameter(target_volatility**2),加 variance <= target_variance 约束。_make_weight_sum_constraint(market_neutral)——非市场中性时 sum(w)==1,市场中性时 sum(w)==0。💡 为何用 Parameter:把
target_variance包成cp.Parameter而非常数,是为了支持后续update_parameter_value重解(不改约束结构只改值)。这让「扫不同目标」更高效——但需要配合is_parameter_defined的逻辑,且改market_neutral仍需重建实例。
定位 efficient_return(源码 418-470 行)。核心片段:
def efficient_return(self, target_return, market_neutral=False): if not isinstance(target_return, float): raise ValueError("target_return should be a float") if not self._max_return_value: a = self.deepcopy() self._max_return_value = a._max_return() if target_return > self._max_return_value: raise ValueError( "target_return must be lower than the maximum possible return" ) # 目标:最小化方差 self._objective = objective_functions.portfolio_variance(self._w, self.cov_matrix) ret = objective_functions.portfolio_return(self._w, self.expected_returns, negative=False) for obj in self._additional_objectives: self._objective += obj # 约束:收益 ≥ 目标收益 target_return_par = cp.Parameter(name="target_return", value=target_return) self.add_constraint(lambda _: ret >= target_return_par) self._make_weight_sum_constraint(market_neutral) return self._solve_cvxpy_opt_problem()
关键点:
target_return 必须是 float(不接受 int——源码故意如此,提示用户传浮点)。_max_return() 内部方法计算——它求解一个「只最大化收益、不加风险约束」的子问题,得到可行域内的最高收益。portfolio_variance(最小化方差)。Parameter,加 ret >= target_return_par 约束。注意这里 negative=False(取正收益)。deepcopy:计算 _max_return_value 时用 self.deepcopy() 避免污染当前实例的求解状态——因为 _max_return 内部会调用 _solve_cvxpy_opt_problem,改了 objective。两者都支持 market_neutral=True:
ef.efficient_return(target_return=0.2, market_neutral=True) ef.efficient_risk(target_volatility=0.2, market_neutral=True)
源码 _make_weight_sum_constraint(152-193 行)的逻辑:
def _make_weight_sum_constraint(self, is_market_neutral): if is_market_neutral: portfolio_possible = np.any(self._lower_bounds < 0) if not portfolio_possible: warnings.warn( "Market neutrality requires shorting - bounds have been amended", RuntimeWarning, ) self._map_bounds_to_constraints((-1, 1)) ... self.add_constraint(lambda w: cp.sum(w) == 0) # 权重和为 0 else: self.add_constraint(lambda w: cp.sum(w) == 1) # 权重和为 1
市场中性要求权重和为 0(多空抵消,组合 beta 趋零),这必然需要负权重(做空)。若你的 weight_bounds 不允许负值,会自动改成 (-1, 1) 并发警告。
⚠️ 为何 max_sharpe 与 min_volatility 不支持市场中性:文档明确——市场中性(权重和为 0)对杠杆不变性有要求,而 max_sharpe(切线组合)与 min_volatility(GMV)对杠杆不满足不变性。只有
efficient_risk、efficient_return、max_quadratic_utility三者支持。
ef = EfficientFrontier(mu, S) ef.efficient_risk(target_volatility=0.20) # 目标年化波动 20% weights = ef.clean_weights() ef.portfolio_performance(verbose=True)
输出可能:
Expected annual return: 28.5% Annual volatility: 20.0% # 等于目标(约束生效) Sharpe Ratio: 1.42
ef = EfficientFrontier(mu, S) ef.efficient_return(target_return=0.25) # 目标年化收益 25% ef.portfolio_performance(verbose=True)
输出可能:
Expected annual return: 25.0% Annual volatility: 19.2% Sharpe Ratio: 1.30
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1)) ef.efficient_return(target_return=0.2, market_neutral=True) for name, value in ef.clean_weights().items(): print(f"{name}: {value:.4f}")
权重会有正有负,且和为 0。
targets = [0.15, 0.17, 0.19, 0.21, 0.23, 0.25] frontier = [] for tv in targets: ef = EfficientFrontier(mu, S) ef.efficient_risk(target_volatility=tv) ret, vol, _ = ef.portfolio_performance() frontier.append((vol, ret)) # 画 (vol, ret) 散点 + 连线即有效前沿
| 参数 | efficient_risk | efficient_return |
|---|---|---|
| 必填 | target_volatility (float > 0) |
target_return (float) |
| 可选 | market_neutral=False |
market_neutral=False |
| 上界检查 | 不能低于 global_min_volatility |
不能高于 _max_return_value |
| 类型陷阱 | int 也接受(自动转 float) | 必须 float,int 抛错 |
⚠️ target_return 必须 float:源码
if not isinstance(target_return, float): raise ValueError。传0.2没问题,传1(int)会报错。这是刻意设计——提示用户「收益是连续浮点」。
docs/MeanVariance.rst 有一段刺眼的 caution:
若你给
efficient_risk或efficient_return传入不合理的目标,优化器会静默失败并返回怪异权重。Caveat emptor(买者自负)!
虽然源码有边界检查(全局最小波动率、最大可能收益),但数值上接近边界时仍可能出问题:
💡 实务建议:目标值要离边界留有余量。先用
min_volatility()查 GMV 波动率,再设efficient_risk目标比它高 2~5 个百分点;先用_max_return或试算查最大收益,再设efficient_return目标比它低几个百分点。
efficient_risk 最大化收益受波动约束,efficient_return 最小化方差受收益约束——两方向逼近同一前沿。_max_return 内部算)。market_neutral=True(权重和为 0,需负权重);max_sharpe 与 min_volatility 不支持。下一节是本章最后一个目标——
max_quadratic_utility,用风险厌恶系数 δ 平衡收益与方差。