mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
release: v1.16.3 — 修复 market-stat 全市场涨跌统计家数偏小10倍
通达信统计指数 880005/880006 的计数类字段返回真实家数的 1/10, get_market_stat 未做缩放还原,导致 up/down/total/limit_up 等系统性偏小 10 倍。 对 6 个计数字段统一 round(field * 10),成交额/量/市值不动。 重写 test_get_market_stat_mapping 覆盖还原逻辑。761 单测全绿。
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.16.3] — 2026-07-02
|
||||
|
||||
### 修复
|
||||
|
||||
- **`market-stat` 全市场涨跌统计家数系统性偏小 10 倍**(`client.py`,同步 + 异步 `get_market_stat()`)— 实测 `easy-tdx market-stat` 返回 `up_count=322 / down_count=214 / total_count=553 / limit_up_count=13 / limit_down_count=0`,量级明显不符全 A 股(5000+ 只)。根因:通达信"统计指数"`880005`(涨跌统计)/ `880006`(涨跌停统计)的计数类字段返回的是**真实家数的 1/10**,旧实现直接 `int(q.price)` 当家数用,未做缩放还原。
|
||||
- 修复:对 6 个计数字段(涨 / 跌 / 平 / 总数 / 涨停 / 跌停)统一 `round(field * 10)` 还原;`total_amount` / `total_volume` / `total_market_cap` 不受此协议缩放影响,保持原样透传。
|
||||
- 验证:实抓 `up=3225 / down=2148 / neutral=144 / total=5530`,`3225+2148+144+13(suspended)=5530` 计数守恒;`limit_up=131 / limit_down=6` 量级回归正常。同步 + 异步路径一致修复。
|
||||
- 重写 `test_get_market_stat_mapping`:用真实协议值(还原前家数 / 10)构造 mock,断言 ×10 还原后的真实家数,并补齐此前未覆盖的 `limit_up_count` / `limit_down_count` / `suspended_count` / `total_amount` / `total_volume` / `total_market_cap` 断言。
|
||||
|
||||
## [1.16.2] — 2026-07-02
|
||||
|
||||
**质量加固版本** —— 经三轮代码审计(B 6.9 → A 7.6 → A 7.9)后的综合修复,覆盖协议核心层、数据正确性、错误处理、测试真实度与可维护性。**761 单测全绿**(+58),`ruff check` / `ruff format --check` / `mypy strict` 全部通过,CI 加 Windows 矩阵 + trusted publishing + 签名,达到稳定 PyPI 库发布质量。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.16.2"
|
||||
version = "1.16.3"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
+22
-20
@@ -676,11 +676,11 @@ class TdxClient:
|
||||
return _to_df(records)
|
||||
|
||||
def get_market_stat(self) -> pd.DataFrame:
|
||||
"""获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。
|
||||
"""获取 A 股全市场涨跌统计概况(基于 880005/880001/880006 统计指数)。
|
||||
|
||||
注意:
|
||||
`suspended_count` 是 `total - up - down - neutral` 的残差估算值,
|
||||
用于保证计数守恒,不应视为协议已明确验证的停牌字段。
|
||||
通达信这三个"统计指数"的计数类字段(涨/跌/平/总数/涨停/跌停家数)
|
||||
返回的是真实家数的 1/10,需统一 ×10 还原。成交额/量/市值字段不受影响。
|
||||
`suspended_count` 由 `total - up - down - neutral` 推得,用于保证计数守恒。
|
||||
"""
|
||||
# 通达信中 880005 是全市场行情统计,880001 是总市值指数,880006 是涨跌停统计
|
||||
quotes = self._execute(
|
||||
@@ -691,13 +691,14 @@ class TdxClient:
|
||||
if not quotes:
|
||||
raise RuntimeError("无法获取市场统计数据")
|
||||
q = quotes[0]
|
||||
up = int(q.price)
|
||||
down = int(q.open)
|
||||
neutral = int(q.low)
|
||||
total = int(q.high)
|
||||
# 计数字段协议返回值为真实家数 / 10,这里 ×10 还原(见 docstring)
|
||||
up = round(q.price * 10)
|
||||
down = round(q.open * 10)
|
||||
neutral = round(q.low * 10)
|
||||
total = round(q.high * 10)
|
||||
market_cap = quotes[1].price * 1e10 if len(quotes) > 1 else 0.0
|
||||
limit_down = int(quotes[2].open) if len(quotes) > 2 else 0
|
||||
limit_up = int(quotes[2].price) if len(quotes) > 2 else 0
|
||||
limit_down = round(quotes[2].open * 10) if len(quotes) > 2 else 0
|
||||
limit_up = round(quotes[2].price * 10) if len(quotes) > 2 else 0
|
||||
return _to_df(
|
||||
MarketStat(
|
||||
up_count=up,
|
||||
@@ -1192,11 +1193,11 @@ class AsyncTdxClient(AsyncHeartbeatMixin):
|
||||
return _to_df(records)
|
||||
|
||||
async def get_market_stat(self) -> pd.DataFrame:
|
||||
"""获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。
|
||||
"""获取 A 股全市场涨跌统计概况(基于 880005/880001/880006 统计指数)。
|
||||
|
||||
注意:
|
||||
`suspended_count` 是 `total - up - down - neutral` 的残差估算值,
|
||||
用于保证计数守恒,不应视为协议已明确验证的停牌字段。
|
||||
通达信这三个"统计指数"的计数类字段(涨/跌/平/总数/涨停/跌停家数)
|
||||
返回的是真实家数的 1/10,需统一 ×10 还原。成交额/量/市值字段不受影响。
|
||||
`suspended_count` 由 `total - up - down - neutral` 推得,用于保证计数守恒。
|
||||
"""
|
||||
# 通达信中 880005 是全市场行情统计,880001 是总市值指数,880006 是涨跌停统计
|
||||
quotes = await self._execute(
|
||||
@@ -1207,13 +1208,14 @@ class AsyncTdxClient(AsyncHeartbeatMixin):
|
||||
if not quotes:
|
||||
raise RuntimeError("无法获取市场统计数据")
|
||||
q = quotes[0]
|
||||
up = int(q.price)
|
||||
down = int(q.open)
|
||||
neutral = int(q.low)
|
||||
total = int(q.high)
|
||||
# 计数字段协议返回值为真实家数 / 10,这里 ×10 还原(见 docstring)
|
||||
up = round(q.price * 10)
|
||||
down = round(q.open * 10)
|
||||
neutral = round(q.low * 10)
|
||||
total = round(q.high * 10)
|
||||
market_cap = quotes[1].price * 1e10 if len(quotes) > 1 else 0.0
|
||||
limit_down = int(quotes[2].open) if len(quotes) > 2 else 0
|
||||
limit_up = int(quotes[2].price) if len(quotes) > 2 else 0
|
||||
limit_down = round(quotes[2].open * 10) if len(quotes) > 2 else 0
|
||||
limit_up = round(quotes[2].price * 10) if len(quotes) > 2 else 0
|
||||
return _to_df(
|
||||
MarketStat(
|
||||
up_count=up,
|
||||
|
||||
@@ -102,61 +102,69 @@ def test_get_security_list_all_filtering(_mock_conn_cls):
|
||||
|
||||
@patch("easy_tdx.client.TdxConnection")
|
||||
def test_get_market_stat_mapping(_mock_conn_cls):
|
||||
"""测试市场统计字段映射。"""
|
||||
"""测试市场统计字段映射。
|
||||
|
||||
通达信统计指数的计数字段返回真实家数的 1/10,get_market_stat 内部需 ×10 还原。
|
||||
这里构造的原始协议值是还原后家数的 1/10,断言还原后等于真实家数。
|
||||
"""
|
||||
client = TdxClient("127.0.0.1")
|
||||
|
||||
mock_quote = SecurityQuote(
|
||||
Market.SH,
|
||||
def _zero_quote(code, **kw):
|
||||
"""构造一只仅关键字段非零的 SecurityQuote,其余五档/活跃度字段取默认 0。"""
|
||||
base = dict(
|
||||
price=0, pre_close=0, open=0, high=0, low=0,
|
||||
vol=0, cur_vol=0, amount=0, s_vol=0, b_vol=0,
|
||||
active1=0, active2=0,
|
||||
bid1=0, bid_vol1=0, bid2=0, bid_vol2=0, bid3=0, bid_vol3=0,
|
||||
bid4=0, bid_vol4=0, bid5=0, bid_vol5=0,
|
||||
ask1=0, ask_vol1=0, ask2=0, ask_vol2=0, ask3=0, ask_vol3=0,
|
||||
ask4=0, ask_vol4=0, ask5=0, ask_vol5=0,
|
||||
rise_speed=0, limit_up=0, limit_down=0,
|
||||
)
|
||||
base.update(kw)
|
||||
return SecurityQuote(Market.SH, code, **base)
|
||||
|
||||
# 880005: 计数字段=真实家数/10;amount/vol 不缩放,原样透传
|
||||
q_stat = _zero_quote(
|
||||
"880005",
|
||||
price=3000.0, # up = int(price)
|
||||
pre_close=0,
|
||||
open=2000.0, # down = int(open)
|
||||
high=5500.0, # total = int(high)
|
||||
low=500.0, # neutral = int(low)
|
||||
price=300.0, # up = 300 * 10 = 3000
|
||||
open=200.0, # down = 200 * 10 = 2000
|
||||
high=550.0, # total= 550 * 10 = 5500
|
||||
low=50.0, # neutral = 50 * 10 = 500
|
||||
vol=1000000.0,
|
||||
cur_vol=0,
|
||||
amount=50000000.0,
|
||||
s_vol=0,
|
||||
b_vol=0,
|
||||
active1=0,
|
||||
active2=0,
|
||||
bid1=0,
|
||||
bid_vol1=0,
|
||||
bid2=0,
|
||||
bid_vol2=0,
|
||||
bid3=0,
|
||||
bid_vol3=0,
|
||||
bid4=0,
|
||||
bid_vol4=0,
|
||||
bid5=0,
|
||||
bid_vol5=0,
|
||||
ask1=0,
|
||||
ask_vol1=0,
|
||||
ask2=0,
|
||||
ask_vol2=0,
|
||||
ask3=0,
|
||||
ask_vol3=0,
|
||||
ask4=0,
|
||||
ask_vol4=0,
|
||||
ask5=0,
|
||||
ask_vol5=0,
|
||||
rise_speed=0,
|
||||
limit_up=0,
|
||||
limit_down=0,
|
||||
)
|
||||
# 880001: 总市值指数点位(不缩放)
|
||||
q_cap = _zero_quote("880001", price=1186.579)
|
||||
# 880006: 涨跌停家数=真实/10
|
||||
q_limit = _zero_quote(
|
||||
"880006",
|
||||
price=13.1, # limit_up = 131
|
||||
open=0.6, # limit_down = 6
|
||||
)
|
||||
|
||||
def mock_execute(cmd):
|
||||
if isinstance(cmd, GetSecurityQuotesCmd):
|
||||
return [mock_quote]
|
||||
return [q_stat, q_cap, q_limit]
|
||||
return []
|
||||
|
||||
with patch.object(TdxClient, "_execute", side_effect=mock_execute):
|
||||
stat = client.get_market_stat()
|
||||
assert isinstance(stat, pd.DataFrame)
|
||||
# 计数字段 ×10 还原
|
||||
assert stat["up_count"].iloc[0] == 3000
|
||||
assert stat["down_count"].iloc[0] == 2000
|
||||
assert stat["neutral_count"].iloc[0] == 500
|
||||
assert stat["total_count"].iloc[0] == 5500
|
||||
assert stat["limit_up_count"].iloc[0] == 131
|
||||
assert stat["limit_down_count"].iloc[0] == 6
|
||||
# suspended = total - up - down - neutral = 5500 - 5500 = 0
|
||||
assert stat["suspended_count"].iloc[0] == 0
|
||||
# 成交额/量不缩放,原样透传
|
||||
assert stat["total_amount"].iloc[0] == 50000000.0
|
||||
assert stat["total_volume"].iloc[0] == 1000000.0
|
||||
# 总市值 = 1186.579 * 1e10
|
||||
assert stat["total_market_cap"].iloc[0] == 1186.579 * 1e10
|
||||
|
||||
|
||||
def test_get_history_fund_flow_parsing():
|
||||
|
||||
Reference in New Issue
Block a user