akg-factor-bridge/test_card.py

113 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""card.judge() 纯逻辑单测(无需 DB、无需 pandas
覆盖2026-09-03 台账 013 的关注定义细化):候选 / 关注只剩"确认线缺失或陈旧"两种 /
只差覆盖与潜在吸筹归仅展示 / 仅展示(未启动、硬风险否决)/ 缺失文案与判决依据 /
因果论断证据线只展示不进判决 / 旋钮 / 卡内序 / 坏信号集合与 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)
CLAIM = {"direction": "利好", "mechanism": "液冷散热渗透率提升带动订单", "horizon": "半年内",
"condition": None, "doc_title": "散热行业深度", "disclosure_date": "2026-08-20",
"claim_id": "clm_001"}
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])
t("候选的判决依据一句话", "明确吸筹" in r["basis"] and r["failed_gates"] == [])
t("无论断时 logic 为空列表", r["logic"] == [])
# ---- 关注只剩两种:确认线缺失(无评分)、确认线陈旧(明确吸筹但日龄超限或未知)----
r = card.judge({**BASE, "accum_state": ""})
t("缺评分 -> 仍关注且缺失文案", r["verdict"] == "关注" and "无吸筹评分(未入池)" in r["missing"])
t("关注的判决依据写明无法判断、交人裁决", "无法判断" in r["basis"] and "交人裁决" in r["basis"])
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, "accum_age": None})
t("明确吸筹但日龄未知 -> 关注(陈旧一类)", r["verdict"] == "关注"
and any("日龄未知" in m for m in r["missing"]))
# ---- 只差覆盖与潜在吸筹不再升格关注(台账 013----
r = card.judge({**BASE, "covered": False, "upside": None})
t("只差覆盖 -> 仅展示,缺失文案标明不升格", r["verdict"] == "仅展示" and "covered" in r["failed_gates"]
and any(m.startswith("无券商覆盖") and "仅展示" in m for m in r["missing"]))
r = card.judge({**BASE, "accum_state": "潜在吸筹·低位企稳"})
t("潜在吸筹 -> 仅展示failed_gates 附加 confirm", r["verdict"] == "仅展示" and not r["confirm"]
and "confirm" in r["failed_gates"] and any("潜在吸筹" in m and "仅展示" in m for m in r["missing"]))
t("潜在吸筹的判决依据写明未达确认线", "未达确认线" in r["basis"])
r = card.judge({**BASE, "accum_state": "信号不明"})
t("信号不明等非明确状态 -> 仅展示", r["verdict"] == "仅展示" and "confirm" in r["failed_gates"])
r = card.judge({**BASE, "upside": -0.05})
t("负容忍默认 0轻微为负不过覆盖门槛 -> 仅展示", r["verdict"] == "仅展示" and "covered" in r["failed_gates"]
and any("负容忍线" in m for m in r["missing"]))
# ---- 其余仅展示与硬风险 ----
r = card.judge({**BASE, "pct0": 1.2})
t("未启动 -> 仅展示", r["verdict"] == "仅展示" and "started" in r["failed_gates"] and "门槛未过" in r["basis"])
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"] and "硬风险" in r["basis"])
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"]
and not any("派发" in m for m in r["missing"]))
r = card.judge({**BASE, "risk_name": True})
t("ST 族 -> 仅展示", r["verdict"] == "仅展示" and "clean_name" in r["failed_gates"])
# ---- 因果论断:只展示,不改判决 ----
r = card.judge({**BASE, "logic": [CLAIM, {**CLAIM, "claim_id": "clm_002", "doc_title": None}]})
t("带论断仍是候选logic 两行带出处", r["verdict"] == "候选" and len(r["logic"]) == 2
and "《散热行业深度》" in r["logic"][0] and "2026-08-20" in r["logic"][0] and "clm_001" in r["logic"][0])
t("论断不进理由", len(r["reasons"]) == 4)
r = card.judge({**BASE, "pct0": 1.2, "logic": [CLAIM]})
t("论断不能把未启动拉成候选", r["verdict"] == "仅展示" and r["logic"])
t("坏论断条目被跳过", card.logic_lines([None, "x", {}]) == ["因果论断"])
# ---- 旋钮 ----
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"] == "候选")
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()