from __future__ import annotations import time import urllib.error import urllib.request from datetime import datetime from typing import Any from datahub.adapters.base import AdapterError, MarketAdapter from datahub.numbers import finite_number, round4 TENCENT_INDEX_URL = "https://qt.gtimg.cn/q=sh000001,sz399001,sz399006" BROWSER_UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" ) class TencentAdapter(MarketAdapter): name = "tencent" def __init__(self, timeout: int = 8) -> None: self.timeout = timeout def probe(self) -> dict[str, Any]: started = time.perf_counter() try: rows = self.fetch_indices() state = "ok" if len(rows) == 3 else "empty" except AdapterError as exc: return { "provider": self.name, "configured": True, "state": "error", "message": str(exc), "latency_ms": round((time.perf_counter() - started) * 1000), } return { "provider": self.name, "configured": True, "state": state, "latency_ms": round((time.perf_counter() - started) * 1000), } def fetch(self, dataset: str, params: dict[str, Any]) -> list[dict[str, Any]]: if dataset in {"indexes_quotes", "index_quotes"}: return self.fetch_indices() raise AdapterError(f"{self.name} unsupported dataset: {dataset}") def normalize(self, dataset: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: return list(rows) def fetch_indices(self) -> list[dict[str, Any]]: request = urllib.request.Request( TENCENT_INDEX_URL, headers={"User-Agent": BROWSER_UA, "Referer": "https://gu.qq.com/"}, method="GET", ) try: with urllib.request.urlopen(request, timeout=self.timeout) as response: raw = response.read().decode("gb18030", errors="ignore") except Exception as exc: raise AdapterError(f"tencent request failed: {exc}") from exc result = [] for line in raw.splitlines(): if '="' not in line: continue fields = line.split('="', 1)[1].rsplit('";', 1)[0].split("~") if len(fields) < 38: continue code = fields[2] if code not in {"000001", "399001", "399006"}: continue try: quote_time = datetime.strptime(fields[30], "%Y%m%d%H%M%S").astimezone() except ValueError as exc: raise AdapterError(f"Tencent invalid quote time for {code}") from exc ts_code = "000001.SH" if code == "000001" else f"{code}.SZ" result.append( { "ts_code": ts_code, "code": code, "name": fields[1] or code, "price": round4(finite_number(fields[3])), "pct_chg": round4(finite_number(fields[32])), "change_amount": round4(finite_number(fields[31])), "open": round4(finite_number(fields[5])), "high": round4(finite_number(fields[33])), "low": round4(finite_number(fields[34])), "previous_close": round4(finite_number(fields[4])), "amount": round4(finite_number(fields[37]) * 10000), "quote_time_epoch": int(quote_time.timestamp()), "quote_time": quote_time.isoformat(timespec="seconds"), "source": "tencent_qt", } ) if len(result) != 3: raise AdapterError(f"Tencent returned {len(result)}/3 indices") return result