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 及数据模型变更。
This commit is contained in:
M
2026-04-14 22:57:45 +08:00
parent b049511010
commit ad2fc9c84c
5 changed files with 97 additions and 0 deletions
+66
View File
@@ -17,6 +17,7 @@ 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 .codec.industry import parse_tdxhy_cfg
from .exceptions import TdxConnectionError
from .models.bar import SecurityBar
from .models.enums import KlineCategory, Market
@@ -145,6 +146,41 @@ class TdxClient:
"""获取证券列表(每页约1000条,按 start 分页)。"""
return self._execute(GetSecurityListCmd(market, start))
def get_security_list_all(self) -> list[SecurityInfo]:
"""获取全市场(沪深 A 股)完整证券列表,并自动挂载行业信息。"""
# 1. 尝试获取行业配置
industry_map = {}
try:
cfg_data = self.get_report_file("tdxhy.cfg")
if cfg_data:
industry_map = parse_tdxhy_cfg(cfg_data)
except Exception:
pass
all_stocks: list[SecurityInfo] = []
for market in [Market.SH, Market.SZ]:
count = self.get_security_count(market)
for start in range(0, count, 1000):
stocks = self.get_security_list(market, start)
for s in stocks:
# 精确 A 股过滤规则
is_a_share = False
if market == Market.SH:
# 沪市 A 股:60xxxx, 68xxxx
if s.code.startswith(("60", "68")):
is_a_share = True
elif market == Market.SZ:
# 深市 A 股:00xxxx (主板), 30xxxx (创业板)
if s.code.startswith(("00", "30")):
is_a_share = True
if is_a_share:
# 挂载行业信息
if s.code in industry_map:
s.industry_tdx, s.industry_sw = industry_map[s.code]
all_stocks.append(s)
return all_stocks
def get_security_quotes(
self, stocks: list[tuple[Market, str]]
) -> list[SecurityQuote]:
@@ -416,6 +452,36 @@ class AsyncTdxClient:
async def get_security_list(self, market: Market, start: int) -> list[SecurityInfo]:
return await self._execute(GetSecurityListCmd(market, start))
async def get_security_list_all(self) -> list[SecurityInfo]:
"""获取全市场完整证券列表,并自动挂载行业信息。"""
industry_map = {}
try:
cfg_data = await self.get_report_file("tdxhy.cfg")
if cfg_data:
industry_map = parse_tdxhy_cfg(cfg_data)
except Exception:
pass
all_stocks: list[SecurityInfo] = []
for market in [Market.SH, Market.SZ]:
count = await self.get_security_count(market)
for start in range(0, count, 1000):
stocks = await self.get_security_list(market, start)
for s in stocks:
is_a_share = False
if market == Market.SH:
if s.code.startswith(("60", "68")):
is_a_share = True
elif market == Market.SZ:
if s.code.startswith(("00", "30")):
is_a_share = True
if is_a_share:
if s.code in industry_map:
s.industry_tdx, s.industry_sw = industry_map[s.code]
all_stocks.append(s)
return all_stocks
async def get_security_quotes(
self, stocks: list[tuple[Market, str]]
) -> list[SecurityQuote]:
+23
View File
@@ -0,0 +1,23 @@
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
"""解析 tdxhy.cfg 字节内容。
返回字典: { "code": (tdx_industry, sw_industry), ... }
"""
results = {}
try:
text = content.decode("gbk", errors="replace")
for line in text.splitlines():
parts = line.strip().split("|")
if len(parts) >= 3:
# 格式: 市场|代码|行业1|||行业2
# 我们只关心 A 股 6 位代码
code = parts[1]
if len(code) == 6:
tdx_ind = parts[2]
sw_ind = parts[5] if len(parts) >= 6 else ""
results[code] = (tdx_ind, sw_ind)
except Exception:
pass
return results
+4
View File
@@ -16,4 +16,8 @@ class SecurityInfo:
decimal_point: int # 价格小数位数
pre_close: float # 昨收价(已修复 pytdx Bug #3:改用正确价格解码)
# 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得)
industry_tdx: str = "" # 通达信行业代码 (如 T1001)
industry_sw: str = "" # 申万行业代码 (如 X500102)
_raw: bytes = field(default=b"", repr=False, compare=False)
+2
View File
@@ -21,8 +21,10 @@ _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",