from __future__ import annotations from datetime import date, datetime, timedelta from typing import Any from backend.bootstrap.config import normalize_date, tushare_code, validate_text from backend.data.providers.tushare_client import TushareError from backend.features.review.agent import ReviewAssistantError, stream_review_assistant class ReviewServiceMixin: def trade_entries( self, start_date: str = "", end_date: str = "", code: str = "" ) -> dict[str, Any]: return self.trade_journal.list_entries( self.current_user_id, start_date, end_date, code ) def review_watchlist(self, trade_date: str) -> dict[str, Any]: normalized_date = normalize_date(trade_date) items = self.database.list_watchlist(self.current_user_id) if not items: return {"items": [], "trade_date": normalized_date} resolved_date = normalized_date if self.configured: try: client = self._tushare_client() resolved_date, _ = client.resolve_trade_context(normalized_date) history = self.database.watchlist_price_history( [str(item["code"]) for item in items], resolved_date ) missing_codes = [ str(item["code"]) for item in items if len(history.get(str(item["code"])) or []) < 6 ] start_date = ( datetime.strptime(resolved_date, "%Y%m%d") - timedelta(days=24) ).strftime("%Y%m%d") for code in missing_codes: rows = client.query( "daily", { "ts_code": tushare_code(code), "start_date": start_date, "end_date": resolved_date, }, "ts_code,trade_date,open,high,low,close,pct_chg,vol,amount", ) if rows: self.database.upsert_daily_bars(rows) if missing_codes: history = self.database.watchlist_price_history( [str(item["code"]) for item in items], resolved_date ) except (TushareError, ValueError): history = self.database.watchlist_price_history( [str(item["code"]) for item in items], resolved_date ) else: history = self.database.watchlist_price_history( [str(item["code"]) for item in items], resolved_date ) auction_scores: dict[str, Any] = {} try: auction = self.auction_center(normalized_date, False) auction_scores = { str(row.get("code") or ""): row.get("attention_score") for row in (auction.get("watchlist_rows") or []) if row.get("available", True) } except (TushareError, ValueError): pass enriched = [] for item in items: code = str(item.get("code") or "") bars = history.get(code) or [] latest = bars[-1] if bars else {} close = float(latest.get("close") or 0) base_close = float(bars[-6].get("close") or 0) if len(bars) >= 6 else 0 enriched.append( { **item, "change": ( round(float(latest.get("pct_chg") or 0), 2) if latest else None ), "return_5d": ( round((close / base_close - 1) * 100, 2) if close > 0 and base_close > 0 else None ), "attention_score": auction_scores.get(code), "market_date": str(latest.get("trade_date") or ""), } ) return {"items": enriched, "trade_date": resolved_date} def save_trade_entry(self, payload: dict[str, Any]) -> dict[str, Any]: trade_id = self.trade_journal.save(self.current_user_id, payload) return {"id": trade_id, **self.trade_entries()} def delete_trade_entry(self, trade_id: int) -> dict[str, Any]: deleted = self.trade_journal.delete(self.current_user_id, trade_id) return {"deleted": deleted, **self.trade_entries()} def assistant_messages(self) -> list[dict[str, Any]]: return self.database.list_assistant_messages(self.current_user_id) def clear_assistant_messages(self) -> int: return self.database.delete_assistant_messages(self.current_user_id) def assistant_stream(self, payload: dict[str, Any]): question = validate_text(payload.get("question"), "问题", 2000, required=True) trade_date = normalize_date( str(payload.get("trade_date") or date.today().isoformat()) ) context = self._assistant_context(trade_date) history = [ {"role": item["role"], "content": str(item["content"])[:4000]} for item in self.assistant_messages()[-12:] if item.get("role") in {"user", "assistant"} ] def generate(): answer_parts: list[str] = [] events = self.llm_gateway.stream( "assistant", "review-assistant-v1", lambda profile: stream_review_assistant( context, question, history, profile.api_key, profile.base_url, profile.model, ), (ReviewAssistantError,), ) for event in events: if event.kind == "delta": chunk = str(event.value or "") answer_parts.append(chunk) yield chunk elif event.kind == "complete": self.database.save_assistant_exchange( self.current_user_id, question, "".join(answer_parts).strip(), trade_date, ) return generate() def _assistant_context(self, trade_date: str) -> dict[str, Any]: dashboard = self.get_dashboard(trade_date) actual_date = normalize_date( str((dashboard.get("meta") or {}).get("trade_date") or trade_date) ) sentiment = self.sentiment_history(actual_date, 10) tracking = self.strategy_tracking.list_tracking(self.current_user_id, 5) alerts = self.alert_service.list_alerts( self.current_user_id, "all", date.today().isoformat() ) trades = self.trade_journal.list_entries( self.current_user_id, end_date=actual_date ) return { "data_date": actual_date, "market": { "overview": dashboard.get("overview") or {}, "top_sectors": (dashboard.get("sectors") or [])[:8], "limit_performance": dashboard.get("limit_performance") or {}, "sentiment_history": (sentiment.get("rows") or [])[-10:], }, "personal": { "watchlist": self.database.list_watchlist(self.current_user_id)[:30], "review_notes": self.database.list_notes( self.current_user_id, scope="daily" )[:10], "strategy_tracking": { "summary": tracking.get("summary") or {}, "batches": (tracking.get("batches") or [])[:5], }, "alerts": (alerts.get("items") or [])[:20], "trade_summary": trades.get("summary") or {}, "trade_entries": (trades.get("items") or [])[:30], }, }