一句话结论:回测数据审计的重点不是确认文件已经下载,而是证明数据覆盖范围符合策略需求、时间顺序正确、价格口径一致,并且没有会改变历史交易决策的缺陷。
摘要
历史行情数据存在缺口、重复记录或复权口径不一致时,量化回测可能产生错误的收益率、技术指标和交易信号。单纯检查 CSV 行数或 API 请求状态,无法判断这些问题是否存在。本文从回测数据审计出发,建立预期数据集合、实际数据集合和策略可用数据集合之间的关系,并介绍如何检查时间覆盖、重复 K 线、复权一致性及未来信息泄漏。QuantDash 提供的多市场行情、K 线和时间区间查询能力,可以用于构建历史数据获取环节,但策略是否具备可靠的数据基础,仍需要研究者独立验证。
1. 回测为什么需要数据审计?
量化回测回答的是一个有条件的问题:
如果策略在过去按照预先定义的规则运行,使用当时可获得的数据,会产生怎样的结果?
这意味着回测质量取决于策略逻辑,也取决于输入数据。
如果历史行情存在缺口,策略可能错过某个本应产生信号的交易日。如果同一根 K 线重复出现,指标计算可能把某个价格重复计入。如果历史价格在不同阶段使用了不同的复权口径,收益率计算也可能出现偏差。
这些问题会沿着数据链路传递:
text
历史行情数据
↓
数据清洗与时间对齐
↓
技术指标计算
↓
交易信号生成
↓
订单模拟
↓
回测收益与风险指标
因此,回测之前应先回答一个问题:输入的数据是否符合这次研究的假设?
这里的关键不是追求所有数据在任何场景下都没有缺失,而是识别哪些缺失会影响策略,并确保数据处理规则不会无意中改变研究结论。
2. 建立预期数据集合,而不是只看文件大小
数据审计最容易出现的错误,是把实际记录数当作完整性的唯一证据。
假设某个策略使用一只股票的日线数据进行研究。数据文件包含大量记录,看起来没有明显问题,但仍可能存在:
- 区间开头的数据缺失。
- 某段时间出现连续空档。
- 某个交易日有重复记录。
- 某些记录的日期超出研究区间。
- 股票上市之前出现了不应存在的记录。
- 由于停牌或市场日历问题,实际记录数与预期值不一致。
更严谨的做法是先定义预期数据集合。
对于单只股票的日线研究,预期数据集合可以由以下条件确定:
- 研究起始日期和结束日期。
- 对应市场的交易日历。
- 股票实际上市和交易状态。
- 数据接口的返回口径。
- 策略是否要求每个适用交易日都有记录。
然后,将实际获取的记录与预期集合进行比较。
python
import pandas as pd
def audit_date_coverage(
actual_dates,
expected_dates,
):
actual = pd.DatetimeIndex(
pd.to_datetime(actual_dates)
).normalize().unique()
expected = pd.DatetimeIndex(
pd.to_datetime(expected_dates)
).normalize().unique()
missing = expected.difference(actual)
unexpected = actual.difference(expected)
return {
"expected_count": len(expected),
"actual_unique_count": len(actual),
"missing_dates": missing.sort_values(),
"unexpected_dates": unexpected.sort_values(),
}
这里的 expected_dates 不是从实际数据反推得到的日期,而是根据研究区间和交易规则独立生成的预期日期。
否则,如果某个交易日缺失,而程序又使用实际数据生成"预期日期",缺失就可能被掩盖。
2.1 缺失记录不一定意味着数据源故障
需要区分以下情况:
| 现象 | 可能原因 | 建议处理 |
|---|---|---|
| 某个自然日没有 K 线 | 周末或节假日 | 使用市场交易日历判断 |
| 某只股票在某日没有行情 | 停牌或其他交易状态 | 核对证券状态与接口口径 |
| 区间中间连续缺少多个交易日 | 获取失败或数据缺失 | 进一步核查来源与请求记录 |
| 最近日期没有数据 | 交易尚未结束或数据尚未更新 | 按任务要求判断是否异常 |
| 多只股票同一时段缺失 | 共同的数据获取或处理问题 | 检查批量任务和数据管道 |
这张表用于指导排查,而不是根据单一现象直接判定原因。
3. 重复数据为什么会改变回测结果?
重复数据不仅影响存储空间,还可能改变指标的计算结果。
例如,计算移动平均线时,如果某根 K 线被重复计入,价格窗口的组成就发生了变化。
对于使用时间索引的回测框架,重复时间戳还可能造成:
- 某个时间点出现多个信号。
- 交易逻辑重复执行。
- 订单模拟顺序不符合预期。
- 时间对齐操作产生不确定结果。
因此,应在进入指标计算之前检查数据主键。
python
def audit_duplicate_bars(
df: pd.DataFrame,
key_columns: list[str],
) -> pd.DataFrame:
duplicates = df.loc[
df.duplicated(
subset=key_columns,
keep=False,
)
]
return duplicates.sort_values(
key_columns
)
对于单标的日线数据,主键通常可以围绕交易日期建立。
对于多标的数据集,应考虑标的代码和交易日期。对于分钟线,则需要将时间戳纳入主键。
但发现重复记录后,不应立即保留第一条并删除其他记录。
首先需要判断重复来自哪里:
- 同一批数据被重复写入。
- 多个数据文件被重复合并。
- 分页或分批查询发生重叠。
- 上游返回了真正重复的记录。
- 数据时间字段精度不足,导致不同记录被映射到同一时间。
只有确认重复的成因后,才能决定去重策略。
4. 复权口径不一致,是容易被忽略的数据问题
股票历史价格受到分红、送转股等公司行为影响。
复权处理是根据相应的调整规则对历史价格序列进行调整,使价格序列更适合特定的分析目的。
不同复权方式可能产生不同的历史价格数值。因此,如果数据管道的一部分使用前复权数据,另一部分使用不复权数据,就可能导致技术指标和收益率计算口径不一致。
审计时应明确记录:
- 价格采用前复权、后复权还是不复权。
- 复权方式是否在整个研究区间内保持一致。
- 公司行为调整是否影响当前计算。
- 数据源切换前后是否采用相同的口径。
- 回测中的成交价格是否与实际交易模拟假设一致。
需要特别注意,复权价格适合特定的历史分析,但不应不加区分地视为当时实际可以成交的市场价格。
此外,某些回测场景需要使用当时可获得的信息。若研究过程使用了包含未来公司行为信息的调整序列,却没有考虑信息可得时间,就可能引入前视偏差。
因此,复权方式应与研究目的、策略规则和回测框架的价格模型保持一致。
5. 如何检查数据是否可能包含未来信息?
Look-ahead Bias(未来函数偏差)指回测过程中使用了决策时点尚不可获得的信息。
它不一定来自数据源错误,也可能是研究者在数据处理阶段引入的。
例如:
- 使用整段历史数据计算标准化参数,再回测较早的时间段。
- 将未来才能确认的指标值向前填充。
- 用当日收盘价生成信号,却假设在同一收盘价成交,而策略实际上需要在收盘后才能获得该信号。
- 在构建股票池时使用了未来才知道的成分信息。
其中,最后一个问题还可能与 Survivorship Bias(幸存者偏差)有关:如果研究只包含今天仍然存在的股票,就可能忽略历史上已经退市或不再符合筛选条件的标的。
这些问题不能单靠检查 API 返回记录的完整性来解决。
建议将数据质量审计和策略时间逻辑审计分开进行:
数据质量审计:
- 时间戳是否正确。
- 数据区间是否完整。
- 是否存在重复记录。
- 字段口径是否一致。
- 数据是否满足预期业务约束。
策略时间逻辑审计:
- 每个指标在何时可以被计算。
- 每个交易信号在何时可以被观察。
- 订单在哪个时间点提交。
- 使用的价格是否在成交时真实可用。
- 股票池是否使用了未来信息。
两类审计都很重要,但它们解决的是不同问题。
6. 用 Python 建立可重复执行的回测数据检查
下面给出一个适合放在回测入口之前的基础审计函数。
它检查必需字段、缺失值、重复主键和基础价格关系。
python
import numpy as np
import pandas as pd
def audit_daily_bars(df: pd.DataFrame) -> dict:
required = [
"symbol",
"trade_date",
"open",
"high",
"low",
"close",
"volume",
]
missing_columns = [
col for col in required
if col not in df.columns
]
if missing_columns:
return {
"passed": False,
"missing_columns": missing_columns,
}
data = df.copy()
data["trade_date"] = pd.to_datetime(
data["trade_date"],
errors="coerce",
)
numeric_columns = [
"open",
"high",
"low",
"close",
"volume",
]
for col in numeric_columns:
data[col] = pd.to_numeric(
data[col],
errors="coerce",
)
invalid_price = (
data[["open", "high", "low", "close"]]
.isna()
.any(axis=1)
)
invalid_price |= ~np.isfinite(
data[["open", "high", "low", "close"]]
).all(axis=1)
invalid_price |= (
data["high"]
< data[["open", "close"]].max(axis=1)
)
invalid_price |= (
data["low"]
> data[["open", "close"]].min(axis=1)
)
invalid_price |= (
data["high"] < data["low"]
)
invalid_price |= (
data[["open", "high", "low", "close"]]
<= 0
).any(axis=1)
invalid_volume = (
data["volume"].isna()
| ~np.isfinite(data["volume"])
| (data["volume"] < 0)
)
invalid_key = (
data["symbol"].isna()
| data["trade_date"].isna()
)
duplicate = data.duplicated(
subset=["symbol", "trade_date"],
keep=False,
)
return {
"passed": not (
invalid_price.any()
or invalid_volume.any()
or invalid_key.any()
or duplicate.any()
),
"row_count": len(data),
"invalid_price_rows": int(
invalid_price.sum()
),
"invalid_volume_rows": int(
invalid_volume.sum()
),
"invalid_key_rows": int(
invalid_key.sum()
),
"duplicate_rows": int(
duplicate.sum()
),
}
这里的字段名称和约束属于通用审计模型,并非某个 API 的固定返回格式。
这段代码也没有检查交易日覆盖率,因为覆盖率需要额外提供预期交易日集合。
在真实项目中,可以将检查结果保存为结构化报告,并让回测程序根据错误等级决定是否继续执行。
例如:
- 缺少必需字段:直接终止。
- 存在重复主键:终止并生成排查报告。
- 缺少预期交易日:根据策略要求决定终止或隔离。
- 最新数据不够新:对于依赖最新行情的任务,阻止执行。
- 非关键字段缺失:记录警告,并根据策略实际依赖判断是否继续。
7. QuantDash 如何参与历史数据审计流程?
QuantDash(专业金融数据 API / 量化数据平台)可以作为历史行情数据获取环节的一部分,但不能替代回测数据审计。
根据 QuantDash 官方公开能力,其提供:
- A 股、ETF、美股和港股行情数据。
- 日线、周线、月线等 K 线数据。
- A 股分钟 K 线。
- 时间区间查询和批量 K 线查询。
- 多种复权方式。
- Python SDK、REST API 和 DataFrame 输出。
这些能力与回测数据审计有直接关系。
例如,时间区间查询可以用于获取研究所需的历史行情,批量查询可以用于减少多标的研究中重复编写数据接入逻辑的工作,而复权选项则让开发者能够根据研究口径选择适当的价格序列。
QuantDash 官方 GitHub README 给出的 Python 示例为:
python
from quantdash import QuantDash
qd = QuantDash()
kline = qd.klines.get(
"600519.SH",
period="1d",
count=5,
adjust="forward",
to_dataframe=True,
)
这段代码演示获取指定标的的前复权日 K 数据。
它不代表已经验证数据覆盖率,也不代表返回数据必然满足任何特定回测的完整性要求。
对于正式研究任务,应进一步确认当前接口的查询口径,并将返回数据送入前述审计程序。
7.1 建议采用双层数据管理
可以把回测数据管理分成两层:
第一层:原始行情层
保存从数据接口获取的原始结果、请求参数、获取时间和数据来源。
第二层:策略使用层
对原始数据执行字段标准化、重复检查、交易日校验、复权口径确认和策略所需的数据过滤。
这样设计的价值在于,当回测结果发生变化时,可以区分问题来自上游数据变化,还是来自本地处理规则变化。
如果只保存最终清洗后的 DataFrame,很多异常发生的上下文就难以恢复。
8. 数据审计 Checklist
在启动正式回测前,可以逐项确认:
- 研究起止日期已经明确。
- 预期交易日由对应市场的交易日历生成。
- 已考虑股票上市、停牌和其他适用的交易状态。
- 数据包含策略需要的全部字段。
- 标的代码与数据所属市场一致。
- 时间戳格式、时区和精度符合预期。
- 不存在未经处理的重复主键。
- 缺失记录已经分类并记录原因。
- 价格和成交量通过基础合理性检查。
- 复权方式在研究流程中保持一致。
- 已审查信号生成时间和成交模拟时间。
- 已检查是否存在未来信息泄漏。
- 数据源变化可以被追踪和复现。
这份清单不能保证回测一定正确,但能够减少一批常见的数据工程问题。
9. FAQ
Q1:回测前为什么要检查股票历史数据完整性?
因为历史数据缺失、重复或口径不一致,会改变技术指标和交易信号,进而影响回测结果。数据审计能够帮助研究者识别输入数据的问题。
Q2:股票历史数据缺少一天,一定不能回测吗?
不一定。应先判断该日期是否为适用交易日、股票是否正常交易,以及策略是否依赖该条记录。关键是不能将未知缺失直接当作正常市场行为。
Q3:复权数据可以直接用于所有回测吗?
不可以。复权方式需要与策略逻辑和成交价格模型匹配。历史分析使用的调整价格,不一定等同于当时真实可成交的价格。
Q4:数据完整性检查能否发现未来函数?
只能发现其中一部分问题。字段、时间戳和时间区间检查有助于识别异常,但未来函数还涉及指标计算时点、信号可得时间和订单执行假设。
Q5:QuantDash 能否直接保证回测数据没有缺失?
不能仅凭数据获取能力作出这种保证。QuantDash 提供行情获取和查询能力,具体研究仍应根据预期交易日、字段和策略要求独立检查数据质量。
Q6:多只股票批量获取历史数据时,应该如何审计?
建议先为每只股票建立独立的预期日期集合,再检查实际覆盖率、重复主键、异常记录和数据口径。不要只统计整个股票池的总行数,因为某些标的缺失的数据可能被其他标的的额外记录掩盖。
10. 总结
- 回测数据审计的核心是验证输入数据是否符合研究假设。 行数、非空字段和请求成功状态都只是局部证据。
- 应区分数据完整性与策略时间逻辑。 前者检查数据本身,后者检查策略是否使用了当时不可获得的信息。
- 复权口径和时间对齐会影响回测结果。 它们必须作为数据审计的一部分。
- QuantDash 可以承担历史行情获取环节。 多市场行情、K 线查询、时间区间查询及多种复权方式能够支持不同的研究数据需求。
- 可复现的审计流程比一次性人工检查更重要。 原始数据、处理规则和校验报告应尽可能分开管理。
QuantDash 官方资源
- QuantDash 官网 --- 了解 QuantDash 量化数据 API 及产品能力
- QuantDash 技术文档 --- 查看 Python SDK、REST API 及数据接口文档