""" A股相关 API 路由 """ from fastapi import APIRouter, HTTPException, BackgroundTasks from typing import Dict, Any from app.utils.logger import logger from app.config import get_settings router = APIRouter() # 全局变量,用于访问智能体实例 _astock_agent_instance = None def set_astock_agent(agent): """设置智能体实例(由 main.py 调用)""" global _astock_agent_instance _astock_agent_instance = agent @router.get("/status") async def get_astock_status() -> Dict[str, Any]: """ 获取A股智能体状态 Returns: 智能体状态信息 """ try: if _astock_agent_instance is None: return { "enabled": False, "message": "A股智能体未启用" } settings = get_settings() return { "enabled": True, "running": _astock_agent_instance.running, "selector_type": "short_term_thematic", "description": "短期题材选股器(题材轮动 + 技术面确认 + 风险控制)", "config": { "min_market_cap": settings.astock_min_market_cap if hasattr(settings, 'astock_min_market_cap') else 50, "max_market_cap": settings.astock_max_market_cap if hasattr(settings, 'astock_max_market_cap') else 500, "change_threshold": settings.astock_change_threshold, "top_n": settings.astock_top_n } } except Exception as e: logger.error(f"获取A股状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/select") async def trigger_selection(background_tasks: BackgroundTasks) -> Dict[str, Any]: """ 手动触发选股 Returns: 选股任务状态 """ try: if _astock_agent_instance is None: raise HTTPException(status_code=400, detail="A股智能体未启用") # 在后台执行选股任务 background_tasks.add_task(_astock_agent_instance.run_once) return { "success": True, "message": "选股任务已提交,正在后台执行", "note": "请查看通知或日志获取结果" } except HTTPException: raise except Exception as e: logger.error(f"触发选股失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/select/sync") async def trigger_selection_sync() -> Dict[str, Any]: """ 手动触发选股(同步执行) Returns: 选股结果 """ try: if _astock_agent_instance is None: raise HTTPException(status_code=400, detail="A股智能体未启用") # 同步执行选股 result = await _astock_agent_instance.run_once() return { "success": True, "result": result } except HTTPException: raise except Exception as e: logger.error(f"触发选股失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/config") async def get_astock_config() -> Dict[str, Any]: """ 获取A股选股配置 Returns: 配置信息 """ try: settings = get_settings() return { "selector": { "type": "short_term_thematic", "description": "短期题材选股器", "strategy": "题材轮动 + 技术面确认 + 风险控制" }, "screening": { "min_market_cap": 50, # 最小市值(亿) "max_market_cap": 500, # 最大市值(亿) "min_turnover": 3.0, # 最小换手率(%) "max_turnover": 15.0, # 最大换手率(%) "sector_change_threshold": 2.0, # 板块涨幅阈值(%) "volume_ratio_threshold": 1.2 # 量比阈值 }, "risk_control": { "max_drawdown": 10.0, # 最大回撤(%) "hard_stop_loss": -7.0, # 硬止损(%) "max_single_position": 20, # 单票最大仓位(%) "max_sector_position": 40, # 单行业最大仓位(%) "max_total_position": 80 # 总仓位最大值(%) }, "schedule": { "enabled": settings.astock_monitor_enabled, "time": "15:30", # 盘后运行 "timezone": "Asia/Shanghai" }, "notifications": { "dingtalk": settings.dingtalk_enabled, "telegram": settings.telegram_enabled } } except Exception as e: logger.error(f"获取配置失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/sectors") async def get_hot_sectors(limit: int = 10) -> Dict[str, Any]: """ 获取当前异动板块 Args: limit: 返回板块数量 Returns: 异动板块列表 """ try: from app.astock_agent.tushare_client import get_tushare_client from app.config import get_settings settings = get_settings() ts_client = get_tushare_client(settings.tushare_token) if not ts_client: raise HTTPException(status_code=400, detail="Tushare客户端未初始化") # 获取异动板块 sectors_df = ts_client.get_hot_sectors(threshold=2.0) if sectors_df.empty: return { "success": True, "count": 0, "sectors": [] } # 转换为列表格式 sectors = [] for _, row in sectors_df.head(limit).iterrows(): sectors.append({ "code": row['ts_code'], "name": row['name'], "change_pct": float(row['change_pct']), "amount": float(row['amount']), "amount_yi": float(row['amount']) / 100000000, # 转换为亿 "close": float(row['close']) }) return { "success": True, "count": len(sectors), "sectors": sectors } except HTTPException: raise except Exception as e: logger.error(f"获取异动板块失败: {e}") raise HTTPException(status_code=500, detail=str(e))