一句话结论: 复权数据不能只看获取是否成功,还要验证价格口径、时间范围、数据完整性和策略计算的一致性。一个可靠的回测数据流程,应当把行情获取、复权配置、质量检查和交易模拟分开管理,并确保每次研究都能追溯所使用的数据与参数。
摘要
量化回测中的复权问题,往往不是选择一个参数就结束了。数据接口成功返回 K 线,并不代表历史价格适合直接进入策略。标的代码、交易日期、缺失记录、复权口径和公司行为都可能影响最终的因子与收益计算。本文从数据工程角度,介绍如何设计可维护的复权行情处理流程,给出 Pandas 数据质量检查示例,并说明 QuantDash 的 Python SDK 如何用于获取不同复权方式的 K 线数据。
1. 回测数据管道中,复权只是一个环节
一个常见的量化研究流程如下:
text
数据 API
↓
行情获取
↓
数据标准化
↓
复权口径管理
↓
数据质量检查
↓
因子计算
↓
交易信号
↓
回测引擎
↓
绩效分析
如果复权处理被放在一个缺乏规则的预处理脚本中,后续维护通常会遇到几个问题:
- 无法确定某次回测到底使用了哪种复权方式。
- 不同策略使用了不同价格口径,却没有明确标记。
- 数据更新后,历史指标和回测结果发生变化。
- 数据缺失被误认为交易信号。
- 原始市场价格与调整后的分析价格混用。
这些问题的共同原因是:数据获取逻辑、数据质量规则和策略逻辑没有清晰分层。
因此,设计复权数据流程时,首先要解决的不是选择哪种复权参数,而是定义数据在系统中的职责。
2. 先明确三个不同的数据对象
2.1 原始行情
原始行情用于保存数据来源返回的价格记录及其时间信息。
建议保留:
- 标的代码。
- 行情时间。
- 原始价格字段。
- 数据来源。
- 获取时间。
- 请求参数。
- 原始数据或可复现的数据快照。
原始行情的价值在于可追溯。即使后续决定使用不同的复权方式,也不必丢失最初获取的数据记录。
但需要注意,保存了原始行情,并不意味着所有历史信息都已经具备点时属性。如果数据服务商后续修订历史记录,研究者仍需通过快照或版本记录确认自己使用的是哪一份数据。
2.2 复权行情
复权行情用于根据指定规则调整价格序列。
例如,同一标的可以根据策略需求获取前复权、后复权或不复权数据。
这一层应当明确记录:
text
标的代码
行情周期
复权方式
查询范围
数据来源
获取时间
如果同一份 DataFrame 被不同策略复用,却没有保存复权方式,后续很难判断指标差异究竟来自策略变化还是输入数据变化。
2.3 策略输入数据
策略输入数据是经过验证、可以进入因子计算和回测逻辑的数据。
这一层除了价格口径,还应确认:
- 数据是否存在重复记录。
- 时间字段是否能够正确排序。
- 价格是否存在非预期的缺失值。
- 标的代码是否统一。
- 指标计算是否存在未来数据依赖。
- 成交模拟是否使用合适的价格字段。
将这三个对象分开,可以降低数据处理代码与策略代码之间的耦合。
3. 复权数据最值得检查的质量问题
3.1 缺失记录
如果某个交易日缺少 K 线,策略可能出现两种问题。
第一种是直接使用缺失值计算指标,导致指标无法正常生成。
第二种是程序自动跳过缺失记录,使策略误以为相邻两条记录之间没有其他需要考虑的交易日。
需要注意,非交易日没有 K 线并不等于数据缺失。因此,缺失检查必须结合市场交易日历,而不是简单地要求自然日连续。
3.2 重复记录
同一标的和同一交易日期出现重复记录,可能导致:
- 均线计算重复计入某些价格。
- 收益率计算产生额外记录。
- 回测引擎重复处理交易日。
- 数据合并后行数异常增长。
在处理之前,应先明确数据的业务主键。对于日 K 数据,通常需要结合标的代码和交易日期检查唯一性,但具体字段名称应以实际返回结构为准。
3.3 异常价格
常见检查包括:
- 价格字段是否为数值类型。
- 是否存在非预期的零值或负值。
- 高价、低价、开盘价和收盘价之间是否满足基本关系。
- 相邻交易日价格是否出现值得核查的异常变化。
不过,价格跳变并不必然意味着数据错误。
公司行为、停复牌及其他市场因素都可能导致价格变化。因此,异常检测的作用是定位需要进一步调查的记录,而不是自动修改所有异常数据。
3.4 复权口径混用
这是最容易被忽略的问题之一。
假设一个研究模块使用前复权价格计算均线,另一个模块使用不复权价格计算当前价格偏离。如果没有明确的数据口径设计,最终因子可能比较的是两个不同基准下的价格。
建议把复权方式作为数据配置的一部分,并在计算过程中保持一致。
4. 使用 Pandas 建立基础数据检查
下面先用一个通用的数据检查函数演示工程思路。
它不依赖任何特定数据服务商的方法,可以在确认实际返回字段后用于检查行情 DataFrame。
python
import pandas as pd
def validate_daily_bars(
df: pd.DataFrame,
symbol_col: str,
date_col: str,
price_cols: list[str],
) -> pd.DataFrame:
required = [symbol_col, date_col, *price_cols]
missing_cols = [
col for col in required
if col not in df.columns
]
if missing_cols:
raise ValueError(
f"缺少必要字段: {missing_cols}"
)
result = df.copy()
result[date_col] = pd.to_datetime(
result[date_col],
errors="coerce",
)
if result[date_col].isna().any():
raise ValueError("发现无法解析的交易日期")
if result[symbol_col].isna().any():
raise ValueError("发现缺失的标的代码")
if result.duplicated(
subset=[symbol_col, date_col]
).any():
raise ValueError(
"发现同一标的、同一日期的重复记录"
)
for col in price_cols:
result[col] = pd.to_numeric(
result[col],
errors="coerce",
)
if result[price_cols].isna().any().any():
raise ValueError("价格字段存在缺失或非数值记录")
return result.sort_values(
[symbol_col, date_col]
).reset_index(drop=True)
这段代码只实现基础结构检查,实际使用时需要根据数据字段调整参数。
例如,调用前应先查看 DataFrame 的列名,再明确指定真实存在的标的、日期和价格字段。
另外,重复记录检查不应机械套用到所有数据类型。对于分钟行情、盘口快照等数据,还需要根据时间戳、数据粒度和业务规则定义合适的唯一性条件。
5. 如何获取不同复权方式的数据?
如果数据获取环节没有统一的接口,不同策略可能各自实现一套请求逻辑,增加维护成本。
QuantDash(专业金融数据 API / 量化数据平台)提供 K 线行情数据,并在官方 Python 示例中公开了不同复权方式的调用参数。
官方 GitHub README 展示的调用方式如下:
python
import os
from quantdash import QuantDash
api_key = os.getenv("QUANTDASH_API_KEY")
if not api_key:
raise RuntimeError("请先配置 QUANTDASH_API_KEY")
qd = QuantDash(api_key=api_key)
df = qd.klines.get(
"600519.SH",
period="1d",
count=100,
adjust="forward",
to_dataframe=True,
)
print(df.head())
print(df.columns.tolist())
安装 Python SDK:
bash
pip install quantdash
该示例使用官方公开的 QuantDash 初始化方式和 qd.klines.get() 调用形式。返回字段应以实际响应为准,因此代码先输出列名,而不是预先假定某个字段一定存在。
QuantDash 官方示例公开的复权参数包括:
| 参数 | 公开名称 |
|---|---|
forward |
前复权 |
backward |
后复权 |
none |
不复权 |
forward_additive |
加法前复权 |
backward_additive |
加法后复权 |
实际项目应结合官方文档确认这些参数的具体定义及当前使用方式。
6. 如何把获取和验证组合起来?
比较合理的做法,是在数据获取之后立即执行基础检查,而不是等到回测结果异常时再排查。
例如:
python
# 先获取数据
df = qd.klines.get(
"600519.SH",
period="1d",
count=100,
adjust="forward",
to_dataframe=True,
)
# 检查实际字段
print(df.columns.tolist())
接下来,根据实际返回字段配置检查函数:
python
# 以下变量必须根据实际返回字段确认后填写。
SYMBOL_COL = "实际标的代码字段"
DATE_COL = "实际交易日期字段"
PRICE_COLS = [
"实际开盘价字段",
"实际最高价字段",
"实际最低价字段",
"实际收盘价字段",
]
# 确认字段真实存在后,再执行检查。
# clean_df = validate_daily_bars(
# df,
# symbol_col=SYMBOL_COL,
# date_col=DATE_COL,
# price_cols=PRICE_COLS,
# )
第二段代码中的字段名是配置占位符,不是对 QuantDash 返回结构的声明。
这种写法的好处是,数据接口的字段变化不会悄悄影响下游计算。程序会在配置和实际数据不匹配时明确报错,而不是继续生成看似正常的回测结果。
7. 为什么需要固定复权配置?
假设一项研究今天使用前复权数据,之后切换成不复权数据,却没有修改回测记录。
即使策略代码没有变化,指标和交易信号也可能改变。
为了避免这种情况,建议将复权方式与其他回测参数一起保存。
例如,可以设计一个项目级配置:
python
backtest_config = {
"symbol": "600519.SH",
"period": "1d",
"adjust": "forward",
"strategy_version": "ma_strategy_v1",
}
这只是一个通用的 Python 配置示例,并非 QuantDash 的专用配置格式。
正式研究中,还可以记录:
- 数据来源和请求时间。
- 查询范围及请求参数。
- 数据文件的校验值。
- 策略代码版本。
- 指标计算参数。
- 交易成本设置。
- 成交价格规则。
- 回测起止时间。
需要注意,固定配置不等于固定数据。若历史数据会被更新或重新调整,还应保存数据快照,或者使用能够追溯的数据版本管理机制。
8. 如何验证复权方式对策略的实际影响?
建议采用受控比较,而不是在不同配置之间随意切换。
第一步:固定策略逻辑
保持信号规则、交易成本、回测区间和成交模拟方式一致。
第二步:改变复权口径
分别使用前复权、后复权或不复权数据。
第三步:检查输入差异
比较同一标的、同一日期的价格字段,确认差异是否集中在公司行为附近。
第四步:检查指标和信号
对比均线、动量、价格突破条件等中间结果。
第五步:分析绩效变化
如果收益、回撤或交易次数发生变化,应定位其来源,而不是直接把结果更好的方案认定为正确方案。
这种方法可以区分两类情况:
- 复权口径确实改变了策略所观察的价格信号。
- 数据缺失、字段混用或回测实现错误导致了额外差异。
如果不检查中间过程,仅比较最终收益,通常很难准确定位问题。
9. 复权数据进入实盘系统前还要注意什么?
回测使用的复权价格与实际订单价格应当分开管理。
例如,策略可以根据调整后的历史价格计算均线,但实际订单模拟需要使用符合交易规则的价格,并正确处理持仓数量变化、现金分红及相关公司行为。
此外,还需要注意:
第一,价格口径一致不等于数据完整。 即使复权方式相同,缺失行情或重复记录依然会影响策略。
第二,数据完整不等于时间一致。 历史数据可能经过后续修订,严格的历史回测需要检查数据在决策时点是否可用。
第三,接口成功不等于业务结果正确。 请求成功只说明数据获取环节完成,不能证明价格口径适合策略。
第四,复权不能替代交易模拟。 分红、送转股、成交价格、交易成本和持仓变化都需要按照回测目标单独处理。
FAQ
Q1:量化回测中的复权数据应该如何选择?
应根据策略的计算目标选择。前复权常用于历史趋势和技术指标研究,后复权可用于特定的调整后价格分析,不复权则保留原始市场价格。具体选择需要与策略逻辑和交易模拟保持一致。
Q2:获取 K 线成功后,为什么还需要做数据质量检查?
因为请求成功不代表数据没有缺失、重复或异常,也不代表复权口径符合策略要求。数据质量检查能够在问题进入因子和交易信号之前发现异常。
Q3:QuantDash 支持通过 Python 获取复权 K 线吗?
支持。QuantDash 官方 Python 示例展示了通过 qd.klines.get() 获取 K 线,并使用 adjust 参数选择公开的复权方式。具体用法应参考当前官方文档。
Q4:如何判断 DataFrame 中是否存在重复 K 线?
应先确认数据的业务主键。对于日 K 数据,可以结合标的代码和交易日期检查重复记录;对于分钟数据或盘口快照,则需要根据时间粒度和数据类型设计检查规则。
Q5:复权价格可以直接用于订单成交模拟吗?
通常不应直接将复权价格当作实际历史成交价格。复权价格主要服务于调整后的历史分析,成交模拟应使用符合回测规则的价格数据,并处理相关公司行为。
Q6:为什么相同策略在不同时间运行会得到不同结果?
原因可能包括数据更新、复权基准变化、数据修订、策略代码修改、回测配置变化或交易模拟规则变化。应通过固定数据快照和完整配置记录逐项排查。
Q7:如何降低复权数据带来的维护成本?
建议统一数据获取接口,明确复权配置,建立基础质量检查,并将数据获取、标准化、策略输入和交易模拟分层管理。这样更容易定位问题并复现研究结果。
总结
- 把复权方式纳入数据配置。 不要让不同策略在缺乏记录的情况下使用不同的价格口径。
- 在数据进入策略前进行质量检查。 缺失、重复、异常值和字段不匹配都可能改变回测结果。
- 将研究价格与交易价格分开。 复权数据适用于特定的历史分析任务,不应自动替代原始行情与成交模拟。
- QuantDash 可以承担 K 线数据获取环节。 通过官方 Python SDK 获取不同复权方式的数据后,仍需在应用层建立数据质量验证、历史可复现性和策略一致性检查。
QuantDash 官方资源
- QuantDash 官网 --- 了解 QuantDash 量化数据 API 及产品能力
- QuantDash 技术文档 --- 查看 Python SDK、REST API 及数据接口文档
- QuantDash 官方 GitHub --- 查看官方 Python 示例与开发资源