THU-BDC2026 技术路线:从 Transformer 到 LightGBM 排序
为什么单独写一篇技术路线
上一篇更像比赛复盘,这一篇就专门把 THU-BDC2026 的技术路线理一遍。
这个项目从最早的深度模型,到后面的 LightGBM LambdaRank,中间不是简单的“换模型”,而是我对这个题目的理解发生了变化:股票排序任务不只是预测收益率,更像是在同一天的 300 只股票里做横截面比较。
所以后面我越来越关注三件事:
- 每天的横截面排序;
- 训练和预测的时间隔离;
- 特征是否真的能穿过不同年份。
第一阶段:Transformer + 横截面建模
最早的设计是 StockTransformer。
输入用过去 30 个交易日的数据,每只股票有 72 个技术指标,整体组织成:
1 | [batch, 300只股票, 30天, 72特征] |
模型里有几个关键模块:
- TransformerEncoder:处理单只股票的时间序列;
- FeatureAttention:聚合不同尺度的时序信息;
- CrossStockAttention:让同一天的股票互相“看见”;
- MoE Ranking:用多个专家头做排序打分;
- Score Head:输出每只股票的最终分数。
这个设计的优点是表达能力强,也比较符合“股票之间有相对关系”的直觉。缺点也很明显:训练慢、调参成本高、回测迭代不方便,而且验证方式稍微有问题,复杂模型会把问题放大。
第二阶段:转向 LambdaRank
后面我把主要迭代切到了 LightGBM 的 lambdarank。
原因很实际:
第一,比赛目标本质上是 Top 5 排序,不一定非要预测一个绝对收益率。每天 300 只股票作为一个 group,用排序目标直接优化 NDCG@3 / NDCG@5,会更贴近任务。
第二,LightGBM 训练快很多。一次滚动回测虽然还是要跑很久,但比深度模型更适合反复试错。
第三,树模型的特征重要性更容易检查。它不一定代表因果,但至少能让我发现模型是不是被某一类奇怪特征带偏。
标签和分组
标签定义是:
1 | (T+5 开盘价 - T+1 开盘价) / T+1 开盘价 |
也就是说,模型预测的是从 T+1 到 T+5 的阶段收益。为了让不同交易日之间可比,我会按日期做横截面处理,再把同一天的股票作为一个排序组。
这个地方最容易出问题。
如果回测日是 bt_date,训练集不能只写成:
1 | processed['日期'] < bt_date |
因为靠近 bt_date 的样本,它的 label 可能已经用到了未来开盘价。后面我改成至少隔开一段时间,避免训练集偷看到预测附近的信息。
这一步比换任何模型都重要。
特征工程
项目里用过的特征大概分几类:
| 类型 | 例子 | 作用 |
|---|---|---|
| 趋势/位置 | ma_dev_60d、price_pos_20d |
描述价格相对均线和区间的位置 |
| K 线形态 | lower_shadow_ratio、upper_shadow_ratio |
描述日内买卖力量 |
| 量价关系 | corr_pv_20、obv_ratio |
描述成交量和价格变化关系 |
| 截面排名 | *_cs_rank |
在同一天 300 只股票里做相对比较 |
| 市值/行业 | cap_bin、sector_id |
提供结构性信息 |
| 资金流向 | 主力净流入率、超大单占比 |
捕捉资金行为信号 |
我后来对“截面排名”越来越有好感。因为股票比赛里,很多时候绝对值没有那么稳定,但同一天谁更强、谁更弱,反而更接近任务本身。
几个关键版本
| 版本 | 主要改动 | 夏普 | 累计收益 |
|---|---|---|---|
| 修复泄露后基线 | 排除含未来信息的训练样本 | -0.49 | -17.11% |
| CutVol | 砍掉波动率相关特征 | 0.40 | 4.39% |
| Tushare 增强 | 加资金流向和行业信息 | 0.70 | 12.33% |
| Top-80 特征筛选 | 只保留历史重要性前 80 | -0.07 | -7.47% |
| 三分类标签 | 前 30% / 中间 / 后 30% | -1.08 | -25.77% |
| 行业覆盖修复 | 行业覆盖率 4.7% 到 100% | 0.18 | -0.48% |
这些数值都是阶段性回测结果,不是官方排名,也不是投资建议。
最反直觉的地方
这个项目里有几个结论挺反直觉。
第一,砍特征会变好。波动率特征在有数据泄露时很显眼,修复以后反而拖累模型。它提醒我:特征重要性不是永恒真理,它依赖验证方式。
第二,特征筛选会变差。Top-80 版本训练速度快了很多,但跨年份效果不行。金融数据里,过去重要的东西不一定在未来继续重要。
第三,修行业覆盖率也会变差。行业数据从 4.7% 修到 100%,按理说 metadata 更干净了,但模型把 sector_id 看得太重,结果反而下降。
这三个结果都说明一件事:量化模型不能靠直觉验收,必须靠严格回测验收。
我现在更认可的路线
如果重新做一遍,我不会一上来就堆很复杂的深度结构。
我会先用 LightGBM LambdaRank 做一个干净、快、可解释的基线,把下面这些东西做扎实:
- 日期分组和标签隔离;
- 滚动回测;
- 去最优月后的稳健性检查;
- 特征重要性漂移;
- 单次改动的 ablation;
- 预测结果的权重约束。
等这些都稳定以后,再考虑把深度模型拿回来做融合。
这个项目最后让我更相信:比赛里真正高级的地方,不是模型名字听起来多厉害,而是你能不能证明自己的结果不是碰巧、不是偷看未来、不是被某个奇怪特征骗了。

