Files
easy_tdx_max/tests/unit/test_async_transport.py
awayingsandClaude Code f31790e6d9 fix: 适配 2026-09 主站新式握手,修复 K线/市场统计返回空数据
2026-09 起行情主站拒绝旧版 pytdx 三条固定 msg_id 握手建立的连接:
握手有响应,但该连接上 K 线一律返回 2 字节空包(0x0320,声称 800 条)、
880xxx 统计指数快照返回空——服务器不报错只是不给数据,导致 web
/market/stat 500、/bars/index 空列表。

修复:改为新式单条握手(0x000d + payload 0x01 + 随机 msg_id,每连接
新生成),sync/async transport 连接与 sync 心跳统一走
easy_tdx.commands.setup.build_handshake_command()。业务请求格式与固定
msg_id 无需改动(对照实验:新式握手连接上固定 msg_id 请求全部正常;
旧三条命令即使 msg_id 随机化仍被拒)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 11:25:28 +08:00

143 lines
4.7 KiB
Python

"""异步 transport 回归测试。"""
from __future__ import annotations
import asyncio
import struct
import time
from easy_tdx import AsyncTdxClient, Market
from easy_tdx.commands.security_count import GetSecurityCountCmd
from easy_tdx.commands.setup import build_handshake_command
from easy_tdx.exceptions import TdxConnectionError
def _pack_frame(body: bytes) -> bytes:
return struct.pack("<IIIHH", 0, 0, 0, len(body), len(body)) + body
_HANDSHAKE_LEN = len(build_handshake_command())
async def _read_and_ack_handshake(
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
"""假服务器:读取一条握手命令并回一个空响应帧(新式握手,2026-09)。"""
await reader.readexactly(_HANDSHAKE_LEN)
writer.write(_pack_frame(b""))
await writer.drain()
def test_async_client_serializes_concurrent_calls() -> None:
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
try:
await _read_and_ack_handshake(reader, writer)
await reader.readexactly(request_len)
writer.write(_pack_frame(struct.pack("<H", 5)))
await writer.drain()
await reader.readexactly(request_len)
writer.write(_pack_frame(struct.pack("<H", 6)))
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
async def main() -> None:
server = await asyncio.start_server(handle, "127.0.0.1", 0)
port = server.sockets[0].getsockname()[1]
try:
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.2)
await client.connect()
try:
sh_count, sz_count = await asyncio.gather(
client.get_security_count(Market.SH),
client.get_security_count(Market.SZ),
)
finally:
await client.close()
finally:
server.close()
await server.wait_closed()
assert sh_count == 5
assert sz_count == 6
asyncio.run(main())
def test_async_client_auto_reconnect() -> None:
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
connection_ids: list[int] = []
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
connection_ids.append(len(connection_ids) + 1)
connection_id = connection_ids[-1]
try:
await _read_and_ack_handshake(reader, writer)
await reader.readexactly(request_len)
writer.write(_pack_frame(struct.pack("<H", 10 + connection_id)))
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
async def main() -> None:
server = await asyncio.start_server(handle, "127.0.0.1", 0)
port = server.sockets[0].getsockname()[1]
try:
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.2)
first = await client.get_security_count(Market.SH)
second = await client.get_security_count(Market.SH)
await client.close()
finally:
server.close()
await server.wait_closed()
assert first == 11
assert second == 12
assert len(connection_ids) == 2
asyncio.run(main())
def test_async_client_request_timeout() -> None:
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
try:
await _read_and_ack_handshake(reader, writer)
await reader.readexactly(request_len)
await asyncio.sleep(1.0)
finally:
writer.close()
await writer.wait_closed()
async def main() -> None:
server = await asyncio.start_server(handle, "127.0.0.1", 0)
port = server.sockets[0].getsockname()[1]
try:
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.05, auto_reconnect=False)
await client.connect()
t0 = time.monotonic()
try:
await client.get_security_count(Market.SH)
except TdxConnectionError as exc:
elapsed = time.monotonic() - t0
assert "超时" in str(exc) or "timed out" in str(exc)
assert elapsed < 0.3
else: # pragma: no cover - 防御性断言
raise AssertionError("expected timeout")
finally:
await client.close()
finally:
server.close()
await server.wait_closed()
asyncio.run(main())