第 8 章 · 02 临界线算法 CLA


文档摘要

第 8 章 · 02 临界线算法 CLA 本节摘要:均值-方差用通用凸优化器(cvxpy)求解,但 Markowitz 1952 年原始论文用的是「临界线算法(Critical Line Algorithm,CLA)」——专为带不等式约束(权重上下界)的二次规划设计,它不是给出一个最优解,而是精确算出整条有效前沿上的所有转折点,保证收敛。本节讲清 PyPortfolioOpt 里 类的核心: 找转折点、 / 在转折点间搜索、 输出整条前沿。读完本节,你能用 CLA 替代 EfficientFrontier,并理解它相对通用凸求解器的优势在「整条前沿」而非「单点」。 内容来源:源码 、文档 ,汉化并套用体系化模板。

第 8 章 · 02 临界线算法 CLA

本节摘要:均值-方差用通用凸优化器(cvxpy)求解,但 Markowitz 1952 年原始论文用的是「临界线算法(Critical Line Algorithm,CLA)」——专为带不等式约束(权重上下界)的二次规划设计,它不是给出一个最优解,而是精确算出整条有效前沿上的所有转折点,保证收敛。本节讲清 PyPortfolioOpt 里 CLA 类的核心:_solve 找转折点、max_sharpe/min_volatility 在转折点间搜索、efficient_frontier 输出整条前沿。读完本节,你能用 CLA 替代 EfficientFrontier,并理解它相对通用凸求解器的优势在「整条前沿」而非「单点」。

内容来源:源码 pypfopt/cla.py、文档 docs/OtherOptimizers.rst,汉化并套用体系化模板。

学习目标

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

  1. CLA 替代 EfficientFrontier 出权重。
  2. 说清 CLA 与通用凸求解器的本质区别
  3. 理解「转折点(turning point)」的几何含义。
  4. efficient_frontier 输出整条前沿。
  5. 知道 CLA 的适用与不适用场景。

一、为什么会有 CLA:Markowitz 的原始解法

现代均值-方差优化用 cvxpy 内点法,但 Markowitz 1952 年的原始论文是另一套——临界线算法(CLA),由 Markowitz 自己和 Bailey、López de Prado 实现。它专为下面的二次规划设计:

min ½ wᵀΣw s.t. μᵀw = target_return Σw = 1 lb ≤ w ≤ ub

CLA 不像内点法迭代逼近最优,而是沿着有效前沿几何地走——每次走到一个「临界点」,某个权重恰好碰到边界(lb 或 ub),状态切换(自由变受限、受限变自由)。这些切换点叫转折点,整条前沿就是转折点之间的线段。

💡 CLA 的独特价值:不是算「一个最优」,而是把整条前沿精确解析地分段。每段是线性的,转折点用闭式公式算。这是通用凸求解器做不到的。

二、CLA 类的构造

from pypfopt import CLA cla = CLA( expected_returns=mu, cov_matrix=S, weight_bounds=(0, 1), # 可单个、可成对、可每个资产一组 )

源码 __init__ 把 bounds 解析成 lB / uB 数组,并初始化四个核心存储:

self.w = [] # 转折点的权重解列表 self.ls = [] # 每个转折点对应的 λ(风险厌恶) self.g = [] # gamma self.f = [] # 每个转折点的「自由资产」索引列表 self.frontier_values = None

weight_bounds 灵活:

  • (0, 1):所有资产相同区间。
  • (lb_array, ub_array):每个资产一对。
  • [(lb_0, ub_0), (lb_1, ub_1), ...]:逐资产 bounds,长度等于 N。

三、转折点:_solve 的核心循环

_solve 是 CLA 的灵魂——一个 while 循环,每次找下一个转折点:

def _solve(self): f, w = self._init_algo() # 起点:最高 μ 资产满仓 self.w.append(np.copy(w)) self.ls.append(None); self.g.append(None); self.f.append(f[:]) while True: # case a): 某个自由权重撞到边界 l_in = None if len(f) > 1: # ... 在 f 中找下一个进边界的 λ # case b): 某个受限权重被释放 l_out = None if len(f) < self.mean.shape[0]: # ... 在 b 中找下一个出边界的 λ # 决策:取较大者 if ...: self.ls.append(l_in); f.remove(i_in); w[i_in] = bi_in else: self.ls.append(l_out); f.append(i_out) # 算当前转折点的权重 covarF, covarFB, meanF, wB = self._get_matrices(f) covarF_inv = np.linalg.inv(covarF) wF, g = self._compute_w(covarF_inv, covarFB, meanF, wB) for i in range(len(f)): w[f[i]] = wF[i] self.w.append(np.copy(w)) self.g.append(g); self.f.append(f[:]) if self.ls[-1] == 0: break self._purge_num_err(10e-10) # 清理数值误差导致的非法解 self._purge_excess() # 清理非凸包点

_init_algo 选起点:按 μ 降序,把高 μ 资产依次装满直到 Σw=1,其余在边界:

def _init_algo(self): a = np.zeros((self.mean.shape[0]), dtype=[("id", int), ("mu", float)]) b = [self.mean[i][0] for i in range(self.mean.shape[0])] a[:] = list(zip(list(range(self.mean.shape[0])), b)) b = np.sort(a, order="mu") i, w = b.shape[0], np.copy(self.lB) while sum(w) < 1: i -= 1 w[b[i][0]] = self.uB[b[i][0]] w[b[i][0]] += 1 - sum(w) return [b[i][0]], w

_purge_num_err_purge_excess 是清理步骤——剔除数值误差和不在有效前沿上的转折点。

四、max_sharpe 与 min_volatility:转折点间搜索

CLA 算完所有转折点后,max_sharpe相邻转折点的凸组合上用黄金分割搜索最高 Sharpe:

def max_sharpe(self): if not self.w: self._solve() w_sr, sr = [], [] for i in range(len(self.w) - 1): w0 = np.copy(self.w[i]) w1 = np.copy(self.w[i + 1]) kargs = {"minimum": False, "args": (w0, w1)} a, b = self._golden_section(self._eval_sr, 0, 1, **kargs) w_sr.append(a * w0 + (1 - a) * w1) sr.append(b) self.weights = w_sr[sr.index(max(sr))].reshape((self.n_assets,)) return self._make_output_weights()

min_volatility 更直接——遍历所有转折点,选方差最小的:

def min_volatility(self): if not self.w: self._solve() var = [np.dot(np.dot(w.T, self.cov_matrix), w) for w in self.w] self.weights = self.w[var.index(min(var))].reshape((self.n_assets,)) return self._make_output_weights()

💡 CLA 的 max_sharpe 比 cvxpy 更准:因为 Sharpe 是非凸的(分子分母都是 w 的函数),cvxpy 不能直接优化 Sharpe,只能间接最大化 μᵀw - ½γwᵀΣw 然后扫描 γ。CLA 在解析前沿上用黄金分割搜,精度更高。

五、efficient_frontier:输出整条前沿

CLA 的杀手锏——一次性输出整条有效前沿:

mus, sigmas, weights = cla.efficient_frontier(points=100)

源码在相邻转折点间线性插值:

def efficient_frontier(self, points=100): if not self.w: self._solve() mu, sigma, weights = [], [], [] a = np.linspace(0, 1, points // len(self.w))[:-1] for i in range(len(self.w) - 1): w0, w1 = self.w[i], self.w[i + 1] for j in a: w = w1 * j + (1 - j) * w0 weights.append(np.copy(w)) mu.append(np.dot(w.T, self.mean)[0, 0]) sigma.append(np.dot(np.dot(w.T, self.cov_matrix), w)[0, 0] ** 0.5) self.frontier_values = (mu, sigma, weights) return mu, sigma, weights

返回三个并行列表:收益、风险、每个点的权重。可直接喂给 matplotlib 画前沿曲线。

六、完整示例:CLA 优化与可视化

from pypfopt import CLA, expected_returns, risk_models from pypfopt import plotting import matplotlib.pyplot as plt mu = expected_returns.mean_historical_return(prices) S = risk_models.sample_cov(prices) cla = CLA(mu, S) cla.max_sharpe() weights = cla.clean_weights() cla.portfolio_performance(verbose=True) # 画前沿 fig, ax = plt.subplots() plotting.plot_efficient_frontier(cla, ax=ax, show_assets=True) plt.show()

七、CLA vs EfficientFrontier

对比维度 EfficientFrontier(cvxpy) CLA
算法 通用凸优化(内点法) 专用临界线
输出 单个最优组合 整条前沿的转折点
Sharpe 优化 间接(扫 γ) 直接(黄金分割)
约束支持 灵活(add_constraint) 仅 weight_bounds
大规模 慢(N 大时转折点多)
适合 通用、需多约束 画整条前沿、研究用

⚠️ 官方建议:除非要画整条前沿,优先用 EfficientFrontier。CLA 的优势集中在「精确前沿」,日常出单个组合 cvxpy 足矣。

本节要点回顾

  1. CLA 是 Markowitz 原始解法:专为带 weight_bounds 的二次规划设计,精确解析地分段算整条有效前沿。
  2. 转折点(turning point):每个权重撞边界的瞬间,状态切换;整条前沿是转折点之间的线段。
  3. _solve 循环:起点(高 μ 满仓)→ 沿前沿走 → 每次找下一个进/出边界的资产 → 算新转折点权重。
  4. max_sharpe:在相邻转折点的凸组合上用黄金分割搜 Sharpe,精度比 cvxpy 间接法更高。
  5. efficient_frontier:一次性输出 (mus, sigmas, weights) 三列表,可直接画前沿曲线。
  6. 应用边界:仅支持 weight_bounds 一种约束,大规模 N 时慢;官方推荐除非要整条前沿,否则用 EfficientFrontier。

至此第 8 章讲完。下一章进入后处理——把任一优化器输出的连续权重,转成可执行的整数股数。


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