""" 复盘计划接口 - 提供复盘与交易计划相关数据 (V1 + V2) """ import logging from datetime import datetime from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from app.database import get_db from app.analysis_db import get_analysis_db from app.analysis_models import ( ReviewDate, SymbolRanking, TradingPlan, ReviewPlanV2, SymbolScoreV2, TradingPlanV2, SectorHeat, ) from app.services.plan_generator import generate_plan, save_plan_to_db, get_plan_data logger = logging.getLogger(__name__) router = APIRouter(prefix="/review", tags=["复盘计划"]) # ==================== 请求体模型 ==================== class GeneratePlanRequest(BaseModel): review_date: str # ==================== V1 复盘日期管理 ==================== @router.get("/dates") def get_review_dates(limit: int = 10, db: Session = Depends(get_analysis_db)): """获取复盘日期列表""" dates = db.query(ReviewDate).order_by( ReviewDate.review_date.desc() ).limit(limit).all() return { "success": True, "data": [{ "id": d.id, "review_date": d.review_date, "week_day": d.week_day } for d in dates] } @router.post("/dates") def create_review_date( review_date: str, week_day: Optional[str] = None, db: Session = Depends(get_analysis_db) ): """创建复盘日期""" existing = db.query(ReviewDate).filter( ReviewDate.review_date == review_date ).first() if existing: return { "success": False, "message": f"日期 {review_date} 已存在" } if not week_day: try: dt = datetime.strptime(review_date, "%Y-%m-%d") week_days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] week_day = week_days[dt.weekday()] except: week_day = "" new_date = ReviewDate( review_date=review_date, week_day=week_day ) db.add(new_date) db.commit() db.refresh(new_date) return { "success": True, "data": { "id": new_date.id, "review_date": new_date.review_date, "week_day": new_date.week_day } } # ==================== V1 品种排名 ==================== @router.get("/rankings/{review_date_id}") def get_rankings(review_date_id: int, db: Session = Depends(get_analysis_db)): """获取指定日期的品种排名""" review_date = db.query(ReviewDate).filter( ReviewDate.id == review_date_id ).first() if not review_date: raise HTTPException(status_code=404, detail="复盘日期不存在") rankings = db.query(SymbolRanking).filter( SymbolRanking.review_date_id == review_date_id ).order_by( SymbolRanking.rank_type, SymbolRanking.rank ).all() rank_types = ["volume", "amplitude", "change", "open_interest"] rank_type_names = { "volume": "成交量排名", "amplitude": "振幅排名", "change": "涨跌幅排名", "open_interest": "持仓量排名" } result = {} for rt in rank_types: result[rt] = { "title": rank_type_names.get(rt, rt), "items": [] } for r in rankings: if r.rank_type in result: result[r.rank_type]["items"].append({ "id": r.id, "symbol": r.symbol, "name": r.name or r.symbol, "rank": r.rank, "value": r.value, "price": r.price, "change_pct": r.change_pct }) return { "success": True, "data": { "review_date": review_date.review_date, "week_day": review_date.week_day, "rankings": result } } @router.post("/rankings") def create_or_update_rankings( review_date_id: int, rankings: List[dict], db: Session = Depends(get_analysis_db) ): """创建或更新品种排名""" review_date = db.query(ReviewDate).filter( ReviewDate.id == review_date_id ).first() if not review_date: raise HTTPException(status_code=404, detail="复盘日期不存在") db.query(SymbolRanking).filter( SymbolRanking.review_date_id == review_date_id ).delete() created = [] for r in rankings: new_ranking = SymbolRanking( review_date_id=review_date_id, symbol=r.get("symbol"), name=r.get("name"), rank_type=r.get("rank_type"), rank=r.get("rank", 1), value=r.get("value"), price=r.get("price"), change_pct=r.get("change_pct") ) db.add(new_ranking) created.append({ "id": new_ranking.id, "symbol": new_ranking.symbol, "rank_type": new_ranking.rank_type, "rank": new_ranking.rank }) db.commit() return { "success": True, "data": created } # ==================== V1 交易计划 ==================== @router.get("/plans/{review_date_id}") def get_trading_plans(review_date_id: int, db: Session = Depends(get_analysis_db)): """获取指定日期的交易计划""" review_date = db.query(ReviewDate).filter( ReviewDate.id == review_date_id ).first() if not review_date: raise HTTPException(status_code=404, detail="复盘日期不存在") plans = db.query(TradingPlan).filter( TradingPlan.review_date_id == review_date_id ).order_by( TradingPlan.score.desc() ).all() return { "success": True, "data": { "review_date": review_date.review_date, "week_day": review_date.week_day, "plans": [{ "id": p.id, "symbol": p.symbol, "name": p.name or p.symbol, "plan_type": p.plan_type, "score": p.score, "logic": p.logic, "reason": p.reason, "entry_price": p.entry_price, "stop_loss": p.stop_loss, "take_profit": p.take_profit, "confidence": p.confidence, "position_suggestion": p.position_suggestion, "created_at": p.created_at.isoformat() if p.created_at else None } for p in plans] } } @router.post("/plans") def create_or_update_plans( review_date_id: int, plans: List[dict], db: Session = Depends(get_analysis_db) ): """创建或更新交易计划""" review_date = db.query(ReviewDate).filter( ReviewDate.id == review_date_id ).first() if not review_date: raise HTTPException(status_code=404, detail="复盘日期不存在") db.query(TradingPlan).filter( TradingPlan.review_date_id == review_date_id ).delete() created = [] for p in plans: new_plan = TradingPlan( review_date_id=review_date_id, symbol=p.get("symbol"), name=p.get("name"), plan_type=p.get("plan_type"), score=p.get("score", 0), logic=p.get("logic"), reason=p.get("reason"), entry_price=p.get("entry_price"), stop_loss=p.get("stop_loss"), take_profit=p.get("take_profit"), confidence=p.get("confidence"), position_suggestion=p.get("position_suggestion") ) db.add(new_plan) created.append({ "id": new_plan.id, "symbol": new_plan.symbol, "plan_type": new_plan.plan_type, "score": new_plan.score }) db.commit() return { "success": True, "data": created } @router.post("/plans/batch") def batch_create_review_data( review_data: dict, db: Session = Depends(get_analysis_db) ): """批量创建复盘数据(日期+排名+计划)""" review_date_str = review_data.get("review_date") week_day = review_data.get("week_day") existing = db.query(ReviewDate).filter( ReviewDate.review_date == review_date_str ).first() if existing: review_date_id = existing.id else: if not week_day: try: dt = datetime.strptime(review_date_str, "%Y-%m-%d") week_days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] week_day = week_days[dt.weekday()] except: week_day = "" new_date = ReviewDate( review_date=review_date_str, week_day=week_day ) db.add(new_date) db.commit() db.refresh(new_date) review_date_id = new_date.id rankings = review_data.get("rankings", []) if rankings: db.query(SymbolRanking).filter( SymbolRanking.review_date_id == review_date_id ).delete() for r in rankings: new_ranking = SymbolRanking( review_date_id=review_date_id, symbol=r.get("symbol"), name=r.get("name"), rank_type=r.get("rank_type"), rank=r.get("rank", 1), value=r.get("value"), price=r.get("price"), change_pct=r.get("change_pct") ) db.add(new_ranking) plans = review_data.get("plans", []) if plans: db.query(TradingPlan).filter( TradingPlan.review_date_id == review_date_id ).delete() for p in plans: new_plan = TradingPlan( review_date_id=review_date_id, symbol=p.get("symbol"), name=p.get("name"), plan_type=p.get("plan_type"), score=p.get("score", 0), logic=p.get("logic"), reason=p.get("reason"), entry_price=p.get("entry_price"), stop_loss=p.get("stop_loss"), take_profit=p.get("take_profit"), confidence=p.get("confidence"), position_suggestion=p.get("position_suggestion") ) db.add(new_plan) db.commit() return { "success": True, "data": { "review_date_id": review_date_id, "review_date": review_date_str, "rankings_count": len(rankings), "plans_count": len(plans) } } @router.get("/summary/{review_date_id}") def get_review_summary(review_date_id: int, db: Session = Depends(get_analysis_db)): """获取复盘摘要(包含排名和计划的完整数据)""" review_date = db.query(ReviewDate).filter( ReviewDate.id == review_date_id ).first() if not review_date: raise HTTPException(status_code=404, detail="复盘日期不存在") rankings = db.query(SymbolRanking).filter( SymbolRanking.review_date_id == review_date_id ).order_by( SymbolRanking.rank_type, SymbolRanking.rank ).all() plans = db.query(TradingPlan).filter( TradingPlan.review_date_id == review_date_id ).order_by( TradingPlan.score.desc() ).all() rank_types = ["volume", "amplitude", "change", "open_interest"] rank_type_names = { "volume": "成交量排名 Top 5", "amplitude": "振幅排名 Top 5", "change": "涨跌幅排名 Top 5", "open_interest": "持仓量排名 Top 5" } rankings_result = {} for rt in rank_types: rankings_result[rt] = { "title": rank_type_names.get(rt, rt), "items": [] } for r in rankings: if r.rank_type in rankings_result: rankings_result[r.rank_type]["items"].append({ "symbol": r.symbol, "name": r.name or r.symbol, "rank": r.rank, "value": r.value, "price": r.price, "change_pct": r.change_pct }) plans_result = [{ "id": p.id, "symbol": p.symbol, "name": p.name or p.symbol, "plan_type": p.plan_type, "score": p.score, "logic": p.logic, "reason": p.reason, "entry_price": p.entry_price, "stop_loss": p.stop_loss, "take_profit": p.take_profit, "confidence": p.confidence, "position_suggestion": p.position_suggestion, "created_at": p.created_at.isoformat() if p.created_at else None } for p in plans] return { "success": True, "data": { "review_date": review_date.review_date, "week_day": review_date.week_day, "rankings": rankings_result, "plans": plans_result } } @router.delete("/clear") def clear_review_data(db: Session = Depends(get_analysis_db)): """清除所有V1复盘与交易计划数据""" try: db.query(TradingPlan).delete() db.query(SymbolRanking).delete() db.query(ReviewDate).delete() db.commit() return { "success": True, "message": "复盘数据已清除" } except Exception as e: db.rollback() return { "success": False, "message": f"清除失败: {str(e)}" } # ==================== V2 生成交易计划 ==================== @router.post("/generate") def generate_review_plan( req: GeneratePlanRequest, buffer_db: Session = Depends(get_db), analysis_db: Session = Depends(get_analysis_db) ): """生成V2交易计划""" try: review_date_str = req.review_date dt = datetime.strptime(review_date_str, "%Y-%m-%d") week_days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] week_day = week_days[dt.weekday()] # buffer_db 用于获取K线数据,analysis_db 用于保存计划 plan_data = generate_plan(buffer_db, review_date_str, week_day) plan_id = save_plan_to_db(analysis_db, plan_data) return { "success": True, "data": { "plan_id": plan_id, "summary": { "review_date": plan_data["review_date"], "week_day": plan_data["week_day"], "core_conclusion": plan_data.get("core_conclusion"), "bull_count": plan_data.get("bull_count"), "bear_count": plan_data.get("bear_count"), "neutral_count": plan_data.get("neutral_count"), "opportunity_count": plan_data.get("opportunity_count"), "symbol_count": plan_data.get("symbol_count"), } } } except Exception as e: logger.exception("生成交易计划失败") return { "success": False, "message": f"生成失败: {str(e)}" } # ==================== V2 查询接口 ==================== @router.get("/v2/plans") def list_v2_plans( limit: int = 20, db: Session = Depends(get_analysis_db) ): """获取V2复盘计划列表""" try: plans = db.query(ReviewPlanV2).order_by( ReviewPlanV2.review_date.desc() ).limit(limit).all() return { "success": True, "data": [{ "id": p.id, "review_date": p.review_date, "week_day": p.week_day, "core_conclusion": p.core_conclusion, "opportunity_count": p.opportunity_count, "created_at": p.created_at.isoformat() if p.created_at else None, } for p in plans] } except Exception as e: logger.exception("获取V2计划列表失败") return { "success": False, "message": f"查询失败: {str(e)}" } @router.get("/v2/plan/{plan_id}") def get_v2_plan(plan_id: int, db: Session = Depends(get_analysis_db)): """根据ID获取完整V2计划数据""" try: result = get_plan_data(db, plan_id) if not result: return { "success": False, "message": f"计划 {plan_id} 不存在" } return { "success": True, "data": result } except Exception as e: logger.exception(f"获取V2计划 {plan_id} 失败") return { "success": False, "message": f"查询失败: {str(e)}" } @router.get("/v2/plan/by-date/{date}") def get_v2_plan_by_date(date: str, db: Session = Depends(get_analysis_db)): """根据日期获取V2计划数据""" try: plan_meta = db.query(ReviewPlanV2).filter( ReviewPlanV2.review_date == date ).first() if not plan_meta: return { "success": False, "message": f"日期 {date} 的计划不存在" } result = get_plan_data(db, plan_meta.id) return { "success": True, "data": result } except Exception as e: logger.exception(f"获取V2计划 by-date {date} 失败") return { "success": False, "message": f"查询失败: {str(e)}" } @router.delete("/v2/clear") def clear_v2_data(db: Session = Depends(get_analysis_db)): """清除所有V2数据""" try: db.query(SectorHeat).delete() db.query(TradingPlanV2).delete() db.query(SymbolScoreV2).delete() db.query(ReviewPlanV2).delete() db.commit() return { "success": True, "message": "V2复盘数据已清除" } except Exception as e: db.rollback() return { "success": False, "message": f"清除失败: {str(e)}" }