feat(live): B4前端三层展示+预算表单+账户实况端点(spec§B4)——①新建实盘表单:起始资金升级『实例预算』,默认值=剩余可分配(budget-info,用户改过不跟随),占用率进度条(已分配+本次 vs 现金),超限红条+禁提交,快照不可用警告条(与后端fail-closed双保险)②监控页三层重构:本策略买卖(live_trades归因,今日/全部)/本策略账本+持仓(实例视图,现金/市值/净值语义重命名)/账户实况(新卡:现金/市值/总资产+Σ实例市值+未归因市值=账户市值−Σ实例+实例分解表+QMT全账户持仓表)③列表页:每QMT账号一条占用率横幅,存量超限亮黄条⚠④后端补GET /live/account-snapshot(注册在{aid}前):全局快照+同账号实例最新账本分解+unattributed_mv,快照缺失fresh=False数值None;⑤live.ts补BudgetInfo/AccountSnapshotView类型+updateLive带initial_capital;+3端点测试(分解算术/无快照降级/跨账号过滤);api 164绿;npm run build绿 [vps]
CI/CD / test (push) Successful in 15s
CI/CD / nas-deploy (push) Successful in 1m24s
CI/CD / nas-verify (push) Successful in 15s

This commit is contained in:
2026-08-19 22:26:41 +08:00
parent 5a91be2efd
commit 537fd2d571
6 changed files with 431 additions and 18 deletions
+51
View File
@@ -258,6 +258,57 @@ def get_budget_info(account: str = ""):
return _budget_state(db, account)
@router.get("/live/account-snapshot", dependencies=[Depends(verify_token)])
def get_account_snapshot_route(account: str = ""):
"""账户实况(B4 三层展示第三层):全局快照 + Σ实例市值分解。
instances = 同 QMT 账号各实盘实例的最新账本(live_balance 实例视图);
unattributed = 全账户市值 − Σ实例市值(重建后应≈0,大数=遗留仓/手动仓)。
快照缺失时 fresh=False、数值字段 None(前端显示『快照不可用』)。
"""
from sanguo_live.persistence import (
get_account_snapshot, list_accounts, get_last_balance,
)
db = _db_path["path"]
if not db:
raise HTTPException(400, "db 未配置")
acc = (account or "").strip()
snap = get_account_snapshot(db, acc) if acc else None
from sanguo_live.persistence import get_fresh_account_snapshot
fresh = get_fresh_account_snapshot(db, acc) is not None if acc else False
instances = []
for row in list_accounts(db):
if (row.get("account") or "").strip() != acc:
continue
last = get_last_balance(db, row["id"])
instances.append({
"id": row["id"], "name": row.get("name") or "",
"status": row.get("status") or "",
"initial_capital": float(row.get("initial_capital") or 0),
"cash": (last or {}).get("cash"),
"market_value": (last or {}).get("market_value"),
"equity": (last or {}).get("total"),
})
instance_mv = sum(
float(i["market_value"] or 0) for i in instances)
snap_mv = float(snap.get("market_value")) if snap else None
return {
"account": acc,
"fresh": fresh,
"cash": (snap or {}).get("cash"),
"market_value": snap_mv,
"total": (snap or {}).get("total"),
"positions": (snap or {}).get("positions") or [],
"updated_at": (snap or {}).get("updated_at"),
"instances": instances,
"instance_mv_total": instance_mv,
"unattributed_mv": (snap_mv - instance_mv
if snap_mv is not None else None),
}
@router.get("/live/{aid}", dependencies=[Depends(verify_token)])
def get_live(aid: int):
from sanguo_live.persistence import get_account, get_first_balance, get_last_balance