rebuild(stage-10): deliver mentor and unified llm streaming

This commit is contained in:
leefer
2026-07-30 05:52:12 +08:00
parent 532f0cfc11
commit f1fa104641
62 changed files with 6880 additions and 7 deletions
+261
View File
@@ -0,0 +1,261 @@
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"(?<!\d)\d{6}(?!\d)", question))
result = []
for identifier, row in directory.items():
code = str(row.get("symbol") or identifier.split(".")[0])
name = str(row.get("name") or "")
if code in codes or (len(name) >= 2 and name in question):
result.append(identifier)
if len(result) == 2:
break
return result