行业催化只进提议卡与账本,不送研判;单测(台账 051)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-08 15:21:21 +08:00
parent 7dcf039cf3
commit 499b1342e4
4 changed files with 51 additions and 1 deletions

View File

@ -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"), "pricing_state": c.get("pricing_state"), "pricing_text": c.get("pricing_text"),
# 相关快讯 (2026-09-08 台账 050): 原值进账本与提议卡, 整句另送研判。只是上下文, 不进判定。 # 相关快讯 (2026-09-08 台账 050): 原值进账本与提议卡, 整句另送研判。只是上下文, 不进判定。
"news": c.get("news"), "news_text": c.get("news_text"), "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 送研判时会把这几项过滤掉, # ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉,
# 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。 # 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。
"target_pct": target_pct, "target_amount": round(full_amt, 2), "target_pct": target_pct, "target_amount": round(full_amt, 2),

View File

@ -189,6 +189,22 @@ def _news_or_none(v, limit=5):
return {"count": count, "latest": _text_or_none(v.get("latest")), "items": items} 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): def _events_or_none(v):
"""催化事件归一 (2026-09-08): 上游给 {latest, count, events:[{date, types, orgs, title, n_reports, compound}]}。 """催化事件归一 (2026-09-08): 上游给 {latest, count, events:[{date, types, orgs, title, n_reports, compound}]}。
只收约定的键, 事件最多五条; 不是字典或没有事件一律 None""" 只收约定的键, 事件最多五条; 不是字典或没有事件一律 None"""
@ -290,6 +306,9 @@ def _rows(raw, bucket: str) -> list:
# (judge.OPEN_JUDGE_KEYS 收 news_text)。只是上下文, 不判正负, 不进任何判定。 # (judge.OPEN_JUDGE_KEYS 收 news_text)。只是上下文, 不判正负, 不进任何判定。
"news": _news_or_none(it.get("news")), "news": _news_or_none(it.get("news")),
"news_text": _text_or_none(it.get("news_text")), "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 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"), "valuation_text": r.get("valuation_text"),
"events": r.get("events"), "events_text": r.get("events_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")} "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]] for r in passed[:n]]
return {"date": plan.get("date"), "considered": len(pool), "eligible": len(passed), return {"date": plan.get("date"), "considered": len(pool), "eligible": len(passed),
"items": items, "dropped": dropped, "st_unknown": st_unknown, "items": items, "dropped": dropped, "st_unknown": st_unknown,

View File

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

View File

@ -499,6 +499,32 @@ def test_news_passthrough():
assert "news_text" in jd.OPEN_JUDGE_KEYS 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, 算不出或缺就不给") @case("参考目标价·状态应答里的估值子字典归一, 持仓行只在算得出时给 ref_target, 算不出或缺就不给")
def _(): def _():
val = {"neut": 56.05, "pess": 42.64, "opt": 84.96, "price": 50.17, "quarter": "2026Q4", "firms": 8, val = {"neut": 56.05, "pess": 42.64, "opt": 84.96, "price": 50.17, "quarter": "2026Q4", "firms": 8,