"""推荐列表 API""" import asyncio import logging from datetime import datetime from fastapi import APIRouter from app.engine.recommender import ( refresh_recommendations, get_latest_recommendations, get_recommendation_history, get_performance_stats, ) from app.config import is_trading_hours logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/recommendations", tags=["recommendations"]) @router.get("/latest") async def get_latest(): """获取最新推荐列表""" result = await get_latest_recommendations() mt = result.get("market_temp") return { "market_temperature": { "trade_date": mt.trade_date if mt else "", "temperature": mt.temperature if mt else 0, "up_count": mt.up_count if mt else 0, "down_count": mt.down_count if mt else 0, "limit_up_count": mt.limit_up_count if mt else 0, "limit_down_count": mt.limit_down_count if mt else 0, "max_streak": mt.max_streak if mt else 0, "broken_rate": mt.broken_rate if mt else 0, "index_above_ma20": mt.index_above_ma20 if mt else False, } if mt else None, "recommendations": [ { "ts_code": r.ts_code, "name": r.name, "sector": r.sector, "score": r.score, "level": r.level, "signal": r.signal, "market_temp_score": r.market_temp_score, "sector_score": r.sector_score, "capital_score": r.capital_score, "technical_score": r.technical_score, "supply_demand_score": r.supply_demand_score, "price_action_score": r.price_action_score, "position_score": r.position_score, "valuation_score": r.valuation_score, "entry_price": r.entry_price, "target_price": r.target_price, "stop_loss": r.stop_loss, "reasons": r.reasons, "risk_note": r.risk_note, "llm_analysis": r.llm_analysis, "entry_timing": r.entry_timing, "llm_score": r.llm_score, "strategy": r.strategy, "entry_signal_type": r.entry_signal_type, "scan_session": r.scan_session, "created_at": r.created_at.isoformat() if r.created_at else None, } for r in result.get("recommendations", []) ], "scan_mode": result.get("scan_mode", "unknown"), } @router.post("/refresh") async def refresh(scan_session: str = "manual"): """手动触发一次全量筛选(后台执行,立即返回)""" from app.engine.recommender import _scan_running, _scan_lock if _scan_running: return { "status": "already_running", "message": "扫描正在执行中,请稍候", "is_trading": is_trading_hours(), } # 在后台执行扫描,立即返回响应 asyncio.create_task(_run_scan_background(scan_session)) return { "status": "scanning", "message": "扫描已启动,完成后自动刷新", "is_trading": is_trading_hours(), } async def _run_scan_background(scan_session: str): """后台执行扫描并推送结果""" from app.engine.recommender import refresh_recommendations from app.api.websocket import broadcast_update try: result = await refresh_recommendations(scan_session=scan_session) rec_count = len(result.get("recommendations", [])) mt = result.get("market_temp") # 通过 WebSocket 推送扫描完成通知 await broadcast_update({ "type": "scan_update", "session": scan_session, "count": rec_count, "temperature": mt.temperature if mt else 0, "scan_mode": result.get("scan_mode", "unknown"), "timestamp": datetime.now().isoformat(), }) except Exception as e: logger.error(f"后台扫描失败: {e}") await broadcast_update({ "type": "scan_error", "session": scan_session, "message": str(e), "timestamp": datetime.now().isoformat(), }) @router.post("/update-tracking") async def update_tracking(): """独立更新推荐跟踪数据(不触发新扫描,盘中可单独调用)""" from app.engine.recommender import _update_tracking await _update_tracking() stats = await get_performance_stats() return { "status": "ok", "tracked": stats.get("tracked", 0), "win_rate": stats.get("win_rate", 0), "avg_return": stats.get("avg_return", 0), } @router.get("/status") async def get_scan_status(): """获取当前扫描状态信息""" return { "is_trading": is_trading_hours(), "scan_mode": "intraday" if is_trading_hours() else "post_market", "description": "盘中实时扫描(腾讯行情)" if is_trading_hours() else "盘后分析(Tushare日级数据)", } @router.get("/history") async def get_history(days: int = 7): """获取历史推荐(按日期分组)""" return await get_recommendation_history(days) @router.get("/performance") async def performance(): """获取推荐胜率统计""" return await get_performance_stats()