diff --git a/card.py b/card.py index 17bfdf4..6315a5b 100644 --- a/card.py +++ b/card.py @@ -73,7 +73,7 @@ def _num(v) -> float | None: def judge(ev: dict[str, Any], *, start_pct: float = 3.0, accum_max_age: int = 30, neg_tol: float = 0.0, - logic_stale_days: int = 90) -> dict[str, Any]: + logic_stale_days: int = 90, require_started: bool = True) -> dict[str, Any]: """一只票的证据字典 -> 判决字典。 ev 里认的键(缺键按缺失处理,不报错): @@ -104,7 +104,10 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0, gates = { "pointed": bool(ev.get("pointed")), - "started": pct0 is not None and pct0 >= float(start_pct), + # require_started 关掉时这一条恒过:涨跌多少都不拦,没有当日行情也不拦。 + # 这是整条门槛的开关,不是把阈值调成一个很小的负数——那样"没有行情"的票 + # 仍然会被拦下,而关掉门槛的本意是这一条根本不参与判断。 + "started": (not require_started) or (pct0 is not None and pct0 >= float(start_pct)), "covered": bool(ev.get("covered")) and upside is not None and upside >= -float(neg_tol or 0.0), "clean_name": not bool(ev.get("risk_name")), @@ -154,8 +157,11 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0, theme = ev.get("theme") or "环节" n = ev.get("n_sources") reasons.append(f"所在环节「{theme}」被{n}路指向" if n else f"所在环节「{theme}」被指向") - if gates["started"]: + if gates["started"] and require_started: reasons.append(f"当日已启动 {pct0:+.1f}%") + elif not require_started and pct0 is not None: + # 门槛关着时它不再是一条理由,但当日涨跌仍是人要看的信息,照样写出来。 + reasons.append(f"当日涨幅 {pct0:+.1f}%(启动线已关闭,不作门槛)") if confirm: reasons.append(f"明确吸筹(评分 {accum_score:.0f},{accum_age} 日前)" if accum_score is not None else f"明确吸筹({accum_age} 日前)") diff --git a/config.py b/config.py index 09ed05e..0b83039 100644 --- a/config.py +++ b/config.py @@ -191,6 +191,17 @@ BIONIC_SCAN_KEY = os.environ.get("BIONIC_SCAN_KEY", "") # 两个旋钮保持默认、不在历史样本上挑参数,等样本外复盘读数再拍(09-02 拍板)。 FACTOR_REGISTER_STATUS = os.environ.get("FACTOR_REGISTER_STATUS", "paused") # 平台注册状态:paused=不参评(方案 2.5,平台参评列表只取 active),active=参评 CARD_START_PCT = float(os.environ.get("CARD_START_PCT", "3")) # 门槛二:数据日涨幅达到几个百分点算已启动(与基座热点扫描同口径) + +# 门槛二整条开关。置 0 就是不要这条门槛,涨跌多少都不拦。 +# +# 这条门槛回答的不是"这只票好不好",是"今天是不是买点":只在票已经动起来的当天买, +# 不去猜底。关掉它等于改成左侧买入——好处是研究结论成立的当天就能进场,不用等市场 +# 先认同;代价是要承受不确定的等待,而且候选数会明显变多。 +# +# 2026-09-04 关掉。关之前的实测:当日 83 只强传导票全部落到仅展示,其中绝大多数就 +# 卡在这一条上(那天大盘在跌,三只被问到的票分别跌 3.2%、1.8%、2.2%)。 +# 想开回去就把这个值设回 1 并重建容器,判决当天即恢复原样。 +CARD_REQUIRE_STARTED = os.environ.get("CARD_REQUIRE_STARTED", "1") == "1" CARD_ACCUM_MAX_AGE = int(os.environ.get("CARD_ACCUM_MAX_AGE", "30")) # 确认线:吸筹评分日龄上限(交易日) # 计划快照落点(容器内路径;仓库根挂在 /app,故默认落在仓库 data/plan/)。 # 每日 JSON 含主榜与观察档全部行与全部证据线,是复盘与对账的唯一底本。 diff --git a/plan.py b/plan.py index 72dc962..a1db33a 100644 --- a/plan.py +++ b/plan.py @@ -149,7 +149,8 @@ def _assemble_cards(ds: str, codes: list, ev: dict, upside: pd.Series, j = card.judge(evd, start_pct=config.CARD_START_PCT, accum_max_age=config.CARD_ACCUM_MAX_AGE, neg_tol=config.UPSIDE_NEG_TOLERANCE, - logic_stale_days=config.LOGIC_STALE_DAYS) + logic_stale_days=config.LOGIC_STALE_DAYS, + require_started=config.CARD_REQUIRE_STARTED) cards[k] = { **j, "theme": theme, "n_sources": n_sources, "chain_fit": evd["chain_fit"], @@ -393,6 +394,7 @@ def collect(date: str | None = None, top: int = 20, obs_top: int = 10, "观察档分=100+0.6z(传导)+0.4z(−热度)", # ---- 候选卡(2026-09-02):分数与档位之外的另一份产物,不受 top / theme_cap 裁剪 ---- "card_params": {"start_pct": config.CARD_START_PCT, + "require_started": config.CARD_REQUIRE_STARTED, "accum_max_age": config.CARD_ACCUM_MAX_AGE, "neg_tol": config.UPSIDE_NEG_TOLERANCE, "logic_per_stock": config.LOGIC_CLAIMS_PER_STOCK, diff --git a/test_card.py b/test_card.py index dcbcb8a..fdc4b5b 100644 --- a/test_card.py +++ b/test_card.py @@ -161,6 +161,35 @@ def main(): r["logic_quality"] is None and not [m for m in r["missing"] if "论断" in m]) t("论断为空列表时不产生提示", card.logic_notes([]) == [] and card.logic_quality([]) is None) + # ---- 启动涨幅线的整条开关(2026-09-04 拍板关闭)---- + r = card.judge({**BASE, "pct0": -3.2}, require_started=False) + t("门槛关掉后,当日下跌也能判候选", r["verdict"] == "候选" and r["failed_gates"] == []) + t("关掉后理由里写明这条线已关,不再说「当日已启动」", + any("启动线已关闭" in x for x in r["reasons"]) + and not any("当日已启动" in x for x in r["reasons"])) + r = card.judge({**BASE, "pct0": None}, require_started=False) + t("关掉后连没有当日行情也不拦(这是整条关掉,不是把阈值调成负数)", + r["verdict"] == "候选") + t("但没有行情这件事仍写进缺失项", "没有当日行情" in r["missing"]) + + # 关掉这一条不许顺带放松别的门槛与风险 + r = card.judge({**BASE, "pct0": -3.2, "pointed": False, "theme": None, + "n_sources": None}, require_started=False) + t("关掉启动线不影响传导门槛", r["verdict"] == "仅展示" and "pointed" in r["failed_gates"]) + r = card.judge({**BASE, "pct0": -3.2, "covered": False, "upside": None}, + require_started=False) + t("关掉启动线不影响覆盖门槛", r["verdict"] == "仅展示" and "covered" in r["failed_gates"]) + r = card.judge({**BASE, "pct0": -3.2, "accum_state": "潜在吸筹"}, require_started=False) + t("关掉启动线不影响吸筹确认线", r["verdict"] == "仅展示" and not r["confirm"]) + r = card.judge({**BASE, "pct0": -3.2, "y_signal": "SELL"}, require_started=False) + t("关掉启动线不影响硬风险否决", r["verdict"] == "仅展示" and r["risk"]) + r = card.judge({**BASE, "pct0": -3.2, "accum_state": ""}, require_started=False) + t("关掉启动线后,判断不了的仍是关注", r["verdict"] == "关注") + + r = card.judge({**BASE, "pct0": -3.2}) + t("默认仍是开着的(改口径要靠配置,不靠默认值悄悄变)", + r["verdict"] == "仅展示" and "started" in r["failed_gates"]) + # ---- 给人看的文字里不许出现代码名(2026-09-04)---- # 起因:页面上出现过「选股系统判为仅展示,不出提议:门槛未过:started」。 # started 是门槛在代码里的变量名,SELL 是决策系统的枚举值,两者都只该在系统之间