feat: add hub fund overview tab with 180-day equity curves

Add /funds page for total and per-account balance (funding+trading), drawdown, and daily snapshots from monitor board aggregation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 16:50:47 +08:00
parent 6eb17b7ddc
commit 77c7bbbb13
14 changed files with 1069 additions and 112 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ CHAT_TEMPERATURE = 0.5
CHAT_MAX_HISTORY_TURNS = 20
SUMMARY_RETENTION_DAYS = 90
CHAT_SESSION_RETENTION_DAYS = 60
FUND_HISTORY_DAYS = 15
FUND_HISTORY_DAYS = 180
CHAT_MAX_ATTACHMENTS = 3
CHAT_MAX_IMAGE_BYTES = 4 * 1024 * 1024
CHAT_MAX_TEXT_FILE_BYTES = 200 * 1024
+8 -1
View File
@@ -391,7 +391,14 @@ def build_daily_context(
"total_funding_usdt": round(total_funding, 4) if total_funding is not None else None,
"total_trading_usdt": round(total_trading, 4) if total_trading is not None else None,
}
record_fund_snapshot(day, accounts, keep_days=FUND_HISTORY_DAYS)
snap_accounts = [
{
**ac,
"monitored": ac.get("status") != "未监控",
}
for ac in accounts
]
record_fund_snapshot(day, snap_accounts, keep_days=FUND_HISTORY_DAYS)
fund_history = get_fund_history(anchor_day=day, keep_days=FUND_HISTORY_DAYS)
account_names = {str(ac.get("key") or ac.get("id")): ac.get("name") for ac in accounts}
fund_history_text = format_fund_history_text(fund_history, account_names=account_names)
+13 -104
View File
@@ -1,109 +1,18 @@
"""中控 AI:分户资金快照(保留 15 天,供总结/聊天上下文)。"""
"""中控 AI:分户资金快照(委托 hub_fund_history_lib,保留 180 交易日)。"""
from __future__ import annotations
import json
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Optional
from hub_ai.config import FUND_HISTORY_DAYS
from hub_fund_history_lib import (
FUND_HISTORY_DAYS,
format_fund_history_text,
get_fund_history,
record_fund_snapshot,
)
HUB_DIR = Path(__file__).resolve().parent.parent
FUND_HISTORY_PATH = HUB_DIR / "hub_ai_fund_history.json"
def _now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _atomic_write(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
os.replace(tmp, path)
def _load_store() -> dict:
if not FUND_HISTORY_PATH.is_file():
return {"version": 1, "days": {}}
try:
loaded = json.loads(FUND_HISTORY_PATH.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
loaded.setdefault("version", 1)
loaded.setdefault("days", {})
return loaded
except Exception:
pass
return {"version": 1, "days": {}}
def _prune_days(days: dict, *, keep_days: int, anchor_day: str) -> dict:
try:
anchor = datetime.strptime(anchor_day[:10], "%Y-%m-%d")
except ValueError:
anchor = datetime.now()
cutoff = (anchor - timedelta(days=max(1, keep_days) - 1)).strftime("%Y-%m-%d")
out = {k: v for k, v in (days or {}).items() if str(k) >= cutoff}
return out
def record_fund_snapshot(
trading_day: str,
accounts: list[dict],
*,
keep_days: int = FUND_HISTORY_DAYS,
) -> dict[str, Any]:
"""写入当日各户资金账户/交易账户余额,并裁剪历史。"""
day = (trading_day or "").strip()[:10]
if not day:
return {}
store = _load_store()
days = dict(store.get("days") or {})
row_accounts: dict[str, dict] = {}
for ac in accounts or []:
key = str(ac.get("key") or ac.get("id") or "")
if not key:
continue
row_accounts[key] = {
"name": ac.get("name"),
"funding_usdt": ac.get("funding_usdt"),
"trading_usdt": ac.get("trading_usdt"),
"recorded_at": _now_str(),
}
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
days = _prune_days(days, keep_days=keep_days, anchor_day=day)
_atomic_write(FUND_HISTORY_PATH, {"version": 1, "days": days})
return days
def get_fund_history(*, anchor_day: str, keep_days: int = FUND_HISTORY_DAYS) -> dict[str, dict]:
store = _load_store()
days = _prune_days(
dict(store.get("days") or {}),
keep_days=keep_days,
anchor_day=anchor_day,
)
return days
def format_fund_history_text(history: dict[str, dict], *, account_names: Optional[dict[str, str]] = None) -> str:
if not history:
return "(暂无资金历史快照)"
names = account_names or {}
lines = ["【近15日资金快照(资金账户 / 交易账户 USDT)】"]
for day in sorted(history.keys()):
block = history.get(day) or {}
ac_map = block.get("accounts") or {}
if not ac_map:
continue
parts = []
for key, ac in ac_map.items():
label = names.get(key) or ac.get("name") or key
fu = ac.get("funding_usdt")
tu = ac.get("trading_usdt")
fu_txt = f"{fu}U" if fu is not None else "未知"
tu_txt = f"{tu}U" if tu is not None else "未知"
parts.append(f"{label}: 资金{fu_txt} / 交易{tu_txt}")
lines.append(f"- {day}: " + "".join(parts))
return "\n".join(lines) if len(lines) > 1 else "(暂无资金历史快照)"
__all__ = [
"FUND_HISTORY_DAYS",
"format_fund_history_text",
"get_fund_history",
"record_fund_snapshot",
]