""" 统计分析API """ from fastapi import APIRouter, Query import sys from pathlib import Path from datetime import datetime, timedelta import logging project_root = Path(__file__).parent.parent.parent.parent sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'backend')) from database.models import AccountSnapshot, Trade, MarketScan, TradingSignal from fastapi import HTTPException logger = logging.getLogger(__name__) router = APIRouter() @router.get("/performance") async def get_performance_stats(days: int = Query(7, ge=1, le=365)): """获取性能统计""" try: # 账户快照 snapshots = AccountSnapshot.get_recent(days) # 交易统计 start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') trades = Trade.get_all(start_date=start_date) return { "snapshots": snapshots, "trades": trades, "period": f"Last {days} days" } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/dashboard") async def get_dashboard_data(): """获取仪表板数据""" try: account_data = None account_error = None # 优先尝试获取实时账户数据 try: from api.routes.account import get_realtime_account_data account_data = await get_realtime_account_data() logger.info("成功获取实时账户数据") except HTTPException as e: # HTTPException 需要特殊处理,提取错误信息 account_error = e.detail logger.warning(f"获取实时账户数据失败 (HTTP {e.status_code}): {account_error}") # 回退到数据库快照 try: snapshots = AccountSnapshot.get_recent(1) if snapshots: account_data = { "total_balance": snapshots[0].get('total_balance', 0), "available_balance": snapshots[0].get('available_balance', 0), "total_position_value": snapshots[0].get('total_position_value', 0), "total_pnl": snapshots[0].get('total_pnl', 0), "open_positions": snapshots[0].get('open_positions', 0) } logger.info("使用数据库快照作为账户数据") else: logger.warning("数据库中没有账户快照数据") except Exception as db_error: logger.error(f"从数据库获取账户快照失败: {db_error}") except Exception as e: account_error = str(e) logger.warning(f"获取实时账户数据失败: {account_error}", exc_info=True) # 回退到数据库快照 try: snapshots = AccountSnapshot.get_recent(1) if snapshots: account_data = { "total_balance": snapshots[0].get('total_balance', 0), "available_balance": snapshots[0].get('available_balance', 0), "total_position_value": snapshots[0].get('total_position_value', 0), "total_pnl": snapshots[0].get('total_pnl', 0), "open_positions": snapshots[0].get('open_positions', 0) } logger.info("使用数据库快照作为账户数据") except Exception as db_error: logger.error(f"从数据库获取账户快照失败: {db_error}") # 获取持仓数据(优先实时,回退到数据库) open_trades = [] positions_error = None try: from api.routes.account import get_realtime_positions positions = await get_realtime_positions() # 转换为前端需要的格式 open_trades = positions logger.info(f"成功获取实时持仓数据: {len(open_trades)} 个持仓") except HTTPException as e: positions_error = e.detail logger.warning(f"获取实时持仓失败 (HTTP {e.status_code}): {positions_error}") # 回退到数据库记录 try: db_trades = Trade.get_all(status='open')[:10] # 格式化数据库记录,添加 entry_value_usdt 字段 open_trades = [] for trade in db_trades: entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0)) leverage = float(trade.get('leverage', 1)) pnl = float(trade.get('pnl', 0)) # 数据库中的pnl_percent是价格涨跌幅,需要转换为收益率 # 收益率 = 盈亏 / 保证金 margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt pnl_percent = (pnl / margin * 100) if margin > 0 else 0 formatted_trade = { **trade, 'entry_value_usdt': entry_value_usdt, 'mark_price': trade.get('entry_price', 0), # 数据库中没有标记价,使用入场价 'pnl': pnl, 'pnl_percent': pnl_percent # 使用重新计算的收益率 } open_trades.append(formatted_trade) logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓") except Exception as db_error: logger.error(f"从数据库获取持仓记录失败: {db_error}") except Exception as e: positions_error = str(e) logger.warning(f"获取实时持仓失败: {positions_error}", exc_info=True) # 回退到数据库记录 try: db_trades = Trade.get_all(status='open')[:10] # 格式化数据库记录,添加 entry_value_usdt 字段 open_trades = [] for trade in db_trades: entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0)) leverage = float(trade.get('leverage', 1)) pnl = float(trade.get('pnl', 0)) # 数据库中的pnl_percent是价格涨跌幅,需要转换为收益率 # 收益率 = 盈亏 / 保证金 margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt pnl_percent = (pnl / margin * 100) if margin > 0 else 0 formatted_trade = { **trade, 'entry_value_usdt': entry_value_usdt, 'mark_price': trade.get('entry_price', 0), # 数据库中没有标记价,使用入场价 'pnl': pnl, 'pnl_percent': pnl_percent # 使用重新计算的收益率 } open_trades.append(formatted_trade) logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓") except Exception as db_error: logger.error(f"从数据库获取持仓记录失败: {db_error}") # 最近的扫描记录 recent_scans = [] try: recent_scans = MarketScan.get_recent(10) except Exception as e: logger.error(f"获取扫描记录失败: {e}") # 最近的信号 recent_signals = [] try: recent_signals = TradingSignal.get_recent(20) except Exception as e: logger.error(f"获取交易信号失败: {e}") # 计算仓位占比信息 position_stats = None if account_data: try: from database.models import TradingConfig total_balance = float(account_data.get('total_balance', 0)) total_position_value = float(account_data.get('total_position_value', 0)) max_total_position_percent = float(TradingConfig.get_value('MAX_TOTAL_POSITION_PERCENT', 0.30)) # 当前仓位占比 current_position_percent = (total_position_value / total_balance * 100) if total_balance > 0 else 0 # 最大仓位量(根据配置的最大占比计算) max_position_value = total_balance * max_total_position_percent position_stats = { "current_position_percent": round(current_position_percent, 2), "max_position_percent": round(max_total_position_percent * 100, 2), "max_position_value": round(max_position_value, 2), "total_balance": round(total_balance, 2), "total_position_value": round(total_position_value, 2) } except Exception as e: logger.warning(f"计算仓位占比信息失败: {e}") result = { "account": account_data, "open_trades": open_trades, "recent_scans": recent_scans, "recent_signals": recent_signals, "position_stats": position_stats } # 如果有错误,在响应中包含错误信息(但不影响返回) if account_error or positions_error: result["warnings"] = {} if account_error: result["warnings"]["account"] = account_error if positions_error: result["warnings"]["positions"] = positions_error return result except Exception as e: logger.error(f"获取仪表板数据失败: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"获取仪表板数据失败: {str(e)}")