from __future__ import annotations import json import re from typing import Any from backend.data.gateway import DataGateway from backend.data.repository import MarketRepository from backend.database.connection import Database from backend.features.mentor.skills import MentorSkill from backend.features.screener.repository import ScreenerRepository INDEX_UNIVERSE = ( ("index", "000001.SH", "上证指数"), ("index", "399001.SZ", "深证成指"), ("index", "399006.SZ", "创业板指"), ("index", "000016.SH", "上证50"), ("index", "000300.SH", "沪深300"), ("index", "000905.SH", "中证500"), ("index", "000852.SH", "中证1000"), ("index", "932000.CSI", "中证2000"), ) ETF_UNIVERSE = ( ("stock", "510050.SH", "上证50ETF"), ("stock", "510300.SH", "沪深300ETF"), ("stock", "510500.SH", "中证500ETF"), ("stock", "512100.SH", "中证1000ETF"), ) class MentorContextBuilder: def __init__( self, database: Database, market_repository: MarketRepository, screener_repository: ScreenerRepository, gateway: DataGateway, ) -> None: self._database = database self._market = market_repository self._screener = screener_repository self._gateway = gateway def build(self, requested_date: str, question: str, skill: MentorSkill) -> dict[str, Any]: context = self._gateway.trade_context(requested_date) trade_date = context.actual_date or requested_date with self._database.read() as connection: summary_row = self._market.latest_summary(connection, trade_date) summary = _payload(summary_row) history = [ _summary_item(row) for row in self._market.summaries(connection, trade_date, 10) ] popularity = _payload( self._market.latest_insight_snapshot(connection, "popularity", trade_date) ) dragon = _payload( self._market.latest_insight_snapshot(connection, "dragon-list", trade_date) ) matched = self._matched_stocks(connection, trade_date, question) indexes = self._market_matrix(connection, INDEX_UNIVERSE) etfs = self._market_matrix(connection, ETF_UNIVERSE) result: dict[str, Any] = { "data_trade_date": str(summary_row["trade_date"]) if summary_row else trade_date, "data_profile": skill.profile, "overview": summary.get("overview") or {}, "sentiment": summary.get("sentiment") or {}, "recent_market_history": history, "question_matched_stocks": matched, } self._apply_profile(result, skill.profile, summary, popularity, indexes, etfs) if _requires_dragon_context(question): result["dragon_list"] = _dragon_context(dragon, matched) return result def _matched_stocks( self, connection, trade_date: str, question: str ) -> list[dict[str, Any]]: directory = self._gateway.stock_directory() identifiers = _matched_identifiers(directory, question) snapshot = self._screener.latest_factor_snapshot(connection, trade_date) factor_rows = ( { row["identifier"]: row for row in self._screener.factor_rows(connection, int(snapshot["id"])) } if snapshot else {} ) result = [] for identifier in identifiers[:2]: row = dict(factor_rows.get(identifier) or directory.get(identifier) or {}) chart = self._market.chart(connection, "stock", identifier, "day") row["recent_prices"] = (_payload(chart).get("points") or [])[-20:] result.append(row) return result def _market_matrix( self, connection, universe: tuple[tuple[str, str, str], ...] ) -> list[dict[str, Any]]: result = [] for entity_type, identifier, name in universe: row = self._market.chart(connection, entity_type, identifier, "day") points = _payload(row).get("points") or [] closes = [float(item["close"]) for item in points if item.get("close")] result.append( { "code": identifier, "name": name, "available": bool(closes), "close": closes[-1] if closes else None, "change": _return(closes, 1), "return_5d": _return(closes, 5), "return_10d": _return(closes, 10), "return_20d": _return(closes, 20), "amount": points[-1].get("amount") if points else None, } ) return result @staticmethod def _apply_profile( result: dict[str, Any], profile: str, summary: dict[str, Any], popularity: dict[str, Any], indexes: list[dict[str, Any]], etfs: list[dict[str, Any]], ) -> None: limits = list(summary.get("limits") or []) broken = list(summary.get("broken") or []) yesterday = list(summary.get("yesterday_limits") or []) rotation = list(summary.get("sector_rotation") or []) ordered = sorted( limits, key=lambda row: (int(row.get("streak") or 0), float(row.get("amount") or 0)), reverse=True, ) if profile == "first_board": result["first_board_environment"] = { "seal_rate": (summary.get("overview") or {}).get("seal_rate"), "first_boards": [row for row in ordered if int(row.get("streak") or 1) == 1][:35], "broken_stocks": broken[:30], "hot_sectors": rotation[:12], } elif profile == "leader": result.update( { "limit_ladder": summary.get("ladders") or [], "multi_board_leaders": [ row for row in ordered if int(row.get("streak") or 0) >= 2 ][:25], "sector_rotation": rotation[:12], "popularity_core": (popularity.get("combined") or [])[:10], } ) elif profile == "trend": result.update( { "index_momentum": indexes, "sector_rotation": rotation[:20], "market_breadth": summary.get("overview") or {}, } ) elif profile == "low_absorption": result.update( { "yesterday_limit_performance": yesterday[:35], "broken_stocks": broken[:20], "hot_sectors": rotation[:12], } ) elif profile == "macro": result.update( { "broad_indexes": indexes, "core_etfs": etfs, "market_style": { "overview": summary.get("overview") or {}, "top_sectors": rotation[:15], }, "unavailable_data": [ "政策原文与隔夜资讯尚未接入", "汇率、利率和商品宏观序列当前不可用", ], } ) else: result.update( { "limit_ladder": summary.get("ladders") or [], "limit_performance": summary.get("limit_performance") or [], "sector_rotation": rotation[:15], "limit_up_stocks": ordered[:30], "broken_stocks": broken[:20], "limit_down_stocks": (summary.get("down_limits") or [])[:20], "yesterday_limit_performance": yesterday[:20], } ) def _payload(row) -> dict[str, Any]: if row is None: return {} try: value = json.loads(str(row["payload_json"])) except (KeyError, TypeError, json.JSONDecodeError): return {} return value if isinstance(value, dict) else {} def _summary_item(row) -> dict[str, Any]: payload = _payload(row) return { "trade_date": str(row["trade_date"]), "overview": payload.get("overview") or {}, "sentiment": payload.get("sentiment") or {}, } def _return(closes: list[float], days: int) -> float | None: if len(closes) <= days or closes[-days - 1] <= 0: return None return round((closes[-1] / closes[-days - 1] - 1) * 100, 2) def _dragon_context(payload: dict[str, Any], matched: list[dict[str, Any]]) -> dict[str, Any]: codes = { str(item.get("code") or item.get("identifier") or "").split(".")[0] for item in matched } operations = payload.get("stocks") or payload.get("operations") or [] return { "matched": [ row for row in operations if str(row.get("ts_code") or row.get("code") or "").split(".")[0] in codes ][:20], "largest_net_flows": sorted( operations, key=lambda row: abs(float(row.get("net_buy") or row.get("net_million") or 0)), reverse=True, )[:12], } def _requires_dragon_context(question: str) -> bool: return any(word in question for word in ("龙虎榜", "席位", "机构", "游资")) def _matched_identifiers(directory: dict[str, dict[str, Any]], question: str) -> list[str]: codes = set(re.findall(r"(?= 2 and name in question): result.append(identifier) if len(result) == 2: break return result