"""腾讯财经数据源适配器。""" from __future__ import annotations from datetime import datetime from typing import List, Optional from urllib.parse import urlencode from urllib.request import Request, urlopen from sidecar.models import KlineBar, Snapshot, parse_float from sidecar.symbols import normalize_symbol class TencentFinanceSource: """腾讯财经行情适配器。""" def __init__(self, timeout_seconds: int = 8) -> None: """ 功能说明:创建腾讯财经数据源。 参数说明:timeout_seconds 为 HTTP 请求超时时间。 返回值说明:无返回值。 注意事项:腾讯接口无需 Key,但字段为 GBK 编码和波浪线分隔。 """ self.timeout_seconds = timeout_seconds def fetch_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]: """ 功能说明:获取 K 线数据。 参数说明:symbol 为股票代码,period 为周期,limit 为最大条数。 返回值说明:腾讯适配器暂不提供 K 线,固定抛出 NotImplementedError。 注意事项:本项目 K 线优先由 mootdx 提供。 """ raise NotImplementedError("腾讯财经适配器暂不提供 K 线") def fetch_snapshot(self, symbol: str) -> Snapshot: """ 功能说明:获取腾讯财经实时行情快照。 参数说明:symbol 为股票代码。 返回值说明:返回统一 Snapshot 模型。 注意事项:PB 使用索引 46,PE_TTM 使用索引 39,避免常见错误映射。 """ normalized = normalize_symbol(symbol) query = urlencode({"q": normalized}) request = Request( "https://qt.gtimg.cn/?" + query, headers={"User-Agent": "Mozilla/5.0 TortoiseSidecar/0.1"}, ) with urlopen(request, timeout=self.timeout_seconds) as response: text = response.read().decode("gbk", errors="ignore") fields = parse_tencent_payload(text) return snapshot_from_tencent_fields(normalized, fields) def parse_tencent_payload(payload: str) -> List[str]: """ 功能说明:解析腾讯财经原始响应。 参数说明:payload 为形如 v_sh600000="..." 的响应文本。 返回值说明:返回按波浪线拆分后的字段列表。 注意事项:响应为空或格式异常时抛出 ValueError。 """ if '="' not in payload: raise ValueError("腾讯财经响应格式异常") content = payload.split('="', 1)[1].split('"', 1)[0] fields = content.split("~") if len(fields) < 53: raise ValueError("腾讯财经响应字段不足") return fields def snapshot_from_tencent_fields(symbol: str, fields: List[str]) -> Snapshot: """ 功能说明:把腾讯财经字段转换为统一快照。 参数说明:symbol 为标准股票代码,fields 为腾讯响应字段列表。 返回值说明:返回 Snapshot 模型。 注意事项:字段索引按项目文档校准,43 是振幅不是 PB。 """ return Snapshot( symbol=symbol, name=_field(fields, 1) or None, trade_time=_parse_trade_time(_field(fields, 30)), price=parse_float(_field(fields, 3)), previous_close=parse_float(_field(fields, 4)), open=parse_float(_field(fields, 5)), high=parse_float(_field(fields, 33)), low=parse_float(_field(fields, 34)), volume=parse_float(_field(fields, 6)), amount=parse_float(_field(fields, 37)), change=parse_float(_field(fields, 31)), change_percent=parse_float(_field(fields, 32)), turnover_rate=parse_float(_field(fields, 38)), pe_ttm=parse_float(_field(fields, 39)), pe_static=parse_float(_field(fields, 52)), pb=parse_float(_field(fields, 46)), market_cap=parse_float(_field(fields, 44)), float_market_cap=parse_float(_field(fields, 45)), limit_up=parse_float(_field(fields, 47)), limit_down=parse_float(_field(fields, 48)), source="tencent", ) def _field(fields: List[str], index: int) -> str: """ 功能说明:安全读取腾讯字段。 参数说明:fields 为字段列表,index 为字段索引。 返回值说明:字段存在返回原始字符串,不存在返回空字符串。 注意事项:腾讯字段偶发缺失时可避免 IndexError。 """ return fields[index] if index < len(fields) else "" def _parse_trade_time(value: str) -> Optional[datetime]: """ 功能说明:解析腾讯交易时间字段。 参数说明:value 为 YYYYMMDDHHMMSS 格式字符串。 返回值说明:解析成功返回 datetime,空值或异常返回 None。 注意事项:返回值不带时区,表示交易所本地时间。 """ if not value: return None try: return datetime.strptime(value[:14], "%Y%m%d%H%M%S") except ValueError: return None