From 717a3d5694ffea98ddc9a65408e4a348fb3d4281 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 16 Jul 2026 00:19:43 +0800 Subject: [PATCH] Add cumulative P&L banner to fund overview for at-a-glance profit/loss. Expose period delta from equity curve start and surface it in the toolbar and summary cards with green/red cues. Co-authored-by: Cursor --- lib/hub/hub_fund_history_lib.py | 32 ++++++++ manual_trading_hub/static/app.css | 106 ++++++++++++++++++++++++++- manual_trading_hub/static/funds.js | 49 +++++++++++++ manual_trading_hub/static/index.html | 20 ++++- manual_trading_hub/资金概况说明.md | 5 +- tests/test_hub_fund_history_lib.py | 15 ++++ 6 files changed, 223 insertions(+), 4 deletions(-) diff --git a/lib/hub/hub_fund_history_lib.py b/lib/hub/hub_fund_history_lib.py index 09d573b..f38a3af 100644 --- a/lib/hub/hub_fund_history_lib.py +++ b/lib/hub/hub_fund_history_lib.py @@ -74,6 +74,30 @@ def compute_drawdown(values: list[float]) -> dict[str, Any]: } +def compute_period_delta(series: list[dict]) -> dict[str, Any]: + """相对曲线起点的资金变动(U 与 %);含出入金影响,口径同权益曲线.""" + pts = [ + p + for p in (series or []) + if isinstance(p, dict) and isinstance(p.get("total_usdt"), (int, float)) + ] + if not pts: + return { + "start_usdt": None, + "period_delta_usdt": None, + "period_delta_pct": None, + } + start = round(float(pts[0]["total_usdt"]), 4) + end = round(float(pts[-1]["total_usdt"]), 4) + delta = round(end - start, 4) + pct = round((delta / start) * 100, 2) if start > 0 else None + return { + "start_usdt": start, + "period_delta_usdt": delta, + "period_delta_pct": pct, + } + + def _atomic_write(path: Path, data: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) tmp = path.with_suffix(path.suffix + ".tmp") @@ -343,6 +367,7 @@ def build_fund_overview( day_delta = round(series[-1]["total_usdt"] - series[-2]["total_usdt"], 4) elif data_ok and total is not None: day_delta = round(total - series[-1]["total_usdt"], 4) + period = compute_period_delta(series) accounts_out.append( { @@ -361,6 +386,9 @@ def build_fund_overview( "series": series, "drawdown": dd, "day_delta_usdt": day_delta, + "start_usdt": period["start_usdt"], + "period_delta_usdt": period["period_delta_usdt"], + "period_delta_pct": period["period_delta_pct"], } ) if key: @@ -387,6 +415,7 @@ def build_fund_overview( total_day_delta = round( total_series[-1]["total_usdt"] - total_series[-2]["total_usdt"], 4 ) + total_period = compute_period_delta(total_series) return { "ok": True, @@ -400,6 +429,9 @@ def build_fund_overview( "live_known_count": live_known, "total_usdt": round(live_total, 4) if live_known > 0 else None, "day_delta_usdt": total_day_delta, + "start_usdt": total_period["start_usdt"], + "period_delta_usdt": total_period["period_delta_usdt"], + "period_delta_pct": total_period["period_delta_pct"], "series": total_series, "drawdown": total_dd, }, diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index e8f0cea..014e38a 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -7266,6 +7266,10 @@ html[data-theme="light"] .funds-stage { 50% { opacity: 0.55; transform: scale(0.88); } } .funds-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 12px; margin-bottom: 14px; } .funds-btn-refresh { @@ -7273,15 +7277,115 @@ html[data-theme="light"] .funds-stage { letter-spacing: 0.06em; font-size: 0.72rem; } +.funds-pnl-banner { + flex: 1 1 240px; + min-width: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px 16px; + padding: 10px 14px; + border-radius: var(--radius); + border: 1px solid var(--border-soft); + background: rgba(0, 0, 0, 0.28); +} +html[data-theme="light"] .funds-pnl-banner { + background: rgba(255, 255, 255, 0.82); +} +.funds-pnl-banner.is-pos { + border-color: color-mix(in srgb, var(--green) 45%, var(--border-soft)); + background: color-mix(in srgb, var(--green) 10%, rgba(0, 0, 0, 0.28)); + box-shadow: inset 0 0 28px color-mix(in srgb, var(--green) 8%, transparent); +} +.funds-pnl-banner.is-neg { + border-color: color-mix(in srgb, var(--red) 45%, var(--border-soft)); + background: color-mix(in srgb, var(--red) 10%, rgba(0, 0, 0, 0.28)); + box-shadow: inset 0 0 28px color-mix(in srgb, var(--red) 8%, transparent); +} +html[data-theme="light"] .funds-pnl-banner.is-pos { + background: color-mix(in srgb, var(--green) 8%, rgba(255, 255, 255, 0.9)); +} +html[data-theme="light"] .funds-pnl-banner.is-neg { + background: color-mix(in srgb, var(--red) 8%, rgba(255, 255, 255, 0.9)); +} +.funds-pnl-main { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px 12px; + min-width: 0; +} +.funds-pnl-label { + font-family: var(--display); + font-size: 0.62rem; + letter-spacing: 0.12em; + color: var(--muted); + text-transform: uppercase; +} +.funds-pnl-value { + font-family: var(--font); + font-variant-numeric: tabular-nums; + font-size: 1.35rem; + font-weight: 700; + letter-spacing: 0.01em; + line-height: 1.1; +} +.funds-pnl-pct { + font-family: var(--mono); + font-size: 0.85rem; + font-weight: 600; + opacity: 0.92; +} +.funds-pnl-value.pos, +.funds-pnl-pct.pos, +.funds-pnl-side-val.pos { + color: var(--green); +} +.funds-pnl-value.neg, +.funds-pnl-pct.neg, +.funds-pnl-side-val.neg { + color: var(--red); +} +.funds-pnl-side { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + padding-left: 12px; + border-left: 1px solid var(--border-soft); +} +.funds-pnl-side-label { + font-family: var(--display); + font-size: 0.58rem; + letter-spacing: 0.1em; + color: var(--muted); + text-transform: uppercase; +} +.funds-pnl-side-val { + font-family: var(--font); + font-variant-numeric: tabular-nums; + font-size: 0.95rem; + font-weight: 600; +} .funds-status.err { color: var(--red); } .funds-summary { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 12px; } +.funds-stat-card-pnl .funds-stat-val { + font-size: 1.35rem; +} +.funds-stat-sub { + margin-top: 4px; + font-size: 0.72rem; + font-family: var(--mono); + color: var(--muted); +} .funds-stat-card { position: relative; background: rgba(0, 0, 0, 0.28); diff --git a/manual_trading_hub/static/funds.js b/manual_trading_hub/static/funds.js index e101824..77cf5f7 100644 --- a/manual_trading_hub/static/funds.js +++ b/manual_trading_hub/static/funds.js @@ -10,6 +10,12 @@ const elDdU = document.getElementById("funds-total-dd-u"); const elDdPct = document.getElementById("funds-total-dd-pct"); const elDelta = document.getElementById("funds-total-delta"); + const elPeriod = document.getElementById("funds-total-period"); + const elPeriodSub = document.getElementById("funds-total-period-sub"); + const elPeriodBanner = document.getElementById("funds-period-delta"); + const elPeriodPct = document.getElementById("funds-period-pct"); + const elDayChip = document.getElementById("funds-day-chip"); + const elPnlBanner = document.getElementById("funds-pnl-banner"); const elMeta = document.getElementById("funds-meta"); const elDescBody = document.getElementById("funds-desc-body"); const elChartSub = document.getElementById("funds-chart-sub"); @@ -50,6 +56,13 @@ return sign + v.toFixed(2) + " U"; } + function fmtPct(n) { + if (n == null || !Number.isFinite(Number(n))) return "—"; + const v = Number(n); + const sign = v > 0 ? "+" : ""; + return sign + v.toFixed(2) + "%"; + } + function deltaClass(n) { if (!Number.isFinite(Number(n))) return ""; if (Number(n) > 0) return "pos"; @@ -193,6 +206,8 @@ const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—"; const deltaCls = deltaClass(ac.day_delta_usdt); const deltaText = monitored ? fmtDelta(ac.day_delta_usdt) : "—"; + const periodCls = deltaClass(ac.period_delta_usdt); + const periodText = monitored ? fmtDelta(ac.period_delta_usdt) : "—"; return ( ' +
+
+ 累计盈亏 · P&L + + +
+
+ 较昨日 + +
+
@@ -632,6 +643,11 @@
总资金 · TOTAL
+
+
累计盈亏 · SINCE START
+
+
相对统计起点
+
较昨日 · Δ24H
@@ -1301,7 +1317,7 @@ - + diff --git a/manual_trading_hub/资金概况说明.md b/manual_trading_hub/资金概况说明.md index 245604f..aad7a0f 100644 --- a/manual_trading_hub/资金概况说明.md +++ b/manual_trading_hub/资金概况说明.md @@ -27,6 +27,7 @@ ### 总览 - **总资金**:当前监控板最新一轮聚合的实时合计(资金户+交易户齐全才计入) +- **累计盈亏**:相对统计起点(`HUB_FUND_HISTORY_START_DAY`)首个快照的总资金变动(U / %);顶栏大字绿涨红跌,一眼可看盈亏。含出入金影响,口径同权益曲线,不含浮盈 - **较昨日**:相对上一交易日快照点的变动(U) - **最大回撤**:总资金历史曲线的峰值回撤(U / %) - **总资金曲线**:近 180 交易日 @@ -78,9 +79,11 @@ 返回字段概要: - `totals.total_usdt` — 当前总资金 +- `totals.day_delta_usdt` — 较昨日变动 +- `totals.period_delta_usdt` / `period_delta_pct` / `start_usdt` — 相对曲线起点累计盈亏 - `totals.series[]` — `{ day, total_usdt }` 总曲线 - `totals.drawdown` — `{ peak_usdt, max_drawdown_u, max_drawdown_pct }` -- `accounts[]` — 分户实时余额,曲线,回撤,`monitored` 标记 +- `accounts[]` — 分户实时余额,曲线,回撤,日/累计变动,`monitored` 标记 --- diff --git a/tests/test_hub_fund_history_lib.py b/tests/test_hub_fund_history_lib.py index 732495d..94c0596 100644 --- a/tests/test_hub_fund_history_lib.py +++ b/tests/test_hub_fund_history_lib.py @@ -5,6 +5,7 @@ from lib.hub.hub_fund_history_lib import ( account_total_usdt, build_fund_overview, compute_drawdown, + compute_period_delta, get_fund_history, record_fund_snapshot, ) @@ -23,6 +24,20 @@ def test_compute_drawdown(): assert dd["max_drawdown_pct"] == 25.0 +def test_compute_period_delta(): + out = compute_period_delta( + [ + {"day": "2026-06-09", "total_usdt": 100}, + {"day": "2026-06-10", "total_usdt": 112.5}, + ] + ) + assert out["start_usdt"] == 100.0 + assert out["period_delta_usdt"] == 12.5 + assert out["period_delta_pct"] == 12.5 + empty = compute_period_delta([]) + assert empty["period_delta_usdt"] is None + + def test_build_fund_overview_skips_unmonitored(tmp_path, monkeypatch): hist_path = tmp_path / "hub_fund_history.json" monkeypatch.setattr("hub_fund_history_lib.FUND_HISTORY_PATH", hist_path)