From d376a55fbd15fbd0451f4eaf02ebc89193575bed Mon Sep 17 00:00:00 2001 From: zlt Date: Mon, 3 Aug 2026 10:19:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E4=B8=80=E4=B8=AA=E7=9B=91=E6=B5=8B?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/watch.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/scripts/watch.py b/scripts/watch.py index a852d66..d478838 100644 --- a/scripts/watch.py +++ b/scripts/watch.py @@ -208,6 +208,58 @@ def outbound(): f"这才是「发不出去」, 看 pms-ws 日志") +def crosscheck(): + """出口表与父指令必须对得上 —— 对不上是**本端记账掉了一环**, 比发不出去更隐蔽。 + + 正常次序 (设计「先记账后动作」): executor 判 FIRE → dispatcher 落出口表 → + 立刻把父指令置 DISPATCHED 并把这一笔记进 progress.children。三件事要么都成, + 要么都没发生。只成了第一件的话: + + * 父指令不知道自己今天已经发过 → 下一跳可能重复出手 (出口表 instruction_id + 唯一索引会拦住, 于是它转而卡死: 每一跳都撞重复键, 再也发不出去) + * 当日配额 fired_today 恒为 0 → 分日节奏失效 + * 页面看到的是「PROPOSED / 已成 0」, 而 QMT 那边单子挂着 —— 两边说的不是一回事 + + 2026-08-03 实机撞到过一次, 所以做成常驻检查。 + """ + from app.repo import pms_repo, qmt_repo + from app.services import param_store + if param_store.get("PMS_DISPATCH_MODE", "shadow") != "ws": + return + try: + orders = qmt_repo.list_orders(limit=200) + ins = {r["instruction_id"]: r for r in pms_repo.list_instructions(limit=200)} + except Exception: + return + bad = [] + by_parent = {} + for o in orders: + by_parent.setdefault(o.get("parent_id"), []).append(o) + for pid, os_ in by_parent.items(): + p = ins.get(pid) + if not p: + bad.append(f"出口表有 {len(os_)} 张 {os_[0]['ts_code']} 的委托, " + f"但本端找不到父指令 {pid}") + continue + n_child = len((p.get("progress") or {}).get("children") or []) + if p["status"] in ("PROPOSED", "RULE_PASSED"): + bad.append(f"{p['ts_code']} 出口表已有 {len(os_)} 张委托 " + f"({os_[0]['status']}), 而父指令还停在 {p['status']} —— " + f"下发成了但**本端没记上**") + if n_child != len(os_): + bad.append(f"{p['ts_code']} 父指令记着 {n_child} 笔子单, " + f"出口表实际有 {len(os_)} 张 —— 分日配额会算错") + if not p.get("dispatch_ref"): + bad.append(f"{p['ts_code']} 出口表有委托但父指令 dispatch_ref 是空的") + if not bad: + return + _sec("⚠ 出口表与本端指令对不上") + for b in dict.fromkeys(bad): + print(f" ✗ {b}") + print(" 这一类不是「发不出去」, 是**发出去了但账没记全**。查 executor.run_tick 里") + print(" dispatcher.dispatch 之后那几行 (update_instruction / insert_ledger) 报了什么。") + + def positions(): from app.services import portfolio try: @@ -278,6 +330,7 @@ def render(wide, clear=False): account() instructions() outbound() + crosscheck() positions() if wide: plans()