模型吐出一列分数之后呢:从打分到持仓,中间还有一整套决定
大部分量化教程的结尾长得都差不多:数据洗好了,因子算出来了,模型训完了,评估指标也画了图——完。
然后你打开输出文件,看见的是这样一坨东西:几千行,每行一个股票代码,后面跟一个小数。0.0231、0.0187、-0.0044……
你盯着它,脑子里冒出一个很朴素的问题:所以呢?
这一列数字本身买不了任何东西。它不告诉你买几只,不告诉你每只买多少钱,不告诉你明天这列数字变了要不要跟着换。从「一列分数」到「账户里躺着哪几只、各多少股」,中间隔着一整套必须有人拍板的决定。这段路在教程里几乎总是被跳过,但它恰恰是最容易把前面所有努力糟蹋掉的一段。
先说人话:这就是招聘
你是个小公司的负责人,办公室里只有八张工位,现在八张都坐满了人。
某天来了个猎头,给你递了一份两百人的名单,每个人后面标了个分数,说这是他们对「适不适合你们公司」的评估。名单第一名的分数,比你现在最差的那个员工高一点点。
问题来了。
第一,你要不要为了这「一点点」换人? 换一个人不是零成本:招聘费、交接期、新人上手要两个月、老人走的时候还带走一堆没写下来的东西。如果新人只比老人好 3%,这笔账多半是亏的。
第二,一次换几个? 一次换一个,公司稳,但名单更新得快,你可能永远追不上最优配置。一次换五个,办公室直接瘫痪一个季度。
第三,多久看一次名单? 猎头说他每天都能更新。你每天照着新名单换人吗?
第四,新来的人给多少工位、多少资源? 平均分?还是分数高的多给?
第五,也是最容易被忘的:名单上排第一的那个人,他愿意来吗? 分数是猎头算的,人家可能压根不跳槽。名单上有他,不等于你招得到他。
这五个问题,一个都不能不回答。而你会注意到,猎头给你的那份分数表,一个都没回答。
投资里完全是同一回事。模型的输出叫预测分(或者叫信号、打分),意思是「模型认为这只票接下来相对好的程度」。它是一个排序依据,不是一个指令。把排序变成指令的那套规则,量化里叫策略层——具体说,是组合构建那一层。
投资层:分数到持仓,要拍板的四件事
把上面的比喻翻译过来,你要定的是这几件事。
持几只(宽度)。 只买分数最高的那一只,赌性太大——模型的判断本来就是概率性的,单只票上模型说的话基本没有意义。买五百只,那你基本就是买了个指数,模型选出来的那点区分度被稀释光了。这个数往哪边挪,都在牺牲一样东西。
多久重算一次(频率)。 分数每天都在变,但你不必每天都跟着变。这件事牵涉的东西比直觉多得多,单独有一篇讲——调仓频率,它同时压着交易成本、容量和信号新鲜度,往哪边拧都要还债。
每次换几只(换手强度)。 这是最被低估的一个旋钮。频率决定「多久看一次」,换手强度决定「看完之后动多大」。同样是每周看一次,每次只换一只和每次全换掉,是两套完全不同的策略。
每只放多少钱(权重)。 等权?按分数加权?按波动率倒数加权?以及——留不留现金?
四件事定完,你才有一个可执行的东西。少定任何一件,程序都跑不起来。
系统层:qlib 是怎么把这四件事写成代码的
qlib 里有一个专门干这件事的模块,名字直白得可爱:TopkDropoutStrategy(在 qlib/contrib/strategy/signal_strategy.py 里)。名字本身就是它的全部思路——Topk(取前 k 名)+ Dropout(每次踢掉几个)。
它对外只有几个旋钮,但每一个旋钮背后都藏着一个上面提过的决定。
topk 是持仓只数。n_drop 是每个调仓日最多替换几只。这两个数一定,宽度和换手强度就都定了。
有意思的地方在于,它并不是「简单粗暴地把持仓里分数最低的 n_drop 只卖掉,换成候选里分数最高的 n_drop 只」。如果这么干,会出一个很蠢的事故:你手上排名第八的票,可能比外面排名第九的候选还要好,结果你把好的卖了、买了差的,白付一趟手续费。
源码里处理这件事的方式,是先把「现有持仓」和「今天想买的候选」合成一张大表,重新排一次序,然后只卖那些掉到这张大表末尾的:
# combine(new stocks + last stocks), we will drop stocks from this list
# In case of dropping higher score stock and buying lower score stock.
comb = pred_score.reindex(last.union(pd.Index(today))).sort_values(ascending=False).index
# Get the stock list we really want to sell (After filtering the case that we sell high and buy low)
sell = last[last.isin(get_last_n(comb, self.n_drop))]
注释写得比代码还清楚:合并再排序,就是为了避免「卖高买低」。
这里有一层含义值得停一下:n_drop 是上限,不是配额。 如果今天外面的候选都不如你手上现有的,合并排序之后末尾那几个全是候选而不是持仓,那么 sell 这个列表就会是空的——这一天你什么都不用换。真正卖掉几只,是算出来的,不是设定的。很多人把换手率理解成「我设定它每周换 20%」,而在这个实现里,换手是排序竞争的结果。
买入数量也是跟着实际卖出数量走的:
buy = today[: len(sell) + self.topk - len(last)]
len(last) 是当前持仓数。这一行保证了持仓只数守恒:卖了几只补几只,仓位不满就补到满。策略不会因为某天信号特别好就多买,也不会因为信号差就空仓——它永远端着 topk 只票。这是个很强的设定,后面会说它的代价。
权重:这里有个容易看漏的细节
买入金额的计算只有一行:
value = cash * self.risk_degree / len(buy) if len(buy) > 0 else 0
risk_degree 是「总资产里拿多少比例出来投」,代码里的默认取值是 0.95,也就是留一点现金不满仓(这是当前版本的默认值,可配置)。留这一手不是为了择时,是为了给撮合留缓冲——买入价、手续费、最小交易单位取整,全都可能让你算出来的钱不够用。
真正容易看漏的是除法那部分:可用现金在今天新买的那几只之间平分。
注意,这不是把整个组合重新拉回等权。老仓位一动不动。也就是说,如果你半年前买的某只票翻了一倍,它在组合里的权重就是别人的两倍,这个策略不会把它削回来。它是「新钱等权」,不是「组合等权」。
这个区别不小。真按等权跑,赢家会被持续卖出、输家被持续买入,那是一种偏均值回归的隐性下注;而让权重自然漂移,等于对赢家持续加注。同一份分数,配上不同的权重规则,长期表现能差出一大截——而这一层跟你的模型好不好,一点关系都没有。
名单上有,不等于你买得到
前面招聘比喻里的第五个问题,源码里也有对应的处理,而且是两个独立的开关。
only_tradable:选股的时候要不要先过滤掉当天不能交易的票。打开它,挑候选时就跳过停牌的;关掉它,策略会照着分数排名往下选,选到停牌股才在下单环节被拦掉。这两种做法给出的名单可以完全不同。
forbid_all_trade_at_limit:涨跌停时是否禁止一切交易。这里有一处很诚实的地方——源码顶部的 TODO 列表里,作者自己写着这个默认值和现实不一致(现实中涨停是可以卖出的),只是改了会导致历史结果对不上,所以留着(当前版本的默认取值,可配置)。
这类开关看着琐碎,但它们决定的是同一件事:你的回测里,那些「买不到的收益」有没有被算进去。 一个不检查可交易状态的回测,会把大量涨停开盘、你根本抢不到的票算成你的持仓。这不是模型的功劳,是撮合假设送的。相关的坑在 撮合与成交假设 里有系统的讲法。
还有一个小细节:hold_thresh 是最短持有期,卖出前会检查这只票已经拿了几天,不够就跳过这次卖出。它的作用是防止信号抖动导致的来回打脸——今天分数掉出去卖了,明天又涨回来买回来,两趟手续费换了个寂寞。
分数是哪天的分数
还有一行值得拎出来:取信号的时候,用的是上一个时间步的预测:
pred_start_time, pred_end_time = self.trade_calendar.get_step_time(trade_step, shift=1)
pred_score = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
今天要交易,用的是昨天收盘后算出来的分数。这不是随手写的,是纪律。如果这里图省事用了当天的分数,整套东西就废了——你在用今天的信息决定今天开盘的动作,这就是前视偏差,它会让你的历史成绩好看得离谱,然后在真钱上一分不剩地还回去。
另一种答案:不给名单,给权重
TopkDropoutStrategy 的思路是「输出一张名单」。qlib 里还有另一条路:直接输出目标权重。
WeightStrategyBase 要求你实现一个「给我分数和当前持仓,我告诉你目标权重长什么样」的函数,剩下的事交给一个订单生成器——它拿目标权重和当前持仓做差,差多少下多少单。
这条路的表达力强得多。比如 EnhancedIndexingStrategy 走的就是这条路:它不再是简单排序取前几名,而是把「跑赢指数」和「别偏离指数太远」当成一个带约束的优化问题来解,需要额外准备风险模型数据(因子暴露、因子协方差、特质风险这些),并且支持给换手加一个显式的上限。
两条路的差别,本质上是**「排序思维」和「组合思维」**的差别:
| 排序思维(Topk + Dropout) | 组合思维(目标权重 + 优化) | |
|---|---|---|
| 输出 | 一张名单 | 一组权重 |
| 换手怎么控 | 每次最多换几只 | 优化时加约束项 |
| 风险怎么控 | 靠分散(持够只数) | 显式建模,约束偏离度 |
| 需要什么 | 只要分数 | 分数 + 风险模型 |
| 谁在用 | 研究阶段、验证信号有没有用 | 要对标基准、有明确跟踪误差要求时 |
排序思维的优点是看得懂。出了问题你能一眼看出是哪只票拖的后腿。组合思维的优点是能把「我想要什么、不想要什么」写清楚,代价是多了一堆需要估计的输入,而每一个估计都可能错。
不用急着觉得后者更高级。研究阶段用简单的排序策略,恰恰是为了让你看清楚分数本身有没有价值——如果加了一堆优化才有超额,那超额多半来自优化器而不是模型,这是两件事。
为什么这一段最容易白干
这才是本篇最想让你记住的。
从分数到持仓这一段,是纯成本的一段。 模型那边的所有工作是在创造 alpha,而这一段的每一个动作都在花钱:手续费、印花税、买卖价差、你的单子推动价格产生的滑点。分数的区分度是有限的,而交易成本是确定的。当 n_drop 调大、频率调快,成本是线性往上涨的,可分数带来的那点优势并不会同步变大。
于是一个很常见的结局出现了:因子评估指标漂亮,回测收益一样漂亮,但把成本假设调到接近现实的水平之后,超额收益整个消失。问题不在模型,在从分数到持仓这一段,你换手换得太勤。
持仓只数守恒,意味着你永远满仓。 前面说 buy 的数量绑定 sell,保证仓位不空。这个设定隐含了一个假设:策略只做相对选择,不做「今天全场都不值得买」的判断。牛熊都端着同样多的票。这不是 bug,是这类策略的定位——它赚的是排序的钱,市场整体的涨跌不归它管。你如果指望它躲开系统性下跌,那是找错了工具,那件事得靠别的层来做。
排名靠前的票,往往正是你最买不动的票。 模型偏好的常常是小票、冷门票、刚被消息炒起来的票。分数上它们排第一,真金白银下去,一笔单子就能把价格打上去几个点。名单越集中,这个问题越严重。这属于策略容量的范畴——同一套逻辑,一百万能跑,一个亿跑不了,本库里流动性与容量单独有一篇。
别把 topk 和 n_drop 当参数去优化。 这两个数一调,回测收益就会动,于是很容易被拿去网格搜索一遍,挑一组最好看的。但它们不是模型参数,是你对「持多少只算够分散、多久动一次手算合理」的判断。用历史成绩来选它们,选出来的必然是最合身于那段历史的组合方式,换一段时间就散架。更麻烦的是这类调整看起来特别合理——你调的不是模型,是「策略设置」,心理上完全没有过拟合的负罪感。
失效边界与常见误用
以为策略层能救回一个没用的模型。 救不了。分数如果没有区分度,怎么排、排几名、换几只,结果都是随机的。策略层能做的是「别把有区分度的分数糟蹋掉」,做不到无中生有。
回测里换手很低,就以为实盘也会低。 实盘的分数噪音比回测大得多——数据延迟、行情异常、模型偶尔抽风,都会让排名剧烈跳动,进而制造出回测里不存在的换手。最短持有期这类机制存在的意义就在这儿。
把持仓只数和风险分散画等号。 持二十只票,如果这二十只全在同一个板块、同一种风格上,那它们会一起涨一起跌,分散是假的。持仓只数只解决「单只出黑天鹅」的问题,不解决共同暴露的问题。
忽略「买不到」和「卖不掉」。 名单是理想世界的产物,停牌、涨跌停、流动性不足,全都会让实际持仓和名单对不上。回测里这一层假设越宽松,回测越好看,落差也越大。
觉得权重规则无所谓。 前面说过了,等权、新钱等权、分数加权,是三种不同的隐性下注。同一份分数配不同权重规则,长期结果差出来的部分,跟你的模型没有任何关系。
小结
- 模型输出的分数是排序依据,不是交易指令。中间必须有人回答四件事:持几只、多久重算一次、每次换几只、每只放多少钱。
- qlib 的
TopkDropoutStrategy是这套决定的一个具体实现:topk定宽度,n_drop定换手上限;它把现有持仓和新候选合并重排,只卖掉落到末尾的那些,避免「卖高买低」;买入数量绑定实际卖出数量,持仓只数守恒。 n_drop是上限不是配额——真正换几只是排序竞争的结果,可能是零。- 权重那一行只把现金在当天新买的票之间平分,老仓位不动。这是「新钱等权」而非「组合等权」,两者的长期含义完全不同。
- 名单上有,不等于买得到。可交易状态、涨跌停、最短持有期这些开关,决定了你的回测里有没有混进「买不到的收益」。
- 另一条路是输出目标权重、交给优化器,表达力更强但需要风险模型这类额外输入;研究阶段用简单排序,反而更容易看清分数本身有没有价值。
- 这一段是纯成本段:模型创造优势,这一段花钱。换手调得越勤,成本涨得越快,而分数的优势不会同步变大——很多策略的超额收益就是死在这儿。
一句话记住这篇:打分的是模型,掏钱的是你——分数只告诉你谁看起来更好,没告诉你值不值得为这点「更好」付一趟手续费。
本文所引源码均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;文中提到的默认取值随版本变化,请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见 免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。