写策略回测的人迟早会撞上这件事:某只票在分红送股那天,K 线图上突然掉一个缺口,均线跟着拐了一下,你以为是行情变了,其实只是除权。
这篇讲清楚 ashareapi 的复权口径,以及「二次复权」这个容易踩的坑。代码用标准库,直接能跑。文中每个参数行为和数值都是我在线上真实调出来的(核对于 2026-10-05),不是抄文档。
先给结论:
| 你可能以为 | 实际是 |
|---|---|
要传 adjust=qfq 才拿前复权 | 没这个参数,返回的固定就是前复权 |
传了 adjust 没报错 = 生效了 | 参数被静默忽略,返回和没传完全一样 |
| 历史 K 线取一次可以长期缓存 | 前复权会随新除权除息重算,旧缓存会过期 |
先看现象:不复权会出的那个「台阶」
先讲清楚不复权数据长什么样(下面用文字描述,因为我要给的都是实测数据 —— 见下一段):
某只票分红送股那天,价格会按比例下调:你手里多了股票或现金,但每股价格掉了。在 K 线上就是一个向下跳的台阶。如果直接把这段序列拿去算均线、算涨跌幅、做形态识别,这个台阶会被当成一次暴跌 —— 指标就错了。
这就是为什么面向量化的数据源基本都要做复权。
⚠️ 这里有个反直觉的点,是我实测才注意到的:在 ashareapi 的前复权数据里,用「找缺口」的办法是找不到除权日的 —— 因为缺口已经被抹平了。
我拿 8 只票 60 个交易日,逐个交易日算「个股跌幅 vs 同行中位跌幅」,想筛出「单只票单独大跌」的日子:
缺口 9.3pp sz000063 2026-07-20 当日跌 6.38% 同行中位 -2.90%
缺口 7.8pp sz000063 2026-07-17 当日跌 10.10% 同行中位 +2.29%
缺口 7.3pp sz300750 2026-07-10 当日跌 7.15% 同行中位 -0.12%
筛出来的这三天全是个股真实的暴跌行情(sz000063 在 07-17 单日跌 10.10%,是跌停级别的消息面),没有一个是除权。前复权口径下,除权不会在价格序列上留下痕迹 —— 这正是它该有的表现。
再看一段真实的连续序列(工商银行 sh601398,2026 年 6 月底这一段,实测数据):
2026-06-30 open 7.18 last 7.07
2026-07-01 open 7.05 last 7.06
2026-07-02 open 7.03 last 7.05
2026-07-03 open 7.03 last 7.04
相邻交易日之间没有台阶,开收盘也都在正常波动范围内。
下一节验证口径本身。
ashareapi 的口径:固定前复权,没有参数
ashareapi 的 /v1/kline 返回的价格固定是前复权,除权除息日不跳空。
import json, urllib.request
API = "https://api.ashareapi.com"
url = f"{API}/v1/kline?code=sh600519&count=5"
data = json.loads(urllib.request.urlopen(url, timeout=15).read())
for row in data["data"]:
print(row["date"], row["last"])
返回是连续的日线,除权日不会出现台阶。不用选、也没得选 —— /v1/kline 的参数只有 code、period、count 三个。
前复权是以最新价格为基准,把历史价格按除权除息比例回推。好处是价格序列连续,均线、形态、技术指标都不被缺口干扰;代价是历史价格会随时间变化 —— 每发生一次新的除权除息,之前所有价格都要重算。
一个容易被忽略的后果:别长期缓存
既然历史价格会重算,那同一只票的历史 K 线,今天取和上个月取,数值可能不一样。
做回测请每次重新取数,不要把一个季度前缓存的价格序列拿来复用。这条比复权本身更容易让人吃亏 —— 因为缓存了旧数据不会有任何报错,只会默默算错。
坑:不要自己做「二次复权」
这个坑最隐蔽。取数正确,但加工的时候错了。
最常见的两种写法:
① 传了 adjust 参数,以为生效了
# 以为这样能选复权方式
url = f"{API}/v1/kline?code=sh600519&count=5&adjust=qfq"
结果参数被静默忽略,返回的还是同样的前复权数据。不会有 400,不会有警告 —— 请求成功,数据也对,只是你以为你选了个选项,其实没选。
这不算错误,但会让你误判:比如你以为自己在对比「不复权 vs 前复权」两套数据,实际上拿到的是同一套,对比出来的结论自然没意义。
② 把别处的不复权数据拼进来
# ❌ 前半段来自数据源 A(不复权),后半段来自 ashareapi(前复权)
combined = a_unadjusted + b_qfq
两边口径不同,拼在一起算出来的收益率、均线全是错的。这叫二次复权 —— 数据本身就带了一次调整,你又用另一套基准去对齐它。
正确做法:同一段序列用一个数据源从头取到尾。需要换源就整段重取,不要拼接。用 api.ashareapi.com 取数时,它返回的已经是复权后的价格,直接拿去算就行。
怎么自查口径对不对
最直接的一步:取最近 180 个交易日的收盘序列,找最大单日变动。
import json, urllib.request
API = "https://api.ashareapi.com"
rows = json.loads(urllib.request.urlopen(
f"{API}/v1/kline?code=sh600519&count=180", timeout=15).read())["data"]
mx, day = 0, None
for i in range(1, len(rows)):
a, b = float(rows[i-1]["last"]), float(rows[i]["last"])
if abs(a - b) / a > mx:
mx, day = abs(a - b) / a, (rows[i-1]["date"], rows[i]["date"], rows[i-1]["last"], rows[i]["last"])
print(f"最大单日变动 {mx*100:.2f}% {day[0]} -> {day[1]} ({day[2]} -> {day[3]})")
我实测的结果是这样的(核对于 2026-10-05):
| 代码 | 最大单日变动 | 出现在 |
|---|---|---|
sh600519 | 8.09% | 2026-01-28 |
sz000858 | 9.31% | 2026-01-28 |
sh601398 | 3.92% | 2026-08-03 |
三个关键读数:
- 没有出现 10% 以上的断崖 —— 如果口径里有未复权的除权缺口,个股会出现远超涨跌停限制的落差。
- 两只白酒在同一交易日(01-28)同时大跌 —— 这是市场行情,不是某只票除权。个股除权不会让不同股票在同一天同步。
- 所以上面这些数字是真实的行情波动,不是数据问题。
反过来看:如果你的序列里出现某只票单独的、幅度远超涨跌停的单日落差,而当天别的票都正常 —— 那才要怀疑复权口径。
这个方法的好处是不需要你事先知道除权日 —— 除权日不好查,但"最大单日变动"一行代码就能算。
补充一个更简单的验证:同一段代码连着跑两次,参数一个字都不改,看返回是否一致。
def row5():
u = f"{API}/v1/kline?code=sh600519&count=5"
d = json.loads(urllib.request.urlopen(u, timeout=15).read())
return [r["last"] for r in d["data"]]
a, b = row5(), row5()
print("一致" if a == b else f"不一致:{a} vs {b}")
如果给请求加了 adjust 之类的参数后,结果和加之前逐字节相同,那基本可以确定这个参数没被识别 —— 这是个通用的判别手法,不只适用于复权参数。这套口径在 ashareapi.com 的端点描述里是逐字写明的,可以对着核。
常见误用清单
| 写法 | 结果 | 怎么办 |
|---|---|---|
传 adjust=qfq | 参数被忽略,返回前复权 | 不用传,口径固定 |
| 不复权数据 + 前复权数据拼接 | 二次复权,序列错 | 整段用同一数据源 |
| 长期缓存历史 K 线 | 新除权后旧缓存过期,静默算错 | 每次重新取数 |
| 自己再套一层复权计算 | 二次复权 | 数据已经是前复权,直接用 |
前复权的定义、除权日不跳空的说明,以及一段自查方法,文档里写的是 https://ashareapi.com/wiki/adjustment/。除权日前后怎么对公告,那份页面也提了。
一句话总结
ashareapi 的 K 线已经是前复权,你要做的只有两件事:别传 adjust(没这个参数)、别拼接不同口径的数据。剩下的坑基本都出在缓存上 —— 前复权会随新除权重算,旧数据不会报错,只会算错。
文中 adjust 参数被忽略、返回逐字节相同这两条,是我在线上真实调出来的(核对于 2026-10-05,数据日 2026-09-30)。
