起因

这次 THU-BDC2026,我手里其实留了三份项目目录:原版、archive、v2。看起来像三个项目,翻完之后发现更准确的说法是:这是同一个比赛项目的三段成长史。

赛题本身很直接:根据沪深 300 成分股的历史行情,对股票做排序,选出 Top 5 组成投资组合,再给它们分配权重。说白了,就是让模型回答一个很现实的问题:下一段时间里,哪几只股票更值得排在前面。

一开始我很兴奋,因为这个题目天然适合写得很“高级”:Transformer、CrossStockAttention、MoE、横截面标准化、Top 5 softmax 权重。关键词堆起来很漂亮,代码也确实能跑。

但后来我发现,金融比赛里最可怕的东西不是模型不够复杂,而是模型看起来太对了。

第一版:看起来很猛

最早的方案是深度模型路线。

我把过去 30 个交易日作为时间窗口,给每只股票计算 72 个技术指标,然后构造成一个四维张量:

1
[batch, 股票数, 30天, 特征数]

模型结构大概是:

  • StockTransformer 建模时间序列;
  • CrossStockAttention 建模同一天不同股票之间的关系;
  • MoE Ranking 增加模型容量;
  • 最后输出每只股票的排序分数,取 Top 5。

这个方案从工程上看挺完整,甚至还有点“比赛味”。但做量化不能只看结构漂不漂亮,核心还是回测能不能经得住审。

那个让我冷静下来的 bug

后面我在回测代码里发现了一个很严重的问题:训练数据过滤只用了预测日前的数据。

原来的逻辑类似这样:

1
train_proc = processed[processed['日期'] < bt_date].copy()

问题在于,标签是用未来 5 天开盘价算的。也就是说,预测日前几天的样本虽然日期小于预测日,但它们的 label 里可能已经包含了预测日附近的未来价格信息。

修复后,我把训练样本和预测日之间隔开:

1
2
3
4
train_proc = processed[
(processed['日期'] < bt_date) &
(processed['日期'] + pd.Timedelta(days=10) < bt_date)
].copy()

这一刀下去,结果非常难看。

指标 修复前声称 修复后回测
夏普 2.30 -0.49
累计收益 - -17.11%
跑赢等权胜率 - 44.8%
最大回撤 - -31.17%

最难受的不是亏损,而是我必须承认:之前那个漂亮结果不可信。

重新开始做一个更诚实的版本

修完数据泄露以后,我没有继续往模型上堆东西,而是先做减法。

我发现一些波动率相关特征在泄露存在时特别“有用”,但修复之后反而像噪声源。于是砍掉了 vol_5dvol_10dvol_20dboll_pctbboll_widthatr_pct 这些特征。

特征维度从 289 降到 227,回测反而变好了:

版本 夏普 累计收益 胜率
修复泄露后基线 -0.49 -17.11% 44.8%
砍波动率特征 0.40 4.39% 51.7%

这一步给我的提醒很大:不是特征越多越好。有些特征在错误的验证方式下会显得特别聪明,但一旦验证变干净,它就露馅了。

Tushare 增强版:比较像样的一版

后面我加了 Tushare 的资金流向因子和申万行业分类,主要包括:

  • 主力净流入率;
  • 小单净流入率;
  • 超大单占比;
  • 行业和市值相关特征。

这版阶段性回测表现最好:

指标 Tushare 增强版
夏普 0.70
累计收益 12.33%
年化收益 29.90%
跑赢等权胜率 51.7%
最大回撤 -25.13%

其中 主力净流入率_ma20_cs_rank 进入了 Top 20 特征重要性,这说明资金流向类因子确实提供了一些额外信息。

但我也不想把这写成“终于成功了”。因为回测窗口只有 2024 年到 2026 年的一段滚动测试,而且去掉最好的月份后,收益稳定性会明显变弱。它是一个阶段性更好的版本,不是能拿来吹一辈子的神模型。

那些没变好的尝试

这个项目后面还有几次很有意思的失败。

我试过 Top-80 特征筛选,训练时间确实从 116 分钟降到 50 分钟,但夏普从 0.70 掉到 -0.07。原因大概是 2023 年前的重要特征,不一定能迁移到 2024-2026 年。

我也试过三分类标签,把股票分成“前 30% / 中间 40% / 后 30%”。结果更差,夏普掉到 -1.08。后来想想也合理:股票排序任务需要保留尽量多的相对顺序信息,粗暴三分类反而把中间信息抹掉了。

还有一次我把行业覆盖率从 4.7% 修到 100%,听起来肯定是对的,但回测从 0.70 掉到 0.18。这个结果很反直觉,也挺真实:数据更完整,不代表模型一定更好。如果模型过度依赖行业特征,修 metadata 也可能把它带偏。

最大的收获

这次比赛给我的最大教训不是某个模型结构,也不是某个指标,而是四个字:别骗自己。

金融机器学习里,最危险的是“看起来合理”。代码能跑、曲线好看、指标很高,这些都不能自动证明模型真的有预测能力。

真正重要的是:

  • 标签有没有偷看未来;
  • 训练集和预测日有没有隔离;
  • 回测窗口是不是太短;
  • 最优月份是不是撑起了全部收益;
  • 特征重要性是不是只是某个时间段的幻觉;
  • 改动变好,是不是只是碰巧。

这篇文章只记录比赛和回测复盘,不构成任何投资建议。

如果以后我再做类似项目,我会先把回测卫生、数据泄露检查、滚动验证这些东西放在第一位。模型可以慢慢变复杂,但验证方式一旦错了,后面所有努力都会被带到沟里。

这次最有价值的地方,可能就是我真的掉进去了一次,然后把它记录下来了。