扩展盘后正式集(涨跌停/人气/龙虎榜/板块日线)与盘中观察 API(报价/指数/分时),网站 bridge 按开关接入并回退旧链路;问天改为按数据依赖跟随开关,不再整栈强制旧路径。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
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
|