行业催化:按票所在环节读基座 segment_catalysts,卡上整句与复盘分组,只展示不进判决(台账 051)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-08 15:21:10 +08:00
parent 4e5cd525a1
commit 81a50734d2
5 changed files with 132 additions and 0 deletions

20
card.py
View File

@ -464,6 +464,26 @@ def news_view(nv) -> str:
return f"相关快讯(近 3 天 {nv.get('count')} 条,财联社电报点名,不判利好利空):" + "".join(parts)
def industry_catalyst_view(lst) -> str:
"""行业催化的整句2026-09-08 台账 051票所在环节的行业级催化量级大→小最多三条。只展示不进判决。"""
if not isinstance(lst, list) or not lst:
return "行业催化:暂无(环节评析未标出行业级催化,或材料未到)"
parts = []
for x in lst:
head = f"{(x.get('event_date') or '日期不明')[5:] if x.get('event_date') else '日期不明'} [{x.get('magnitude')}·{x.get('horizon')}·{x.get('direction')}·把握{x.get('confidence')}] {x.get('segment')}{x.get('title')}"
if x.get("mechanism"):
head += f"{x['mechanism'][:60]}"
parts.append(head)
return f"行业催化(环节级,数据基座按材料标出,{len(lst)} 条,不进判决):" + "".join(parts)
def industry_catalyst_short(lst) -> str:
if not isinstance(lst, list) or not lst:
return ""
x = lst[0]
return f"{x.get('magnitude')}·{x.get('horizon')} {str(x.get('title') or '')[:20]}"
def news_short(nv) -> str:
if not isinstance(nv, dict) or not nv.get("items"):
return ""

10
plan.py
View File

@ -305,6 +305,12 @@ def _assemble_cards(ds: str, codes: list, ev: dict, upside: pd.Series,
# 相关快讯2026-09-08 台账 050财联社电报近 3 天点名这只票的条目,只当上下文送人看与送研判,
# 不判正负、不进判决、不进定价状态。表读不到整体缺席不断产。
news = sources.news_flashes(codes, ds)
# 行业催化2026-09-08 台账 051数据基座按环节评析材料标出的行业级催化挂在环节上
# 按票所在的被指向环节取,只展示加复盘分组,不进判决、不进逻辑状态。表没建就整体缺席。
_seg_of = inp["seg_of"]
ind_cat = sources.catalysts_for_codes(
{k: _seg_of.get(k, []) for k in codes},
sources.segment_catalysts(sorted({t for k in codes for t in _seg_of.get(k, [])}), ds))
if risk is None: # collect 会传入读过一次的名单;单独调用时自己读
try:
risk = factors._risk_set() or set() # noqa: SLF001 —— 同仓自用
@ -342,6 +348,7 @@ def _assemble_cards(ds: str, codes: list, ev: dict, upside: pd.Series,
"events": events.get(k), "events_text": card.events_view(events.get(k)),
"pricing_state": card.pricing_state(ev_fields.get(k)),
"news": news.get(k), "news_text": card.news_view(news.get(k)),
"industry_catalyst": ind_cat.get(k), "industry_catalyst_text": card.industry_catalyst_view(ind_cat.get(k)),
"theme": theme, "n_sources": n_sources, "chain_fit": evd["chain_fit"],
"started_source": "moved_view" if mv else None,
"logic_claims": evd["logic"],
@ -476,6 +483,9 @@ def collect(date: str | None = None, top: int = 20, obs_top: int = 10,
pricing_text=card.pricing_view(c.get("pricing_state")),
# 相关快讯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"),
card={"pct0": c.get("pct0"), "net_z": c.get("net_z"),
"heat_chg": c.get("heat_chg"), "accum": c.get("accum"),
"night": c.get("night"), "gates": c.get("gates"),

View File

@ -449,6 +449,15 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str
if s:
rows.append({"date": day, "h": h, "list": lst_name, "group": f"催化事件={label}",
"regime_post": regime_post, "regime_pre": regime_pre, **s})
# 行业催化有无2026-09-08 台账 051环节级催化只作观察分组材料未到时全部落"无"。
ind_of = {r["code"]: bool(r.get("industry_catalyst")) for r in main_rows + obs_rows}
for lst_name, lst_codes in (("候选单", cands), ("档位表", list(ind_of))):
for flag, label in ((True, ""), (False, "")):
codes = [c for c in lst_codes if ind_of.get(c, False) is flag]
s = summarize(ret, codes, base_all, base_main, caps, cap_base)
if s:
rows.append({"date": day, "h": h, "list": lst_name, "group": f"行业催化={label}",
"regime_post": regime_post, "regime_pre": regime_pre, **s})
notes.append(f"{day}: 主榜 {len(main_codes)} 观察 {len(obs_rows)} 候选 {len(cands)} "
f"关注 {len(watch)} 生产 {len(prod)}{prod_note}"
f"账本 机器通过 {len(ledger['机器通过名单'])} 人批 {len(ledger['人批名单'])} "

View File

@ -1103,3 +1103,68 @@ def news_flashes(codes, ds: str, *, days: int = NEWS_WINDOW_DAYS, read_mysql=Non
slot["items"].append(item)
return out
# ---------------------------------------------------------------------------
# 行业催化2026-09-08台账 051数据基座按环节评析的材料另标出来的"行业级催化事件"
# 挂在环节上不挂个股(表 segment_catalysts基座 PG。候选卡按票所在的被指向环节取
# 只展示、只分组复盘,不进判决、不进逻辑状态。表没建或没材料就整体缺席,不断产。
# ---------------------------------------------------------------------------
CATALYST_WINDOW_DAYS = 180
CATALYST_MAX_PER_SEGMENT = 3
_MAG_ORDER = {"": 0, "": 1, "": 2}
def segment_catalysts(segments, ds: str, *, days: int = CATALYST_WINDOW_DAYS, read_pg=None,
limit_per_segment: int = CATALYST_MAX_PER_SEGMENT) -> dict:
"""{环节名: [{segment, event_date, title, direction, magnitude, horizon, confidence, mechanism}, ...]}
每个环节按量级大事件日新旧排最多 limit_per_segment """
rp = read_pg or db.read_pg
want = sorted({str(x) for x in (segments or []) if x})
if not want:
return {}
try:
since = (dt.date.fromisoformat(ds) - dt.timedelta(days=int(days))).isoformat()
except ValueError:
return {}
try:
df = rp("SELECT segment_name, event_date, first_seen, title, direction, magnitude, horizon, "
"confidence, mechanism FROM segment_catalysts "
"WHERE segment_name = ANY(%s) AND coalesce(event_date, first_seen) >= %s",
(want, since))
except Exception as e: # noqa: BLE001 —— 表没建、连接失败:整体缺席
logging.getLogger("bridge.catalyst").warning("segment_catalysts 读不到: %r", e)
return {}
rows = []
for r in df.to_dict("records"):
ev = r.get("event_date") or r.get("first_seen")
rows.append({"segment": str(r.get("segment_name")), "event_date": str(ev)[:10] if ev else None,
"title": str(r.get("title") or "")[:80], "direction": r.get("direction") or "利好",
"magnitude": r.get("magnitude") or "", "horizon": r.get("horizon") or "短期",
"confidence": r.get("confidence") or "",
"mechanism": (str(r.get("mechanism") or "")[:120] or None)})
rows.sort(key=lambda x: (_MAG_ORDER.get(x["magnitude"], 9), x["event_date"] or ""), reverse=False)
rows.sort(key=lambda x: (_MAG_ORDER.get(x["magnitude"], 9), -(int((x["event_date"] or "0000-00-00").replace("-", "") or 0))))
out: dict = {}
for x in rows:
slot = out.setdefault(x["segment"], [])
if len(slot) < limit_per_segment:
slot.append(x)
return out
def catalysts_for_codes(seg_of: dict, cat_by_seg: dict, *, limit: int = 3) -> dict:
"""按票汇总所在环节的行业催化:同标题去重,量级大→小,最多 limit 条。没有的票不在字典里。"""
out: dict = {}
for k, segs in (seg_of or {}).items():
seen: set = set()
lst = []
for seg in segs or []:
for x in cat_by_seg.get(seg) or []:
if x["title"] in seen:
continue
seen.add(x["title"])
lst.append(x)
if lst:
lst.sort(key=lambda x: (_MAG_ORDER.get(x["magnitude"], 9), -(int((x["event_date"] or "0000-00-00").replace("-", "") or 0))))
out[k] = lst[:limit]
return out

View File

@ -230,11 +230,39 @@ def test_news():
t("短句是最新一条", card.news_short(out["SH688172"]).startswith("09-08 12:39 燕东微"))
def test_industry_catalyst():
"""行业催化(台账 051按环节取、按票汇总、量级排序、缺席不断产。"""
rows = [
{"segment_name": "端侧AI芯片", "event_date": "2026-09-05", "first_seen": "2026-09-06", "title": "2B 模型媲美百亿模型",
"direction": "利好", "magnitude": "", "horizon": "长期", "confidence": "", "mechanism": "端侧推理可行性大增"},
{"segment_name": "端侧AI芯片", "event_date": None, "first_seen": "2026-09-01", "title": "某厂扩产",
"direction": "利好", "magnitude": "", "horizon": "短期", "confidence": "", "mechanism": None},
{"segment_name": "存储", "event_date": "2026-09-07", "first_seen": "2026-09-07", "title": "2B 模型媲美百亿模型",
"direction": "利好", "magnitude": "", "horizon": "中期", "confidence": "", "mechanism": "端侧内存需求上移"},
]
calls = []
def rp(sql, params=None):
calls.append(params); return _DF(rows)
by_seg = sources.segment_catalysts(["端侧AI芯片", "存储", "无材料环节"], "2026-09-08", read_pg=rp)
t("按环节名与 180 天窗口查", calls[0][0] == ["存储", "无材料环节", "端侧AI芯片"] and calls[0][1] == "2026-03-12")
t("量级大的排前、没事件日的用首见日", by_seg["端侧AI芯片"][0]["magnitude"] == "" and by_seg["端侧AI芯片"][1]["event_date"] == "2026-09-01")
t("没材料的环节不在字典里", "无材料环节" not in by_seg)
per = sources.catalysts_for_codes({"SH600000": ["端侧AI芯片", "存储"], "SZ000001": ["无材料环节"], "SZ000002": []}, by_seg)
t("按票汇总同标题去重、量级大→小", [x["title"] for x in per["SH600000"]] == ["2B 模型媲美百亿模型", "某厂扩产"])
t("所在环节没有催化的票不在字典里", "SZ000001" not in per and "SZ000002" not in per)
v = card.industry_catalyst_view(per["SH600000"])
t("整句带量级持续期方向与环节名", v.startswith("行业催化(环节级") and "[大·长期·利好·把握中] 端侧AI芯片" in v)
t("缺席整句与短句", "暂无" in card.industry_catalyst_view(None) and card.industry_catalyst_short([]) == "")
def boom(*a, **k): raise RuntimeError("no table")
t("表读不到整体缺席不断产", sources.segment_catalysts(["端侧AI芯片"], "2026-09-08", read_pg=boom) == {})
def main():
test_events()
test_event_day_fields()
test_pricing_state()
test_news()
test_industry_catalyst()
print("ALL OK — 四类事件 / 事件日字段 / 定价状态四情形 / 卡上文字 / 相关快讯 全部通过")