mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 15:44:15 +08:00
1. 核心模型:增加 TdxBlock dataclass。 2. 协议命令:实现 GetBlockInfoMetaCmd 与 GetBlockInfoCmd。 3. 编解码器:增加 codec/block.py,支持 .dat 板块文件二进制解析。 4. 客户端 API:TdxClient 和 AsyncTdxClient 增加 get_block_info(),支持分片拉取。 5. 测试与验证:增加单元测试 tests/unit/test_block_info.py 及实测脚本。 6. 文档更新:README.md 同步 API 及安装说明。
426 lines
15 KiB
Python
426 lines
15 KiB
Python
"""高层行情 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)
|