""" FastAPI主应用 """ import asyncio from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading, news from app.utils.error_handler import setup_global_exception_handler, init_error_notifier from app.utils.system_status import get_system_monitor import os # 后台任务 _price_monitor_task = None _stock_agent_task = None _crypto_agent_task = None _news_agent_task = None _astock_monitor_task = None _astock_scheduler = None _astock_monitor_instance = None async def is_trading_day() -> bool: """检查今天是否为A股交易日""" try: from datetime import datetime from app.config import get_settings from app.astock_agent.tushare_client import TushareClient settings = get_settings() token = settings.tushare_token if not token: logger.warning("Tushare token 未配置,使用简单的周末判断") # 简单判断:周一到周五是交易日(不包含节假日) return datetime.now().weekday() < 5 client = TushareClient(token=token) pro = client.pro # 获取今天的日期 today = datetime.now().strftime("%Y%m%d") # 查询交易日历(最近3天) df = pro.trade_cal( exchange='SSE', start_date=(datetime.now().replace(day=datetime.now().day-2)).strftime("%Y%m%d") if datetime.now().day > 2 else today, end_date=today ) if df is not None and not df.empty: # 检查今天是否为交易日 today_cal = df[df['cal_date'] == today] if not today_cal.empty: is_open = today_cal.iloc[0]['is_open'] logger.info(f"交易日历查询: 今天 {today} {'是' if is_open == 1 else '不是'}交易日") return is_open == 1 # Fallback: 简单周末判断 is_weekday = datetime.now().weekday() < 5 logger.warning(f"交易日历查询失败,使用简单判断: 今天 {'是' if is_weekday else '不是'}工作日") return is_weekday except Exception as e: logger.error(f"检查交易日失败: {e}") # Fallback: 简单周末判断 return datetime.now().weekday() < 5 async def run_scheduled_astock_monitor(): """定时运行A股板块异动监控(每天 15:30)""" global _astock_monitor_instance if not _astock_monitor_instance: logger.warning("A股监控实例未初始化") return try: # 检查今天是否为交易日 if not await is_trading_day(): logger.info("📅 今天不是交易日,跳过板块异动分析") return logger.info("🔔 开始执行定时板块异动分析...") result = await _astock_monitor_instance.check_once() hot_sectors = result.get('hot_sectors', 0) stocks = result.get('stocks', 0) notified = result.get('notified', 0) logger.info(f"✅ 定时板块分析完成: {hot_sectors}个异动板块, {stocks}只龙头股, {notified}条通知") except Exception as e: logger.error(f"定时板块分析失败: {e}") async def start_scheduler(): """启动定时任务调度器""" from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger global _astock_scheduler # 创建调度器 _astock_scheduler = AsyncIOScheduler(timezone='Asia/Shanghai') # 添加定时任务:每天 15:30 运行板块异动分析 _astock_scheduler.add_job( run_scheduled_astock_monitor, trigger=CronTrigger(hour=15, minute=30), id='daily_astock_monitor', name='A股板块异动分析', replace_existing=True ) _astock_scheduler.start() logger.info("📅 定时任务调度器已启动: 每天 15:30 (A股板块异动分析)") async def price_monitor_loop(): """后台价格监控循环 - 使用轮询检查止盈止损""" from app.services.paper_trading_service import get_paper_trading_service from app.services.bitget_service import bitget_service from app.services.feishu_service import get_feishu_service from app.services.telegram_service import get_telegram_service logger.info("后台价格监控任务已启动(轮询模式)") feishu = get_feishu_service() telegram = get_telegram_service() paper_trading = get_paper_trading_service() # 价格更新回调 - 检查止盈止损 async def on_price_update(symbol: str, price: float): """价格更新时检查止盈止损""" try: # 检查止盈止损 triggered = paper_trading.check_price_triggers(symbol, price) # 发送通知 for result in triggered: status = result.get('status', '') event_type = result.get('event_type', 'order_closed') # 处理挂单成交事件 if event_type == 'order_filled': side_text = "做多" if result.get('side') == 'long' else "做空" side_icon = "🟢" if result.get('side') == 'long' else "🔴" grade = result.get('signal_grade', 'N/A') symbol = result.get('symbol', '') entry_price = result.get('entry_price', 0) filled_price = result.get('filled_price', 0) stop_loss = result.get('stop_loss', 0) take_profit = result.get('take_profit', 0) # 根据交易对精度格式化价格 try: from app.services.bitget_service import bitget_service precision = bitget_service.get_precision(symbol) price_fmt = f"{{:,.{precision['pricePrecision']}f}}" except: price_fmt = "{:,.2f}" title = f"✅ 挂单成交 - {symbol}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"⭐ **信号等级**: {grade}", f"💰 **挂单价**: ${price_fmt.format(entry_price)}", f"🎯 **成交价**: ${price_fmt.format(filled_price)}", f"💵 **仓位**: ${result.get('quantity', 0):,.0f}", ] if stop_loss: content_parts.append(f"🛑 **止损**: ${price_fmt.format(stop_loss)}") if take_profit: content_parts.append(f"🎯 **止盈**: ${price_fmt.format(take_profit)}") content = "\n".join(content_parts) # 发送通知 await feishu.send_card(title, content, "green") await telegram.send_message(f"{title}\n\n{content}") logger.info(f"后台监控触发挂单成交: {result.get('order_id')} | {symbol}") continue # 处理订单平仓事件 is_win = result.get('is_win', False) if status == 'closed_tp': emoji = "🎯" status_text = "止盈平仓" color = "green" elif status == 'closed_sl': emoji = "🛑" status_text = "止损平仓" color = "red" elif status == 'closed_be': emoji = "🔒" status_text = "保本止损" color = "orange" else: emoji = "📤" status_text = "平仓" color = "blue" win_text = "盈利" if is_win else "亏损" win_emoji = "✅" if is_win else "❌" side_text = "做多" if result.get('side') == 'long' else "做空" side_icon = "🟢" if result.get('side') == 'long' else "🔴" title = f"{emoji} 订单{status_text} - {result.get('symbol')}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"📊 **入场**: ${result.get('entry_price', 0):,.2f}", f"🎯 **出场**: ${result.get('exit_price', 0):,.2f}", f"{win_emoji} **{win_text}**: {result.get('pnl_percent', 0):+.2f}% (${result.get('pnl_amount', 0):+.2f})", f"⏱️ **持仓时间**: {result.get('hold_duration', 'N/A')}", ] content = "\n".join(content_parts) # 发送通知 await feishu.send_card(title, content, color) await telegram.send_message(f"{title}\n\n{content}") logger.info(f"后台监控触发平仓: {result.get('order_id')} | {symbol}") except Exception as e: logger.error(f"处理 {symbol} 价格更新失败: {e}") # 持续监控活跃订单 while True: try: # 获取活跃订单 active_orders = paper_trading.get_active_orders() if not active_orders: await asyncio.sleep(10) # 没有活跃订单时,10秒检查一次 continue # 获取所有需要的交易对 symbols = set(order.get('symbol') for order in active_orders if order.get('symbol')) # 获取价格并检查止盈止损 for symbol in symbols: try: price = bitget_service.get_current_price(symbol) if not price: continue # 检查止盈止损 triggered = paper_trading.check_price_triggers(symbol, price) # 发送通知 for result in triggered: status = result.get('status', '') event_type = result.get('event_type', 'order_closed') # 处理止损移动事件 if event_type == 'stop_loss_moved': move_type = result.get('move_type', '') side_text = "做多" if result.get('side') == 'long' else "做空" side_icon = '🟢' if result.get('side') == 'long' else '🔴' pnl = result.get('current_pnl_percent', 0) symbol_display = result.get('symbol', '') new_stop_loss = result.get('new_stop_loss', 0) # 根据交易对精度格式化价格 try: from app.services.bitget_service import bitget_service precision = bitget_service.get_precision(symbol_display) price_fmt = f"{{:,.{precision['pricePrecision']}f}}" except: price_fmt = "{:,.2f}" if move_type == 'trailing_first': title = f"📈 移动止损已激活 - {symbol_display}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"", f"📈 **当前盈利**: {pnl:+.2f}%", f"🛑 **新止损价**: ${price_fmt.format(new_stop_loss)}", f"", f"💰 锁定利润,让利润奔跑" ] color = "green" elif move_type == 'trailing_update': title = f"📊 止损已上移 - {symbol_display}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"", f"📈 **当前盈利**: {pnl:+.2f}%", f"🛑 **新止损价**: ${price_fmt.format(new_stop_loss)}", f"", f"🎯 继续锁定更多利润" ] color = "blue" elif move_type == 'breakeven': title = f"📈 移动止损已启动 - {symbol_display}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"", f"📈 **当前盈利**: {pnl:+.2f}%", f"🛑 **新止损价**: ${price_fmt.format(new_stop_loss)}", f"", f"💰 锁定利润,让利润奔跑" ] color = "green" else: continue content = "\n".join(content_parts) # 发送通知 await feishu.send_card(title, content, color) if telegram: message = f"{title}\n\n{content}" await telegram.send_message(message) logger.info(f"后台监控触发止损移动: {result.get('order_id')} | {symbol}") continue # 处理挂单成交事件 if event_type == 'order_filled': side_text = "做多" if result.get('side') == 'long' else "做空" side_icon = '🟢' if result.get('side') == 'long' else '🔴' grade = result.get('signal_grade', 'N/A') symbol = result.get('symbol', '') entry_price = result.get('entry_price', 0) filled_price = result.get('filled_price', 0) stop_loss = result.get('stop_loss', 0) take_profit = result.get('take_profit', 0) # 根据交易对精度格式化价格 try: from app.services.bitget_service import bitget_service precision = bitget_service.get_precision(symbol) price_fmt = f"{{:,.{precision['pricePrecision']}f}}" except: price_fmt = "{:,.2f}" title = f"✅ 挂单成交 - {symbol}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"", f"⭐ **信号等级**: {grade}", f"", f"💰 **挂单价**: ${price_fmt.format(entry_price)}", f"🎯 **成交价**: ${price_fmt.format(filled_price)}", f"📊 **持仓价值**: ${result.get('quantity', 0):,.0f}", f"", f"🛑 **止损价**: ${price_fmt.format(stop_loss)}", f"🎯 **止盈价**: ${price_fmt.format(take_profit)}" ] content = "\n".join(content_parts) # 发送通知 await feishu.send_card(title, content, "blue") if telegram: message = f"{title}\n\n{content}" await telegram.send_message(message) logger.info(f"后台监控触发挂单成交: {result.get('order_id')} | {symbol}") continue # 处理订单平仓事件 is_win = result.get('is_win', False) if status == 'closed_tp': emoji = "🎯" status_text = "止盈平仓" color = "green" elif status == 'closed_sl': emoji = "🛑" status_text = "止损平仓" color = "red" elif status == 'closed_be': emoji = "📈" status_text = "移动止损" color = "orange" else: emoji = "📤" status_text = "平仓" color = "blue" win_text = "盈利" if is_win else "亏损" win_emoji = "✅" if is_win else "❌" side_text = "做多" if result.get('side') == 'long' else "做空" side_icon = "🟢" if result.get('side') == 'long' else "🔴" title = f"{emoji} 订单{status_text} - {result.get('symbol')}" content_parts = [ f"{side_icon} **方向**: {side_text}", f"📊 **入场**: ${result.get('entry_price', 0):,.2f}", f"🎯 **出场**: ${result.get('exit_price', 0):,.2f}", f"{win_emoji} **{win_text}**: {result.get('pnl_percent', 0):+.2f}% (${result.get('pnl_amount', 0):+.2f})", f"⏱️ **持仓时间**: {result.get('hold_duration', 'N/A')}", ] content = "\n".join(content_parts) # 发送通知 await feishu.send_card(title, content, color) await telegram.send_message(f"{title}\n\n{content}") logger.info(f"后台监控触发平仓: {result.get('order_id')} | {symbol}") except Exception as e: logger.error(f"检查 {symbol} 价格失败: {e}") # 每 3 秒检查一次 await asyncio.sleep(3) except Exception as e: logger.error(f"价格监控循环出错: {e}") await asyncio.sleep(5) async def _print_system_status(): """打印系统状态摘要""" from app.utils.system_status import get_system_monitor monitor = get_system_monitor() summary = monitor.get_summary() logger.info("\n" + "=" * 60) logger.info("📊 系统状态摘要") logger.info("=" * 60) logger.info(f"启动时间: {summary['system_start_time']}") logger.info(f"运行时长: {int(summary['uptime_seconds'] // 60)} 分钟") logger.info(f"Agent 总数: {summary['total_agents']}") logger.info(f"运行中: {summary['running_agents']} | 错误: {summary['error_agents']}") if summary['agents']: logger.info("\n🤖 Agent 详情:") for agent_id, agent_info in summary['agents'].items(): status_icon = { "运行中": "✅", "启动中": "🔄", "已停止": "⏸️", "错误": "❌", "未初始化": "⚪" }.get(agent_info['status'], "❓") logger.info(f" {status_icon} {agent_info['name']} - {agent_info['status']}") # 显示配置 config = agent_info.get('config', {}) if config: if 'symbols' in config: logger.info(f" 监控: {', '.join(config['symbols'])}") if 'paper_trading_enabled' in config: pt_status = "已启用" if config['paper_trading_enabled'] else "未启用" logger.info(f" 模拟交易: {pt_status}") if 'analysis_interval' in config: logger.info(f" 分析间隔: {config['analysis_interval']}") logger.info("=" * 60 + "\n") @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" global _price_monitor_task, _stock_agent_task, _crypto_agent_task, _news_agent_task, _astock_monitor_task # 启动时执行 logger.info("应用启动") # 初始化全局异常处理器 setup_global_exception_handler() logger.info("全局异常处理器已安装") # 初始化飞书错误通知 try: from app.services.feishu_service import FeishuService from app.config import get_settings settings = get_settings() # 使用专用的系统异常 webhook feishu_error_service = FeishuService( webhook_url=settings.feishu_error_webhook_url, service_type="error" ) init_error_notifier( feishu_service=feishu_error_service, enabled=True, # 启用异常通知 cooldown=300 # 5分钟冷却时间 ) logger.info("✅ 系统异常通知已启用(异常将发送到飞书)") except Exception as e: logger.warning(f"飞书异常通知初始化失败: {e}") # 启动后台任务 settings = get_settings() if getattr(settings, 'paper_trading_enabled', True): _price_monitor_task = asyncio.create_task(price_monitor_loop()) logger.info("后台价格监控任务已创建") # 启动加密货币智能体 if getattr(settings, 'crypto_symbols', '') and settings.crypto_symbols.strip(): try: from app.crypto_agent.crypto_agent import get_crypto_agent crypto_agent = get_crypto_agent() # 防止重复启动 if not crypto_agent.running: _crypto_agent_task = asyncio.create_task(crypto_agent.run()) logger.info(f"加密货币智能体已启动,监控: {settings.crypto_symbols}") else: logger.info("加密货币智能体已在运行中") except Exception as e: logger.error(f"加密货币智能体启动失败: {e}") # 启动股票智能体(美股 + 港股) us_symbols = getattr(settings, 'stock_symbols_us', '') or '' hk_symbols = getattr(settings, 'stock_symbols_hk', '') or '' if (us_symbols.strip() or hk_symbols.strip()): try: from app.stock_agent.stock_agent import get_stock_agent stock_agent = get_stock_agent() _stock_agent_task = asyncio.create_task(stock_agent.start()) # 设置智能体实例到 API 模块 stocks.set_stock_agent(stock_agent) symbols_list = [] if us_symbols: symbols_list.append(f"美股({len(us_symbols.split(','))}只)") if hk_symbols: symbols_list.append(f"港股({len(hk_symbols.split(','))}只)") logger.info(f"股票智能体已启动,监控: {', '.join(symbols_list)}") except Exception as e: logger.error(f"股票智能体启动失败: {e}") logger.error(f"提示: 请确保已安装 yfinance (pip install yfinance)") else: logger.info("股票智能体未启动(未配置股票代码)") # 启动新闻智能体 try: from app.news_agent.news_agent import get_news_agent news_agent = get_news_agent() _news_agent_task = asyncio.create_task(news_agent.start()) logger.info("新闻智能体已启动") except Exception as e: logger.error(f"新闻智能体启动失败: {e}") logger.error(f"提示: 请确保已安装 feedparser 和 beautifulsoup4 (pip install feedparser beautifulsoup4)") # 启动A股智能体 if getattr(settings, 'astock_monitor_enabled', True): try: from app.astock_agent import SectorMonitor sector_monitor = SectorMonitor( change_threshold=settings.astock_change_threshold, top_n=settings.astock_top_n, enable_notifier=bool(settings.dingtalk_astock_webhook) ) # 保存实例供定时任务使用 _astock_monitor_instance = sector_monitor logger.info(f"A股智能体已初始化") except Exception as e: logger.error(f"A股智能体初始化失败: {e}") # 启动定时任务调度器 await start_scheduler() # 显示系统状态摘要 await _print_system_status() yield # 关闭时执行 if _price_monitor_task: _price_monitor_task.cancel() try: await _price_monitor_task except asyncio.CancelledError: pass logger.info("后台价格监控任务已停止") # 停止加密货币智能体 if _crypto_agent_task: _crypto_agent_task.cancel() try: await _crypto_agent_task except asyncio.CancelledError: pass logger.info("加密货币智能体已停止") # 停止美股智能体 if _stock_agent_task: _stock_agent_task.cancel() try: await _stock_agent_task except asyncio.CancelledError: pass logger.info("美股智能体已停止") # 停止新闻智能体 if _news_agent_task: try: from app.news_agent.news_agent import get_news_agent news_agent = get_news_agent() await news_agent.stop() except asyncio.CancelledError: pass except Exception as e: logger.error(f"停止新闻智能体失败: {e}") logger.info("新闻智能体已停止") # 停止A股智能体 global _astock_scheduler if _astock_scheduler: _astock_scheduler.shutdown(wait=False) logger.info("A股定时任务已停止") if _astock_monitor_task: _astock_monitor_task.cancel() try: await _astock_monitor_task except asyncio.CancelledError: pass logger.info("A股智能体已停止") logger.info("应用关闭") # 创建FastAPI应用 app = FastAPI( title="A股AI分析Agent系统", description="基于AI Agent的股票智能分析系统", version="1.0.0", lifespan=lifespan ) # 配置CORS settings = get_settings() app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins.split(","), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 注册路由 app.include_router(auth.router, tags=["认证"]) app.include_router(admin.router, tags=["后台管理"]) app.include_router(chat.router, prefix="/api/chat", tags=["对话"]) app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"]) app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(llm.router, tags=["LLM模型"]) app.include_router(paper_trading.router, tags=["模拟交易"]) app.include_router(real_trading.router, tags=["实盘交易"]) app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"]) app.include_router(signals.router, tags=["信号管理"]) app.include_router(news.router, tags=["新闻管理"]) app.include_router(system.router, prefix="/api/system", tags=["系统状态"]) # 挂载静态文件 frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend") if os.path.exists(frontend_path): app.mount("/static", StaticFiles(directory=frontend_path), name="static") @app.get("/") async def root(): """根路径,返回主应用页面""" index_path = os.path.join(frontend_path, "index.html") if os.path.exists(index_path): return FileResponse(index_path) return {"message": "页面不存在"} @app.get("/app") async def app_page(): """主应用页面""" index_path = os.path.join(frontend_path, "index.html") if os.path.exists(index_path): return FileResponse(index_path) return {"message": "页面不存在"} @app.get("/health") async def health_check(): """健康检查""" return {"status": "healthy"} @app.get("/paper-trading") async def paper_trading_page(): """模拟交易页面""" page_path = os.path.join(frontend_path, "paper-trading.html") if os.path.exists(page_path): return FileResponse(page_path) return {"message": "页面不存在"} @app.get("/real-trading") async def real_trading_page(): """实盘交易页面""" page_path = os.path.join(frontend_path, "real-trading.html") if os.path.exists(page_path): return FileResponse(page_path) return {"message": "页面不存在"} @app.get("/signals") async def signals_page(): """信号列表页面""" page_path = os.path.join(frontend_path, "signals.html") if os.path.exists(page_path): return FileResponse(page_path) return {"message": "页面不存在"} @app.get("/status") async def status_page(): """系统状态监控页面""" page_path = os.path.join(frontend_path, "status.html") if os.path.exists(page_path): return FileResponse(page_path) return {"message": "页面不存在"} if __name__ == "__main__": import uvicorn # 设置全局异常处理器(防止主线程异常退出) setup_global_exception_handler() try: uvicorn.run( "app.main:app", host=settings.api_host, port=settings.api_port, reload=settings.debug ) except Exception as e: logger.error(f"应用启动失败: {e}") import traceback logger.error(traceback.format_exc()) raise