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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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