Point-In-Time 数据:一个数字「什么时候才被人知道」,比它是多少更重要
你想验证一个很朴素的想法:每年只在「赚钱效率高」的那批公司里挑——比如净资产收益率靠前的,这个指标说的是公司用股东的每一块钱,一年能赚出多少利润——长期是不是能跑赢大盘。数据你有,几百家公司十几年的财务指标,整整齐齐一张表,每一行都标着「2019 年二季度」这样的期号。你按期号把数字对到日期上,跑完,结果好看得让你心里一动。
麻烦在于,那张表里没有一列告诉你:这个数字,是哪一天才真正出现在世界上的。
先说人话:期末考的分数,什么时候才算「你知道了」
你孩子期末考在六月三十号,成绩单七月十八号才发到你手里。中间这十八天,分数客观上已经存在了——卷子改完了,分数躺在老师的表格里——但你不知道。
现在假设有人问你:「六月三十号那天,你是根据什么决定给孩子报暑期班的?」你要是回答「根据期末考成绩」,那就是在撒谎。哪怕分数确实是六月三十号那场考试的分数,你六月三十号那天手里也没有它。
事情还能更绕一点。七月十八号那份成绩单发下来,过两天老师说算错了一道大题,重发了一版,分数变了。这下同一场考试就有了两个分数:先公布的那个,和修订后的那个。你手上留着的是哪一版?如果学校档案里只保留最终版,那么一年后你去翻记录,看到的是修订后的分数,而当时那十八天里你脑子里想的其实是另一个数。
一个数字有两条时间线:它描述的是哪一段时间(报告期),和它什么时候被人知道(公布日)。 大部分人只记得前一条。Point-In-Time——直译「时点」——说的就是把后一条时间线也老老实实存下来,这样你才能问出这句话:在某年某月某日那一刻,我手上能拿到的、关于这家公司的最新数字,是多少。
投资层:这件事对你的决策意味着什么
报告期和公布日,差着一两个月
一份标着「二季度」的财报,覆盖的是四月到六月的经营,但它印出来、挂到交易所网站上,是七月甚至更晚的事。中间这段空档,长度还不固定——同一家公司不同年份的公布节奏会变,不同公司之间差得更远。
于是有个很容易滑过去的动作:你在做历史检验时,把二季度的数字直接贴回到六月三十号那一格。程序不会拦你,表格看着也很整齐。可从那一刻起,你的模拟世界里的「你」,就比真实世界里的所有人早知道了一两个月。这个提前量足够让很多平庸的想法看起来像天才。它属于前视偏差那一大类问题——用了当时还拿不到的信息——而 PIT 是专门对付其中「基本面数据」这一支的解法。
同一期数字,会被改好几次
这是更少人知道、也更难自己发现的一层。
同一个报告期的同一个指标,往往不止一个版本:先出一版快报口径的,正式财报再出一版,年报出来时可能又对一次,日后遇到会计政策变更、追溯调整还会再动。这不是异常,是常态。
而你今天从任何一个数据服务下载到的历史数据,绝大多数只保存最后那一版。也就是说,你拿到的是一份被后来所有修订反复擦洗过的、干净得不真实的历史。当年真正摆在投资者面前的那些数字——包括那些后来被证明报错了、又悄悄改回来的数字——已经从记录里消失了。
用这份干净数据去检验一套依赖财报的选股方法,等于让它在一个从来没存在过的世界里做题。而且偏差的方向是单边的:修订往往发生在数据「变得更准确」的方向上,你等于免费获得了一次事后校对。
你手算的时候也在犯同样的错
不写程序的人别急着松口气。你翻一家公司过去五年的市盈率走势,看到某年某月「这么便宜」,于是感慨当时怎么没买。可那个时点的市盈率,分母用的是哪一期的盈利、那一期盈利当天公布了没有、后来改没改过——这三个问题你多半没问。你在图上看到的那个「便宜」,很可能是用后来才知道的盈利数字,反推回去画出来的。
系统层:一个只肯给你「当时那一版」的数据库长什么样
qlib 把这件事做成了一套专门的文件格式,读它的设计比读任何定义都清楚。
为一个数字多存三列
普通的行情数据,一天一个值,一列就够。PIT 数据里的每一条记录有四列(以下结构与示例数据取自其文档快照):
date:公布日,这条记录是哪天被公开的period:报告期,它描述的是哪一个季度或哪一年(季度用类似201902的整数,后两位是第几季)value:数值本身_next:一个位置指针,指向「这一期数据的下一次修订记录存在文件的什么地方」
前两列是全篇的关键:公布日和报告期是两个独立的维度,混用就出事。 第四列则说明了另一件事——这个格式从一开始就假定「同一期会被改很多次」,所以把同一期的历次版本用指针串成一条链。
文档里这两行示例,把整个问题浓缩在一处:
(20190713, 201902, 0. , 1040)
(20190718, 201902, 0.175322 , 4294967295)
同一个报告期 201902,七月十三号公布了一版,值是 0;七月十八号又公布了一版,值变成了 0.175322。第一行末尾的 1040 就是指针,指向后面那条修订记录的位置;第二行末尾那个很大的数是「没有下一版了」的标记。
查询的动作:先砍掉未来,再顺着链往前走
它的查询逻辑,拆开看只有三步,但每一步都在防同一件事。
第一步,你给一个时间点,它先在公布日这一列上做一次二分查找,把公布日晚于这个时间点的记录全部切掉。切完之后剩下的,就是「那天为止全世界能看到的所有版本」。
第二步,在剩下的记录里,看看当时最早和最晚分别到了哪一期,把中间的期号补齐成一张连续的清单——季度数据就按每年四期展开,年度数据按年展开。这一步是为了让「往前数两期」这种表达有确定的含义,哪怕中间某一期没有数据(缺的那一期会留成空值,而不是被悄悄跳过)。
第三步,对清单上的每一期,从索引文件里查到它第一条记录的位置,然后顺着 _next 指针一条条往后走,一旦碰到公布日晚于查询时点的记录就停下,返回停下来之前的那个值。
三步走完,你拿到的就是「那一天,最新的那一版」,而不是「今天回头看,最终的那一版」。这里有一句话值得单独记住:它没有替你修正历史,它替你保留了历史的无知。
测试文件把机制讲得比文档还清楚
上游测试里有一段很能说明问题的数据。同一只股票的同一个季度指标,在几个连续交易日上查出来是这样:
2019-07-15 0.000000
2019-07-16 0.000000
2019-07-17 0.000000
2019-07-18 0.175322
2019-07-19 0.175322
对照前面那两行原始记录就明白了:第一版在七月十三号(周六)公布,值是 0,所以下一个交易日十五号开始,你查到的就是 0;直到十八号那一版出来,查询结果才跟着变。中间那三天,数据库明知道后面会改,也坚决不告诉你。
更有意思的是派生指标。同一份测试里还查了「最近两期的平均」,七月十五号那天的结果是 0.047369——正好是上一期的 0.094737 和当期那个 0 的平均;到十八号变成 0.135029,等于 0.094737 和 0.175322 的平均。你可以自己按一下计算器。这说明**「当时的无知」会一路传染到所有基于它的计算**:均值、同比、比率,全都得建在当天那个可能是错的原始值上,才算诚实。一个把 0 当成真值算出来的平均,看着刺眼,但它就是那天的真相。
三道拦你的闸门
这套设计里还有几处「宁可报错也不给你算」的地方,比机制本身更值得体会。
第一,不允许引用未来期。当你写出一个往后取期数的表达式时,它不会默默算给你,而是直接抛错:
if end_ws > 0:
raise ValueError(
"PIT database does not support referring to future period ..."
)
第二,不允许绕过时点直接取原始字段。你必须用它的时点算子把「按报告期排列的数据」折叠成「按观察日排列的数据」,否则它会报错并在错误信息里直接告诉你正确写法。这一步折叠是整套设计的核心:原始数据的坐标是报告期,而回测需要的坐标是「你在哪一天」,两者之间必须显式转换,不能靠对齐日期蒙混过去。
第三,没有数据就是没有数据。测试里查了一只没有对应财务数据的股票,返回的是空值,不是拿别的期、别的股票的数字填上。这一条听着理所当然,但很多数据处理流程会「贴心地」向前填充,那恰恰是把「我不知道」偷偷变成「我知道」的经典手法。
顺带说一句它自己声明的边界:这套时点机制主要面向会被修订的季度、年度基本面数据(其文档在已知限制里就是这么写的),文件名的后缀决定按季还是按年解析。价格、成交量这类不做修订的数据不走这套机制——它们也有自己的坑,但不在这一篇。
不写代码的人,怎么用这个概念
这才是这篇最实用的部分。你不需要自己搭数据库,但你可以用它当一把尺子,去量任何一份摆到你面前的历史统计。
第一问:这份数据有没有公布日? 如果一份财务数据表只有报告期没有公布日,那它就没法回答「那天我知道什么」。任何基于它做出的历史检验,都天然带着一两个月的提前量。
第二问:这些数字被改过吗,改之前是多少? 只保留最终版的数据源,做基本面研究时是有系统性偏差的。这不代表它没用,但你得知道自己在用一份被校对过的答案。
第三问:别人给你看的历史成绩,用的是哪一版数据? 看到一套宣称「历史上每年都能筛出好公司」的方法,这是最该问的一句。同一套规则,用时点数据跑和用修订后数据跑,结果差多少,没人不做实验就能拍胸脯说清。
还有一层是关于你自己的记忆的。你回想「当时我就觉得这家公司不行」,可你现在脑子里关于这家公司的所有数字,都是修订后的版本、都带着后来发生的事。人脑没有 _next 指针,它会用最新版本悄悄覆盖旧版本,而且不留痕迹。投资决策日志之所以有用,本质上就是给自己手动建一个时点数据库——把你当天知道的、以及你当天怎么想的,冻结在那里。
「这个数据是准确的」和「这个数据当时可得」是两回事。 修订后的数字更准确,这没错;但用更准确的数字去检验一套当年只能靠不准确数字运行的方法,检验的就不是那套方法。历史检验的目标从来不是算出正确答案,而是复现当时的信息状态。把「更准」当成「更好」,是这一类错误里最体面的一种。
失效边界与常见误用
以为用了时点数据就没有前视了。 时点数据只堵住了基本面这一条路。指数成分名单是什么时候的名单、停牌那段日子的价格怎么填、当天能不能成交是用哪个价判断的,这些一个都没解决。回测到底在算什么里讲的那些环节,每一处都可以独立出问题。
把「按报告期对齐」当成「延后几天就安全了」。 有人图省事,统一给财报加一个固定延迟。这比不加好,但公布节奏本身就不固定,固定延迟对早公布的公司太保守、对晚公布的公司仍然是提前量,而且它完全处理不了修订。
忽略了名单本身也有时点问题。 你拿今天的成分股名单回到三年前跑,那份名单里早就剔掉了三年间掉队的公司——这属于幸存者偏差,成因和修订不同,但同样是「今天的信息漏进了过去」。
把时点数据当成能提升收益的东西。 恰恰相反,换上时点数据之后,几乎所有依赖财报的历史成绩都会变差。变差的那部分不是损失,是原本就不属于你的虚账。一套用时点数据跑出来仍然平庸的方法,比一套用修订数据跑出来很漂亮的方法,诚实得多。
在不需要的地方过度纠结。 如果你的判断依据是「这家公司我打算拿五年」这种量级的东西,一两个月的信息提前量对结论的影响有限。时点问题咬人最狠的地方,是那些换手频繁、靠财报数字排序打分的做法——排序对输入的微小差异极其敏感。想系统看这些概念各自管什么,可以回到量化与 AI 投研卷顺着读;回测这个词本身的定义在术语库里有一张卡。
小结
- 一个数字有两条时间线:它描述哪段时间(报告期)、它什么时候被人知道(公布日)。多数数据表只有前一条。
- 财报的报告期和公布日差着一两个月,同一期数据还会被反复修订,而市面上大多数历史数据只保存最后一版——那是一份被后来的修订擦洗过的、当年从未存在过的历史。
- 时点数据库的做法:为每条记录同时存公布日、报告期、值和指向下一次修订的指针;查询时先切掉公布日晚于查询时点的记录,再顺着修订链取「当时最新的那一版」。
- 它宁可报错也不放水:不许引用未来期,不许绕过时点直接取原始字段,查不到就返回空值而不是向前填充。
- 换上时点数据,历史成绩通常会变差。变差的那部分本来就不该算你的。
- 你自己的记忆没有这套机制——它会用最新版本覆盖旧版本且不留痕迹,所以决策日志的价值是冻结当时的信息状态。
一句话记住这篇:别问这个数字是多少,先问它是哪天才到你手上的。
本文所引源码、文档与测试数据均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回量化与 AI 投研概念库看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。