第 24 讲:凸优化求解器(cvxpy)实战
本讲核心目标
在现代买方量化机构中,“机器学习打分(Alpha)”只是前置原料,**投资组合凸优化(Portfolio Convex Optimization)**才是将预测转化为稳健资管产品的“总装反应炉”。
初级量化研究员往往按预测分数挑选前 50 或 100 只股票等权买入,结果组合无意识暴露了数十个百分点的行业偏斜和小市值下沉风险,一旦市场风格突变便遭毁灭性回撤。本讲融合 MSCI Barra 组合构建规范 与 华泰证券多因子体系优化模型:系统推导指数增强(Index Enhancement)二次规划(QP)与二阶锥规划(SOCP)数学模型;详解利用 Barra 结构化特性在 cvxpy 中降维提速至毫秒级的核心算法;严格构建行业硬中性、市值偏离容差、个股超配上限与预测跟踪误差(TE)硬上限;并推导**因子边际风险贡献(FMCAR)**对主动风险来源的穿透解析!
1. 经典文献与工业界理论基石
构建严谨的组合优化系统,离不开数学规划与现代金融工程的经典文献支持:
- Markowitz, Harry (1952). "Portfolio Selection", The Journal of Finance, 7(1): 77-91.
现代投资组合理论(MPT)奠基之作,首次将投资决策转化为均值-方差二次规划极值问题。
- Boyd, Stephen, & Vandenberghe, Lieven (2004). Convex Optimization. Cambridge University Press.
全球运筹学与凸优化圣经,系统阐释了凸集、凸函数、对偶理论(KKT 条件)、二次规划(QP)与二阶锥规划(SOCP)的标准范式。
- 华泰证券研究所 (林晓明、陈烨, 2016). 《华泰多因子模型体系初探——华泰多因子系列之一》, 第四部分:优化模型.
系统推导了国内 A 股指数增强下的二次规划目标函数、哑变量行业中性约束矩阵、个股非负下界约束及因子边际风险贡献(FMCAR)解析体系。
- Boyd, S., Busseti, E., He, S., & Kang, J. (2017). "Multi-Period Trading via Convex Optimization", Foundations and Trends in Optimization, 3(1): 1-76.
斯坦福大学 Boyd 团队关于凸优化在买方多期量化交易中的前沿综述,详述了交易成本、冲击与跟踪误差的多目标求解。
- Lobo, M. S., Vandenberghe, L., Boyd, S., & Lebret, H. (1998). "Applications of second-order cone programming", Linear Algebra and its Applications, 284(1-3): 193-228.
证明了二次风险约束可通过 Cholesky 分解严格转化为二阶锥约束(Second-Order Cone, SOC),为内点法高效求解奠定基础。
2. 为什么“等权选股”在指数增强实盘中是死路一条?
许多量化团队训练出了 Rank IC 高达
根本原因在于:未加风险控制的主动投资组合,本质上是在用全部净值豪赌行业和风格!
3. 指数增强组合优化的严谨数学建模
设全市场可交易股票池(或基准成分股扩展池)共有
3.1 决策变量与主动偏离权重
:待求解的投资组合持仓权重向量(决策变量); :基准指数成分权重向量(如中证 500 成分股权重,满足 ); :主动偏离权重向量(Active Weights)。
3.2 组合优化的三大经典形式(华泰多因子框架)
在买方实际运作中,二次规划通常有以下三种等价变换形式:
- 形式一(给定风险预算,最大化收益):
- 形式二(给定预期收益目标,最小化风险):
- 形式三(风险惩罚均值-方差效用最大化,工业界最常用):
其中:
:经第 22 讲样本外校准的持有期 日预期主动超额收益率向量; :将年化 Barra 协方差折算为持有期协方差; :风险厌恶系数(Risk Aversion Parameter)。
4. Barra 结构化低维加速(cvxpy 算力优化秘籍)
若在 cvxpy 中直接书写 cp.quad_form(h, Sigma),当
利用上一讲推导的 Barra 结构化分解
定义主动因子暴露向量为
在 cvxpy 中,这一变换不仅将内存消耗降低 99%,而且严格符合 DCP(Disciplined Convex Programming)规范:
# cvxpy 结构化低维算力优化实现
active_factor_exp = cp.matmul(X.T, h) # (K,) 维线性变换
factor_risk = cp.quad_form(active_factor_exp, F) # 仅 (K, K) 维小矩阵二次型!
specific_risk = cp.sum(cp.multiply(delta, cp.square(h))) # (N,) 向量逐元素相乘!
total_variance = factor_risk + specific_risk5. A 股实盘约束系统全解
实盘指数增强模型必须施加多维硬性与弹性约束,以满足基金合同与中国证监会监管法规:
5.1 资金预算与单边多头约束(Budget & Long-Only)
在主动权重形式下:
因为无法裸卖空股票(
5.2 行业硬中性约束(Industry Neutrality)
设
此约束强制组合在每一个一级行业上的配置比例与基准指数严丝合缝,彻底剔除行业轮动 Beta 波动。
5.3 风格因子偏离容忍约束(Style Tolerance)
防止模型为博取收益而隐性向微盘股、高波动股票下沉:
例如设定对数市值风格主动暴露
5.4 个股权重主动偏离上下限(Active Stock Bounds)与“双十原则”
- 相对基准超配上限:
实盘中中证 500 指增通常设定 ,中证 1000 指增设定 ; - 绝对持仓上限(公募“双十原则”):单只股票持仓市值不得超过基金资产净值的
(实盘指增通常单股上限设为 以极致分散特异风险)。
5.5 预测跟踪误差硬上限(Tracking Error Hard Bound)
目标函数中的风险厌恶系数
6. 因子边际风险贡献(FMCAR)与主动风险穿透解析
华泰证券多因子报告系统推导了组合优化完成后,如何通过解析求导反向穿透追踪风险来源:
6.1 因子对主动风险的边际贡献(FMCAR)公式
组合的总主动方差为:
其中
对主动因子暴露求偏导数,定义因子对主动风险的边际贡献(Factor Marginal Contribution to Active Risk, FMCAR):
6.2 主动风险精准加总分解
由矩阵乘法展开,得到因子风险对主动波动率的贡献:
总主动跟踪误差
- 实战意义:在完成组合优化后,投资经理可直接打印每个因子的边际贡献
。若发现 Size 因子的风险贡献占比达到 40%,即可迅速警觉市值暴露过载,并在优化器中调紧市值容差参数。
7. 现代凸优化求解器选型:Clarabel vs OSQP vs MOSEK
| 求解器 (Solver) | 算法架构 | 是否支持 SOCP 跟踪误差硬约束 | 开源/商业许可 | 求解速度与数值精度 | 工业界应用场景 |
|---|---|---|---|---|---|
| Clarabel | 原生内点法(Interior Point Method) | ✅ 原生支持 | 开源(Apache 2.0,Rust 内核) | 极高精度,对病态矩阵极稳健,毫秒级收敛 | cvxpy 官方推荐首选现代化开源锥求解器 |
| OSQP | 交替方向乘子法(ADMM) | ❌ 仅支持标准线性约束 QP | 开源(Apache 2.0) | 极快(早期迭代),但达到极高精度收敛较慢 | 适用于无二次风险硬约束的大规模高频 QP 优化 |
| MOSEK | 工业级对偶原点内点法 | ✅ 原生支持 | 商业闭源(学术免费) | 工业界全球金标准,极速稳定,鲁棒性登峰造极 | 头部百亿量化私募与主权基金生产标配 |
| SCS | 分裂圆锥求解器(ADMM) | ✅ 支持 | 开源(MIT) | 适合超大规模问题,但数值精度较低 | 适用于快速原型验证 |
8. 工业级 cvxpy 指数增强优化器实战
在 tutorial/examples/lecture_24_cvxpy_portfolio_optimization.py 中,我们实现了生产级 AshareIndexEnhancementOptimizer:
- 封装了 Barra 结构化协方差降维加速;
- 完整构建行业硬中性、市值容差、个股主动超配与年化跟踪误差硬上限约束;
- 输出最优配置权重
,并严格校核每一项 KKT 约束残差。
可在终端直接运行验证:
python3 -m examples.lecture_24_cvxpy_portfolio_optimization"""
教学用 A 股指数增强凸优化器 (AshareIndexEnhancementOptimizer)
基于 cvxpy 与 Barra 结构化风险模型构建
实现:行业硬中性、市值约束、个股上下界限制与二次跟踪误差惩罚
"""
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
import cvxpy as cp
import numpy as np
from examples.portfolio_utils import validate_risk_inputs, solve_checked
@dataclass
class OptimizationResult:
"""优化结果与组合全景体检报告"""
horizon_days: int
optimal_weights: np.ndarray
expected_active_return: float
annualized_tracking_error: float
active_share: float
effective_stocks_count: int
industry_max_deviation: float
size_style_deviation: float
status: str
def print_dossier(self):
"""格式化打印优化报告"""
print("=" * 70)
print(f"🎯 指数增强凸优化求解报告 [求解状态: {self.status}]")
print("=" * 70)
print("【核心收益与主动风险】")
print(f" • 预期持有期主动超额 (Alpha) : {self.expected_active_return * 100:8.2f}%")
print(f" • 预期年化跟踪误差 (TE) : {self.annualized_tracking_error * 100:8.2f}%")
print(f" • 收益预测持有期 : {self.horizon_days} 个交易日")
print(f" • 主动份额 (Active Share) : {self.active_share * 100:8.2f}%")
print(f" • 有效持仓股票数 (ENP) : {self.effective_stocks_count:8d} 支")
print("-" * 70)
print("【风控合规穿透检查】")
print(f" • 给定行业最大净暴露偏差 : {self.industry_max_deviation * 100:8.4f}% ")
print(f" • 对数市值风格净偏离 (Size) : {self.size_style_deviation:8.4f} ")
print("=" * 70)
class AshareIndexEnhancementOptimizer:
"""
A 股指数增强二次凸优化器
"""
def __init__(
self,
risk_aversion: float = 2.0, # 风险厌恶系数 lambda
max_active_weight: float = 0.015, # 单只股票最大主动超配权重 +1.5%
size_tolerance: float = 0.05, # 市值风格偏离硬容忍上限
max_tracking_error: float = 0.045,
horizon_days: int = 5,
annual_factor: float = 250.
):
if risk_aversion < 0 or max_active_weight < 0 or size_tolerance < 0 or max_tracking_error <= 0 or horizon_days < 1 or annual_factor <= 0:
raise ValueError("风险/约束参数范围不合法")
self.max_tracking_error = max_tracking_error
self.horizon_days, self.annual_factor = horizon_days, annual_factor
self.risk_aversion = risk_aversion
self.max_active_weight = max_active_weight
self.size_tolerance = size_tolerance
def optimize(
self,
expected_returns: np.ndarray, # (N,) H 日预期超额收益率;不能传入未经校准的排序分数
benchmark_weights: np.ndarray, # (N,) 基准指数成分股权重 (sum=1)
exposure_matrix: np.ndarray, # (N, K) 因子暴露矩阵 (前 K_ind 列为行业, 之后为风格)
factor_covariance: np.ndarray, # (K, K) 因子协方差矩阵 F
specific_variance: np.ndarray, # (N,) 特异性方差对角元素 Δ
n_industries: int = 6, # 行业因子列数
size_col_idx: int = 6 # 市值因子列索引
) -> OptimizationResult:
"""
构建带二次风险约束的凸优化问题 问题并求解最优持仓权重
"""
expected_returns, benchmark_weights, exposure_matrix, factor_covariance, specific_variance = validate_risk_inputs(
expected_returns, benchmark_weights, exposure_matrix, factor_covariance, specific_variance, n_industries, size_col_idx)
N = len(expected_returns)
# 1. 定义优化决策变量: 投资组合最终权重 w (N,)
w = cp.Variable(N, nonneg=True) # 单边做多约束: w_i >= 0
h = w - benchmark_weights # 主动偏离向量 h
# 2. 目标函数: max h^T alpha - (lambda / 2) * (h^T Σ h)
# 利用 Barra 形式降低维度: h^T Σ h = y^T F y + h^T Δ h
y = exposure_matrix.T @ h # 主动因子暴露 (K,)
# 风险项: 因子风险 + 特异性风险
factor_risk = cp.quad_form(y, factor_covariance)
specific_risk = cp.sum(cp.multiply(specific_variance, cp.square(h)))
portfolio_risk = factor_risk + specific_risk
expected_return = expected_returns @ h
period_risk = portfolio_risk * self.horizon_days / self.annual_factor
objective = cp.Maximize(expected_return - (self.risk_aversion / 2.0) * period_risk)
# 3. 约束条件构建
constraints = [
cp.sum(w) == 1.0, # 全额投资预算约束
w >= 0,
portfolio_risk <= self.max_tracking_error ** 2,
]
# 3.1 行业硬中性约束: X_ind^T (w - w_b) = 0
X_ind = exposure_matrix[:, :n_industries]
constraints.append(X_ind.T @ h == 0.0)
# 3.2 市值风格偏离软容忍约束: |X_size^T h| <= delta
size_exp = exposure_matrix[:, size_col_idx]
constraints.append(cp.abs(size_exp @ h) <= self.size_tolerance)
# 3.3 个股主动权重上限约束: w_i - w_bi <= max_active_weight
constraints.append(h <= self.max_active_weight)
# 二次 TE 硬约束需要锥求解器;求解后检查所有约束,不裁剪权重。
prob = cp.Problem(objective, constraints)
opt_w = solve_checked(prob, w, constraints)
# 5. 体检指标测算
active_w = opt_w - benchmark_weights
exp_ret = float(expected_returns @ active_w)
# 跟踪误差
y_opt = exposure_matrix.T @ active_w
f_risk = float(y_opt.T @ factor_covariance @ y_opt)
s_risk = float(np.sum(specific_variance * (active_w ** 2)))
te_ann = np.sqrt(f_risk + s_risk)
# 主动份额 (Active Share = 1/2 * sum(|w - w_b|))
active_share = float(0.5 * np.sum(np.abs(active_w)))
# 有效持仓股票数 (Herfindahl-Hirschman: 1 / sum(w_i^2))
enp = int(np.round(1.0 / np.sum(opt_w ** 2)))
# 行业最大暴露偏差
ind_bias = np.abs(X_ind.T @ active_w)
max_ind_dev = float(np.max(ind_bias))
# 市值暴露偏差
size_dev = float(size_exp @ active_w)
return OptimizationResult(
horizon_days=self.horizon_days,
optimal_weights=opt_w,
expected_active_return=exp_ret,
annualized_tracking_error=te_ann,
active_share=active_share,
effective_stocks_count=enp,
industry_max_deviation=max_ind_dev,
size_style_deviation=size_dev,
status=prob.status
)
# ================= 单元测试与指增组合优化实战 =================
if __name__ == "__main__":
np.random.seed(42)
n_stocks = 500 # 模拟中证 500 成分股池
n_factors = 7 # 6 个行业因子 + 1 个 Size 因子
# 1. 模拟基准指数权重 (如中证 500 等权偏离,各成分股权重在 0.1% ~ 0.5% 之间)
raw_b = np.random.uniform(0.1, 0.5, n_stocks)
w_benchmark = raw_b / np.sum(raw_b)
# 2. 模拟 Barra 因子暴露
X_mat = np.zeros((n_stocks, n_factors))
ind_assign = np.random.choice(6, n_stocks)
for i in range(n_stocks):
X_mat[i, ind_assign[i]] = 1.0 # 行业 One-Hot
# 最后一列为标准化市值因子
X_mat[:, 6] = np.random.normal(0, 1.0, n_stocks)
# 3. 模拟 Barra 风险参数
F_cov = np.eye(n_factors) * 0.04
spec_var = np.random.uniform(0.04, 0.12, n_stocks) # 个股年化特异方差
# 4. 模拟持有期收益率预测,独立于排序模型分数
alpha = np.random.normal(0.02, 0.05, n_stocks) * 5 / 250 # 合成 5 日预期收益,不是排序分数
# 5. 实例化并求解指数增强优化
optimizer = AshareIndexEnhancementOptimizer(
risk_aversion=3.0,
max_active_weight=0.012, # 单只股票最多超配 1.2%
size_tolerance=0.03 # 市值偏离严格限制在 0.03 以内
)
report = optimizer.optimize(
expected_returns=alpha,
benchmark_weights=w_benchmark,
exposure_matrix=X_mat,
factor_covariance=F_cov,
specific_variance=spec_var,
n_industries=6,
size_col_idx=6
)
# 输出买方体检卷宗
report.print_dossier()9. 本讲小结与核心自测
| 核心考点 | 工业标准回答要点 | 常见工程/学术陷阱 |
|---|---|---|
| 为什么个股主动偏离下限是 | A 股公募与普通私募严禁卖空( | 随意将下限设为固定的 |
| 因子边际风险贡献(FMCAR)在组合风控中有何作用? | 解析计算 | 仅关注单因子暴露大小,忽视了因子之间协方差联动对跟踪误差的放大。 |
| ** Clarabel 求解器相比 OSQP 的本质区别?** | Clarabel 支持二阶锥(SOCP),能够严格处理形如 | 在包含二次不等式约束时强行指定 solver=cp.OSQP,导致求解器抛出不支持的锥异常。 |
在下一讲中,我们将进入 第 25 讲:换手率约束与交易成本惩罚项引入,将线性佣金税费与非线性市场冲击摩擦直接作为惩罚项编入凸优化目标函数!