plan v2: 每主题限额(默认5, 防单板块刷屏) + 预期空间列现算兜底

(当日 upside 表在 18:40 建表时行情未到、常为空——读表改为空则现算)
This commit is contained in:
zlt 2026-07-30 12:55:24 +08:00
parent 83f2b1409f
commit dc201905f6
2 changed files with 41 additions and 7 deletions

44
plan.py
View File

@ -94,7 +94,27 @@ def _num(v) -> str:
return "" if v is None or pd.isna(v) else f"{v:.2f}" 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") ds = date or _latest_date("t_factor_akg_score")
if not ds: if not ds:
raise SystemExit("t_factor_akg_score 还没有数据——先 build akg_score。") 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: if score.empty or gate.empty:
raise SystemExit(f"{ds} 缺 akg_score / akg_gate——先 build 该日再出计划。") raise SystemExit(f"{ds} 缺 akg_score / akg_gate——先 build 该日再出计划。")
upside = _factor("t_factor_akg_upside", ds) 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) hd = _latest_date("t_factor_akg_heat", ds)
heat = _factor("t_factor_akg_heat", hd) if hd else pd.Series(dtype=float) heat = _factor("t_factor_akg_heat", hd) if hd else pd.Series(dtype=float)
names = _names() names = _names()
@ -119,23 +148,26 @@ def generate(date: str | None = None, top: int = 20, obs_top: int = 10) -> str:
f"\"谁已经动了\"看的是上个交易日收盘;拍点方案定版前均如此)。") f"\"谁已经动了\"看的是上个交易日收盘;拍点方案定版前均如此)。")
L.append("") 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("| # | 代码 | 名称 | 总分 | 档位 | 传导证据 | 热度 | 预期空间 |") 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) e = ev.get(k)
etxt = f"{e[0]}{e[1]} 源,已动 {e[2]:.0%}" if e else "" 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)} " L.append(f"| {i} | {k} | {names.get(k, '')} | {s:.1f} | {_tier_label(s)} "
f"| {etxt} | {_num(heat.get(k))} | {_pct(upside.get(k))} |") f"| {etxt} | {_num(heat.get(k))} | {_pct(upside.get(k))} |")
L.append("") L.append("")
L.append(f"## 观察档 Top {min(obs_top, len(obs))}" obs_rows = _pick(obs, ev, obs_top, theme_cap)
f"(无券商预期、但在传导链上——没有估值锚,置信度低)") L.append(f"## 观察档 Top {len(obs_rows)}"
f"(无券商预期、但在传导链上——没有估值锚,置信度低{cap_txt}")
L.append("") L.append("")
L.append("| # | 代码 | 名称 | 分 | 传导证据 | 热度 |") 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) e = ev.get(k)
etxt = f"{e[0]}{e[1]} 源,已动 {e[2]:.0%}" if e else "" etxt = f"{e[0]}{e[1]} 源,已动 {e[2]:.0%}" if e else ""
L.append(f"| {i} | {k} | {names.get(k, '')} | {s:.1f} " L.append(f"| {i} | {k} | {names.get(k, '')} | {s:.1f} "

4
run.py
View File

@ -145,6 +145,8 @@ def main():
pl.add_argument("--date", help="默认取 score 表最新日") pl.add_argument("--date", help="默认取 score 表最新日")
pl.add_argument("--top", type=int, default=20, help="主榜条数") pl.add_argument("--top", type=int, default=20, help="主榜条数")
pl.add_argument("--obs-top", type=int, default=10, 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 = sub.add_parser("probe")
p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"], p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"],
default="all", default="all",
@ -178,7 +180,7 @@ def main():
cmd_register() cmd_register()
elif a.cmd == "plan": elif a.cmd == "plan":
import 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": elif a.cmd == "tracks":
import tracks import tracks
tracks.coverage_report() tracks.coverage_report()