diff --git a/app/core/action_engine.py b/app/core/action_engine.py index 553329d..1d08078 100644 --- a/app/core/action_engine.py +++ b/app/core/action_engine.py @@ -693,6 +693,8 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float): # (它们是事件与量价, 不是产业逻辑, 见 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"), + # 相关快讯 (2026-09-08 台账 050): 原值进账本与提议卡, 整句另送研判。只是上下文, 不进判定。 + "news": c.get("news"), "news_text": c.get("news_text"), # ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉, # 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。 "target_pct": target_pct, "target_amount": round(full_amt, 2), diff --git a/app/services/judge.py b/app/services/judge.py index e84f1e4..6c1d832 100644 --- a/app/services/judge.py +++ b/app/services/judge.py @@ -48,7 +48,10 @@ OPEN_JUDGE_KEYS = ("price", "score", "theme", "tier", "upside", "heat", # 正是研报里决策智能体的"事件上下文", 不是产业逻辑 —— 与判决依据、论断、逻辑状态、 # 安全边际那几项不同, 这两句要送。送整句不送字典: 研判提示词把硬数字逐行印成 # "键: 值", 字典会印成一串代码, 整句模型才读得顺。 - "events_text", "pricing_text") + "events_text", "pricing_text", + # 相关快讯整句 (2026-09-08 台账 050): 财联社电报近 3 天点名这只票的条目。它是市场 + # 反应与公司快讯, 不是产业逻辑; 正负不由 PMS 判, 交给研判连同事件上下文一起看。 + "news_text") # 新建仓送研判时的必答题 (2026-09-03)。候选卡的理由随硬数字送过去了, 研判要逐条回答 # 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。 diff --git a/app/services/plan_feed.py b/app/services/plan_feed.py index 849eb1b..6d28e4b 100644 --- a/app/services/plan_feed.py +++ b/app/services/plan_feed.py @@ -167,6 +167,28 @@ def _logic_state_or_none(v): "reasons": _list_or_none(v.get("reasons"), limit=LOGIC_STATE_REASONS_KEEP) or []} +def _news_or_none(v, limit=5): + """相关快讯 (2026-09-08 台账 050): 上游按财联社电报点名算的条目, 只留 count/latest 与最多 limit 条 + {time, level, title, url}。只是上下文: 进硬数字给人看, 整句送研判; 不进任何判定。""" + if not isinstance(v, dict) or not v.get("items"): + return None + items = [] + for it in v.get("items") or []: + if not isinstance(it, dict): + continue + items.append({"time": _text_or_none(it.get("time")), "level": _text_or_none(it.get("level")), + "title": _text_or_none(it.get("title")), "url": _text_or_none(it.get("url"))}) + if len(items) >= limit: + break + if not items: + return None + try: + count = int(v.get("count") or len(items)) + except (TypeError, ValueError): + count = len(items) + return {"count": count, "latest": _text_or_none(v.get("latest")), "items": items} + + def _events_or_none(v): """催化事件归一 (2026-09-08): 上游给 {latest, count, events:[{date, types, orgs, title, n_reports, compound}]}。 只收约定的键, 事件最多五条; 不是字典或没有事件一律 None。""" @@ -264,6 +286,10 @@ def _rows(raw, bucket: str) -> list: "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")), + # 相关快讯 (2026-09-08 台账 050): 财联社电报点名这只票的条目, 原值给人看, 整句送研判 + # (judge.OPEN_JUDGE_KEYS 收 news_text)。只是上下文, 不判正负, 不进任何判定。 + "news": _news_or_none(it.get("news")), + "news_text": _text_or_none(it.get("news_text")), }) return out @@ -458,7 +484,8 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N "logic_state": r.get("logic_state"), "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")} + "pricing_state": r.get("pricing_state"), "pricing_text": r.get("pricing_text"), + "news": r.get("news"), "news_text": r.get("news_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 126412a..089cd2c 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') }}
+
{{ propText(p, 'news_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 e72e664..b5ba62b 100644 --- a/scripts/test_batch22_units.py +++ b/scripts/test_batch22_units.py @@ -473,6 +473,32 @@ def _(): assert "events_text" in jd.OPEN_JUDGE_KEYS and "pricing_text" in jd.OPEN_JUDGE_KEYS +@case("相关快讯: 原值裁到五条进硬数字, 整句送研判, 缺了就是空 (台账 050)") +def test_news_passthrough(): + items = [{"time": f"09-08 13:0{i}", "level": "C", "title": f"快讯{i}", "url": f"u{i}", "extra": 1} for i in range(7)] + nv = {"count": 7, "latest": "09-08 13:06", "items": items} + row = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "news": nv, + "news_text": "相关快讯(近 3 天 7 条,财联社电报点名,不判利好利空):…"}], "main")[0] + assert row["news"]["count"] == 7 and len(row["news"]["items"]) == 5 and "extra" not in row["news"]["items"][0] + assert row["news_text"].startswith("相关快讯") + empty = pf._rows([{"code": "600000.SH", "rank": 1, "score": 210, "news": {"count": 0, "items": []}}], "main")[0] + assert empty["news"] is None and empty["news_text"] is None + plan = {"date": "2026-09-08", "main": [row], "observe": []} + item = pf.select_candidates(plan, top_n=10)["items"][0] + assert item["news"]["latest"] == "09-08 13:06", 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["news"]["count"] == 7 and hn["news_text"] == row["news_text"], why + sent = jd._judge_hard_numbers("OPEN", hn) + assert sent["news_text"] == row["news_text"] and "news" not in sent, sent + assert "news_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,