# -*- coding: utf-8 -*- """ 清空 PMS 账本并把回放游标对齐到当前 (影子运行期的重来一次) =========================================================== 运行: docker compose run --rm pms-web python scripts/reset_ledger.py # 演练, 只统计不删 docker compose run --rm pms-web python scripts/reset_ledger.py --yes # 实际执行 docker compose run --rm pms-web python scripts/reset_ledger.py --yes --keep-commands 什么时候用 ---------- 账本被脏数据污染、需要从干净起点重来时。典型场景就是 2026-07-29 那次: 首次启动时回放游标 为空, `replay_fills` 从 `trading_order` 表头开始扫, 把旧系统多年的历史成交**全部**当成 「外部成交」并入 BASE 批次 —— 账本上凭空长出一堆早已清掉的持仓, 摊薄成本与安全垫全错。 (该 bug 已修: 游标为空时只对齐不追认, 见 ledger_service._seed_cursor。) **只动 PMS 自有表, 绝不碰下游的 trading_*** —— 那三张表归下游系统维护, PMS 只读。 清完之后账本是空的, 而真实持仓在 QMT 那边。要让账本反映真实持仓, 有两条路: * 让日终对账把下游持仓补进来 (以下游为准, 会生成 RECON 批次), 或 * 在页面用建仓命令把已有持仓登记成 BASE 批次 影子运行期一般选前者: 跑一次 `POST /api/ops/reconcile?apply_fix=true`。 """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # 账本类表 —— 会被清空 LEDGER_TABLES = ["pms_lot", "pms_position", "pms_instruction", "pms_action_ledger", "pms_qmt_order", "pms_qmt_inbox"] # 命令与方案 —— 默认也清 (方案指向的指令都没了, 留着只会是孤儿); --keep-commands 可保留 COMMAND_TABLES = ["pms_plan", "pms_command", "pms_proposal"] # 绝不碰: 下游三表 / 行业映射 / 参数表 / 日报 NEVER_TOUCH = ["trading_order", "trading_position", "trading_buy_plan", "pms_industry_map", "pms_runtime_param", "pms_daily_report"] def main(): ap = argparse.ArgumentParser() ap.add_argument("--yes", action="store_true", help="确认执行 (缺省只演练)") ap.add_argument("--keep-commands", action="store_true", help="保留命令/方案/提议, 只清账本与指令") ap.add_argument("--no-seed", action="store_true", help="不重置回放游标 (默认会把它对齐到当前最新成交)") args = ap.parse_args() from app.db.session import execute, fetch_one from app.repo import downstream_repo, pms_repo from app.services.ledger_service import CURSOR_KEY tables = list(LEDGER_TABLES) + ([] if args.keep_commands else COMMAND_TABLES) print("将清空以下表:") total = 0 for t in tables: try: n = int((fetch_one(f"SELECT COUNT(*) AS n FROM {t}") or {}).get("n") or 0) total += n print(f" {t:<22} {n} 行") except Exception as e: print(f" {t:<22} 读取失败: {type(e).__name__}: {e}") print(f"\n合计 {total} 行。以下表**不会**被动: {', '.join(NEVER_TOUCH)}") cur = pms_repo.get_param(CURSOR_KEY) print(f"\n当前回放游标: {cur or '(空)'}") if not args.no_seed: try: anchor = downstream_repo.latest_filled_order_id() print(f"将把游标对齐到当前最新成交: {anchor or '(下游暂无成交)'}") print(" → 此后只跟踪新成交, 不再追认历史") except Exception as e: anchor = None print(f"读下游最新成交失败 (游标将清空): {type(e).__name__}: {e}") else: anchor = None print("--no-seed: 游标保持不变") if not args.yes: print("\n[演练模式] 未删除任何数据。确认后加 --yes 重跑:") print(" docker compose run --rm pms-web python scripts/reset_ledger.py --yes") return print() failed = [] for t in tables: try: n = execute(f"DELETE FROM {t}") print(f" OK {t:<22} 删除 {n} 行") except Exception as e: print(f" FAIL {t:<22} {type(e).__name__}: {e}") failed.append(t) if not args.no_seed: try: pms_repo.set_param(CURSOR_KEY, anchor or "", updated_by="reset_ledger") print(f" OK 回放游标 → {anchor or '(空, 下次回放自动对齐)'}") except Exception as e: print(f" FAIL 游标重置: {type(e).__name__}: {e}") failed.append(CURSOR_KEY) # ws 通道状态一并归零 —— 指令与 inbox 都清了, 留着旧水位会让补发对不上 try: execute("UPDATE pms_ws_state SET last_seq = 0, acked_seq = 0, server_seq = 0, " "resync_flag = 0, conn_state = 'INIT', updated_at = NOW() WHERE id = 1") print(" OK pms_ws_state 水位归零 (inbox 已清空, 水位必须跟着回零)") except Exception as e: print(f" WARN pms_ws_state 归零失败 (表可能还没建): {type(e).__name__}: {e}") print("\n" + "-" * 62) if failed: print(f"FAILED: {', '.join(failed)}") sys.exit(1) print("账本已清空。下一步:") print(" 1. 让对账把下游真实持仓补进账本 (以下游为准):") print(" curl -X POST 'http://127.0.0.1:38100/api/ops/reconcile?apply_fix=true'") print(" 2. 或在页面用建仓命令把已有持仓登记成 BASE 批次") print(" 3. 核对: python scripts/check_db.py") if __name__ == "__main__": main()