"""card.judge() 纯逻辑单测(无需 DB、无需 pandas)。 覆盖:候选 / 关注(缺覆盖)/ 关注(缺确认)/ 仅展示(未启动)/ 仅展示(硬风险否决) / 缺失文案 / 评分陈旧 / 旋钮 / 卡内序 / 坏信号集合与 pool 同源。 跑法:python3 test_card.py 或 pytest test_card.py """ import card BASE = dict(pointed=True, theme="散热器件", n_sources=2, pct0=4.1, covered=True, upside=0.25, risk_name=False, accum_state="明确吸筹·量在价先", accum_score=78, accum_age=3, y_signal="WATCH", stale_snapshot=False) def t(name, cond): assert cond, name print(" ok", name) def main(): r = card.judge(dict(BASE)) t("三门槛全过且明确吸筹 -> 候选", r["verdict"] == "候选" and r["confirm"] and not r["risk"]) t("候选理由四条齐", len(r["reasons"]) == 4 and "已启动" in r["reasons"][1]) r = card.judge({**BASE, "covered": False, "upside": None}) t("只差覆盖 -> 关注", r["verdict"] == "关注" and "无券商覆盖" in r["missing"]) r = card.judge({**BASE, "accum_state": "潜在吸筹·低位企稳"}) t("门槛全过但潜在吸筹不算确认 -> 关注", r["verdict"] == "关注" and not r["confirm"]) r = card.judge({**BASE, "accum_state": ""}) t("无评分 -> 关注且缺失文案", r["verdict"] == "关注" and "无吸筹评分(未入池)" in r["missing"]) r = card.judge({**BASE, "pct0": 1.2}) t("未启动 -> 仅展示", r["verdict"] == "仅展示" and "started" in r["failed_gates"]) r = card.judge({**BASE, "pointed": False}) t("未被指向 -> 仅展示且标无传导", r["verdict"] == "仅展示" and "无传导" in r["missing"]) r = card.judge({**BASE, "y_signal": "sell"}) t("决策系统 SELL -> 硬风险否决为仅展示", r["verdict"] == "仅展示" and r["risk"]) r = card.judge({**BASE, "stale_snapshot": True}) t("快照日不符 -> 硬风险", r["verdict"] == "仅展示" and "传导快照日与计划日不符" in r["risk"]) r = card.judge({**BASE, "accum_state": "⚠️ 高位派发"}) t("高位派发 -> 硬风险", r["verdict"] == "仅展示" and "高位派发" in r["risk"]) r = card.judge({**BASE, "accum_age": 45}) t("评分陈旧 -> 不确认、关注、缺失文案", r["verdict"] == "关注" and not r["confirm"] and any("陈旧 45" in m for m in r["missing"])) r = card.judge({**BASE, "pct0": 2.5}, start_pct=2.0) t("启动阈值旋钮生效", r["verdict"] == "候选") r = card.judge({**BASE, "accum_age": 45}, accum_max_age=60) t("评分日龄旋钮生效", r["verdict"] == "候选") r = card.judge({**BASE, "upside": -0.05}, neg_tol=0.10) t("负容忍线传入生效", r["verdict"] == "候选") r = card.judge({**BASE, "upside": -0.05}) t("负容忍默认 0:轻微为负不过覆盖门槛 -> 关注", r["verdict"] == "关注") r = card.judge({**BASE, "risk_name": True}) t("ST 族 -> 仅展示", r["verdict"] == "仅展示" and "clean_name" in r["failed_gates"]) rows = [{"verdict": "关注", "pct0": 9.0}, {"verdict": "候选", "pct0": 3.5}, {"verdict": "候选", "pct0": 7.2}, {"verdict": "仅展示", "pct0": None}] rows.sort(key=card.sort_key) t("卡内序:候选在前、同判决涨幅降序、缺行情垫底", [x["pct0"] for x in rows] == [7.2, 3.5, 9.0, None]) t("坏信号集合口径", card.BAD_SIGNALS == {"SELL", "AVOID", "DROPPED"}) print("ALL OK — 候选卡判决 / 关注两种 / 硬风险三种 / 缺失文案 / 旋钮 / 卡内序 全部通过") if __name__ == "__main__": main()