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
+53
View File
@@ -174,3 +174,56 @@ def test_delete_frees_budget(live_db):
aid = _create(live_db, capital=300_000)
rl.delete_live(aid)
assert rl.get_budget_info("66639661")["remaining"] == 1_000_000
# ---------------- account-snapshot(B4 第三层数据源) ----------------
def test_account_snapshot_breakdown(live_db):
"""全局快照 + Σ实例分解:unattributed = 全账户市值 − Σ实例市值。"""
lp.upsert_account_snapshot(
live_db, "66639661", cash=400_000.0, market_value=600_000.0,
total=1_000_000.0,
positions=[{"symbol": "600036.SH", "volume": 1000, "can_use": 1000,
"avg_price": 38.0, "mv": 38_000.0}])
aid = _create(live_db, capital=300_000)
lp.save_balance(live_db, aid, "2026-08-19 15:00:00",
100_000.0, 200_000.0, 300_000.0)
r = rl.get_account_snapshot_route("66639661")
assert r["fresh"] is True
assert r["cash"] == 400_000.0
assert r["market_value"] == 600_000.0
assert len(r["positions"]) == 1
assert len(r["instances"]) == 1
assert r["instances"][0]["market_value"] == 200_000.0
assert r["instance_mv_total"] == 200_000.0
assert r["unattributed_mv"] == 400_000.0
def test_account_snapshot_no_snapshot_all_none(live_db):
"""快照缺失 → fresh=False 数值 None,但实例列表仍返回(有实例无快照)。"""
_snapshot(live_db, cash=1_000_000)
aid = _create(live_db, capital=300_000) # 播种快照后建,再删快照
with sqlite3.connect(live_db) as conn:
conn.execute("DELETE FROM qmt_account_snapshot")
r = rl.get_account_snapshot_route("66639661")
assert r["fresh"] is False
assert r["cash"] is None
assert r["unattributed_mv"] is None
assert len(r["instances"]) == 1
def test_account_snapshot_filters_other_accounts(live_db):
"""实例只归同 QMT 账号——别的账号实例不进分解。"""
_snapshot(live_db, cash=1_000_000)
_snapshot(live_db, account="OTHER", cash=500_000)
aid1 = _create(live_db, capital=100_000)
lp.save_balance(live_db, aid1, "d1", 0, 50_000.0, 50_000.0)
# OTHER 账号的实例
req = rl.LiveCreateRequest(
account="OTHER", strategy_name="p2", strategy_type="portfolio",
strategy_class="all_weather", initial_capital=100_000)
aid2 = rl.create_live(req)["account_id"]
lp.save_balance(live_db, aid2, "d1", 0, 70_000.0, 70_000.0)
r = rl.get_account_snapshot_route("66639661")
assert [i["id"] for i in r["instances"]] == [aid1]
assert r["instance_mv_total"] == 50_000.0