休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

量化回测的数据管道:采集、复权、对齐与存储

回测失真很少源于策略逻辑,多半出在数据管道。本文拆解四个阶段——粒度选择、复权处理、时间对齐、存储查询,说明为什么停牌不能向前填充、为什么后复权才能保证可复现,并给出完整的 Python 数据准备实现。

回测结果不可信,原因很少出在策略逻辑上,更多出在数据管道。一个看起来能跑通的回测,可能因为复权处理错误、时间对齐偏差或缺失值填充,得出了完全失真的结论——而且这些错误不会报错,只会让回测曲线变好看。

这篇文章讲回测数据管道的四个阶段,以及每个阶段最容易出错的地方。

回测数据管道的四个阶段

阶段目标主要风险
采集与粒度选择拿到正确粒度的原始数据粒度不足导致信号丢失
复权处理价格序列可比拆股分红造成虚假跳空
时间对齐多标的时间轴一致时区与交易日历错位
存储与查询高效可复现地取数查询口径不一致

四个阶段的顺序不能颠倒——先采集后复权,再对齐,最后存储。反过来做会导致大量重复计算。

阶段一:粒度选择决定了策略的上限

粒度选择的判断标准很简单:策略信号的产生频率,必须高于数据粒度。

  • 按日调仓:日线数据足够,且体积小、计算快。
  • 日内策略:至少需要分钟线,具体取决于信号周期。
  • 高频与微观结构策略:必须用 tick 数据,且要关注 tick 级的成交方向。

一个常见的错误是「用分钟线回测分钟级信号」。这样做会引入系统性偏差:分钟线只保留该分钟的 OHLC 四个点,策略在分钟内的入场与出场价格无法还原,回测结果通常比实际乐观。

另一个错误是反过来——用 tick 数据回测日频策略。这不会让结果更准,只会让计算量和存储成本高出一个数量级。

阶段二:复权处理,最容易出错的一步

股票发生拆股或分红时,价格会「跳空」。如果不做复权处理,回测会把这次跳空当成一次真实的暴跌或暴涨,从而产生虚假信号。

复权有前复权与后复权两种方式,选择取决于回测类型:

方式特点适用场景
前复权以最新价格为基准向前调整历史价格技术指标计算、图表展示
后复权以上市首日为基准向后调整长期收益计算、回测
不复权保留原始成交价需要真实成交价的场景,如撮合模拟

回测场景通常推荐后复权,因为它不会因为新增数据而改变历史价格,保证回测结果可复现。前复权的历史序列会随最新价格变化而变化,用前复权数据做的回测在不同时间跑出来的结果可能不同——这是很隐蔽的复现性问题。

还需要注意:复权因子必须来自可靠数据源,且要覆盖所有公司行为(拆股、合股、分红、配股)。漏掉任何一个都会在历史上留下一个假跳空。

阶段三:时间对齐与缺失处理

多标的回测时,时间对齐的核心原则是:不要用「向前填充」掩盖缺失。

三种缺失需要区别对待:

  • 休市:正常情况,从时间轴上移除即可,不应该算作缺失。
  • 停牌:该标的当日无交易。回测中应保持仓位不变、标记为不可交易,而不是填充价格。
  • 数据缺失:数据源问题,需要告警并补数,绝不能静默填充。

把停牌当成缺失并向前填充价格,会让回测产生「零波动区间的虚假持仓」,从而低估风险、高估收益。这是回测失真最常见的原因之一。

阶段四:存储与查询口径

最后一步的目标是可复现。同一个回测,在不同时间、不同机器上跑,应该得到完全相同的结果。要做到这一点需要:

  1. 数据版本化:每次回测记录所用数据的快照标识或时间范围,而不是「取最新数据」。
  2. 查询口径固定:明确复权方式、时区、缺失处理规则,并写进代码而不是文档。
  3. 列式存储:回测通常只需要少数几列,列式格式能显著减少扫描量。
  4. 保留原始数据:聚合数据可以快速迭代,但原始数据是重算的依据,不应丢弃。

三个最常见的回测数据错误

  1. 幸存者偏差:只用当前仍在交易的标的回测,退市与并购的标的被排除,结果系统性偏乐观。
  2. 未来函数:用当日收盘价生成信号,又用当日开盘价成交。这在真实交易中不可能实现。
  3. 填充掩盖缺失:用向前填充处理停牌,人为压低波动率。

这三个错误都不会让回测报错,只会让曲线变好看。所以回测系统的第一优先级不是「跑得快」,而是「能发现自己的数据问题」。

Python 实现:回测数据准备

Python
import httpx
import pandas as pd

API_KEY = "your_api_key"
BASE = "https://api.tiqex.io/api/v1/market"


def fetch_candles(symbol: str, market: str, interval: str = "1d") -> pd.DataFrame:
    """拉取 K 线并规范化为 UTC 索引的 DataFrame。"""
    resp = httpx.get(
        f"{BASE}/{market}/kline",
        params={"symbols": symbol, "interval": interval},
        headers={"X-API-Key": API_KEY},
        timeout=30,
    )
    rows = resp.json()["data"]
    df = pd.DataFrame(rows)

    # 1) 时间统一为 UTC,避免跨市场错位
    df["ts"] = pd.to_datetime(df["timestampMs"], unit="ms", utc=True)
    df = df.set_index("ts").sort_index()

    # 2) 校验时间戳单调递增,出现回退说明数据有问题
    if not df.index.is_monotonic_increasing:
        raise ValueError(f"{symbol} 时间戳非单调递增,需人工检查")

    # 3) 只保留回测需要的列,降低扫描量
    keep = [c for c in ("open", "high", "low", "close", "volume") if c in df.columns]
    return df[keep]


def apply_backward_adjustment(df: pd.DataFrame, factors: pd.Series) -> pd.DataFrame:
    """后复权:以上市首日为基准向后调整,保证历史价格可复现。"""
    aligned = factors.reindex(df.index, method="ffill")
    out = df.copy()
    for col in ("open", "high", "low", "close"):
        if col in out.columns:
            out[col] = out[col] * aligned
    return out


def mark_trading_gaps(df: pd.DataFrame, calendar) -> pd.DataFrame:
    """把休市 / 停牌 / 缺失分开标记,而不是一律向前填充。"""
    expected = pd.DatetimeIndex(calendar.sessions_in_range(df.index[0], df.index[-1]))
    missing = expected.difference(df.index)
    if len(missing) == 0:
        return df

    # 用显式标志记录,不填充价格
    df = df.reindex(df.index.union(missing))
    df["no_trade"] = df["close"].isna()
    return df


candles = fetch_candles("AAPL", "equity")
print(f"行数 {len(candles)}  区间 {candles.index[0].date()} → {candles.index[-1].date()}")
print(candles.tail(3))

这段代码体现了三个关键约束:时间统一为 UTC、时间戳单调性校验、缺失用显式标志而非填充。apply_backward_adjustment 用后复权保证历史序列不随新数据变化,这是回测可复现的前提。

常见问题

回测应该用前复权还是后复权?

通常推荐后复权。后复权的历史价格不会随新增数据变化,保证回测结果可复现;前复权的历史序列会随最新价格调整,同一策略在不同时间跑出的结果可能不同。

停牌期间应该怎么处理?

保持仓位不变并标记为不可交易,绝对不要向前填充价格。填充会产生零波动区间的虚假持仓,低估风险并高估收益。

回测需要 tick 数据吗?

取决于策略周期。日频与分钟级策略用 K 线数据即可;只有高频与微观结构策略才需要 tick 数据。用 tick 回测日频策略只会增加成本,不会提高准确性。

为什么同样的回测跑两次结果不一样?

通常是数据口径不固定:用了「取最新数据」而不是固定快照,或者用了前复权数据。解决办法是数据版本化,并把复权方式、时区、缺失规则写进代码。

怎么避免未来函数?

严格区分「信号生成时点」与「成交时点」。信号只能使用该时点已经可得的数据,成交价必须用信号之后的价格,并在回测框架里显式约束这一点。

下一步

回测数据管道的核心是三个词:可复现、不填充、显式标记。完整的数据准备流程见 回测数据准备指南,复权机制的细节见 拆股复权,K 线字段定义见 K 线 OHLCV 字段,场景方案可参考 回测方案。历史数据的拉取量直接影响 套餐额度,建议先做一次量级估算。