Files
easy_tdx_max/src/xmtdx/transport/sync.py
T
M ad2fc9c84c feat: 实现全量 A 股列表获取与行业信息自动挂载
1. 编解码器:新增 codec/industry.py 用于解析 tdxhy.cfg 行业配置。
2. 数据模型:SecurityInfo 增加 industry_tdx 和 industry_sw 字段。
3. 客户端 API:增加 get_security_list_all(),支持分页拉取全市场 A 股并自动匹配行业标签。
4. 通讯优化:更新 KNOWN_HOSTS 包含更稳定的华为云及招商证券节点。
5. 文档更新:README.md 同步 API 及数据模型变更。
2026-04-14 22:57:45 +08:00

194 lines
5.8 KiB
Python

"""同步 TCP 连接(基于 socket)。"""
import socket
import time
from types import TracebackType
from typing import TYPE_CHECKING, TypeVar
from ..codec.frame import HEADER_SIZE, decompress_body, parse_header
from ..commands.setup import SETUP_COMMANDS
from ..exceptions import TdxConnectionError
if TYPE_CHECKING:
from ..commands.base import BaseCommand
T = TypeVar("T")
_DEFAULT_HOST = "180.153.18.170"
_DEFAULT_PORT = 7709
_DEFAULT_TIMEOUT = 15.0
# 已知可用的通达信行情服务器(按优先级排序)
KNOWN_HOSTS: list[str] = [
"180.153.18.170",
"124.71.187.122",
"180.153.18.171",
"180.153.18.172",
"119.147.212.81",
"115.238.56.198",
"115.238.90.165",
"218.75.126.9",
"47.107.75.159",
"59.175.238.38",
]
def ping_host(
host: str,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> float | None:
"""测量连接到指定服务器并完成握手所需的时间(秒)。
返回延迟(秒),连接失败时返回 None。
"""
t0 = time.monotonic()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((host, port))
# 发送第一条握手命令并等待响应作为可用性验证
sock.sendall(SETUP_COMMANDS[0])
hdr_buf = _recv_exact_sock(sock, HEADER_SIZE)
hdr = parse_header(hdr_buf)
if hdr.zipsize > 0:
_recv_exact_sock(sock, hdr.zipsize)
return time.monotonic() - t0
except OSError:
return None
finally:
try:
sock.close()
except OSError:
pass
def ping_all(
hosts: list[str] = KNOWN_HOSTS,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
"""并发测量多台服务器延迟,返回按延迟排序的 (host, latency_seconds) 列表。
不可达的服务器不包含在结果中。
"""
import concurrent.futures
results: list[tuple[str, float]] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=len(hosts)) as pool:
futures = {pool.submit(ping_host, h, port, timeout): h for h in hosts}
for fut in concurrent.futures.as_completed(futures):
host = futures[fut]
latency = fut.result()
if latency is not None:
results.append((host, latency))
results.sort(key=lambda t: t[1])
return results
def _recv_exact_sock(sock: socket.socket, n: int) -> bytes:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise TdxConnectionError("连接被服务器关闭")
buf.extend(chunk)
return bytes(buf)
class TdxConnection:
"""同步通达信 TCP 连接。
使用示例::
with TdxConnection("180.153.18.170") as conn:
result = conn.execute(SomeCommand(...))
"""
def __init__(
self,
host: str = _DEFAULT_HOST,
port: int = _DEFAULT_PORT,
timeout: float = _DEFAULT_TIMEOUT,
) -> None:
self.host = host
self.port = port
self.timeout = timeout
self._sock: socket.socket | None = None
def connect(self) -> None:
"""建立 TCP 连接并完成握手(发送3条 setup 命令)。"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
try:
sock.connect((self.host, self.port))
except OSError as e:
sock.close()
raise TdxConnectionError(f"无法连接 {self.host}:{self.port}: {e}") from e
self._sock = sock
self._send_setup()
def close(self) -> None:
"""关闭连接。"""
if self._sock is not None:
try:
self._sock.close()
except OSError:
pass
self._sock = None
def execute(self, cmd: "BaseCommand[T]") -> T:
"""执行一条命令:发送请求,接收并解压响应,返回解析结果。"""
if self._sock is None:
raise TdxConnectionError("未连接,请先调用 connect()")
request = cmd.build_request()
try:
self._sock.sendall(request)
header_buf = self._recv_exact(HEADER_SIZE)
header = parse_header(header_buf)
raw_body = self._recv_exact(header.zipsize)
except OSError as e:
raise TdxConnectionError(f"通信错误: {e}") from e
body = decompress_body(header, raw_body)
return cmd.parse_response(body)
# ------------------------------------------------------------------ #
# context manager
# ------------------------------------------------------------------ #
def __enter__(self) -> "TdxConnection":
self.connect()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()
# ------------------------------------------------------------------ #
# internals
# ------------------------------------------------------------------ #
def _send_setup(self) -> None:
"""按序发送三条握手命令并丢弃响应。"""
assert self._sock is not None
for cmd_bytes in SETUP_COMMANDS:
self._sock.sendall(cmd_bytes)
# 读取并丢弃握手响应
try:
hdr_buf = self._recv_exact(HEADER_SIZE)
hdr = parse_header(hdr_buf)
if hdr.zipsize > 0:
self._recv_exact(hdr.zipsize)
except OSError:
# 部分服务器的握手无响应,忽略错误
pass
def _recv_exact(self, n: int) -> bytes:
"""循环 recv 直到读满 n 字节。"""
assert self._sock is not None
return _recv_exact_sock(self._sock, n)