THU-BDC2026 复盘:我差点被一个漂亮回测骗了
起因
这次 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 | train_proc = processed[ |
这一刀下去,结果非常难看。
| 指标 | 修复前声称 | 修复后回测 |
|---|---|---|
| 夏普 | 2.30 | -0.49 |
| 累计收益 | - | -17.11% |
| 跑赢等权胜率 | - | 44.8% |
| 最大回撤 | - | -31.17% |
最难受的不是亏损,而是我必须承认:之前那个漂亮结果不可信。
重新开始做一个更诚实的版本
修完数据泄露以后,我没有继续往模型上堆东西,而是先做减法。
我发现一些波动率相关特征在泄露存在时特别“有用”,但修复之后反而像噪声源。于是砍掉了 vol_5d、vol_10d、vol_20d、boll_pctb、boll_width、atr_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 也可能把它带偏。
最大的收获
这次比赛给我的最大教训不是某个模型结构,也不是某个指标,而是四个字:别骗自己。
金融机器学习里,最危险的是“看起来合理”。代码能跑、曲线好看、指标很高,这些都不能自动证明模型真的有预测能力。
真正重要的是:
- 标签有没有偷看未来;
- 训练集和预测日有没有隔离;
- 回测窗口是不是太短;
- 最优月份是不是撑起了全部收益;
- 特征重要性是不是只是某个时间段的幻觉;
- 改动变好,是不是只是碰巧。
这篇文章只记录比赛和回测复盘,不构成任何投资建议。
如果以后我再做类似项目,我会先把回测卫生、数据泄露检查、滚动验证这些东西放在第一位。模型可以慢慢变复杂,但验证方式一旦错了,后面所有努力都会被带到沟里。
这次最有价值的地方,可能就是我真的掉进去了一次,然后把它记录下来了。

