diff --git a/app/services/proposal_service.py b/app/services/proposal_service.py
index 89fe837..0901849 100644
--- a/app/services/proposal_service.py
+++ b/app/services/proposal_service.py
@@ -209,6 +209,41 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
return res["candidates"]
+def _plain_check(c) -> str:
+ """规则闸的 failed 串形如 "T1_UNAVAILABLE: 卖出…" —— 去掉给运维看的大写代码前缀, 只留中文说明。"""
+ s = str(c)
+ head, sep, tail = s.partition(":")
+ if sep and head.strip() and all(ch.isupper() or ch.isdigit() or ch == "_" for ch in head.strip()):
+ return tail.strip()
+ return s.strip()
+
+
+def _today_open_reject_reasons(now):
+ """今天各票**新建仓**被驳回的实因, 供候选处置显示真正的「为什么」:
+ 判 = 决策系统给的原话 (评审账本 arbiter=judge 那行的 reason);
+ 合规 = 规则闸具体未过的检查 (failed_checks, 去掉大写代码前缀)。
+ 只读评审账本、按票取当天最新一条 (list_ledger 按 id 倒序); 读不到就返回空, 调用方退回概述。"""
+ judge, rule = {}, {}
+ today = now.strftime("%Y-%m-%d")
+ try:
+ for r in pms_repo.list_ledger(limit=500):
+ if r.get("verdict") != "REJECT" or r.get("action") != ae.A_OPEN:
+ continue
+ if str(r.get("decided_at"))[:10] != today:
+ continue
+ code = r.get("ts_code")
+ if not code:
+ continue
+ if r.get("arbiter") == "judge" and code not in judge:
+ judge[code] = (r.get("reason") or "").strip()
+ elif r.get("arbiter") == "rule" and code not in rule:
+ fc = r.get("failed_checks") or []
+ rule[code] = ";".join(_plain_check(x) for x in fc) if fc else (r.get("reason") or "").strip()
+ except Exception:
+ logger.exception("读当日新建仓驳回实因失败 (退回概述)")
+ return judge, rule
+
+
def disposition_snapshot(now=None) -> dict:
"""候选池处置快照 (只读, 无副作用): 候选池里每只票今天为什么下单 / 没下单。
@@ -239,14 +274,17 @@ def disposition_snapshot(now=None) -> dict:
view = portfolio.positions_view()
params = _scan_params(view)
stock_params = command_service.effective_stock_params()
- # 给交易员的白话原因: 用同一套去重键 (口径与实盘扫描一致), 但值不含内部闸门术语、
- # 也不指引任何终端命令 —— 原来的 _*_keys() 串带「研判闸/规则闸/看 make t-gate」, 那是
- # 给日志与运维看的, 不该出现在交易员页面。落库顺序同实盘 (在途最后, 覆盖被拒)。
+ # 给交易员的白话原因: 用同一套去重键 (口径与实盘扫描一致), 但值不含内部闸门术语、也不指引
+ # 任何终端命令。能取到实因就带上实因 (决策系统原话 / 合规具体未过项), 取不到才退回概述。
+ # 落库顺序同实盘 (在途最后, 覆盖被拒)。
+ _jr, _rr = _today_open_reject_reasons(now)
skip = {}
for _k in _judge_rejected_open_keys():
- skip[_k] = "决策系统今天判过暂不建仓,明天开盘会重新评估"
+ _w = _jr.get(_k[0])
+ skip[_k] = ("决策系统判暂不建仓:" + _w) if _w else "决策系统今天判过暂不建仓,明天开盘会重新评估"
for _k in _rejected_today_keys():
- skip[_k] = "今天没通过合规检查,明天开盘会重新评估"
+ _w = _rr.get(_k[0])
+ skip[_k] = ("未通过合规检查:" + _w) if _w else "今天没通过合规检查,明天开盘会重新评估"
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)"
sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它
diff --git a/app/web/static/index.html b/app/web/static/index.html
index 37738db..3311d87 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -305,6 +305,14 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius: