189 lines
7.8 KiB
Python
189 lines
7.8 KiB
Python
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],
|
|
},
|
|
}
|