① 复权价与未复权价不能混用
同一段逻辑里出现 Close(未复权)与 Adj Close(复权)的混算,会让收益里混进除权跳空。实测 auto_adjust=True 时 Adj Close 列根本不存在,所以「想混用」往往是因为没意识到口径已被默认值改变。
yfinance 数据审计 · 从取数到回测的最后一道关
这是本站认为最该先补上的一环:中文教程大多停在「怎么取数」,很少回答「取到的数能不能用」。而真正让回测结论失真的,通常不是代码 bug,而是口径问题——混用复权价与未复权价、用报告期当成公告日、用今天的存续标的回测历史。本页给出一套可执行的审计清单与三态结论,让你在把数据交给策略之前,先有一个明确的判断依据。
左列是检查项、中列是为什么、右列是本站实测基线(AAPL 一个月日线)。
| # | 检查项 | 为什么重要 | 实测基线(AAPL, 1mo) |
|---|---|---|---|
| 1 | 是否为空表 df.empty | 空表会让后续所有计算静默产出空结果 | 非空,22 行 |
| 2 | 索引是否重复 index.duplicated() | 重复日期会让位置运算整体错位 | 重复数 0 |
| 3 | 索引是否单调递增 | 时间序列运算的前提 | 单调递增 |
| 4 | 索引时区是否符合预期 | 不同取数方式的时区不同,拼接会错位 | America/New_York |
| 5 | OHLC 关系是否成立 | 价格错误最直接的暴露口 | 成立(未逐行断言外的行) |
| 6 | 是否存在负价格 | 负价通常意味着数据错误 | 无 |
| 7 | 涨跌幅是否有极端跳点 | 极端跳点多来自复权错误或单位错位 | 未发现 |
| 8 | 事件列与价格是否一致 | 分红/拆股应与价格跳变对应 | 97 行 actions 记录可对照 |
| 9 | 最新一行是否为预期交易日 | 区分「没数据」与「取数失败」 | 2026-10-08 |
| 10 | 跨市场表 NaN 分布 | NaN 常来自交易日历不同,不是停牌 | 单市场无此问题 |
| 11 | dtype 是否符合预期 | 跨市场并集会把 Volume 变 float | Volume 为 int64 |
| 12 | 股票池是否含已退市标的 | 否则存在幸存者偏差 | 单标的回测不涉及 |
| 13 | 报表是否按公告日对齐 | 报告期 ≠ 公告日,point-in-time 红线 | 本页仅行情,见财务页 |
本站建议每次审计都输出一个明确结论,而不是「看起来还行」。
| 结论 | 判定条件 | 可以做什么 | 不可以做什么 |
|---|---|---|---|
| 可用于探索性分析 | 1–7 项全部通过;事件列与价格一致;最新日期符合预期 | 画图、算描述性统计、做初步收益与波动观察 | 仍不能直接作为策略绩效依据——还没处理成本、滑点与样本外 |
| 需要人工复核 | 出现极端跳点、事件与价格不一致、跨市场 NaN 分布异常、dtype 意外变化等任一项 | 定位具体日期,用事件列或第三方数据交叉核对 | 不要用 ffill()/均值填充把问题掩盖掉 |
| 不建议直接用于回测 | 复权口径混用;point-in-time 未处理;股票池含幸存者偏差;无法解释的跳变 | 先修口径或换数据源,再回到首步 | 不要用「回测结果看起来合理」当作口径正确的证据 |
每条都对应一个真实会发生的口径错误,且都不会让代码报错。
同一段逻辑里出现 Close(未复权)与 Adj Close(复权)的混算,会让收益里混进除权跳空。实测 auto_adjust=True 时 Adj Close 列根本不存在,所以「想混用」往往是因为没意识到口径已被默认值改变。
报表列是报告期结束日,公告晚数周。用报告期对齐等于提前知道未来。官方没有 point-in-time 视图,只能自建归档。
用今天存续的标的回测历史,会系统性剔除退市与衰败公司。本站实测的单标的取数不涉及这点,但做多标的组合时必然遇到。
日线只有开高低收,没有盘口深度与成交量分布。用它回测时,成交假设(按收盘、按次日开盘、加多少滑点)必须显式写出。
这些在 yfinance 的原始数据里不是统一处理的:分红拆股有列可查,但交易成本、滑点模型、涨跌停与停牌语义都要你自己定义。
实测最新一行是最近一个交易日(如 2026-10-08),而你的策略可能在别的日期运行。没有对齐「数据截止日」的回测,会出现用了未来数据或漏了最新数据的两种相反错误。
把下面这段放进数据管道,落盘前跑一次即可得到三态结论。
import yfinance as yf
import pandas as pd
def audit(df, expect_tz=None):
issues = []
price_cols = [c for c in ("Open", "High", "Low", "Close") if c in df.columns]
if df.empty:
return "不建议直接用于回测", ["empty dataframe"]
if df.index.duplicated().any():
issues.append("duplicated index: %d" % int(df.index.duplicated().sum()))
if not df.index.is_monotonic_increasing:
issues.append("index not sorted")
if expect_tz and str(getattr(df.index, "tz", None)) != expect_tz:
issues.append("tz mismatch: %s" % getattr(df.index, "tz", None))
if price_cols:
p = df[price_cols]
bad_ohlc = (p["High"] < p[["Open", "Close"]].max(axis=1)) | \
(p["Low"] > p[["Open", "Close"]].min(axis=1))
if bad_ohlc.any():
issues.append("OHLC relation broken: %d rows" % int(bad_ohlc.sum()))
if (p < 0).any().any():
issues.append("negative price")
jump = (p["Close"].pct_change().abs() > 0.5).sum()
if jump:
issues.append("extreme moves: %d" % int(jump))
if "Adj Close" in df.columns and ("Close" in df.columns):
ratio = (df["Adj Close"] / df["Close"]).round(6)
if ratio.diff().abs().max() > 0:
pass # 正常的复权跳变处,需人工确认对应事件
verdict = ("可用于探索性分析" if not issues
else ("需要人工复核" if len(issues) <= 2 else "不建议直接用于回测"))
return verdict, issues
df = yf.Ticker("AAPL").history(period="1y", auto_adjust=False)
print(audit(df, expect_tz="America/New_York"))
下面两条是本站可以明确说的差异来源;具体数值取决于标的与区间,本站不发布绩效数字。
| 你换了什么 | 会改变什么 | 为什么 | 本站是否实测 |
|---|---|---|---|
auto_adjust True ↔ False | 收益序列里是否包含分红再投资 | 复权价把分红连续地体现在价格里,未复权价则把除权当成一次下跌 | 列集合变化已实测;收益数值差异未做量化断言 |
指数(^GSPC)vs 成分股 | 是否存在成分股变动的幸存者偏差 | 指数历史上会调整成分,用今天的成分股回测过去等于用了未来名单 | 本页只作方法说明,未做实测 |
| 跨市场并集表 vs 分市场处理 | 缺失值处理方式与 dtype | 交易日历不同会产生 NaN,直接算收益会把 NaN 当 0 或报错 | 并集行数与 dtype 变化已实测 |
repair=True ↔ False | 个别日期上的价格是否被改写 | repair 会修正 Yahoo 侧的价格错误,但存在假阳性风险 | 本站样本未触发修复,未做效果断言 |
下面的回答都指向可核验的官方文件或本站实测;与官方表述冲突时,以官方仓库与 docs 为准。
本站建议的顺序是:①是否为空表;②索引是否重复;③索引是否单调递增;④索引时区是否符合预期;⑤OHLC 是否满足 High ≥ max(Open,Close) ≥ min(Open,Close) ≥ Low;⑥是否存在负价格;⑦涨跌幅是否有极端跳点;⑧分红拆股事件与价格是否一致;⑨最近一行日期是否为预期的最新交易日;⑩跨市场表里 NaN 的分布。前四项能筛掉绝大多数「看起来有数据、其实不能用」的情况。
因为下游所有基于位置的运算都会因为重复日期而错位:pct_change()、shift()、按日重采样都会给出错误结果,而且往往不报错。本站实测的干净样本(AAPL 一个月日线)重复数为 0,但这不代表所有标的都如此,尤其是你自己拼接多个来源之后。
本站给三个硬性判据:①存在无法解释的价格跳变且无法定位到分红/拆股事件;②复权口径混杂(同一段代码里同时用了 Close 与 Adj Close);③依赖了当时不可知的信息(报告期当成公告日、用了未来的成分股名单)。命中任意一条,就应停下修口径,而不是继续调策略参数。
①复权混用:用未复权价算收益,把除权当成暴跌;②point-in-time:财务报表的列是报告期,公告要晚数周,回测若按报告期对齐就是用了未来信息;③幸存者偏差:用今天还在的标的列表回测十年前,等于剔除了所有退市与衰败的公司。这三条都不会让代码报错。
本站不建议。官方 README 把定位写成 research and educational purposes,并写明与 Yahoo 无隶属关系、数据受 Yahoo Terms of Use 约束;库本身不含交易执行能力,也没有延迟与准确率承诺。本站没有实测其延迟与稳定性,因此不发布「实时」「低延迟」类结论。实盘请使用交易所或券商级行情源,并自行完成合规确认。
要。审计的价值在于「每次都跑」而不是「想起来才跑」。本页给出的检查函数可以直接放进数据管道,在落盘前执行并把结果写进研究记录。本站也建议把审计结果与数据一起归档——半年后回看时,你会需要知道当初用的数据是什么状态。