From 499b1342e478ad5fbda831576318425e52e54919 Mon Sep 17 00:00:00 2001 From: zlt Date: Tue, 8 Sep 2026 15:21:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=8C=E4=B8=9A=E5=82=AC=E5=8C=96=E5=8F=AA?= =?UTF-8?q?=E8=BF=9B=E6=8F=90=E8=AE=AE=E5=8D=A1=E4=B8=8E=E8=B4=A6=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E9=80=81=E7=A0=94=E5=88=A4=EF=BC=9B=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=EF=BC=88=E5=8F=B0=E8=B4=A6=20051=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- app/core/action_engine.py | 2 ++ app/services/plan_feed.py | 23 ++++++++++++++++++++++- app/web/static/index.html | 1 + scripts/test_batch22_units.py | 26 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/core/action_engine.py b/app/core/action_engine.py index 1d08078..a995d6a 100644 --- a/app/core/action_engine.py +++ b/app/core/action_engine.py @@ -695,6 +695,8 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float): "pricing_state": c.get("pricing_state"), "pricing_text": c.get("pricing_text"), # 相关快讯 (2026-09-08 台账 050): 原值进账本与提议卡, 整句另送研判。只是上下文, 不进判定。 "news": c.get("news"), "news_text": c.get("news_text"), + # 行业催化 (2026-09-08 台账 051): 环节级, 原值进账本与提议卡; 产业逻辑不送研判。 + "industry_catalyst": c.get("industry_catalyst"), "industry_catalyst_text": c.get("industry_catalyst_text"), # ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉, # 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。 "target_pct": target_pct, "target_amount": round(full_amt, 2), diff --git a/app/services/plan_feed.py b/app/services/plan_feed.py index 6d28e4b..445658c 100644 --- a/app/services/plan_feed.py +++ b/app/services/plan_feed.py @@ -189,6 +189,22 @@ def _news_or_none(v, limit=5): return {"count": count, "latest": _text_or_none(v.get("latest")), "items": items} +def _industry_catalyst_or_none(v, limit=3): + """行业催化 (2026-09-08 台账 051): 上游按环节标出的行业级催化, 只留最多 limit 条固定键。 + 产业逻辑不送研判 (judge.OPEN_JUDGE_KEYS 不收), 只进硬数字给人看与复盘。""" + if not isinstance(v, list) or not v: + return None + out = [] + for it in v: + if not isinstance(it, dict) or not it.get("title"): + continue + out.append({k: _text_or_none(it.get(k)) for k in + ("segment", "event_date", "title", "direction", "magnitude", "horizon", "confidence", "mechanism")}) + if len(out) >= limit: + break + return out or None + + def _events_or_none(v): """催化事件归一 (2026-09-08): 上游给 {latest, count, events:[{date, types, orgs, title, n_reports, compound}]}。 只收约定的键, 事件最多五条; 不是字典或没有事件一律 None。""" @@ -290,6 +306,9 @@ def _rows(raw, bucket: str) -> list: # (judge.OPEN_JUDGE_KEYS 收 news_text)。只是上下文, 不判正负, 不进任何判定。 "news": _news_or_none(it.get("news")), "news_text": _text_or_none(it.get("news_text")), + # 行业催化 (2026-09-08 台账 051): 环节级, 只给人看与复盘, 不送研判、不进判定。 + "industry_catalyst": _industry_catalyst_or_none(it.get("industry_catalyst")), + "industry_catalyst_text": _text_or_none(it.get("industry_catalyst_text")), }) return out @@ -485,7 +504,9 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N "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"), - "news": r.get("news"), "news_text": r.get("news_text")} + "news": r.get("news"), "news_text": r.get("news_text"), + "industry_catalyst": r.get("industry_catalyst"), + "industry_catalyst_text": r.get("industry_catalyst_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, diff --git a/app/web/static/index.html b/app/web/static/index.html index 089cd2c..eb0890d 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1035,6 +1035,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
{{ propText(p, 'events_text') }}
{{ propText(p, 'pricing_text') }}
{{ propText(p, 'news_text') }}
+
{{ propText(p, 'industry_catalyst_text') }}
择时层两个期限:{{ propHeads(p) }}
{{ propWhy(p) }}
约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }} diff --git a/scripts/test_batch22_units.py b/scripts/test_batch22_units.py index b5ba62b..b07a730 100644 --- a/scripts/test_batch22_units.py +++ b/scripts/test_batch22_units.py @@ -499,6 +499,32 @@ def test_news_passthrough(): assert "news_text" in jd.OPEN_JUDGE_KEYS +@case("行业催化: 裁到三条固定键进硬数字与提议卡, 不送研判 (台账 051)") +def test_industry_catalyst_passthrough(): + lst = [{"segment": f"环节{i}", "event_date": "2026-09-05", "title": f"催化{i}", "direction": "利好", + "magnitude": "大", "horizon": "长期", "confidence": "中", "mechanism": "m", "extra": 1} for i in range(4)] + row = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "industry_catalyst": lst, + "industry_catalyst_text": "行业催化(环节级,数据基座按材料标出,4 条,不进判决):…"}], "main")[0] + assert len(row["industry_catalyst"]) == 3 and "extra" not in row["industry_catalyst"][0] + assert row["industry_catalyst_text"].startswith("行业催化") + empty = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "industry_catalyst": [], "industry_catalyst_text": None}], "main")[0] + assert empty["industry_catalyst"] is None and empty["industry_catalyst_text"] is None + plan = {"date": "2026-09-08", "main": [row], "observe": []} + item = pf.select_candidates(plan, top_n=10)["items"][0] + assert item["industry_catalyst"][0]["segment"] == "环节0", 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["industry_catalyst_text"] == row["industry_catalyst_text"], why + sent = jd._judge_hard_numbers("OPEN", hn) + assert "industry_catalyst" not in sent and "industry_catalyst_text" not in sent, sent + assert "industry_catalyst_text" not 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,