先说结论

THU-BDC2026 最值得记录的,不是某一次看起来很漂亮的收益率,而是那些把模型从“看起来有效”拉回“真的可验证”的实验。

这篇就是我的实验日志。它不追求把项目包装成一路顺风,反而专门把几次把结果越改越差的优化写下来,因为比赛里真正能留下来的经验,往往就藏在这些失败里。

时间 实验 结果 我的判断
2026-05-31 单周期回测 最新数据集模型单周 +5.9533% 很亮,但周期太短,只能当线索
2026-06-04 修复数据泄露 夏普从 2.30 掉到 -0.49 难看,但终于诚实
2026-06-05 删除波动率特征 特征 289 -> 227,夏普回到 0.40 做减法有效
2026-06-06 加资金流向因子 夏普 0.70,累计收益 12.33% 目前最像样的一版
2026-06-06 Top-80 特征筛选 训练快 57%,收益掉到 -7.47% 快不等于好
2026-06-07 行业覆盖修复 覆盖率 4.7% -> 100%,夏普降到 0.18 数据完整不等于模型会用

为什么要写成日志

这个项目一开始很容易让人上头:模型复杂、指标丰富、还有一些看起来很强的回测结果。

但量化项目最怕的就是“漂亮得不正常”。只要训练集切分、标签构造、特征窗口里有一点点未来信息,模型就会像突然开窍一样变强。问题是这种强不是真的强,它只是提前看了答案。

所以这篇文章的重点不是“我做出了一个多厉害的模型”,而是记录我怎么一步步把水分挤掉:

  • 先承认单周期结果不能当结论;
  • 再修复训练和预测之间的时间隔离;
  • 接着删掉可能依赖泄露的特征;
  • 最后再看哪些因子还能留下真实增益。

早期单周期回测:很亮,但不能信太多

archive 里有一份早期回测报告,只测了 2026-03-09 到 2026-03-13 这 5 个交易日。

当时结果看起来很好:

模型 单周期收益
原始数据集模型 -0.7367%
最新数据集模型 5.9533%
等权基准 1.3114%

如果只看这张表,很容易觉得方向已经对了。但 5 个交易日太短,运气成分很大。它更像一个提示:“这里可能有东西,值得继续查”,但不能证明模型稳定有效。

后来回头看,这个阶段最大的价值不是收益,而是提醒我必须把验证周期拉长,把回测流程做严。

修复数据泄露:最痛,但最值

真正改变项目走向的是数据泄露修复。

原来的回测只保证训练样本日期早于回测日,但标签使用了未来 5 天开盘价。也就是说,靠近回测日的训练样本虽然日期在过去,标签却可能已经跨进未来窗口。

这类泄露非常隐蔽。代码看起来没问题,结果也很好看,但本质上模型已经偷看了答案。

修复后结果马上变难看:

1
2
3
4
夏普:         -0.49
累计组合: -17.11%
跑赢等权胜率: 44.8%
最大回撤: -31.17%

这一步很打击人,但它是整条实验线里最重要的一步。因为从这里开始,后面的结果才有讨论价值。

做减法:砍掉波动率特征

修复泄露之后,我开始怀疑一批波动率相关特征。

例如:

  • boll_width
  • atr_pct
  • vol_20d
  • 由这些指标衍生出来的聚合特征和横截面排名特征

它们在旧版本里重要性很高,但旧版本本身有泄露。所以我不确定它们是真的有效,还是只是帮模型记住了错误验证里的捷径。

于是我做了一版删除实验:

1
2
3
4
特征维度: 289 -> 227
夏普: -0.49 -> 0.40
累计收益: -17.11% -> 4.39%
胜率: 44.8% -> 51.7%

这次结果反而变好。它给我的提醒很直接:特征不是越多越安全。尤其在金融数据里,有些特征看似信息量很大,其实只是把噪声和错误验证一起放大了。

资金流向:少数真正带来增益的东西

后面我加入了 Tushare 资金流向相关因子,主要包括三类:

  • 主力净流入率;
  • 小单净流入率;
  • 超大单占比。

这一版是目前实验日志里比较像样的一版:

1
2
3
4
5
夏普:         0.70
累计组合: 12.33%
年化收益: 29.90%
跑赢等权胜率: 51.7%
最大回撤: -25.13%

更关键的是,主力净流入率_ma20_cs_rank 进入了特征重要性 Top 20。

这让我觉得,资金行为信号可能比单纯堆价格形态更有价值。价格形态很容易过拟合,资金流向至少在这个窗口里提供了另一类信息。

三个失败优化

Top-80 特征筛选

我用 2023 年前的数据筛出重要性最高的 80 个特征,想减少噪声、加快训练。

训练速度确实变快了:

1
2
训练时间: 116 分钟 -> 50 分钟
速度提升: 约 57%

但结果变差了:

1
2
累计收益: +12.33% -> -7.47%
夏普: 0.70 -> -0.07

这说明历史窗口里的重要性不一定能迁移到未来。金融数据不是静态数据集,市场状态一变,过去最重要的特征可能反而拖后腿。

三分类标签

我还试过把股票粗分成三档:

  • 前 30%;
  • 中间 40%;
  • 后 30%。

本意是让模型更关注“买入区”和“避雷区”,但实际效果很差,夏普掉到 -1.08。

原因大概是排序任务不能切得太粗。10 bins 标签虽然复杂一点,但保留了更多相对顺序信息,更适合每天从 300 只股票里选 Top 5。

行业覆盖修复

还有一次很反直觉的失败:行业覆盖修复。

原来行业覆盖率只有 4.7%,这肯定不合理,所以我把行业信息补到了 100%。从数据完整性的角度看,这一步绝对是正确的。

但模型表现却从夏普 0.70 降到 0.18,而且 sector_id 变成了最重要特征。

这说明模型可能不是“学到了行业”,而是“太相信行业”。行业信息需要更细的约束,比如降权、正则、分组验证,不能直接一股脑塞进去。

最后留下的规则

这轮实验之后,我给自己留下了几条规则:

  1. 单周期结果只能当线索,不能当结论。
  2. 回测第一步永远是查数据泄露。
  3. 特征重要性要按时间段看,不能拿一个历史窗口当真理。
  4. 做减法不丢人,能删掉噪声也是能力。
  5. 更完整的数据也需要被模型正确使用。
  6. 所有“看起来合理”的改动,最后都要让回测说话。

下一步怎么做

如果继续推进这个项目,我会优先做三件事:

  • 把滚动回测周期拉长,不再被单周收益带节奏;
  • 对行业、市值、流动性这类强结构特征做分组验证;
  • 保留 LightGBM LambdaRank 主线,把深度模型作为辅助实验,而不是主战场。

这篇记录不构成任何投资建议。它更像一份比赛实验备忘录:把好看的结果拆开,把难看的结果留下,最后剩下的才可能是真的经验。