跳到正文

你的数据是错的。下面是修法。

大多数回测失败不是因为策略不对,是因为数据不对。下面是怎么拿到干净的行情数据,以及让所有人都栽过的三个坑。

第 5 篇 · 共 8 篇 约 6 分钟

大多数回测都在说谎。不是因为策略错,是因为数据错。

一个本真实夏普 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美股日线 + tick30–30–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() 就能让你中招。

问题: 你用的数据其实在你假装用的那个时间点还不可用。

常见的三个来源:

  1. 除权调整被回溯应用。 股票拆分发生在某个具体日期。6 月 15 日 1 拆 2 意味着 6 月 15 日之前的价格要除以 2 才能对比。但如果你的数据商把调整直接打在原始文件里你没注意到,你可能在回测中用了调整价,回测起始日却早于 6 月 15 日 —— 价格对得上今天看到的,但 2019 年的交易员实际上看到的不是这个价。通常危害不大,但说明数据卫生有问题。
  2. 基本面带报告滞后。 公司一季度的业绩(1–3 月)四月中才公布。如果策略用了一季度业绩但假设它 3 月 31 日就已经能拿到,你已经 lookahead 了两周左右。大多数基本面策略需要一个 reporting_lag 参数,至少设到 14 天。
  3. 上一篇 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 语法。这是在浪费你的时间。语法够用了。重要的是这套心智模型:

  1. 我的股票池是不是有生存偏差?→ 用历史成分股。
  2. 我的数据在我声称能拿到它的那一刻真的能拿到吗?→ 审查报告滞后和除权调整。
  3. 公司行为都处理对了吗?→ 用复权价,或者用原始价手动调整。

三条都是,那你的数据就能信。任何一条不是,你的回测只是故事,不是证据。先修数据,再操心策略。

本文学习路径的下一阶段讲因子和回测设计 —— 建立在”你现在有干净的数据和一套能用的 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. 特别是公司行为调整方法那一页。