feat: improve stock detail charts and quote fallbacks

This commit is contained in:
wanghep
2026-04-18 22:16:03 +08:00
parent dc205c5f1b
commit 36fda633df
9 changed files with 851 additions and 83 deletions

View File

@ -8,6 +8,7 @@ from typing import Any
from .db import db_cursor
from .sources.eastmoney import EastMoneyClient
from .sources.sina import SinaClient
from .sources.tencent import TencentClient
def _normalize_value(value: Any) -> Any:
@ -467,6 +468,11 @@ def fetch_stock_detail(stock_code: str) -> dict[str, Any]:
quote_snapshot = eastmoney.fetch_quote_snapshot(stock_code)
except Exception:
quote_snapshot = {}
if not quote_snapshot:
try:
quote_snapshot = TencentClient().fetch_quote_snapshot(stock_code)
except Exception:
quote_snapshot = {}
if not market_daily:
try:
market_daily = SinaClient().fetch_daily_kline(stock_code)

View File

@ -10,6 +10,7 @@ from .config import AppConfig
from .db import db_cursor
from .queries import fetch_trader_actions, fetch_watchlist, fetch_warnings
from .sources.eastmoney import EastMoneyClient
from .sources.tencent import TencentClient
@dataclass(slots=True)
@ -130,30 +131,51 @@ def enrich_stock_metadata(*, config: AppConfig, stock_codes: set[str]) -> None:
existing_rows = {row["stock_code"]: row for row in cursor.fetchall()}
client = EastMoneyClient()
quote_client = TencentClient()
for stock_code in sorted(stock_codes):
row = existing_rows.get(stock_code)
has_industry = bool(row and row.get("industry"))
has_concepts = bool(row and row.get("concept_tags"))
if has_industry and has_concepts:
has_market_value = bool(row and row.get("total_market_value") is not None)
has_circulating_market_value = bool(row and row.get("circulating_market_value") is not None)
if has_industry and has_concepts and has_market_value and has_circulating_market_value:
continue
profile = client.fetch_company_profile(stock_code)
try:
quote_snapshot = quote_client.fetch_quote_snapshot(stock_code)
except Exception:
quote_snapshot = {}
cursor.execute(
"""
INSERT INTO stocks (stock_code, stock_name, market, industry, concept_tags)
VALUES (%s, %s, %s, %s, %s)
INSERT INTO stocks (
stock_code,
stock_name,
market,
industry,
concept_tags,
total_market_value,
circulating_market_value
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
stock_name = COALESCE(NULLIF(VALUES(stock_name), ''), stock_name),
market = COALESCE(NULLIF(VALUES(market), ''), market),
industry = COALESCE(NULLIF(VALUES(industry), ''), industry),
concept_tags = COALESCE(VALUES(concept_tags), concept_tags)
concept_tags = COALESCE(VALUES(concept_tags), concept_tags),
total_market_value = COALESCE(VALUES(total_market_value), total_market_value),
circulating_market_value = COALESCE(VALUES(circulating_market_value), circulating_market_value)
""",
(
stock_code,
profile.get("stock_name") or (row.get("stock_name") if row else stock_code),
profile.get("stock_name")
or quote_snapshot.get("stock_name")
or (row.get("stock_name") if row else stock_code),
profile.get("market"),
profile.get("industry"),
json.dumps(profile.get("concept_tags") or [], ensure_ascii=False),
quote_snapshot.get("total_market_value"),
quote_snapshot.get("circulating_market_value"),
),
)

View File

@ -0,0 +1,81 @@
from __future__ import annotations
from typing import Any
import requests
DEFAULT_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/135.0.0.0 Safari/537.36"
),
"Referer": "https://finance.qq.com/",
}
def infer_symbol(stock_code: str) -> str:
if stock_code.startswith(("6", "9", "5", "688")):
return f"sh{stock_code}"
return f"sz{stock_code}"
def _to_float(value: str | None) -> float | None:
if value in (None, "", "-"):
return None
try:
return float(value)
except (TypeError, ValueError):
return None
class TencentClient:
def fetch_quote_snapshot(self, stock_code: str) -> dict[str, Any]:
symbol = infer_symbol(stock_code)
response = requests.get(
"https://qt.gtimg.cn/q=" + symbol,
headers=DEFAULT_HEADERS,
timeout=20,
)
response.raise_for_status()
text = response.text
if '"' not in text:
return {}
payload = text.split('"', 1)[1].rsplit('"', 1)[0]
parts = payload.split("~")
if len(parts) < 49:
return {}
latest_price = _to_float(parts[3])
previous_close = _to_float(parts[4])
open_price = _to_float(parts[5])
price_chg = _to_float(parts[31])
pct_chg = _to_float(parts[32])
high_price = _to_float(parts[33])
low_price = _to_float(parts[34])
amount_wan = _to_float(parts[37])
turnover_pct = _to_float(parts[38])
amplitude_pct = _to_float(parts[43])
circulating_market_value_yi = _to_float(parts[44])
total_market_value_yi = _to_float(parts[45])
return {
"stock_code": parts[2] or stock_code,
"stock_name": parts[1] or None,
"latest_price": None if latest_price is None else latest_price * 100,
"high_price": None if high_price is None else high_price * 100,
"low_price": None if low_price is None else low_price * 100,
"open_price": None if open_price is None else open_price * 100,
"amount": None if amount_wan is None else amount_wan * 10000,
"previous_close": None if previous_close is None else previous_close * 100,
"turnover": None if turnover_pct is None else turnover_pct * 100,
"price_chg": None if price_chg is None else price_chg * 100,
"pct_chg": None if pct_chg is None else pct_chg * 100,
"amplitude": None if amplitude_pct is None else amplitude_pct * 100,
"circulating_market_value": (
None if circulating_market_value_yi is None else circulating_market_value_yi * 100000000
),
"total_market_value": None if total_market_value_yi is None else total_market_value_yi * 100000000,
}