组合优化解决的问题是:给定一组预期收益和风险估计,如何分配权重。数学上这是一个二次规划问题,求解只要几毫秒;但工程上,组合优化的 90% 难度在于输入的质量——预期收益的估计误差、协方差的估计误差,都会被优化器放大。
Markowitz 的均值方差模型有一个著名的问题:它对输入极度敏感。预期收益变动 1%,最优权重可能变动 50%。这就是「误差最大化器」(error maximizer)的由来。工程实践中的大部分技巧,都是在抑制这种敏感性。
本文按「模型 → 估计 → 结构 → 约束 → 稳健 → 替代目标 → 求解 → 再平衡」的顺序展开。因子的构造与检验在 因子研究与有效性检验 中讨论,本文假设已经有一组经过验证的信号。
目录
- 从信号到组合的完整链路
- 均值方差模型与它的病态
- 协方差矩阵估计
- 结构化风险模型
- 约束与换手惩罚
- 稳健优化与收缩估计
- 风险平价与替代目标
- 优化求解器工程
- 再平衡与换手控制
1. 从信号到组合的完整链路
从因子到最终下单,中间有一系列变换,每一步都可能引入损失:
因子值 → 因子合成 → 预期收益预测 → 组合优化 → 目标权重
→ 交易成本约束 → 订单生成 → 执行
def signal_to_weights(factor_composite, cov, prev_w,
risk_aversion=5.0, max_weight=0.03):
mu = factor_composite * 0.01 # 因子 → 预期收益(年化 1% 尺度)
n = len(mu)
def neg_utility(w): # 目标:max mu^T w - lambda/2 * w^T Σ w
return -(mu @ w - risk_aversion / 2 * w @ cov @ w)
constraints = [{'type': 'eq', 'fun': lambda w: w.sum() - 1}] # 满仓
bounds = [(-max_weight, max_weight)] * n # 单票上限
x0 = prev_w if prev_w is not None else np.ones(n) / n
res = minimize(neg_utility, x0, bounds=bounds, constraints=constraints)
return res.x
risk_aversion(风险厌恶系数)是关键参数:它决定了在「追求收益」和「控制风险」之间的平衡。实践中通常通过目标波动率反推:先定组合年化波动 8%,再反解 λ。
2. 均值方差模型与它的病态
标准形式:
max μᵀw − (λ/2)·wᵀΣw
s.t. 1ᵀw = 1 (满仓)
w ≥ 0 (不做空)
|w| ≤ u (单票上限)
问题在于 μ 的估计误差远大于 Σ 的估计误差。预期收益的估计需要几十年的数据才有统计显著性,而协方差用一年数据就相对稳定。当 μ 有噪声时,优化器会把权重集中到「噪声最大的那几个」上。
| 输入 | 估计难度 | 误差影响 |
|---|---|---|
| 预期收益 μ | 极高 | 权重剧烈变化 |
| 协方差 Σ | 中 | 影响风险估计 |
| 交易成本 | 低 | 影响换手 |
一个经典的诊断实验:对 μ 加入 1% 的随机扰动,看最优权重的变化幅度。如果换手超过 100%,说明模型不可用。
def sensitivity_test(mu, cov, n_trials=100, noise=0.01):
base = optimize(mu, cov)
turns = []
for _ in range(n_trials):
mu_noisy = mu + np.random.normal(0, noise, len(mu))
w = optimize(mu_noisy, cov)
turns.append(np.abs(w - base).sum())
return np.mean(turns), np.percentile(turns, 95)
3. 协方差矩阵估计
样本协方差矩阵在股票数量大于时间长度时是奇异的(不可逆)。A 股 5000 只股票,一年 250 个交易日,样本协方差矩阵的秩只有 249,直接求逆会得到无意义的结果。
三种解决方案:
| 方法 | 思路 | 适用 |
|---|---|---|
| 收缩估计 | 向结构化目标收缩 | 通用,最稳健 |
| 因子模型 | 用因子解释协方差 | 股票数多 |
| 指数加权 | 近期数据权重高 | 波动聚集 |
Ledoit-Wolf 收缩是最实用的方法,它把样本协方差向一个结构化目标(如对角矩阵或常数相关矩阵)加权平均:
from sklearn.covariance import LedoitWolf
def estimate_cov(returns): # returns 为 T×N 矩阵,T 天数,N 股票数
lw = LedoitWolf(assume_centered=False).fit(returns)
shrunk = lw.covariance_ # 收缩后的协方差
shrinkage = lw.shrinkage_ # 收缩强度,0~1
return shrunk * 252, shrinkage # 年化
收缩强度 shrinkage_ 本身是个有用的诊断:如果它接近 1,说明样本协方差几乎全是噪声;接近 0 说明样本足够。
指数加权(EWMA)适合捕捉波动率聚集:
def ewma_cov(returns, halflife=60):
lam = np.exp(np.log(0.5) / halflife) # RiskMetrics 风格衰减因子
T, N = returns.shape
weights = lam ** np.arange(T)[::-1]
weights /= weights.sum()
demeaned = returns - returns.mean(axis=0)
return (demeaned * weights[:, None]).T @ demeaned * 252
半衰期 60 天是常见起点:太短则协方差不稳定,太长则反应迟钝。
4. 结构化风险模型
Barra 式的风险模型把个股收益拆成「因子暴露 × 因子收益 + 特质收益」:
r = X·f + u
X:N×K 的因子暴露矩阵(行业 + 风格)
f:K 维因子收益
u:特质收益(假设对角)
组合协方差变成:
Σ = X·Σf·Xᵀ + Δ
Σf:K×K 的因子协方差(K 通常 30~60)
Δ:N×N 的对角矩阵(特质风险)
这个结构的巨大优势是把 N×N 的估计问题降维成 K×K。5000 只股票直接估需要 1250 万个参数,用 50 个因子只需要 1275 个。
def factor_cov(X, factor_returns, spec_var): # X 为 N×K 暴露,factor_returns 为 T×K
Sigma_f = np.cov(factor_returns, rowvar=False) * 252 # K×K
Delta = np.diag(spec_var) # 特质风险对角阵
return X @ Sigma_f @ X.T + Delta
工程实现上,这类矩阵运算的规模(N=5000,K=50)用 numpy 就能秒级完成,不需要分布式计算。如果组合优化需要在大规模股票池上高频运行,才考虑用 C++ 或 GPU 加速,相关数据结构参考 C++ 无锁数据结构 。
5. 约束与换手惩罚
约束是让优化结果「可用」的关键。没有约束的最优解通常包含大量不可交易的极端权重。
| 约束 | 形式 | 作用 |
|---|---|---|
| 满仓 | 1ᵀw = 1 | 资金约束 |
| 单票上限 | w ≤ 3% | 分散风险 |
| 行业中性 | Xᵀw = 0 | 剥离行业暴露 |
| 风格中性 | 市值/BETA 暴露 = 0 | 剥离风格 |
| 换手上限 | |w − w_prev|₁ ≤ τ | 控制成本 |
| 换手惩罚 | −c·|w − w_prev|₁ | 软约束 |
换手惩罚的实现需要注意:|x| 在 0 处不可导,需要引入辅助变量线性化:
def build_l1_turnover(n, prev_w):
c = np.ones(2 * n) # 最小化 sum(c*(u+v)),约束 w - prev_w = u - v
A_eq = np.hstack([np.eye(n), -np.eye(n)]) # w - prev_w = u - v
return c, A_eq
这样就把 L1 范数转化成了线性规划,可以用标准的 QP 求解器处理。
6. 稳健优化与收缩估计
抑制 μ 误差的三种主流方法:
| 方法 | 思路 | 效果 |
|---|---|---|
| 收缩预期收益 | μ 向横截面均值收缩 | 简单有效 |
| 约束预期收益 | 限制 μ 的取值范围 | 稳健 |
| 重采样 | 多次 bootstrap 求平均 | 计算量大 |
| Black-Litterman | 贝叶斯融合市场均衡 | 需要观点输入 |
收缩预期收益最简单:
def shrink_mu(mu, alpha=0.5): # alpha 越大,收缩越强
return alpha * mu.mean() + (1 - alpha) * mu
alpha = 0.5 意味着「一半相信因子,一半相信所有股票一样好」,听起来很保守,但实测中常常比全信因子表现更好。
**重采样(Resampled Efficiency)**是更彻底的方法:对 μ 和 Σ 做多次 bootstrap,每次求最优权重,最后取平均。它把优化结果从「一个点」变成「一个分布的中心」,天然稳健。代价是计算量 ×1000。
7. 风险平价与替代目标
当预期收益不可信时,可以完全放弃收益预测,只做风险分配:
| 方法 | 目标 | 特点 |
|---|---|---|
| 等权 | w = 1/N | 最稳健,无参数 |
| 最小方差 | min wᵀΣw | 只依赖 Σ |
| 风险平价 | 各资产风险贡献相等 | 分散风险 |
| 最大分散化 | max 分散度 | 兼顾相关性 |
风险平价的目标是让每个资产的风险贡献(RC)相等:
RC_i = w_i · (Σw)_i / (wᵀΣw)
目标:RC_i = 1/N,对所有 i
from scipy.optimize import minimize
def risk_parity(cov):
n = cov.shape[0]
def objective(w):
port_var = w @ cov @ w
rc = w * (cov @ w) / port_var # 各资产风险贡献
return ((rc - 1.0 / n) ** 2).sum() # 目标:都等于 1/N
cons = [{'type': 'eq', 'fun': lambda w: w.sum() - 1}]
res = minimize(objective, np.ones(n) / n, bounds=[(1e-6, 1)] * n, constraints=cons)
return res.x
风险平价在债券/商品/股票多资产配置中表现稳定,因为它不依赖任何收益预测。代价是它放弃了 alpha——如果某个资产确实有更高收益,风险平价不会给它更多权重。
8. 优化求解器工程
求解器选择直接影响可用性:
| 求解器 | 支持问题 | 速度 | 开源 |
|---|---|---|---|
| scipy.optimize | 通用非线性 | 慢 | 是 |
| cvxpy | 凸优化建模 | 中 | 是 |
| OSQP | 二次规划 | 快 | 是 |
| MOSEK | 锥规划 | 极快 | 否 |
| Gurobi | 混合整数 | 快 | 否 |
cvxpy 的建模能力最强,把约束写成数学形式即可:
import cvxpy as cp
def optimize_cvxpy(mu, cov, prev_w, risk_aversion=5.0,
max_weight=0.03, turnover_limit=0.3):
n = len(mu)
w = cp.Variable(n)
objective = cp.Maximize(mu @ w - risk_aversion / 2 * cp.quad_form(w, cov))
constraints = [
cp.sum(w) == 1,
w <= max_weight,
w >= -max_weight,
cp.norm1(w - prev_w) <= turnover_limit,
]
prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.OSQP) # OSQP 适合大规模 QP
return w.value
生产环境要注意求解失败的降级路径:如果优化不收敛或超时,应该回退到上一个有效权重或等权组合,而不是用半成品结果下单。
9. 再平衡与换手控制
即使优化结果正确,每天全量再平衡也会被成本吃掉。控制换手的三种手段:
1. 不交易带(no-trade band):偏离目标权重 < 0.5% 不动
2. 分批执行:把大调仓拆成几天完成
3. 换手上限:约束每日换手不超过 20%
def apply_no_trade_band(target_w, current_w, band=0.005):
diff = target_w - current_w # 偏离小于阈值的股票保持不动
new_w = np.where(np.abs(diff) < band, current_w, target_w)
return new_w / new_w.sum() # 重新归一化
不交易带的副作用是组合会缓慢偏离目标,需要监控偏离度并在超过阈值时强制再平衡。这与 交易风控与实时限额 中的限额监控是同一套机制。
权衡取舍
| 维度 | 均值方差 | 最小方差 | 风险平价 | 等权 |
|---|---|---|---|---|
| 需要收益预测 | 是 | 否 | 否 | 否 |
| 参数敏感性 | 极高 | 中 | 低 | 无 |
| 理论收益 | 最高 | 低 | 中 | 中 |
| 实盘稳健性 | 低 | 高 | 高 | 高 |
实践中的常见组合是**「因子预测 + 稳健优化 + 约束」**:用因子给出 μ,用收缩和约束抑制误差,用换手惩罚控制成本。纯均值方差(无约束、无收缩)在实盘中几乎不可用。
风险模型的选择上,样本协方差适合股票数少(<100)的场景,因子模型适合股票数多(>500)的场景。中间的 100~500 区间可以用收缩估计过渡。
常见坑清单
- 直接用样本协方差求逆:股票数大于天数时矩阵奇异,必须收缩或降维。
- 预期收益不做收缩:优化器会把权重集中到噪声最大的股票上。
- 没有单票上限:最优解可能给某只票 30% 权重,无法交易。
- 不做行业中性:组合会意外押注某个行业,风格反转时巨亏。
- 每天全量再平衡:换手成本会吃掉大部分 alpha。
- 忽略交易成本:优化目标里没有成本项,结果必然换手过高。
- 协方差不做年化:日频协方差与年化收益混用,风险预算差 250 倍。
- 求解失败仍下单:优化不收敛时必须回退到安全权重。
- 不监控风险暴露:优化结果的名义约束满足,但实际暴露超标。
- 忽略停牌与涨跌停:优化出的权重包含无法交易的股票。
小结
组合优化的本质是在不确定的输入上做出稳健的决策。数学模型只是框架,真正的功夫在于:把预期收益的估计做得保守、把协方差的估计做得稳健、把约束设得贴近交易现实、把换手控制在成本可承受的范围内。
一个实用的判断标准:如果你的组合优化器每天给出的权重变化超过 30%,说明输入噪声太大,应该加强收缩和约束,而不是相信这些变化。
下一步建议阅读 订单管理与执行算法 ,看目标权重如何变成实际成交。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。