157 lines
6.4 KiB
Python
157 lines
6.4 KiB
Python
|
|
# -*- 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)
|
|||
|
|
|
|||
|
|
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())
|