halo 的技术博客

返回

如果你做过均值-方差优化,一定见过这个画面:换一组历史数据,组合权重就天翻地覆;某个预期收益被样本噪声推高的资产,会被配上 30%、40% 甚至更高的极端权重。均值-方差组合的脆弱,根子在”预期收益 μ”上——它只有一个样本均值可以估计,而它的估计误差会被目标函数里的 w'μ 项直接放大成权重。

结论先放这里:把”估计可能错了”显式写进优化目标,组合在最坏情形下的表现会稳得多。 具体做法是——不再对”真实 μ 等于样本均值 μ̂”深信不疑,而是承认 μ 落在一个以 μ̂ 为中心的不确定性集里,然后在”这个集合里最差的那一个 μ”下求最优。我们用一个参数 κ 控制”有多保守”。在 10 个资产、用 60 天历史估计、真实最优 Sharpe 约 0.84 的设定下,朴素均值-方差的样本外 Sharpe 中位数只有 0.66、最差 5% 分位掉到 0.48;而鲁棒优化(κ=1.5)把它拉回 0.78 / 0.62——相当于把估计误差导致的”翻车”砍掉一大半。

这篇文章用最朴素的 numpy + scipy,从零实现这套鲁棒均值-方差优化,并画出它为什么有效。

一、问题:均值-方差为什么对 μ 的误差过敏#

经典(多头)均值-方差问题:

import numpy as np
from scipy.optimize import minimize

def solve_mv(mu, Sigma, lam, lbound=0.0):
    """max  w'mu - (lam/2) w'Sigma w   s.t. sum(w)=1, w>=lbound"""
    n = len(mu)
    def neg_obj(w):
        return -(w @ mu - 0.5 * lam * (w @ Sigma @ w))
    cons = [{"type": "eq", "fun": lambda w: w.sum() - 1.0}]
    bounds = [(lbound, 1.0)] * n
    res = minimize(neg_obj, np.ones(n) / n, method="SLSQP",
                   bounds=bounds, constraints=cons, options={"ftol": 1e-12})
    return res.x
python

麻烦在于:实盘里你只有样本均值 μ̂ = (1/T)Σ r_t,而不是真实 μ。样本均值本身是个随机变量,它的误差协方差是 Σ_μ = Σ_true / T。当 T 不大(比如几十天)、资产数 N 又不少时,μ̂ 里混着大量噪声。优化器可不管这是噪声还是信号——它只看到”资产 A 的样本均值最高”,于是把权重狠狠压向 A。等样本外真刀真枪跑,μ̂ 里的噪声被证伪,组合就崩了。

这就是估计误差驱动的下注:你以为自己在押”高预期收益”,其实在押”样本噪声”。

二、鲁棒化的核心:给 μ 套一个不确定性集#

鲁棒优化的思路极其直白——不假设 μ = μ̂,而是假设 μ 落在某个集合 U 里,然后在 U 里最坏的那个 μ 下,仍然让目标尽量好:

max_w  min_{μ ∈ U}  [ w'μ - (λ/2) w'Σ̂ w ]      s.t.  sum(w)=1
plaintext

最常用、也最好算的是椭球不确定性集(ellipsoidal uncertainty set):

U = { μ : (μ - μ̂)' Σ_μ^{-1} (μ - μ̂)  ≤  κ² }
plaintext

这里 Σ_μ 是你对估计误差的认知(通常用 Σ_true / T),κ 是”半径”——κ 越大,你认为 μ 可能偏离 μ̂ 越远,优化就越保守。

关键一步来了:先对 μ 求最坏情形,内层 min 有解析解。对任意给定的 w,min_{μ∈U} w'μ 发生在 μ 沿 -w 方向走到椭球边界上,结果是:

min_{μ∈U} w'μ  =  w'μ̂  −  κ · √( w'Σ_μ w )
plaintext

√(w'Σ_μ w) 正是 w 在估计误差方向上的”暴露”。权重越集中,这个暴露越大,被扣的最坏情形惩罚也越狠。这样一来,原本”无脑押高 μ̂ 资产”的集中下注,会被自动惩罚——因为集中组合的 √(w'Σ_μ w) 最大。

于是鲁棒目标变成:

max_w  w'μ̂  −  κ·√(w'Σ_μ w)  −  (λ/2) w'Σ̂ w      s.t. sum(w)=1, w≥0
plaintext

三、从零实现鲁棒求解器#

比看起来简单。注意 −κ√(w'Σ_μ w) 是凹的(√· 凸,取负变凹),加上 −(λ/2)w'Σ̂ w 也是凹的,所以整个目标是凹函数,在单纯形上最大化——scipy 的 SLSQP 轻松搞定:

def solve_robust(mu_hat, Sigma_mu, Sigma_hat, lam, kappa, lbound=0.0):
    """鲁棒均值-方差: max w'mu_hat - kappa*sqrt(w'S_mu w) - (lam/2)w'Sig_hat w"""
    n = len(mu_hat)
    def neg_obj(w):
        wc = np.sqrt(max(w @ Sigma_mu @ w, 1e-18))
        return -(w @ mu_hat - kappa * wc - 0.5 * lam * (w @ Sigma_hat @ w))
    cons = [{"type": "eq", "fun": lambda w: w.sum() - 1.0}]
    bounds = [(lbound, 1.0)] * n
    res = minimize(neg_obj, np.ones(n) / n, method="SLSQP",
                   bounds=bounds, constraints=cons, options={"ftol": 1e-12})
    return res.x
python

椭球不确定性集下,集中下注被推向最坏方向

上图把这件事画了出来:μ̂ 周围那圈椭球就是”μ 可能的真实取值”。对某个偏向资产 B 的权重 w,它对应的最坏情形均值(红色虚线端点)被拉到了椭球另一侧——你押得越集中,最坏情形把你推得越远。鲁棒优化就是提前把这个”被推远”的量算进目标里。

四、κ 的作用:一个旋钮,把权重从极端拉回分散#

把 κ 从 0 调到 4,看权重怎么变(数据:10 个资产、因子模型协方差、60 天历史估计、风险厌恶 λ=5):

T = 60
R = rng.multivariate_normal(mu_true, SIGMA_true, size=T)
mu_hat, Sigma_hat = R.mean(0), np.cov(R.T)
Sigma_mu = SIGMA_true / T
for kappa in [0.0, 0.5, 1.0, 2.0, 4.0]:
    w = solve_robust(mu_hat, Sigma_mu, Sigma_hat, lam=5.0, kappa=kappa)
python

鲁棒性参数 κ 越大,权重越分散(向等权靠拢)

  • κ=0(退化为朴素均值-方差):权重极度集中,少数资产吃下大部分仓位——这正是估计误差在作祟。
  • κ 增大:权重迅速摊开,向等权(10%)靠拢。

这给了我们一个极其实用的直觉:鲁棒优化 ≈ “在估计不可靠时,主动放弃那些靠噪声撑起来的超额收益,换回分散化”。κ 就是你愿意为”睡得着觉”付出的预期收益代价。

五、样本外:鲁棒到底有没有用#

光看权重不够。真正的问题是——样本外,谁更扛造? 我们用蒙特卡洛把”估计误差”真实地跑出来:

rng = np.random.default_rng(2026)
nom, rob = [], []
for _ in range(600):
    R = rng.multivariate_normal(mu_true, SIGMA_true, size=60)
    mh, Sh, Sm = R.mean(0), np.cov(R.T), SIGMA_true / 60.0
    wn = solve_mv(mh, Sh, lam=5.0)                       # 名义
    wr = solve_robust(mh, Sm, Sh, lam=5.0, kappa=1.5)    # 鲁棒
    nom.append(wn @ mu_true / np.sqrt(wn @ SIGMA_true @ wn))  # 用真实参数算 Sharpe
    rob.append(wr @ mu_true / np.sqrt(wr @ SIGMA_true @ wr))
python

注意评价口径:用”真实” μ、Σ 算 Sharpe,这样衡量的是”这个权重在现实里到底行不行”,而不是在它自己那套有偏估计里自嗨。

估计误差下,鲁棒组合的样本外 Sharpe 更稳

结果(真实最优 Sharpe ≈ 0.84):

方法样本外 Sharpe 中位数最差 5% 分位
名义均值-方差 (κ=0)0.660.48
鲁棒 (κ=1.5)0.780.62

名义组合的 Sharpe 中位数就比真实最优低了 0.18——这就是估计误差吃掉的部分;而它的最差情形(5% 分位)只剩 0.48,翻车概率明显更高。鲁棒组合把中位数拉到 0.78、最差分位拉到 0.62,几乎把”估计误差导致的翻车”抹平。

关键认知:鲁棒优化不是让”平均情况”变好,而是把”最坏情况”兜底。它是保险,不是印钞机。

六、κ 该取多大?看你的样本有多短#

鲁棒优化的收益,高度依赖”估计有多不可靠”。样本越短,μ̂ 越脏,鲁棒越值钱:

for T in [30, 45, 60, 90, 120, 180, 240, 360]:
    # 同样 600 次蒙特卡洛,比较名义 vs 鲁棒(κ=1.5) 的样本外 Sharpe
    ...
python

样本越短,鲁棒优化的优势越大(随 T 增大收敛)

曲线很清楚:T=30 天时鲁棒优势最大,随着 T 拉长到 360 天,两条线几乎重合。这符合直觉——当你有几年日线、几百个样本时,μ̂ 已经够稳,鲁棒化的边际收益趋近于零。 所以鲁棒优化最该用在:短窗口、高换手、或因子收益本身就飘(宏观/危机期)的场景。

七、真实陷阱(别直接照抄)#

  1. Σ_μ 怎么来是个大坑。理论上 Σ_μ = Σ_true / T,但你连 Σ_true 都不知道。用样本协方差 S/T 代替会低估误差(尤其 T 小、N 大时 S 本身病态);更稳的是用 Ledoit-Wolf 收缩后的 Σ 再除以 T。κ 和 Σ_μ 是耦合的——Σ_μ 估小了,κ 再大也白搭。

  2. κ 没有”正确答案”,只有”你怕疼程度”。它本质是风险厌恶在”参数不确定性”维度上的投影。别用样本内表现去挑 κ(那会过拟合到某一个估计误差实现),应该用样本外/滚动回测固定一个值。

  3. 椭球集假设误差是椭圆对称的,现实不是。真实估计误差常是厚尾、非对称、且不同资产相关性结构会崩。更保守的替代是盒式集(box) |μ_i − μ̂_i| ≤ κ_i(最坏情形变成 w'μ̂ − Σ κ_i|w_i|,求解变 LP)或多胞形集。椭球只是计算最便宜的那个。

  4. 鲁棒化只管 μ,没管 Σ。协方差的估计误差同样致命(见最小方差组合那篇)。完整的鲁棒优化要对 (μ, Σ) 同时套不确定性集,求解会复杂很多(通常变成半定或二阶锥规划)。本文的单点鲁棒是个干净的起点,不是终点。

  5. 长仓约束改变了游戏。本文用了 w ≥ 0(不允许做空)。若放开做空,朴素均值-方差会借估计误差玩出负权重对冲的”幻觉收益”,鲁棒化的边际收益更大,但数值也更野——实盘慎用无约束做空。

八、小结#

鲁棒投资组合优化的精髓,一句话:不要把样本均值当成真相,要把”它可能错了多少”量化成一个集合,在最坏情形下求最优。 它的工程实现出奇简单——给目标函数加一项 −κ√(w'Σ_μ w),优化器自然会把集中下注的”噪声赌博”惩罚掉,把权重推向分散。

它不创造 alpha,但它保护你不在估计误差上翻车。在样本短、波动大、因子收益飘的环境里,这一个 κ 旋钮,往往比多挖十个因子更值得。

下一篇我们换个角度聊”执行”——当你真要卖出一个大头寸时,连”怎么卖”本身都是一门优化。

鲁棒投资组合优化:在最坏情形下也能站着
https://blog.halo26812.eu.org/blog/robust-portfolio-optimization
Author halo
Published at 2026年7月13日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨