组合优化与风险模型

组合优化与风险模型的工程实践:从信号到权重的完整链路、均值方差模型的病态问题、协方差矩阵的收缩与因子估计、Barra 式结构化风险模型、约束与换手惩罚建模、稳健优化、风险平价等替代目标,以及优化求解器的工程落地与再平衡控制,回答为什么均值方差被称为误差最大化器以及如何抑制输入噪声。

组合优化解决的问题是:给定一组预期收益和风险估计,如何分配权重。数学上这是一个二次规划问题,求解只要几毫秒;但工程上,组合优化的 90% 难度在于输入的质量——预期收益的估计误差、协方差的估计误差,都会被优化器放大。

Markowitz 的均值方差模型有一个著名的问题:它对输入极度敏感。预期收益变动 1%,最优权重可能变动 50%。这就是「误差最大化器」(error maximizer)的由来。工程实践中的大部分技巧,都是在抑制这种敏感性。

本文按「模型 → 估计 → 结构 → 约束 → 稳健 → 替代目标 → 求解 → 再平衡」的顺序展开。因子的构造与检验在 因子研究与有效性检验 中讨论,本文假设已经有一组经过验证的信号。

目录

  1. 从信号到组合的完整链路
  2. 均值方差模型与它的病态
  3. 协方差矩阵估计
  4. 结构化风险模型
  5. 约束与换手惩罚
  6. 稳健优化与收缩估计
  7. 风险平价与替代目标
  8. 优化求解器工程
  9. 再平衡与换手控制

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 区间可以用收缩估计过渡。

常见坑清单

  1. 直接用样本协方差求逆:股票数大于天数时矩阵奇异,必须收缩或降维。
  2. 预期收益不做收缩:优化器会把权重集中到噪声最大的股票上。
  3. 没有单票上限:最优解可能给某只票 30% 权重,无法交易。
  4. 不做行业中性:组合会意外押注某个行业,风格反转时巨亏。
  5. 每天全量再平衡:换手成本会吃掉大部分 alpha。
  6. 忽略交易成本:优化目标里没有成本项,结果必然换手过高。
  7. 协方差不做年化:日频协方差与年化收益混用,风险预算差 250 倍。
  8. 求解失败仍下单:优化不收敛时必须回退到安全权重。
  9. 不监控风险暴露:优化结果的名义约束满足,但实际暴露超标。
  10. 忽略停牌与涨跌停:优化出的权重包含无法交易的股票。

小结

组合优化的本质是在不确定的输入上做出稳健的决策。数学模型只是框架,真正的功夫在于:把预期收益的估计做得保守、把协方差的估计做得稳健、把约束设得贴近交易现实、把换手控制在成本可承受的范围内。

一个实用的判断标准:如果你的组合优化器每天给出的权重变化超过 30%,说明输入噪声太大,应该加强收缩和约束,而不是相信这些变化。

下一步建议阅读 订单管理与执行算法 ,看目标权重如何变成实际成交。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「量化交易」更多文章

  1. 风险模型与因子归因
  2. 回测偏差与过拟合防范
  3. 市场微结构与流动性