"""高层行情 API:TdxClient(同步)和 AsyncTdxClient(asyncio)。""" import asyncio from types import TracebackType from typing import TypeVar from .commands.base import BaseCommand from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd from .commands.finance_info import GetFinanceInfoCmd from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd from .commands.security_count import GetSecurityCountCmd from .commands.security_list import GetSecurityListCmd from .commands.security_quotes import GetSecurityQuotesCmd from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd from .commands.xdxr_info import GetXdxrInfoCmd from .codec.block import parse_block_dat from .exceptions import TdxConnectionError from .models.bar import SecurityBar from .models.enums import KlineCategory, Market from .models.finance import CompanyInfoCategory, FinanceInfo, TdxBlock, XdxrRecord from .models.quote import SecurityQuote from .models.security import SecurityInfo from .models.timeseries import MinuteBar, TransactionRecord from .transport.async_ import AsyncTdxConnection from .transport.sync import KNOWN_HOSTS, TdxConnection, ping_all _DEFAULT_PORT = 7709 _T = TypeVar("_T") # ============================================================ # 同步客户端 # ============================================================ class TdxClient: """同步通达信行情客户端,支持 IP 优选与断线自动重连。 使用示例:: # 单台服务器 with TdxClient("180.153.18.170") as c: bars = c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 100) # 自动从候选列表中选延迟最低的服务器 with TdxClient.from_best_host() as c: count = c.get_security_count(Market.SH) """ def __init__( self, host: str = KNOWN_HOSTS[0], port: int = _DEFAULT_PORT, timeout: float = 15.0, auto_reconnect: bool = True, ) -> None: self._host = host self._port = port self._timeout = timeout self._auto_reconnect = auto_reconnect self._conn = TdxConnection(host, port, timeout) # ------------------------------------------------------------------ # # 工厂方法:自动优选最低延迟服务器 # ------------------------------------------------------------------ # @classmethod def from_best_host( cls, hosts: list[str] = KNOWN_HOSTS, port: int = _DEFAULT_PORT, timeout: float = 15.0, ping_timeout: float = 5.0, auto_reconnect: bool = True, ) -> "TdxClient": """测量 hosts 中所有服务器延迟,选最低延迟的建立连接。 若所有服务器均不可达,回退到 hosts[0]。 """ ranked = ping_all(hosts, port, ping_timeout) best = ranked[0][0] if ranked else hosts[0] return cls(best, port, timeout, auto_reconnect) @staticmethod def ping_all( hosts: list[str] = KNOWN_HOSTS, port: int = _DEFAULT_PORT, timeout: float = 5.0, ) -> list[tuple[str, float]]: """测量多台服务器延迟,返回按延迟排序的 (host, seconds) 列表。""" return ping_all(hosts, port, timeout) # ------------------------------------------------------------------ # # 连接管理 # ------------------------------------------------------------------ # def connect(self) -> None: self._conn.connect() def close(self) -> None: self._conn.close() def __enter__(self) -> "TdxClient": self.connect() return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.close() # ------------------------------------------------------------------ # # 内部执行:含自动重连 # ------------------------------------------------------------------ # def _execute(self, cmd: "BaseCommand[_T]") -> _T: """执行命令;断线时尝试重连一次再重试(若 auto_reconnect=True)。""" try: return self._conn.execute(cmd) except TdxConnectionError: if not self._auto_reconnect: raise # 重连后重试一次 self._conn.close() self._conn = TdxConnection(self._host, self._port, self._timeout) self._conn.connect() return self._conn.execute(cmd) # ------------------------------------------------------------------ # # 市场信息 # ------------------------------------------------------------------ # def get_security_count(self, market: Market) -> int: """获取市场证券总数。""" return self._execute(GetSecurityCountCmd(market)) def get_security_list(self, market: Market, start: int) -> list[SecurityInfo]: """获取证券列表(每页约1000条,按 start 分页)。""" return self._execute(GetSecurityListCmd(market, start)) def get_security_quotes( self, stocks: list[tuple[Market, str]] ) -> list[SecurityQuote]: """批量获取实时五档行情(最多80只/次)。""" return self._execute(GetSecurityQuotesCmd(stocks)) # ------------------------------------------------------------------ # # K 线 # ------------------------------------------------------------------ # def get_security_bars( self, market: Market, code: str, category: KlineCategory, start: int, count: int = 800, ) -> list[SecurityBar]: """获取 K 线数据(最多800条/次,按 start 分页)。""" return self._execute(GetSecurityBarsCmd(market, code, category, start, count)) def get_index_bars( self, market: Market, code: str, category: KlineCategory, start: int, count: int = 800, ) -> list[SecurityBar]: """获取指数 K 线数据。""" return self._execute(GetIndexBarsCmd(market, code, category, start, count)) # ------------------------------------------------------------------ # # 分时 # ------------------------------------------------------------------ # def get_minute_time_data(self, market: Market, code: str) -> list[MinuteBar]: """获取今日分时数据(240条)。""" return self._execute(GetMinuteTimeDataCmd(market, code)) def get_history_minute_time_data( self, market: Market, code: str, date: int ) -> list[MinuteBar]: """获取历史某日分时数据(date: YYYYMMDD)。""" return self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) # ------------------------------------------------------------------ # # 逐笔成交 # ------------------------------------------------------------------ # def get_transaction_data( self, market: Market, code: str, start: int, count: int = 800 ) -> list[TransactionRecord]: """获取当日逐笔成交(分页)。""" return self._execute(GetTransactionDataCmd(market, code, start, count)) def get_history_transaction_data( self, market: Market, code: str, date: int, start: int, count: int = 800 ) -> list[TransactionRecord]: """获取历史逐笔成交(date: YYYYMMDD,分页)。""" return self._execute( GetHistoryTransactionDataCmd(market, code, date, start, count) ) # ------------------------------------------------------------------ # # 财务 / 公司 # ------------------------------------------------------------------ # def get_xdxr_info(self, market: Market, code: str) -> list[XdxrRecord]: """获取除权除息历史记录。""" return self._execute(GetXdxrInfoCmd(market, code)) def get_finance_info(self, market: Market, code: str) -> FinanceInfo: """获取最新财务数据。""" return self._execute(GetFinanceInfoCmd(market, code)) def get_company_info_category( self, market: Market, code: str ) -> list[CompanyInfoCategory]: """获取公司信息文件目录。""" return self._execute(GetCompanyInfoCategoryCmd(market, code)) def get_company_info_content( self, market: Market, code: str, filename: str, offset: int, length: int ) -> str: """读取公司信息文本。""" return self._execute( GetCompanyInfoContentCmd(market, code, filename, offset, length) ) def get_block_info(self, filename: str) -> list[TdxBlock]: """获取并解析板块文件(行业、概念、风格等)。 常用文件名: 'block_zs.dat' - 行业/指数板块 'block_gn.dat' - 概念板块 'block_fg.dat' - 风格板块 """ size, _hash = self._execute(GetBlockInfoMetaCmd(filename)) full_data = bytearray() pos = 0 chunk_size = 30000 while pos < size: chunk = self._execute(GetBlockInfoCmd(filename, pos, chunk_size)) if not chunk: break full_data.extend(chunk) pos += len(chunk) return parse_block_dat(bytes(full_data), filename) # ============================================================ # 异步客户端 # ============================================================ class AsyncTdxClient: """异步通达信行情客户端(asyncio)。 使用示例:: async with AsyncTdxClient("180.153.18.170") as c: bars = await c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 100) 注意: 单个 AsyncTdxClient 仅维护一条 TCP 连接;并发调用会在连接内串行执行。 """ def __init__( self, host: str = KNOWN_HOSTS[0], port: int = _DEFAULT_PORT, timeout: float = 15.0, auto_reconnect: bool = True, ) -> None: self._host = host self._port = port self._timeout = timeout self._auto_reconnect = auto_reconnect self._conn = AsyncTdxConnection(host, port, timeout) self._execute_lock = asyncio.Lock() @classmethod def from_best_host( cls, hosts: list[str] = KNOWN_HOSTS, port: int = _DEFAULT_PORT, timeout: float = 15.0, ping_timeout: float = 5.0, auto_reconnect: bool = True, ) -> "AsyncTdxClient": """测量 hosts 中所有服务器延迟,选最低延迟的建立连接。""" ranked = ping_all(hosts, port, ping_timeout) best = ranked[0][0] if ranked else hosts[0] return cls(best, port, timeout, auto_reconnect) @staticmethod def ping_all( hosts: list[str] = KNOWN_HOSTS, port: int = _DEFAULT_PORT, timeout: float = 5.0, ) -> list[tuple[str, float]]: """测量多台服务器延迟,返回按延迟排序的 (host, seconds) 列表。""" return ping_all(hosts, port, timeout) async def connect(self) -> None: await self._conn.connect() async def close(self) -> None: await self._conn.close() async def __aenter__(self) -> "AsyncTdxClient": await self.connect() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.close() async def _execute(self, cmd: "BaseCommand[_T]") -> _T: """执行命令;断线时尝试重连一次再重试(若 auto_reconnect=True)。""" async with self._execute_lock: try: return await self._conn.execute(cmd) except TdxConnectionError: if not self._auto_reconnect: raise await self._conn.close() self._conn = AsyncTdxConnection(self._host, self._port, self._timeout) await self._conn.connect() return await self._conn.execute(cmd) async def get_security_count(self, market: Market) -> int: return await self._execute(GetSecurityCountCmd(market)) async def get_security_list(self, market: Market, start: int) -> list[SecurityInfo]: return await self._execute(GetSecurityListCmd(market, start)) async def get_security_quotes( self, stocks: list[tuple[Market, str]] ) -> list[SecurityQuote]: return await self._execute(GetSecurityQuotesCmd(stocks)) async def get_security_bars( self, market: Market, code: str, category: KlineCategory, start: int, count: int = 800, ) -> list[SecurityBar]: return await self._execute( GetSecurityBarsCmd(market, code, category, start, count) ) async def get_index_bars( self, market: Market, code: str, category: KlineCategory, start: int, count: int = 800, ) -> list[SecurityBar]: return await self._execute(GetIndexBarsCmd(market, code, category, start, count)) async def get_minute_time_data(self, market: Market, code: str) -> list[MinuteBar]: return await self._execute(GetMinuteTimeDataCmd(market, code)) async def get_history_minute_time_data( self, market: Market, code: str, date: int ) -> list[MinuteBar]: return await self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) async def get_transaction_data( self, market: Market, code: str, start: int, count: int = 800 ) -> list[TransactionRecord]: return await self._execute(GetTransactionDataCmd(market, code, start, count)) async def get_history_transaction_data( self, market: Market, code: str, date: int, start: int, count: int = 800 ) -> list[TransactionRecord]: return await self._execute( GetHistoryTransactionDataCmd(market, code, date, start, count) ) async def get_xdxr_info(self, market: Market, code: str) -> list[XdxrRecord]: return await self._execute(GetXdxrInfoCmd(market, code)) async def get_finance_info(self, market: Market, code: str) -> FinanceInfo: return await self._execute(GetFinanceInfoCmd(market, code)) async def get_company_info_category( self, market: Market, code: str ) -> list[CompanyInfoCategory]: return await self._execute(GetCompanyInfoCategoryCmd(market, code)) async def get_company_info_content( self, market: Market, code: str, filename: str, offset: int, length: int ) -> str: return await self._execute( GetCompanyInfoContentCmd(market, code, filename, offset, length) ) async def get_block_info(self, filename: str) -> list[TdxBlock]: """获取并解析板块文件(行业、概念、风格等)。""" size, _hash = await self._execute(GetBlockInfoMetaCmd(filename)) full_data = bytearray() pos = 0 chunk_size = 30000 while pos < size: chunk = await self._execute(GetBlockInfoCmd(filename, pos, chunk_size)) if not chunk: break full_data.extend(chunk) pos += len(chunk) return parse_block_dat(bytes(full_data), filename)