astock-agent/backend/app/data/tencent_client.py
2026-05-14 17:02:13 +08:00

238 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""腾讯实时行情 API 客户端
通过 qt.gtimg.cn 获取 A 股实时报价数据,无需认证。
用于补充 Tushare 的实时行情数据。
"""
import logging
import httpx
from app.data.cache import cache
from app.config import settings
from app.data.code_utils import normalize_ts_code, split_ts_code
from app.data.models import StockQuote
from app.db.error_logger import log_error
logger = logging.getLogger(__name__)
TENCENT_API_URL = "http://qt.gtimg.cn/q="
HEADERS = {
"Referer": "http://finance.qq.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
}
def _ts_code_to_tencent(ts_code: str) -> str:
"""600519.SH -> sh600519"""
code, market = split_ts_code(ts_code)
return f"{market.lower()}{code}"
def _parse_tencent_response(raw: str) -> dict | None:
"""解析腾讯行情接口返回的 GBK 编码、~分隔字段"""
try:
data_start = raw.find('"')
data_end = raw.rfind('"')
if data_start == -1 or data_end == -1:
return None
content = raw[data_start + 1:data_end]
fields = content.split("~")
if len(fields) < 48:
return None
return {
"name": fields[1],
"code": fields[2],
"price": float(fields[3]) if fields[3] else 0,
"pre_close": float(fields[4]) if fields[4] else 0,
"open": float(fields[5]) if fields[5] else 0,
"volume": float(fields[6]) if fields[6] else 0, # 手
"bid_volume": float(fields[7]) if fields[7] else 0, # 外盘
"ask_volume": float(fields[8]) if fields[8] else 0, # 内盘
"pct_chg": float(fields[32]) if fields[32] else 0, # %
"high": float(fields[33]) if fields[33] else 0,
"low": float(fields[34]) if fields[34] else 0,
"amount": float(fields[37]) if fields[37] else 0, # 万元
"turnover_rate": float(fields[38]) if fields[38] else 0, # %
"pe": float(fields[39]) if fields[39] else None,
"amplitude": float(fields[43]) if fields[43] else 0, # %
"circ_mv": float(fields[44]) if fields[44] else None, # 亿
"total_mv": float(fields[45]) if fields[45] else None, # 亿
"pb": float(fields[46]) if fields[46] else None,
"limit_up": float(fields[47]) if fields[47] else None,
"limit_down": float(fields[48]) if fields[48] else None,
"volume_ratio": float(fields[49]) if len(fields) > 49 and fields[49] else None,
}
except (IndexError, ValueError) as e:
logger.warning(f"腾讯行情解析失败: {e}")
return None
async def get_realtime_quote(ts_code: str) -> StockQuote | None:
"""获取单只股票实时行情"""
ts_code = normalize_ts_code(ts_code)
cache_key = f"rt_quote:{ts_code}"
cached = cache.get(cache_key)
if cached is not None:
return cached
symbol = _ts_code_to_tencent(ts_code)
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{TENCENT_API_URL}{symbol}",
headers=HEADERS,
timeout=10
)
resp.encoding = "gbk"
data = _parse_tencent_response(resp.text)
if not data or data["price"] == 0:
return None
quote = StockQuote(
ts_code=ts_code,
name=data["name"],
price=data["price"],
pct_chg=data["pct_chg"],
volume=data["volume"],
amount=data["amount"],
turnover_rate=data["turnover_rate"],
pe=data["pe"],
pb=data["pb"],
circ_mv=data["circ_mv"],
total_mv=data["total_mv"],
volume_ratio=data["volume_ratio"],
high=data["high"],
low=data["low"],
open=data["open"],
pre_close=data["pre_close"],
limit_up=data["limit_up"],
limit_down=data["limit_down"],
amplitude=data["amplitude"],
)
cache.set(cache_key, quote, settings.cache_ttl_realtime)
return quote
except Exception as e:
logger.error(f"腾讯行情获取失败 {ts_code}: {e}")
await log_error("tencent", f"腾讯行情获取失败 {ts_code}: {e}")
return None
async def get_realtime_quotes_batch(ts_codes: list[str]) -> dict[str, StockQuote]:
"""批量获取实时行情(腾讯支持逗号拼接多只)"""
results = {}
uncached = []
ts_codes = [normalize_ts_code(code) for code in ts_codes if normalize_ts_code(code)]
for code in ts_codes:
cached = cache.get(f"rt_quote:{code}")
if cached is not None:
results[code] = cached
else:
uncached.append(code)
if not uncached:
return results
# 腾讯支持批量查询但单次不宜太多分批每批最多50只
batch_size = 50
for i in range(0, len(uncached), batch_size):
batch = uncached[i:i + batch_size]
symbols = ",".join(_ts_code_to_tencent(c) for c in batch)
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{TENCENT_API_URL}{symbols}",
headers=HEADERS,
timeout=15
)
resp.encoding = "gbk"
lines = resp.text.strip().split("\n")
for j, line in enumerate(lines):
if j >= len(batch):
break
data = _parse_tencent_response(line)
if data and data["price"] > 0:
ts_code = batch[j]
quote = StockQuote(
ts_code=ts_code,
name=data["name"],
price=data["price"],
pct_chg=data["pct_chg"],
volume=data["volume"],
amount=data["amount"],
turnover_rate=data["turnover_rate"],
pe=data["pe"],
pb=data["pb"],
circ_mv=data["circ_mv"],
total_mv=data["total_mv"],
volume_ratio=data["volume_ratio"],
high=data["high"],
low=data["low"],
open=data["open"],
pre_close=data["pre_close"],
limit_up=data["limit_up"],
limit_down=data["limit_down"],
amplitude=data["amplitude"],
)
cache.set(f"rt_quote:{ts_code}", quote, settings.cache_ttl_realtime)
results[ts_code] = quote
except Exception as e:
logger.error(f"腾讯批量行情获取失败: {e}")
await log_error(
"tencent",
f"腾讯批量行情获取失败: {e}",
detail=f"batch_size={len(batch)}",
)
return results
async def get_index_realtime(index_codes: list[str] = None) -> dict[str, dict]:
"""获取指数实时行情
index_codes: Tushare 格式的指数代码,如 ["000001.SH", "399001.SZ"]
默认返回上证、深证、创业板
"""
if not index_codes:
index_codes = ["000001.SH", "399001.SZ", "399006.SZ"]
index_codes = [normalize_ts_code(code) for code in index_codes if normalize_ts_code(code)]
results = {}
symbols = ",".join(_ts_code_to_tencent(c) for c in index_codes)
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{TENCENT_API_URL}{symbols}",
headers=HEADERS,
timeout=10
)
resp.encoding = "gbk"
lines = resp.text.strip().split("\n")
for i, line in enumerate(lines):
if i >= len(index_codes):
break
data = _parse_tencent_response(line)
if data and data["price"] > 0:
ts_code = index_codes[i]
results[ts_code] = {
"ts_code": ts_code,
"name": data["name"],
"price": data["price"],
"pct_chg": data["pct_chg"],
"volume": data["volume"],
"amount": data["amount"],
"high": data["high"],
"low": data["low"],
"open": data["open"],
"pre_close": data["pre_close"],
}
except Exception as e:
logger.error(f"腾讯指数行情获取失败: {e}")
await log_error(
"tencent",
f"腾讯指数行情获取失败: {e}",
detail=f"indices={','.join(index_codes)}",
)
return results