A股回测用前复权还是后复权:同一只股票,两条收益曲线差 34 个百分点

摘要:同一只A股、同一段 250 个交易日,只改 adjust 参数,累计收益从 −29.92% 变成 +4.50%,差 34.42 个百分点。本文用实测拆清五种复权取值:不传参数默认给不复权,除权日跳空会被当成真实下跌(实测 −36.70% 对实际 −5.60%);前复权按该K线之后的因子调整,未来每次除权都会改写历史,回测不可复现;后复权历史不动,可复现。复权因子可自行复算,实测 250 根全部吻合。读者为搭回测框架的量化开发者与排查「回测稳、实盘偏」的研究员,适用回测取数、历史数据落库与数据源迁移校验;价值在于把复权语义固定在取数层,交付五条验收规矩与一段自检代码。

我在 10 月 19 日取了寒武纪(688256.SH)近 250 个交易日的日K,只改 adjust 参数,取了三次。

累计收益率分别是:

adjust首根收盘末根收盘累计收益
none(不复权)1325928.51−29.92%
forward(前复权)888.544946928.51+4.50%
backward(后复权)13251384.595969+4.50%

读法:同一区间、同一标的,三次调用只差一个参数。实测取数日 2026-10-19。

picture.image

一个亏 30%,两个赚 4.5%。这不是数据质量问题——三条序列都是对的,差别在于它们各自假设了一套不同的持仓成本。

区间里有一次除权,发生在 2026 年 5 月 8 日。不复权序列在那天从 1868 跌到 1182.53,−36.70%。而同一天,前复权序列是从 1252.68 跌到 1182.53,−5.60%。

picture.image

−36.70% 那个数字不是跌出来的,是股本变了。但它在一列收盘价里和真实暴跌长得完全一样。

这篇适合谁:

  • 搭回测框架、要保证结果能重跑的量化开发者;
  • 在排查"回测稳、实盘偏"的策略研究员——在怀疑策略逻辑之前,先确认价格序列本身没变过;
  • 做数据层的工程师:取数契约里该写清哪些参数。

不写代码的读者,读完前三部分就够了,第四部分可以直接转给技术同事。

一、不传 adjust,默认给的是不复权

先说最容易出事的一点。我试了一次不带 adjust 的请求,返回体里的 adjust 字段回显是 none。

也就是说,没写这个参数的取数代码,拿到的是带除权跳空的原始价格序列——上面那条 −29.92% 的曲线。不会报错,字段齐全,时间戳连续,价格范围合理。所有常规校验都会通过。

顺带一个发现:这个参数实际有五个合法值,不是三个。传一个非法值 qfq 时,接口返回的错误信息把全部取值列了出来:

{"error":"2001","message":"invalid adjust value: qfq, must be one of:
 forward, backward, forward_additive, backward_additive, none","code":"2001"}

forward_additive 和 backward_additive 是加法复权。我把五个值都跑了一遍:

adjust首根收盘末根收盘累计收益
none1325928.51−29.92%
forward888.544946928.51+4.50%
backward13251384.595969+4.50%
forward_additive888.255034928.51+4.53%
backward_additive13251384.9799+4.53%

读法:乘法复权和加法复权差 0.03 个百分点,来自除权的处理方式不同。实测取数日 2026-10-19。

乘法和加法的差异在这只标的上只有 0.03 个百分点,但它会随回溯期和除权次数累积。重点不是哪个更准,是同一份研究里必须固定用一种,否则不同时间跑出来的结果没法比。

二、两种复权的因子应用方向相反,所以只有一条会变

官方文档对两种复权的定义是这样的:

复权价 = 原始价 × factor_a + factor_b。前复权按时间顺序应用该K线之后的因子,后复权逆序应用该K线及之前的因子。

注意方向:前复权用的是这根K线之后发生的事件,后复权用的是这根K线及之前发生的事件。

picture.image

"之后"这个词有个直接后果:未来还会发生的除权事件会改写今天这根K线的历史价格。所以前复权序列不是静态数据,每次新的分红送股落地,整条历史都要按新因子重算一遍。

这一点在实测数据里看得很清楚。同一只标的、同一区间:

序列首根收盘末根收盘
不复权1325928.51
后复权1325(与不复权相同)1384.595969
前复权888.544946(被因子压低)928.51(与不复权相同)

读法:后复权的首根等于原始价,说明历史没动;前复权的末根等于原始价,说明它把基准对齐到了现价。

后复权的历史价格就是原始价格,所以它不会变。前复权把历史整体压低到和现价同一基准上,而"现价的基准"会随每次除权移动。

后果是回测不可复现。同一个策略、同一段区间、同一套参数,隔了一次除权再跑,输入数据已经不同,收益曲线自然对不上。这个变化没有任何信号:不报错,不改字段,文件大小可能都差不多。

这就是"回测稳、实盘偏"里最难查的一种。排查通常从策略逻辑、滑点、手续费开始,很少有人回头怀疑价格序列——毕竟上周它还是对的。

三、复权因子可以自己复算,而且能对上

既然文档给了公式,这件事可以验证。

我取了这只标的的复权因子,区间内只有一条除权记录,2026 年 5 月 8 日:

adjustfactor_afactor_b
forward0.6705999589592825120
backward1.4912020

两个 factor_a 相乘等于 1.000000000,互为倒数——这正是"方向相反"在数值上的体现。

然后按公式复算:把除权日之前的每一根原始收盘价乘上 forward 的 factor_a,和接口返回的前复权序列逐根比对。250 根全部吻合,误差在小数第六位以内。 后复权同理,用 backward 的 factor_a 乘除权日及之后的价格,也是 250 根全中。

这件事的实用价值在第四部分第四条:有了因子和原始价,任何时刻都能复算出当时那条前复权序列。存因子比存一份会过期的复权价可靠。

四、回测取数层的五条规矩

规矩一:adjust 显式传值

默认值是 none,而大多数回测要的不是它。显式写出来,代码读起来也能看出这份数据是什么。注意只接受那五个值,qfq 这类写法返回 2001 错误。

规矩二:落库时记下序列类型

加一列记录,或者写进表名、文件名。几个月后回头看一份历史数据,你需要能立刻判断它是哪一条,而不是重新取一遍比对。

规矩三:要可复现就用后复权

需要跨时间比对、要求三个月后重跑出同样曲线的研究,用 backward,它的历史不会变。只做当期回测、且需要价格和行情软件对得上的,用 forward,但要接受它会随除权重算。

规矩四:用前复权就把因子和取数日期存下来

因子表很小,一次除权一条记录。存了因子和原始价,就能复算出任意时点的前复权序列——上一节已经验证过这条路走得通。

规矩五:回测和实盘的价格变量分开命名

回测用复权序列算收益,实盘下单参照实际成交价,这本来是两个数。用不同的变量名区分,别共用一个 price。

落成一张清单:

检查项怎么验不做的后果
adjust 是否显式传值搜一遍取数代码里的K线调用默认拿到不复权,收益率差几十个百分点
落库是否记录序列类型查表结构或文件命名历史数据无法判断来源
可复现研究是否用后复权确认研究周期是否跨除权回测结果无法重现
前复权是否存了因子和日期查是否有因子表旧序列无法复算
回测与实盘价格是否分开搜 price 变量的用处成本假设和实盘不一致

读法:前两项是取数层的最小要求,后三项按研究类型取舍。

另外两个会和复权叠加的坑,顺手一起检查。日K的时间戳取各市场当地零点——我实测的这根日K时间戳 1792339200000 对应北京时间 10 月 19 日 00:00,换成 UTC 是 10 月 18 日 16:00,前端按 UTC 格式化会显示成前一天。还有返回顺序不统一,部分品种按时间倒序,不排序就做滚动计算,结果是镜像的。这两个误差和复权偏差叠在一起,排查时很难分清是哪一层出的问题。

怎么做

TickDB 以 REST 和 WebSocket 连接实时与历史行情,并通过统一的数据入口支持研究、监控、产品和 AI 工作流。

文中全部数字来自它的K线接口:GET /market/kline,adjust 五个取值,一次最多 1000 根;复权因子来自 GET /market/kline/ex-factors,返回 factor_a 和 factor_b,支持A股、港股、美股(Free 套餐不提供这个接口)。因为五个取值在同一个接口上切换,上面那组对照才能在同一时刻、同一区间完成,不用拼接多个来源。

下面这段代码先打印三条序列的累计收益,再用因子复算校验前复权序列。输出就是本文第一张表和第三部分的结论:

# 测试环境:Python 3.11 / 2026-10-19(用 688256.SH 近 250 个交易日的真实返回验证)
# 用途:确认自己拿到的是哪条复权序列,并用复权因子复算校验
import os
from decimal import Decimal

import requests

BASE = "https://api.tickdb.ai/v1"
HEAD = {"X-API-Key": os.environ["TICKDB_API_KEY"]}   # Key 只放服务端

def call(path, **params):
    r = requests.get(f"{BASE}{path}", params=params, headers=HEAD, timeout=20)
    body = r.json()
    if body.get("code") != 0:                        # HTTP 200 不代表成功
        raise RuntimeError(f"{body.get('code')}: {body.get('message')}")
    return body["data"]

def closes(symbol, adjust, limit=250):
    data = call("/market/kline", symbol=symbol, interval="1d",
                adjust=adjust, limit=limit)
    bars = sorted(data["klines"], key=lambda b: int(b["time"]))   # 顺序不保证,先排
    return [(int(b["time"]), Decimal(b["close"])) for b in bars]  # 价格是字符串

def cumulative(series):
    return (series[-1][1] / series[0][1] - 1) * 100

def verify_factor(symbol, limit=250):
    """用 forward 因子复算前复权:除权日之前的原始价 × factor_a 应等于前复权价"""
    raw, fwd = closes(symbol, "none", limit), closes(symbol, "forward", limit)
    ex = call("/market/kline/ex-factors", symbol=symbol)["data"][symbol]
    ok = 0
    for ts, raw_close in raw:
        calc = raw_close
        for e in ex:
            if e["adjust"] == "forward" and ts < e["timestamp"]:
                calc *= Decimal(str(e["factor_a"]))
                calc += Decimal(str(e["factor_b"]))
        if abs(calc - dict(fwd)[ts]) <= Decimal("0.000001"):
            ok += 1
    return ok, len(raw)

if __name__ == "__main__":
    sym = "688256.SH"
    for adj in ("none", "forward", "backward"):
        s = closes(sym, adj)
        print(f"{adj:9s} 首 {s[0][1]:>12} 末 {s[-1][1]:>12} 累计 {cumulative(s):+7.2f}%")
    ok, total = verify_factor(sym)
    print(f"因子复算吻合 {ok}/{total} 根")

设置环境变量 TICKDB_API_KEY 后运行,Key 只放在服务端。我这边跑出来是三条序列 −29.92%/+4.50%/+4.50%,因子复算 250/250 吻合。换成你自己的标的跑一次,就知道自己平时用的是哪一条。


前复权和后复权的因子应用方向相反,这一条决定了哪条序列可复现:后复权的历史价格就是原始价格,不会变;前复权的历史会被未来的除权事件改写。要做能重跑的研究就用后复权;要用前复权,把因子和取数日期一起存下来。

今天可以搜一遍自己的取数代码,看K线调用里有没有写 adjust。没写的话默认是不复权——用上面那段代码跑一次,看看你的回测收益率和复权后差多少。

参考资料

  1. TickDB 官方文档,K线接口 adjust 参数与复权因子接口说明。docs.tickdb.ai
  2. 寒武纪(688256.SH)近 250 个交易日日K与复权因子:TickDB 实测,2026 年 10 月 19 日,五个 adjust 取值各取一次。
  3. 上海证券交易所,除权除息业务规则。sse.com.cn
0
0
0
0
评论
未登录
暂无评论