""" 系统状态 API """ from datetime import datetime, timedelta from fastapi import APIRouter, HTTPException from typing import Dict, Any import numpy as np from app.utils.logger import logger from app.utils.system_status import get_system_monitor from app.crypto_agent.crypto_agent import get_crypto_agent from app.services.signal_database_service import get_signal_db_service from app.services.paper_trading_service import get_paper_trading_service from app.services.bitget_live_trading_service import get_all_bitget_live_services, get_bitget_live_service router = APIRouter() def _sanitize_for_response(value: Any) -> Any: if isinstance(value, dict): return {str(key): _sanitize_for_response(item) for key, item in value.items()} if isinstance(value, list): return [_sanitize_for_response(item) for item in value] if isinstance(value, tuple): return [_sanitize_for_response(item) for item in value] if isinstance(value, set): return [_sanitize_for_response(item) for item in value] if isinstance(value, np.bool_): return bool(value) if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): return float(value) if isinstance(value, np.ndarray): return [_sanitize_for_response(item) for item in value.tolist()] if isinstance(value, np.generic): return value.item() return value def _parse_signal_timestamp(value: Any) -> datetime | None: if value is None: return None if isinstance(value, datetime): return value.replace(tzinfo=None) if value.tzinfo else value text = str(value).replace("Z", "+00:00") try: parsed = datetime.fromisoformat(text) return parsed.replace(tzinfo=None) if parsed.tzinfo else parsed except ValueError: return None def _safe_float(value: Any, default: float = 0.0) -> float: try: if value is None or value == "": return default return float(value) except (TypeError, ValueError): return default def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dict[str, Any]: side_raw = str(position.get("side") or "").lower() side = "long" if side_raw in {"buy", "long"} else "short" symbol = position.get("symbol") or position.get("coin") or "-" if isinstance(symbol, str) and symbol.endswith("USDTUSDT"): symbol = symbol.replace("USDTUSDT", "USDT") entry_price = _safe_float(position.get("entry_price") or position.get("filled_price")) mark_price = _safe_float(position.get("mark_price") or position.get("current_price")) if mark_price <= 0: mark_price = entry_price size = abs(_safe_float(position.get("size") or position.get("quantity"))) leverage = _safe_float(position.get("leverage")) margin = _safe_float(position.get("margin") or position.get("initialMargin") or position.get("initial_margin")) unrealized_pnl = _safe_float(position.get("unrealized_pnl") or position.get("pnl_amount")) pnl_percent = _safe_float(position.get("unrealized_pnl_pct") or position.get("pnl_percent") or position.get("percentage")) return { "platform": platform, "symbol": symbol, "side": side, "size": size, "entry_price": entry_price, "mark_price": mark_price, "leverage": leverage, "margin": margin, "unrealized_pnl": unrealized_pnl, "pnl_percent": pnl_percent, "take_profit": position.get("take_profit"), "stop_loss": position.get("stop_loss"), "liquidation_price": position.get("liquidation_price"), "opened_at": position.get("opened_at") or position.get("created_at"), } def _normalize_platform_order(platform: str, order: Dict[str, Any]) -> Dict[str, Any]: side_raw = str(order.get("side") or "").lower() side = "long" if side_raw in {"buy", "long", "b"} else "short" symbol = order.get("symbol") or order.get("coin") or "-" if isinstance(symbol, str) and symbol.endswith("USDTUSDT"): symbol = symbol.replace("USDTUSDT", "USDT") price = _safe_float(order.get("price") or order.get("entry_price")) size = abs(_safe_float(order.get("size") or order.get("quantity"))) order_type = str(order.get("order_type") or order.get("entry_type") or order.get("type") or "").lower() status = str(order.get("status") or "").lower() is_reduce_only = bool(order.get("is_reduce_only")) category = "tp_sl" if is_reduce_only else "entry" if platform == "paper" and status == "pending": category = "entry" return { "platform": platform, "account_id": order.get("account_id"), "symbol": symbol, "side": side, "category": category, "price": price, "size": size, "leverage": _safe_float(order.get("leverage")), "margin": _safe_float(order.get("margin")), "order_type": order_type, "status": status, "stop_loss": order.get("stop_loss"), "take_profit": order.get("take_profit"), "signal_grade": order.get("signal_grade"), "signal_type": order.get("signal_type"), "confidence": _safe_float(order.get("confidence")), "created_at": order.get("created_at") or order.get("timestamp"), } def _build_bitget_account_summary(account_id: str, service: Any) -> Dict[str, Any]: bg_account = service.get_account_state() bg_positions = service.get_open_positions() bg_orders = service.get_open_orders() bg_total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in bg_positions) bg_drawdown = 0.0 if service.initial_balance and service.initial_balance > 0: bg_drawdown = (service.initial_balance - bg_account["account_value"]) / service.initial_balance * 100 return { "account_id": account_id, "enabled": True, "account": { "account_value": bg_account.get("account_value", 0), "available_balance": bg_account.get("available_balance", 0), "total_margin_used": bg_account.get("total_margin_used", 0), "initial_balance": service.initial_balance, }, "positions": { "count": len(bg_positions), "total_value": bg_total_position_value, "items": bg_positions[:8], }, "orders": { "count": len(bg_orders), "entry_orders": len([o for o in bg_orders if not o.get("is_reduce_only")]), "tp_sl_orders": len([o for o in bg_orders if o.get("is_reduce_only")]), "items": bg_orders[:8], }, "risk": { "current_leverage": bg_total_position_value / bg_account["account_value"] if bg_account.get("account_value", 0) > 0 else 0, "max_leverage": service.max_total_leverage, "drawdown_percent": bg_drawdown, "circuit_breaker_threshold": service.circuit_breaker_drawdown * 100, }, } def _build_attention_items( platform_halts: Dict[str, Any], platforms: Dict[str, Any], unified_positions: list[Dict[str, Any]], unified_orders: list[Dict[str, Any]], execution_events: list[Dict[str, Any]], ) -> list[Dict[str, Any]]: items: list[Dict[str, Any]] = [] for platform, halt in (platform_halts or {}).items(): if halt and halt.get("halted"): items.append({ "severity": "danger", "title": f"{platform} 已停机", "detail": halt.get("reason") or "平台已触发停机/熔断", "timestamp": halt.get("halted_at"), }) for platform_name, payload in (platforms or {}).items(): if payload.get("enabled") is False: continue risk = payload.get("risk") or {} current_leverage = _safe_float(risk.get("current_leverage")) max_leverage = _safe_float(risk.get("max_leverage")) drawdown_pct = _safe_float(risk.get("drawdown_percent") or risk.get("drawdown")) breaker_pct = _safe_float(risk.get("circuit_breaker_threshold"), 25.0) if breaker_pct > 0 and drawdown_pct >= breaker_pct * 0.7: items.append({ "severity": "warning", "title": f"{platform_name} 回撤接近熔断", "detail": f"当前回撤 {drawdown_pct:.1f}% / 阈值 {breaker_pct:.1f}%", "timestamp": None, }) if max_leverage > 0 and current_leverage >= max_leverage * 0.8: items.append({ "severity": "warning", "title": f"{platform_name} 杠杆占用偏高", "detail": f"当前总杠杆 {current_leverage:.2f}x / 上限 {max_leverage:.2f}x", "timestamp": None, }) for pos in unified_positions: if not pos.get("stop_loss") or not pos.get("take_profit"): items.append({ "severity": "warning", "title": f"{pos['platform']} {pos['symbol']} 风控不完整", "detail": "持仓缺少止盈或止损,请确认执行层保护单状态", "timestamp": pos.get("opened_at"), }) pending_entry_count = sum(1 for order in unified_orders if order.get("category") == "entry") if pending_entry_count > 0: items.append({ "severity": "info", "title": "存在待成交入场单", "detail": f"当前共有 {pending_entry_count} 笔待成交入场单,建议关注价格接近度和资金占用。", "timestamp": None, }) for event in (execution_events or [])[:8]: if event.get("status") in {"error", "warning"}: items.append({ "severity": event.get("status"), "title": f"{event.get('platform', '-')}: {event.get('event_type', '-')}", "detail": event.get("reason") or "最近执行事件需要关注", "timestamp": event.get("timestamp"), }) return items[:12] @router.get("/status", response_model=Dict[str, Any]) async def get_system_status(): """ 获取系统状态 返回所有 Agent 的运行状态和系统信息 """ try: monitor = get_system_monitor() summary = monitor.get_summary() # 添加额外的系统信息 response = { "status": "success", "data": summary } return response except Exception as e: logger.error(f"获取系统状态失败: {e}") raise HTTPException(status_code=500, detail=f"获取系统状态失败: {str(e)}") @router.get("/status/summary") async def get_status_summary(): """ 获取系统状态摘要 返回简化的状态信息,用于快速检查 """ try: monitor = get_system_monitor() summary = monitor.get_summary() return { "status": "success", "data": { "total_agents": summary["total_agents"], "running_agents": summary["running_agents"], "error_agents": summary["error_agents"], "uptime_seconds": summary["uptime_seconds"], "agents": { agent_id: { "name": info["name"], "status": info["status"] } for agent_id, info in summary["agents"].items() } } } except Exception as e: logger.error(f"获取状态摘要失败: {e}") raise HTTPException(status_code=500, detail=f"获取状态摘要失败: {str(e)}") @router.get("/status/agents/{agent_id}") async def get_agent_status(agent_id: str): """ 获取指定 Agent 的详细状态 Args: agent_id: Agent ID (如: crypto_agent) """ try: monitor = get_system_monitor() agent_info = monitor.get_agent_status(agent_id) if agent_info is None: raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' 不存在") return { "status": "success", "data": agent_info.to_dict() } except HTTPException: raise except Exception as e: logger.error(f"获取 Agent 状态失败: {e}") raise HTTPException(status_code=500, detail=f"获取 Agent 状态失败: {str(e)}") @router.get("/console", response_model=Dict[str, Any]) async def get_console_snapshot(): """ 获取总控台快照 聚合系统运行态、信号统计、模拟盘与实盘平台状态,供总控台页面使用。 """ try: monitor = get_system_monitor() summary = monitor.get_summary() now = datetime.now() signal_db = get_signal_db_service() signal_stats = signal_db.get_signal_stats(days=7) latest_signals = signal_db.get_crypto_signals(limit=12, days=3) crypto_agent = get_crypto_agent() crypto_status = crypto_agent.get_status() paper_service = get_paper_trading_service() paper_account = paper_service.get_account_status() paper_orders = paper_service.get_active_orders() paper_positions = [o for o in paper_orders if o.get('status') == 'open'] paper_pending = [o for o in paper_orders if o.get('status') == 'pending'] paper_stats = paper_service.calculate_statistics() bitget_services = get_all_bitget_live_services() if not bitget_services: default_bitget = get_bitget_live_service() if default_bitget: bitget_services = {"default": default_bitget} bitget_accounts = [] for account_id, service in bitget_services.items(): try: bitget_accounts.append(_build_bitget_account_summary(account_id, service)) except Exception as exc: logger.error(f"获取 Bitget 账号摘要失败: account={account_id} error={exc}") if bitget_accounts: total_account_value = sum(item["account"]["account_value"] for item in bitget_accounts) total_available_balance = sum(item["account"]["available_balance"] for item in bitget_accounts) total_margin_used = sum(item["account"]["total_margin_used"] for item in bitget_accounts) total_positions_count = sum(item["positions"]["count"] for item in bitget_accounts) total_position_value = sum(item["positions"]["total_value"] for item in bitget_accounts) total_orders_count = sum(item["orders"]["count"] for item in bitget_accounts) total_entry_orders = sum(item["orders"]["entry_orders"] for item in bitget_accounts) total_tp_sl_orders = sum(item["orders"]["tp_sl_orders"] for item in bitget_accounts) leverage_weight = total_account_value if total_account_value > 0 else len(bitget_accounts) weighted_drawdown = sum( item["risk"]["drawdown_percent"] * ( item["account"]["account_value"] if total_account_value > 0 else 1 ) for item in bitget_accounts ) / leverage_weight if leverage_weight > 0 else 0 max_leverage = max((item["risk"]["max_leverage"] for item in bitget_accounts), default=0) breaker_threshold = max((item["risk"]["circuit_breaker_threshold"] for item in bitget_accounts), default=0) bitget_summary = { "enabled": True, "accounts": bitget_accounts, "account": { "account_value": total_account_value, "available_balance": total_available_balance, "total_margin_used": total_margin_used, }, "positions": { "count": total_positions_count, "total_value": total_position_value, "items": [item for account in bitget_accounts for item in account["positions"]["items"]][:12], }, "orders": { "count": total_orders_count, "entry_orders": total_entry_orders, "tp_sl_orders": total_tp_sl_orders, "items": [item for account in bitget_accounts for item in account["orders"]["items"]][:12], }, "risk": { "current_leverage": total_position_value / total_account_value if total_account_value > 0 else 0, "max_leverage": max_leverage, "drawdown_percent": weighted_drawdown, "circuit_breaker_threshold": breaker_threshold, }, } else: bitget_summary = {"enabled": False, "accounts": []} recent_cutoff = now - timedelta(minutes=30) recent_signal_count = sum( 1 for signal in latest_signals if (_parse_signal_timestamp(signal.get("created_at")) or datetime.min) >= recent_cutoff ) paper_position_items = [ _normalize_platform_position("paper", pos) for pos in paper_service.get_open_positions()[:12] ] paper_order_items = [ _normalize_platform_order("paper", order) for order in paper_pending[:12] ] bitget_position_items = [] bitget_order_items = [] for account in bitget_accounts: for pos in account["positions"]["items"][:12]: normalized = _normalize_platform_position("bitget", pos) normalized["account_id"] = account["account_id"] bitget_position_items.append(normalized) for order in account["orders"]["items"][:12]: enriched_order = dict(order) enriched_order["account_id"] = account["account_id"] bitget_order_items.append(_normalize_platform_order("bitget", enriched_order)) unified_positions = sorted( paper_position_items + bitget_position_items, key=lambda item: _parse_signal_timestamp(item.get("opened_at")) or datetime.min, reverse=True, ) unified_orders = sorted( paper_order_items + bitget_order_items, key=lambda item: _parse_signal_timestamp(item.get("created_at")) or datetime.min, reverse=True, ) platforms_payload = { "paper": { "enabled": True, "account": paper_account, "positions": { "count": len(paper_positions), "items": paper_positions[:8], }, "orders": { "count": len(paper_orders), "pending_count": len(paper_pending), "items": paper_pending[:8], }, "statistics": { "win_rate": paper_stats.get("win_rate", 0), "total_trades": paper_stats.get("total_trades", 0), "total_pnl": paper_stats.get("total_pnl", 0), "max_drawdown": paper_stats.get("max_drawdown", 0), "by_grade": paper_stats.get("by_grade", {}), }, "risk": { "current_leverage": paper_account.get("current_total_leverage", 0), "max_leverage": paper_account.get("max_total_leverage", 0), "drawdown_percent": paper_account.get("max_drawdown", 0), "circuit_breaker_threshold": 25, }, }, "bitget": bitget_summary, } execution_events = crypto_agent.get_recent_execution_events(limit=40) attention_items = _build_attention_items( crypto_status.get("platform_halts", {}), platforms_payload, unified_positions, unified_orders, execution_events, ) payload = { "status": "success", "data": { "generated_at": now.isoformat(), "system": summary, "crypto_agent": crypto_status, "execution_events": execution_events, "signals": { "stats_7d": { "crypto": signal_stats.get("crypto", {"total": 0, "buy": 0, "sell": 0, "recent_24h": 0}), "grades": signal_stats.get("grades", {}), "total": signal_stats.get("crypto", {}).get("total", 0), }, "latest": latest_signals, "recent_30m_count": recent_signal_count, }, "platforms": platforms_payload, "management": { "positions": unified_positions[:18], "orders": unified_orders[:24], "attention_items": attention_items, }, } } return _sanitize_for_response(payload) except Exception as e: logger.error(f"获取总控台快照失败: {e}") raise HTTPException(status_code=500, detail=f"获取总控台快照失败: {str(e)}")