"""Account-centric read model for live trading console.""" from __future__ import annotations from app.db.live_trading import _safe_float, get_live_account, list_live_order_events, list_live_order_intents from app.integrations.binance_live import LiveTradingConfigError, build_binance_client _ACCOUNT_OVERVIEW_CACHE: dict[int, dict] = {} def _compact_balance(balance: dict) -> dict: total = balance.get("total") if isinstance(balance.get("total"), dict) else {} free = balance.get("free") if isinstance(balance.get("free"), dict) else {} used = balance.get("used") if isinstance(balance.get("used"), dict) else {} assets = [] for asset in sorted(set(total) | set(free) | set(used)): total_value = _safe_float(total.get(asset)) free_value = _safe_float(free.get(asset)) used_value = _safe_float(used.get(asset)) if abs(total_value) > 0 or abs(free_value) > 0 or abs(used_value) > 0: assets.append({"asset": asset, "free": free_value, "used": used_value, "total": total_value}) return { "assets": assets, "usdt": { "free": _safe_float(free.get("USDT")), "used": _safe_float(used.get("USDT")), "total": _safe_float(total.get("USDT")), }, } def _position_side_label(side: str) -> str: side = str(side or "").strip().lower() if side in {"long", "buy"}: return "多" if side in {"short", "sell"}: return "空" return "--" def _compact_position(item: dict, account: dict | None = None) -> dict: info = item.get("info") if isinstance(item.get("info"), dict) else {} contracts = _safe_float(item.get("contracts") or info.get("positionAmt")) notional = _safe_float(item.get("notional") or info.get("notional")) entry_price = _safe_float(item.get("entryPrice") or info.get("entryPrice")) mark_price = _safe_float(item.get("markPrice") or info.get("markPrice")) position_value = abs(notional) if position_value <= 0 and abs(contracts) > 0 and mark_price > 0: position_value = abs(contracts) * mark_price margin = _safe_float( item.get("initialMargin") or item.get("collateral") or info.get("initialMargin") or info.get("positionInitialMargin") or info.get("isolatedWallet") ) leverage = _safe_float(item.get("leverage") or info.get("leverage")) leverage_source = "exchange" if leverage <= 0 and position_value > 0 and margin > 0: leverage = position_value / margin leverage_source = "computed" if leverage <= 0 and account: risk = account.get("risk_config") if isinstance(account.get("risk_config"), dict) else {} leverage = _safe_float(risk.get("max_symbol_leverage"), 0) leverage_source = "account_config" if leverage > 0 else "missing" side = item.get("side") or ("long" if contracts > 0 else ("short" if contracts < 0 else "")) return { "symbol": item.get("symbol") or info.get("symbol"), "side": side, "side_label": _position_side_label(side), "contracts": abs(contracts), "entry_price": entry_price, "mark_price": mark_price, "notional": notional, "position_value_usdt": position_value, "margin_usdt": margin, "unrealized_pnl": _safe_float(item.get("unrealizedPnl") or info.get("unrealizedProfit")), "leverage": leverage, "leverage_source": leverage_source, } def _compact_order(item: dict) -> dict: info = item.get("info") if isinstance(item.get("info"), dict) else {} return { "id": str(item.get("id") or info.get("orderId") or ""), "client_order_id": item.get("clientOrderId") or info.get("clientOrderId") or "", "symbol": item.get("symbol") or info.get("symbol"), "type": item.get("type") or info.get("type"), "side": item.get("side") or info.get("side"), "status": item.get("status") or info.get("status"), "price": _safe_float(item.get("price") or info.get("price")), "amount": _safe_float(item.get("amount") or info.get("origQty")), "filled": _safe_float(item.get("filled") or info.get("executedQty")), "average": _safe_float(item.get("average") or info.get("avgPrice")), "timestamp": item.get("datetime") or item.get("timestamp") or info.get("updateTime") or info.get("time"), } def _normalize_symbol(symbol: str) -> str: value = str(symbol or "").strip().upper() if value and "/" not in value and value.endswith("USDT"): value = value[:-4] + "/USDT" return value def _order_history_symbols(account: dict, overview: dict) -> list[str]: """Build the smallest safe symbol set for Binance order-history queries.""" risk = account.get("risk_config") if isinstance(account.get("risk_config"), dict) else {} symbols: list[str] = [] for raw in risk.get("allowed_symbols") or []: symbol = _normalize_symbol(raw) if symbol: symbols.append(symbol) for row in (overview.get("positions") or []) + (overview.get("open_orders") or []): symbol = _normalize_symbol(row.get("symbol")) if symbol: symbols.append(symbol) for row in overview.get("intent_history") or []: symbol = _normalize_symbol(row.get("symbol")) if symbol: symbols.append(symbol) result = [] seen = set() for symbol in symbols: key = symbol.upper() if key not in seen: seen.add(key) result.append(symbol) return result[:20] def _fetch_order_history_by_symbol(client, symbols: list[str], limit: int) -> tuple[list[dict], list[str]]: orders = [] errors = [] if not symbols: return orders, errors per_symbol_limit = max(1, min(int(limit or 30), 50)) for symbol in symbols: try: orders.extend(_compact_order(o) for o in client.fetch_orders(symbol, limit=per_symbol_limit)) except Exception as exc: errors.append(f"订单历史读取失败 {symbol}:{exc}") orders.sort(key=lambda x: str(x.get("timestamp") or ""), reverse=True) return orders[: max(1, int(limit or 30))], errors def _account_risk_view(account: dict) -> dict: risk = account.get("risk_config") if isinstance(account.get("risk_config"), dict) else {} allowed = [str(x).strip().upper() for x in risk.get("allowed_symbols", []) if str(x).strip()] max_leverage = _safe_float(risk.get("max_symbol_leverage"), 1) margin = _safe_float(risk.get("max_order_margin_usdt"), 0) return { "max_order_margin_usdt": margin, "max_symbol_leverage": max_leverage, "max_order_notional_usdt": _safe_float(risk.get("max_order_notional_usdt"), margin * max(1.0, max_leverage)), "max_cumulative_leverage": _safe_float(risk.get("max_cumulative_leverage"), 1), "max_daily_order_count": int(risk.get("max_daily_order_count") or 0), "allowed_symbols": allowed, "symbol_policy": "all" if not allowed else "allowlist", } def _cache_overview(account_id: int, overview: dict) -> dict: _ACCOUNT_OVERVIEW_CACHE[int(account_id)] = overview return overview def _cached_overview(account_id: int) -> dict | None: item = _ACCOUNT_OVERVIEW_CACHE.get(int(account_id)) if not item: return None cached = dict(item) cached["exchange_cache"] = {**(cached.get("exchange_cache") or {}), "cached": True} return cached def get_live_account_overview(account_id: int, *, history_limit: int = 30, refresh: bool = False, client_factory=None) -> dict: account = get_live_account(account_id) if not account: raise LiveTradingConfigError("live account not found") overview = { "account": account, "risk": _account_risk_view(account), "balance": {"assets": [], "usdt": {"free": 0, "used": 0, "total": 0}}, "positions": [], "open_orders": [], "order_history": [], "intent_history": list_live_order_intents(limit=history_limit, account_id=account_id).get("items", []), "events": list_live_order_events(limit=history_limit).get("items", []), "exchange_cache": {"cached": False, "loaded": False, "requires_refresh": True}, "errors": [], } if account.get("status") != "enabled": return overview if not refresh: cached = _cached_overview(account_id) if cached: cached["account"] = account cached["risk"] = overview["risk"] cached["intent_history"] = overview["intent_history"] cached["events"] = overview["events"] return cached overview["exchange_cache"]["reason"] = "点击刷新交易所数据后读取余额、持仓和订单" return overview try: client = client_factory(account) if client_factory else build_binance_client(account, require_testnet=True) client.load_markets() except Exception as exc: overview["errors"].append(f"账户连接失败:{exc}") return overview try: overview["balance"] = _compact_balance(client.fetch_balance()) except Exception as exc: overview["errors"].append(f"余额读取失败:{exc}") try: overview["positions"] = [ item for item in (_compact_position(p, account) for p in client.fetch_positions(None)) if abs(_safe_float(item.get("contracts"))) > 0 ] except Exception as exc: overview["errors"].append(f"持仓读取失败:{exc}") try: overview["open_orders"] = [_compact_order(o) for o in client.fetch_open_orders(None)] except Exception as exc: overview["errors"].append(f"挂单读取失败:{exc}") symbols = _order_history_symbols(account, overview) order_history, order_history_errors = _fetch_order_history_by_symbol(client, symbols, history_limit) overview["order_history"] = order_history overview["errors"].extend(order_history_errors) overview["exchange_cache"] = {"cached": False, "loaded": True, "requires_refresh": False} return _cache_overview(account_id, overview)