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.
807 lines
28 KiB
807 lines
28 KiB
"""复盘计划 API 路由 - V1 复盘日期/排名/计划 + V2 评分/交易计划/板块热度"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.models.analysis import (
|
|
ReviewDate,
|
|
ReviewPlanV2,
|
|
SectorHeat,
|
|
SymbolRanking,
|
|
SymbolScoreV2,
|
|
TradingPlan,
|
|
TradingPlanV2,
|
|
)
|
|
from app.services.plan_generator import PlanGenerator, load_symbols_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/review", tags=["复盘计划"])
|
|
|
|
|
|
class GeneratePlanRequest(BaseModel):
|
|
"""生成V2交易计划请求体。"""
|
|
|
|
review_date: str
|
|
|
|
|
|
# ==================== V1 复盘日期管理 ====================
|
|
|
|
|
|
@router.get("/dates")
|
|
async def get_review_dates(
|
|
limit: int = 10,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取复盘日期列表。"""
|
|
stmt = select(ReviewDate).order_by(ReviewDate.review_date.desc()).limit(limit)
|
|
result = await db.execute(stmt)
|
|
dates = result.scalars().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")
|
|
async def create_review_date(
|
|
review_date: str,
|
|
week_day: str | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建复盘日期。"""
|
|
stmt = select(ReviewDate).where(ReviewDate.review_date == review_date)
|
|
result = await db.execute(stmt)
|
|
existing = result.scalar_one_or_none()
|
|
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 ValueError:
|
|
week_day = ""
|
|
|
|
new_date = ReviewDate(review_date=review_date, week_day=week_day)
|
|
db.add(new_date)
|
|
await db.flush()
|
|
await 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}")
|
|
async def get_rankings(
|
|
review_date_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取指定日期的品种排名。"""
|
|
stmt_date = select(ReviewDate).where(ReviewDate.id == review_date_id)
|
|
result_date = await db.execute(stmt_date)
|
|
review_date = result_date.scalar_one_or_none()
|
|
if not review_date:
|
|
raise HTTPException(status_code=404, detail="复盘日期不存在")
|
|
|
|
stmt = (
|
|
select(SymbolRanking)
|
|
.where(SymbolRanking.review_date_id == review_date_id)
|
|
.order_by(SymbolRanking.rank_type, SymbolRanking.rank)
|
|
)
|
|
result = await db.execute(stmt)
|
|
rankings = result.scalars().all()
|
|
|
|
rank_types = ["volume", "amplitude", "change", "open_interest"]
|
|
rank_type_names = {
|
|
"volume": "成交量排名",
|
|
"amplitude": "振幅排名",
|
|
"change": "涨跌幅排名",
|
|
"open_interest": "持仓量排名",
|
|
}
|
|
result_data: dict = {rt: {"title": rank_type_names.get(rt, rt), "items": []} for rt in rank_types}
|
|
for r in rankings:
|
|
if r.rank_type in result_data:
|
|
result_data[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_data,
|
|
},
|
|
}
|
|
|
|
|
|
@router.post("/rankings")
|
|
async def create_or_update_rankings(
|
|
review_date_id: int,
|
|
rankings: list[dict],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建或更新品种排名。"""
|
|
stmt_date = select(ReviewDate).where(ReviewDate.id == review_date_id)
|
|
result_date = await db.execute(stmt_date)
|
|
review_date = result_date.scalar_one_or_none()
|
|
if not review_date:
|
|
raise HTTPException(status_code=404, detail="复盘日期不存在")
|
|
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(
|
|
sa_delete(SymbolRanking).where(SymbolRanking.review_date_id == review_date_id)
|
|
)
|
|
|
|
created: list[dict] = []
|
|
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)
|
|
await db.flush()
|
|
created.append({
|
|
"id": new_ranking.id,
|
|
"symbol": new_ranking.symbol,
|
|
"rank_type": new_ranking.rank_type,
|
|
"rank": new_ranking.rank,
|
|
})
|
|
return {"success": True, "data": created}
|
|
|
|
|
|
# ==================== V1 交易计划 ====================
|
|
|
|
|
|
@router.get("/plans/{review_date_id}")
|
|
async def get_trading_plans(
|
|
review_date_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取指定日期的交易计划。"""
|
|
stmt_date = select(ReviewDate).where(ReviewDate.id == review_date_id)
|
|
result_date = await db.execute(stmt_date)
|
|
review_date = result_date.scalar_one_or_none()
|
|
if not review_date:
|
|
raise HTTPException(status_code=404, detail="复盘日期不存在")
|
|
|
|
stmt = (
|
|
select(TradingPlan)
|
|
.where(TradingPlan.review_date_id == review_date_id)
|
|
.order_by(TradingPlan.score.desc())
|
|
)
|
|
result = await db.execute(stmt)
|
|
plans = result.scalars().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")
|
|
async def create_or_update_plans(
|
|
review_date_id: int,
|
|
plans: list[dict],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建或更新交易计划。"""
|
|
stmt_date = select(ReviewDate).where(ReviewDate.id == review_date_id)
|
|
result_date = await db.execute(stmt_date)
|
|
review_date = result_date.scalar_one_or_none()
|
|
if not review_date:
|
|
raise HTTPException(status_code=404, detail="复盘日期不存在")
|
|
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(
|
|
sa_delete(TradingPlan).where(TradingPlan.review_date_id == review_date_id)
|
|
)
|
|
|
|
created: list[dict] = []
|
|
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)
|
|
await db.flush()
|
|
created.append({
|
|
"id": new_plan.id,
|
|
"symbol": new_plan.symbol,
|
|
"plan_type": new_plan.plan_type,
|
|
"score": new_plan.score,
|
|
})
|
|
return {"success": True, "data": created}
|
|
|
|
|
|
@router.post("/plans/batch")
|
|
async def batch_create_review_data(
|
|
review_data: dict,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""批量创建复盘数据(日期+排名+计划)。"""
|
|
review_date_str = review_data.get("review_date")
|
|
week_day = review_data.get("week_day")
|
|
|
|
stmt = select(ReviewDate).where(ReviewDate.review_date == review_date_str)
|
|
result = await db.execute(stmt)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
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 ValueError:
|
|
week_day = ""
|
|
new_date = ReviewDate(review_date=review_date_str, week_day=week_day)
|
|
db.add(new_date)
|
|
await db.flush()
|
|
await db.refresh(new_date)
|
|
review_date_id = new_date.id
|
|
|
|
rankings = review_data.get("rankings", [])
|
|
if rankings:
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(
|
|
sa_delete(SymbolRanking).where(SymbolRanking.review_date_id == review_date_id)
|
|
)
|
|
for r in rankings:
|
|
db.add(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"),
|
|
))
|
|
|
|
plans = review_data.get("plans", [])
|
|
if plans:
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(
|
|
sa_delete(TradingPlan).where(TradingPlan.review_date_id == review_date_id)
|
|
)
|
|
for p in plans:
|
|
db.add(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"),
|
|
))
|
|
|
|
await db.flush()
|
|
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}")
|
|
async def get_review_summary(
|
|
review_date_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取复盘摘要(包含排名和计划的完整数据)。"""
|
|
stmt_date = select(ReviewDate).where(ReviewDate.id == review_date_id)
|
|
result_date = await db.execute(stmt_date)
|
|
review_date = result_date.scalar_one_or_none()
|
|
if not review_date:
|
|
raise HTTPException(status_code=404, detail="复盘日期不存在")
|
|
|
|
stmt_rankings = (
|
|
select(SymbolRanking)
|
|
.where(SymbolRanking.review_date_id == review_date_id)
|
|
.order_by(SymbolRanking.rank_type, SymbolRanking.rank)
|
|
)
|
|
result_rankings = await db.execute(stmt_rankings)
|
|
rankings = result_rankings.scalars().all()
|
|
|
|
stmt_plans = (
|
|
select(TradingPlan)
|
|
.where(TradingPlan.review_date_id == review_date_id)
|
|
.order_by(TradingPlan.score.desc())
|
|
)
|
|
result_plans = await db.execute(stmt_plans)
|
|
plans = result_plans.scalars().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: dict = {rt: {"title": rank_type_names.get(rt, rt), "items": []} for rt in rank_types}
|
|
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")
|
|
async def clear_review_data(
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""清除所有V1复盘与交易计划数据。"""
|
|
try:
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(sa_delete(TradingPlan))
|
|
await db.execute(sa_delete(SymbolRanking))
|
|
await db.execute(sa_delete(ReviewDate))
|
|
await db.flush()
|
|
return {"success": True, "message": "复盘数据已清除"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"清除失败: {e!s}"}
|
|
|
|
|
|
# ==================== V2 生成交易计划 ====================
|
|
|
|
|
|
@router.post("/generate")
|
|
async def generate_review_plan(
|
|
req: GeneratePlanRequest,
|
|
db: AsyncSession = Depends(get_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()]
|
|
|
|
symbols_config = load_symbols_config()
|
|
generator = PlanGenerator()
|
|
plan_data = await generator.generate(db, symbols_config)
|
|
|
|
# 保存计划到数据库
|
|
summary = plan_data.get("summary", {})
|
|
plan_v2 = ReviewPlanV2(
|
|
review_date=review_date_str,
|
|
week_day=week_day,
|
|
data_basis=summary.get("data_basis"),
|
|
core_conclusion=summary.get("core_conclusion"),
|
|
bull_count=summary.get("bull_count"),
|
|
bear_count=summary.get("bear_count"),
|
|
neutral_count=summary.get("neutral_count"),
|
|
opportunity_count=summary.get("opportunity_count"),
|
|
risk_warnings=summary.get("risk_warnings"),
|
|
actual_data_date=summary.get("actual_data_date"),
|
|
)
|
|
db.add(plan_v2)
|
|
await db.flush()
|
|
|
|
# 保存品种评分
|
|
for s in plan_data.get("scores", []):
|
|
score_v2 = SymbolScoreV2(
|
|
review_date_id=plan_v2.id,
|
|
symbol=s.get("symbol"),
|
|
name=s.get("name"),
|
|
close_price=s.get("close_price"),
|
|
prev_close=s.get("prev_close"),
|
|
high_price=s.get("high_price"),
|
|
low_price=s.get("low_price"),
|
|
volume=s.get("volume"),
|
|
avg_volume_5=s.get("avg_volume_5"),
|
|
amplitude_score=s.get("amplitude_score"),
|
|
volume_score=s.get("volume_score"),
|
|
change_score=s.get("change_score"),
|
|
trend_score=s.get("trend_score"),
|
|
activity_score=s.get("activity_score"),
|
|
composite_score=s.get("composite_score"),
|
|
amplitude_pct=s.get("amplitude_pct"),
|
|
change_pct=s.get("change_pct"),
|
|
volume_ratio=s.get("volume_ratio"),
|
|
trend_60m=s.get("trend_60m"),
|
|
trend_15m=s.get("trend_15m"),
|
|
trend_5m=s.get("trend_5m"),
|
|
direction=s.get("direction"),
|
|
direction_tag=s.get("direction_tag"),
|
|
category=s.get("category"),
|
|
pivot=s.get("pivots", {}).get("pivot"),
|
|
r1=s.get("pivots", {}).get("r1"),
|
|
r2=s.get("pivots", {}).get("r2"),
|
|
s1=s.get("pivots", {}).get("s1"),
|
|
s2=s.get("pivots", {}).get("s2"),
|
|
rank=s.get("rank"),
|
|
data_date=s.get("data_date"),
|
|
)
|
|
db.add(score_v2)
|
|
|
|
# 保存交易计划
|
|
for p in plan_data.get("plans", []):
|
|
plan_v2_item = TradingPlanV2(
|
|
review_date_id=plan_v2.id,
|
|
symbol=p.get("symbol"),
|
|
name=p.get("name"),
|
|
direction=p.get("direction"),
|
|
composite_score=p.get("composite_score"),
|
|
entry_low=p.get("entry_low"),
|
|
entry_high=p.get("entry_high"),
|
|
stop_loss=p.get("stop_loss"),
|
|
target1=p.get("target1"),
|
|
target2=p.get("target2"),
|
|
trigger=p.get("trigger"),
|
|
amplitude_score=p.get("amplitude_score"),
|
|
volume_score=p.get("volume_score"),
|
|
trend_score=p.get("trend_score"),
|
|
activity_score=p.get("activity_score"),
|
|
category=p.get("category"),
|
|
)
|
|
db.add(plan_v2_item)
|
|
|
|
# 保存板块热度
|
|
for sec in plan_data.get("sectors", []):
|
|
sector_heat = SectorHeat(
|
|
review_date_id=plan_v2.id,
|
|
sector_name=sec.get("sector_name"),
|
|
avg_score=sec.get("avg_score"),
|
|
avg_trend=sec.get("avg_trend"),
|
|
direction=sec.get("direction"),
|
|
heat_level=sec.get("heat_level"),
|
|
leader_symbol=sec.get("leader_symbol"),
|
|
leader_score=sec.get("leader_score"),
|
|
members=sec.get("members"),
|
|
)
|
|
db.add(sector_heat)
|
|
|
|
await db.flush()
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"plan_id": plan_v2.id,
|
|
"summary": {
|
|
"review_date": review_date_str,
|
|
"week_day": week_day,
|
|
"core_conclusion": summary.get("core_conclusion"),
|
|
"bull_count": summary.get("bull_count"),
|
|
"bear_count": summary.get("bear_count"),
|
|
"neutral_count": summary.get("neutral_count"),
|
|
"opportunity_count": summary.get("opportunity_count"),
|
|
"symbol_count": plan_data.get("symbol_count"),
|
|
},
|
|
},
|
|
}
|
|
except Exception as e:
|
|
logger.exception("生成交易计划失败")
|
|
return {"success": False, "message": f"生成失败: {e!s}"}
|
|
|
|
|
|
# ==================== V2 查询接口 ====================
|
|
|
|
|
|
@router.get("/v2/plans")
|
|
async def list_v2_plans(
|
|
limit: int = 20,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取V2复盘计划列表。"""
|
|
try:
|
|
stmt = select(ReviewPlanV2).order_by(ReviewPlanV2.review_date.desc()).limit(limit)
|
|
result = await db.execute(stmt)
|
|
plans = result.scalars().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"查询失败: {e!s}"}
|
|
|
|
|
|
@router.get("/v2/plan/{plan_id}")
|
|
async def get_v2_plan(
|
|
plan_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""根据ID获取完整V2计划数据。"""
|
|
try:
|
|
stmt_plan = select(ReviewPlanV2).where(ReviewPlanV2.id == plan_id)
|
|
result_plan = await db.execute(stmt_plan)
|
|
plan_meta = result_plan.scalar_one_or_none()
|
|
if not plan_meta:
|
|
return {"success": False, "message": f"计划 {plan_id} 不存在"}
|
|
|
|
stmt_scores = (
|
|
select(SymbolScoreV2)
|
|
.where(SymbolScoreV2.review_date_id == plan_id)
|
|
.order_by(SymbolScoreV2.composite_score.desc())
|
|
)
|
|
result_scores = await db.execute(stmt_scores)
|
|
scores = result_scores.scalars().all()
|
|
|
|
stmt_plans = (
|
|
select(TradingPlanV2)
|
|
.where(TradingPlanV2.review_date_id == plan_id)
|
|
.order_by(TradingPlanV2.composite_score.desc())
|
|
)
|
|
result_plans = await db.execute(stmt_plans)
|
|
trading_plans = result_plans.scalars().all()
|
|
|
|
stmt_sectors = (
|
|
select(SectorHeat)
|
|
.where(SectorHeat.review_date_id == plan_id)
|
|
.order_by(SectorHeat.avg_score.desc())
|
|
)
|
|
result_sectors = await db.execute(stmt_sectors)
|
|
sectors = result_sectors.scalars().all()
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"summary": {
|
|
"id": plan_meta.id,
|
|
"review_date": plan_meta.review_date,
|
|
"week_day": plan_meta.week_day,
|
|
"data_basis": plan_meta.data_basis,
|
|
"core_conclusion": plan_meta.core_conclusion,
|
|
"bull_count": plan_meta.bull_count,
|
|
"bear_count": plan_meta.bear_count,
|
|
"neutral_count": plan_meta.neutral_count,
|
|
"opportunity_count": plan_meta.opportunity_count,
|
|
"risk_warnings": plan_meta.risk_warnings,
|
|
"actual_data_date": plan_meta.actual_data_date,
|
|
"created_at": plan_meta.created_at.isoformat() if plan_meta.created_at else None,
|
|
},
|
|
"scores": [{
|
|
"symbol": s.symbol,
|
|
"name": s.name,
|
|
"close_price": s.close_price,
|
|
"composite_score": s.composite_score,
|
|
"direction": s.direction,
|
|
"direction_tag": s.direction_tag,
|
|
"category": s.category,
|
|
"rank": s.rank,
|
|
} for s in scores],
|
|
"plans": [{
|
|
"symbol": p.symbol,
|
|
"name": p.name,
|
|
"direction": p.direction,
|
|
"composite_score": p.composite_score,
|
|
"entry_low": p.entry_low,
|
|
"entry_high": p.entry_high,
|
|
"stop_loss": p.stop_loss,
|
|
"target1": p.target1,
|
|
"target2": p.target2,
|
|
"trigger": p.trigger,
|
|
"category": p.category,
|
|
} for p in trading_plans],
|
|
"sectors": [{
|
|
"sector_name": s.sector_name,
|
|
"avg_score": s.avg_score,
|
|
"direction": s.direction,
|
|
"heat_level": s.heat_level,
|
|
"leader_symbol": s.leader_symbol,
|
|
"leader_score": s.leader_score,
|
|
} for s in sectors],
|
|
},
|
|
}
|
|
except Exception as e:
|
|
logger.exception("获取V2计划 %d 失败", plan_id)
|
|
return {"success": False, "message": f"查询失败: {e!s}"}
|
|
|
|
|
|
@router.get("/v2/plan/by-date/{date}")
|
|
async def get_v2_plan_by_date(
|
|
date: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""根据日期获取V2计划数据。"""
|
|
try:
|
|
stmt_meta = select(ReviewPlanV2).where(ReviewPlanV2.review_date == date)
|
|
result_meta = await db.execute(stmt_meta)
|
|
plan_meta = result_meta.scalar_one_or_none()
|
|
if not plan_meta:
|
|
return {"success": False, "message": f"日期 {date} 的计划不存在"}
|
|
|
|
# 复用 get_v2_plan 的逻辑
|
|
stmt_scores = (
|
|
select(SymbolScoreV2)
|
|
.where(SymbolScoreV2.review_date_id == plan_meta.id)
|
|
.order_by(SymbolScoreV2.composite_score.desc())
|
|
)
|
|
result_scores = await db.execute(stmt_scores)
|
|
scores = result_scores.scalars().all()
|
|
|
|
stmt_plans = (
|
|
select(TradingPlanV2)
|
|
.where(TradingPlanV2.review_date_id == plan_meta.id)
|
|
.order_by(TradingPlanV2.composite_score.desc())
|
|
)
|
|
result_plans = await db.execute(stmt_plans)
|
|
trading_plans = result_plans.scalars().all()
|
|
|
|
stmt_sectors = (
|
|
select(SectorHeat)
|
|
.where(SectorHeat.review_date_id == plan_meta.id)
|
|
.order_by(SectorHeat.avg_score.desc())
|
|
)
|
|
result_sectors = await db.execute(stmt_sectors)
|
|
sectors = result_sectors.scalars().all()
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"summary": {
|
|
"id": plan_meta.id,
|
|
"review_date": plan_meta.review_date,
|
|
"week_day": plan_meta.week_day,
|
|
"data_basis": plan_meta.data_basis,
|
|
"core_conclusion": plan_meta.core_conclusion,
|
|
"bull_count": plan_meta.bull_count,
|
|
"bear_count": plan_meta.bear_count,
|
|
"neutral_count": plan_meta.neutral_count,
|
|
"opportunity_count": plan_meta.opportunity_count,
|
|
"risk_warnings": plan_meta.risk_warnings,
|
|
"actual_data_date": plan_meta.actual_data_date,
|
|
"created_at": plan_meta.created_at.isoformat() if plan_meta.created_at else None,
|
|
},
|
|
"scores": [{
|
|
"symbol": s.symbol,
|
|
"name": s.name,
|
|
"close_price": s.close_price,
|
|
"composite_score": s.composite_score,
|
|
"direction": s.direction,
|
|
"direction_tag": s.direction_tag,
|
|
"category": s.category,
|
|
"rank": s.rank,
|
|
} for s in scores],
|
|
"plans": [{
|
|
"symbol": p.symbol,
|
|
"name": p.name,
|
|
"direction": p.direction,
|
|
"composite_score": p.composite_score,
|
|
"entry_low": p.entry_low,
|
|
"entry_high": p.entry_high,
|
|
"stop_loss": p.stop_loss,
|
|
"target1": p.target1,
|
|
"target2": p.target2,
|
|
"trigger": p.trigger,
|
|
"category": p.category,
|
|
} for p in trading_plans],
|
|
"sectors": [{
|
|
"sector_name": s.sector_name,
|
|
"avg_score": s.avg_score,
|
|
"direction": s.direction,
|
|
"heat_level": s.heat_level,
|
|
"leader_symbol": s.leader_symbol,
|
|
"leader_score": s.leader_score,
|
|
} for s in sectors],
|
|
},
|
|
}
|
|
except Exception as e:
|
|
logger.exception("获取V2计划 by-date %s 失败", date)
|
|
return {"success": False, "message": f"查询失败: {e!s}"}
|
|
|
|
|
|
@router.delete("/v2/clear")
|
|
async def clear_v2_data(
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""清除所有V2数据。"""
|
|
try:
|
|
from sqlalchemy import delete as sa_delete
|
|
await db.execute(sa_delete(SectorHeat))
|
|
await db.execute(sa_delete(TradingPlanV2))
|
|
await db.execute(sa_delete(SymbolScoreV2))
|
|
await db.execute(sa_delete(ReviewPlanV2))
|
|
await db.flush()
|
|
return {"success": True, "message": "V2复盘数据已清除"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"清除失败: {e!s}"}
|