feat(web): 对账中心上线——恒等式B5只读端点+前端恒等式区块,双轨对账页升级为对账中心(issue#42-A) [nas]
- API: GET /paper/reconcile/identity?date&refresh——缺省读 supervisor 15:10/23:27
已存 identity_reconcile 行,无存行现算并落库(与 build/save 同套幂等);
路由置于 /paper/{aid} 前,与既有 reconcile 端点同组
- 前端: Reconcile.vue 顶部新增恒等式卡片(账户级红绿/未归因±/占比vs容忍0.5%/
可展开逐票缺口+实例市值拆分),页面与菜单更名为「对账中心」;
api/paper.ts 补 IdentityReport 类型族
- 测试: 绿线(50/10050=0.497%pass)+存行复用+refresh强制重算(200=1.96%红)+
snapshot_missing 如实标注,12/12 绿
- 附: docs/session_prompts/ 两份 session 移交提示词入库(fundamentals 18min→
数据session / 统一ex定寸→策略session,用户08-26拍板)
This commit is contained in:
@@ -240,3 +240,84 @@ def test_reconcile_routes(tmp_path):
|
||||
r3 = c.get("/api/v1/paper/39", headers=_auth(token))
|
||||
assert r3.status_code == 200
|
||||
assert r3.json()["mode"] == "shadow"
|
||||
|
||||
|
||||
def test_identity_reconcile_route(tmp_path):
|
||||
"""GET /paper/reconcile/identity:现算→落库→二次读存行;超容忍标红。"""
|
||||
import sqlite3
|
||||
|
||||
c, token = _client(tmp_path)
|
||||
db = os.path.join(str(tmp_path), "p.db")
|
||||
|
||||
def _seed(snap_mv: float) -> None:
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute("DELETE FROM live_accounts")
|
||||
conn.execute("DELETE FROM live_balance")
|
||||
conn.execute("DELETE FROM live_positions")
|
||||
conn.execute("DELETE FROM qmt_account_snapshot")
|
||||
conn.execute(
|
||||
"INSERT INTO live_accounts (id,name,account,vt_symbol,"
|
||||
"strategy_class,strategy_name,status) VALUES "
|
||||
"(5,'live','66639661','pool','channel_test',"
|
||||
"'portfolio_channel_test','running')")
|
||||
conn.execute(
|
||||
"INSERT INTO live_balance (account_id,date,cash,market_value,"
|
||||
"total) VALUES (5,'2026-08-15',1000.0,10000.0,11000.0)")
|
||||
conn.execute(
|
||||
"INSERT INTO live_positions (account_id,symbol,volume,avg_price) "
|
||||
"VALUES (5,'510300.XSHG',1000,4.0)")
|
||||
conn.execute(
|
||||
"INSERT INTO qmt_account_snapshot (account,mini_path,cash,"
|
||||
"market_value,total,positions,updated_at) VALUES "
|
||||
"('66639661','',1000.0,?,10100.0,"
|
||||
"'[{\"symbol\": \"510300.SH\", \"volume\": 1000}]',"
|
||||
"'2026-08-15 15:10:00')", (snap_mv,))
|
||||
conn.commit()
|
||||
|
||||
# 绿线:快照 10050 − 账本 10000 = 50(0.497% < 0.5%) → pass
|
||||
_seed(10050.0)
|
||||
r = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["identity_passed"] is True
|
||||
row = data["rows"][0]
|
||||
assert row["account"] == "66639661"
|
||||
assert row["status"] == "pass"
|
||||
assert abs(row["unattributed_mv"] - 50.0) < 1e-6
|
||||
assert row["unattributed_positions"] == [] # 逐票 1000=1000 无分歧
|
||||
|
||||
# 二次调用走已存行(identity_reconcile 同日已落库),结果一致
|
||||
r2 = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r2.json()["rows"][0]["unattributed_mv"] == row["unattributed_mv"]
|
||||
|
||||
# 超红线(改库后 refresh=true 强制重算):快照 10200 − 账本 10000 = 200(1.96%)
|
||||
_seed(10200.0)
|
||||
r3 = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15&refresh=true",
|
||||
headers=_auth(token))
|
||||
d3 = r3.json()
|
||||
assert d3["identity_passed"] is False
|
||||
assert d3["rows"][0]["status"] == "unattributed_over_tol"
|
||||
assert abs(d3["rows"][0]["unattributed_mv"] - 200.0) < 1e-6
|
||||
|
||||
|
||||
def test_identity_reconcile_snapshot_missing(tmp_path):
|
||||
"""无快照账号如实标 snapshot_missing,不算通过也不崩。"""
|
||||
import sqlite3
|
||||
|
||||
c, token = _client(tmp_path)
|
||||
db = os.path.join(str(tmp_path), "p.db")
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO live_accounts (id,name,account,vt_symbol,strategy_class,"
|
||||
"strategy_name,status) VALUES (6,'l2','88888888','pool',"
|
||||
"'all_weather','portfolio_all_weather_ex','running')")
|
||||
conn.commit()
|
||||
|
||||
r = c.get("/api/v1/paper/reconcile/identity?date=2026-08-15",
|
||||
headers=_auth(token))
|
||||
assert r.status_code == 200
|
||||
row = r.json()["rows"][0]
|
||||
assert row["status"] == "snapshot_missing"
|
||||
assert r.json()["identity_passed"] is False
|
||||
|
||||
Reference in New Issue
Block a user