tradingSystem/scripts/rebuild_ledger.py

167 lines
7.1 KiB
Python
Raw Normal View History

2026-07-31 13:46:15 +08:00
# -*- 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)
2026-08-03 09:10:56 +08:00
# 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
2026-07-31 13:46:15 +08:00
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())