股票历史数据的复权参数对照:多级别返回的字段与数值差异
一、问题:只换一个参数段,价格就差了两个数量级
历史分时接口的路径里有一段「除权方式」。很多人默认它只是同一个序列的展示选项,于是把不同参数下返回的 c 直接拼进同一张表做比较。实际请求下来会发现:n 下日线收盘价是 11.71,b 下同一天的收盘价是 1197.96,相差两个数量级,而 v 与 a 却完全不变。
如果落库时没把参数写进元数据,后续按同一个阈值过滤价格,会把一半记录判成异常;如果解析层默认所有参数组合都会返回 JSON 数组,遇到一个无效参数就会在 json.loads 那一行直接抛错。
本文用一组真实请求,把五种除权方式的返回字段与数值差异并列摆出来,给出一段能同时处理正常数组与 404 纯文本的解析代码,并说明不同分时级别下这个参数该怎么取。级别与除权方式的取值定义以当期接口文档为准,本文只对可观测的返回结构与数值做记录,不涉及行情判断。
二、先看结果:几条可以在线验证的返回
下面几条地址可以直接点开对比,区别只在路径中的「除权方式」这一段:
- 不复权
n:api.mairuiapi.com/hsstock/his… - 前复权
f:api.mairuiapi.com/hsstock/his… - 后复权
b:api.mairuiapi.com/hsstock/his… - 等比前复权
fr:api.mairuiapi.com/hsstock/his… - 等比后复权
br:api.mairuiapi.com/hsstock/his…
紧跟一句说明:hsstock/history 是历史分时路径,000001.SZ 是「股票代码.市场」,d 是日线级别,倒数第二段是除权方式,最后一段是演示证书;st、et 是起止时间,lt=3 指定取最新 3 条。五条请求都返回数组,字段集合一致,只有价格类字段的数值不同。
n 数组首条的实测内容(原样):
{"t":"2025-11-27","o":11.66,"h":11.71,"l":11.58,"c":11.71,"v":1011847.0,"a":1178308953.0,"pc":11.69,"sf":0.0}
b 数组首条的实测内容(原样):
{"t":"2025-11-27","o":1193.91,"h":1197.96,"l":1187.45,"c":1197.96,"v":1011847.0,"a":1178308953.0,"pc":1196.34,"sf":0.0}
对照看得很清楚:t、v、a、sf 逐字段相同,o、h、l、c、pc 全部换了一组数值。这个差别就是本文要处理的问题。
三、返回字段逐项说明
五种除权方式返回的字段集合实测一致,都是 9 个。下表以 n 与 b 的首条为样例:
| 字段 | 实测类型 | 含义(接口文档定义) | n 首条 | b 首条 | 随除权方式变化 |
|---|---|---|---|---|---|
t | str | 交易时间 | "2025-11-27" | "2025-11-27" | 否 |
o | float | 开盘价 | 11.66 | 1193.91 | 是 |
h | float | 最高价 | 11.71 | 1197.96 | 是 |
l | float | 最低价 | 11.58 | 1187.45 | 是 |
c | float | 收盘价 | 11.71 | 1197.96 | 是 |
v | float | 成交量 | 1011847.0 | 1011847.0 | 否 |
a | float | 成交额 | 1178308953.0 | 1178308953.0 | 否 |
pc | float | 前收盘价 | 11.69 | 1196.34 | 是 |
sf | 数值 | 停牌标记:1 停牌、0 不停牌 | 0.0 | 0.0 | 否 |
三点从表里直接读出的约束:
- 价格类 5 个字段整体变化,量类 4 个字段不动。所以「换参数只影响价格」可以被当作结构事实写进落库设计:同一份量类数据不必重复存五遍,但价格列必须带口径标签。
- 不要按数值大小反推口径。五种参数里有两种在 11 附近、两种在 1200 附近、一种在 11.09 附近,仅凭量级猜是哪一种,遇到上游调整就会静默出错。口径以接口文档中该参数的定义为准。
sf是0.0而不是空值。它是有效的「不停牌」标记,校验写「键存在且为数值」。
四、接口结构与参数
地址结构:
https://api.mairuiapi.com/hsstock/history/{股票代码.市场}/{分时级别}/{除权方式}/{licence}?st=&et=<=
按接口文档定义,日线及以上级别可选的除权方式有五种:
| 除权方式 | 接口文档定义 | 实测首条 c | 实测首条 o |
|---|---|---|---|
n | 不复权 | 11.71 | 11.66 |
f | 前复权 | 11.1 | 11.05 |
b | 后复权 | 1197.96 | 1193.91 |
fr | 等比前复权 | 11.09 | 11.05 |
br | 等比后复权 | 1220.5 | 1215.29 |
分时级别支持 5、15、30、60、d、w、m、y。这里有一个和「多级别」直接相关的约束:分钟级别没有除权数据,该段按文档只用 n;日线以上才用得上上面那张表。把 d 级别的参数组合直接套到 5 级别上,得到的不是预期结构,排查时又容易先怀疑数值本身。
同一除权方式下切换级别,返回形态不变(都是数组),但 t 的格式与数值量级会变。本次实测 d 首条 t=2025-11-27、c=11.71;w 首条 t=2025-11-16、c=11.75;m 首条 t=2025-09-30、c=11.34;5 首条 t=2025-12-01 14:50:00、c=11.69。所以落库时至少要有两列元数据:级别、除权方式。
还有一个必须处理的分支:无效参数不返回 JSON。本次把除权段写成 q,实测返回 HTTP 404 与一段纯文本 404 page not found,既不是数组也不是错误对象。客户端不能无条件 json.loads。
五、代码实现:对照五种口径并区分 404 文本
import json
from urllib.request import Request, urlopen
from urllib.error import HTTPError
LICENCE = "LICENCE-2413-E1FB-743443B19BBD"
BASE = ("https://api.mairuiapi.com/hsstock/history/000001.SZ/d/"
"{div}/" + LICENCE + "?st=20250101&et=20251231<=3")
DIVS = ("n", "f", "b", "fr", "br", "q")
def fetch(url):
"""返回 (status, text),把 HTTPError 也转成可判断的结构。"""
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
with urlopen(req, timeout=20) as resp:
return resp.status, resp.read().decode("utf-8")
except HTTPError as e:
return e.code, e.read().decode("utf-8")
for div in DIVS:
status, text = fetch(BASE.format(div=div))
if status >= 400:
print("%-3s HTTP=%d body=%r" % (div, status, text.strip()))
continue
rows = json.loads(text)
if not isinstance(rows, list) or not rows:
print("%-3s HTTP=%d unexpected shape" % (div, status))
continue
r = rows[0]
print("%-3s HTTP=%d t=%s c=%-9s o=%-9s a=%-16s v=%s" % (
div, status, r["t"], r["c"], r["o"], r["a"], r["v"]))
实测运行输出(原样):
n HTTP=200 t=2025-11-27 c=11.71 o=11.66 a=1178308953.0 v=1011847.0
f HTTP=200 t=2025-11-27 c=11.1 o=11.05 a=1178308953.0 v=1011847.0
b HTTP=200 t=2025-11-27 c=1197.96 o=1193.91 a=1178308953.0 v=1011847.0
fr HTTP=200 t=2025-11-27 c=11.09 o=11.05 a=1178308953.0 v=1011847.0
br HTTP=200 t=2025-11-27 c=1220.5 o=1215.29 a=1178308953.0 v=1011847.0
q HTTP=404 body='404 page not found'
关键在两处:一是 fetch() 把 HTTPError 也读成文本返回,404 时不会丢掉响应体;二是 status >= 400 先分支出去,不把纯文本交给 json.loads。这样无效参数与「成功但空数组」就不会被混成同一类结局。
六、演示证书的行为说明
可以自己点开两条对照,只把股票代码从 000001.SZ 换成 600519.SH:
实测两次返回逐字节相同。把除权方式换成 f、b、fr、br,把级别换成 w、m、5 重复这个对照,结果同样是逐字节相同。
示例使用的是演示证书,服务端固定返回 000001 的样例数据,因此替换请求中的股票代码不会改变返回结果。演示证书的作用只有两个:确认接口连通、看清返回字段结构。若需要验证其他标的的真实历史数据,需要在请求中使用自己的 licence 参数。
这也意味着本文上表里的数值只能用于理解字段结构差异,不能当作某个标的真实数值序列使用。
七、常见问题与踩坑
把不同除权方式的价格拼进同一列。 n 的 c 是 11.71,b 的 c 是 1197.96,写进同一列再按同一阈值过滤,会直接把一半记录判成异常。分表或在表里加口径标签是更稳的做法。
凭数值猜口径。 五种参数里有两组数值接近,仅靠量级无法区分。参数定义以当期接口文档为准,别把猜测写进代码注释或字段映射表。
分钟级别沿用日线的除权参数。 接口文档说明分钟级别无除权数据、该段只用 n。把它当成通用段拼接,排查时容易先怀疑数据而不是参数。
把无效参数当成空数组。 q 实测是 404 加纯文本,不是 []。解析前先看状态码,再看响应体能不能解析成 JSON。
把 v / a 当成随口径变化的字段。 本次五种方式下这两个字段逐字段相同,但这是实测观察,不是可以写死的不变量;落库时仍按「每次响应原值透传」处理。
混淆 t 与级别。 日线 t 是纯日期,分钟级 t 带时分秒。除权方式不改变 t 的格式,改变格式的是分时级别。
把样例特征写成通用规则。 「3 条」「某个 c 值」都是这次演示返回的特征,不是接口契约。校验只约束字段集合、类型和结构,不约束具体数值范围。
八、小结与数据来源说明
历史分时的除权方式差异集中在一点:价格类字段换一组数值,量类与时间字段保持不动。落库时把「分时级别」和「除权方式」都写成元数据,解析时先分流 404 与空数组,后面就不会在换参数时把数据写错列。
本文所有返回样例、字段表、参数对照与代码输出均来自对上述公开接口地址的实际请求,采集时间为 2026-11-12;除权方式与分时级别的取值定义以当期接口文档为准,演示返回的条数与数值是固定样例特征,不是接口契约。
本文只讨论接口返回结构与数据处理,不涉及任何行情判断或投资建议。