fix(bars): 指数/个股 K 线 vol 字段协议语义修正(#64)

通达信服务端 K 线记录第一个 4 字节字段的语义随周期/品种变化,此前原样
透传错误数据(逐字节拆包 + 新浪实时行情/东方财富三方交叉验证锁定):

- 指数分钟线(MIN_1/3/5/15/30/60,含 880xxx 板块指数):f1 实为
  成交额(百元),与 amount 恒差 100 倍,真实分钟成交量不在报文中
  (15:00 上证 5min 真值 13,954,814 手 vs 返回 208,748,512≈amount/100)
  → vol 置 NaN,不拿成交额冒充成交量;
- 指数与个股周/月/季/年线(cat 5/6/10/11):f1 = 真实成交量/100
  (上证本周三日日线 vol 合计 1,666,668,288 手 vs 周线 16,666,683)
  → ×100 还原,与日线单位对齐(指数=手、个股=股);
- 日线(cat 4)与 cat 9(日线变体,枚举名误标 YEAR,真年线是 cat 11)
  不受影响,cat 9 明确不套 ×100 并由测试锁定。

配套:DataFrameResponse NaN→null(Starlette allow_nan=False 透传会 500);
client/路由 docstring 写明各单位;回归测试 7 例(实抓原始字节构造报文);
验收脚本 scripts/verify_issue64.py 连真实服务器复测。附带发现仅记录:
指数分时 vol=成交额(万元)、/bars?category=YEAR 实际返回日线(cat 9)。
This commit is contained in:
GitHub
2026-09-02 17:12:00 +08:00
parent 8a5ed52a1f
commit eeed45b171
8 changed files with 282 additions and 2 deletions
+69
View File
@@ -0,0 +1,69 @@
"""issue #64 验证脚本:指数 K 线 vol 字段语义核查(连真实服务器)。
结论(2026-09-02 实测,对照新浪实时行情与东方财富分钟K):
- 指数分钟线(MIN_1/3/5/15/30/60):协议每条记录的两个 4 字节字段
f1 ≈ f2/100、f2 = 成交额(元)。f1 是"成交额(百元)"而非成交量,
真实分钟成交量不在报文中(东财 15:00 5min bar 实测 13,954,814 手,
而协议 f1 返回 208,748,512 ≈ amount/100)。
- 指数日线:f1 = 成交量(手),与新浪实时行情一致(差值仅为自定义
浮点解码噪声 ~1e-7)。日线路径正确。
- 指数周/月/季/年:f1 = 真实成交量/100(本周三交易日日线 vol 合计
1,666,668,288 手,周线 f1 = 16,666,683,恰好 ÷100)。
- 附带发现:股票周/月线 f1 同样 = 真实vol/100(浦发 8/31-9/2 三日
日线 vol 合计 269,388,528 股,周线 f1×100 = 269,388,500);
指数分时接口 vol 列 = 成交额(万元)(全日合计 ≈ 日成交额/10000)。
用法:python scripts/verify_issue64.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from easy_tdx.client import TdxClient # noqa: E402
from easy_tdx.models.enums import KlineCategory, Market # noqa: E402
def main() -> None:
cl = TdxClient()
cl.connect()
try:
print(f"{'调用':<44s} {'vol(返回)':>16s} {'amount(返回)':>18s} amt/vol")
cases = [
("SH000001 MIN_5 (指数分钟, issue场景)", Market.SH, "000001", KlineCategory.MIN_5),
("SH000001 MIN_1 (指数分钟)", Market.SH, "000001", KlineCategory.MIN_1),
("SH000001 DAY (指数日线, 正确)", Market.SH, "000001", KlineCategory.DAY),
("SH000001 WEEK (指数周线, /100)", Market.SH, "000001", KlineCategory.WEEK),
("SH000001 MONTH (指数月线, /100)", Market.SH, "000001", KlineCategory.MONTH),
("SZ399001 MIN_5 (深成指分钟)", Market.SZ, "399001", KlineCategory.MIN_5),
("SZ399006 MIN_5 (创业板指分钟)", Market.SZ, "399006", KlineCategory.MIN_5),
]
for label, mkt, code, cat in cases:
df = cl.get_index_bars(mkt, code, cat, 0, 1)
r = df.iloc[-1]
ratio = r["amount"] / r["vol"] if r["vol"] else float("nan")
print(f"{label:<44s} {r['vol']:>16,.0f} {r['amount']:>18,.0f} {ratio:>10.2f}")
# 分钟线 f1 与 amount/100 的偏差(仅剩解码噪声)
df = cl.get_index_bars(Market.SH, "000001", KlineCategory.MIN_5, 0, 5)
print("\n分钟线 f1 vs amount/100(应≈1.0,偏差为解码噪声):")
for _, r in df.iterrows():
print(
f" {r['datetime']} vol={r['vol']:>13,.0f} amount/100={r['amount'] / 100:>13,.0f}"
f" 比值={r['vol'] / (r['amount'] / 100):.8f}"
)
# 分时接口 vol 全日合计 ≈ 日成交额/10000(万元)
mt = cl.get_minute_time_data(Market.SH, "000001")
day = cl.get_index_bars(Market.SH, "000001", KlineCategory.DAY, 0, 1).iloc[-1]
print(
f"\n指数分时 vol 全日合计 = {mt['vol'].sum():,.0f}"
f" 日成交额/10000 = {day['amount'] / 10000:,.0f}(≈成交额万元,非成交量)"
)
finally:
cl.close()
if __name__ == "__main__":
main()