"""板块实时数据富化。 复用东方财富板块实时排名,为数据库中的板块快照补充今日实时涨幅、广度和成交额。 不触发扫描,只做轻量覆盖。 """ import logging from app.config import should_prefer_realtime_today, today_trade_date from app.data.eastmoney_client import get_sector_realtime_ranking from app.data.models import SectorInfo from app.data.tushare_client import tushare_client from app.analysis.theme_mapper import merge_sectors_to_themes logger = logging.getLogger(__name__) def _match_sector_name(em_name: str, ts_name: str) -> bool: """东方财富板块名与 Tushare 板块名模糊匹配。""" em_clean = em_name.rstrip("行业").rstrip("板块").rstrip("概念").strip() ts_clean = ts_name.rstrip("行业").rstrip("板块").rstrip("概念").strip() if em_clean == ts_clean: return True short, long = (em_clean, ts_clean) if len(em_clean) <= len(ts_clean) else (ts_clean, em_clean) return short in long def _apply_empty_overlay(sector: SectorInfo) -> SectorInfo: sector.realtime_pct_change = None sector.realtime_limit_up_count = None sector.realtime_amount = None sector.realtime_turnover_rate = None sector.realtime_up_count = None sector.realtime_down_count = None sector.leading_stocks_realtime = [] sector.is_realtime = False sector.data_mode = "daily_snapshot" sector.source = sector.source or "snapshot" sector.board_type = sector.board_type or "snapshot" return sector def _sector_from_eastmoney(item: dict) -> SectorInfo: """把东方财富板块榜转换成今日展示用 SectorInfo。""" sector = SectorInfo( sector_code=item.get("sector_code", ""), sector_name=item.get("sector_name", ""), board_type=item.get("board_type", "industry"), trade_date=today_trade_date(), pct_change=float(item.get("pct_change", 0) or 0), capital_inflow=0, limit_up_count=0, days_continuous=0, heat_score=round(max(float(item.get("pct_change", 0) or 0), 0) * 12, 1), stage="intraday", turnover_avg=float(item.get("turnover_rate", 0) or 0), realtime_pct_change=float(item.get("pct_change", 0) or 0), realtime_limit_up_count=None, realtime_amount=round(float(item.get("amount", 0) or 0) / 10000, 2), realtime_turnover_rate=float(item.get("turnover_rate", 0) or 0), realtime_up_count=int(item.get("up_count", 0) or 0), realtime_down_count=int(item.get("down_count", 0) or 0), is_realtime=True, data_mode="realtime_today", source=item.get("source", "eastmoney"), ) if item.get("leading_stock_name"): sector.leading_stocks_realtime = [{ "ts_code": item.get("leading_stock_code", ""), "name": item.get("leading_stock_name", ""), "pct_chg": float(item.get("leading_stock_pct", 0) or 0), "amount": 0, }] sector.leading_stocks = sector.leading_stocks_realtime return sector async def get_today_realtime_sector_board(limit: int = 20) -> list[SectorInfo]: """用实时行业榜 + 概念榜生成展示列表,作为 Tushare/定时扫描滞后的兜底。""" industry_sectors = await get_sector_realtime_ranking(fs="m:90+t:2", page_size=max(limit, 20), notify=False) concept_sectors = await get_sector_realtime_ranking(fs="m:90+t:3", page_size=max(limit, 20), notify=False) em_sectors = industry_sectors + concept_sectors if not em_sectors: try: from app.data.sina_client import get_sector_realtime_ranking_by_industry em_sectors = await get_sector_realtime_ranking_by_industry(limit=max(limit, 20)) except Exception as e: logger.warning("新浪行业实时榜兜底失败: %s", e) em_sectors = [] deduped = {} for item in em_sectors: key = f"{item.get('board_type', 'industry')}::{item.get('sector_name', '')}" existing = deduped.get(key) if existing is None or float(item.get("pct_change", 0) or 0) > float(existing.get("pct_change", 0) or 0): deduped[key] = item sectors = [_sector_from_eastmoney(item) for item in deduped.values()] sectors = merge_sectors_to_themes(sectors, limit=limit) return sectors[:limit] async def enrich_sectors_with_realtime(sectors: list[SectorInfo]) -> list[SectorInfo]: """按需为板块快照追加实时字段并重排。""" if not sectors: return sectors latest_trade_date = sectors[0].trade_date or tushare_client.get_latest_trade_date() if not should_prefer_realtime_today(latest_trade_date): return merge_sectors_to_themes([_apply_empty_overlay(sector) for sector in sectors], limit=len(sectors)) try: em_sectors = await get_sector_realtime_ranking() except Exception: logger.warning("东方财富板块实时数据获取失败,回退到日级快照") return merge_sectors_to_themes([_apply_empty_overlay(sector) for sector in sectors], limit=len(sectors)) if not em_sectors: return merge_sectors_to_themes([_apply_empty_overlay(sector) for sector in sectors], limit=len(sectors)) em_name_map = {item["sector_name"]: item for item in em_sectors} matched = 0 for sector in sectors: em_data = em_name_map.get(sector.sector_name) if not em_data: for item in em_sectors: if _match_sector_name(item["sector_name"], sector.sector_name): em_data = item break if not em_data: _apply_empty_overlay(sector) continue matched += 1 sector.realtime_pct_change = float(em_data.get("pct_change", 0) or 0) sector.realtime_limit_up_count = None sector.realtime_amount = round(float(em_data.get("amount", 0) or 0) / 10000, 2) sector.realtime_turnover_rate = float(em_data.get("turnover_rate", 0) or 0) sector.realtime_up_count = int(em_data.get("up_count", 0) or 0) sector.realtime_down_count = int(em_data.get("down_count", 0) or 0) sector.leading_stocks_realtime = [] if em_data.get("leading_stock_name"): sector.leading_stocks_realtime = [{ "ts_code": em_data.get("leading_stock_code", ""), "name": em_data.get("leading_stock_name", ""), "pct_chg": float(em_data.get("leading_stock_pct", 0) or 0), "amount": 0, }] sector.is_realtime = True sector.data_mode = "realtime_overlay" sector.source = em_data.get("source", "eastmoney") logger.info("板块实时覆盖: %s/%s 匹配成功", matched, len(sectors)) sectors.sort(key=lambda s: (s.realtime_pct_change if s.realtime_pct_change is not None else s.pct_change), reverse=True) return merge_sectors_to_themes(sectors, limit=len(sectors))