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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user