Fix transport and fund flow protocol edge cases

This commit is contained in:
minionszyw
2026-04-22 17:54:13 +08:00
parent 4cb48daabb
commit aa3b1ac319
8 changed files with 127 additions and 31 deletions
+22 -7
View File
@@ -4,6 +4,7 @@ import struct
from unittest.mock import patch
from xmtdx import Market, TdxClient
from xmtdx.client import _classify_fund_flow
from xmtdx.models.bar import SecurityBar
from xmtdx.models.quote import SecurityQuote
from xmtdx.models.security import SecurityInfo
@@ -18,7 +19,7 @@ def test_get_fund_flow_logic(_mock_conn_cls):
# 构造模拟 Tick 数据
mock_recs = [
TransactionRecord(10, 0, 100.0, 100, 0, 0), # super_in (100*100*100 = 100w)
TransactionRecord(10, 0, 100.0, 101, 0, 0), # super_in (100*101*100 = 101w)
TransactionRecord(10, 1, 10.0, 250, 1, 0), # large_out (10*250*100 = 25w)
TransactionRecord(10, 2, 10.0, 10, 0, 0), # small_in (10*10*100 = 1w)
]
@@ -26,10 +27,24 @@ def test_get_fund_flow_logic(_mock_conn_cls):
with patch.object(TdxClient, "get_transaction_data", return_value=mock_recs):
flow = client.get_fund_flow(Market.SH, "600000")
assert flow.super_in == 1000000.0
assert flow.super_in == 1010000.0
assert flow.large_out == 250000.0
assert flow.small_in == 10000.0
assert flow.main_net_inflow == 1000000.0 - 250000.0
assert flow.main_net_inflow == 1010000.0 - 250000.0
def test_classify_fund_flow_exact_thresholds_use_lower_bucket():
"""恰好命中阈值时,应落入较低一档。"""
flow = _classify_fund_flow([
TransactionRecord(10, 0, 100.0, 100, 0, 0), # 100w -> large
TransactionRecord(10, 1, 100.0, 20, 0, 0), # 20w -> medium
TransactionRecord(10, 2, 100.0, 4, 0, 0), # 4w -> small
])
assert flow.super_in == 0.0
assert flow.large_in == 1000000.0
assert flow.medium_in == 200000.0
assert flow.small_in == 40000.0
@patch("xmtdx.client.TdxConnection")
def test_get_security_list_all_filtering(_mock_conn_cls):
@@ -103,11 +118,11 @@ def test_get_history_fund_flow_parsing():
body = bytearray(9)
body.extend(struct.pack("<H", 1)) # 1 record
# Record: Date(I) + 8 * custom_float(i)
# Record: Date(I) + 8 * custom_float(uint32)
# 2025-01-08
date = 20250108
# 模拟 8 个流向金额
record = struct.pack("<Iiiiiiiii", date, 100, 200, 300, 400, 500, 600, 700, 800)
record = struct.pack("<IIIIIIIII", date, 100, 200, 300, 400, 500, 600, 700, 800)
body.extend(record)
cmd = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 1)
@@ -130,7 +145,7 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
]
txn_map = {
20250108: [
TransactionRecord(10, 0, 100.0, 100, 0, 0),
TransactionRecord(10, 0, 100.0, 101, 0, 0),
TransactionRecord(10, 1, 10.0, 250, 1, 0),
],
20250109: [
@@ -155,7 +170,7 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
year=2025,
month=1,
day=8,
super_in=1000000.0,
super_in=1010000.0,
super_out=0.0,
large_in=0.0,
large_out=250000.0,
+38
View File
@@ -1,6 +1,7 @@
"""协议底层修复验证(针对 2026-04-15 审查结论)。"""
import struct
from unittest.mock import patch
from xmtdx.codec.price_rules import compute_price_limits
from xmtdx.commands.fund_flow import GetHistoryFundFlowCmd
@@ -93,6 +94,14 @@ def test_security_quotes_limit_mapping():
assert q.pre_close == 10.05
def test_security_quotes_server_time_format():
"""服务器时间应按“小时 + 百万分之一小时”统一解码。"""
from xmtdx.commands.security_quotes import _format_server_time
assert _format_server_time(9500000) == "09:30:00.000"
assert _format_server_time(14999212) == "14:59:57.163"
def test_compute_price_limits_for_stocks():
"""普通股票 / ST / 创业板 / 科创板 / 北交所规则应可正确计算。"""
assert compute_price_limits(Market.SH, "600000", "浦发银行", 10.05) == (11.06, 9.05)
@@ -123,3 +132,32 @@ def test_compute_price_limits_for_newly_listed_stocks():
assert compute_price_limits(
Market.BJ, "920002", "北交所新股", 84.36, listed_days=2
) == (109.67, 59.05)
def test_history_fund_flow_uses_uint32_volume_words():
"""历史资金流金额字段必须按 uint32 传给 _decode_volume。"""
raw_words = [
0x80000001,
0xFFFFFFFF,
0x7FFFFFFF,
0x90000000,
0xA0000000,
0xB0000000,
0xC0000000,
0xD0000000,
]
body = bytearray(9)
body.extend(struct.pack("<H", 1))
body.extend(struct.pack("<IIIIIIIII", 20250108, *raw_words))
seen: list[int] = []
def fake_decode(raw: int) -> float:
seen.append(raw)
return float(raw)
with patch("xmtdx.commands.fund_flow._decode_volume", side_effect=fake_decode):
records = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 1).parse_response(bytes(body))
assert seen == raw_words
assert records[0].small_out == float(raw_words[-1])
+44
View File
@@ -0,0 +1,44 @@
"""同步 transport 回归测试。"""
from unittest.mock import patch
from xmtdx.exceptions import TdxConnectionError
from xmtdx.transport.sync import TdxConnection
class _FakeSocket:
def __init__(self) -> None:
self.timeout: float | None = None
self.connected_to: tuple[str, int] | None = None
self.closed = False
def settimeout(self, timeout: float) -> None:
self.timeout = timeout
def connect(self, address: tuple[str, int]) -> None:
self.connected_to = address
def close(self) -> None:
self.closed = True
def test_sync_connection_closes_socket_when_setup_fails() -> None:
sock = _FakeSocket()
conn = TdxConnection("127.0.0.1", port=7709, timeout=0.2)
with patch("xmtdx.transport.sync.socket.socket", return_value=sock), patch.object(
TdxConnection,
"_send_setup",
side_effect=TdxConnectionError("setup failed"),
):
try:
conn.connect()
except TdxConnectionError as exc:
assert "setup failed" in str(exc)
else: # pragma: no cover - 防御性断言
raise AssertionError("expected setup failure")
assert sock.timeout == 0.2
assert sock.connected_to == ("127.0.0.1", 7709)
assert sock.closed is True
assert conn._sock is None