量价研判链 3.5:催化事件与定价状态随候选进硬数字与提议卡,两句整句送研判;提议卡补显示安全边际整句(第二十二批加一例,全套 679)

This commit is contained in:
zlt 2026-09-08 11:44:33 +08:00
parent 48a562f170
commit 7d670b364c
6 changed files with 100 additions and 5 deletions

View File

@ -689,6 +689,10 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float):
"logic_state": c.get("logic_state"),
# 安全边际三情景的整句 (2026-09-07 第四件): 只给人看, 不送研判。
"valuation_text": c.get("valuation_text"),
# 催化事件与定价状态 (2026-09-08 量价研判链 3.5): 原值进账本与提议卡; 两句整句另送研判
# (它们是事件与量价, 不是产业逻辑, 见 judge.OPEN_JUDGE_KEYS)。不参与任何判定。
"events": c.get("events"), "events_text": c.get("events_text"),
"pricing_state": c.get("pricing_state"), "pricing_text": c.get("pricing_text"),
# ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉,
# 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。
"target_pct": target_pct, "target_amount": round(full_amt, 2),

View File

@ -43,7 +43,12 @@ OPEN_JUDGE_KEYS = ("price", "score", "theme", "tier", "upside", "heat",
# 上游候选卡的判决与理由 (2026-09-02 起随计划下发)。决策系统那侧是逐键
# 渲染进提示词的, 所以这五项放行之后不用改它的提示词就能看见; 配合下面
# must_answer 里那条「每条理由到今天是否仍成立」, 研判就有了可逐条核对的靶子。
"verdict", "reasons", "missing", "risk", "card_rank")
"verdict", "reasons", "missing", "risk", "card_rank",
# 催化事件与定价状态的整句 (2026-09-08《量价研判链吸收方案》3.5): 它们是事件与量价,
# 正是研报里决策智能体的"事件上下文", 不是产业逻辑 —— 与判决依据、论断、逻辑状态、
# 安全边际那几项不同, 这两句要送。送整句不送字典: 研判提示词把硬数字逐行印成
# "键: 值", 字典会印成一串代码, 整句模型才读得顺。
"events_text", "pricing_text")
# 新建仓送研判时的必答题 (2026-09-03)。候选卡的理由随硬数字送过去了, 研判要逐条回答
# 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。

View File

@ -167,6 +167,39 @@ def _logic_state_or_none(v):
"reasons": _list_or_none(v.get("reasons"), limit=LOGIC_STATE_REASONS_KEEP) or []}
def _events_or_none(v):
"""催化事件归一 (2026-09-08): 上游给 {latest, count, events:[{date, types, orgs, title, n_reports, compound}]}。
只收约定的键, 事件最多五条; 不是字典或没有事件一律 None"""
if not isinstance(v, dict) or not isinstance(v.get("events"), list) or not v["events"]:
return None
evs = []
for e in v["events"][:5]:
if not isinstance(e, dict) or not _text_or_none(e.get("date")):
continue
evs.append({"date": _text_or_none(e.get("date")), "types": _list_or_none(e.get("types")) or [],
"orgs": _list_or_none(e.get("orgs")) or [], "title": _text_or_none(e.get("title")),
"n_reports": _int_or_none(e.get("n_reports")), "compound": bool(e.get("compound"))})
if not evs:
return None
return {"latest": _text_or_none(v.get("latest")) or evs[0]["date"],
"count": _int_or_none(v.get("count")) or len(evs), "events": evs}
_PRICING_NUM_KEYS = ("pre5", "pre20", "gap", "intraday", "close_pos", "vol_ratio", "day_pct")
def _pricing_or_none(v):
"""定价状态归一 (2026-09-08): state 是四情形之一或 None (算不出时 why 写原因), 六个数与涨停标记原样收。"""
if not isinstance(v, dict) or not _text_or_none(v.get("event_date")):
return None
out = {"state": _text_or_none(v.get("state")), "why": _text_or_none(v.get("why")),
"event_date": _text_or_none(v.get("event_date")), "has_event": bool(v.get("has_event")),
"limit_up": (None if v.get("limit_up") is None else bool(v.get("limit_up")))}
for k in _PRICING_NUM_KEYS:
out[k] = _float_or_none(v.get(k))
return out
def _list_or_none(v, limit: int = 0):
"""字符串列表归一: 列表逐项去空白、丢空项, 单个字符串当一项; 缺失或类型不对一律 None。
@ -225,6 +258,12 @@ def _rows(raw, bucket: str) -> list:
# 安全边际三情景的整句 (2026-09-07 第四件): 上游按盈利预测算的悲观/中性/乐观估值与赔率,
# 只给人看 (提议卡与账本), 不参与任何判定, 也不送研判。
"valuation_text": _text_or_none(it.get("valuation_text")),
# 催化事件与定价状态 (2026-09-08《量价研判链吸收方案》3.5): 原值进硬数字给人看与复盘,
# 两句整句送研判 (judge.OPEN_JUDGE_KEYS 收 events_text / pricing_text)。都不进任何判定。
"events": _events_or_none(it.get("events")),
"events_text": _text_or_none(it.get("events_text")),
"pricing_state": _pricing_or_none(it.get("pricing_state")),
"pricing_text": _text_or_none(it.get("pricing_text")),
})
return out
@ -417,7 +456,9 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N
# 逻辑状态 (2026-09-07 第三件): 动作引擎放进硬数字; 判决候选而逻辑存疑的在 scan_open
# 里强制交人。它不改判决、不改排序 —— 收敛规则是单调的 (选股系统 logic_state.apply_to_card)。
"logic_state": r.get("logic_state"),
"valuation_text": r.get("valuation_text")}
"valuation_text": r.get("valuation_text"),
"events": r.get("events"), "events_text": r.get("events_text"),
"pricing_state": r.get("pricing_state"), "pricing_text": r.get("pricing_text")}
for r in passed[:n]]
return {"date": plan.get("date"), "considered": len(pool), "eligible": len(passed),
"items": items, "dropped": dropped, "st_unknown": st_unknown,

View File

@ -1029,6 +1029,11 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<div class="st" v-if="(p.hard_numbers||{}).verdict"><span class="muted">选股系统:</span>{{ propVerdictLine(p) }}</div>
<div class="st muted" v-if="propBasis(p)">{{ propBasis(p) }}</div>
<div class="st" v-if="propLogic(p)"><span class="muted">研报说:</span>{{ propLogic(p) }}<span class="muted" v-if="propLogicMore(p)">(另有 {{ propLogicMore(p) }} 条)</span></div>
<!-- 三句整句都是选股系统写好的中文 (2026-09-07 安全边际、2026-09-08 催化事件与定价状态),
只展示不进判定; 缺就不显示这一行, 不留空壳。 -->
<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 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>
@ -2574,6 +2579,11 @@ createApp({
// 公司好在哪, 每行自带「——出处:《研报标题》 披露日」。卡片窄, 只摆第一条论断,
// 但那一条必须原样显示到出处与日期为止 —— 一条 2024 年的论断与一条上周的, 人得一眼分得出。
function propBasis(p) { return (p.hard_numbers || {}).basis || ''; }
// 硬数字里由上游写好的整句 (安全边际 / 催化事件 / 定价状态): 原样显示, 不是字符串就不显示。
function propText(p, key) {
const v = (p.hard_numbers || {})[key];
return (typeof v === 'string' && v.trim()) ? v : '';
}
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 第三件): 解析层收逻辑状态/硬数字带它而研判
白名单不收/判决候选而逻辑存疑强制交人/持仓存疑停增持侧/研究走弱
减持默认关且开了必交人/同轮减持优先级/早上取回与映射新鲜度/策略
买入腿按来源暂停恢复/持仓视图两栏三情形/假仓库签名/安全边际整句透传/参考目标价 (17 )
买入腿按来源暂停恢复/持仓视图两栏三情形/假仓库签名/安全边际整句透传/参考目标价/催化事件与定价状态透传 (18 )
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) +
目标价到价必定入队 (档位 full 也不自动卖) +
用户设的止损价与目标价单独成列显示 (70 )
678
679
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 654; 又加了页面文案守卫一例, 655; 09-07 审查修复加了跨轮减持等五例, 660; 第二件低把握驳回交人一例, 661; 第三件逻辑状态接入第二十二批十五例, 676; 第四件安全边际整句透传一例, 677; 参考目标价一例, 现为 678)
减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 654; 又加了页面文案守卫一例, 655; 09-07 审查修复加了跨轮减持等五例, 660; 第二件低把握驳回交人一例, 661; 第三件逻辑状态接入第二十二批十五例, 676; 第四件安全边际整句透传一例, 677; 参考目标价一例, 678; 催化事件与定价状态透传一例, 现为 679)
任一子集失败即整体失败 (退出码 1)
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):

View File

@ -438,6 +438,41 @@ def _():
assert nv["state"] == "逻辑成立" and nv["settle_note"] == "维持" and "研报论断" in nv["text"], nv
@case("催化事件与定价状态·解析层归一, 候选与硬数字带原值与整句, 研判白名单只收两句整句 (量价研判链 3.5)")
def _():
ev = {"latest": "2026-09-01", "count": 2, "events": [
{"date": "2026-09-01", "types": ["上调盈利预测", "业绩超预期"], "orgs": ["", ""], "title": "x", "n_reports": 2, "compound": True},
{"date": "2026-08-20", "types": ["深度覆盖"], "orgs": [""], "title": "y", "n_reports": 1, "compound": False},
"不是字典"]}
ps = {"state": "价格发现", "why": "事件前没涨", "event_date": "2026-09-01", "has_event": True, "pre5": 0.01,
"pre20": 0.02, "gap": 0.0, "intraday": 0.01, "close_pos": 0.8, "vol_ratio": 2.0, "day_pct": 0.03,
"limit_up": False, "history_days": 40}
row = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "events": ev, "events_text": "催化事件:…",
"pricing_state": ps, "pricing_text": "定价状态(事件日 2026-09-01价格发现。…"}], "main")[0]
assert row["events"]["count"] == 2 and len(row["events"]["events"]) == 2, row["events"]
assert row["events"]["events"][0]["compound"] and row["events"]["events"][0]["orgs"] == ["", ""]
assert row["pricing_state"]["state"] == "价格发现" and row["pricing_state"]["vol_ratio"] == 2.0
assert "history_days" not in row["pricing_state"] and row["pricing_state"]["limit_up"] is False
assert row["events_text"].startswith("催化事件") and row["pricing_text"].startswith("定价状态")
empty = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "events": None, "pricing_state": {"state": None}}], "main")[0]
assert empty["events"] is None and empty["pricing_state"] is None and empty["pricing_text"] is None
plan = {"date": "2026-09-04", "main": [row], "observe": []}
item = pf.select_candidates(plan, top_n=10)["items"][0]
assert item["events"]["latest"] == "2026-09-01" and item["pricing_state"]["state"] == "价格发现", item
c = {**item, "price": 10.0}
params = {"scale": 1_000_000, "stock_target_default": 0.06, "batch_split": (0.5, 0.25, 0.25)}
with _Patch() as p:
p(ae, "check_all_caps", lambda **kw: [])
p(ae, "_new_name_ctx", lambda caps, c: caps)
cand, why = ae.eval_open(c, params, {"names_count": 0, "max_names": 10}, 500_000)
hn = cand["hard_numbers"]
assert hn["events"]["count"] == 2 and hn["pricing_state"]["state"] == "价格发现", why
sent = jd._judge_hard_numbers("OPEN", hn)
assert sent["events_text"] == row["events_text"] and sent["pricing_text"] == row["pricing_text"], sent
assert "events" not in sent and "pricing_state" not in sent and "valuation_text" not in sent, sent
assert "events_text" in jd.OPEN_JUDGE_KEYS and "pricing_text" in jd.OPEN_JUDGE_KEYS
@case("参考目标价·状态应答里的估值子字典归一, 持仓行只在算得出时给 ref_target, 算不出或缺就不给")
def _():
val = {"neut": 56.05, "pess": 42.64, "opt": 84.96, "price": 50.17, "quarter": "2026Q4", "firms": 8,