"""交易日历与关键事件提醒""" import datetime as dt from typing import List, Optional, Dict from sqlalchemy import select, and_ from db import get_session from models import Trade, Security, CorporateEvent, AlertEvent import akshare_service as svc try: import akshare as ak AK_OK = True except Exception: ak = None AK_OK = False def get_upcoming_dividends(days_ahead: int = 30) -> Dict: """获取即将到来的除权除息日""" today = dt.date.today() end = today + dt.timedelta(days=days_ahead) # 获取持仓股票代码 with get_session() as s: trades = s.execute(select(Trade).order_by(Trade.date)).scalars().all() # 计算当前持仓 pos = {} for t in trades: if t.code not in pos: pos[t.code] = {"qty": 0, "name": t.name} if t.side == "buy": pos[t.code]["qty"] += t.qty else: pos[t.code]["qty"] = max(0, pos[t.code]["qty"] - t.qty) holding_codes = [c for c, v in pos.items() if v["qty"] > 0] events = [] if AK_OK and holding_codes: try: df = ak.stock_zh_a_dividend() if df is not None and not df.empty: for _, r in df.iterrows(): code = str(r.get("代码", "")) if code not in holding_codes: continue ex_date_str = str(r.get("除权除息日", "")) if not ex_date_str or ex_date_str == "nan": continue try: ex_date = dt.date.fromisoformat(ex_date_str[:10]) if today <= ex_date <= end: days_left = (ex_date - today).days events.append({ "code": code, "name": pos[code]["name"], "event_type": "除权除息", "event_date": ex_date.isoformat(), "days_left": days_left, "detail": f"送股: {r.get('送股', 0)}, 转增: {r.get('转增', 0)}, 派息: {r.get('派息', 0)}", "is_holding": True, "urgency": "high" if days_left <= 3 else "medium" }) except Exception: continue except Exception: pass # 补充从数据库中获取的事件 with get_session() as s: db_events = s.execute( select(CorporateEvent).where( and_( CorporateEvent.event_type == "dividend", CorporateEvent.event_date >= today, CorporateEvent.event_date <= end ) ).order_by(CorporateEvent.event_date) ).scalars().all() for e in db_events: if any(ev["code"] == e.code for ev in events): continue days_left = (e.event_date - today).days events.append({ "code": e.code, "name": e.name, "event_type": "除权除息", "event_date": e.event_date.isoformat(), "days_left": days_left, "detail": e.description, "is_holding": e.code in holding_codes, "urgency": "high" if days_left <= 3 else "medium" }) events.sort(key=lambda x: x["event_date"]) return {"ok": True, "events": events, "count": len(events)} def get_unlock_calendar(days_ahead: int = 90) -> Dict: """获取限售解禁日历""" today = dt.date.today() end = today + dt.timedelta(days=days_ahead) events = [] if AK_OK: try: df = ak.stock_restricted_release_summary_em() if df is not None and not df.empty: for _, r in df.head(50).iterrows(): date_str = str(r.get("解禁日期", "")) if not date_str or date_str == "nan": continue try: unlock_date = dt.date.fromisoformat(date_str[:10]) if today <= unlock_date <= end: amount = float(r.get("解禁数量", 0) or 0) market_val = float(r.get("解禁市值", 0) or 0) events.append({ "code": str(r.get("代码", "")), "name": str(r.get("名称", "")), "event_type": "限售解禁", "event_date": unlock_date.isoformat(), "days_left": (unlock_date - today).days, "detail": f"解禁市值: {round(market_val/1e8, 2)}亿", "amount_billion": round(market_val / 1e8, 2), "urgency": "high" if market_val >= 10e8 else "medium" }) except Exception: continue except Exception: pass # 从数据库补充 with get_session() as s: db_events = s.execute( select(CorporateEvent).where( and_( CorporateEvent.event_type == "unlock", CorporateEvent.event_date >= today, CorporateEvent.event_date <= end ) ).order_by(CorporateEvent.event_date) ).scalars().all() for e in db_events: if any(ev["code"] == e.code for ev in events): continue events.append({ "code": e.code, "name": e.name, "event_type": "限售解禁", "event_date": e.event_date.isoformat(), "days_left": (e.event_date - today).days, "detail": e.description, "amount_billion": e.amount, "urgency": "high" if e.amount >= 10 else "medium" }) events.sort(key=lambda x: x["event_date"]) return {"ok": True, "events": events, "count": len(events)} def get_earnings_calendar(days_ahead: int = 30, holding_only: bool = False) -> Dict: """获取财报披露日历""" today = dt.date.today() end = today + dt.timedelta(days=days_ahead) # 获取持仓代码 holding_codes = set() if holding_only: with get_session() as s: trades = s.execute(select(Trade).order_by(Trade.date)).scalars().all() pos = {} for t in trades: if t.code not in pos: pos[t.code] = 0 pos[t.code] += t.qty if t.side == "buy" else -t.qty holding_codes = {c for c, q in pos.items() if q > 0} events = [] if AK_OK: try: df = ak.stock_notice_report() if df is not None and not df.empty: for _, r in df.head(100).iterrows(): date_str = str(r.get("公告日期", "")) code = str(r.get("代码", "")) if not date_str or date_str == "nan": continue if holding_only and code not in holding_codes: continue try: report_date = dt.date.fromisoformat(date_str[:10]) if today <= report_date <= end: events.append({ "code": code, "name": str(r.get("名称", "")), "event_type": "财报披露", "event_date": report_date.isoformat(), "days_left": (report_date - today).days, "report_type": str(r.get("公告类型", "")), "is_holding": code in holding_codes, "urgency": "high" if code in holding_codes else "low" }) except Exception: continue except Exception: pass # 从数据库补充 with get_session() as s: db_events = s.execute( select(CorporateEvent).where( and_( CorporateEvent.event_type == "earnings", CorporateEvent.event_date >= today, CorporateEvent.event_date <= end ) ).order_by(CorporateEvent.event_date) ).scalars().all() for e in db_events: if any(ev["code"] == e.code for ev in events): continue if holding_only and e.code not in holding_codes: continue events.append({ "code": e.code, "name": e.name, "event_type": "财报披露", "event_date": e.event_date.isoformat(), "days_left": (e.event_date - today).days, "report_type": e.title, "is_holding": e.code in holding_codes, "urgency": "high" if e.code in holding_codes else "low" }) events.sort(key=lambda x: x["event_date"]) return {"ok": True, "events": events, "count": len(events)} def get_all_upcoming_events(days_ahead: int = 30) -> Dict: """获取所有即将到来的关键事件(综合视图)""" today = dt.date.today() all_events = [] # 合并所有事件 for result in [ get_upcoming_dividends(days_ahead), get_earnings_calendar(days_ahead), get_unlock_calendar(days_ahead) ]: all_events.extend(result.get("events", [])) # 按日期排序 all_events.sort(key=lambda x: x["event_date"]) # 按日期分组 grouped = {} for event in all_events: date = event["event_date"] if date not in grouped: grouped[date] = [] grouped[date].append(event) # 生成日历视图 calendar = [] for date_str, events in sorted(grouped.items()): date = dt.date.fromisoformat(date_str) calendar.append({ "date": date_str, "weekday": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][date.weekday()], "days_left": (date - today).days, "events": events, "has_high_urgency": any(e["urgency"] == "high" for e in events), "has_holding": any(e.get("is_holding", False) for e in events) }) # 紧急事件(3天内) urgent = [e for e in all_events if e.get("days_left", 99) <= 3] return { "ok": True, "calendar": calendar, "urgent": urgent, "total": len(all_events), "summary": { "dividends": len([e for e in all_events if e["event_type"] == "除权除息"]), "earnings": len([e for e in all_events if e["event_type"] == "财报披露"]), "unlocks": len([e for e in all_events if e["event_type"] == "限售解禁"]), "urgent": len(urgent) } } def check_and_push_calendar_alerts() -> Dict: """检查并推送日历事件预警(定时任务调用)""" try: from notifier import notify except Exception: return {"ok": False, "msg": "推送模块不可用"} result = get_all_upcoming_events(days_ahead=7) urgent = result.get("urgent", []) if not urgent: return {"ok": True, "msg": "无紧急事件", "pushed": 0} # 生成推送内容 lines = [f"📅 未来7天关键事件提醒({len(urgent)}条)\n"] for event in urgent[:10]: # 最多推送10条 urgency_icon = "🔴" if event["urgency"] == "high" else "🟡" holding_icon = "💰" if event.get("is_holding") else "" lines.append( f"{urgency_icon}{holding_icon} {event['event_date']} " f"{event['name']}({event['code']}) " f"{event['event_type']} " f"({event['days_left']}天后)" ) message = "\n".join(lines) notify("【Blackdata】关键事件提醒", message) # 写入站内通知 with get_session() as s: for event in urgent[:10]: alert = AlertEvent( rule_id=0, code=event["code"], name=event["name"], message=f"{event['event_type']}: {event.get('detail', '')} ({event['days_left']}天后)", value=event.get("amount_billion", 0) ) s.add(alert) s.commit() return {"ok": True, "pushed": len(urgent), "msg": f"已推送 {len(urgent)} 条事件提醒"}