plan v2: 每主题限额(默认5, 防单板块刷屏) + 预期空间列现算兜底
(当日 upside 表在 18:40 建表时行情未到、常为空——读表改为空则现算)
This commit is contained in:
parent
83f2b1409f
commit
dc201905f6
44
plan.py
44
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} "
|
||||
|
|
|
|||
4
run.py
4
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue