"""中控数据看板:四户当日总览(无 AI,纯数据聚合)。""" from __future__ import annotations from datetime import datetime, timezone from typing import Any, Optional from hub_ai.context import ( build_daily_context, collect_closed_trades_snapshot, format_account_remark, ) from hub_ai.config import trading_day_reset_hour from hub_trades_lib import current_trading_day LOSS_ALERT_PCT = 5.0 DASHBOARD_POLL_INTERVAL_SEC = 60 def _safe_float(v: Any) -> Optional[float]: try: if v is None or v == "": return None return float(v) except (TypeError, ValueError): return None def _account_capital_base(ac: dict) -> Optional[float]: funding = _safe_float(ac.get("funding_usdt")) trading = _safe_float(ac.get("trading_usdt")) if funding is not None and trading is not None: return funding + trading if funding is not None: return funding if trading is not None: return trading return None def _enrich_account_row(ac: dict) -> dict: st = ac.get("trade_stats") or {} capital = _account_capital_base(ac) day_pnl = float(st.get("total_pnl_u") or 0) loss_pct: Optional[float] = None loss_alert = False if capital is not None and capital > 0 and day_pnl < -1e-9: loss_pct = round(abs(day_pnl) / capital * 100.0, 2) loss_alert = loss_pct >= LOSS_ALERT_PCT return { "id": ac.get("id"), "key": ac.get("key"), "name": ac.get("name"), "status": ac.get("status"), "monitored": ac.get("status") != "未监控", "funding_usdt": ac.get("funding_usdt"), "trading_usdt": ac.get("trading_usdt"), "capital_total_usdt": round(capital, 4) if capital is not None else None, "available_trading_usdt": ac.get("available_trading_usdt"), "pnl_u": st.get("total_pnl_u"), "closed_count": st.get("closed_count"), "win_count": st.get("win_count"), "loss_count": st.get("loss_count"), "float_pnl_u": ac.get("float_pnl_u"), "open_position_count": ac.get("open_position_count"), "remark": format_account_remark(ac), "issues": ac.get("issues") or [], "daily_loss_pct": loss_pct, "loss_alert": loss_alert, } def build_dashboard_payload( exchanges: list[dict], *, trading_day: str | None = None, ) -> dict[str, Any]: ctx = build_daily_context(exchanges, trading_day=trading_day) day = ctx["trading_day"] accounts_raw = ctx.get("accounts") or [] accounts = [_enrich_account_row(ac) for ac in accounts_raw] closed_trades = collect_closed_trades_snapshot(accounts_raw, today=day) loss_alert_count = sum(1 for ac in accounts if ac.get("loss_alert")) now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S") return { "ok": True, "updated_at": now, "trading_day": day, "totals": ctx.get("totals"), "accounts": accounts, "closed_trades": closed_trades, "loss_alert_pct_threshold": LOSS_ALERT_PCT, "loss_alert_count": loss_alert_count, "poll_interval_sec": DASHBOARD_POLL_INTERVAL_SEC, } def default_trading_day() -> str: return current_trading_day(reset_hour=trading_day_reset_hour())