""" 系统状态 API """ from datetime import datetime, timedelta from fastapi import APIRouter, HTTPException, Depends from typing import Dict, Any import numpy as np from app.config import get_settings from app.middleware.console_auth import require_console_access 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 from app.services.price_monitor_service import get_price_monitor_service from app.services.runtime_status_service import get_runtime_status 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_contract_symbol(symbol: Any) -> str: text = str(symbol or "").strip().upper() if not text: return "-" if text.endswith("USDTUSDT"): return text[:-4] if text.endswith("/USDT:USDT"): return f"{text.split('/')[0]}USDT" if text.endswith("USDT"): return text if "/" in text: return f"{text.split('/')[0]}USDT" return f"{text}USDT" 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 = _normalize_contract_symbol(position.get("symbol") or position.get("coin") or "-") 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")) if margin <= 0 and entry_price > 0 and size > 0 and leverage > 0: margin = (entry_price * size) / leverage 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"), "setup_type": position.get("setup_type"), "setup_basis": position.get("setup_basis"), "entry_basis": position.get("entry_basis"), "opened_at": position.get("opened_at") or position.get("created_at"), "protection": position.get("protection"), } def _apply_live_position_metrics(position: Dict[str, Any], latest_price: float | None) -> Dict[str, Any]: price = _safe_float(latest_price) if price <= 0: return position entry_price = _safe_float(position.get("entry_price")) size = abs(_safe_float(position.get("size"))) side = str(position.get("side") or "").lower() margin = _safe_float(position.get("margin")) leverage = _safe_float(position.get("leverage")) if entry_price <= 0 or size <= 0: position["mark_price"] = price return position direction = 1 if side == "long" else -1 pnl_percent_unlevered = ((price - entry_price) / entry_price) * 100 * direction if str(position.get("platform")) == "paper": notional = _safe_float(position.get("size")) unrealized_pnl = notional * pnl_percent_unlevered / 100 position["unrealized_pnl"] = round(unrealized_pnl, 2) position["pnl_percent"] = round(pnl_percent_unlevered, 4) else: unrealized_pnl = (price - entry_price) * size * direction if margin <= 0 and leverage > 0: margin = (entry_price * size) / leverage position["margin"] = round(margin, 8) leveraged_pnl_pct = (unrealized_pnl / margin * 100) if margin > 0 else ( pnl_percent_unlevered * leverage if leverage > 0 else pnl_percent_unlevered ) position["unrealized_pnl"] = round(unrealized_pnl, 8) position["pnl_percent"] = round(leveraged_pnl_pct, 4) position["mark_price"] = price return position 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 = _normalize_contract_symbol(order.get("symbol") or order.get("coin") or "-") 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"), "setup_type": order.get("setup_type"), "setup_basis": order.get("setup_basis"), "entry_basis": order.get("entry_basis"), "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(_: dict = Depends(require_console_access)): """ 获取总控台快照 聚合系统运行态、信号统计、模拟盘与实盘平台状态,供总控台页面使用。 """ 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 ) price_monitor = get_price_monitor_service() configured_symbols = [symbol.strip().upper() for symbol in (get_settings().crypto_symbols or "").split(",") if symbol.strip()] for symbol in configured_symbols: price_monitor.subscribe_symbol(symbol) raw_paper_positions = paper_service.get_open_positions()[:12] for pos in raw_paper_positions: symbol = _normalize_contract_symbol(pos.get("symbol") or pos.get("coin") or "-") if symbol and symbol != "-": price_monitor.subscribe_symbol(symbol) paper_position_items = [] for pos in raw_paper_positions: normalized = _normalize_platform_position("paper", pos) live_price = price_monitor.get_latest_price(normalized.get("symbol", "")) paper_position_items.append(_apply_live_position_metrics(normalized, live_price)) 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: protection_state_map = {} bitget_executor = getattr(crypto_agent, "bitget_executors", {}).get(account["account_id"]) if bitget_executor and hasattr(bitget_executor, "export_position_protection_state"): protection_state_map = bitget_executor.export_position_protection_state() for pos in account["positions"]["items"][:12]: normalized = _normalize_platform_position("bitget", pos) if normalized.get("symbol") and normalized.get("symbol") != "-": price_monitor.subscribe_symbol(normalized["symbol"]) live_price = price_monitor.get_latest_price(normalized.get("symbol", "")) normalized = _apply_live_position_metrics(normalized, live_price) normalized["account_id"] = account["account_id"] state_key = f"{account['account_id']}:{normalized.get('symbol', '').upper()}:{normalized.get('side', '')}:{float(normalized.get('entry_price', 0) or 0):.8f}" if state_key in protection_state_map: normalized["protection"] = protection_state_map[state_key] 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, "monitoring": { "price_monitor": { "running": price_monitor.is_running(), "mode": "websocket" if getattr(price_monitor, "_use_websocket", False) else "polling", "subscribed_symbols": price_monitor.get_subscribed_symbols(), "latest_prices": price_monitor.get_all_prices(), "checked_at": now.isoformat(), }, "execution_loop": get_runtime_status("price_monitor_loop"), }, "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)}")