启动涨幅线做成整条开关,2026-09-04 按拍板关闭

原先只有一个阈值 CARD_START_PCT,想不要这条门槛只能把它调成一个很小的负数。那是绕法,
不是关闭:没有当日行情的票仍然会被拦下,而关掉门槛的本意是这一条根本不参与判断。

新增 CARD_REQUIRE_STARTED,置 0 即整条不参与。关掉后:涨跌多少都不拦,没有行情也不拦,
理由行改写成「当日涨幅 -3.2%(启动线已关闭,不作门槛)」——涨跌仍是人要看的信息,
只是不再当作一条通过的理由。

这条门槛回答的不是这只票好不好,是今天是不是买点:只在票已经动起来的当天买,不猜底。
关掉它等于改成左侧买入,好处是研究结论成立的当天就能进场,代价是要承受不确定的等待,
候选数会明显变多。09-04 关之前的实测:当日 83 只强传导票全部落到仅展示,绝大多数就
卡在这一条上。

九条用例钉住关掉后的行为,重点是不许顺带放松别的门槛:传导、覆盖、吸筹确认线、
硬风险四道都仍然照拦,判断不了的仍归关注。另有一条钉住默认值仍是开着的——改口径
要靠配置,不靠默认值悄悄变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-04 10:49:30 +08:00
parent 29621432f5
commit de7f83d18d
4 changed files with 52 additions and 4 deletions

12
card.py
View File

@ -73,7 +73,7 @@ def _num(v) -> float | None:
def judge(ev: dict[str, Any], *, start_pct: float = 3.0, def judge(ev: dict[str, Any], *, start_pct: float = 3.0,
accum_max_age: int = 30, neg_tol: float = 0.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 里认的键缺键按缺失处理不报错 ev 里认的键缺键按缺失处理不报错
@ -104,7 +104,10 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0,
gates = { gates = {
"pointed": bool(ev.get("pointed")), "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 "covered": bool(ev.get("covered")) and upside is not None
and upside >= -float(neg_tol or 0.0), and upside >= -float(neg_tol or 0.0),
"clean_name": not bool(ev.get("risk_name")), "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 "环节" theme = ev.get("theme") or "环节"
n = ev.get("n_sources") n = ev.get("n_sources")
reasons.append(f"所在环节「{theme}」被{n}路指向" if n else f"所在环节「{theme}」被指向") reasons.append(f"所在环节「{theme}」被{n}路指向" if n else f"所在环节「{theme}」被指向")
if gates["started"]: if gates["started"] and require_started:
reasons.append(f"当日已启动 {pct0:+.1f}%") reasons.append(f"当日已启动 {pct0:+.1f}%")
elif not require_started and pct0 is not None:
# 门槛关着时它不再是一条理由,但当日涨跌仍是人要看的信息,照样写出来。
reasons.append(f"当日涨幅 {pct0:+.1f}%(启动线已关闭,不作门槛)")
if confirm: if confirm:
reasons.append(f"明确吸筹(评分 {accum_score:.0f}{accum_age} 日前)" reasons.append(f"明确吸筹(评分 {accum_score:.0f}{accum_age} 日前)"
if accum_score is not None else f"明确吸筹({accum_age} 日前)") if accum_score is not None else f"明确吸筹({accum_age} 日前)")

View File

@ -191,6 +191,17 @@ BIONIC_SCAN_KEY = os.environ.get("BIONIC_SCAN_KEY", "")
# 两个旋钮保持默认、不在历史样本上挑参数等样本外复盘读数再拍09-02 拍板)。 # 两个旋钮保持默认、不在历史样本上挑参数等样本外复盘读数再拍09-02 拍板)。
FACTOR_REGISTER_STATUS = os.environ.get("FACTOR_REGISTER_STATUS", "paused") # 平台注册状态paused=不参评(方案 2.5,平台参评列表只取 activeactive=参评 FACTOR_REGISTER_STATUS = os.environ.get("FACTOR_REGISTER_STATUS", "paused") # 平台注册状态paused=不参评(方案 2.5,平台参评列表只取 activeactive=参评
CARD_START_PCT = float(os.environ.get("CARD_START_PCT", "3")) # 门槛二:数据日涨幅达到几个百分点算已启动(与基座热点扫描同口径) 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")) # 确认线:吸筹评分日龄上限(交易日) CARD_ACCUM_MAX_AGE = int(os.environ.get("CARD_ACCUM_MAX_AGE", "30")) # 确认线:吸筹评分日龄上限(交易日)
# 计划快照落点(容器内路径;仓库根挂在 /app故默认落在仓库 data/plan/)。 # 计划快照落点(容器内路径;仓库根挂在 /app故默认落在仓库 data/plan/)。
# 每日 JSON 含主榜与观察档全部行与全部证据线,是复盘与对账的唯一底本。 # 每日 JSON 含主榜与观察档全部行与全部证据线,是复盘与对账的唯一底本。

View File

@ -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, j = card.judge(evd, start_pct=config.CARD_START_PCT,
accum_max_age=config.CARD_ACCUM_MAX_AGE, accum_max_age=config.CARD_ACCUM_MAX_AGE,
neg_tol=config.UPSIDE_NEG_TOLERANCE, 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] = { cards[k] = {
**j, **j,
"theme": theme, "n_sources": n_sources, "chain_fit": evd["chain_fit"], "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(−热度)", "观察档分=100+0.6z(传导)+0.4z(−热度)",
# ---- 候选卡2026-09-02分数与档位之外的另一份产物不受 top / theme_cap 裁剪 ---- # ---- 候选卡2026-09-02分数与档位之外的另一份产物不受 top / theme_cap 裁剪 ----
"card_params": {"start_pct": config.CARD_START_PCT, "card_params": {"start_pct": config.CARD_START_PCT,
"require_started": config.CARD_REQUIRE_STARTED,
"accum_max_age": config.CARD_ACCUM_MAX_AGE, "accum_max_age": config.CARD_ACCUM_MAX_AGE,
"neg_tol": config.UPSIDE_NEG_TOLERANCE, "neg_tol": config.UPSIDE_NEG_TOLERANCE,
"logic_per_stock": config.LOGIC_CLAIMS_PER_STOCK, "logic_per_stock": config.LOGIC_CLAIMS_PER_STOCK,

View File

@ -161,6 +161,35 @@ def main():
r["logic_quality"] is None and not [m for m in r["missing"] if "论断" in m]) 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) 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---- # ---- 给人看的文字里不许出现代码名2026-09-04----
# 起因页面上出现过「选股系统判为仅展示不出提议门槛未过started」。 # 起因页面上出现过「选股系统判为仅展示不出提议门槛未过started」。
# started 是门槛在代码里的变量名SELL 是决策系统的枚举值,两者都只该在系统之间 # started 是门槛在代码里的变量名SELL 是决策系统的枚举值,两者都只该在系统之间