From 0fa4a5a2ff8519d892b6eee48c6a4a81cc0a6f9e Mon Sep 17 00:00:00 2001 From: zlt Date: Tue, 8 Sep 2026 12:29:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8F=E4=BB=B7=E7=A0=94=E5=88=A4=E9=93=BE?= =?UTF-8?q?=EF=BC=9A=E7=A0=94=E5=88=A4=E5=BA=94=E7=AD=94=E5=BD=92=E4=B8=80?= =?UTF-8?q?=E5=B8=A6=E5=87=BA=E4=B8=A4=E4=B8=AA=E6=9C=9F=E9=99=90=E7=9A=84?= =?UTF-8?q?=E5=A4=B4=E8=BF=9B=E6=8F=90=E8=AE=AE=E7=A1=AC=E6=95=B0=E5=AD=97?= =?UTF-8?q?=E4=B8=8E=E6=8F=90=E8=AE=AE=E5=8D=A1=EF=BC=9B=E9=A9=B3=E5=9B=9E?= =?UTF-8?q?=E8=A1=8C=E8=AE=B0=E6=8A=8A=E6=8F=A1=E5=BA=A6=EF=BC=9B=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=A5=91=E7=BA=A6=20V1.2=EF=BC=88=E7=AC=AC=E4=BA=8C?= =?UTF-8?q?=E5=8D=81=E4=BA=8C=E6=89=B9=E5=8A=A0=E4=B8=80=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E5=85=A8=E5=A5=97=20680=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BIONIC_PMS_INTERFACE.md | 1 + app/services/judge.py | 29 ++++++++++++++++++++++++++--- app/services/proposal_service.py | 9 +++++++-- app/web/static/index.html | 15 +++++++++++++++ scripts/run_tests.py | 6 +++--- scripts/test_batch22_units.py | 19 +++++++++++++++++++ 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/BIONIC_PMS_INTERFACE.md b/BIONIC_PMS_INTERFACE.md index 9dd5303..d5e52d2 100644 --- a/BIONIC_PMS_INTERFACE.md +++ b/BIONIC_PMS_INTERFACE.md @@ -299,3 +299,4 @@ docker compose run --rm --no-deps pms-web python scripts/probe_bionic.py --base |---|---|---| | V1.0 | 2026-08-03 | 首版定稿并双侧落码。择时部分是一套盘中判定规则(资金阈值、动量追买等) | | V1.1 | 2026-08-03 | 按用户审核意见重做择时:删掉 V1.0 的盘中判定规则(违反「提前计算为主、盘中监控为辅、不另做盘中判断、可以接受买不上」),改为由昨夜支撑压力推出执行区间、盘中只做区间比对与当日监控核对;研判留痕不再写 `decision_ledger`(每晚判分全表扫描,已核实);文档与代码注释清理生造词 | +| V1.2 | 2026-09-08 | 量价研判链(《量价研判链吸收方案_2026-09-08》):请求侧硬数字白名单多送 `events_text`、`pricing_text` 两句整句(催化事件与定价状态,事件与量价不是产业逻辑);应答侧新建仓研判可并列多带 `pv_heads`(`h5`、`h20` 各含 `score` −1 到 1、`direction` 看多/中性/看空、`justification`),PMS 归一后进提议硬数字 `judge_pv_heads`,只显示不触发;三值 verdict 口径不变,缺 `pv_heads` 就是 None,向下兼容 | diff --git a/app/services/judge.py b/app/services/judge.py index b3f32af..e84f1e4 100644 --- a/app/services/judge.py +++ b/app/services/judge.py @@ -135,6 +135,26 @@ def _conf_or_none(v): return None +def _pv_heads_or_none(v): + """应答里的 pv_heads 归一: {h5: {score, direction, justification}, h20: {...}}; 评分夹到 -1 到 1, + 两个头都没有评分就是 None。""" + if not isinstance(v, dict): + return None + out = {} + for k in ("h5", "h20"): + h = v.get(k) + if not isinstance(h, dict): + continue + score = _conf_or_none(h.get("score")) + if score is None: + continue + score = max(-1.0, min(1.0, score)) + direction = "看多" if score > 0.2 else ("看空" if score < -0.2 else "中性") + out[k] = {"score": round(score, 3), "direction": direction, + "justification": str(h.get("justification") or "").strip()[:120]} + return out or None + + def _map_verdict(data: dict) -> dict: """把决策系统应答的 verdict 归一到 PASS / REJECT / UNAVAILABLE, 并带出原因与置信度。 @@ -153,16 +173,19 @@ def _map_verdict(data: dict) -> dict: verdict = str(data.get("verdict") or data.get("decision") or "").upper() reason = data.get("reason") or data.get("rationale") or "" conf = _conf_or_none(data.get("confidence")) + # 两个期限的头 (2026-09-08 量价研判链): 决策系统在应答里并列给的 5 日与 20 日判断, 原样带出进提议硬数字, + # 只显示不触发。缺就是 None, 旧应答一个字不用改。 + heads = _pv_heads_or_none(data.get("pv_heads")) if verdict in ("PASS", "APPROVE", "APPROVED", "ALLOW", "通过"): return {"verdict": PASS, "reason": reason, "degraded": False, "raw": data, - "confidence": conf} + "confidence": conf, "pv_heads": heads} if verdict in ("REJECT", "DENY", "DENIED", "BLOCK", "驳回"): return {"verdict": REJECT, "reason": reason, "degraded": False, "raw": data, - "confidence": conf} + "confidence": conf, "pv_heads": heads} if verdict in ("UNAVAILABLE", "PMS_UNAVAILABLE", "NA", "N/A", "不可用"): logger.warning("[研判闸] 决策系统回不可用, 降级人工确认: %s", reason or "(无原因)") return {"verdict": UNAVAILABLE, "reason": reason or "决策系统研判不可用", - "degraded": True, "raw": data, "confidence": conf} + "degraded": True, "raw": data, "confidence": conf, "pv_heads": heads} logger.error("[研判闸] 答复无法识别 (%s), 按不可用降级", verdict or data) return {"verdict": UNAVAILABLE, "reason": f"研判答复无法识别: {verdict or data}", "degraded": True, "raw": data, "confidence": conf} diff --git a/app/services/proposal_service.py b/app/services/proposal_service.py index 9d55211..033612d 100644 --- a/app/services/proposal_service.py +++ b/app/services/proposal_service.py @@ -527,9 +527,12 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out, out["rejected"].append({"ts_code": code, "action": action, "by": "judge", "failed": [verdict.get("reason") or "决策系统驳回"]}) if not dry_run: + # 驳回也把把握度记进硬数字 (2026-09-08): 低把握驳回转交人那道保险 (PMS_JUDGE_REJECT_CONF_MIN) + # 是否在起作用, 只能从驳回行的把握度分布看出来; 此前只有提议行记它, 驳回行没有, 复核无据。 pms_repo.insert_ledger(ts_code=code, action=action, arbiter="judge", verdict="REJECT", price_at=price, - hard_numbers=c["hard_numbers"], + hard_numbers={**(c["hard_numbers"] or {}), + "judge_conf": verdict.get("confidence")}, reason=verdict.get("reason") or "决策系统驳回") return if verdict.get("degraded"): @@ -737,7 +740,9 @@ def _make_proposal(c, price, verdict) -> str: "source": c.get("source") or ae.SRC_ENGINE, # 研判应答的结论与置信度 (2026-09-03): 人裁决时要看得见决策系统怎么说、有多确定。 # judge_reason 另有一列, 这两项进硬数字是为了随账本走 (采纳/驳回时原样落账)。 - "judge_verdict": verdict.get("verdict"), "judge_conf": verdict.get("confidence")} + "judge_verdict": verdict.get("verdict"), "judge_conf": verdict.get("confidence"), + # 两个期限的头 (2026-09-08 量价研判链): 决策系统并列给的 5 日与 20 日判断, 只显示不触发。 + "judge_pv_heads": verdict.get("pv_heads")} try: pms_repo.insert_proposal( proposal_id=pid, ts_code=c["ts_code"], action=c["action"], qty=c["qty"], diff --git a/app/web/static/index.html b/app/web/static/index.html index b524c8e..126412a 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1034,6 +1034,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
{{ propText(p, 'valuation_text') }}
{{ propText(p, 'events_text') }}
{{ propText(p, 'pricing_text') }}
+
择时层两个期限:{{ propHeads(p) }}
{{ propWhy(p) }}
约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }} · 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}
@@ -2584,6 +2585,20 @@ createApp({ const v = (p.hard_numbers || {})[key]; return (typeof v === 'string' && v.trim()) ? v : ''; } + // 决策系统并列给的 5 日与 20 日判断 (2026-09-08 量价研判链): 方向词是它给的中文, 评分是 -1 到 1 的数。 + // 只显示不触发; 没有就不显示这一行。 + function propHeads(p) { + const h = (p.hard_numbers || {}).judge_pv_heads; + if (!h || typeof h !== 'object') return ''; + const bits = []; + [['h5', '5 日'], ['h20', '20 日']].forEach(([k, name]) => { + const x = h[k]; + if (x && typeof x.score === 'number') { + bits.push(name + x.direction + '(' + (x.score >= 0 ? '+' : '') + x.score.toFixed(2) + (x.justification ? ',' + x.justification : '') + ')'); + } + }); + return bits.join(';'); + } function propLogic(p) { const l = (p.hard_numbers || {}).logic; return Array.isArray(l) && l.length ? l[0] : ''; diff --git a/scripts/run_tests.py b/scripts/run_tests.py index ed79cdd..532d949 100644 --- a/scripts/run_tests.py +++ b/scripts/run_tests.py @@ -59,16 +59,16 @@ test_batch22_units.py 逻辑状态四态接入 (2026-09-07 第三件): 解析层收逻辑状态/硬数字带它而研判 白名单不收/判决候选而逻辑存疑强制交人/持仓存疑停增持侧/研究走弱 减持默认关且开了必交人/同轮减持优先级/早上取回与映射新鲜度/策略 - 买入腿按来源暂停恢复/持仓视图两栏三情形/假仓库签名/安全边际整句透传/参考目标价/催化事件与定价状态透传 (18 例) + 买入腿按来源暂停恢复/持仓视图两栏三情形/假仓库签名/安全边际整句透传/参考目标价/催化事件与定价状态透传/两个期限的头 (19 例) test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许 直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象 打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例) test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) + 目标价到价必定入队 (档位 full 也不自动卖) + 用户设的止损价与目标价单独成列显示 (70 例) - 共 679 例 + 共 680 例 (总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条 - 减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 现为 679) + 减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680) 任一子集失败即整体失败 (退出码 1)。 哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了): diff --git a/scripts/test_batch22_units.py b/scripts/test_batch22_units.py index 54e2a14..8547c14 100644 --- a/scripts/test_batch22_units.py +++ b/scripts/test_batch22_units.py @@ -496,6 +496,25 @@ def _(): assert lss.ref_target_view(None) is None and lss.ref_target_view({}) is None +@case("两个期限的头·研判应答归一带出 pv_heads (评分夹区间、方向按评分), 提议硬数字带 judge_pv_heads, 旧应答为 None") +def _(): + r = jd._map_verdict({"verdict": "PASS", "reason": "ok", "confidence": 70, + "pv_heads": {"h5": {"score": 1.7, "direction": "看空", "justification": "x"}, + "h20": {"score": "-0.1", "justification": "y"}, "h99": {"score": 0.5}}}) + assert r["verdict"] == jd.PASS and r["pv_heads"]["h5"] == {"score": 1.0, "direction": "看多", "justification": "x"}, r + assert r["pv_heads"]["h20"]["direction"] == "中性" and "h99" not in r["pv_heads"], r + assert jd._map_verdict({"verdict": "REJECT", "reason": "no"})["pv_heads"] is None + assert jd._map_verdict({"verdict": "UNAVAILABLE", "reason": "x", "pv_heads": {"h5": {"direction": "看多"}}})["pv_heads"] is None + c = {"ts_code": CODE, "action": "OPEN", "side": "buy", "qty": 1000, "reason": "测试新建仓", "price": 10.0, + "hard_numbers": {"price": 10.0, "verdict": "候选"}, "needs_user_confirm": False, "judge_required": True, + "source": ae.SRC_ENGINE, "target_amount": 10000.0, "sector": None} + out, got = _route(c, autonomy="propose_only", judge_resp={**JUDGE_PASS, "pv_heads": {"h5": {"score": 0.4, "direction": "看多", "justification": "a"}}}) + assert len(out["queued"]) == 1 and got["proposals"], out + assert got["proposals"][0]["hard_numbers"]["judge_pv_heads"] == {"h5": {"score": 0.4, "direction": "看多", "justification": "a"}}, got["proposals"][0]["hard_numbers"] + out2, got2 = _route(c, autonomy="propose_only", judge_resp=JUDGE_PASS) + assert got2["proposals"][0]["hard_numbers"].get("judge_pv_heads") is None + + @case("假仓库·ledger_by_ref 与真 repo 同名同签名 (第十批 [M1] 也会扫, 这里先钉一次)") def _(): import ast