ZVT 项目研究站 · 点时数据
ZVT 点时数据与未来函数:六个时间概念,决定你的回测数字是真是假
「把数据取下来」和「用对数据」是两件事。回测里最常见的失真不是代码写错,而是用了当时还不存在的信息:拿今天才公布的财报去回测去年、拿现在的成分股去回测历史、用当天收盘价当作当天成交价。这一页把六个时间概念讲清楚,并给出 ZVT 里能真正落地的抓手。
同一条财报数据的六个时间点
六个时间概念:各自是什么、错了会怎样
下面这六个名词经常被混着用。它们在回测里的先后顺序是固定的,任何一次错位都会让结果偏乐观。
| 时间概念 | 含义 | 在 ZVT 里对应什么 | 错位的后果 | 怎么自查 |
|---|---|---|---|---|
| 报告期 | 数据所属的会计期间(如 2024 年报、2025Q1) | report_period(String 列,README 用 'year' 过滤) | 把不同期的数据混在一张横截面上 | 打印该列所有唯一值,确认口径 |
| 公告日 | 该期数据首次对外披露的日期 | report_date(DateTime 列)——是否等于公告日需自行核验 | 用还没公布的数据做决策 | 拿一家熟悉公司的公告日与库里日期对照 |
| 数据可获得日 | 你的本地库真正能查到这条数据的日期(受抓取/入库时间影响) | 框架没有专门列;与 provider 写入时间相关 | 回测时点早于入库时点 → 事实上用了未来的数据 | 记录每次写库日期;对历史回测按公告日重排 |
| 因子计算日 | 你在哪一天计算出这个因子值 | 由你的代码决定(query 的时间范围) | 把未来数据算进因子 | 在因子函数里打印实际用到的时间范围 |
| 信号日 | 产生买卖决策的那一天 | 策略里 on_time(timestamp) 的 timestamp | 用当天收盘数据当天成交 | 检查信号所用数据的最晚时间戳 |
| 成交日 | 实际以什么价格成交 | trade_the_targets(due_timestamp=..., happen_timestamp=...) | 无手续费、无滑点、无 T+1 | 核对两个时间戳是否被赋成同一天 |
三类典型未来函数与它们的自查方法
这三类在量化研究里反复出现,且共同点是——代码跑得通、曲线很好看。
| 类型 | 典型写法 | 为什么是未来函数 | 怎么自查 | 正确做法 |
|---|---|---|---|---|
| 财务数据穿越 | 用 report_period == '2024' 的报告在 2024 年初就选股 | 2024 年报要到 2025 年才公布 | 把所用数据的可得日期与信号日期并排打印 | 按公告日做 as-of 对齐(ZVT 里的锚点是 report_date,需先核验它等于公告日) |
| 幸存者偏差 / 股票池穿越 | 用「今天的股票清单」跑 2018 年的横截面 | 当年的退市股、未上市股被排除,等于提前知道谁能活下来 | 看股票池的来源时间:是不是查询了当前清单 | 用历史成分(如指数成分股表)或在清单里保留退市记录 |
| 同价成交 / 收盘价穿越 | 用当天收盘价产生信号并用同一天收盘价成交 | 收盘价要收盘后才知道,实盘无法在这个价格成交 | 检查 due_timestamp 与 happen_timestamp 是否被赋成同一天 | 信号用 T 日数据、成交放 T+1(框架用两个时间戳把这件事显式化) |
ZVT 里真正可用的四个抓手
下面是源码与 README 里能查到的具体机制——它们不是为「防止未来函数」设计的功能,但可以被你这样用。
两个时间字段(report_period / report_date)
按源码,
FinanceFactor里report_period = Column(String(32))、report_date = Column(DateTime)。前者筛期间类别,后者做时间对齐。把「期间」与「日期」分开是这套设计里最值得利用的一点。get_recent_report_date(timestamp):给定时间点,取最近的报告日期这个函数存在于
src/zvt/api/portfolio.py,README 的示例策略里用它来判断「当前能看到的最近一期报告」。这正是 as-of 对齐需要的原语:回测时间点 → 当时可见的报告期。due_timestamp与happen_timestamp:把「信号时点」和「成交时点」拆开trade_the_targets(due_timestamp=..., happen_timestamp=...)要求显式传两个时间。把 happen 设为 T+1,就在接口层面把「当天信号当天成交」这个漏洞堵住了。可用的另一个参数是profit_threshold(官方示例里设为 None 表示不启用止盈)。provider列:确认数据来自哪一家同一张表可能混写多个源,而不同源的更新时间与口径不同。做时间一致性检查时按
provider拆开,能排除「同一指标两家源打架」造成的假信号。
回测前自查清单:八项,逐条打勾
把这份清单存进你的研究记录模板。任何一项答不上来,绩效数字就还不能对外说。
| # | 检查项 | 怎么查 | 不合格的典型表现 |
|---|---|---|---|
| 1 | 财务数据用的是公告日还是报告期 | 用 get_recent_report_date 或按 report_date 加时间条件 | 代码里出现 report_period == '2024' 却用在 2024 年内选股 |
| 2 | 股票池是不是历史时点的 | 查股票池来源表与其时间字段 | 用「当前 A 股清单」回测 2018 年 |
| 3 | 信号的成交时点是否晚于信息时点 | 核对 due_timestamp 与 happen_timestamp | 两个参数传了同一个 timestamp |
| 4 | 是否计入手续费、印花税与滑点 | 看回测是否对这些成本建模 | 零成本回测 |
| 5 | 停牌与涨跌停是否被排除在可成交之外 | 对照行情数据里停牌/一字板日期 | 涨停当天按涨停价买入 |
| 6 | 复权口径是否全流程一致 | 检查数据表、因子、基准用的是不是同一口径 | 因子用后复权、成交价用真实价 |
| 7 | 样本内外的划分是否事先确定 | 看研究记录里有没有时间切分规则 | 调参调到样本外也好看为止 |
| 8 | 绩效指标是否包含回撤与波动率 | 至少报告年化、夏普、最大回撤、换手 | 只报收益率 |
domain/fundamental/finance.py、api/portfolio.py)与 README 示例;清单里第 4–8 项属于通用回测规范,框架不替你完成,需要你在策略与数据层自行实现。本站未在本机运行 ZVT,不提供任何绩效数字。把清单落成代码:三段可以直接抄的自查
清单不落到代码就会变成口号。下面三段都不依赖 ZVT 的框架特性,直接在你的研究脚本里跑。
检查一:股票池是不是「当时」的
# 池子里的标的是哪天定下来的? pool = Stock.query_data(filters=[...], columns=['entity_id', 'list_date', 'end_date']) print(pool['list_date'].max(), pool['end_date'].notnull().sum())说明:如果清单里所有标的都是「至今仍在交易」,那它就带幸存者偏差。预期:能看出池子里是否保留了已退市记录(
end_date非空的条数)。检查二:信号与成交的时间戳是否被拉开
# 在 on_time 里打印两个时间,确认不是同一天 self.logger.info(f"signal={timestamp} due={due_ts} happen={happen_ts}")说明:
trade_the_targets(due_timestamp=..., happen_timestamp=...)两个参数被赋成同一天,就是「当天信号当天成交」。预期:日志里出现至少一个交易日的间隔。检查三:因子用到的最晚数据日期
# 财务因子:把「实际用到的最晚可得日期」打出来 latest = df.index.get_level_values('timestamp').max() print('factor uses data up to', latest, 'signal at', timestamp)说明:如果
latest晚于信号日回到的全部日期,说明用到了未来数据。预期:latest <= timestamp。这条断言建议直接写成assert,跑不过就中断回测。
点时数据与未来函数常见问题
涉及框架机制的回答都给出源码位置;涉及通用规范的部分标明「非框架能力」。
什么是未来函数(前视偏差)?
在回测的某个时点使用了当时还不可能获得的信息。它不会让代码报错,只会让结果变好——所以必须靠流程和自查发现,不能靠调试。
ZVT 会自动帮我避免未来函数吗?
不会。框架提供的是原料与接口:两个时间字段、get_recent_report_date、以及把信号与成交时点分开的 due_timestamp / happen_timestamp。真正的 as-of 对齐逻辑要你自己写。
财报的 report_date 就是公告日吗?
官方文档没有定义,不能假设。请做一次对照实验:挑一家公司,把公开的年报公告日与库里的 report_date 并排打印。一致才能当公告日用;不一致就要另找公告日来源。
用当前股票清单回测历史,偏差有多大?
取决于期间内退市与上市的数量,无法给一个通用数字。但机制是明确的:这条清单已经见过「谁能活下来」,所以任何长期横截面回测都会偏乐观。正确做法是使用历史成分或保留退市标的。
日内策略也需要注意这些吗?
一样需要,而且更敏感:分钟级数据同样存在「信号时点 vs 可成交时点」的问题,且涨停、停牌在日内会造成完全不可成交的价格。ZVT 提供分钟级数据,但可成交性判断要自己做。
本站为什么反复强调这件事?
因为它是本站与「只介绍 ZVT 能做什么」的文章最大的区别:那些内容告诉你如何取数,这一页告诉你取到的数什么时候才能用。这一条做错,后面所有优化都是在拟合噪声。