feat: improve stock detail charts and quote fallbacks
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
81
backend/src/lhbfx/sources/tencent.py
Normal file
81
backend/src/lhbfx/sources/tencent.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user