量化交易中文教材

第 05 章 组合构建与优化

研究循环走到这一步,手里已经有了信号(本册第 03 章)和风险模型(第 04 章)。本章回答的问题是:怎样把"对收益的预测"和"对风险的估计"变成一组可以下单的目标权重,并且让这组权重在样本外不至于被估计误差摧毁。优化理论(KKT 条件、二次规划算法)已在第 04 册第 12、16a、16b 章讲透,本章只讲工程:输入怎样处理,约束怎样写,成本怎样进入目标,结果怎样检查,以及几种主流构建方法各自适合什么场合。

学习目标

读完本章,你应当能够:

  1. 解释均值–方差优化为什么会放大估计误差,能用特征分解和条件数定量说明放大的来源,并知道收缩期望收益和协方差各自能挽回多少。
  2. 把权重上下限、行业与风格中性、beta、杠杆、换手、跟踪误差写成凸约束,用买卖拆分变量交给 scipy.optimize 求解,并用有限差分读出约束的影子价格。
  3. 把交易成本写进目标函数,理解线性成本产生"不交易区间"、二次成本产生"部分调整",并知道成本与收益的时间尺度必须对齐。
  4. 实现最小方差、风险平价(ERC)、层次风险平价(HRP)和 Black–Litterman,并说出各自依赖哪些输入、在什么情况下会失效。
  5. 设计一个公平的滚动样本外比较,读懂其中哪些差异是噪声、哪些差异是真实的。

读前导读

这一章在解决什么问题。 这一章对应基金经理和投资决策委员会的工作:手上有研究员的观点(信号)和风控给的风险模型,要决定"每只股票具体买多少"。你在 CFA 里学过 Markowitz 有效前沿和切点组合,那是"参数已知"的理想情形。本章要回答的是:参数是估计出来的、有误差时,最优化会怎样出错,以及实务中怎样防范。

把各节对应到实际工作:

  • 5.2 误差放大:对应投委会常见的困惑——"模型给出的最优组合为什么总是重仓几只奇怪的股票?"答案是优化器在利用估计误差。
  • 5.3 数值稳定性:对应量化团队的"模型验证"职能,检查计算本身有没有出错。
  • 5.4 约束:对应投资合同、投资政策说明书(IPS)和风控限额:个股上限、行业偏离、跟踪误差、换手上限。影子价格回答"这条限额每年让我们少赚多少",是和合规、风控谈判时最有用的数字。
  • 5.5 交易成本:对应交易台的执行成本分析。核心结论是"不是每个信号变化都值得交易"。
  • 5.6 风险平价与 HRP:对应 FOF、多资产配置和养老金常用的"按风险分配"思路,桥水全天候策略就是风险平价的代表。
  • 5.7 Black–Litterman:对应资产配置委员会的流程:以市场均衡为基准,只在有明确观点的地方偏离,偏离幅度取决于观点的信心。
  • 5.8 样本外比较:对应绩效评估中的"业绩是能力还是运气"问题。

需要先想起来的数学。

  1. 二次型与组合方差:\(w^\top\Sigma w\) 就是 CFA 里的 \(\sum_i\sum_jw_iw_j\sigma_{ij}\)。\(w^\top\) 表示转置,\(\alpha^\top w=\sum_i\alpha_iw_i\) 是组合的期望 alpha。见 第 00 册第 06 章 线性代数速成。
  2. 梯度与一阶条件:多变量函数的最大值处,对每个变量的偏导数都为零(无约束时)。二次型的梯度 \(\nabla_w(w^\top\Sigma w)=2\Sigma w\),就像 \(\sigma^2w^2\) 对 \(w\) 求导得 \(2\sigma^2w\)。\(\nabla\) 读作 nabla,表示"对每个分量求偏导排成的向量"。
  3. 拉格朗日乘子、KKT 条件与影子价格:带约束优化时,给每条约束配一个乘子;乘子的经济含义是"约束放松一单位,目标改善多少",即影子价格。KKT 条件是拉格朗日方法在不等式约束下的推广:不起作用的约束乘子为零。例:若换手上限的影子价格是 0.7bp/1%,说明多允许 1% 换手每年多赚 0.7bp。见 第 00 册第 05 章 多元微积分与优化。
  4. 特征分解与条件数:\(\Sigma=\sum_k\ell_kv_kv_k^\top\) 把协方差拆成若干互相正交的"组合方向" \(v_k\) 及其方差 \(\ell_k\)。条件数 \(\kappa=\ell_1/\ell_n\) 衡量求逆时误差放大的最坏倍数。
  5. 范数 \(\|\cdot\|_1\)、\(\|\cdot\|_2\):\(\|w\|_1=\sum|w_i|\)(总杠杆或换手),\(\|w\|_2=\sqrt{\sum w_i^2}\)(向量的长度)。
  6. 凸函数与凸集:凸问题的局部最优就是全局最优,求解器可以放心使用;持仓数上限这类"非凸"约束则没有这个保证。

怎么读这一章。 必读 5.1、5.2(尤其 5.2.3 的实验结论)、5.4.3 的影子价格解读、5.5.2 的不交易区间、5.8 的结论表。5.3 是检查清单式的内容,读一遍留个印象即可。5.6 和 5.7 根据工作需要选读:做多资产配置的读 5.6 和 5.7,做股票增强的重点读 5.4 和 5.5。代码 5.2 的约束写法第一次可以只看讲解框,不必逐行对照。


5.1 问题与动机

先给结论:组合优化器的主要风险不是算错,而是算得太对。 它会精确地找到期望收益估计偏高、风险估计偏低的那些方向,并在这些方向上重仓。Michaud(1989)把均值–方差优化称为"误差最大化器",说的就是这件事。所以组合构建的工程重点有三项:控制误差放大(收缩、约束、正则化),把投资政策和交易成本写进问题,以及在求解之后做检查。

一个完整的组合构建问题有六类输入:

输入 来源 典型问题
期望收益或 alpha 预测 \(\alpha\) 本册第 03 章因子研究、第 10 章机器学习 噪声远大于信号,量纲常常与风险不一致
协方差 \(\Sigma\) 本册第 04 章风险模型 样本协方差维度灾难,小特征值偏低
当前持仓 \(w^0\) 交易系统 决定换手与成本
基准 \(w_b\) 投资合同 决定主动权重 \(a=w-w_b\)
约束 投资政策、风控、监管 可行性、非凸性
交易成本模型 本册第 07 章 时间尺度、非线性

本章的合成市场沿用全册的做法:收益由 1 个市场因子和若干行业因子驱动,市场因子服从 GARCH(1,1),所有冲击都是单位方差的 \(t(5)\) 分布,所以有厚尾和波动聚集。它与真实数据的差别要先说清楚:真实市场的因子暴露、相关结构和期望收益都会随时间漂移,有停牌、退市、涨跌停和流动性分层;而这里的真实期望收益和无条件协方差是固定的,并且我们知道它们的真值。正因为知道真值,模拟才能做真实数据做不到的事:在真实参数下给每个组合打分,把"方法好"和"运气好"分开。模拟得到的 Sharpe 比率不代表任何真实策略的收益。

下面的代码 5.0 是本章所有代码共用的数据生成器和两个小工具。后续每段代码都写明需要先运行哪段。

# 代码 5.0  本章公共函数:合成多资产日收益(因子结构 + GARCH 波动聚集 + t 厚尾)
import numpy as np
import pandas as pd

def make_market(T=2520, N=30, n_ind=5, seed=0):
    """返回 (收益 DataFrame, 行业标签, 真实期望收益 mu, 无条件真实协方差 Sigma)。
    市场因子:GARCH(1,1) + t(5) 冲击;行业因子与特异收益:t(5),特异波动按股票不同。"""
    rng = np.random.default_rng(seed)
    ind = np.repeat(np.arange(n_ind), N // n_ind)
    K = 1 + n_ind
    B = np.zeros((N, K))
    B[:, 0] = rng.uniform(0.6, 1.4, N)               # 市场 beta
    B[np.arange(N), 1 + ind] = 1.0                    # 行业哑变量
    tz = lambda size: rng.standard_t(5, size) / np.sqrt(5 / 3)   # 单位方差 t(5)
    # 市场因子:GARCH(1,1),年化波动约 17%
    omega, a, b = 1.2e-6, 0.08, 0.91
    h = omega / (1 - a - b)
    fm = np.empty(T)
    for t in range(T):
        fm[t] = np.sqrt(h) * tz(1)[0]
        h = omega + a * fm[t] ** 2 + b * h
    fi = 0.006 * tz((T, n_ind))                        # 行业因子
    idio_vol = rng.uniform(0.008, 0.02, N)
    eps = idio_vol * tz((T, N))
    mu = 0.0003 * B[:, 0] + rng.normal(0, 0.0001, N)  # 真实日期望收益(年化约 7.5%)
    R = mu + fm[:, None] * B[:, [0]].T + fi[:, ind] + eps
    F = np.diag(np.r_[omega / (1 - a - b), np.full(n_ind, 0.006 ** 2)])
    Sigma = B @ F @ B.T + np.diag(idio_vol ** 2)
    dates = pd.bdate_range("2015-01-01", periods=T)
    return pd.DataFrame(R, index=dates, columns=[f"S{i:02d}" for i in range(N)]), ind, mu, Sigma

def lw_cov(X):
    """Ledoit–Wolf 收缩估计(sklearn 实现,目标为缩放单位阵;原理见本册第 04 章)。"""
    from sklearn.covariance import LedoitWolf
    return LedoitWolf().fit(X).covariance_

def risk_contrib(w, S):
    """各资产风险贡献占比 RC_i = w_i (S w)_i / (w' S w)。"""
    m = S @ w
    return w * m / (w @ m)

5.2 均值–方差:从 KKT 条件到误差放大

5.2.1 公式回顾

带风险厌恶系数 \(\lambda\) 的均值–方差问题是

\[ \max_w\ \alpha^\top w-\lambda\,w^\top\Sigma w\quad\text{s.t.}\quad w\in\mathcal W , \]

\(\mathcal W\) 是可行集。没有约束时一阶条件给出 \(w^*=\frac{1}{2\lambda}\Sigma^{-1}\alpha\);只有预算约束 \(\mathbf 1^\top w=1\) 时得到切点组合与两基金分离;有上下限时,KKT 条件说的是"等边际原则":所有不在边界上的持仓,其边际风险调整收益 \(\alpha_i-2\lambda(\Sigma w)_i\) 都相等,等于预算约束的影子价格。推导、二阶条件和影子价格的含义见第 04 册第 12 章 12.10 节;有效集法、内点法等求解算法见第 04 册第 16a、16b 章。本章直接使用这些结论。

5.2.2 误差放大的来源

把 \(\Sigma\) 做特征分解 \(\Sigma=\sum_k \ell_k v_kv_k^\top\)(\(\ell_1\ge\cdots\ge\ell_n>0\)),无约束解可以写成

\[ w^*=\frac{1}{2\lambda}\sum_{k=1}^n\frac{v_k^\top\alpha}{\ell_k}\,v_k . \]

推导拆解:这个式子怎么来的?分三步。 第一步,无约束时对目标 \(\alpha^\top w-\lambda w^\top\Sigma w\) 求梯度并令其为零:\(\alpha-2\lambda\Sigma w=0\),所以 \(w^*=\frac1{2\lambda}\Sigma^{-1}\alpha\)。 第二步,协方差矩阵可以写成 \(\Sigma=\sum_k\ell_kv_kv_k^\top\),其中 \(v_k\) 是互相正交、长度为 1 的特征向量。它的逆有同样的特征向量,特征值取倒数:\(\Sigma^{-1}=\sum_k\frac1{\ell_k}v_kv_k^\top\)。直观上,把一个矩阵"求逆"就是在每个特征方向上做"除法"。 第三步,代入:\(\Sigma^{-1}\alpha=\sum_k\frac1{\ell_k}v_k(v_k^\top\alpha)\)。\(v_k^\top\alpha\) 是 alpha 在第 \(k\) 个方向上的投影(一个数),再除以该方向的方差 \(\ell_k\)。 数值例:两只股票方差都是 1、相关 0.9,特征值为 1.9(同涨同跌方向)和 0.1(一多一空方向)。若 alpha 估计为 \((1\%,\ 1.2\%)\),在"一多一空"方向上的投影是 \((1.2\%-1\%)/\sqrt2\approx0.14\%\),除以 0.1 后放大 10 倍;而这 0.2% 的差别很可能只是估计噪声。优化器据此建立大额的"多 B 空 A"头寸,这就是"误差最大化"。

这个式子说明了三件事。

第一,权重在第 \(k\) 个特征方向上的大小与 \(1/\ell_k\) 成正比。最小特征值方向通常是"两只高度相关的股票一多一空"这类组合,它们的风险被认为很小,于是任何一点点 \(\alpha\) 在这些方向上的投影都会被放大成巨大的多空头寸。

第二,样本协方差的特征值有系统偏差:大的偏大,小的偏小(Marchenko–Pastur 现象,见第 01 册第 04a 章的量化实战和本册第 04 章)。最小特征值被低估,正好落在放大倍数最大的位置上。

第三,扰动的相对放大有一个上界:若 \(\alpha\) 有误差 \(\delta\alpha\),则

\[ \frac{\|\delta w\|_2}{\|w\|_2}\le\kappa(\Sigma)\,\frac{\|\delta\alpha\|_2}{\|\alpha\|_2},\qquad \kappa(\Sigma)=\frac{\ell_1}{\ell_n}. \]

条件数 \(\kappa\) 就是最坏情况下的误差放大倍数。第 10 册第 09 章从优化收敛的角度讲过"条件数决定一切",这里是同一个量在统计意义上的后果。

期望收益的误差比协方差的误差更致命。日收益均值的标准误是 \(\sigma/\sqrt T\):一只年化波动 30% 的股票,用 5 年数据估计年化期望收益,标准误约 \(30\%/\sqrt5\approx13\%\),比任何合理的 alpha 都大。Chopra 和 Ziemba(1993)的数值实验发现,均值误差对最优组合效用的损害比方差和协方差误差大约高一个数量级。所以在实务中,期望收益几乎从不直接用历史均值,而是用信号模型的预测,并且要向截面均值或零大幅收缩(Grinold–Kahn 的"alpha = IC × 波动率 × 分数"就是一种收缩,IC 的估计见本册第 03 章 3.4 节,用法见 5.4 节代码)。

5.2.3 实验:估计误差能吃掉多少 Sharpe

代码 5.1 固定一组真实参数(60 只股票),反复生成 1 年长度的样本,每次用样本估计 \(\mu\) 和 \(\Sigma\) 后构造切点组合,然后在真实参数下评价它的 Sharpe 比率。

# 代码 5.1  条件数与"误差最大化":估计误差如何被均值–方差放大(需先运行代码 5.0)
N, W = 60, 252                                   # 60 只股票、1 年日数据估计
R, ind, mu, Sig = make_market(T=W, N=N, n_ind=6, seed=1)
X = R.values
S_smp, S_lw = np.cov(X, rowvar=False), lw_cov(X)
for name, S in [("真实", Sig), ("样本", S_smp), ("LW", S_lw)]:
    ev = np.linalg.eigvalsh(S) * 252
    print(f"{name:4s}: 最小特征值 {ev[0]:.4f}  最大特征值 {ev[-1]:.3f}  条件数 {ev[-1]/ev[0]:6.0f}")

def tangency(m, S):
    w = np.linalg.solve(S, m)                    # 解线性方程组,不显式求逆
    return w / w.sum()

def true_sr(w):                                   # 在真实参数下评价组合(年化)
    return w @ mu / np.sqrt(w @ Sig @ w) * np.sqrt(252)

# 200 次独立重复:真实参数固定,只换样本噪声;每次只用 1 年数据估计 mu 与 Sigma
L = np.linalg.cholesky(Sig)
keys = ["真实最优", "样本mu+样本S", "样本mu+LW", "收缩mu+LW", "最小方差LW", "等权1/N"]
res = {k: [] for k in keys}
under = {"样本": [], "LW": []}                    # 最小方差组合:真实波动 / 样本内预测波动
for rep in range(200):
    rr = np.random.default_rng(1000 + rep)
    Z = rr.standard_t(5, (W, N)) / np.sqrt(5 / 3)
    Xr = mu + Z @ L.T
    m_hat, S_hat, S_l = Xr.mean(0), np.cov(Xr, rowvar=False), lw_cov(Xr)
    m_shr = 0.2 * m_hat + 0.8 * m_hat.mean()      # 期望收益向截面均值收缩 80%
    for k, w in zip(keys, [tangency(mu, Sig), tangency(m_hat, S_hat), tangency(m_hat, S_l),
                           tangency(m_shr, S_l), tangency(np.ones(N), S_l), np.ones(N) / N]):
        res[k].append(true_sr(w))
    for k, S in [("样本", S_hat), ("LW", S_l)]:
        w = tangency(np.ones(N), S)
        under[k].append(np.sqrt(w @ Sig @ w / (w @ S @ w)))
out = pd.DataFrame({k: [np.median(v), np.percentile(v, 10), np.percentile(v, 90)] for k, v in res.items()},
                   index=["中位数", "10%分位", "90%分位"]).T
print("\n真实参数下的年化 Sharpe(200 次重复估计)")
print(out.round(2))
for k, v in under.items():
    print(f"最小方差组合({k}协方差):真实波动 / 预测波动 = {np.mean(v):.2f}")

输出:

真实  : 最小特征值 0.0171  最大特征值 2.138  条件数    125
样本  : 最小特征值 0.0096  最大特征值 1.385  条件数    145
LW  : 最小特征值 0.0149  最大特征值 1.289  条件数     86

真实参数下的年化 Sharpe(200 次重复估计)
           中位数  10%分位  90%分位
真实最优      0.94   0.94   0.94
样本mu+样本S  0.05  -0.13   0.23
样本mu+LW   0.07  -0.12   0.22
收缩mu+LW   0.15  -0.03   0.29
最小方差LW    0.21   0.15   0.29
等权1/N     0.44   0.44   0.44
最小方差组合(样本协方差):真实波动 / 预测波动 = 1.32
最小方差组合(LW协方差):真实波动 / 预测波动 = 1.24

白话解释:代码 5.1 的设计相当于"同一个考题考 200 次"。真实参数 mu、Sig 固定不变,每次只换一组随机样本(模拟"如果历史换一种走法"),用这 1 年样本估计参数、构造组合,再用真参数评价它的 Sharpe。这样,组合表现的差别只可能来自估计误差,而不是市场环境变化。六个组合分别是:上帝视角的最优(用真参数)、完全用样本、样本均值配收缩协方差、收缩均值配收缩协方差、不用均值的最小方差、等权。 m_shr = 0.2 * m_hat + 0.8 * m_hat.mean() 是最简单的均值收缩:只保留个股均值偏离截面平均的 20%。tangency(np.ones(N), S) 把 alpha 设为全 1,切点组合就退化成最小方差组合,这是一个代码上的小技巧。 最后的 under 计算"真实波动 / 优化器自报的波动",检验优化器有没有在风险上"自我感觉良好"。

读这组结果:

  • 真实最优组合的年化 Sharpe 是 0.94,等权组合是 0.44。用 1 年样本均值加样本协方差构造的切点组合,中位数只有 0.05,有相当比例是负的——不是因为"市场变了",真实参数一直没变,全部损失来自估计误差。
  • 把协方差换成 Ledoit–Wolf 收缩,几乎没有帮助(0.05 → 0.07);把期望收益向截面均值收缩 80%,中位数翻倍到 0.15。这印证了上一节的判断:在这个规模上,误差的主要来源是 \(\hat\mu\),不是 \(\hat\Sigma\)。
  • 完全不用期望收益的最小方差组合(0.21)和等权组合(0.44)都好于"最优"组合。等权在这里格外好,是因为这个合成世界里期望收益与市场 beta 成正比,等权恰好持有了 beta;最小方差组合则倾向低 beta 股票。换一个期望收益结构,排序可能不同,但"估计出来的切点组合不如简单规则"这一点在 DeMiguel、Garlappi 和 Uppal(2009)对真实数据的大规模比较中同样成立。
  • 最后两行说的是风险被低估:用样本协方差构造的最小方差组合,真实波动是样本内预测波动的 1.32 倍;LW 收缩把这个比值降到 1.24,但没有消除。优化器挑中的恰恰是"看起来风险低"的方向,所以样本内的风险预测对优化后的组合系统性偏乐观。上线前应当用独立样本或更长窗口重新估计组合风险,不要直接相信优化器报告的数字。

5.3 数值稳定性:别让线性代数出错

统计误差之外,还有纯粹的数值问题。它们不常见,一旦出现却很难查,因为程序不报错,只是给出一组离谱的权重。

不要显式求逆。 \(\Sigma^{-1}\alpha\) 一律用 np.linalg.solve 或 Cholesky 分解(scipy.linalg.cho_factor/cho_solve)计算。显式求逆多做一倍运算,误差也更大。Cholesky 分解失败本身就是一个有用的信号:矩阵不是正定的。

检查半正定性并修复。 样本协方差在 \(T<n\) 时奇异;成对删除缺失值估计出的相关矩阵、人工调过的相关矩阵可能有负特征值。修复方法(特征值截断、最近相关矩阵的牛顿法)见第 04 册第 06 章 6.6 节。半正定但奇异的 \(\Sigma\) 会让最优解不唯一(第 04 册 12.10.3 节的讨论),求解器给出的解会随初值漂移。

利用因子结构。 风险模型 \(\Sigma=BFB^\top+D\)(\(B\) 为 \(n\times K\) 暴露,\(D\) 为对角特异方差)可以用 Sherman–Morrison–Woodbury 公式(第 01 册第 00 章 0.7 节)求逆:

\[ \Sigma^{-1}=D^{-1}-D^{-1}B\big(F^{-1}+B^\top D^{-1}B\big)^{-1}B^\top D^{-1}, \]

只需要求一个 \(K\times K\) 矩阵的逆,计算量从 \(O(n^3)\) 降到 \(O(nK^2)\),而且因为 \(D\) 的对角元都为正,结果天然正定。第 04 册附录 A.3 节给出了这套计算的实现与病态分析。几千只股票的组合优化应当始终在因子形式下做,而不是先拼出 \(n\times n\) 的 \(\Sigma\)。

统一量纲。 年化的 \(\alpha\) 配日度的 \(\Sigma\),风险厌恶系数 \(\lambda\) 的含义会差 252 倍。约定一种单位(本章用年化)并写进函数文档。求解器的停止准则 ftol 是绝对量,目标函数在 \(10^{-4}\) 量级时,默认的 1e-6 可能让求解器在第一步就宣布收敛。必要时把目标乘以常数,使它在 1 的量级。

求解后检查。 至少检查四件事:约束是否满足(逐条打印残差);KKT 条件是否成立(自由变量的边际值是否相等,见第 04 册 12.10.3 节的表);权重是否落在可解释的范围;换一个初值或换一个求解器,结果是否一致。

按块降低条件数。 第 01 册第 04a 章证明了主子矩阵的条件数不超过全矩阵的条件数。把大问题拆成行业内的小问题,或者像 HRP(5.6.2 节)那样沿层次结构逐层分配,是从结构上降低条件数的办法。


5.4 约束:把投资政策写进可行集

5.4.1 常用约束的凸形式

约束 数学形式 凸性 备注
预算 \(\mathbf 1^\top w=1\)(多空组合为 \(0\)) 线性
个股上下限 \(l\le w\le u\) 或 \(\lvert w-w_b\rvert\le u\) 线性 只做多就是 \(l=0\)
行业中性 \(\lvert H^\top(w-w_b)\rvert\le\delta\) 线性 \(H\) 为 \(n\times G\) 行业哑变量
风格/beta 暴露 \(\lvert X^\top(w-w_b)\rvert\le\delta_X\) 线性 \(X\) 来自风险模型
总杠杆 \(\lVert w\rVert_1\le L\) 凸 拆成 \(w=p-q\),\(p,q\ge0\),\(\mathbf 1^\top(p+q)\le L\)
换手 \(\lVert w-w^0\rVert_1\le\tau\) 凸 拆成 \(w=w^0+b-s\),\(b,s\ge0\)
跟踪误差 \((w-w_b)^\top\Sigma(w-w_b)\le\sigma_{TE}^2\) 凸(二次锥)
持仓数 \(\le K\)、最小交易单位 含整数变量 非凸 需要混合整数求解器或启发式

\(\ell_1\) 约束的拆分技巧在第 04 册 12.1.2 节和第 16b 章 16.11.1 节讲过:只要目标里对 \(b+s\) 有正的成本,最优解就不会同时买又卖同一只股票,于是 \(\mathbf 1^\top(b+s)\) 精确等于换手。非凸约束的处理和伪 KKT 点的问题见第 04 册 12.10.4 节。

约束还有一个容易被忽略的统计作用。Jagannathan 和 Ma(2003)证明,在最小方差问题里加上"不许做空",等价于把协方差矩阵的某些元素向下修正,作用类似收缩估计。所以约束不仅是投资政策,也是一种正则化:它限制了优化器在误差方向上加杠杆的能力。反过来,约束太紧时,最终组合主要由约束决定而不是由信号决定,这时应当检查信号到底还剩多少影响。

5.4.2 可行性先于最优性

从当前持仓出发,有些约束要求必须先做一定量的交易(比如某行业已经超配 3%,而上限是 1%)。如果换手预算小于这个最低必需量,问题不可行,求解器可能返回一个违反约束的点并报告失败,也可能在宽松的容差下"成功"。稳妥的做法是先解一个线性规划:在所有其他约束下最小化 \(\mathbf 1^\top(b+s)\),得到最小可行换手(第 04 册第 16b 章 16.11 节的代码做了这一步),然后再设换手预算。

5.4.3 代码:带约束的主动组合

代码 5.2 构造一个 30 只股票、等权基准的增强型组合:alpha 用 Grinold 公式 \(\alpha_i=\text{IC}\cdot\sigma_i\cdot z_i\) 生成(IC 取 0.05,\(z_i\) 为标准化分数),约束包括只做多、主动权重上限 3%、行业偏离 ±1%、beta 偏离 ±0.05、换手预算和跟踪误差上限 2%。目标里有 15bp 的线性成本。求解器是 SLSQP,变量是买入量 \(b\) 和卖出量 \(s\),并提供解析梯度和约束 Jacobian——这对 SLSQP 的速度和精度都很重要。

# 代码 5.2  带约束的主动组合优化:SLSQP + 买卖拆分 + 约束影子价格(需先运行代码 5.0)
from scipy.optimize import minimize

R, ind, mu, Sig = make_market(T=756, N=30, n_ind=5, seed=3)
N = R.shape[1]
S = lw_cov(R.values[-504:]) * 252                       # 年化协方差(2 年窗口 LW)
vol = np.sqrt(np.diag(S))
H = np.eye(5)[ind]                                      # N x 5 行业哑变量
beta = (S @ np.ones(N) / N) / (np.ones(N) @ S @ np.ones(N) / N**2)   # 相对等权基准的 beta
rng = np.random.default_rng(11)
score = rng.standard_normal(N)
alpha = 0.05 * vol * score                              # Grinold 公式: alpha = IC * vol * score, IC=0.05
wb = np.ones(N) / N                                     # 基准:等权
w0 = np.clip(wb + rng.normal(0, 0.01, N), 0, None); w0 /= w0.sum()   # 当前持仓

lam, c, u_act, d_ind, d_beta, te_max = 2.0, 0.0015, 0.03, 0.01, 0.05, 0.02

def solve(tau=0.30, te=te_max, cost=c):
    """变量 x = (b, s),w = w0 + b - s;目标 = -(alpha'w - lam*a'Sa - cost*1'(b+s))。"""
    def unpack(x): return w0 + x[:N] - x[N:]
    def f(x):
        w = unpack(x); a = w - wb
        return -(alpha @ w - lam * a @ S @ a - cost * x.sum())
    def g(x):
        w = unpack(x); a = w - wb
        gw = alpha - 2 * lam * S @ a
        return -np.r_[gw - cost, -gw - cost]
    J = np.c_[np.eye(N), -np.eye(N)]                    # dw/dx
    cons = [
        {"type": "eq",   "fun": lambda x: x[:N].sum() - x[N:].sum(), "jac": lambda x: np.r_[np.ones(N), -np.ones(N)][None]},
        {"type": "ineq", "fun": lambda x: unpack(x), "jac": lambda x: J},                         # w >= 0
        {"type": "ineq", "fun": lambda x: wb + u_act - unpack(x), "jac": lambda x: -J},          # 主动权重上限
        {"type": "ineq", "fun": lambda x: d_ind - H.T @ (unpack(x) - wb), "jac": lambda x: -H.T @ J},
        {"type": "ineq", "fun": lambda x: d_ind + H.T @ (unpack(x) - wb), "jac": lambda x: H.T @ J},
        {"type": "ineq", "fun": lambda x: np.r_[d_beta - beta @ (unpack(x) - wb), d_beta + beta @ (unpack(x) - wb)],
         "jac": lambda x: np.vstack([-beta @ J, beta @ J])},
        {"type": "ineq", "fun": lambda x: tau - x.sum(), "jac": lambda x: -np.ones(2 * N)[None]},  # 换手预算
        {"type": "ineq", "fun": lambda x: te**2 - (unpack(x) - wb) @ S @ (unpack(x) - wb),       # 跟踪误差上限
         "jac": lambda x: -(2 * S @ (unpack(x) - wb)) @ J},
    ]
    r = minimize(f, np.zeros(2 * N), jac=g, constraints=cons, bounds=[(0, None)] * (2 * N),
                 method="SLSQP", options={"maxiter": 500, "ftol": 1e-12})
    w = unpack(r.x); a = w - wb
    return r, w, dict(obj=-r.fun, alpha=alpha @ a, te=np.sqrt(a @ S @ a), turnover=r.x.sum(),
                      both=int(np.sum((r.x[:N] > 1e-6) & (r.x[N:] > 1e-6))))

r, w, info = solve()
print(f"收敛: {r.success}, 迭代 {r.nit}")
print({k: round(float(v), 4) for k, v in info.items()})
print("行业主动暴露:", np.round(H.T @ (w - wb), 4), " beta 主动暴露:", round(beta @ (w - wb), 4))
print("顶到主动上限的股票数:", int(np.sum(w > wb + u_act - 1e-6)), " 权重为 0 的股票数:", int(np.sum(w < 1e-6)))

# 影子价格:把约束放松一点,看目标改善多少(有限差分)
cases = [("换手预算 0.30→0.31", dict(tau=0.30), dict(tau=0.31)),
         ("TE 上限 2.0%→2.1%(换手不设限)", dict(tau=2.0), dict(tau=2.0, te=te_max + 0.001))]
for name, kw0, kw1 in cases:
    o0, o1 = solve(**kw0)[2]["obj"], solve(**kw1)[2]["obj"]
    print(f"{name}: 目标改善 {1e4 * (o1 - o0):.2f} bp")

# 换手预算扫描:成本惩罚与换手约束之间的取舍
print("\n换手预算  主动alpha   TE     实际换手")
for tau in [0.05, 0.10, 0.20, 0.40, 0.80]:
    _, _, inf2 = solve(tau=tau)
    print(f"{tau:6.2f}   {inf2['alpha']:.4f}   {inf2['te']:.4f}   {inf2['turnover']:.3f}")

输出:

收敛: True, 迭代 16
{'obj': 0.0012, 'alpha': 0.0045, 'te': 0.0199, 'turnover': 0.3, 'both': 0.0}
行业主动暴露: [ 0.01  0.01 -0.   -0.01 -0.01]  beta 主动暴露: -0.0103
顶到主动上限的股票数: 2  权重为 0 的股票数: 2
换手预算 0.30→0.31: 目标改善 0.71 bp
TE 上限 2.0%→2.1%(换手不设限): 目标改善 1.34 bp

换手预算  主动alpha   TE     实际换手
  0.05   0.0006   0.0097   0.050
  0.10   0.0018   0.0142   0.100
  0.20   0.0034   0.0182   0.200
  0.40   0.0052   0.0200   0.400
  0.80   0.0056   0.0200   0.530

几点解读:

  • both = 0:没有任何股票同时被买和卖,验证了买卖拆分是精确的。
  • 行业暴露有 4 个顶在 ±1% 的边界上,跟踪误差 1.99% 几乎顶到 2%,换手恰好用满 30%。多个约束同时起作用是常态。
  • 影子价格用有限差分读出:换手预算从 30% 放宽到 31%,目标改善 0.71bp;在换手不设限时,跟踪误差上限放宽 0.1 个百分点,目标改善 1.34bp。目标的单位是"年化 alpha 减风险惩罚减成本",所以这些数字可以直接和风控、合规讨论"这条约束值多少钱"。比有限差分更精确的做法是读求解器返回的乘子(第 04 册第 16b 章 16.11.2 节用 trust-constr 做了这一步),但有限差分不依赖求解器,适合做交叉检查。
  • 换手预算扫描显示收益递减:从 5% 放到 20%,主动 alpha 从 0.06% 升到 0.34%;放到 40% 以后跟踪误差约束开始起作用,再放宽几乎没有收益,实际换手停在 53%。

白话解释:代码 5.2 的结构可以按"投资合同条款"来读。变量是 60 个数:30 只股票的买入量和 30 只的卖出量。f 是要最小化的目标(负的"alpha − 风险惩罚 − 成本"),g 是它的梯度,手工给出可以让求解器更快、更准。cons 列表里每一项对应合同里的一条:买卖总量相等(资金守恒,不加杠杆)、不许做空、个股主动权重不超过 3%、每个行业偏离不超过 ±1%、β 偏离不超过 ±0.05、换手不超过 \(\tau\)、跟踪误差不超过 2%。每条约束都附带 jac(约束对变量的导数),同样是为了让求解器更快。 影子价格的读法:约束放宽一点点,目标改善多少。"换手预算 30%→31%,目标改善 0.71bp",可以直接翻译成"换手上限每放宽 1 个百分点,组合每年多 0.71bp 的风险调整后收益"。如果风控说"换手上限必须保持 30%",你就知道这条限额的机会成本。若某条约束的影子价格是 0,说明它当前不起作用,收紧它也不花钱。

金融直觉:换手预算扫描那张表是一条典型的"边际收益递减"曲线:前 20% 的换手带来了大部分 alpha,之后跟踪误差上限开始起作用,再多换手也换不来更多 alpha。这和你在资本预算里看到的"项目按 IRR 排序、边际项目回报递减"是同一种图形。

关于求解器:SLSQP 对几十到几百个变量的稠密问题够用。股票池上千、约束上百时,应当用专门的凸优化求解器(OSQP、Clarabel、ECOS、MOSEK,通常通过 CVXPY 建模),它们的原理就是第 04 册第 16b 章的内点法和算子分裂法。本册的运行环境没有安装这些包,所以代码只用 scipy。


5.5 交易成本进入目标函数

5.5.1 成本项的形式

本册第 07 章会讲到,一笔交易的成本大致由三部分组成:佣金和印花税(与成交额成正比)、半个买卖价差(与成交额成正比)、市场冲击(单位成本随交易量的平方根增长,总成本因此与交易量的 1.5 次方成正比)。写进优化目标就是

\[ \max_w\ \alpha^\top w-\lambda(w-w_b)^\top\Sigma(w-w_b)-\sum_i c_i\lvert w_i-w_i^0\rvert-\sum_i \eta_i\lvert w_i-w_i^0\rvert^{3/2}. \]

\(3/2\) 次项仍然是凸的,但不是二次的,标准 QP 求解器处理不了,常见替代是用二次项 \(\eta_i(w_i-w_i^0)^2\) 近似,或用二阶锥约束精确表示。

时间尺度必须对齐。 这是成本惩罚最常见的错误。\(\alpha\) 通常是年化的,成本却是一次性的。如果信号的预测期是 20 个交易日,年化 alpha 10% 的股票在持有期内只能赚到约 \(10\%\times20/252\approx0.8\%\);用年化 alpha 去和一次性 30bp 的成本比较,会高估交易的价值约 12 倍,组合换手会远高于合理水平。两种对齐方式:把 alpha 换算到持有期,或者把成本按预期持有期摊销成年化成本(成本 × 年换手次数)。

5.5.2 线性成本产生不交易区间

只看一个资产、一期,最大化 \(\alpha w-\lambda\sigma^2w^2-c\lvert w-w^0\rvert\)。目标是凹的,用次梯度写最优性条件:记无成本的目标 \(w^\dagger=\alpha/(2\lambda\sigma^2)\),则

\[ w^*=\begin{cases}w^\dagger-\dfrac{c}{2\lambda\sigma^2}, & w^0<w^\dagger-\dfrac{c}{2\lambda\sigma^2}\\[2mm] w^0, & \lvert w^0-w^\dagger\rvert\le\dfrac{c}{2\lambda\sigma^2}\\[2mm] w^\dagger+\dfrac{c}{2\lambda\sigma^2}, & w^0>w^\dagger+\dfrac{c}{2\lambda\sigma^2}\end{cases} \]

即目标周围有一个半宽 \(c/(2\lambda\sigma^2)\) 的区间:当前持仓落在区间内就不动,落在区间外就只交易到区间边界,而不是交易到目标本身。多资产时这个区间变成一个多面体,但性质相同:线性成本让很多股票的最优交易量恰好为零,这正是 \(\ell_1\) 惩罚产生稀疏解的同一个机制。

推导拆解:分三种情况讨论("次梯度"只是给尖角处的导数一个区间,这里用分情况代替)。 情况 1,买入(\(w>w^0\)):成本项为 \(c(w-w^0)\),对 \(w\) 求导为 \(c\)。一阶条件 \(\alpha-2\lambda\sigma^2w-c=0\),得 \(w=\frac{\alpha-c}{2\lambda\sigma^2}=w^\dagger-\frac{c}{2\lambda\sigma^2}\)。这个解要成立,必须满足 \(w>w^0\),即 \(w^0<w^\dagger-\frac{c}{2\lambda\sigma^2}\)。 情况 2,卖出(\(w<w^0\)):成本导数为 \(-c\),同理得 \(w=w^\dagger+\frac{c}{2\lambda\sigma^2}\),要求 \(w^0>w^\dagger+\frac{c}{2\lambda\sigma^2}\)。 情况 3,两者都不满足:说明往任何方向交易,第一单位的边际收益都抵不过边际成本 \(c\),最优是不动,\(w=w^0\)。 白话:买入时,每多买一单位,边际收益 \(\alpha-2\lambda\sigma^2w\) 随持仓增加而下降;降到恰好等于成本 \(c\) 时就停手,此时还没到无成本目标 \(w^\dagger\)。数值例:\(w^\dagger=10\%\),半宽 2%,当前持仓 5%,则只买到 8%;当前持仓 9%,在 \([8\%,12\%]\) 内,不动。

5.5.3 二次成本产生部分调整

如果冲击成本是二次的 \(\eta(\Delta w)^2\),最优策略不再有不交易区间,而是每期向目标走一部分。Gârleanu 和 Pedersen(2013)在多期框架下给出了解析解:最优持仓是当前持仓与一个"瞄准组合"的加权平均,瞄准组合是当前和未来各期无成本目标的加权平均,衰减慢的信号权重更高。直觉是:持续时间长的信号值得慢慢建仓,转瞬即逝的信号不值得为它付冲击成本。

推导拆解:单期版本可以手算。最大化 \(\alpha w-\lambda\sigma^2w^2-\eta(w-w^0)^2\),对 \(w\) 求导:\(\alpha-2\lambda\sigma^2w-2\eta(w-w^0)=0\)。整理得 \(w^*=\dfrac{\lambda\sigma^2}{\lambda\sigma^2+\eta}\,w^\dagger+\dfrac{\eta}{\lambda\sigma^2+\eta}\,w^0\), 即"无成本目标"与"当前持仓"的加权平均,或等价地写成 \(w^*=w^0+\kappa(w^\dagger-w^0)\),\(\kappa=\frac{\lambda\sigma^2}{\lambda\sigma^2+\eta}\)。冲击系数 \(\eta\) 越大,\(\kappa\) 越小,每次走得越少。和线性成本不同,二次成本在交易量很小时几乎为零(\(0.01^2\) 很小),所以总值得动一点,不会出现"完全不动"的区间。 金融直觉:大资金建仓时交易员常说"分批建仓",就是这个道理——冲击成本随单次交易量加速上升,拆成多次比一次做完便宜。

5.5.4 实验:调仓规则对净收益的影响

代码 5.3 用 100 个独立资产模拟一个信号驱动的策略。信号服从 AR(1),半衰期约 13.5 天;成本同时有线性部分(5bp)和二次部分。比较三类调仓规则:每天调到目标、不交易区间、部分调整。

# 代码 5.3  交易成本惩罚:线性成本 -> 不交易区间;二次成本 -> 部分调整(需先运行代码 5.0)
rng = np.random.default_rng(5)
T, N = 2520, 100                                   # 100 个独立资产,10 年
phi, sig = 0.95, 0.02                              # 信号 AR(1) 系数(半衰期约 13.5 天),日波动 2%
s = np.zeros((T, N))
for t in range(1, T):
    s[t] = phi * s[t - 1] + np.sqrt(1 - phi**2) * rng.standard_normal(N)
mu_t = 0.0003 * s                                  # 条件期望收益:信号 1 个标准差对应 3bp/天
ret = np.r_[mu_t[:-1]] + sig * rng.standard_t(5, (T - 1, N)) / np.sqrt(5 / 3)   # t 日信号 -> t+1 日收益
gamma = 1.0
w_star = mu_t[:-1] / (gamma * sig**2)              # 无成本时的目标仓位(单资产 Merton 比例)
c_lin, eta = 0.0005, 0.0005                        # 线性成本 5bp/单位换手;二次冲击系数

def run(policy, p):
    w = np.zeros(N); pnl = np.zeros(T - 1); cost = np.zeros(T - 1); to = 0.0
    for t in range(T - 1):
        tgt = w_star[t]
        if policy == "full":
            w_new = tgt
        elif policy == "band":                     # 只在偏离目标超过 p 时交易到区间边界
            w_new = np.clip(w, tgt - p, tgt + p)
        else:                                      # 部分调整:每天向目标走 p 的比例
            w_new = w + p * (tgt - w)
        dw = w_new - w
        cost[t] = (c_lin * np.abs(dw) + eta * dw**2).sum()
        to += np.abs(dw).sum()
        w = w_new
        pnl[t] = w @ ret[t]
    pnl, cost = pnl / N, cost / N                 # 每个资产分得 1/N 资本
    net = pnl - cost
    ann = lambda x: x.mean() * 252
    return dict(毛收益=ann(pnl), 成本=ann(cost), 净收益=ann(net),
                净Sharpe=net.mean() / net.std() * np.sqrt(252), 日均换手=to / (T - 1) / N)

# 单资产解析的不交易半宽度:c_lin / (gamma*sig^2)(只考虑一期,忽略信号持续性)
print(f"一期模型的不交易半宽度 = {c_lin / (gamma * sig**2):.3f}(目标仓位标准差约 {w_star.std():.3f})")
rows = {"每日调到目标": run("full", None)}
for b in [0.1, 0.25, 0.5, 1.0, 1.25]:
    rows[f"不交易区间 ±{b:.2f}"] = run("band", b)
for k in [0.5, 0.2, 0.1, 0.05]:
    rows[f"部分调整 κ={k:.2f}"] = run("partial", k)
print(pd.DataFrame(rows).T.round(3))

输出:

一期模型的不交易半宽度 = 1.250(目标仓位标准差约 0.751)
               毛收益     成本    净收益  净Sharpe   日均换手
每日调到目标       0.064  0.031  0.033    1.403  0.190
不交易区间 ±0.10  0.063  0.019  0.044    1.932  0.116
不交易区间 ±0.25  0.062  0.011  0.051    2.284  0.068
不交易区间 ±0.50  0.053  0.006  0.047    2.273  0.035
不交易区间 ±1.00  0.031  0.002  0.029    1.768  0.012
不交易区间 ±1.25  0.024  0.001  0.023    1.590  0.006
部分调整 κ=0.50  0.062  0.016  0.047    2.058  0.107
部分调整 κ=0.20  0.057  0.008  0.049    2.346  0.058
部分调整 κ=0.10  0.049  0.005  0.044    2.317  0.036
部分调整 κ=0.05  0.038  0.003  0.035    2.141  0.022

白话解释:代码 5.3 是一个"同一信号、三种交易纪律"的对照实验。100 个资产各有一个缓慢变化的信号(AR(1),\(\phi=0.95\)),信号决定次日期望收益。无成本时的理想仓位 w_star 与信号成正比(期望收益 ÷ 风险厌恶 × 方差,即单资产的 Merton 比例)。run 函数逐日循环,三种规则只在"今天调到哪里"上不同:full 每天直接调到目标;band 用 np.clip(w, tgt-p, tgt+p) 实现"只有超出区间才交易,并且只交易到区间边界";partial 每天走差距的 \(p\) 比例。成本按 5bp 线性 + 二次冲击扣除。 为什么必须写循环而不能向量化?因为今天的持仓取决于昨天的持仓(路径依赖),这正是第 01 章说的"路径依赖的部分老老实实写循环"。

几点观察:

  • 每天调到目标的毛收益最高(6.4%),但成本吃掉一半,净 Sharpe 只有 1.40。不交易区间 ±0.25 或部分调整 \(\kappa=0.2\) 把成本压到原来的四分之一到三分之一,毛收益只损失很少,净 Sharpe 升到 2.3 左右。
  • 一期模型算出的不交易半宽度是 1.25,比最优区间(0.25 到 0.5)宽得多。原因是一期模型假设交易的好处只持续一期,而这里的信号会持续十几天,一次调仓的好处会延续多期,值得付更多成本。这又是一个时间尺度问题:一期模型会高估不交易区间,信号越持久,高估越多。
  • 两类规则的最好结果相近。实务中常把两者结合:先用优化器(目标中含成本项)求出目标,再在执行层对小额调整设最小交易门槛。
  • 这里的 Sharpe 偏高,是因为 100 个资产完全独立、信号的 IC 恒定。真实信号的 IC 会随时间变化,资产之间也有共同因子,同样的设置在真实数据上得到的 Sharpe 会低得多。

5.6 最小方差与风险平价

5.6.1 不依赖期望收益的组合

既然期望收益最难估,一类方法干脆不用它。

最小方差组合只用 \(\Sigma\):\(\min w^\top\Sigma w\),s.t. \(\mathbf 1^\top w=1\)(加上只做多和上限)。无约束解 \(\Sigma^{-1}\mathbf 1/(\mathbf 1^\top\Sigma^{-1}\mathbf 1)\) 的性质见第 04 册 12.10.1 节。它的问题是集中:只做多时,往往只有十来只低波动、低相关的股票拿到正权重。

风险贡献。组合波动 \(\sigma_p=\sqrt{w^\top\Sigma w}\) 是 \(w\) 的一次齐次函数,由 Euler 定理

\[ \sigma_p=\sum_i w_i\frac{\partial\sigma_p}{\partial w_i}=\sum_i\frac{w_i(\Sigma w)_i}{\sigma_p},\qquad RC_i=\frac{w_i(\Sigma w)_i}{w^\top\Sigma w},\ \ \sum_iRC_i=1 . \]

\(RC_i\) 是资产 \(i\) 贡献的风险占比。它也是本册第 11 章风险归因的基础。

推导拆解:Euler 定理一步步看。"一次齐次"指所有权重同乘 \(k\),组合波动也乘 \(k\)(仓位翻倍,波动翻倍)。对这类函数有 \(f(w)=\sum_iw_i\,\partial f/\partial w_i\)。 求偏导:\(\sigma_p=(w^\top\Sigma w)^{1/2}\),用链式法则,\(\partial\sigma_p/\partial w_i=\frac12(w^\top\Sigma w)^{-1/2}\cdot2(\Sigma w)_i=(\Sigma w)_i/\sigma_p\)。这一项叫边际风险贡献:资产 \(i\) 加一点仓位,组合波动增加多少。见 第 00 册第 02 章 导数与泰勒展开 的链式法则。 乘上权重 \(w_i\) 再求和就回到 \(\sigma_p\),所以 \(w_i(\Sigma w)_i/\sigma_p\) 是资产 \(i\) 的"风险贡献",各项加总恰好等于总风险,除以 \(\sigma_p\) 后变成占比、加总为 1。 数值例:两资产,权重各 50%,波动 20% 和 10%,相关 0。\(\Sigma w=(0.02,\ 0.005)\),\(w^\top\Sigma w=0.0125\)。\(RC_1=0.5\times0.02/0.0125=80\%\),\(RC_2=20\%\)。资金五五开,风险却是八二开。风险平价要求风险五五开,就得给低波动资产更多资金。

金融直觉:这就是风控部门做"风险预算"的数学:先决定每个资产(或每个策略、每个交易员)可以贡献多少比例的风险,再反推资金分配。传统 60/40 股债组合,资金六四开,风险却有九成来自股票,这正是风险平价批评传统配置的出发点。

风险平价(ERC) 要求所有 \(RC_i\) 相等(Maillard、Roncalli 和 Teiletche,2010);更一般的风险预算要求 \(RC_i=b_i\)。直接解 \(RC_i=b_i\) 是非凸的方程组,但 Spinu 给出了一个等价的凸问题

\[ \min_{y>0}\ \tfrac12y^\top\Sigma y-\sum_i b_i\ln y_i , \]

一阶条件 \(y_i(\Sigma y)_i=b_i\) 正是风险预算条件,归一化 \(w=y/\mathbf 1^\top y\) 即得解。第 04 册第 06 章 6.6 节用 Hessian-free 牛顿–CG 在 2000 只股票上解过这个问题,这里只用 L-BFGS-B。

ERC 的波动介于最小方差和等权之间。在多资产配置中(股票、债券、商品),ERC 会给低波动的债券很高的资金权重,为了达到目标收益通常需要加杠杆,这时融资成本和杠杆约束就进入了问题。

5.6.2 层次风险平价(HRP)

López de Prado(2016)提出的 HRP 绕开了求逆,分三步:

  1. 聚类:用相关距离 \(d_{ij}=\sqrt{(1-\rho_{ij})/2}\) 做层次聚类(原文用单链接)。
  2. 准对角化:按聚类树的叶序重排资产,使相关性高的资产相邻,协方差矩阵近似分块对角。
  3. 递归二分:把排好序的资产对半切开,两半各自用逆方差权重算出子组合方差 \(V_L,V_R\),按 \(1-V_L/(V_L+V_R)\) 和 \(V_L/(V_L+V_R)\) 分配权重;对每一半递归,直到单个资产。

白话解释:HRP 像一个"层层分预算"的组织架构。第一步按相关性把股票分组成一棵家族树(相关距离 \(d_{ij}=\sqrt{(1-\rho_{ij})/2}\):相关 1 时距离 0,相关 −1 时距离 1)。第二步按树的顺序排队,让"亲戚"站在一起。第三步从总预算开始,每次把队伍切成两半,按两半的风险高低分钱:风险小的那半多拿;然后每一半再往下切、再分,直到每只股票。 数值例:左半边子组合方差 \(V_L=0.04\),右半边 \(V_R=0.01\),则左半拿 \(1-0.04/0.05=20\%\),右半拿 \(80\%\),相当于按方差的倒数分配。 设计动机:整个过程只用到每个小组内部的方差,从不对完整的 \(\Sigma\) 求逆,所以不受 5.2.2 节"小特征值被放大"问题的影响。这和大公司的预算流程相似:总部不直接给每个员工定预算,而是先分给事业部,事业部再往下分,每一层只需要比较少数几个单位。

HRP 只在每个子块内用到逆方差(对角阵求逆),从不对整个 \(\Sigma\) 求逆,所以对病态的协方差矩阵不敏感,这正是 5.3 节"按块降低条件数"的思路。它的弱点是聚类树本身不稳定:相关结构稍有变化,叶序可能大幅调整,递归二分的切点随之改变。

5.6.3 代码:四种组合的风险结构与稳定性

# 代码 5.4  最小方差、风险平价(ERC)与 HRP(需先运行代码 5.0)
from scipy.optimize import minimize
from scipy.cluster.hierarchy import linkage, leaves_list
from scipy.spatial.distance import squareform

def min_var_long(S):
    n = len(S)
    r = minimize(lambda w: w @ S @ w, np.ones(n) / n, jac=lambda w: 2 * S @ w, method="SLSQP",
                 bounds=[(0, 1)] * n, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}],
                 options={"ftol": 1e-14, "maxiter": 500})
    return r.x

def erc(S, b=None):
    """Spinu 凸表述:min 0.5 y'Sy - sum b_i ln y_i,解满足 y_i (Sy)_i = b_i,再归一化。"""
    n = len(S); b = np.ones(n) / n if b is None else b
    f = lambda y: 0.5 * y @ S @ y - b @ np.log(y)
    g = lambda y: S @ y - b / y
    y0 = 1 / np.sqrt(np.diag(S)); y0 /= np.sqrt(y0 @ S @ y0)
    r = minimize(f, y0, jac=g, method="L-BFGS-B", bounds=[(1e-10, None)] * n, options={"gtol": 1e-12})
    return r.x / r.x.sum()

def hrp(S):
    """López de Prado (2016) 层次风险平价:相关距离聚类 -> 准对角化 -> 递归二分。"""
    sd = np.sqrt(np.diag(S)); C = S / np.outer(sd, sd)
    D = np.sqrt(np.clip(0.5 * (1 - C), 0, None)); np.fill_diagonal(D, 0)
    order = leaves_list(linkage(squareform(D, checks=False), method="single"))
    w = np.ones(len(S))
    clusters = [order]
    def cvar(idx):                                 # 子簇内用逆方差权重算簇方差
        iv = 1 / np.diag(S)[idx]; iv /= iv.sum()
        return iv @ S[np.ix_(idx, idx)] @ iv
    while clusters:
        nxt = []
        for c in clusters:
            if len(c) <= 1: continue
            L, Rr = c[: len(c) // 2], c[len(c) // 2:]
            vL, vR = cvar(L), cvar(Rr)
            a = 1 - vL / (vL + vR)                 # 方差小的一半得到更多权重
            w[L] *= a; w[Rr] *= 1 - a
            nxt += [L, Rr]
        clusters = nxt
    return w / w.sum()

R, ind, mu, Sig = make_market(T=2520, N=30, n_ind=5, seed=4)
X = R.values
S = lw_cov(X[-504:])
methods = {"等权": lambda S: np.ones(len(S)) / len(S), "最小方差(多头)": min_var_long, "ERC": erc, "HRP": hrp}
rows = {}
for k, fn in methods.items():
    w = fn(S); rc = risk_contrib(w, S)
    # 稳定性:估计窗口后移 21 天,权重变化多少
    w2 = fn(lw_cov(X[-504 - 21:-21]))
    rows[k] = dict(年化波动=np.sqrt(w @ S @ w * 252), 最大权重=w.max(), 有效持仓数=1 / (w**2).sum(),
                  风险贡献最大=rc.max(), 风险贡献最小=rc.min(), 窗口后移换手=np.abs(w - w2).sum())
print(pd.DataFrame(rows).T.round(3))
w_erc = erc(S)
print("ERC 验证:风险贡献占比的极差 =", f"{np.ptp(risk_contrib(w_erc, S)):.2e}")
print("HRP 各行业权重:", np.round(np.bincount(ind, hrp(S)), 3))

输出:

           年化波动   最大权重   有效持仓数  风险贡献最大  风险贡献最小  窗口后移换手
等权        0.138  0.033  30.000   0.045   0.020   0.000
最小方差(多头)  0.119  0.233   9.133   0.233   0.000   0.115
ERC       0.134  0.051  29.122   0.033   0.033   0.020
HRP       0.132  0.064  27.384   0.044   0.023   0.129
ERC 验证:风险贡献占比的极差 = 1.46e-06
HRP 各行业权重: [0.221 0.203 0.158 0.2   0.218]

读表:

  • 最小方差组合的波动最低(11.9%),但最大权重 23%、有效持仓数只有 9 只,有一只股票的风险贡献为 0(权重为 0)。
  • ERC 的风险贡献极差只有 \(10^{-6}\) 量级,所有股票的风险贡献都是 1/30 ≈ 0.033,验证了 Spinu 表述的正确性。它的权重接近等权(有效持仓数 29),因为这个合成市场里各股票的波动差别不大。
  • 最后一列是本节最值得注意的数字:把估计窗口后移 21 天重新计算,ERC 的权重只变了 2%,最小方差组合变了 11.5%,HRP 变了 12.9%,比最小方差还不稳定。原因就是上面说的聚类树不稳定:单链接聚类在行业内部相关性接近时,叶序会因为很小的估计差异而重排。这个结果不能推广为"HRP 一定不稳定",但它提醒我们:任何构建方法都要测一下对输入扰动的敏感度。常用的改进是换用 Ward 或平均链接、对多个 bootstrap 样本的 HRP 权重取平均,或者在行业等先验结构上固定第一层切分。

5.7 Black–Litterman:从均衡出发,只在有观点处偏离

5.7.1 模型

Black 和 Litterman(1992)的出发点是:既然直接估计期望收益不可靠,就从一个"不会离谱"的先验出发。先验均值取市场均衡隐含收益 \(\pi=\delta\Sigma w_{\text{mkt}}\)(反向优化:如果所有人都用风险厌恶 \(\delta\) 做均值–方差优化,市场组合就是最优组合,那么期望收益必须是 \(\pi\)),先验协方差取 \(\tau\Sigma\)。投资者的观点写成 \(P\mu=q+\varepsilon\),\(\varepsilon\sim N(0,\Omega)\)。按多元正态–正态共轭,后验均值为

\[ \mu_{BL}=\big[(\tau\Sigma)^{-1}+P^\top\Omega^{-1}P\big]^{-1}\big[(\tau\Sigma)^{-1}\pi+P^\top\Omega^{-1}q\big]. \]

推导见第 03 册第 11 章 11.10.3 节。这里关心工程问题:

  • \(\tau\) 和 \(\Omega\) 只有比值重要。 常用的 He–Litterman 设定 \(\Omega=\operatorname{diag}(P\tau\Sigma P^\top)\) 让每个观点的不确定性与它所涉及组合的先验方差成正比,等于"观点与先验同样可信"。代码中用一个信心系数缩放 \(\Omega\)。
  • 偏离只发生在观点组合张成的空间里。 在 \(\Omega\) 设定合理时,无约束最优权重满足 \(w_{BL}=w_{\text{mkt}}+P^\top\Lambda\),\(\Lambda\) 是某个向量。与观点无关的资产权重保持市场权重不变,这让结果容易解释。
  • 观点从哪里来。 量化流程里,观点通常就是信号:对每只股票一个观点(\(P=I\)),\(q\) 是信号预测的收益,\(\Omega\) 由信号的历史 IC 决定。这时 BL 就是把信号预测向均衡收益收缩,收缩强度由 IC 决定,与 Grinold 的 alpha 公式同一思想。

白话解释:先看单个资产、单个观点的标量版本,后验公式就一目了然。设先验均值 \(\pi\)、先验方差 \(\tau\sigma^2\),观点 \(q\)、观点方差 \(\omega\),则 \(\mu_{BL}=\dfrac{\pi/(\tau\sigma^2)+q/\omega}{1/(\tau\sigma^2)+1/\omega}\), 即"先验和观点按各自精度(方差的倒数)加权平均"。矩阵公式只是把"除以方差"换成"乘以协方差矩阵的逆"。数值例:先验 5%、观点 8%,若两者同样可信,后验 6.5%;若观点的方差只有先验的三分之一(精度是 3 倍),后验 \(=(5+3\times8)/4=7.25\%\)。 反向优化 \(\pi=\delta\Sigma w_{\text{mkt}}\) 的含义:由 5.2.2 节,无约束最优权重 \(w=\frac1\delta\Sigma^{-1}\mu\)(这里风险厌恶写成 \(\delta/2\) 的形式)。反过来,如果市场组合就是大家的最优选择,那么它"隐含"的期望收益就是 \(\pi=\delta\Sigma w_{\text{mkt}}\)。这和用期权价格反推隐含波动率是同一个思路:用观察到的结果(市场权重)反推大家心中的参数(期望收益)。

金融直觉:BL 就是资产配置委员会的标准流程的数学版。基准是"什么都不知道时就持有市场组合";研究员提出观点("未来一年科技股跑赢消费股 3%")并给出信心;最终配置只在有观点的地方偏离基准,偏离幅度由信心决定。没有观点的资产保持基准权重,委员会成员很容易理解和审查每一笔偏离。

5.7.2 代码

# 代码 5.5  Black–Litterman:从均衡先验出发,只在有观点的地方偏离(需先运行代码 5.0)
R, ind, mu, Sig = make_market(T=1260, N=30, n_ind=5, seed=6)
S = lw_cov(R.values[-504:]) * 252
N = len(S)
rng = np.random.default_rng(6)
cap = rng.lognormal(0, 1, N); w_mkt = cap / cap.sum()            # 市值权重
delta, tau = 2.5, 0.05
pi = delta * S @ w_mkt                                           # 均衡隐含收益(反向优化)

# 观点 1:行业 0 跑赢行业 1 年化 3%(行业内等权);观点 2:S10 绝对收益 12%
P = np.zeros((2, N))
P[0, ind == 0] = 1 / np.sum(ind == 0); P[0, ind == 1] = -1 / np.sum(ind == 1)
P[1, 10] = 1.0
q = np.array([0.03, 0.12])

def bl(conf=1.0):
    """conf 缩放观点精度:Omega = diag(P tau S P') / conf(He–Litterman 设定的变体)。"""
    Om = np.diag(np.diag(P @ (tau * S) @ P.T)) / conf
    A = np.linalg.inv(tau * S) + P.T @ np.linalg.inv(Om) @ P
    m = np.linalg.solve(A, np.linalg.solve(tau * S, pi) + P.T @ np.linalg.solve(Om, q))
    return m, np.linalg.solve(delta * S, m)

print(f"先验下观点组合的隐含收益: 行业0-行业1 = {P[0] @ pi:.4f}, S10 = {pi[10]:.4f}")
for conf in [0.25, 1.0, 4.0, 100.0]:
    m, w = bl(conf)
    tilt = w - w_mkt
    print(f"信心×{conf:<6}: 后验 P·mu = {np.round(P @ m, 4)}, 权重偏离 L1 = {np.abs(tilt).sum():.3f}, "
          f"最大偏离 {tilt[np.argmax(np.abs(tilt))]:+.3f}(S{np.argmax(np.abs(tilt)):02d})")
# 不受观点影响的资产,偏离是否为零?
m, w = bl(1.0)
untouched = (ind >= 2) & (np.arange(N) != 10)
print(f"与观点无关的资产(行业2–4,除 S10)最大偏离: {np.abs(w - w_mkt)[untouched].max():.4f}")
# 对照:把历史均值直接塞进 MV
w_naive = np.linalg.solve(delta * S, R.values[-504:].mean(0) * 252)
print(f"对照:用 2 年样本均值直接做 MV,权重 L1 = {np.abs(w_naive).sum():.2f},最大 {w_naive.max():.2f},最小 {w_naive.min():.2f}")

输出:

先验下观点组合的隐含收益: 行业0-行业1 = 0.0045, S10 = 0.0271
信心×0.25  : 后验 P·mu = [0.0061 0.043 ], 权重偏离 L1 = 0.219, 最大偏离 +0.083(S10)
信心×1.0   : 后验 P·mu = [0.0114 0.0683], 权重偏离 L1 = 0.640, 最大偏离 +0.218(S10)
信心×4.0   : 后验 P·mu = [0.0208 0.0973], 权重偏离 L1 = 1.210, 最大偏离 +0.374(S10)
信心×100.0 : 后验 P·mu = [0.0295 0.1188], 权重偏离 L1 = 1.679, 最大偏离 +0.492(S10)
与观点无关的资产(行业2–4,除 S10)最大偏离: 0.0000
对照:用 2 年样本均值直接做 MV,权重 L1 = 34.99,最大 2.10,最小 -3.23
  • 先验认为行业 0 只比行业 1 高 0.45%,观点说是 3%;信心系数为 1 时,后验落在 1.14%,在两者之间。信心越高,后验越接近观点,权重偏离越大;信心 ×100 时后验几乎等于观点。
  • 与观点无关的 18 只股票(行业 2–4)权重偏离是 0,验证了上面说的性质。
  • 对照组用 2 年样本均值直接做均值–方差,权重 \(\ell_1\) 范数 35,单只股票最大做空 323%。BL 的价值主要就在这一行的对比里:同样是均值–方差框架,先验决定了结果是否可用。

5.8 样本外比较:哪种方法"最好"

5.8.1 公平比较的要求

比较组合构建方法时,最常见的错误是只比一条回测曲线。公平的比较至少要做到:

  1. 滚动估计:每个调仓日只用之前的数据估计输入(本册第 06 章详细讨论时间对齐)。
  2. 相同的输入:所有方法用同一个协方差估计、同一个调仓频率、同一个成本模型,只换构建规则。
  3. 报告换手和成本:构建方法之间最大的差别往往在换手上。
  4. 给出统计误差:年化 Sharpe 的标准误约为 \(\sqrt{(1+SR^2/2)/\text{年数}}\)(第 03 册第 01 章 1.6 节,正态近似;厚尾时更大,含偏度、峰度与自相关修正的完整处理见本册第 06 章 6.7 节)。

5.8.2 代码:六种方法的滚动比较

代码 5.6 在 10 年合成数据上,用 2 年窗口、月度调仓、10bp 单边成本比较六种方法。因为是模拟,我们还能在每个调仓日用真实参数算出该组合的事前 Sharpe,这是真实数据做不到的。

# 代码 5.6  滚动样本外比较:月度调仓、扣成本(需先运行代码 5.0 与代码 5.4 中的函数定义)
R, ind, mu, Sig = make_market(T=2520, N=30, n_ind=5, seed=8)
X = R.values; T, N = X.shape
win, step, cost_bp = 504, 21, 10

def mv_long(m, S, lam=5.0, cap=0.10):
    r = minimize(lambda w: -(m @ w - lam * w @ S @ w), np.ones(N) / N,
                 jac=lambda w: -(m - 2 * lam * S @ w), method="SLSQP", bounds=[(0, cap)] * N,
                 constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}], options={"ftol": 1e-12})
    return r.x

strategies = {
    "等权1/N": lambda m, S: np.ones(N) / N,
    "MV(样本均值,上限10%)": lambda m, S: mv_long(m, S),
    "MV(收缩均值,上限10%)": lambda m, S: mv_long(0.2 * m + 0.8 * m.mean(), S),
    "最小方差(多头)": lambda m, S: min_var_long(S),
    "ERC": lambda m, S: erc(S),
    "HRP": lambda m, S: hrp(S),
}
out = {}
for name, fn in strategies.items():
    w_prev = np.zeros(N); rets = []; tos = []; ex_ante = []
    for t0 in range(win, T - step + 1, step):
        Xw = X[t0 - win:t0]
        w = fn(Xw.mean(0) * 252, lw_cov(Xw) * 252)
        tos.append(np.abs(w - w_prev).sum())
        ex_ante.append(w @ mu / np.sqrt(w @ Sig @ w) * np.sqrt(252))   # 用真实参数评价(只有模拟里做得到)
        r = X[t0:t0 + step] @ w                      # 期内权重不漂移(简化:每日再平衡回目标)
        r[0] -= cost_bp * 1e-4 * tos[-1]             # 调仓成本记在调仓日
        rets.append(r); w_prev = w
    r = np.concatenate(rets)
    nav = np.cumprod(1 + r); dd = 1 - nav / np.maximum.accumulate(nav)
    sr = r.mean() / r.std() * np.sqrt(252)
    out[name] = dict(年化收益=r.mean() * 252, 年化波动=r.std() * np.sqrt(252), Sharpe=sr,
                     Sharpe标准误=np.sqrt((1 + sr**2 / 2) / (len(r) / 252)),
                     最大回撤=dd.max(), 月均换手=np.mean(tos[1:]), 真实参数Sharpe=np.mean(ex_ante))
print(pd.DataFrame(out).T.round(3))

输出:

                 年化收益   年化波动  Sharpe  Sharpe标准误   最大回撤   月均换手  真实参数Sharpe
等权1/N          -0.013  0.211  -0.060      0.354  0.482  0.000       0.411
MV(样本均值,上限10%)  0.027  0.198   0.135      0.355  0.477  0.208       0.406
MV(收缩均值,上限10%)  0.036  0.188   0.190      0.357  0.408  0.141       0.406
最小方差(多头)        0.045  0.184   0.247      0.359  0.396  0.116       0.396
ERC            -0.004  0.205  -0.018      0.354  0.471  0.014       0.412
HRP             0.002  0.201   0.010      0.354  0.465  0.076       0.411

这张表是本章最重要的一张表:

  • 按实现的 Sharpe 排序,最小方差最好(0.25),等权最差(−0.06)。但 8 年样本的 Sharpe 标准误约 0.35,所有方法之间的差别都在一个标准误以内,在统计上无法区分。
  • 最后一列在真实参数下评价:六种方法的事前 Sharpe 都在 0.40 到 0.41 之间,几乎没有区别。实现值的排序完全是这一条市场路径的运气。如果只看实现值,研究员会得出"最小方差显著更好"的错误结论。
  • 真正稳健的差别在风险和换手上:最小方差和收缩均值 MV 的波动和最大回撤更低;MV 类方法的月均换手是 ERC 的 10 到 15 倍;HRP 的换手是 ERC 的 5 倍,与 5.6.3 节的稳定性结果一致。
  • 收缩均值的 MV 在每一项上都不差于样本均值的 MV,换手还低三分之一。

推导拆解:Sharpe 标准误 0.35 怎么来的?公式 \(\sqrt{(1+SR^2/2)/\text{年数}}\) 中,样本外约 8 年(10 年减去 2 年估计窗口),\(SR\) 在 0 附近,所以约为 \(\sqrt{1/8}\approx0.35\)。两个方法的 Sharpe 差 0.3,还不到一个标准误;若要用 t 检验区分两个 Sharpe 相差 0.3 的策略(假设二者独立),差值的标准误约 \(0.35\times\sqrt2\approx0.5\),需要 \(t\approx2\) 即差值约 1.0,或者把样本拉长到约 90 年。这就是为什么"回测曲线谁更好"几乎从来不能作为选方法的依据。(实际中各方法收益高度相关,差值的标准误会小一些,但量级结论不变。)

金融直觉:最后一列"真实参数 Sharpe"是只有模拟才有的"上帝视角"。它告诉我们六种方法的真实质量几乎一样(0.40–0.41),而实现值从 −0.06 到 0.25,排序完全是运气。这和基金评价中的经典困境一致:同一类型的基金,事后业绩的差异大部分是噪声,因此 CFA 在业绩评价中强调要看风格一致性、风险和费用这类"可持续"的特征,而不是短期排名。在组合构建上,"可持续"的差别是风险、换手和稳定性。

所以选择构建方法的依据,应当是你对输入质量的判断,而不是一条回测曲线:

你有什么 推荐
可靠的 alpha 预测(有样本外 IC 支持)与因子风险模型 带约束和成本惩罚的均值–方差,alpha 先收缩
只有风险估计,没有可靠 alpha 最小方差(加上限)、ERC/风险预算
资产多、协方差病态、样本短 HRP 或分块优化,注意测稳定性
有基准、有少量主观或宏观观点 Black–Litterman
什么都不确定 等权或市值加权,它们是难以战胜的基准

5.9 常见陷阱与检查清单

陷阱

  1. 把历史均值当期望收益直接放进优化器。5.2.3 节的实验显示这几乎等于随机组合。
  2. 时间尺度不一致:年化 alpha 配一次性成本、日度协方差配年化 alpha、预测期 20 天的信号每天调仓。
  3. 相信样本内风险:优化后组合的真实风险系统性高于预测,5.2.3 节的比值是 1.24 到 1.32。
  4. 约束太紧而不自知:最终组合主要由约束决定,信号几乎不起作用。检查方法是比较"有信号"和"信号置零"时的组合差异。
  5. 把求解器的"成功"当作正确:success=True 只说明满足了停止准则,不说明满足了所有约束,更不说明量纲正确。
  6. 非凸约束当凸问题解:持仓数上限、整手约束会让 SLSQP 停在局部解。
  7. 只看一条回测曲线比较方法:见 5.8 节。
  8. 忽略稳定性:权重对估计窗口的微小移动很敏感的方法,实盘换手会远高于回测假设。

上线前的检查清单

  • [ ] \(\alpha\) 的量纲、预测期与成本、协方差的时间单位一致,并写在代码文档里。
  • [ ] \(\Sigma\) 正定(Cholesky 成功),条件数已记录;大规模问题用因子形式。
  • [ ] 期望收益已收缩,收缩强度有依据(IC、BL 信心)。
  • [ ] 先求最小可行换手,确认问题可行。
  • [ ] 求解后逐条检查约束残差,抽查 KKT 条件,换初值或求解器复核。
  • [ ] 读出主要约束的影子价格,确认没有"昂贵而无意义"的约束。
  • [ ] 测稳定性:估计窗口后移几天,权重变化多少;对输入加噪声,权重变化多少。
  • [ ] 滚动样本外比较时报告换手、成本、Sharpe 标准误,并与等权基准比较。

本章小结

  • 均值–方差优化按 \(1/\ell_k\) 放大第 \(k\) 个特征方向上的 alpha,而样本协方差的小特征值偏低,所以误差会被系统性放大。条件数是最坏情况下的放大倍数。在本章的实验中,期望收益的误差是主要损失来源,向截面均值收缩比收缩协方差更有效。
  • 数值上:不显式求逆,检查正定性,用 Woodbury 公式利用因子结构,统一量纲,求解后检查约束和 KKT 条件。
  • 约束用线性和二阶锥形式写出,\(\ell_1\) 类约束用拆分变量;约束也是一种正则化;可行性先于最优性;影子价格告诉你每条约束的代价。
  • 线性成本产生不交易区间,二次成本产生部分调整;成本和收益的时间尺度必须对齐;一期模型会高估不交易区间。
  • 最小方差、ERC、HRP 不用期望收益;BL 从均衡先验出发,只在观点空间内偏离。
  • 方法之间的实现 Sharpe 差别通常小于统计误差;稳健的差别在风险、换手和稳定性上。

练习

  1. 条件数与样本长度。 修改代码 5.1,把估计窗口从 252 天改为 63、126、504、1260 天,画出样本协方差和 LW 协方差的条件数随窗口的变化,以及"样本 \(\mu\) + LW"切点组合真实 Sharpe 中位数的变化。窗口加长时,哪一项改善得更快?(提示:\(\hat\mu\) 的标准误按 \(1/\sqrt T\) 下降,而 \(N/T\) 决定特征值偏差。)

  2. 最优收缩强度。 在代码 5.1 中,把期望收益的收缩比例从 0 扫到 1(步长 0.1),找出使真实 Sharpe 中位数最大的收缩比例。它与真实期望收益的截面离散度和估计误差之比有什么关系?(提示:参考第 03 册第 11 章的"按精度加权"。)

  3. 用乘子代替有限差分。 把代码 5.2 改用 method="trust-constr",读取 r.v 中的拉格朗日乘子,与有限差分得到的影子价格比较。注意乘子的符号约定和目标函数的缩放(参见第 04 册 12.3.4 节和第 16b 章 16.11.2 节)。

  4. 3/2 次方冲击成本。 在代码 5.3 中把二次成本 \(\eta\,\Delta w^2\) 换成 \(\eta'\lvert\Delta w\rvert^{3/2}\),调整 \(\eta'\) 使"每日调到目标"的总成本与原来相近。不交易区间和部分调整的最优参数怎样变化?

  5. 时间尺度错配的代价。 在代码 5.2 中把成本从 15bp 改成 15bp × 12(相当于假设持有期 1 个月、alpha 按年计时正确的摊销方式),比较换手和主动 alpha 的变化。哪一种设定更接近"每月调仓的真实权衡"?

  6. HRP 的稳定化。 把代码 5.4 中 HRP 的链接方法换成 "ward" 和 "average",并实现"对 50 个 bootstrap 样本分别计算 HRP 权重再取平均"。比较窗口后移换手。

  7. BL 与信号。 设 \(P=I\)(每只股票一个观点),\(q\) 为代码 5.2 中的 alpha,\(\Omega=\operatorname{diag}(\omega_i)\) 由 IC 决定。推导 \(\tau\to0\) 和 \(\tau\to\infty\) 时 \(\mu_{BL}\) 的极限,并解释它与"alpha 收缩"的关系。

  8. 多次重复的样本外比较。 把代码 5.6 用 20 个不同的随机种子重复,统计每种方法实现 Sharpe 排名第一的次数,并与"真实参数 Sharpe"的排名比较。(提示:这个练习说明为什么本册第 06 章要讨论多重检验。)


延伸阅读

  • 第 04 册第 12 章(约束优化理论,12.10 节均值–方差的 KKT 分析)、第 16a、16b 章(二次规划算法与组合优化实战)、第 06 章 6.6 节(协方差修复与风险预算)、附录 A.3 节(因子模型协方差的快速求解)。
  • 第 01 册第 00 章 0.7 节(Sherman–Morrison–Woodbury 公式)、第 04a 章(特征值不等式、主子矩阵的条件数、收缩与优超)。
  • 第 03 册第 11 章 11.10.3 节(Black–Litterman 的共轭推导);第 03 册第 01 章 1.6 节(Sharpe 比率的标准误)。
  • 第 10 册第 09 章(条件数与优化收敛)。
  • 本册第 04 章(风险模型与收缩估计)、第 06 章(回测方法论)、第 07 章(交易成本模型)、第 11 章(风险归因)。
  • Markowitz, H. (1952). Portfolio Selection. Journal of Finance.
  • Michaud, R. (1989). The Markowitz Optimization Enigma: Is "Optimized" Optimal? Financial Analysts Journal.
  • Black, F., & Litterman, R. (1992). Global Portfolio Optimization. Financial Analysts Journal.
  • Chopra, V., & Ziemba, W. (1993). The Effect of Errors in Means, Variances, and Covariances on Optimal Portfolio Choice. Journal of Portfolio Management.
  • Jagannathan, R., & Ma, T. (2003). Risk Reduction in Large Portfolios: Why Imposing the Wrong Constraints Helps. Journal of Finance.
  • Ledoit, O., & Wolf, M. (2004). A Well-Conditioned Estimator for Large-Dimensional Covariance Matrices. Journal of Multivariate Analysis.
  • DeMiguel, V., Garlappi, L., & Uppal, R. (2009). Optimal Versus Naive Diversification: How Inefficient is the 1/N Portfolio Strategy? Review of Financial Studies.
  • Maillard, S., Roncalli, T., & Teiletche, J. (2010). The Properties of Equally Weighted Risk Contribution Portfolios. Journal of Portfolio Management.
  • Gârleanu, N., & Pedersen, L. H. (2013). Dynamic Trading with Predictable Returns and Transaction Costs. Journal of Finance.
  • López de Prado, M. (2016). Building Diversified Portfolios that Outperform Out of Sample. Journal of Portfolio Management.
  • Grinold, R., & Kahn, R. Active Portfolio Management(第 2 版,2000)。