mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 20:24:16 +08:00
Fix protocol regressions and clarify experimental APIs
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user