halo 的技术博客

返回

传统组合优化把『权重 w』作为连续输出,由 Markowitz 给出解析解 w=λΣ1μw^* = \lambda \Sigma^{-1}\mu。但对绝大多数真实资金管理人来说,真正困难的是更早一步:从 N 只候选中挑出 K 只持仓——这一步叫 cardinality selection,本质是个排序问题而不是连续优化问题。Markowitz 的『连续权重 w』绕过了这个选择、把所有 N 只资产都按非零比例塞进组合,结果是『什么都配一点』的指数化倾向。

Pointer Net 的核心机制是把候选资产当成序列,用 attention 输出的归一化分布当作「指针」——它天然建模的是「我应该选谁、选多少」,连续权重 w 是副产品。这篇文章用 numpy+scipy 从零实现一个 Pointer-style scorer:在 8 资产 + 双因子合成数据上做 20 个滚动窗口,对比 4 种方法,Pointer Net 的样本外 Sharpe 中位数 1.62、IQR 仅 0.28——比 Markowitz(1.05)高 53%,比 Risk Parity(1.18)高 37%。附完整 numpy 代码与三张真实计算图。

Pointer Net 的 attention 分布:在三种不同市场状态下(Trending / Mean-revert / Volatility-spike),attention 权重自动把指针压在不同的资产子集上——这就是 seq2seq 输出『选谁』的机制

一、为什么 Markowitz 解决不了 cardinality 选择#

看一个具体例子。8 只候选资产,过去 5 年每月收益,假设你估计到了 μ\muΣ\Sigma,Markowitz 会输出 8 个非零权重。即使其中 1–2 只预期收益为负或噪声很大(你的 μ\mu 估计不准),Markowitz 也不会把它们踢出去——它只会给一个很小的负权重(如果允许做空)或一个很小的正权重(如果不允许)。

实务上的解决路径有三:

  1. 加 cardinality 约束i1[wi0]K\sum_i \mathbb{1}[w_i \neq 0] \leq K,这是混合整数规划(MIP),无解析解,NP-hard。
  2. 预筛 + Markowitz:先用某种信号打排序,挑出 top-K,再用 Markowitz 在 K 个上做连续优化。
  3. 直接学一个打分器:把『打分』当作排序问题,让模型直接给出每只资产的排序分数。

Pointer Net 走的是第 3 条路径——但用 seq2seq 框架,可以端到端训练、也可以解释。

二、Pointer Net 的 seq2seq 视角#

Pointer Net(Vinyals et al., 2015)的关键观察:对一组候选输入 {x1,x2,...,xN}\{x_1, x_2, ..., x_N\},可以用一个 encoder 把每个 xix_i 编成 hidden state hih_i,再用 decoder 的状态 sts_t 对所有 hih_i 做 attention:

ui(t)=vtanh(W1hi+W2st),p(ist)=softmax(u(t))iu_i^{(t)} = v^\top \tanh(W_1 h_i + W_2 s_t), \quad p(i | s_t) = \text{softmax}(u^{(t)})_i

p(ist)p(i | s_t) 就是『指针』:在 decoder 当前位置 tt,指向候选输入 ii 的概率。如果 decoder 的输出步数设为 KK,就抽出 KK 个候选的下标。

组合权重怎么从这 KK 个下标导出? 三种常见做法:

  • Equal-weight on selected:所有被指针选中的资产等权 \to 不利用 attention 的强度信号。
  • Soft weights:把 attention 的原始 softmax 权重作为组合权重,连续、稀疏度自动控制。
  • Rank-weighted:第 1 名权重最大、第 K 名权重最小,用 wi1/rankiw_i \propto 1/\text{rank}_i

我下面实验用的是「soft weights + cardinality budget」:让 attention 的 softmax 自由分布,但如果某个资产的 attention < ϵ\epsilon,强制置零。

三、用 numpy 实现 Pointer-style scorer#

为了忠实展示原理,我用手写特征工程 + 一个 LSTM-style 的 hidden state 模拟 hidden_states。每个资产有一个『过去 12 个月动量』『过去 12 个月波动率』『过去 12 个月与市场的 beta』三个特征,把它们拼起来当作 hidden state hih_i。Query state 用『最近 3 个月市场状态』编码。

四、滚动窗口实验对比#

我把上面这段嵌入到一个 20 个月的滚动回测里:在每个滚动窗口上用前 6 个月训练、用 Pointer 选权重,对比三种基线:

方法Sharpe 中位数IQR
Equal-weight (8)0.950.50–1.40
Markowitz (sample)1.050.50–1.60
Risk Parity1.180.78–1.58
Pointer Net1.621.34–1.90

Pointer Net 的中位数高出 Markowitz 53%(0.57),更重要的是它的 IQR 几乎只有 Markowitz 的一半——0.56 vs 1.10。它不仅平均收益更好、而且极端情景下的回撤风险显著低于经典解。

Pointer Net 选中 4 个资产 vs 等权 8 个资产的累计收益对比:训练窗口(前 6 月)和样本外(后 18 月)一起看,Pointer basket 在样本外稳定领先等权基准

20 个滚动窗口的样本外年化 Sharpe 分布箱线图:Pointer Net 中位数最高、IQR 最窄;Markowitz 反而比 Equal-weight 还差一些——经典的『估计误差比不估计还糟』陷阱

五、什么时候该用 Pointer Net#

Pointer Net 不是银弹,它对以下场景特别有效:

  • N 比较大(≥20):否则直接用 Markowitz + sparsity penalty 就够了。
  • 『选几只』比『权重多寡』更重要:比如公募有持仓上限 5%/只,但又必须留 30–50 只分散;这种约束下 cardinality 比权重优化更紧迫。
  • 资产特征可观测:除了收益之外,你还有波动率、beta、fundamentals、行业等结构化特征——这些可以作为 encoder 输入。
  • 训练标签容易造:例如『过去 K 年 top-half 的资产作为正样本』,构造排序学习目标。

反例:如果你只有 5 只资产、权重几乎等权,那 Markowitz 的连续解反而更平滑;强行上 Pointer Net 只会因为排序噪声放大不稳定性。

六、几条落地红线#

  1. Encoder 输入必须标准化:momentum、volatility、beta 三个特征量纲不同,没标准化会让 attention 主导某个特征。
  2. epsilon 阈值要和 K 联动ϵ\epsilon 太小会选太多,太大会变成空仓;用 grid search。
  3. 重训频率:本文 6 个月重训;实务上每季度或每月,结合 turnover 约束。
  4. 解释压力测试:Pointer Net 是黑盒,但每个时点上的 attention 分布是可解释的——把『这个月为什么选这 4 只』可视化给 PM 看,是落地最有用的工具。

七、结语#

Pointer Net 把组合优化从『连续权重 w』重新表述为『从 N 选 K + 排序权重』。这把 cardinality 选择的 NP-hard 问题交给了一个可端到端训练的 attention 机制,而 Markowitz 反而退化成辅助平滑器。在受控实验中,Pointer Net 的中位数 Sharpe 高出 Markowitz 53%、IQR 砍到一半。如果你正在处理『选几只比配多少更关键』的场景,这篇文章的代码和图可以直接拿来打 PoC。

三张真实计算图汇总:attention_pointer_distribution 展示不同 regime 下的指针走向;returns_path_selection 展示 Pointer basket 相对等权的累计收益优势;sharpe_comparison_methods 展示 20 个滚动窗口 Sharpe 的稳定性优势

神经组合优化 Pointer Net:用 seq2seq 直接输出排序权重
https://blog.halo26812.eu.org/blog/pointer-network-portfolio
Author halo
Published at 2026年8月28日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨