量价研判链:研判应答归一带出两个期限的头进提议硬数字与提议卡;驳回行记把握度;接口契约 V1.2(第二十二批加一例,全套 680)

This commit is contained in:
zlt 2026-09-08 12:29:39 +08:00
parent 7d670b364c
commit 0fa4a5a2ff
6 changed files with 71 additions and 8 deletions

View File

@ -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向下兼容 |

View File

@ -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}

View File

@ -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"],

View File

@ -1034,6 +1034,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<div class="st muted" v-if="propText(p, 'valuation_text')">{{ propText(p, 'valuation_text') }}</div>
<div class="st muted" v-if="propText(p, 'events_text')">{{ propText(p, 'events_text') }}</div>
<div class="st muted" v-if="propText(p, 'pricing_text')">{{ propText(p, 'pricing_text') }}</div>
<div class="st" v-if="propHeads(p)"><span class="muted">择时层两个期限:</span>{{ propHeads(p) }}</div>
<div class="st muted">{{ propWhy(p) }}</div>
<div class="st">约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }}
· 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}</div>
@ -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] : '';

View File

@ -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 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):

View File

@ -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