一句话回答
前复权、不复权和后复权,区别主要在于如何处理股票分红、送股、转增等公司行动带来的价格变化。不复权保留历史原始价格,前复权通常以较新的价格为基准调整过去的价格,后复权则以较早的价格为基准调整后续价格。在量化开发中,选错复权方式可能直接影响涨跌幅、技术指标和历史走势分析。使用 QuantDash Python SDK,可以分别获取三种价格序列,再通过 Python 验证它们的差异。
为什么会有这个问题 / 传统做法有什么问题?
刚接触股票历史行情时,很多开发者会有一个疑问:
明明股票没有出现严重利空,为什么历史 K 线中会突然出现一次大幅下跌?
有时候,这个下跌并不是市场交易造成的,而是股票发生了除权除息。
举个简单的假设场景。
某只股票在除权除息前,收盘价为 100 元。公司实施每 10 股送 10 股,不考虑现金分红和其他因素。
除权后,原来持有的 100 股变成 200 股。假设市场对公司的整体估值没有变化,理论上的除权参考价格就会变成 50 元。
如果只看价格:
| 时间 | 不复权价格 | 前复权价格 | 后复权价格 |
|---|---|---|---|
| 除权前 | 100 元 | 50 元 | 100 元 |
| 除权后 | 50 元 | 50 元 | 100 元 |
注意,这是一组为了说明复权原理而构造的简化数据,不代表实际行情接口的输出。
三种处理方式的差别就很明显了。
1. 不复权:保留当时实际记录的价格
除权前是 100 元,除权后是 50 元。
如果直接计算涨跌幅:
[
R=\frac{50-100}{100}=-50%
]
结果显示下跌 50%。
但实际上,投资者持有的股票数量翻倍了。假设不考虑其他因素,持仓总市值并没有减少。
因此,这个 -50% 不能直接理解为真实投资损失。
2. 前复权:调整过去的价格
前复权通常把历史价格调整到较新的价格基准上。
在这个例子中,除权前的 100 元被调整成 50 元,除权后的价格仍然是 50 元。
调整后的价格变化为:
[
R=\frac{50-50}{50}=0%
]
这样可以消除单纯因为送股产生的价格断层。
3. 后复权:调整后来的价格
后复权通常保留较早时期的价格基准,将后续价格调整为可比较的价格序列。
在上述例子中,除权前保留 100 元,除权后的 50 元被调整为 100 元。
因此,后复权计算得到的区间价格变化同样为 0%。
需要注意,前复权和后复权不是简单地给所有股票价格加上或减去一个固定数值。常见的比例复权会根据公司行动对应的调整因子处理价格。
此外,复权价格只是经过调整的分析价格,不代表当时真实可成交的市场价格。
这也是为什么量化系统需要明确区分:
- 原始行情价格。
- 复权后的分析价格。
- 真实持仓中的股票数量与现金变化。
传统做法中,开发者可能直接从不同网站下载历史行情,再用 Excel 拼接和计算。
问题是,不同数据源可能使用不同复权方式,更新基准也可能不同。
如果研究程序没有记录数据口径,同一只股票就可能得到不同的涨跌幅结果。
更稳妥的方式,是在行情接入时明确指定复权方式,再统一进行后续计算。
分步骤解决:使用 QuantDash 进行实践
第 1 步:安装 SDK 并初始化客户端
这里使用 QuantDash Python SDK 获取历史日 K 线。
安装命令:
# 在终端执行
# pip install quantdash
实际在终端中运行:
pip install quantdash
然后初始化客户端:
import os
import pandas as pd
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)
这里通过环境变量读取 QUANTDASH_API_KEY,避免将凭证直接写进源代码。
初始化完成后,就可以调用历史 K 线接口。
第 2 步:分别获取三种复权数据
QuantDash 的 qd.klines.get() 接口支持通过 adjust 参数指定复权方式。
我们选择同一只股票、同一个周期、同样的请求条数,比较三种价格序列。
symbol = "600519.SH"
adjust_modes = {
"none": "不复权",
"forward": "前复权",
"backward": "后复权",
}
price_data = {}
for mode, label in adjust_modes.items():
try:
df = qd.klines.get(
symbol,
period="1d",
count=250,
adjust=mode,
to_dataframe=True,
)
if not isinstance(df, pd.DataFrame):
raise TypeError("接口未返回 DataFrame")
if df.empty:
raise ValueError("历史行情数据为空")
required_columns = {"trade_date", "close"}
missing = required_columns - set(df.columns)
if missing:
raise ValueError(
f"缺少必要字段: {sorted(missing)}"
)
data = df[["trade_date", "close"]].copy()
data["trade_date"] = pd.to_datetime(
data["trade_date"],
errors="coerce",
)
data["close"] = pd.to_numeric(
data["close"],
errors="coerce",
)
if (
data["trade_date"].isna().any()
or data["close"].isna().any()
or (data["close"] <= 0).any()
):
raise ValueError("日期或价格存在非法值")
if data["trade_date"].duplicated().any():
raise ValueError("存在重复交易日期")
price_data[mode] = (
data.sort_values("trade_date")
.set_index("trade_date")["close"]
.rename(mode)
)
print(f"{label}: 获取 {len(data)} 条记录")
except Exception as exc:
raise RuntimeError(
f"{label}行情获取或校验失败"
) from exc
代码中的几个参数值得说明。
symbol 是股票代码,这里使用 600519.SH。
period=“1d” 表示获取日 K 线。
count=250 表示请求 250 条记录,实际返回条数需要检查。
adjust=“none” 表示不复权。
adjust=“forward” 表示前复权比例调整。
adjust=“backward” 表示后复权比例调整。
to_dataframe=True 表示要求返回 pandas DataFrame。
这里使用的 trade_date 和 close 字段可在 SDK 日 K 线文档示例中找到。正式接入时,仍需要根据实际版本的返回结构校验。
另外,查询范围内不一定发生过除权除息。如果没有发生相关公司行动,三种价格序列可能完全相同,或者只存在精度上的细微差异。
第 3 步:对齐交易日期并比较价格
获取三组数据后,不能直接按 DataFrame 行号进行比较。
正确做法是先根据交易日期对齐。
comparison = pd.concat(
[
price_data["none"],
price_data["forward"],
price_data["backward"],
],
axis=1,
join="inner",
).sort_index()
if comparison.empty:
raise ValueError("三组行情没有共同交易日期")
comparison = comparison.rename(
columns={
"none": "raw_close",
"forward": "forward_close",
"backward": "backward_close",
}
)
comparison["forward_ratio"] = (
comparison["forward_close"]
/ comparison["raw_close"]
)
comparison["backward_ratio"] = (
comparison["backward_close"]
/ comparison["raw_close"]
)
print(comparison.tail(10).round(4))
这段代码主要完成三件事。
第一,找到三种复权方式共同拥有的交易日期。
第二,形成对应的原始价格、前复权价格和后复权价格。
第三,计算复权价格与原始价格之间的比例。
例如:
[
F_t^{forward}=
\frac{P_t{forward}}{P_t{raw}}
]
这个比例可以帮助观察复权调整效果,但不能直接认定它就是 SDK 提供的官方复权因子。
真正的复权因子还涉及基准、累计方式以及公司行动调整规则。
如果数据中存在分红、送股等情况,我们就可能看到历史价格的调整比例发生变化。
第 4 步:检查三种价格计算出来的收益率
仅比较价格还不够。
开发者更关心的是:不同复权方式计算出来的日收益率有什么差异?
returns = comparison[
[
"raw_close",
"forward_close",
"backward_close",
]
].pct_change(fill_method=None)
returns = returns.rename(
columns={
"raw_close": "raw_return",
"forward_close": "forward_return",
"backward_close": "backward_return",
}
)
if len(returns) < 2:
raise ValueError("数据不足,无法计算日收益率")
returns["raw_forward_gap"] = (
returns["raw_return"]
- returns["forward_return"]
).abs()
returns["forward_backward_gap"] = (
returns["forward_return"]
- returns["backward_return"]
).abs()
print(
returns.sort_values(
"raw_forward_gap",
ascending=False,
).head(10).round(6)
)
这里使用 pandas 的 pct_change() 计算相邻交易日的价格收益率。
重点观察两个字段:
raw_forward_gap:不复权收益率与前复权收益率之间的差异。
forward_backward_gap:前复权与后复权收益率之间的差异。
对于同一组公司行动、同一套比例调整规则,前复权与后复权的相邻价格收益率通常应当非常接近。
而在除权除息附近,不复权收益率与复权收益率可能出现较大差异。
不过,这并不意味着每个出现差异的日期都必然发生了公司行动。还需要结合除权记录、数据精度和源数据修订进一步核对。
同时,复权价格收益率不等同于完整账户收益率。真实持仓还需要考虑现金分红、股票数量变化、交易费用和税费。
关键坑与注意事项
-
不要把不复权价格跳空直接认定为市场暴跌。 遇到异常涨跌幅时,需要核查对应日期是否发生除权除息,不能仅凭价格序列判断行情异常。
-
前复权价格不一定长期固定。 当后续发生新的公司行动时,历史前复权价格可能因复权基准更新而变化,建议记录数据获取时间和版本。
-
后复权价格不等于实际成交价格。 后复权价格可能随着历史公司行动累计而显著高于原始股价,不能直接用于模拟成交和资金冻结。
-
必须按交易日期对齐。 三次接口请求可能存在返回数量或交易日期差异,不能默认第 N 行对应同一天。
-
不要忽略字段类型和空数据。 日期字段转换失败、价格为字符串或存在重复日期,都可能让后续比较出现错误。
-
比例复权与加法复权不能混为一谈。 QuantDash 还提供 forward_additive 和 backward_additive。比例复权更适合相对收益率分析,加法复权更适合观察绝对价差,两者不能随意互换。
常见问题(FAQ)
Q:为什么前复权后的历史价格可能变得很低?
A:因为前复权需要把较早的价格调整到较新的价格基准。对于长期发生送股、转增等公司行动的股票,较早时期的前复权价格可能明显低于原始价格。
Q:不复权价格是不是最真实的价格?
A:不复权更接近当时实际记录的市场交易价格。但它不能直接代表投资者的完整持仓收益,因为投资者还可能获得现金分红或新增股份。
Q:前复权和后复权计算出的涨跌幅一样吗?
A:如果使用相同的公司行动信息、相同的比例调整规则,且计算区间与基准一致,两者的相对价格收益率通常相同或非常接近。实际数据仍需要验证。
Q:QuantDash 会自动计算我的持仓收益吗?
A:这里使用的是 SDK 获取不同复权口径的历史 K 线。账户持仓数量变化、现金分红入账、交易费用和税费等,需要业务系统结合其他数据自行计算。
Q:为什么我获取的三种价格没有明显区别?
A:可能是查询区间没有发生相关公司行动,也可能是股票历史调整幅度较小。建议检查样本时间范围和公司行动记录,不要人为制造价格差异。
小结
前复权、不复权和后复权并不是三种不同的股票行情,而是对同一段历史价格采用了不同的调整方式。不复权保留原始价格,前复权侧重把历史价格调整到较新基准,后复权则从较早的价格基准向后调整。理解这个区别,可以避免把除权除息误判成真实市场涨跌。
在工程实践中,QuantDash Python SDK 可以通过 adjust 参数获取指定复权口径的历史 K 线,减少开发者重复维护数据接入逻辑的工作。不过,交易日期对齐、异常价格识别、公司行动核对以及数据版本管理,仍然需要研究系统自行完成。
真正可靠的历史行情处理,不只是能得到一条价格曲线,还需要明确这条曲线使用什么口径、适合什么计算,以及能否在未来重新复现。
参考
QuantDash Python SDK 文档:https://docs.quantdash.net/zh-Hans/sdk/python-quickstart
1451



被折叠的 条评论
为什么被折叠?



