# -*- coding: utf-8 -*- """ 账本重建: 预检 → 执行 → 判收 (README 待办 #4) ============================================== docker compose run --rm --no-deps pms-web python scripts/rebuild_ledger.py # 只预检 docker compose run --rm --no-deps pms-web python scripts/rebuild_ledger.py --yes # 预检通过就执行 docker compose run --rm --no-deps pms-web python scripts/rebuild_ledger.py --accept # 只看判收 (或 make rebuild / make rebuild GO=1 / make rebuild-accept) 清账之后账本是空的, 真实持仓要按「以下游为准」认领回来。这一步**只发生一次**, 但它定死了 每一只票的开仓价, 而开仓价一错, 后面每一条纪律都错在一个看不出来的地方: 下游 cost_price → RECON 批次开仓价 → 摊薄成本 → 安全垫 ↓ 盈利加仓(≥3%) · 保垫减仓(峰值≥6%) · 补仓评估档(−8%/−15%) 所以本脚本**默认只预检不执行**。加 `--yes` 才真的改账, 且预检不过照样不执行 (要强行放行得 再加 `--force`, 那等于声明"我确认这份数据就是对的, 接受安全垫从 0 起算")。 三段各自的意思: [1] 预检 —— 事实源给不给得出持仓 / 成本价能不能用 / 建完验不验得到纪律 (只读) [2] 执行 —— reconcile(apply_fix=True), 走的是生产同一条路, 不是另写一套 [3] 判收 —— 安全垫分布是最硬的判据: 全 0 就是踩了"拿现价当成本"那个坑 """ from __future__ import annotations import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) OK, BAD, WARN = " OK ", " FAIL ", " WARN " def _line(tag, msg): print(f"[{tag}] {msg}") def _steps(steps): for s in steps: tag = OK if s.get("ok") else (WARN if s.get("blocking") is False else BAD) print(f" [{tag}] {s['step']}: {s['why']}") def show_preflight(pf) -> bool: print("=" * 72) print("[1] 预检 (只读, 一个字都不写)") print("=" * 72) _line("账本", f"当前持仓 {pf['book_held']} 只" + (" —— 空账本, 这是首次建账, 成本价闸生效" if pf["first_build"] else " —— 非空, 走的是日常对账口径, 成本价闸不生效")) src = pf.get("source") or {} _line("事实源", f"{src.get('source')} (mode={src.get('mode')}, " f"age={src.get('age_sec')}s, 列={src.get('columns')})") for a in (src.get("alerts") or []): _line(a.get("level", "WARN"), a.get("message")) _steps(pf.get("steps") or []) chk = pf.get("cost_check") or {} if chk.get("rows"): print(f"\n 成本价明细 ({chk['n']} 只, 判定分布 {chk['counts']}):") for r in chk["rows"]: cu = "" if r.get("cushion_pct") is None else f" 安全垫 {r['cushion_pct']:+.1%}" mark = " " if r["verdict"] == "OK" else "! " print(f" {mark}{r['ts_code']:<12} {str(r['qty']):>7} 股 " f"成本 {_g(r['cost'])} 现价 {_g(r['price'])} " f"[{r['verdict']}]{cu}") if r.get("why"): print(f" {r['why']}") cov = pf.get("coverage") or {} if cov: print("\n 情形覆盖 (不阻断建账, 只影响这轮验不验得到纪律):") for k, v in (cov.get("counts") or {}).items(): print(f" [{OK if v else WARN}] {k}: {v}") sw = pf.get("switches") or {} if sw: print("\n 开关现状: " + " · ".join(f"{k}={v}" for k, v in sw.items())) print(f"\n 结论: {pf.get('hint')}") return bool(pf.get("ready")) def show_accept(ac): print("=" * 72) print("[3] 判收 (只读)") print("=" * 72) _line("持仓", f"{ac.get('held')} 只") for c in ac.get("checks") or []: print(f" [{OK if c['ok'] else BAD}] {c['check']}: {c['why']}") if ac.get("sector", {}).get("top"): print("\n 行业分布 (占规模):") for s in ac["sector"]["top"]: print(f" {s['sector'] or '(无)':<20} {s['ratio']:>7.1%} {s['names']} 只") print(f"\n 结论: {ac.get('hint')}") return bool(ac.get("ok")) def _g(v): return "-" if v in (None, "") else f"{float(v):.4g}" def main(): ap = argparse.ArgumentParser() ap.add_argument("--yes", action="store_true", help="预检通过就执行重建 (缺省只预检)") ap.add_argument("--force", action="store_true", help="预检不过也执行。**这等于声明你接受安全垫从 0 起算**") ap.add_argument("--accept", action="store_true", help="只跑判收段 (重建已经做过了)") args = ap.parse_args() from app.services import ledger_service as ls if args.accept: return 0 if show_accept(ls.rebuild_accept()) else 1 pf = ls.rebuild_preflight() ready = show_preflight(pf) # EMPTY 是**终态不是故障**: 账户空 + 账本空 = 两边一致, 没有需要接管的持仓, # 重建这一步本来就该跳过。原来它和「读不到事实源」「成本价不过关」一起退 2, # make 报 Error 2 —— 一个完全正常的状态被报成失败, 让人以为还有事没做完。 if pf.get("verdict") == "EMPTY": print("\n(没有需要接管的持仓, 重建这一步跳过 —— **这不是错误**。)" if pf.get("ok") else "\n(顺序反了: 先 make reset-ledger CONFIRM=1 清账本, " "别让对账拿空集去核销已有持仓。)") return 0 if pf.get("ok") else 2 if not args.yes: print("\n(只预检, 没有改任何东西。要执行加 --yes)") return 0 if ready else 2 if not ready and not args.force: print("\n预检没过, 不执行。请对端把 trading_position 的 cost_price / " "available_quantity 填成真实值后重跑;\n" "确认这份数据就是对的可以加 --force 放行 (那意味着安全垫从 0 起算, " "这一轮的补仓/加仓/保垫减仓都判不准)。") return 2 print("\n" + "=" * 72) print("[2] 执行 reconcile(apply_fix=True" + (", force=True" if args.force else "") + ")") print("=" * 72) res = ls.reconcile(apply_fix=True, force=args.force) _line("结果", f"ok={res.get('ok')} 事实源={res.get('source')} " f"差异 {len(res.get('diffs') or [])} 项 修正 {len(res.get('fixes') or [])} 项") if res.get("blocked"): _line("BLOCKED", res["blocked"].get("why")) if res["blocked"].get("how"): _line("怎么办", res["blocked"]["how"]) return 1 for f in res.get("fixes") or []: print(f" {f['ts_code']:<12} {f['op']:<14} {f['qty']:>7} 股 " f"@ {_g(f.get('price'))} {f.get('price_source') or ''}") for e in res.get("errors") or []: _line("ERROR", e) print() ok = show_accept(ls.rebuild_accept()) print("\n收尾 (脚本不替你做, 因为这几件事该由人确认):") for s in pf.get("after") or []: print(f" - {s}") return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())