feat(web): 对账中心上线——恒等式B5只读端点+前端恒等式区块,双轨对账页升级为对账中心(issue#42-A) [nas]
CI/CD / test (push) Failing after 23s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped

- 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:
2026-08-26 20:31:42 +08:00
parent 0c81f5b04b
commit df008b985c
7 changed files with 326 additions and 7 deletions
+81
View File
@@ -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