跳转到内容

机器学习排序与指标错配

第 10 章的截面动量、第 11 章的多因子,其实都已经在做「排序」——按过去收益排、按因子值排,然后买最强的那一档。但那都是单变量排序:一个指标定输赢。真实研究里,你手里往往同时有几十个特征:动量、换手、波动、量价形态、财务指标……它们各说各话,有时互相矛盾。怎么把它们拧成一股绳、合成一个总分?

这一章讲当代选股研究的主流答案——机器学习排序(Learning to Rank, L2R),以及它最著名的陷阱:指标错配(objective mismatch)——排序排得再准,也可能赚不到钱。这一对概念放在一起讲,因为陷阱恰恰是工具的代价。

把「给股票打分」交给机器学习,教科书里有三条路,先看两条死路:

回归:预测明天涨百分之多少。死因是第 2 章讲过的现实——个股日收益的噪声是信号的几十倍,模型辛辛苦苦预测出「+1.23%」,实际值在 ±3% 里晃。当你唯一能用的是「预测值的大小」时,这点精度立刻被噪声淹没。

分类:预测明天涨还是跌。听起来合理,死因有两个。其一,基准率作祟:A 股一段普涨行情里,「预测明天上涨」的机械准确率本来就有 55%+,模型报 58% 你根本分不清是本事还是行情;其二,涨跌对称 ≠ 盈亏对称:预测「涨」的票平均涨 0.5%、预测「跌」的票平均跌 3%,55% 的准确率照样亏钱——分类损失函数对幅度完全无感。

排序:不预测任何一只股票的绝对涨跌,只预测同一批股票之间的相对强弱。它背后是第二编反复出现的一个实证事实(第 10 章五分位、第 11 章分层都见过):个股绝对收益几乎不可预测,但截面的相对次序有微弱而稳定的可预测性。既然可预测的只有「谁比谁强」,那就让模型直接学这个。

排序学习的目标函数不长成「预测值 − 真实值」的平方,而是长成一场场比赛。每个交易日是一个「查询组」:组内 50 只股票两两配对,每一对问一个问题——这两个谁下期更强?答错就罚。最常用的成对损失(pairwise logistic loss):

L=∑(i,j): ri>rjlog⁡(1+e−(si−sj))L = \sum_{(i,j):\ r_i > r_j} \log\left(1 + e^{-(s_i - s_j)}\right)

人话解释:rir_i 是股票 ii 的真实下期收益,sis_i 是模型给它的分数。对于每一对「真实上 ii 比 jj 强」的股票,如果分数 sis_i 没有高出 sjs_j,损失很大;高出一截,损失趋近 0。模型从头到尾不知道什么叫「涨 2%」,它只知道「A 应该排在 B 前面」。几千场比赛罚下来,分数就学会了次序。

这类目标在信息检索领域叫 ranknet / LambdaRank 家族,工程上常用梯度提升树实现(如 LightGBM 的 ranker 模式)。下一节用一小段把这个工具的思想讲清楚——本教程照例只讲原理:换一棵树、换一个库,成对比较的思想不变。

上面点名的 LightGBM,属于一个叫梯度提升树(Gradient Boosted Decision Tree, GBDT)的模型家族。它的思想一句话就能说完:先种一棵很浅的小树,再让后面的每一棵小树专职纠正前面所有树还没改完的错。

人话解释:第一棵树粗糙地把股票分成几档(比如「换手率高且动量强的给 80 分,其余 50 分」),错漏百出;第二棵不看原题,只看第一棵树的残差——哪些股票被排错了次序,专门修这些错;第三棵修前两棵剩下的错……几百棵小树各管一小块,加起来就是一个能表达复杂规律的打分器。每棵树都很笨(通常只有几十片叶子),但「逐次纠错」让整体足够聪明——这是「三个臭皮匠赛过诸葛亮」的精确数学版。

为什么金融表格数据(几十列特征 × 几百万行)的事实标准是这个家族,而不是名气更大的神经网络?三个原因都长在数据的天性上:

  • 特征的意义藏在「交互」里。「缩量」单独看没意义,「缩量 + 已经横盘很久」才有意义——树的每一次分叉恰好就是一个「如果…就…」的交互判断,天生合拍;
  • 特征的量纲乱七八糟。换手率是百分比、市值以千亿计、动量是小数——树只问「比阈值大还是小」,对单调变换完全无感,不需要归一化;
  • 样本少、噪声大。A 股全市场十几年也就百万量级的「股票-日」样本,且信噪比极低——小树的「笨」反而是护栏,让模型没那么容易把噪声背下来(第 6 章的幽灵在这里同样游荡)。

往后再遇到「用 GBDT 类模型跑基准」,脑子里对应的就是这套「小树逐次纠错」的打分器。怎么训练、怎么调参属于工程细节,本教程不展开——它和本章主题的关系只有一条:排序目标决定它学什么,树只是它用来学的东西。

模型交卷了,怎么打分?直接看预测排序和真实排序像不像。最常用的指标叫 NDCG,拆开看:

DCG(折价累积增益):把真实收益当作「增益」,按模型排的名次加权求和,排得越靠后折价越狠:

DCG=∑k=1Kr(k)log⁡2(k+1)\text{DCG} = \sum_{k=1}^{K} \frac{r_{(k)}}{\log_2(k+1)}

人话解释:排第 1 名的增益除以 log⁡22=1\log_2 2 = 1(不折价),第 2 名除以 log⁡23≈1.585\log_2 3 \approx 1.585,第 3 名除以 log⁡24=2\log_2 4 = 2,第 5 名除以 log⁡26≈2.585\log_2 6 \approx 2.585……名次每靠后一位,能兑现的增益就打折。为什么这样设计?因为使用者只看得见前几名——你买的是 Top-5,第 4 名排对了还是第 40 名排对了,对你的价值天差地别。

手算一遍:5 只股票,模型打分排序为 A、B、C、D、E,真实下期收益:

模型名次股票真实收益折价除数折价后增益
1A+6.0%1+6.00
2B−1.0%1.585−0.63
3C+4.5%2+2.25
4D−3.0%2.322−1.29
5E+2.0%2.585+0.77

DCG=6.00−0.63+2.25−1.29+0.77≈7.10\text{DCG} = 6.00 - 0.63 + 2.25 - 1.29 + 0.77 \approx 7.10

完美排序(按真实收益降序:A、C、E、B、D)能拿到的上限:

IDCG=6.01+4.51.585+2.02+−1.02.322+−3.02.585≈6.00+2.84+1.00−0.43−1.16=8.25\text{IDCG} = \frac{6.0}{1} + \frac{4.5}{1.585} + \frac{2.0}{2} + \frac{-1.0}{2.322} + \frac{-3.0}{2.585} \approx 6.00 + 2.84 + 1.00 - 0.43 - 1.16 = 8.25

NDCG=7.108.25≈0.86\text{NDCG} = \frac{7.10}{8.25} \approx 0.86

NDCG ∈ [0, 1],越接近 1 排得越准(负增益如何处理有多种约定变体,自比时口径一致即可)。

NDCG 是检索味儿的指标,交易员更常用 Top-K Lift(提升度)——模型前 K 名的平均收益,相对随机基准放大了多少倍:

LiftK=模型 Top-K 平均收益全体平均收益\text{Lift}_K = \frac{\text{模型 Top-K 平均收益}}{\text{全体平均收益}}

手算一遍(沿用上表):模型 Top-2 是 A 和 B,平均收益 (6.0−1.0)/2=2.5%(6.0 - 1.0)/2 = 2.5\%;全体平均 (6.0−1.0+4.5−3.0+2.0)/5=1.7%(6.0 - 1.0 + 4.5 - 3.0 + 2.0)/5 = 1.7\%。于是:

Lift2=2.51.7≈1.47\text{Lift}_2 = \frac{2.5}{1.7} \approx 1.47

随机选 2 只的期望 Lift = 1.0,模型把它抬高了 47%——这就是「排序能力」翻译成钱的汇率。

指标错配:Lift 1.47 的模型,实盘输给随机

Section titled “指标错配:Lift 1.47 的模型,实盘输给随机”

到这里一切都很美好。然后是本章的标题陷阱。

上面所有计算都在一个平行世界里:收盘打分,收盘价成交,无摩擦。真实交易的世界是:打分在收盘后,买入最早发生在次日;而排序模型最偏爱的那些「最强」的票,恰恰有四个特征:① 分数高常常正因为它刚刚暴涨过(动量类特征天然奖励「刚涨完的」);② 次日高开,把一部分未来收益提前透支;③ 涨停封板的一字单排队买不进(下一章详述);④ 双边成本要扣。

手算一遍这个世界的账。沿用上表,纸面 Top-2 平均 +2.5%:

股票纸面收益现实修正可交易收益
A+6.0%打分日已涨停买不进,次日 +5% 高开追入:起点抬到 1.05 倍,实得 1.06/1.05−1≈+0.95%1.06/1.05 - 1 \approx +0.95\%,再扣双边成本 0.3%+0.65%
B−1.0%次日 +0.5% 高开:0.99/1.005−1≈−1.49%0.99/1.005 - 1 \approx -1.49\%,扣成本 0.3%−1.79%
组合+2.5%−0.57%

随机组合(无追高溢价):全体均值 1.7%,扣成本后约 +1.4%。

Lift 1.47 的模型,可交易收益 −0.57%,跑输随机买入近 2 个百分点。 排序没有错——A 确实是未来最强的票;错的是「可交易收益」的定义里藏了一个模型看不见的项:入场溢价随分数递增。分数越高,你为「共识已经形成」付出的价格越贵。写成一行:

可交易收益i=ri⏟纸面−g(si)⏟入场溢价,随分数递增−成本\text{可交易收益}_i = \underbrace{r_i}_{\text{纸面}} - \underbrace{g(s_i)}_{\text{入场溢价,随分数递增}} - \text{成本}

当 g(si)g(s_i) 递增得足够快,d(可交易收益)ds<0\dfrac{d(\text{可交易收益})}{ds} < 0:分数越高,买它越亏。模型辛苦学到的排序能力,在交易层被入场溢价反向吃光。这就是指标错配——你优化的指标(NDCG、Lift)和你真正要的东西(可交易利润)不再是同一个函数。

本章的演示数据(指标错配演示(合成):12 个月的纸面 Lift 与可交易 Lift。纸面 Lift = 模型 Top-2 组合月收益 ÷ 全市场平均月收益(收盘打分、收盘价成交、无摩擦的平行世界);可交易 Lift = 同一组合次日开盘成交、扣双边成本 0.3%、跳过一字板后的真实倍数(随机基准 = 1.0)。生成规则:纸面 = 1.36 + 0.16·N(0,1);可交易 = 纸面 − 0.50 + 0.13·N(0,1)——排序能力真实存在(纸面稳定 > 1),但入场透支吃掉了全部超额)把两个世界的差距摆在一起——每个月的纸面 Lift 与可交易 Lift,虚线是随机基准 1.0:

纸面 Lift 月月站在 1.0 之上,均值 1.376——模型是真会排序,这不是运气;可交易 Lift 的均值却只有 0.842,大半时间在随机基准之下。两条曲线之间的落差,就是「排序能力」与「赚钱能力」之间那道由入场溢价和摩擦构成的鸿沟。数据是合成的(生成规则见 meta),落差刻意做得清晰;真实研究里这道鸿沟可以更隐蔽——这正是它危险的地方:只看纸面指标的研究,连自己已经掉沟里了都不知道。

输入:每日截面特征 X[t][i]、下期收益 r[t][i](严格时间切分,第 6 章封条)
输出:排序模型,以及「纸面」与「可交易」两套评估
第一步:构造训练样本(每个交易日 = 一个查询组)
对每个训练日 t:
组内股票两两配对 (i, j):
若 r[t][i] > r[t][j]:样本标签 =「i 应排在 j 前」
第二步:训练
最小化成对损失 Σ log(1 + e^{−(s_i − s_j)})(梯度提升树等实现均可)
训练只用 t ≤ T0 的数据;样本外数据贴封条
第三步:纸面评估(平行世界)
样本外每日:按分数取 Top-K,按当日收盘价「成交」
计算月度 NDCG / Lift —— 回答「排序准不准」
第四步:可交易评估(现实世界)
同样的 Top-K,但:次日开盘成交、一字板跳过、双边成本照扣
计算真实组合收益,与随机等权组合对照 —— 回答「赚不赚钱」
判读:
纸面好看 + 可交易跑输随机 → 指标错配
→ 模型学到的是「谁刚涨完」而不是「谁接下来涨」
→ 处方:改造特征(剔除已透支的涨幅)、换入场规则、或放弃该信号

第三步和第四步必须同屏报告。只交第三步成绩单的研究,默认按隐瞒第四步处理。

  1. 特征泄漏。打分时刻还不知道的信息混进特征(用当日收盘后才有的大单数据给当日收盘打分、用报告期而非披露日的财务数据),NDCG 会漂亮得离谱——那是未来函数换了件马甲(第 1 章、第 6 章的老朋友)。
  2. 只报 NDCG / Lift,不报可交易 PnL。本章的主罪。会议论文可以只比排序指标,真金白银的研究不行。
  3. Top-K 的 K 是挑的。K=3 最好就报 K=3,K=10 就当没跑过——和第 6 章「只报最好的参数」是同一种骗局。K 与调仓频率要预先定死。
  4. 忽视「强票买不到」。涨停排队、流动性枯竭,回测里假设全能按收盘价成交。下一章把这个坑单独讲一整节。
  5. 把排序当预测用。排序模型只输出相对强弱,不含绝对方向——熊市里它选出的「最强」也只是最抗跌的,照样跌。先过上一章的门禁(环境对不对),再谈排序(环境里谁强)。

排序的教训是「回测世界内部要自洽」;置换、门禁、排序讲完,还剩最后两块拼图:怎么检验「检验本身」没有骗你,以及回测世界与真实 A 股之间还隔着什么。下一章先解决前者——把第 6 章的防骗清单升级成重武器。