量价研判链 3.5:催化事件与定价状态随候选进硬数字与提议卡,两句整句送研判;提议卡补显示安全边际整句(第二十二批加一例,全套 679)
This commit is contained in:
parent
48a562f170
commit
7d670b364c
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)。候选卡的理由随硬数字送过去了, 研判要逐条回答
|
||||
# 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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] : '';
|
||||
|
|
|
|||
|
|
@ -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 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue