← 返回量化与 AI 投研概念库

模型会过期:找到一个好模型,其实才刚开始

最后更新 2026-08-18
R2 · 注意风险

你折腾了三个月,终于把一套东西调到自己满意:历史数据上跑得住,留出来没碰过的那一段也跑得住,换个时间段重新调一遍参数,结论还是差不多。你长舒一口气,觉得这件事总算做完了。

然后你把它挂上去,让它每天自己算。

前两个月还行。第三个月开始,你发现它给出的判断越来越经常地"差一点"——不是错得离谱,是那种钝钝的感觉:以前它挑出来的东西大多能跟着涨一阵,现在挑出来的东西涨一半就熄火。你翻代码,没 bug;你翻数据,没缺失。什么都没坏,但它就是不如从前了。

这不是你的错觉,也不是你运气不好。它就是会这样。一个模型不是一件家具,买回来放那儿就行;它更像牛奶,出厂那一刻就开始倒计时。

先说人话:它到底是件什么事

先把"模型"这个词落到地面。这里说的模型,就是一台从历史里总结出来的、专门做同一件事的机器:你把一堆能算出来的数喂进去(成交量的变化、价格的形态、财报里的比率之类),它吐出一个判断。它总结的依据只有一样东西——过去发生过的事

现在想象一件生活里的事。

你在一个小区门口开了家早餐店。开店前你蹲了两个月,记下来每天几点人最多、大家买什么、什么价位卖得动。这两个月的观察,就是你的"训练数据";你根据它定下来的备货表和出摊时间,就是你的"模型"。头半年生意很好,因为小区还是那个小区。

一年后,地铁口挪了,早高峰的人流从东门改走西门;隔壁开了两家新店;小区里搬进来一批年轻租户,口味跟原来那批上班族不一样。你的备货表一个字都没改,它还是那张按去年的世界画出来的表。它没有出错,它只是在描述一个已经不存在的地方

这就是模型衰减:不是模型坏了,是它当年学的那个世界变了。这件事在学术上有个名字叫概念漂移(concept drift,指数据背后的规律本身随时间改变),但你不用记这个词,记住那张过期的备货表就够了。

qlib 那份关于"动态市场"的示例文档,开头一句话说的就是这个意思:金融市场的环境是非平稳的(说人话就是:它的统计规律本身会变),不同时期的数据分布会不一样,所以在旧数据上建起来的模型,放到未来的数据上,表现会衰减。注意它用的词是"会",不是"可能会"。

对你的决策意味着什么

第一件事:"我找到了一个好模型"这句话,天然缺一个时间状语。 完整的说法是"我找到了一个在某段历史上好的模型"。这两句话的差距,就是你后面所有麻烦的来源。你以为你交付了一个成品,其实你交付的是一个需要被持续供养的东西。

第二件事:你真正要管的不是那个模型,是产出模型的那条流水线。 这是个视角上的翻转,也是本篇最想传达的东西。如果模型注定会过期,那么"这个模型多好"就不是最重要的问题了,"我用多快的速度、多稳的质量,持续造出新的模型来替换它"才是。前者是一次性的成绩,后者是一个可以一直跑下去的能力。

第三件事:衰减和失效不是一回事,处理方式也完全不同。 如果你的信号只是因为市场分布慢慢挪了位置而变钝,重新训练一遍多半能把它拉回来;但如果它是因为规律本身已经不成立了——比如那个价差被越来越多人盯上、赚它的空间被磨没了——那你重训一百遍也只是让模型更快地追上一个已经走掉的东西。前一种叫衰减,后一种叫失效。怎么区分,信号衰减:好因子为什么会失效 那篇是从因子的角度讲的,两者是同一件事的两个切面。

第四件事,最容易被忽略:重训是有代价的。 新模型和旧模型对同一批标的的看法不会完全一样,换模型那一刻,你的持仓就要跟着动一次。换得越勤,交易越多,手续费和冲击成本就越高。这笔账不算清楚,你可能会得到一个"每周都用最新模型"的漂亮方案,然后被成本吃掉全部改善。这一层的账怎么算,调仓频率 那篇讲得更细。

系统层:跑起来之后,每天到底在做什么

前面这些话听着都对,但太虚。真正让人明白的方式是看一眼真实系统在"模型已经上线"这个阶段每天做的动作——你会发现它比想象中琐碎得多,而琐碎的地方正是坑所在。

需要先划清一条界:把历史切成一串窗口、每个窗口重新训练一次,那是离线评估,用来判断"这套流程行不行",滚动前进验证 那篇讲的是它。本篇说的是上线之后——同样的动作真的每天在发生,而且每一步都不可逆。qlib 自己在滚动模块的注释里把这两件事分得很干净:滚动模块只负责离线地把一个任务按时间切成一串任务,它和"在线服务时不断更新模型"是两回事,共用的只是任务生成那一层。评估阶段你可以回头重来,运行阶段不行。

一个循环,五个动作

qlib 的在线管理器把日常循环写成了一个叫 routine 的方法,注释里把顺序交代得很直白:更新预测 → 准备任务 → 准备在线模型 → 准备信号。展开成人话是这么五步:

  1. 数据到了新的一天(这一步是你自己的事,文档专门强调用户必须保证数据源是更新的——数据不更新,后面全是空转);
  2. 检查按你定的节奏,是不是该造新模型了。该,就生成新的训练任务并训练;
  3. 从训练好的模型里挑出哪些"上线",哪些"下线";
  4. 把当前在线的这些模型的预测,往前补到最新一天;
  5. 把多个在线模型的预测合成一个最终信号(默认是把它们平均起来,可以换成别的合成方式)。

值得停一下的是顺序。代码里更新预测这一步被特意放在最后,注释写得很清楚:因为在上面几步里在线模型可能已经变了,所以更新预测必须是最后一步。这个细节很小,但它揭示了一个真实系统里的核心矛盾——"今天用哪个模型"和"今天的预测是什么"是两个会打架的状态,谁先谁后必须钉死。你自己搭一套的时候如果没想过这件事,很可能某天用新模型覆盖了旧模型算出来的一段历史预测,而你完全不会察觉。

预测是补出来的,不是每天重算的

这一点很多人第一次看会愣一下:新的一天来了,系统不会把整段历史的预测重算一遍,它只算缺的那一小截,接在后面。

听上去是纯粹的性能优化,其实不是。它带来一个必须处理的麻烦:有些模型的输入天然依赖过去若干天的数据(比如它要看最近 N 天的序列),那么要算出"今天"这一格,就得往前多取一段数据当引子。qlib 的更新模块把这个长度叫历史依赖,并在注释里说明:起点不包含在这段依赖长度里,所以大多数情况下它等于序列长度减一。它还会在你没指定的时候,试着从数据集本身把这个长度推断出来。

另一处很实在的防呆:如果你要求把预测更新到一个比数据日历上最后一天还晚的日期,它会警告一句然后把目标日期截回到最新的那一天,而不是傻傻地往前算。这是在保护你不去预测一段还没有数据的时间。

if to_date >= latest_date:
    self.logger.warning(
        f"The given `to_date`({to_date}) is later than `latest_date`({latest_date}). "
        f"So `to_date` is clipped to `latest_date`."
    )

顺带说一句,这一整块能成立的前提是"缺哪一截补哪一截"这件事本身不能出错。补的时候多取了一天、少取了一天,或者把新模型算的结果盖到旧时间段上,都会污染你的历史记录,而这类污染事后极难发现。

谁在线,是一件要被记账的事

真实系统里,模型不是"用"或"不用"两种状态,它有一个明确的标签在身上。qlib 的在线工具用一个键记录每个模型的状态,取值只有在线和离线两个(离线的意思就是:训练过,但不参与实盘服务)。切换的时候是先把所有模型置为离线,再把选中的那批置为在线。

同时,管理器会维护一份历史账本,结构是"哪一天 → 哪个策略 → 当时哪几个模型在线"。这份账本的用处不在跑的时候,在事后复盘的时候。三个月后你看到一段成绩很差的区间,第一个要问的问题就是"这段时间是哪个模型在当值"。没有这份账,你根本无法把结果归因到具体的模型上,只能笼统地说"最近不行"。

关于切换的时点,源码注释里有一段很诚实的自我批评,值得原样理解:它说当前的实现方式很朴素(naive),而更贴近实际的做法应该是——在测试段开始的前一天训练出新模型,然后在测试段开始那一天再切换。为什么要拆成两步?因为训练本身要花时间。如果你在需要用它的那一刻才开始训练,那这个模型在你需要它的时候还不存在。这是个纯工程问题,但它会直接变成实盘里的一段空窗。

同一份代码里还埋着一个更隐蔽的警告。在模拟模式下批量补做准备工作时,如果发现信号已经被准备到了比当前时间更远的地方,它会警告说:这可能是因为在线模型预测了超出它应该预测的范围,会导致信号被离线模型污染。翻译成人话就是——某个已经下线的模型,它的预测悄悄留在了本该由新模型负责的那一段时间里。你看到的那条曲线,是两个模型的混合物,但没有任何地方标着这件事。

时间到了就别再预测了

前面说的都是"怎么换新的"。还有一个更朴素的问题:在新模型到位之前,旧模型还能用多久?

freqtrade 的机器学习模块给了一个不讲道理但很有用的答案:看它多老。 它的检查逻辑短到几乎不需要解释——拿当前时间减去这个模型的训练时间,换算成小时,超过用户设定的上限就判定为过期。

def check_if_model_expired(self, trained_timestamp: int) -> bool:
    time = datetime.now(tz=UTC).timestamp()
    elapsed_time = (time - trained_timestamp) / 3600  # hours
    max_time = self.freqai_config.get("expiration_hours", 0)
    if max_time > 0:
        return elapsed_time > max_time
    return False

这个参数当前的默认值是 0,含义是"永不过期"(当前默认值,可配置)。文档对它的描述是:如果模型比设定的小时数更老,就避免用它做预测。

注意"避免做预测"这个措辞——它不是判断模型错了,是判断模型不再可信,于是选择闭嘴。 这两者的区别很大。一个只在自己有把握的时候才发声、其余时间保持沉默的系统,比一个不管新旧都硬要给个答案的系统,安全得多。这个设计思路你完全可以搬到自己身上:与其纠结"我的判断依据是不是还成立",不如先给它加一个保质期,过了就不用它下决定。

另外这个模块还有个相邻的参数,管的是"多久重训一次",它的默认值是尽可能频繁地重训。可以看出这两个旋钮回答的是不同的问题:一个是"什么时候造新的",一个是"旧的什么时候作废"。它们不必相等,中间那段差值就是你愿意承受的风险敞口。

模型没见过的行情:不该预测的时候闭嘴

按时间判过期,粗糙但可靠。还有一种更聪明的判法:不看模型多老,看眼下这个局面模型有没有见过

freqtrade 文档里的相异度指数(Dissimilarity Index)做的就是这件事。它的思路可以完全脱离公式来讲:先量一量训练数据自己有多"散"——把训练集里两两样本之间的距离都算一遍,取个平均,得到一个代表"这堆数据的典型间距"的数;然后对每一个新来的待预测样本,找出它离训练集中最近的那个样本有多远;把这个距离除以刚才那个典型间距,就得到一个比值。

比值小,说明今天这个局面落在模型见过的世界里,它的判断算是在插值;比值大,说明今天这个局面离它见过的一切都很远,它的判断是在外推——而外推是所有预测里最不可信的一种。超过阈值的预测会被直接丢掉,不参与决策。文档也点明了这个阈值的两个方向:调高,模型更宽容,允许离训练数据更远的预测被采用;调低,则丢掉更多预测。具体设多少属于你自己的取舍,这里只讲机制。

这套东西之所以值得单独说,是因为它抓住了模型衰减里最要命的那一半。模型变钝有两种:一种是慢慢的,世界一点点挪;另一种是突然的,出现了一段训练集里压根没有过的行情——极端波动、流动性抽干、某种从未见过的联动。第二种情况里,模型不会告诉你"我不知道",它会照常吐出一个数,而且那个数往往很极端。相异度指数就是在替它说那句"我不知道"。

再上一层:学"怎么重训"的模型

到这里为止,所有的答案都还停留在"多久重训一次"这个层面,而这个数字是你拍脑袋定的。有没有可能,连这件事本身都交给机器去学?

qlib 里有一层叫元控制器(Meta Controller),干的正是这个。它的文档把定位写得很清楚:元控制器为预测模型提供指导,目标是从一连串预测任务中学出规律,再用学到的规律去指导接下来的预测任务。

把这句话拆开看。普通的模型学的是"从特征到结果";元控制器学的是"从一堆训练任务,到怎么设置下一个训练任务"。前者的经验单位是一根 K 线,后者的经验单位是一次重训。这是个挺漂亮的抽象:既然重训这件事你已经做过几十次了,那这几十次本身就是数据。

它给出的示例实现叫 DDG-DA,文档把流程概括成四步:把每次重训的元信息打包成元任务,所有元任务组成元数据集;在这个元数据集上训练元模型;用训练好的元模型做推断,得到"指导信息";把指导信息应用到预测模型上。

"指导信息"具体是什么?从代码里看得出来,这一路的做法是给历史训练样本重新加权——模块名字直译就是"数据选择",里面有个专门按时间给样本赋权的部件。这个思路本身就值得单独理解一遍:不是简单地"只用最近一年的数据",也不是"数据越多越好全都用上",而是判断历史上哪一段的市场状态跟接下来那一段更像,就把那一段的权重调高。它试图回答的是一个比"训练窗多长"精细得多的问题——哪些旧经验在今天仍然作数

这一层要不要用,是另一回事。多加一层能学的东西,就多一层能被过拟合的东西,而且它的调试难度比普通模型高一个量级。但知道有这么一层存在,你对"模型会过期"这件事的理解会完整很多:连"该怎么应对过期"本身,都是一个可以被建模、也可以被搞砸的问题。

为什么"找到一个好模型"不是终点

把上面这些串起来,会得到一个和直觉相当不同的结论。

一次性的好成绩,和一条能持续产出的流水线,是两种完全不同的资产。 前者会折旧,而且折旧速度不由你决定;后者只要还在转,就能一直造出替换件。真正值钱的从来是后者。这也是为什么专业机构的量化团队看上去总在做一些"不产出策略"的事——数据管道、任务调度、结果归档、监控告警。那些不是辅助工作,那就是主要工作。

你真正回测过的东西,可能比你以为的少。 大多数人的回测里,从头到尾只有一个固定的模型。而真实运行是一串模型接力,中间有切换、有空窗、有新旧混合。qlib 的在线管理器专门提供了一个模拟方法,可以从当前时间开始,按日历一天天把整个在线流程重演一遍——把"换模型"这件事本身也纳入检验。这两种回测的结果不会一样,而且不一样的那部分,恰好是你上线后才会遇到的部分。

别指望"重训"能治所有病。 重训只能把模型拉回到最近的数据分布上。如果问题出在你的信号本身已经被太多人使用(因子拥挤 那篇讲的就是这个),那重训只会让你更快、更精准地挤进同一扇门。这时候需要换的不是模型,是想法。

任何在线系统都有它做不到的事,而这些边界通常写在文档最不起眼的地方。 qlib 的在线服务文档在"已知限制"一节里坦白:目前支持的是每日更新次日的预测,但由于公开数据的限制,不支持生成次日的订单。这句话的分量是——从"我知道明天该看好哪些"到"我知道明天几点、什么价、下多少量",中间还隔着一整段路,而那段路不在这个框架的能力范围里。会看这类声明的人,能省下大量把框架当成完整系统的时间。

失效边界与常见误用

把重训频率当成一个可以调优的参数去调。 你试了几个不同的重训间隔,发现某一个的历史成绩特别好,于是就用它。这跟把策略参数调到历史最优是同一种错,只不过换了个更隐蔽的位置。判据也一样:把这个间隔往上下各挪一挪,如果结论翻个个儿,那这个结论本来就不成立。参数高原与参数尖峰 里的判断方法在这里照样适用。

默认"越频繁越好"。 更频繁的重训确实能更快跟上市场,但同时带来三样东西:更高的换手和成本、更少的每次训练可用数据(如果用的是滑动窗口)、以及更大的结果波动(每个新模型都是一次重新抽签)。这三样都会吃掉你追上市场所得到的那点好处。

只在模型表现变差时才想起重训。 等你从收益曲线上"看出来"模型不行了,这个判断通常需要几个月的数据才够可靠,而这几个月的钱已经花掉了。按固定节奏重训、按数据分布判断可信度,都是不依赖"事后看出来"的做法。

把模型过期和判断错误混为一谈。 过期说的是"这个模型的知识来自一个已经过去的时期",判断错误说的是"这一次它猜错了"。一个刚训好的模型也会经常猜错,那很正常;一个从没错过但已经很老的模型,反而更危险,因为它的连续正确会让你不断加码。

忘了数据管道才是命门。 在线服务的所有动作都建立在"数据每天准时、正确地更新"这个假设上。上游停更一天、某个字段格式变了、复权方式换了——这些都不会让程序报错,只会让预测悄悄变成垃圾。上线一套自动运行的东西,你需要监控的第一个对象不是收益,是数据。

以为新模型接管的那一刻是干净的。 前面提过那句关于"信号被离线模型污染"的警告。新旧交接处是整条链路里最容易出错的一段,而且错在这里的表现就是"曲线上多了一小段说不清来路的数据",没有任何报错。

小结

  • 模型不会坏,它会过期——因为它总结的是过去那个世界的规律,而那个世界一直在变。
  • 真正要经营的不是某个模型,是持续产出模型的那条流水线。一次性的好成绩会折旧,流水线不会。
  • 上线之后的日常循环大致是:数据更新 → 该造新模型就造 → 决定谁上线谁下线 → 把预测往前补 → 合成最终信号。顺序不是随便排的,动作之间会互相污染。
  • "谁在什么时候在线"必须被记账,否则事后没法把任何一段成绩归因到具体模型上。
  • 两种让模型闭嘴的机制都值得学:按年龄判过期(超过设定的小时数就不再预测),按陌生度判可信(当前局面离训练数据太远就丢弃这条预测)。
  • 更上面还有一层——把"怎么重训"本身当成学习对象,比如按"历史上哪一段跟接下来更像"给旧样本重新加权。多一层能力,也多一层能被搞砸的地方。
  • 重训只治分布漂移,不治规律失效。信号被用烂了,重训只会让你更快地挤进同一扇门。
  • 重训频率是一个会被过拟合的旋钮,也是一笔实打实的交易成本,两头都得算。

一句话记住这篇:你调出来的那个模型,是一盒有保质期的东西;真正该建的,是那台能一直做出新盒子的机器。

本文所引源码与文档均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么,或从 量化与 AI 投研卷 顺着读;本站内容为投资教育,不构成任何投资建议,边界见 免责声明

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

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