Fix protocol regressions and clarify experimental APIs

This commit is contained in:
M
2026-04-15 21:02:26 +08:00
parent daaba7dc13
commit 96f14f70bc
18 changed files with 574 additions and 258 deletions
+21 -17
View File
@@ -1,18 +1,17 @@
"""针对本轮 A 股增强功能的单元测试。"""
import pytest
import struct
from unittest.mock import patch, MagicMock, AsyncMock
from xmtdx import TdxClient, Market
from unittest.mock import patch
from xmtdx import Market, TdxClient
from xmtdx.models.quote import SecurityQuote
from xmtdx.models.security import SecurityInfo
from xmtdx.models.timeseries import TransactionRecord
from xmtdx.models.quote import SecurityQuote
from xmtdx.models.stats import FundFlow, HistoricalFundFlow, MarketStat
@patch("xmtdx.client.TdxConnection")
def test_get_fund_flow_logic(mock_conn_cls):
def test_get_fund_flow_logic(_mock_conn_cls):
"""测试资金流分类计算逻辑。"""
mock_conn = mock_conn_cls.return_value
client = TdxClient("127.0.0.1")
# 构造模拟 Tick 数据
@@ -31,7 +30,7 @@ def test_get_fund_flow_logic(mock_conn_cls):
assert flow.main_net_inflow == 1000000.0 - 250000.0
@patch("xmtdx.client.TdxConnection")
def test_get_security_list_all_filtering(mock_conn_cls):
def test_get_security_list_all_filtering(_mock_conn_cls):
"""测试三市 A 股过滤与行业挂载逻辑。"""
client = TdxClient("127.0.0.1")
@@ -56,18 +55,18 @@ def test_get_security_list_all_filtering(mock_conn_cls):
patch.object(TdxClient, "get_security_list", side_effect=mock_get_list):
all_stocks = client.get_security_list_all()
assert len(all_stocks) == 3
# 预期只有 SH 和 SZ,BJ 已在扫描中降级移除
assert len(all_stocks) == 2
codes = [s.code for s in all_stocks]
assert "600000" in codes
assert "000001" in codes
assert "830000" in codes
assert "830000" not in codes
s0 = next(s for s in all_stocks if s.code == "600000")
assert s0.industry_tdx == "T01"
@patch("xmtdx.client.TdxConnection")
def test_get_market_stat_mapping(mock_conn_cls):
def test_get_market_stat_mapping(_mock_conn_cls):
"""测试市场统计字段映射。"""
client = TdxClient("127.0.0.1")
@@ -75,18 +74,23 @@ def test_get_market_stat_mapping(mock_conn_cls):
Market.SH, "880005",
price=3000.0, # up
pre_close=2000.0, # down
open=500.0, # neutral
open=0,
high=5500.0, # total
low=100.0, vol=1000000.0, cur_vol=0, amount=50000000.0,
low=500.0, # neutral (low=500 -> neutral_count=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,
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
)
with patch.object(TdxClient, "get_security_quotes", return_value=[mock_quote]):
stat = client.get_market_stat()
assert stat.up_count == 3000
assert stat.down_count == 2000
assert stat.neutral_count == 500
assert stat.total_count == 5500
def test_get_history_fund_flow_parsing():
+6 -7
View File
@@ -1,10 +1,12 @@
"""板块信息单元测试。"""
import pytest
import asyncio
import struct
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from xmtdx.client import AsyncTdxClient, TdxClient
from xmtdx.codec.block import parse_block_dat
from xmtdx.models.finance import TdxBlock
@patch("xmtdx.client.AsyncTdxConnection")
@@ -14,7 +16,7 @@ def test_async_get_block_info_logic(mock_conn_cls):
# 模拟异步 execute
async def mock_execute(cmd):
from xmtdx.commands.block_info import GetBlockInfoMetaCmd, GetBlockInfoCmd
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
if isinstance(cmd, GetBlockInfoMetaCmd):
return 100, "hash"
if isinstance(cmd, GetBlockInfoCmd):
@@ -34,10 +36,7 @@ def test_async_get_block_info_logic(mock_conn_cls):
assert isinstance(res, list)
assert mock_conn.execute.call_count == 2 # 1 meta + 1 data
import asyncio
asyncio.run(main())
from xmtdx.codec.block import parse_block_dat
from xmtdx.models.finance import TdxBlock
def test_parse_block_dat_empty():
@@ -81,7 +80,7 @@ def test_get_block_info_logic(mock_conn_cls):
# 模拟 GetBlockInfoMeta 响应:size=35000 (需要2次拉取)
def mock_execute(cmd):
from xmtdx.commands.block_info import GetBlockInfoMetaCmd, GetBlockInfoCmd
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
if isinstance(cmd, GetBlockInfoMetaCmd):
return 35000, "dummy_hash"
if isinstance(cmd, GetBlockInfoCmd):
+2 -1
View File
@@ -266,7 +266,8 @@ def test_xdxr_info_parse():
# share count decode: 通达信自定义浮点,单位万股,与 FinanceInfo.zong_guben/10000 一致
stock_recs = [r for r in recs if 2 <= r.category <= 10]
last = stock_recs[-1]
# 最近一条 hou_zongguben ≈ 3_330_583.75 万股(与 FinanceInfo.zong_guben 33_305_837_500 ÷ 10000 完全吻合)
# 最近一条 hou_zongguben ≈ 3_330_583.75 万股
# 与 FinanceInfo.zong_guben 33_305_837_500 ÷ 10000 完全吻合
assert last.hou_zongguben is not None
assert abs(last.hou_zongguben - 3_330_583.75) < 1.0
+3 -3
View File
@@ -1,9 +1,9 @@
"""心跳机制单元测试。"""
import asyncio
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from xmtdx import AsyncTdxClient, Market
from unittest.mock import AsyncMock, patch
from xmtdx import AsyncTdxClient
def test_heartbeat_sends_periodically():
+109
View File
@@ -0,0 +1,109 @@
"""协议底层修复验证(针对 2026-04-15 审查结论)。"""
import struct
from xmtdx.codec.price_rules import compute_price_limits
from xmtdx.commands.fund_flow import GetHistoryFundFlowCmd
from xmtdx.commands.security_bars import GetSecurityBarsCmd
from xmtdx.commands.security_list import GetSecurityListCmd
from xmtdx.commands.security_quotes import GetSecurityQuotesCmd
from xmtdx.models.enums import KlineCategory, Market
def test_security_bars_exact_layout():
"""验证 K 线请求包布局与旧版 working bytes 完全一致。"""
cmd = GetSecurityBarsCmd(Market.SH, "600000", KlineCategory.DAY, 0, 10)
req = cmd.build_request()
# Header: 0x010C, 0x01016408, 0x1C, 0x1C
# Payload: 0x052D, 1 (Market.SH), "600000", 4 (KlineCategory.DAY), 1, 0 (start), 10, 0, 0, 0
expected = struct.pack(
"<HIHHHH6sHHHHIIH",
0x010C, 0x01016408, 0x001C, 0x001C,
0x052D, 1, b"600000", 4, 1, 0, 10, 0, 0, 0
)
assert req == expected
assert len(req) == 38
def test_history_fund_flow_exact_layout():
"""验证历史资金流请求包布局与 K 线一致,只差 category=22。"""
cmd = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 10)
req = cmd.build_request()
# Header: 0x010C, 0x01016408, 0x1C, 0x1C
# Payload: 0x052D, 1 (Market.SH), "600000", 22, 1, 0, 10, 0, 0, 0
expected = struct.pack(
"<HIHHHH6sHHHHIIH",
0x010C, 0x01016408, 0x001C, 0x001C,
0x052D, 1, b"600000", 22, 1, 0, 10, 0, 0, 0
)
assert req == expected
assert len(req) == 38
def test_security_list_request_length():
"""验证证券列表请求包载荷长度为 6 字节。"""
cmd = GetSecurityListCmd(Market.SH, 0)
req = cmd.build_request()
# Header 12 + Payload 6 = 18
assert len(req) == 18
payload_len = struct.unpack("<H", req[6:8])[0]
assert payload_len == 6
def test_security_quotes_limit_mapping():
"""验证涨跌停价现在返回 None,且 pre_close 正确。"""
from xmtdx.codec.price import put_price
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
# 构造响应报文
body = bytearray(b"\x00\x00")
body.extend(struct.pack("<H", 1))
# Record: Market(B), Code(6s), Active1(H) + ...
body.extend(struct.pack("<B6sH", 1, b"600000", 0))
body.extend(put_price(1010)) # price_raw
body.extend(put_price(-5)) # last_close_diff
body.extend(put_price(0))
body.extend(put_price(0))
body.extend(put_price(0))
body.extend(put_price(12345))
body.extend(put_price(-1010))
body.extend(put_price(100))
body.extend(put_price(10))
body.extend(struct.pack("<I", 10000))
body.extend(put_price(50))
body.extend(put_price(50))
body.extend(put_price(2))
body.extend(put_price(3))
for _ in range(20):
body.extend(put_price(0))
body.extend(struct.pack("<H", 0))
body.extend(put_price(96))
body.extend(put_price(-106))
body.extend(put_price(0))
body.extend(put_price(0))
body.extend(struct.pack("<hH", 0, 0))
quotes = cmd.parse_response(bytes(body))
q = quotes[0]
assert q.limit_up is None
assert q.limit_down is None
assert q.pre_close == 10.05
def test_compute_price_limits_for_stocks():
"""普通股票 / ST / 创业板 / 科创板 / 北交所规则应可正确计算。"""
assert compute_price_limits(Market.SH, "600000", "浦发银行", 10.05) == (11.06, 9.05)
assert compute_price_limits(Market.SH, "603939", "ST益丰", 22.53) == (23.66, 21.4)
assert compute_price_limits(Market.SZ, "301269", "华大九天", 86.36) == (103.63, 69.09)
assert compute_price_limits(Market.SH, "688981", "中芯国际", 101.52) == (121.82, 81.22)
assert compute_price_limits(Market.BJ, "920002", "万达轴承", 84.36) == (109.67, 59.05)
def test_compute_price_limits_for_indices():
"""指数与板块类代码不应计算涨跌停。"""
assert compute_price_limits(Market.SH, "999999", "上证指数", 4026.63) == (None, None)
assert compute_price_limits(Market.SH, "880005", "涨跌家数", 1841.0) == (None, None)
assert compute_price_limits(Market.SZ, "399001", "深证成指", 10412.63) == (None, None)