一口气造出几百个因子之后,那份「第一名」还值多少钱
- 能说清为什么「从一千个候选里挑出的第一名」,它的历史成绩天然是虚高的,而且候选越多虚高越厉害
- 能指出一套研究框架里哪几处设计是专门用来对付这件事的,以及每一处各自漏在哪里
- 看到任何一份漂亮的因子成绩单时,知道该先问哪一个不在成绩单上的问题
你在配置文件里把一个名字从「一百五十八列那套」换成「三百六十列那套」,存盘,回车。几分钟之后,屏幕上出现一张表:几百个因子,每个都有自己的成绩,从高到低排好了队。
你盯着排在最上面那一行看。它的数字确实比别的都好看。
然后一个念头冒出来,而且很难压下去——它凭什么排第一?
是它真的看懂了什么别人没看懂的东西,还是说,只要我把几百个数字排个序,总得有一个排在最上面?
这个问题听起来像抬杠,但它是这一整篇要讲的全部内容。它也不是「你的代码写错了」那类问题——恰恰相反,这里每一行代码都是对的,每一个数字都是真算出来的,排序也没有作弊。问题出在**「排序」这个动作本身**。
顺带说清楚这篇不讲什么。表达式怎么变成一列数、算子分几类、为什么一个字符串就等于一个因子,这些在 公式化因子 里已经拆得很细了,这篇不重复。这篇只管一件事:当造因子变得极其便宜之后,你手上那份成绩单该怎么重新定价。
先说人话:招人的时候,你其实在挑「面试状态最好的那个」
设想你在招人。
第一种情况:来了三个候选人,你面完,挑了看起来最合适的那个。入职三个月后,他的表现大概和你面试时的判断差不多——可能略好一点,可能略差一点。
第二种情况:来了三千个候选人,你用同一套题、同一个评分表,把三千份成绩从高到低排好,录用第一名。
问题来了:第二种情况里那个第一名,入职后的表现,会比他的面试成绩差。而且几乎必然会差。
道理很朴素。任何一个人的面试成绩,都可以拆成两块:一块是他真实的水平,另一块是当天的状态——题目恰好撞在他准备过的那块、他前一晚睡得特别好、面试官那天心情不错。这两块是加在一起的,你看到的只有和,分不开。
现在你按总分排序。排在最前面的那个人,非常可能是「真实水平不错,同时当天状态也特别好」的那个。 因为要冲到三千人的顶端,光靠水平往往不够,还得运气帮忙。而运气这东西,下次不会跟着他来上班。
候选人越多,这个效应越猛。三个人里的第一名,运气成分有限;三千个人里的第一名,几乎是被运气推上去的。挑选这个动作,会系统性地把运气一起挑上来,而且候选池越大挑得越狠。
把这个想法推到极端,你会看到一件更不舒服的事:
假设那三千个因子全部是纯粹的随机数,一点信息都没有。你把它们的历史成绩排个序,第一名的成绩单依然会非常好看。 不是「有可能好看」,是必然好看——因为你排序的方式就保证了这一点。你从一堆噪音里挑出了形状最讨你喜欢的那一团噪音,然后它举着一份漂亮的成绩单站在你面前。
这份成绩单上没有任何一处写着「我是从三千个里挑出来的」。
投资层:这改变的不是效率,是你和「证据」的关系
上面那件事,落到你的判断上,有三个后果,一个比一个隐蔽。
第一,成绩单需要打折,而折扣率取决于一个不在成绩单上的数。 同样一个「历史上表现优异」的因子,如果它是你带着明确想法造出来的唯一一个,那这份成绩基本可信;如果它是从八百个候选里挑出来的冠军,那这份成绩里有相当一块是选择带来的虚高。两种情况下你看到的数字可能一模一样。决定它值不值钱的信息,压根不在你手上那张纸上。
这跟 数据窥探 讲的分母问题是同一族毛病,但角度不同。那篇讲的是「你试了多少次没人记得」;这篇讲的是——哪怕你老老实实记住了自己试了八百次,第一名的成绩单本身仍然是虚的,你还得知道该往下折多少。 而「该折多少」这件事,没有一个能直接查表的答案。
第二,因子的来源从「理解」悄悄换成了「搜索」,而你不会察觉。 以前一个因子背后是一个想法:我觉得成交量突然放大之后价格会有惯性,所以我造了这么一个数来验证它。想法在前,因子在后,因子是想法的载体。
批量生成之后,顺序反了。你先让程序把算子和窗口的组合枚举一遍,几百个因子摊在面前,然后你去看哪个好看。看到好看的,再回过头去给它编一个道理。 这个「回过头编道理」的动作,人做起来毫不费力,而且编完你自己就信了——你会真心觉得这个二十三天的窗口对应着某种资金行为周期。
想法在前和因子在前,产出的东西长得一模一样,都是一条表达式加一份成绩单。但一个是证据,一个是巧合被追认成了理论。
第三,你以为的「一百条独立证据」,可能只是同一句话被重复了一百遍。 批量生成出来的因子高度同源——同一个量的五天版、十天版、二十天版,说的其实是同一件事,只是清晰度不同。它们摆在一张表上,看着像一百个各自独立的发现,其实彼此高度相关。这一层在 多因子合成 里讲得更细,这里只提醒一句:候选池的「个数」和候选池的「信息量」是两回事,而你排序时用的是个数。
系统层:整条流水线便宜,比造因子便宜更要命
现在看这套东西在真实系统里长什么样,以及为什么风险是从工程设计里天然长出来的。
便宜的不只是造因子,是从造到评的整条路
大家谈论批量挖因子时,注意力都在「生成」这一步——一个循环就能吐出几百条表达式。但真正让人上瘾的不是生成,是生成之后的那一整段路也全是自动的。
在 qlib 里,一次完整的研究被写成一个配置文件:用哪批股票、用哪套因子、切成哪几段、上哪个模型、训完之后跑哪几种评估、结果存到哪。你改一个字段,整条流水线重跑一遍,结束时自动给你一份带各项指标的成绩单。
再叠上缓存这一层——算过的东西不重复算,重跑的成本比首跑还低(这一层属于数据层的事,在 数据层为什么长这样 里展开)。
把这几件事加在一起,结论有点吓人:「看到一个漂亮的历史成绩」这件事,在工程上已经变成了一件可以按需批量购买的商品。 你想要多好看的曲线?多试几轮就有了。以前挡在你和「漂亮结果」之间的是体力和时间,现在这道墙没了。
这是本篇的核心工程判断。风险不来自任何一个功能有缺陷,风险来自整条路太顺了。
框架给了三道防线
值得肯定的是,这套系统并没有装作看不见这个问题。至少有三处设计是明确冲着它去的。
第一道:切分是配置的一部分,不是事后补的动作。
翻开任何一份基准配置,你会看到数据被明确切成了训练段、验证段、测试段三块,起止时间写死在文件里。旁边还单独写着预处理该在哪一段上拟合——也就是说,连「算标准化用的均值和方差」这种细节,都被限定只能看训练段。
这个设计的分量比它看上去重。它意味着在你动手之前,线就已经划好了,而且划在一个所有人都看得见的地方。你没法「跑完之后觉得这段不好看,把起点往后挪一年」——挪了会留在配置文件的改动记录里。相比之下,手工研究里的样本外切分往往是口头承诺,改了没人知道,连你自己第二天都想不起来。样本内外这道线到底在防什么,见 样本内与样本外。
第二道:成绩单不给一个数,一次给四个。
评估模块跑完之后吐出的不是一个孤零零的相关系数,而是一组:数值版的均值、数值版的稳定性、名次版的均值、名次版的稳定性。稳定性那两个是专门用来揭穿「靠某几天暴涨撑起全局」的因子的——一个平时贴着零线、靠三天飞升拉高均值的因子,在稳定性这一栏会原形毕露。这几个指标各自在说什么,IC 与 IR 那篇讲透了。
它还会把每天的原始序列一起存下来,而不只是留下几个汇总数字。这一点很关键:汇总数字会撒谎,原始序列不会。
第三道:每一次实验都能被自动记下来。
框架里有一整套实验记录的东西:跑一次实验,参数、指标、产出的中间文件都能自动落盘,之后可以把历史实验列出来、按条件检索、打标签归类。
理论上,这正是对付「分母不可数」的那件武器——只要每次尝试都留痕,你一共试了多少次就是可以查出来的。 手工研究里那个永远数不清的分母,在这里第一次有了变成可查数据的可能。
为什么这三道只能缓解,不能消除
好,现在讲这篇最想让你记住的部分。上面三道防线都是真的有用,但它们各自漏在哪里,值得一条条看清楚。
漏洞一:那段测试数据是公开的、固定的、大家共用的。
基准配置里的三段日期是写死的,而且几乎每一份基准配置用的都是同一组切分。这在工程上非常合理——不统一切法,几十个模型的成绩就没法横向比。
但请想一想它的另一面。第一个人用这段测试数据的时候,它是干净的样本外;第一百个人用它的时候呢?
这段数据的「新鲜度」是一种公共资源,而且用一次少一点。 每一个在它上面比较过模型、根据结果调整过做法、然后发表出来的人,都消耗掉了一点。等你读到那些公开成绩、照着最好看的那个方向去做的时候,你继承的是一整个社区在这段数据上反复看出来的结论。
最要命的是,这个消耗过程里没有任何人作弊。每个人都严格遵守了训练段/测试段的纪律,每个人的单次研究都无可指摘。可这些无可指摘的研究加在一起,就把这段数据看穿了。样本外之所以有效,唯一的前提是「没被看过」,而这个前提会被正当的、合规的使用慢慢耗光。
漏洞二:记录器记的是「你跑了什么」,不是「你想问什么」。
这个区分很细,但很致命。
你有一个想法:成交量的异动可能有预测力。为了验证它,你造了十二条表达式——五天窗口、十天窗口、二十天窗口,各配三种归一化方式。跑完十二次,挑了最好的那个。
在记录器眼里,这是十二条彼此独立的实验记录。而在真实的统计意义上,这是同一个问题被问了十二遍,你的分母是十二,不是一。
框架里没有「分母」这个字段,因为分母不是一个技术量——它是意图的计数。哪几次尝试属于同一个想法的不同写法,哪几次是真正不同的假设,这个判断只在你脑子里,任何自动记录都读不出来。它能忠实地记下你做了什么,但它永远不知道你为什么做。
漏洞三:记录是可以删的。
框架同时提供了删除实验、删除单条记录的能力。这是完全合理的工程功能——谁没跑坏过几次,谁的硬盘上没堆过一堆废弃产物。
但把它和上一条放在一起看,后果就出来了:最终留在你实验列表里的那些记录,天生是被筛选过的。 不需要你存心隐瞒,只要你顺手清理了那些「跑失败的」「参数写错的」「明显没用的」,分母就被削掉了一块,而且削得毫无痛感。
由此得到一条可以直接拿走的判断标准:任何一份能被事后编辑的记录,都不能充当分母的证据。 你自己的实验列表如此,别人给你看的实验列表更是如此。
漏洞四:稳定性指标本身,也可以被挑选。
这一条最反直觉,因为稳定性指标本来就是用来防挑选的。
设想你已经学乖了,不看均值了,改看稳定性——从八百个候选里挑那个「表现最稳」的。你觉得自己躲开了陷阱。
没有。你只是换了一把尺子来挑,挑选这个动作原封不动。在八百个纯随机的因子里,也一定有一个「稳定性」看起来最好的。 稳定性也是一个从历史数据上算出来的数,它同样由真实成分和运气成分构成,它同样可以被排序,被排序就同样会把运气推到顶端。
再往下一层:如果你先按均值筛一遍,再在剩下的里面按稳定性筛一遍,那是两次挑选叠加,不是一次挑选加一次验证。任何指标,一旦你用它来做「挑哪个」的决定,它就从检验工具变成了被优化的目标。 这条规律没有例外,也包括所有那些名字里带着「稳健」「防过拟合」字样的指标。
漏洞五:配置文件里那些四位小数,是一次你没参与的搜索留下的残渣。
翻基准配置里的模型参数那一段,你会看到一些带着四位小数的数值。
这些数字不是人想出来的。没有人会凭直觉写下一个四位小数。 它们是自动调参跑出来的结果——某次搜索里表现最好的那一组,被记下来固化进了文件。
于是这份配置文件有了双重身份。你打开它,看到的是一份界面友好、看起来经过深思熟虑的设定;而它实际上是一次搜索的胜出者名单,搜索过程本身不在文件里。你接手它、在它基础上继续做研究的时候,已经站在一堆你没数过的分母上了。
这个观察可以推广到你见到的任何一份「推荐配置」「最佳实践参数」。参数值越精确,越说明它是搜出来的而不是想出来的;而搜索的规模,通常不会跟着参数一起交付给你。
根子在哪儿:这些全是统计手段,问题却是认识论的。
把上面五条串起来,会看到一个共同的形状。
样本外检验之所以有效,靠的不是数学,是一个关于你的事实——你没看过这段数据。这个前提不在数据里,不在代码里,不在配置里,它在你的脑子里,在你的记忆里,在你团队过去两年的工作里。
而框架能触及的只有前三样。它可以把切分写死、可以算出稳定性、可以留下记录,但它没有任何办法知道你之前看没看过、看了几次、看完之后做了什么决定。它面对的永远只是一次孤立的调用。
所以这些防线的性质要看清楚:它们能防住手滑,防不住手痒。 防住的是「不小心把测试段混进训练」这类工程错误;防不住的是「反复地、诚实地、合规地在同一段数据上寻找答案」这个行为本身。后者不是错误,它就是研究,而研究做多了必然消耗数据。
顺着这条线,过拟合 那篇讲的是单个模型贴合噪音,本篇讲的是选择流程本身产生偏差——两件事经常一起发生,但哪怕你的每一个候选都简单朴素、单独看毫无过拟合迹象,只要你从八百个里挑第一名,本篇讲的这个问题照样成立。
那到底该怎么用它
说了这么多,结论不是「别用批量生成」。工具本身是中性的,而且横截面研究离了它基本做不动。真正该变的是你用完之后的姿态。
先写下判据,再按下回车。 在生成之前,把你要验证的假设、判定成立的标准、以及「如果不成立我就放弃」这三件事写下来。写下来之后再跑。这一步的全部意义在于:它让你没法在看到结果之后再决定用哪把尺子。
按「想法」计数,不按「因子条数」计数。 同一个量的十二个窗口版本,是一个想法,不是十二个证据。你真正在做的假设检验有多少个,这个数才是分母。
留一段从来不看的数据,并且接受它只能用一次。 这件事的难处不在技术,在忍。而且要认清一个事实:样本外是一次性用品。 你看了第一眼那叫检验,从第二眼开始,它就变成了你调整方案的对象。
把批量生成的产物当线索,不当证据。 排在最上面那几个因子,正确的用法是「值得我回头想想它为什么可能有道理」,而不是「它已经被历史验证过了」。这两种用法差着一整个仓位。
看别人的因子研究时,先问那个不在报告里的问题。 报告写了胜出者的所有细节,唯独不会写它是从多少个候选里胜出的。这个数问不出来,报告的可信度就得大打折扣——不是因为对方不诚实,而是因为这个数往往连他自己都没记。
失效边界与常见误用
以为「候选少就没事」。 挑选带来的虚高是连续的,不是一个开关。从三个里挑第一名,也有虚高,只是幅度小。真正没有虚高的只有一种情况:你事先指定了唯一一个候选,然后接受它的结果,不管好看不好看。
以为「有经济逻辑就免疫」。 人给任何结果编逻辑的能力都强得可怕。逻辑要值钱,必须是在看到结果之前写下来的。事后补的逻辑,不管多顺畅,都不构成任何证据。
把它当成一个可以彻底解决的技术问题。 它不是。所有办法都只能定价,不能消除。目标从来不是「让搜索变得无害」,而是「知道自己该往下折多少」。
只怀疑别人的排行榜,不怀疑自己那份。 看到网上的漂亮曲线,你的第一反应是「肯定挑出来的」;看到自己屏幕上的第一名,第一反应是「找到了」。这两者的生成流程,通常一模一样。
小结
- 从一大堆候选里挑出的第一名,它的历史成绩天然虚高,因为挑选会把运气一起挑上来,候选越多挑得越狠。哪怕候选全是纯随机的,第一名的成绩单照样漂亮。
- 批量生成真正危险的地方不在生成这一步,而在于从生成到出成绩单整条路都是自动的——「看到一个漂亮结果」变成了可以按需购买的商品。
- 框架给了三道防线:切分写进配置(防手滑改线)、成绩单同时给均值和稳定性(防靠几天暴涨撑场面)、实验自动留痕(让分母有变成可查数据的可能)。
- 三道防线各有漏洞:公用的测试段会被合规的使用慢慢耗光新鲜度;记录器记得住「跑了什么」记不住「想问什么」;记录可以删,所以留下的列表天生是筛过的;稳定性指标一旦被用来挑选,它自己就成了被优化的目标;配置里的四位小数是一次你没参与的搜索留下的残渣。
- 根子在于:样本外有效的前提是「你没看过」,而这个前提存在于你的记忆里,不在任何代码能触及的地方。这些机制能防住手滑,防不住手痒。
- 实操上只有一条硬办法:先写判据,再按回车;并且按「想法」计数而不是按因子条数计数。
一句话说完这篇:从一千个里挑出来的最好那个,好看是必然的,而它到底有几分本事、几分运气,只有你自己知道那一千是怎么来的。
本文基于开源仓库的源码与配置文件快照做概念讲解,未实际运行该程序,具体行为请以你手上那个版本为准。想接着看这套系统的其他部分,从 qlib 是什么 进,或看 模型分数怎么变成持仓。本站内容为投资教育,不构成任何投资建议,边界见 免责声明;本卷全部内容在 量化与 AI 投研卷。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。