丙路加覆盖基数门槛:机构太少时收缩比例不作数

2026-09-04 全市场实测发现的:六只判为逻辑存疑的票里有五只,触发是覆盖机构从三家
减到两家,也就是一家机构那两周没发报告就构成 33% 的收缩。机构数这么少时百分比不
说明任何事,而逻辑存疑会出减持提议,误判的代价是让人对着噪声做裁决。

加一条基数门槛,默认前窗口至少五家。它的性质是:五家时走掉一家只有 20%、够不着
30% 那条线,必须走掉两家才算——这正是"覆盖在收缩"该有的含义。基数不够时预测下修
照样记转弱,只是不带硬触发,于是整体落到无法判断进人工复核,而不是出减持提议。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-04 09:15:31 +08:00
parent 86727827ae
commit ac7832620d
2 changed files with 22 additions and 2 deletions

View File

@ -243,7 +243,7 @@ def from_judgement(cur, prev=None, *, stale_days: int = 60) -> dict:
def from_broker(now, prev, *, as_of: str | None = None, min_firms: int = 2, def from_broker(now, prev, *, as_of: str | None = None, min_firms: int = 2,
eps_drop: float = 0.05, eps_rise: float = 0.05, eps_drop: float = 0.05, eps_rise: float = 0.05,
cover_drop: float = 0.30) -> dict: cover_drop: float = 0.30, cover_min_base: int = 5) -> dict:
"""把同一财年同一预测期的每股收益预测中位数与覆盖机构数,归一成一路信号。 """把同一财年同一预测期的每股收益预测中位数与覆盖机构数,归一成一路信号。
now prev 是两个等长窗口的读数各是 {"eps": 中位数, "firms": 机构数} now prev 是两个等长窗口的读数各是 {"eps": 中位数, "firms": 机构数}
@ -252,6 +252,13 @@ def from_broker(now, prev, *, as_of: str | None = None, min_firms: int = 2,
每股收益从正转负或从负转正时变化率没有意义单独标记为转亏或扭亏而不算百分比 每股收益从正转负或从负转正时变化率没有意义单独标记为转亏或扭亏而不算百分比
转亏方向明确是坏消息但它不满足逻辑存疑那个双条件所以只是转弱不带 hard 转亏方向明确是坏消息但它不满足逻辑存疑那个双条件所以只是转弱不带 hard
覆盖收缩要同时满足比例与基数两个条件才算数基数门槛 cover_min_base 2026-09-04
全市场实测加进来的不加它六只判为逻辑存疑的票里有五只的触发是覆盖从三家减到两家
也就是一家机构那两周没发报告就构成 33% 的收缩机构数这么少时百分比不说明任何事
默认 5 家的性质是前窗口有五家时走掉一家只有 20%够不着 30% 这条线必须走掉
两家才算这正是"覆盖在收缩"该有的含义基数不够时预测下修照样记转弱只是不带
hard于是整体落到无法判断进人工复核而不是出减持提议
""" """
n0, n1 = int((prev or {}).get("firms") or 0), int((now or {}).get("firms") or 0) n0, n1 = int((prev or {}).get("firms") or 0), int((now or {}).get("firms") or 0)
if n0 < int(min_firms) or n1 < int(min_firms): if n0 < int(min_firms) or n1 < int(min_firms):
@ -271,10 +278,14 @@ def from_broker(now, prev, *, as_of: str | None = None, min_firms: int = 2,
else "每股收益预测由亏转盈,变化率不适用")) else "每股收益预测由亏转盈,变化率不适用"))
chg = (e1 - e0) / abs(e0) if e0 else 0.0 chg = (e1 - e0) / abs(e0) if e0 else 0.0
if chg <= -float(eps_drop): if chg <= -float(eps_drop):
hard = shrink > float(cover_drop) shrunk = shrink > float(cover_drop)
hard = shrunk and n0 >= int(cover_min_base)
why = f"每股收益预测中位数下修 {abs(chg):.0%}" why = f"每股收益预测中位数下修 {abs(chg):.0%}"
if hard: if hard:
why += f",同时覆盖机构从 {n0} 家收缩到 {n1} 家(减 {shrink:.0%}" why += f",同时覆盖机构从 {n0} 家收缩到 {n1} 家(减 {shrink:.0%}"
elif shrunk:
why += (f",覆盖机构从 {n0} 家减到 {n1} 家,但前窗口不足 {cover_min_base} 家,"
f"这个比例不作数")
return signal(PATH_BROKER, SIG_DOWN, as_of=as_of, coverage=n1, hard=hard, return signal(PATH_BROKER, SIG_DOWN, as_of=as_of, coverage=n1, hard=hard,
why=why, refs=refs) why=why, refs=refs)
if chg >= float(eps_rise): if chg >= float(eps_rise):

View File

@ -112,6 +112,15 @@ def test_broker():
r = ls.from_broker({"eps": 1.8, "firms": 3}, {"eps": 2.4, "firms": 9}, as_of=DS) r = ls.from_broker({"eps": 1.8, "firms": 3}, {"eps": 2.4, "firms": 9}, as_of=DS)
t("下修且覆盖收缩,两个条件同现 -> 转弱且达到存疑门槛", t("下修且覆盖收缩,两个条件同现 -> 转弱且达到存疑门槛",
r["signal"] == ls.SIG_DOWN and r["hard"] and "收缩" in r["why"]) r["signal"] == ls.SIG_DOWN and r["hard"] and "收缩" in r["why"])
r = ls.from_broker({"eps": 1.8, "firms": 2}, {"eps": 2.4, "firms": 3}, as_of=DS)
t("下修且比例上收缩,但前窗口只有三家 -> 转弱不带硬触发(一家没发报告就是 33%",
r["signal"] == ls.SIG_DOWN and not r["hard"] and "不作数" in r["why"])
r = ls.from_broker({"eps": 1.8, "firms": 4}, {"eps": 2.4, "firms": 5}, as_of=DS)
t("前窗口够五家、走掉一家只有 20% -> 够不着收缩线",
r["signal"] == ls.SIG_DOWN and not r["hard"])
r = ls.from_broker({"eps": 1.8, "firms": 3}, {"eps": 2.4, "firms": 5},
as_of=DS, cover_min_base=3)
t("基数门槛旋钮生效", r["signal"] == ls.SIG_DOWN and r["hard"])
r = ls.from_broker({"eps": 2.4, "firms": 3}, {"eps": 2.4, "firms": 9}, as_of=DS) r = ls.from_broker({"eps": 2.4, "firms": 3}, {"eps": 2.4, "firms": 9}, as_of=DS)
t("只覆盖收缩而预测没动 -> 平稳(覆盖只作否决位,不单独定负)", t("只覆盖收缩而预测没动 -> 平稳(覆盖只作否决位,不单独定负)",
r["signal"] == ls.SIG_FLAT) r["signal"] == ls.SIG_FLAT)