行业催化:按票所在环节读基座 segment_catalysts,卡上整句与复盘分组,只展示不进判决(台账 051)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
4e5cd525a1
commit
81a50734d2
20
card.py
20
card.py
|
|
@ -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
10
plan.py
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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['人批名单'])} "
|
||||
|
|
|
|||
65
sources.py
65
sources.py
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 — 四类事件 / 事件日字段 / 定价状态四情形 / 卡上文字 / 相关快讯 全部通过")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue