Files
crypto_monitor/manual_trading_hub/hub_dashboard.py
T
dekun c4c8ad172a Label dashboard positions from monitor sources with priority.
Match hedge/roll/trend/order/key monitors for source badges, show option target monitors in green, and refresh the dashboard with the board 5s cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 14:25:36 +08:00

141 lines
4.8 KiB
Python

"""中控数据看板:三户当日总览(无 AI,纯数据聚合)."""
from __future__ import annotations
import os
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,
format_dashboard_account_detail,
)
from hub_ai.config import trading_day_reset_hour
from lib.hub.hub_trades_lib import current_trading_day
LOSS_ALERT_PCT = 5.0
# 与监控区 board 默认 5s 对齐,看板持仓来源跟监控同步.
DASHBOARD_POLL_INTERVAL_SEC = float(os.getenv("DASHBOARD_POLL_INTERVAL_SEC", "5"))
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 _options_layout_enabled(ac: dict) -> bool:
if str(ac.get("key") or "").lower() != "okx":
return False
snap = ac.get("options_snapshot")
if isinstance(snap, dict) and snap.get("enabled") is not False:
return True
return ac.get("options_funding_usdt") is not None or ac.get("options_trading_usdt") is not None
def _perpetual_float_pnl_u(ac: dict) -> Optional[float]:
try:
total = float(ac.get("float_pnl_u") or 0)
except (TypeError, ValueError):
total = 0.0
opt = ac.get("options_float_pnl_u")
if opt is None:
return round(total, 4)
try:
return round(total - float(opt), 4)
except (TypeError, ValueError):
return round(total, 4)
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"),
"perpetual_funding_usdt": ac.get("perpetual_funding_usdt"),
"perpetual_trading_usdt": ac.get("perpetual_trading_usdt"),
"options_funding_usdt": ac.get("options_funding_usdt"),
"options_trading_usdt": ac.get("options_trading_usdt"),
"options_float_pnl_u": ac.get("options_float_pnl_u"),
"options_open_position_count": ac.get("options_open_position_count"),
"options_layout": _options_layout_enabled(ac),
"perpetual_float_pnl_u": _perpetual_float_pnl_u(ac),
"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),
**format_dashboard_account_detail(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
if ac.get("status") != "未监控"
]
closed_trades = collect_closed_trades_snapshot(
[ac for ac in accounts_raw if ac.get("status") != "未监控"],
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())