From 91f6a884d57d5d6145f4f6394264a6ad6c6dd641 Mon Sep 17 00:00:00 2001 From: minionszyw Date: Wed, 22 Apr 2026 18:50:07 +0800 Subject: [PATCH] Fix security list pre-close decoding --- README.md | 2 +- src/xmtdx/commands/security_list.py | 9 ++++----- src/xmtdx/models/security.py | 2 +- tests/fixtures/security_list.json | 4 ++-- tests/unit/test_commands_offline.py | 29 +++++++++++++++++++++++++++-- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6fed714..13efc38 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ main_net_inflow |---|------|------|------| | 1 | `xdxr_info` | 循环内始终读 `body[:7]`,所有记录字段相同 | 改为从当前 `pos` 读取,pos 正确推进 | | 2 | `security_list` | GBK 解码截断时 crash | `decode('gbk', errors='replace')` | -| 3 | `security_list` | `pre_close` 错用成交量解码函数 | 改用价格解码 | +| 3 | `security_list` | `pre_close` 误当作整数价格 `/100` | 恢复为通达信自定义浮点解码 | | 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` | | 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` | | 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`(通达信自定义浮点),单位万股,与 `FinanceInfo` 完全吻合 | diff --git a/src/xmtdx/commands/security_list.py b/src/xmtdx/commands/security_list.py index edbff31..aeb8f75 100644 --- a/src/xmtdx/commands/security_list.py +++ b/src/xmtdx/commands/security_list.py @@ -1,12 +1,13 @@ """获取证券列表命令(每页最多1000条,按 start 分页)。 修复 pytdx Bug #2:GBK 解码使用 errors='replace',截断多字节序列不再崩溃。 -修复 pytdx Bug #3:pre_close 使用 get_price 解码,而非 get_volume。 +修复 pytdx Bug #3:pre_close 保持使用通达信自定义浮点解码。 """ import struct from .._binary import slice_bytes, unpack_from +from ..codec.volume import _decode_volume from ..models.enums import Market from ..models.security import SecurityInfo from .base import BaseCommand @@ -48,10 +49,8 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]): # Bug #2 修复:errors='replace' 避免截断 GBK 多字节序列时崩溃 name = name_bytes.decode("gbk", errors="replace").rstrip("\x00") - # Bug #3 修复:pre_close 不用 get_volume(成交量解码), - # 而是直接将 uint32 当作价格整数(/ 100) - # 实际服务器返回的 pre_close_raw 是 price * 100 的整数 - pre_close = pre_close_raw / 100.0 + # pre_close_raw 与协议里的成交量/股本字段一样,使用通达信自定义浮点编码。 + pre_close = _decode_volume(pre_close_raw) results.append( SecurityInfo( diff --git a/src/xmtdx/models/security.py b/src/xmtdx/models/security.py index de8f8c7..b9132ba 100644 --- a/src/xmtdx/models/security.py +++ b/src/xmtdx/models/security.py @@ -14,7 +14,7 @@ class SecurityInfo: name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代) volunit: int # 成交量单位(手 = volunit 股) decimal_point: int # 价格小数位数 - pre_close: float # 昨收价(已修复 pytdx Bug #3:改用正确价格解码) + pre_close: float # 昨收价(通达信自定义浮点解码) # 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得) industry_tdx: str = "" # 通达信行业代码 (如 T1001) diff --git a/tests/fixtures/security_list.json b/tests/fixtures/security_list.json index 7bd0181..e636584 100644 --- a/tests/fixtures/security_list.json +++ b/tests/fixtures/security_list.json @@ -3,6 +3,6 @@ "first": { "code": "999999", "name": "上证指数", - "pre_close": 11654847.33 + "pre_close": 3966.171142578125 } -} \ No newline at end of file +} diff --git a/tests/unit/test_commands_offline.py b/tests/unit/test_commands_offline.py index cb0d3a7..3eceee6 100644 --- a/tests/unit/test_commands_offline.py +++ b/tests/unit/test_commands_offline.py @@ -7,6 +7,7 @@ fixtures/ 目录下每个 .hex 文件是一次真实服务器响应的 body( from __future__ import annotations import pathlib +import struct FIXTURES = pathlib.Path(__file__).parent.parent / "fixtures" @@ -50,13 +51,37 @@ def test_security_list_parse(): r0 = records[0] assert r0.code == "999999" assert r0.name == "上证指数" - # pre_close is a float; the index level is stored ×100 and has many decimal places - assert abs(r0.pre_close - 11654847.33) < 1.0 + assert abs(r0.pre_close - 3966.171142578125) < 0.01 # _raw present and non-empty for every record assert all(len(r._raw) > 0 for r in records) +def test_security_list_pre_close_uses_tdx_float_for_a_share(): + from xmtdx.commands.security_list import GetSecurityListCmd + from xmtdx.models.enums import Market + + body = ( + struct.pack("