From dc201905f69ac3b1d1c283fe04e24a15eded0cda Mon Sep 17 00:00:00 2001 From: zlt Date: Thu, 30 Jul 2026 12:55:24 +0800 Subject: [PATCH] =?UTF-8?q?plan=20v2:=20=E6=AF=8F=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E9=99=90=E9=A2=9D(=E9=BB=98=E8=AE=A45,=20=E9=98=B2=E5=8D=95?= =?UTF-8?q?=E6=9D=BF=E5=9D=97=E5=88=B7=E5=B1=8F)=20+=20=E9=A2=84=E6=9C=9F?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=88=97=E7=8E=B0=E7=AE=97=E5=85=9C=E5=BA=95?= =?UTF-8?q?=20=EF=BC=88=E5=BD=93=E6=97=A5=20upside=20=E8=A1=A8=E5=9C=A8=20?= =?UTF-8?q?18:40=20=E5=BB=BA=E8=A1=A8=E6=97=B6=E8=A1=8C=E6=83=85=E6=9C=AA?= =?UTF-8?q?=E5=88=B0=E3=80=81=E5=B8=B8=E4=B8=BA=E7=A9=BA=E2=80=94=E2=80=94?= =?UTF-8?q?=E8=AF=BB=E8=A1=A8=E6=94=B9=E4=B8=BA=E7=A9=BA=E5=88=99=E7=8E=B0?= =?UTF-8?q?=E7=AE=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plan.py | 44 ++++++++++++++++++++++++++++++++++++++------ run.py | 4 +++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/plan.py b/plan.py index c2c89f4..d83a933 100644 --- a/plan.py +++ b/plan.py @@ -94,7 +94,27 @@ def _num(v) -> str: return "—" if v is None or pd.isna(v) else f"{v:.2f}" -def generate(date: str | None = None, top: int = 20, obs_top: int = 10) -> str: +def _pick(ranked: pd.Series, ev: dict, top: int, theme_cap: int): + """按分数从高到低取 top 条;每个传导主题最多 theme_cap 条(0=不设限)。 + + 为什么设限:传导目标是环节级,同环节全体成员共享同一条证据,不设限时 + 榜单会被三四个环节刷屏——20 个名额实际只是 4 注。限额后变成 + "多条线索 × 每条线索取最冷最便宜的几只",被挤掉的仍在完整档位表里。""" + out, cnt = [], {} + for k, s in ranked.items(): + e = ev.get(k) + theme = e[0] if e else "(无传导)" + if theme_cap and cnt.get(theme, 0) >= theme_cap: + continue + cnt[theme] = cnt.get(theme, 0) + 1 + out.append((k, s)) + if len(out) >= top: + break + return out + + +def generate(date: str | None = None, top: int = 20, obs_top: int = 10, + theme_cap: int = 5) -> str: ds = date or _latest_date("t_factor_akg_score") if not ds: raise SystemExit("t_factor_akg_score 还没有数据——先 build akg_score。") @@ -103,6 +123,15 @@ def generate(date: str | None = None, top: int = 20, obs_top: int = 10) -> str: if score.empty or gate.empty: raise SystemExit(f"{ds} 缺 akg_score / akg_gate——先 build 该日再出计划。") upside = _factor("t_factor_akg_upside", ds) + if upside.empty: + # 晚间 18:40 建当日 upside 表时行情源(~19:50 发布)还没到,当日表常为空。 + # 这里现算兜底:此刻库里已有晚到的当日价,as-of 口径不变(consensus<=当日)。 + import factors + df_up = factors.build_upside(ds, ds) + if df_up is not None and not df_up.empty: + x = df_up.copy() + x["k"] = x["stock_code"].map(common.to_prefix) + upside = x.groupby("k")["factor_value"].max().astype(float) hd = _latest_date("t_factor_akg_heat", ds) heat = _factor("t_factor_akg_heat", hd) if hd else pd.Series(dtype=float) names = _names() @@ -119,23 +148,26 @@ def generate(date: str | None = None, top: int = 20, obs_top: int = 10) -> str: f"\"谁已经动了\"看的是上个交易日收盘;拍点方案定版前均如此)。") L.append("") - L.append(f"## 主榜 Top {min(top, len(main))}(有券商预期、目标价不低于现价)") + cap_txt = f",每主题限额 {theme_cap}" if theme_cap else "" + main_rows = _pick(main, ev, top, theme_cap) + L.append(f"## 主榜 Top {len(main_rows)}(有券商预期、目标价不低于现价{cap_txt})") L.append("") L.append("| # | 代码 | 名称 | 总分 | 档位 | 传导证据 | 热度 | 预期空间 |") L.append("|---|------|------|------|------|----------|------|----------|") - for i, (k, s) in enumerate(main.head(top).items(), 1): + for i, (k, s) in enumerate(main_rows, 1): e = ev.get(k) etxt = f"{e[0]}({e[1]} 源,已动 {e[2]:.0%})" if e else "—" L.append(f"| {i} | {k} | {names.get(k, '—')} | {s:.1f} | {_tier_label(s)} " f"| {etxt} | {_num(heat.get(k))} | {_pct(upside.get(k))} |") L.append("") - L.append(f"## 观察档 Top {min(obs_top, len(obs))}" - f"(无券商预期、但在传导链上——没有估值锚,置信度低)") + obs_rows = _pick(obs, ev, obs_top, theme_cap) + L.append(f"## 观察档 Top {len(obs_rows)}" + f"(无券商预期、但在传导链上——没有估值锚,置信度低{cap_txt})") L.append("") L.append("| # | 代码 | 名称 | 分 | 传导证据 | 热度 |") L.append("|---|------|------|----|----------|------|") - for i, (k, s) in enumerate(obs.head(obs_top).items(), 1): + for i, (k, s) in enumerate(obs_rows, 1): e = ev.get(k) etxt = f"{e[0]}({e[1]} 源,已动 {e[2]:.0%})" if e else "—" L.append(f"| {i} | {k} | {names.get(k, '—')} | {s:.1f} " diff --git a/run.py b/run.py index 3769d60..6de2126 100644 --- a/run.py +++ b/run.py @@ -145,6 +145,8 @@ def main(): pl.add_argument("--date", help="默认取 score 表最新日") pl.add_argument("--top", type=int, default=20, help="主榜条数") pl.add_argument("--obs-top", type=int, default=10, help="观察档条数") + pl.add_argument("--theme-cap", type=int, default=5, + help="每个传导主题最多几条(防单板块刷屏;0=不设限)") p = sub.add_parser("probe") p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"], default="all", @@ -178,7 +180,7 @@ def main(): cmd_register() elif a.cmd == "plan": import plan - plan.generate(a.date, a.top, a.obs_top) + plan.generate(a.date, a.top, a.obs_top, a.theme_cap) elif a.cmd == "tracks": import tracks tracks.coverage_report()