96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import secrets
|
|
from datetime import date, datetime
|
|
from typing import Any
|
|
|
|
from backend.bootstrap.config import validate_text
|
|
from backend.database.repositories import AlertRepository
|
|
|
|
|
|
class AlertService:
|
|
def __init__(self, repository: AlertRepository) -> None:
|
|
self.repository = repository
|
|
|
|
def create_manual(self, user_id: int, payload: dict[str, Any]) -> int:
|
|
title = validate_text(payload.get("title"), "提醒标题", 80, required=True)
|
|
content = validate_text(payload.get("content"), "提醒内容", 500)
|
|
code = validate_text(payload.get("code"), "股票代码", 12)
|
|
available_date = self.calendar_date(
|
|
str(payload.get("remind_date") or date.today().isoformat())
|
|
)
|
|
return self.repository.save_alert(
|
|
user_id=user_id,
|
|
kind="manual",
|
|
title=title,
|
|
content=content,
|
|
available_date=available_date,
|
|
code=code,
|
|
dedupe_key=f"manual:{secrets.token_hex(12)}",
|
|
)
|
|
|
|
def sync_strategy_tracking(self, user_id: int, tracking: dict[str, Any]) -> int:
|
|
synced = 0
|
|
today = date.today().strftime("%Y%m%d")
|
|
for batch in tracking.get("batches") or []:
|
|
items = batch.get("items") or []
|
|
summary = batch.get("summary") or {}
|
|
if not items:
|
|
continue
|
|
run_id = int(batch.get("run_id") or 0)
|
|
strategy_name = str(batch.get("strategy_name") or "选股策略")
|
|
observed = int(summary.get("observed") or 0)
|
|
completed = int(summary.get("completed") or 0)
|
|
if observed:
|
|
win_rate = summary.get("t1_win_rate")
|
|
suffix = f",当前红盘率 {win_rate:.1f}%" if win_rate is not None else ""
|
|
self.repository.save_alert(
|
|
user_id, "strategy_t1", f"{strategy_name} 已有 T+1 反馈",
|
|
f"{observed}/{len(items)} 只标的已有首日表现{suffix}。",
|
|
today, "", f"strategy:{run_id}:t1",
|
|
)
|
|
synced += 1
|
|
if completed == len(items):
|
|
average = summary.get("average_t5")
|
|
suffix = f",平均收益 {average:+.2f}%" if average is not None else ""
|
|
self.repository.save_alert(
|
|
user_id, "strategy_t5", f"{strategy_name} 五日跟踪完成",
|
|
f"本批 {len(items)} 只标的已完成 T+5 跟踪{suffix}。",
|
|
today, "", f"strategy:{run_id}:t5",
|
|
)
|
|
synced += 1
|
|
return synced
|
|
|
|
def list_alerts(
|
|
self, user_id: int, status: str = "all", as_of: str = ""
|
|
) -> dict[str, Any]:
|
|
if status not in {"all", "unread"}:
|
|
raise ValueError("提醒筛选不支持。")
|
|
compact_date = self.calendar_date(as_of or date.today().isoformat())
|
|
items = self.repository.list_alerts(user_id, compact_date, status == "unread")
|
|
for item in items:
|
|
item["due"] = str(item.get("available_date") or "") <= compact_date
|
|
return {
|
|
"items": items,
|
|
"unread_count": self.repository.count_unread_alerts(user_id, compact_date),
|
|
"as_of": compact_date,
|
|
}
|
|
|
|
def mark_read(self, user_id: int, alert_id: int) -> bool:
|
|
return self.repository.mark_alert_read(user_id, alert_id)
|
|
|
|
def mark_all_read(self, user_id: int, as_of: str) -> int:
|
|
return self.repository.mark_all_alerts_read(user_id, as_of)
|
|
|
|
def delete(self, user_id: int, alert_id: int) -> bool:
|
|
return self.repository.delete_alert(user_id, alert_id)
|
|
|
|
@staticmethod
|
|
def calendar_date(value: str) -> str:
|
|
compact = value.replace("-", "").strip()
|
|
try:
|
|
parsed = datetime.strptime(compact, "%Y%m%d")
|
|
except ValueError as exc:
|
|
raise ValueError("提醒日期格式应为 YYYY-MM-DD。") from exc
|
|
return parsed.strftime("%Y%m%d")
|