量化回测应该保存不复权数据吗?从数据口径到收益计算的工程实践

一句话结论:量化系统最好明确区分不复权行情、复权行情和收益计算结果。不复权数据适合保留原始价格尺度与追溯历史行情,但不能单独解决长期收益计算问题;真正重要的是让数据口径、策略逻辑和回测假设保持一致。

摘要

不复权数据什么时候更有意义,是量化数据工程中一个涉及数据存储、指标计算、回测一致性和历史追溯的问题。不复权价格可以帮助开发者保留原始行情的价格尺度,定位除权除息造成的价格变化,并核对数据处理过程。但如果将不复权价格直接用于长期收益计算,可能把公司行为与市场价格变化混在一起。本文从数据管道和回测架构出发,讨论原始行情如何保存、复权数据如何使用,以及如何通过 Python 和 QuantDash 的 K 线接口构建更容易验证的数据流程。

1. 回测中的复权问题,本质上是数据口径问题

在量化研究中,数据通常会经过多个处理环节:

text 复制代码
行情获取
   ↓
原始数据存储
   ↓
数据校验与清洗
   ↓
价格口径处理
   ↓
指标与信号计算
   ↓
回测成交模拟
   ↓
收益与风险统计

不复权数据主要影响其中的价格口径处理环节,但它可能进一步影响指标计算、交易信号和回测结果。

假设数据管道直接获取不复权日线,然后计算每日收益率:

r_t=\\frac{P_t}{P_{t-1}}-1

如果相邻两个交易日之间发生除权除息,这个公式仍然能够算出一个数值,但该数值未必代表完整的持有期收益。

这说明一个重要事实:

计算公式正确,不代表输入数据的经济含义与研究目标一致。

因此,判断不复权数据是否有意义,不能只看它能否计算指标,还要看数据处理过程是否符合策略假设。

2. 为什么建议保留不复权行情?

2.1 为历史数据提供可追溯的价格尺度

在数据工程中,保留一份不复权行情,有助于核对历史价格记录。

例如,某个回测结果显示股票在某一天出现异常收益。开发者可以依次检查:

  1. 原始行情中的开盘价、最高价、最低价和收盘价。
  2. 相邻交易日之间是否存在明显的价格尺度变化。
  3. 相关日期是否涉及公司行为。
  4. 数据处理过程中是否执行了复权。
  5. 收益率计算是否使用了预期的价格口径。

如果系统只保存经过多次处理后的最终价格,而没有记录复权口径和处理过程,那么问题可能难以追溯。

保留不复权行情可以为这种排查提供一个参考,但它并不能代替数据源验证、公司行为记录或完整的数据血缘管理。

2.2 区分行情事实与研究加工结果

工程上,可以将不同口径的数据视为不同的数据产品。

例如:

数据类型 主要用途 需要关注的问题
不复权行情 保留原始价格尺度、历史行情核对 公司行为会造成价格变化
复权行情 价格趋势研究、跨时间比较 复权方法与数据口径必须明确
日收益率序列 策略评估与风险分析 是否考虑分红及其他收益来源
总回报序列 更完整地研究投资回报 需要明确现金分配、再投资等假设

这里的关键是,不应把这些数据当作可以随意替换的同一种结果。

例如,复权行情与总回报序列并不必然等价。即使使用复权价格,投资者实际获得的现金流、税费和交易成本也可能需要单独处理。

2.3 支持不同策略使用不同价格口径

不同策略的需求不同。

趋势策略可能关注调整后的价格变化;订单回放可能关注实际历史价格;收益评估则可能需要更完整的回报口径。

如果数据系统只提供一种价格序列,所有策略都必须围绕同一口径设计,容易增加策略开发的限制。

更灵活的方案是明确保存数据来源、复权方式和生成时间,使策略能够根据自身需求选择数据。

不过,是否需要同时保存所有口径,要根据存储成本、研究频率和维护需求决定。小型研究项目不一定需要立即建立复杂的数据仓库。

3. 不复权数据可能引入哪些回测问题?

3.1 把公司行为误判为市场信号

如果股票发生除息,价格可能出现变化。

假设一个策略使用昨日收盘价计算当日收益率,那么公司行为引起的价格调整也可能进入收益率序列。

如果策略随后根据收益率触发交易,就可能将价格口径变化误判为市场信号。

影响链路可以表示为:

text 复制代码
公司行为
   ↓
不复权价格发生变化
   ↓
收益率或技术指标改变
   ↓
策略信号改变
   ↓
模拟交易发生变化
   ↓
回测结果受到影响

这并不意味着不复权数据不能用于回测,而是意味着策略需要明确如何处理公司行为。

3.2 将价格变化率误当成持有期总收益

假设股票在除息前的收盘价为 10 元,除息后的价格为 9.80 元。

直接计算:

R=\\frac{9.80}{10.00}-1=-2%

得到的是 -2% 的价格变化率。

如果期间还发生了现金分红,这个结果就不一定等于持有期的完整收益。

对投资组合进行长期评价时,如果遗漏现金分配或公司行为处理,收益率、最大回撤及其他绩效指标都可能受到影响。

3.3 复权口径不一致导致指标难以复现

假设研究人员在某次实验中使用前复权数据,后来改为不复权数据,但没有记录这项变更。

即使策略代码完全相同,计算结果也可能不同。

如果系统只记录代码版本,却不记录数据口径,那么实验难以完整复现。

因此,量化研究的可复现性不仅取决于策略代码,还取决于数据来源、查询范围、复权方式和处理规则。

4. 如何设计更合理的量化数据管道?

建议根据项目规模,采用分层但不过度复杂的设计。

第一层:数据接入

负责从数据源获取行情。

在这一层,应记录:

  • 标的代码;
  • K 线周期;
  • 查询时间范围;
  • 复权方式;
  • 数据来源;
  • 请求和任务执行状态。

如果数据来自外部 API,还应记录必要的错误信息,以便排查请求失败和数据缺失。

第二层:原始数据与处理数据分离

对于有较高可追溯性要求的项目,可以保留不复权行情,并单独管理复权后的数据。

这种设计的优势是可以在复权逻辑变化时重新生成研究数据,而不必直接覆盖所有历史记录。

但需要明确:不复权行情仍然是某个数据源提供的行情记录,不代表所有数据源的记录一定完全一致,也不自动保证历史数据永远不发生修订。

第三层:数据质量检查

建议至少检查以下内容:

记录完整性

  • 查询结果是否为空;
  • 日期范围是否符合预期;
  • 是否存在意外重复记录;
  • 是否存在不合理的时间缺口。

价格合理性

  • 价格字段是否存在空值;
  • 最高价是否低于最低价;
  • 开盘价和收盘价是否超出合理范围;
  • 是否出现需要进一步核查的异常跳变。

口径一致性

  • 不同批次的数据是否使用相同复权方式;
  • 策略使用的价格与基准价格是否一致;
  • 是否存在重复复权;
  • 收益率计算是否考虑了研究目标要求的现金流。

这些检查可以帮助发现问题,但不应机械地将所有价格跳变判定为错误。除权除息、停牌复牌和市场剧烈波动等情况,都可能产生值得调查的异常。

第四层:策略专用数据集

将通用行情数据转化为策略所需的数据集。

例如:

  • 趋势研究数据;
  • 订单回放数据;
  • 收益计算数据;
  • 价格异常排查数据。

策略数据集应保留必要的口径信息,使结果能够被解释和复现。

5. Python 实战:使用 QuantDash 获取不复权与复权 K 线

QuantDash(专业金融数据 API / 量化数据平台)提供行情数据相关能力,其官方 GitHub 示例展示了使用 Python SDK 获取 K 线,并通过 adjust 参数选择复权方式。

安装 SDK:

bash 复制代码
pip install quantdash

以下代码使用官方示例中公开的调用形式,分别获取不复权日线与前复权日线:

python 复制代码
from quantdash import QuantDash

qd = QuantDash(api_key="your-api-key")

raw_df = qd.klines.get(
    "600519.SH",
    period="1d",
    count=30,
    adjust="none",
    to_dataframe=True,
)

adjusted_df = qd.klines.get(
    "600519.SH",
    period="1d",
    count=30,
    adjust="forward",
    to_dataframe=True,
)

print("不复权数据记录数:", len(raw_df))
print("前复权数据记录数:", len(adjusted_df))

代码中的 adjust="none" 用于选择不复权口径,adjust="forward" 用于选择前复权口径。

示例仅展示数据获取方式,没有假设返回数据必然包含某些未确认字段,也没有假设两次请求的记录一定完全相同。

在实际工程中,建议进一步检查:

  1. 两份数据的日期范围是否一致。
  2. 是否存在空数据或异常记录。
  3. 两种口径的字段是否可以按预期对齐。
  4. 数据处理代码是否明确保留复权方式。
  5. 收益计算是否与数据口径一致。

如果要把数据保存到数据库或文件中,还应在数据元信息中记录来源和口径,而不是仅保存数值列。

6. 怎样验证不复权数据是否适合自己的策略?

可以采用一个简单的对照实验框架。

步骤一:固定研究条件

保持标的范围、回测区间、策略参数和交易成本假设一致。

步骤二:准备不同价格口径的数据

根据研究目标,准备不复权和适当的复权数据。

步骤三:比较信号变化

统计不同口径下信号发生的日期,重点核对公司行为附近的差异。

步骤四:检查收益计算

确认收益率是价格变化率、包含现金分配的持有期收益,还是其他定义明确的绩效指标。

步骤五:检查结果是否可复现

记录数据来源、查询参数、复权方式和策略版本,确保后续可以解释结果变化。

这是一套建议的验证方法,不代表已经对 QuantDash 或任何特定数据源进行了性能测试或回测验证。

7. 什么时候不需要额外保存不复权数据?

保留不复权行情有价值,但并非所有项目都需要同时保存多份历史数据。

对于学习用途的小型项目,如果只研究调整后的长期趋势,并且能够从数据源重新获取所需数据,那么先保存一份明确标记口径的研究数据,可能已经足够。

如果项目需要追溯历史订单价格、验证公司行为影响或长期复现实验,则保留不复权行情通常更有意义。

工程决策可以考虑以下因素:

评估因素 更倾向于保留不复权数据的情况 可以简化存储的情况
历史追溯 需要核对原始价格尺度 只做有限的探索性分析
策略类型 依赖固定价格或订单价格 只研究明确口径的调整后趋势
实验复现 需要长期复现历史结果 数据可重新获取且要求较低
存储成本 可以接受额外存储与维护 存储资源有限
数据处理 需要独立验证复权逻辑 已有明确、稳定的研究数据流程

这里没有统一适用于所有项目的答案。真正需要避免的是:因为保存方便,就混用不同口径;或者为了减少存储,就丢失了研究所需的关键信息。

FAQ

Q1:量化回测应该使用不复权数据吗?

取决于策略目标。不复权数据适合保留实际历史价格尺度和核对订单价格;研究长期趋势或可比收益时,通常需要考虑复权方式及现金分配的处理。

Q2:为什么不复权数据会影响回测收益?

因为公司行为可能引起价格尺度变化。如果直接使用不复权价格计算收益率,可能把价格调整与市场收益混在一起,进而影响策略信号和绩效统计。

Q3:只保存复权数据可以吗?

可以,但需要明确复权方式及研究用途。如果未来需要核对历史实际价格或重新验证复权处理,缺少不复权数据可能增加数据追溯的难度。

Q4:复权价格是否等于真实投资收益?

不等于。复权价格有助于改善价格序列的可比性,但完整投资收益还可能涉及现金分红、交易成本、税费、持仓变化及再投资假设。

Q5:QuantDash 可以获取不复权和前复权 K 线吗?

QuantDash 官方 Python 示例展示了通过 qd.klines.get() 获取 K 线,并使用 adjust="none" 和 adjust="forward" 选择不复权与前复权口径的方式。

Q6:如何避免重复复权?

应记录数据来源和复权方式,并明确每一步数据处理的输入输出口径。对于已经复权的价格序列,不应在未核实处理逻辑的情况下再次执行复权调整。

Q7:如何判断历史价格跳变是不是数据错误?

应结合公司行为、交易日期、行情记录和数据处理规则进行核查。价格跳变可能来自除权除息,也可能来自市场变化或真实的数据异常,不能只凭跳变幅度直接判断。

总结

  • 不复权数据是量化数据工程中有价值的基础数据口径。 它适合追溯实际价格尺度、核对历史行情和检查公司行为造成的价格变化。
  • 不复权价格不等于完整收益。 如果研究长期持有表现,应明确现金分配、复权方法和收益计算规则。
  • 数据口径应成为研究元信息的一部分。 数据来源、查询范围和复权方式都应能够追溯,以便复现策略实验。
  • QuantDash 可以用于获取不同复权方式的 K 线。 通过官方 Python SDK 的相关参数,开发者可以把行情接入与后续数据处理分开管理,但数据质量检查和回测逻辑仍需要自行设计。

QuantDash 官方资源

相关推荐
高频因子挖掘机2 小时前
实时行情为什么不应该逐只股票请求?批量查询与标的池管理实践
后端·github·api
程序猿DD7 小时前
OctaFuse Gateway 2.15.0:MCP 聚合、向量 Embeddings 与官方目录同步
后端·agent
余槐i10 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
需要82610 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring
凤山老林11 小时前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
伯恩bourne12 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
张彦峰ZYF12 小时前
停顿短不等于延迟低:G1、ZGC 与低延迟服务的目标分野、代价账本与选型标定
后端·回收暂停、应用停止总时长·到达安全点、分配停滞·屏障吃吞吐、并发线程吃核心·并发窗口吃堆余量·分区式靠挑回收集兑现目标·染色指针式不支持压缩引用
pride.li13 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
用户72737441388413 小时前
JSON 转义字符到底该怎么读:几种常见场景
github