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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
'<button type="button" class="funds-ac-card' +
|
||||
offCls +
|
||||
@@ -227,6 +242,11 @@
|
||||
trading +
|
||||
"</span></div>" +
|
||||
optLine +
|
||||
'<div><span class="k">累计盈亏</span><span class="v ' +
|
||||
periodCls +
|
||||
'">' +
|
||||
periodText +
|
||||
"</span></div>" +
|
||||
'<div><span class="k">较昨日</span><span class="v ' +
|
||||
deltaCls +
|
||||
'">' +
|
||||
@@ -370,6 +390,35 @@
|
||||
elDelta.textContent = fmtDelta(totals.day_delta_usdt);
|
||||
elDelta.className = "funds-stat-val " + deltaClass(totals.day_delta_usdt);
|
||||
}
|
||||
const periodCls = deltaClass(totals.period_delta_usdt);
|
||||
if (elPeriod) {
|
||||
elPeriod.textContent = fmtDelta(totals.period_delta_usdt);
|
||||
elPeriod.className = "funds-stat-val " + periodCls;
|
||||
}
|
||||
if (elPeriodSub) {
|
||||
const startDay = data.history_start_day || "—";
|
||||
const pct = fmtPct(totals.period_delta_pct);
|
||||
elPeriodSub.textContent =
|
||||
pct !== "—"
|
||||
? "自 " + startDay + " · " + pct
|
||||
: "自 " + startDay + " 起相对起点";
|
||||
}
|
||||
if (elPeriodBanner) {
|
||||
elPeriodBanner.textContent = fmtDelta(totals.period_delta_usdt);
|
||||
elPeriodBanner.className = "funds-pnl-value " + periodCls;
|
||||
}
|
||||
if (elPeriodPct) {
|
||||
elPeriodPct.textContent = fmtPct(totals.period_delta_pct);
|
||||
elPeriodPct.className = "funds-pnl-pct " + periodCls;
|
||||
}
|
||||
if (elDayChip) {
|
||||
elDayChip.textContent = fmtDelta(totals.day_delta_usdt);
|
||||
elDayChip.className = "funds-pnl-side-val " + deltaClass(totals.day_delta_usdt);
|
||||
}
|
||||
if (elPnlBanner) {
|
||||
elPnlBanner.className =
|
||||
"funds-pnl-banner" + (periodCls ? " is-" + periodCls : "");
|
||||
}
|
||||
if (elMeta) {
|
||||
const parts = [
|
||||
"交易日 " + (data.trading_day || "—"),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260715-hub-phone-folds" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260716-funds-pnl-banner" />
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
@@ -625,6 +625,17 @@
|
||||
</div>
|
||||
<div class="funds-toolbar toolbar">
|
||||
<button type="button" id="funds-btn-refresh" class="primary funds-btn-refresh">同步快照</button>
|
||||
<div id="funds-pnl-banner" class="funds-pnl-banner" aria-live="polite">
|
||||
<div class="funds-pnl-main">
|
||||
<span class="funds-pnl-label">累计盈亏 · P&L</span>
|
||||
<span id="funds-period-delta" class="funds-pnl-value">—</span>
|
||||
<span id="funds-period-pct" class="funds-pnl-pct">—</span>
|
||||
</div>
|
||||
<div class="funds-pnl-side">
|
||||
<span class="funds-pnl-side-label">较昨日</span>
|
||||
<span id="funds-day-chip" class="funds-pnl-side-val">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="funds-status" class="toolbar-meta funds-status"></span>
|
||||
</div>
|
||||
<section class="funds-summary">
|
||||
@@ -632,6 +643,11 @@
|
||||
<div class="funds-stat-label">总资金 · TOTAL</div>
|
||||
<div id="funds-total-usdt" class="funds-stat-value">—</div>
|
||||
</div>
|
||||
<div class="funds-stat-card funds-stat-card-pnl">
|
||||
<div class="funds-stat-label">累计盈亏 · SINCE START</div>
|
||||
<div id="funds-total-period" class="funds-stat-val">—</div>
|
||||
<div id="funds-total-period-sub" class="funds-stat-sub">相对统计起点</div>
|
||||
</div>
|
||||
<div class="funds-stat-card">
|
||||
<div class="funds-stat-label">较昨日 · Δ24H</div>
|
||||
<div id="funds-total-delta" class="funds-stat-val">—</div>
|
||||
@@ -1301,7 +1317,7 @@
|
||||
<script src="/assets/calculator.js?v=20260715-calc-tabs"></script>
|
||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||
<script src="/assets/archive.js?v=20260710-archive-cum2"></script>
|
||||
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||
<script src="/assets/funds.js?v=20260716-funds-pnl-banner"></script>
|
||||
<script src="/assets/dashboard.js?v=20260708-options-expiry-cd"></script>
|
||||
<script src="/assets/strategy.js?v=3"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
|
||||
@@ -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` 标记
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user