You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/api/review_plan.py

616 lines
18 KiB

"""
复盘计划接口 - 提供复盘与交易计划相关数据 (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)}"
}