你的数据是错的。下面是修法。
大多数回测失败不是因为策略不对,是因为数据不对。下面是怎么拿到干净的行情数据,以及让所有人都栽过的三个坑。
大多数回测都在说谎。不是因为策略错,是因为数据错。
一个本真实夏普 1.5 的策略,在脏回测里看起来像夏普 4.0。然后你上线,亏六个月。debug 几周。最后你发现 30% 的交易是 2008 年退市的那些股票做的,你那个”当前成分股”列表悄悄地把它们剔了,所以你从来没看到那些亏损。这事儿谁都碰上,而且比你想象的频繁。
这篇文章就讲三个坑和怎么避它们。
行情数据的四类
说清洗之前,先看个全景:
日线 OHLCV。 Open、High、Low、Close、Volume。一根日线 bar。日频策略的主力军。容易拿、容易存、容易回测。本文学习路径里大多数策略都跑在日线上。
日内(1 分钟、5 分钟、tick)。 频率更高、数据集更大,信号多但噪声也多。免费的数据源很少。Tick 数据一天一只标的几个 G,通常要花钱。
基本面数据。 营收、利润、资产负债表项、一些比率。季度发布。带报告滞后 —— 一季度的财报通常四月中才公布,不是 3 月 31 日。忘了这个滞后就会导致 lookahead bias(我们后面会讲到)。
另类数据。 新闻情绪分、停车场卫星图、信用卡流水、网站流量。机器学习驱动的 alpha 现在越来越往这里走。也越来越贵、越来越吵。
前 6–12 个月做量化,盯死在日线 OHLCV 上。其他几类真实存在,但先把基础打对再想它们。
上哪儿拿(以及成本)
主流数据源的老实盘点:
| 数据源 | 类型 | 成本 | 质量 | 注意 |
|---|---|---|---|---|
| Yahoo Finance | 美股日线 | 免费 | 一般 | 没有退市收益。生存偏差是写死的。 |
| Alpha Vantage | 美股日线 + 日内 | 免费版 + $50/月高级版 | 一般 | 免费版每分钟 5 次调用。做过除权,除息不一定。 |
| Polygon.io | 美股日线 + tick | 200/月 | 良好 | 比 Yahoo 好。还是没退市收益。 |
| Quandl(现在叫 Nasdaq Data Link) | 日线、基本面、部分另类 | 看数据集 | 良好 | 高级数据集很贵。 |
| CRSP | 美股(学术级) | 学术用户 $5k+/年 | 最佳 | 学术论文里用的就是它。含退市收益。 |
| Bloomberg | 全部 | $25k+/年 | 最佳 | 只有机构才值得上。 |
对 Arithmion 用户:arithmion.fetch_ohlcv() 走 Polygon 做代理,然后在事后做清洗(退市收益、公司行为调整、交易所代码归一化)。本文学习路径的工作你不用操心这些。
如果自己搞:能用学术权限就 pandas-datareader 配 CRSP。否则得接受数据里有生存偏差,按这个去定仓位大小。
坑 #1:生存偏差(回测的头号谎言)
这是把散户回测毁掉最多的那个坑。它值得专门一节。
问题: 你下载一份”今天的标普 500 成分股”列表。给这 500 只票拉 20 年的价格历史。然后回测一个策略。策略看起来美得很。
为什么是错的: 2025 年的标普 500 不是 2005 年的标普 500。有些公司会退市(雷曼兄弟、安然、玩具反斗城、床上用品 Beyond,等等)。你用今天的成分股列表去拉历史价格,你拿到的就是幸存者 —— 没破产、没被收购、没被踢出指数的那些。
那些本来会亏钱的交易你一个也看不到。你的回测是意淫。
具体的例子: 一个”标普 500 加入就买入、移出就做空”的策略。用今天的成分股列表去回测,2005–2024 看起来漂亮极了:所有加进来的都涨了(大部分),所有移走的都跌了(大部分)。Sharpe 2.5。
用历史上的成分股变动 —— 那些年实际发生的加减 —— 跑一遍,Sharpe 掉到 0.4。移出的那些里有安然,一年跌了 99%。如果你不知道它要被移出,等暴跌开始时你已经来不及避免这 99% 的亏损。
修法: 用历史上的成分股数据库。CRSP 对美股就做这件事。Arithmion 的 fetch_ohlcv 如果传 survivorship_aware=True 就透明地做了。手动做的话,意味着要追踪指数的历史成分变动(大多数数据商会把这个作为单独文件提供)。
这是回测的头号谎言。 如果你只能记住本文里的一个坑,就记这个。
坑 #2:Lookahead bias
第二大谎言。比生存偏差更容易踩进去,因为一行偏移的 .shift() 就能让你中招。
问题: 你用的数据其实在你假装用的那个时间点还不可用。
常见的三个来源:
- 除权调整被回溯应用。 股票拆分发生在某个具体日期。6 月 15 日 1 拆 2 意味着 6 月 15 日之前的价格要除以 2 才能对比。但如果你的数据商把调整直接打在原始文件里你没注意到,你可能在回测中用了调整价,回测起始日却早于 6 月 15 日 —— 价格对得上今天看到的,但 2019 年的交易员实际上看到的不是这个价。通常危害不大,但说明数据卫生有问题。
- 基本面带报告滞后。 公司一季度的业绩(1–3 月)四月中才公布。如果策略用了一季度业绩但假设它 3 月 31 日就已经能拿到,你已经 lookahead 了两周左右。大多数基本面策略需要一个
reporting_lag参数,至少设到 14 天。 - 上一篇 pandas 文章里的
shift()bug。 算 forward return 用了prices.pct_change()而不是prices.pct_change().shift(-1)。你拿明天的收盘算今天的收益。回测看起来 alpha 满满。实盘亏钱。前面的 pandas 文章讲过这个。
修法: 对你用的每一个数据字段都问一句:“在我用的那一天,一个真实的交易员有可能拿到这个值吗?“如果答案是不能,你的回测在说谎。
Arithmion 的 SDK 通过在所有因子计算上强制日期对齐来防止这件事。你能关掉,但你得显式表态。这是故意的设计。
坑 #3:公司行为
拆股、分红、合并、分拆、退市。任何一个处理错了,你的价量序列就会出现假的跳价,那不是真实的收益。
拆股。 AAPL 在 2020 年 8 月 31 日做了 1 拆 4。如果你的数据显示 AAPL 拆股前一天 $499、后一天 $125,你并不是一天亏了 75% —— 你经历了 1 拆 4。把历史价格除以 4 做调整。
分红。 AAPL 分 $0.20 的红,除权日股价要掉 $0.20。股东的总回报不变(拿到现金了),但价格回报少了 $0.20。如果你只盯价格,你的收益序列就少算了这笔分红。如果是总回报策略,分红必须再投资。
并购。 A 公司现金收购 B 公司,B 的股价在收购完成日跳到收购价并停牌。如果没有退市收益,你的回测会在一只不再交易的股票上显示一个未平的仓位。
分拆。 一家公司把子公司的股票分给现有股东。你在母公司的持仓现在含了新实体的份额。两个都得追踪,否则你的收益算不全。
公司行为出大事故的具体例子: 你回测在宣布 1 拆 2 当天买入,期待拆股后会跳涨。但你的数据显示第二天股价腰斩(对的),然后再也不涨回来(错的 —— 因为你忘了把这次拆股向过去回溯)。你的策略”在每次拆股宣布时亏 50%“。实际上你会拿到那个预期中的跳涨。
修法: 到处都用复权价。大多数数据商会替你做(Yahoo、Polygon、Arithmion)。如果手动搭数据集,你得自己追踪公司行为并应用它们。pandas-datareader 配 CRSP 这个组合处理得是对的。
干净的数据长什么样
这儿有一份检查清单。回测之前跑一下:
import pandas as pd
def data_health_check(df):
checks = {}
# 日期是不是排好序、连续的?
checks["date_range"] = (df.index[-1] - df.index[0]).days
checks["n_rows"] = len(df)
checks["expected_rows"] = len(pd.bdate_range(df.index[0], df.index[-1]))
checks["missing_days"] = checks["expected_rows"] - checks["n_rows"]
# 有零价或负价吗?
checks["zero_prices"] = (df["close"] <= 0).sum()
# 突然的巨大单日波动(大概是公司行为或数据错误)?
returns = df["close"].pct_change()
checks["extreme_moves"] = (returns.abs() > 0.5).sum()
# 成交量有没有可疑模式?
checks["zero_volume_days"] = (df["volume"] == 0).sum()
return checks
print(data_health_check(data))
提示词
用 pandas 写一个 data_health_check 函数:检查日期范围、缺失交易日、零价负价、极端涨跌幅、零成交量,返回健康报告字典。
{
"date_range": 1826,
"n_rows": 1305,
"expected_rows": 1305,
"missing_days": 0,
"zero_prices": 0,
"extreme_moves": 0,
"zero_volume_days": 0,
}
零缺失日、零极端波动、零零价 —— 这是健康的数据集。任意一个数字不为零,先去查清楚再相信回测。
不要在数据 wrangling 上讲大道理。展示坑。继续走。
大多数数据清洗教程花 30 页讲 pandas 语法。这是在浪费你的时间。语法够用了。重要的是这套心智模型:
- 我的股票池是不是有生存偏差?→ 用历史成分股。
- 我的数据在我声称能拿到它的那一刻真的能拿到吗?→ 审查报告滞后和除权调整。
- 公司行为都处理对了吗?→ 用复权价,或者用原始价手动调整。
三条都是,那你的数据就能信。任何一条不是,你的回测只是故事,不是证据。先修数据,再操心策略。
本文学习路径的下一阶段讲因子和回测设计 —— 建立在”你现在有干净的数据和一套能用的 Python 环境”这个前提上。
想深入的话可以读这些
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. ISBN 978-1119482086. 第 4 章讲金融数据的交叉验证 —— 直接跟避免 lookahead bias 和多重检验问题相关。
- Campbell, J., Lo, A., MacKinlay, A. (1997). The Econometrics of Financial Markets. Princeton. ISBN 978-0691043012. 第 1 章是关于资产价格非随机游走性质的权威参考,对数据问题讲得很细。
- Arithmion documentation on data cleaning. arithmion.io/docs/data. 特别是公司行为调整方法那一页。