← 返回教程库

模型吐出一列分数,它是怎么变成持仓的

最后更新 2026-08-19
📚 量化与 AI 投研 📖 仓库导览 · qlib ⏱ 约 16 分钟 R2 · 注意风险
你将学到
  • 能说清「定名单」「算差额」「真去交易」是三件不同的事,以及把它们混在一起会出什么错
  • 能解释为什么模型给的分数只有排序含义、没有量纲,以及这限制了你能用哪类策略
  • 能说出「少动」为什么本身就是一种收益来源,而不只是省手续费

有个瞬间大概你也经历过:教程终于跑到最后一格,屏幕上打出一张表,几千行,每行一个代码加一个小数。你心想,行吧,那就买分数最高的那五十个呗。

然后你去翻这个框架里「策略」两个字底下的目录,发现事情不太对——那里不是一个文件,是好几个,各自还在干不太一样的活。有的在算「我想要的组合长什么样」,有的在算「跟现在比差多少」,有的干脆在管「这几笔单子今天分几次下」。你原以为是一行代码的事,被拆成了一条小小的流水线。

这篇就讲这条流水线。这段路上每个角色都在悄悄改动你的想法,而改动的地方,恰恰是很多人回测和实盘对不上的原因。

关于「一列分数为什么不能直接当交易指令」这个概念本身,本站的从分数到持仓已经讲透了,这里不重讲。本篇只看一件事:一个真实系统把这段路切成了几段,为什么这么切。

先想一次大扫除

周末你要把客厅重新摆一遍。这件事在你脑子里是一件事,但真做起来是三件。

第一件,画出理想的样子。 沙发靠窗,书架挪到墙角,茶几换个方向。这一步只关心「摆成什么样最好」,不关心怎么搬。你画这张图的时候是不会去想沙发有多沉的。

第二件,对着现状列出「谁要挪到哪」。 这一步才是真正的麻烦。屋里就这么大空地,你不可能同时把所有东西都悬空——得先搬走一件,才腾得出地方放另一件。所以哪怕理想图上只有三处不同,实际执行顺序也可能有七八步。而且你会发现有几个柜子是钉死在墙上的,图上画得再好看,它就是不能动。

第三件,真的去搬。 沙发一个人抬不动,得分两趟;下午楼道在装修,搬不了;搬到一半发现新位置的插座被挡住了。

这三件事你在脑子里是混着想的,所以经常出岔子:画图的时候没考虑柜子钉死,列步骤的时候没考虑空地不够,真搬的时候才发现全盘要改。

一个把这三件事写成代码的系统,没法混着想。它必须把它们拆开,明确谁负责哪一段、谁向谁交货。拆开之后你才看得见:混着想的时候,你到底跳过了什么。

投资层:这三段各自在回答什么问题

翻译到投资上,这三段分别是三类完全不同的判断。

第一段回答「我希望账户里是什么样」。 持哪些、各占多少比重。它是一个静态的、理想的目标状态,跟你现在手上有什么无关。这一段的输入是模型给的分数,输出是一张「理想图」。

第二段回答「从现在这样,怎么走到那样」。 这一段最容易被跳过,也最容易出事。因为它要处理的全是现实约束:你现在手上有的东西值多少钱、其中有多少是现在就能变现的、卖出去要扣多少费用、买进去还要留多少余地。它的产出不是「理想图」,是一串具体的买单和卖单

第三段回答「这几笔单子今天怎么下」。 一次性砸进去,还是分几笔慢慢来?这一段跟模型好不好完全没关系,但它决定你实际拿到什么价格。

多数人的错,是把第一段的产物直接当成了第三段的结果。你看着理想图说「我这个组合今年涨了 20%」——那是理想图涨了 20%,中间两段每一段都在给这个数字打折。

再说一句更扎心的:理想图是免费的,后两段全都要花钱。 你在第一段里想得再精妙,收益也得靠后两段兑现,而后两段只会做减法。

系统层:qlib 把这三段交给了谁

现在看这个开源框架是怎么切的。

它的选股策略层有两种口径,对应两种「理想图」的画法。一种是直接给名单:按分数排序,取前几名,每期只替换掉掉队的那几只。另一种是给权重:你实现一个函数,告诉系统「在这个时刻,我希望每只票各占总资产的百分之几」——系统不问你怎么算出来的,只要这张表。

给权重这条路,产物只是一张理想图,本身还不能下单。所以它后面必须挂一个专门的角色:订单生成器。这个角色拿两样东西——你的理想图,和你此刻真实的持仓——做差,然后吐出一串委托单。

再往后是执行层。qlib 里还有一类完全不看分数的策略,它们只干一件事:把已经定下来的单子,按某种节奏拆开下出去(比如在一段时间里均匀地分批成交)。这类策略可以嵌在选股策略下面,形成「上面按天决定买什么,下面按更细的粒度决定怎么买」的两层结构。

三个角色,三种职责,交货物不同:第一个交理想图,第二个交订单,第三个交成交。

这个切分本身就是一句话的翻译:选股能力和交易能力是两种能力,不该混在一个人身上评价。一个组合表现不好,可能是选股差,也可能是选得对但买得贵。不拆开,你永远分不清。

第一个坑:分数只有排序,没有量纲

这一段是我最想让你带走的。

模型输出的那一列小数,长得很像收益率。0.0231,看着就像「预期涨 2.31%」。它不是。

qlib 的文档里专门交代了这件事,而且交代得很硬气:框架不给预测分做统一缩放。理由给了两条——一是不是所有策略都在乎数值大小(只按排序取前几名的策略,分数是 0.02 还是 20 都不影响结果);二是模型的训练目标、损失函数、数据处理各不相同,没有一种通用办法能把输出还原成有意义的量。文档甚至给了个务实建议:真想还原成可解释的数值,你得拿模型最近的输出和你真实的目标值再单独做一次拟合。

这件事的后果比它听起来严重。

如果你用的是只看排序的策略,没事。 排序不受缩放影响,分数是什么量级都无所谓。

如果你用的是需要求解的组合优化策略,就出大事了。 那类方法要在「预期收益」和「风险」之间做权衡,它必须知道 0.02 到底是 2% 还是 20%。你喂给它一列量纲不明的数,它照样会给你算出一个漂亮的解——一个建立在错误尺度上的、看起来非常专业的解

这就是为什么框架宁可什么都不做,也不替你猜。在一个诚实的系统里,「我不知道」比「我猜一个」有价值得多。 它把这个决定顶回给你,是因为只有你知道自己的模型在预测什么。

顺带说一句,这也解释了为什么研究阶段大家都爱用最简单的排序策略:它对分数的要求最低,所以它测出来的东西最干净——如果连排序都赚不到钱,那分数就是真没用,跟后面任何一层都无关。至于分数是怎么造出来的,那是因子生成那一层的事,这里不展开。

第二个坑:能动的钱,不等于你有的钱

回到大扫除的第二段——「先腾地方」。这一段在代码里的样子,比你想的琐碎得多,而每一处琐碎都对应现实里一个会让你亏钱的地方。

订单生成器要算「我现在到底有多少钱可以调动」。这个数不是账户总资产,它至少被扣了三刀。

第一刀,不能交易的部分不算数。 系统会分别算两个值:一个是持仓的总市值,一个是其中可交易部分的市值。停牌的、封在涨跌停里的,市值照样在你账上,但你没法把它变成钱去买别的。理想图上让你减仓的那只票,如果今天动不了,那它释放不出任何购买力。

这一刀砍掉的东西,就是很多回测虚高的来源。一个不区分这两个值的回测,等于假设你的持仓随时可以全部变现——撮合与成交假设里说的正是这类事。

第二刀,预留现金。 系统会按你设的投资比例,先把一部分资产划出去不动。留这一手不是为了择时,是为了给现实留缓冲:成交价跟你估的不一样、手续费、最小交易单位取整,任何一处都可能让你算出来的钱不够花。

这里有一个很有画面感的极端分支:如果把能卖的全卖了,都还凑不够那笔要预留的现金,程序不会报错,它会走另一条路——把所有能卖的都卖掉。翻译成人话就是:当你的组合已经被套牢到连留出安全垫都做不到时,唯一的动作是全面撤退。 这是一段写在代码里的、很冷静的风控。

第三刀,成本预扣。 算可用金额的时候,系统会先按交易成本率打个折再去分配。理由很朴素:你有一百块,不能买满一百块的东西,因为买的动作本身要花钱。人做这一步最容易忘——算完仓位发现现金不够,只好临时少买一点,于是实际持仓和你算的那个又差了一截。

三刀砍完,剩下的那个数才是真正能用来实现理想图的钱。这也是为什么实际持仓永远不会正好等于理想图。它们之间的缝,不是 bug,是现实。

第三个坑:一天之内,谁先谁后也是假设

订单生成器最后要把差额变成一串委托单。这里有两处细节,各自藏着一个假设。

第一处:卖单永远排在买单前面。 生成的时候买卖是分开攒的,最后返回的时候,是卖单列表拼上买单列表。

# return order_list : buy + sell
return sell_order_list + buy_order_list

(注释写的是 buy + sell,代码是先 sell 后 buy——这种小小的不一致在真实项目里到处都是,看代码不看注释是个好习惯。)

先卖后买不是随手排的,它对应现金约束:你得先把钱收回来,才有钱花出去。 你手工操作时也是这样——想换仓,总得先卖掉旧的。但你在纸上算收益的时候,往往默认这两件事同时发生。它们不同时发生,中间那段时间你是空着一部分仓的,市场不会等你。

第二处,更有意思:同一天里那几十笔单子,谁排第一?

这个问题看着无聊,其实无解。因为钱是有限的,排在前面的单子先花钱,排在后面的可能就没钱了——顺序会影响结果。可现实里并没有一个「正确的顺序」。

源码里的处理方式是:把涉及的代码先排序,然后用一个固定的随机种子打乱。旁边的注释解释得很直白——集合的遍历顺序不固定,会导致同样的参数跑出不同的回测结果;排序再用固定种子打乱,是为了让顺序既不带偏向、又每次都一样。

停在这里想一下。这是回测这件事的一个缩影:系统遇到了一个「必须选一个、但没有正确答案」的岔路口,它选了一个能复现的做法。选择本身没问题,问题是——这个选择进了你的回测结果,而报告上不会写它。

你的回测里有多少个这样的岔路口?开盘价还是收盘价成交、涨停能不能卖、除权当天怎么算、同一天多笔单子谁先谁后……每一个都被某人替你决定了一次。你看到的那条净值曲线,是这些决定的合集。这就是为什么两个人用同一份分数、同一套规则,能跑出差很远的结果,而且谁都没作弊

为什么「少动」本身就是一种收益来源

现在说这条线里最值钱的一句话。

按分数排序取前几名,最偷懒的做法是每期把名单整个重排一遍:昨天的前几名扔掉,换成今天的前几名。这么干,最坏情况下你每期换掉全部持仓。

qlib 的做法不是这样。它每期只替换掉队的那几只,持仓的大部分原地不动。官方文档给了这个做法的换手率量级——大致是「每期替换的只数」的两倍,除以「总持仓只数」。(换手要乘二,因为卖一只加买一只是两笔交易。)

这个式子里没有魔法,但它把一件事说清楚了:换手率是你自己拧出来的,不是市场给的。 持仓只数越多、每期替换得越少,换手就越低。

那少动为什么是收益来源?关键在两边的性质不对称:

成本是确定的,优势是概率性的。 你每换一只票,手续费、印花税、买卖价差、你的单子把价格推走的那部分——这些一分不少地当场发生,百分之百会发生。而你换的理由是「新的那只分数更高」,这个「更高」只是概率意义上的更高,而且模型的区分度本来就薄。用一笔确定的支出,去换一份概率性的、幅度可能只有零点几个百分点的优势,这笔账在多数情况下是亏的。相关的账怎么算,交易成本假设里有系统的讲法;换手频率往哪边拧要还什么债,看调仓频率

所以「少动」省下来的不只是手续费,它省下的是一整串本来就期望为负的赌注。不下这些注,等于净赚。

这个思路在框架里还有更彻底的一种实现。它给「每只票在一次调仓里权重最多能变多少」加了一个上限——不管理想图跟现状差多远,一次最多挪这么多。这意味着一件很反直觉的事:你可能永远到不了理想仓位。

而这是故意的。理想图在这类设计里不再是一个要达成的目标,它降级成了一个方向。你朝它走,但走多快另有一套规矩管着。为什么敢这么设计?因为理想图本身就是有噪音的——分数天天在抖,今天的理想图和明天的理想图可能差挺多。你要是每次都追到位,追的大半是噪音,付的却是真钱。

这里面还藏着一层:这个上限同时也在护着你不被自己的规模所伤。持仓越集中、单只挪得越猛,你的单子对价格的推动就越明显——那正是流动性与容量会卡住你的地方。给单次变动设个天花板,等于强制自己慢慢来。

对着自己的操作想一想:你有没有过「看了一眼榜单,觉得手上这只不如那只,就换了」的时候?换的理由通常都成立,问题在于它太便宜了——你只需要动一下手指,成本却是真金白银当场付掉的。给自己的换手加一个上限,是普通人能从这类系统里抄走的、最不需要技术的一条纪律。

这一层管不着的事

把职责边界说清楚,比说它能干什么更重要。

它不产生观点。 分数从哪来、好不好,这一层一概不问。给它一列随机数,它照样一丝不苟地给你排序、算差额、生成订单、控制换手。整条流水线会安静地、专业地执行一个没有任何信息量的输入。

它不判断模型是不是过期了。 今天用的分数是昨天那个模型算的,那个模型是什么时候训的、还灵不灵,这一层看不见。模型会随时间失效,重训要怎么排进流程,是在线更新那一层的事。

它不做择时。 按名单走的那类策略,持仓只数是守恒的——卖几只补几只。它永远端着同样多的票,牛熊一个样。它赚的是排序的钱,市场整体涨跌不归它管。指望它帮你躲开系统性下跌,是找错了工具。

它不保证成交。 生成订单是一回事,成交是另一回事。停牌、涨跌停、流动性不足、你的单子把价格推走,全都在这一层之外。

想看这些角色在整个流水线里各自站在哪个位置,qlib 是什么那篇有一张全景。

顺便提醒一句:这类框架里但凡涉及自动下单的部分,都不是「配好就能撒手」的东西。程序不会累但也不会怀疑自己,一个写错的规则会被百分之百忠实地、不知疲倦地执行下去。真要往真钱方向走,风险敞口、断线处理、异常停手这些事,一件都不能省。

小结

  • 「从分数到持仓」在真实系统里不是一件事,是三件:画理想图、算差额生成订单、真去交易。混着想,你就看不见自己跳过了什么。
  • 模型给的分数只有排序含义,没有量纲。只看排序的策略无所谓;需要求解的优化类策略如果拿到量纲不明的数,会算出一个建立在错误尺度上的、非常专业的答案。框架宁可不猜,把这个决定顶回给你。
  • 「能动的钱」被砍了三刀:不可交易的部分不算数、预留现金要划走、交易成本要预扣。所以实际持仓永远不等于理想图——那道缝是现实,不是 bug。
  • 卖单排在买单前面(先收钱才有钱花);同一天里几十笔单子的先后顺序无解但会影响结果,系统只能选一个能复现的做法——你的回测曲线里,混着一堆这样的、报告上不会写的选择
  • 「少动」是收益来源,因为成本是确定的、优势是概率性的。只换掉队的那几只而不是整体重排,以及给单次权重变动设上限,都是在拒绝一串期望为负的赌注。
  • 这一层不产生观点、不判断模型是否过期、不做择时、不保证成交。

一句话说完这篇:分数只告诉你谁看起来更好,剩下的全是搬家的活——先腾地方、再算钱够不够、能搬的才搬,而且慢慢搬比一次搬到位更划算。

本文基于上述源码与文档快照做概念讲解,未实际运行该程序;文中不涉及任何具体标的,也不构成投资建议,边界见免责声明。想系统看这一卷的其他内容,回到量化与 AI 投研卷

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明