tradingSystem/scripts/rebuild_ledger.py

167 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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())