相关快讯透传:解析层、硬数字、送研判白名单、提议卡一行、单测(台账 050)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-08 14:07:19 +08:00
parent ac78693987
commit 7dcf039cf3
5 changed files with 61 additions and 2 deletions

View File

@ -693,6 +693,8 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float):
# (它们是事件与量价, 不是产业逻辑, 见 judge.OPEN_JUDGE_KEYS)。不参与任何判定。 # (它们是事件与量价, 不是产业逻辑, 见 judge.OPEN_JUDGE_KEYS)。不参与任何判定。
"events": c.get("events"), "events_text": c.get("events_text"), "events": c.get("events"), "events_text": c.get("events_text"),
"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): 原值进账本与提议卡, 整句另送研判。只是上下文, 不进判定。
"news": c.get("news"), "news_text": c.get("news_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

@ -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)。候选卡的理由随硬数字送过去了, 研判要逐条回答 # 新建仓送研判时的必答题 (2026-09-03)。候选卡的理由随硬数字送过去了, 研判要逐条回答
# 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。 # 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。

View File

@ -167,6 +167,28 @@ def _logic_state_or_none(v):
"reasons": _list_or_none(v.get("reasons"), limit=LOGIC_STATE_REASONS_KEEP) or []} "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): 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"""
@ -264,6 +286,10 @@ def _rows(raw, bucket: str) -> list:
"events_text": _text_or_none(it.get("events_text")), "events_text": _text_or_none(it.get("events_text")),
"pricing_state": _pricing_or_none(it.get("pricing_state")), "pricing_state": _pricing_or_none(it.get("pricing_state")),
"pricing_text": _text_or_none(it.get("pricing_text")), "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 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"), "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"), "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]] 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

@ -1034,6 +1034,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<div class="st muted" v-if="propText(p, 'valuation_text')">{{ propText(p, 'valuation_text') }}</div> <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, '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" 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

@ -473,6 +473,32 @@ def _():
assert "events_text" in jd.OPEN_JUDGE_KEYS and "pricing_text" in jd.OPEN_JUDGE_KEYS 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, 算不出或缺就不给") @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,