From ac7832620d2d2d4c8cea8aa8a6686dab8af9c3cb Mon Sep 17 00:00:00 2001 From: zlt Date: Fri, 4 Sep 2026 09:15:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=99=E8=B7=AF=E5=8A=A0=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E5=9F=BA=E6=95=B0=E9=97=A8=E6=A7=9B=EF=BC=9A=E6=9C=BA=E6=9E=84?= =?UTF-8?q?=E5=A4=AA=E5=B0=91=E6=97=B6=E6=94=B6=E7=BC=A9=E6=AF=94=E4=BE=8B?= =?UTF-8?q?=E4=B8=8D=E4=BD=9C=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-09-04 全市场实测发现的:六只判为逻辑存疑的票里有五只,触发是覆盖机构从三家 减到两家,也就是一家机构那两周没发报告就构成 33% 的收缩。机构数这么少时百分比不 说明任何事,而逻辑存疑会出减持提议,误判的代价是让人对着噪声做裁决。 加一条基数门槛,默认前窗口至少五家。它的性质是:五家时走掉一家只有 20%、够不着 30% 那条线,必须走掉两家才算——这正是"覆盖在收缩"该有的含义。基数不够时预测下修 照样记转弱,只是不带硬触发,于是整体落到无法判断进人工复核,而不是出减持提议。 Co-Authored-By: Claude Opus 5 --- logic_state.py | 15 +++++++++++++-- test_logic_state.py | 9 +++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/logic_state.py b/logic_state.py index 97071e8..74ad243 100644 --- a/logic_state.py +++ b/logic_state.py @@ -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, 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": 机构数}。 @@ -252,6 +252,13 @@ def from_broker(now, prev, *, as_of: str | None = None, min_firms: int = 2, 每股收益从正转负或从负转正时,变化率没有意义,单独标记为转亏或扭亏而不算百分比。 转亏方向明确是坏消息,但它不满足逻辑存疑那个双条件,所以只是转弱不带 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) 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 "每股收益预测由亏转盈,变化率不适用")) chg = (e1 - e0) / abs(e0) if e0 else 0.0 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%}" if hard: 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, why=why, refs=refs) if chg >= float(eps_rise): diff --git a/test_logic_state.py b/test_logic_state.py index 406d43b..858a38f 100644 --- a/test_logic_state.py +++ b/test_logic_state.py @@ -112,6 +112,15 @@ def test_broker(): r = ls.from_broker({"eps": 1.8, "firms": 3}, {"eps": 2.4, "firms": 9}, as_of=DS) t("下修且覆盖收缩,两个条件同现 -> 转弱且达到存疑门槛", 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) t("只覆盖收缩而预测没动 -> 平稳(覆盖只作否决位,不单独定负)", r["signal"] == ls.SIG_FLAT)