给人看的文字里不再出现代码名
页面上出现过「选股系统判为仅展示,不出提议:门槛未过:started」。started 是门槛在 代码里的变量名,SELL 是决策系统的枚举值,两者都只该在系统之间传递,不该印到页面上。 而且整句话没有说清到底为什么不买。 判决依据、缺失项、风险这三个字段会被 PMS 原样显示给人读,这次全部改写成完整的中文 句子,并且带上具体数字。举例:原先写「门槛未过:started」,现在写「当日涨幅 -3.2%, 没到 3% 的启动线」——后者能让人自己判断差多少。原先写「决策系统昨夜信号 SELL」, 现在写「择时决策系统昨夜给出卖出信号」。 同时删掉几处解释设计规则的话,例如「不升格关注,归仅展示」。那是给写代码的人看的 规则说明,不是给读页面的人看的原因。 新增一条守卫用例:造十五种情形,逐个检查这三个字段里一个代码名都没有,且判决依据 不许短到说不清原因。以后再往文案里写代码名,这条会当场失败。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
99b845eb30
commit
29621432f5
84
card.py
84
card.py
|
|
@ -46,6 +46,13 @@ from typing import Any
|
|||
# 决策系统夜间结论表 signal_type 里的坏信号(三处同源,见模块说明)
|
||||
BAD_SIGNALS = {"SELL", "AVOID", "DROPPED"}
|
||||
|
||||
# 昨夜信号的代号翻译成中文。这些代号是决策系统那边的枚举值,只在系统之间传递;
|
||||
# 一旦要写进给人看的句子里,就必须先翻成人话——页面上出现一个 SELL,读的人得先
|
||||
# 知道这是个枚举值才看得懂。翻不出来的原样带上,总比吞掉强。
|
||||
SIGNAL_CN = {"SELL": "卖出", "AVOID": "回避", "DROPPED": "已剔除",
|
||||
"BUY": "买入", "WATCH": "观察", "HOLD": "持有"}
|
||||
|
||||
|
||||
VERDICT_CANDIDATE = "候选"
|
||||
VERDICT_WATCH = "关注"
|
||||
VERDICT_SHOW = "仅展示"
|
||||
|
|
@ -113,29 +120,30 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0,
|
|||
|
||||
risk: list[str] = []
|
||||
if y_signal in BAD_SIGNALS:
|
||||
risk.append(f"决策系统昨夜信号 {y_signal}")
|
||||
risk.append(f"择时决策系统昨夜给出{SIGNAL_CN.get(y_signal, y_signal)}信号")
|
||||
if ev.get("stale_snapshot"):
|
||||
risk.append("传导快照日与计划日不符")
|
||||
risk.append("用的传导快照不是计划当天的")
|
||||
if "派发" in accum_state:
|
||||
risk.append("高位派发")
|
||||
risk.append("资金在高位派发")
|
||||
|
||||
missing: list[str] = []
|
||||
if pct0 is None:
|
||||
missing.append("无行情")
|
||||
missing.append("没有当日行情")
|
||||
if not ev.get("covered"):
|
||||
missing.append("无券商覆盖(不升格关注,归仅展示)")
|
||||
missing.append("没有券商覆盖")
|
||||
elif not gates["covered"]:
|
||||
missing.append(f"预期空间 {upside:+.0%} 低于负容忍线" if upside is not None else "预期空间缺失")
|
||||
missing.append(f"券商预期空间 {upside:+.0%},低于 {neg_tol:+.0%} 的容忍线"
|
||||
if upside is not None else "算不出券商预期空间")
|
||||
if confirm_missing:
|
||||
missing.append("无吸筹评分(未入池)")
|
||||
missing.append("还没有吸筹评分(这只票没进过夜间扫描)")
|
||||
elif confirm_stale:
|
||||
missing.append(f"吸筹评分陈旧 {accum_age} 日" if isinstance(accum_age, int)
|
||||
else "吸筹评分日龄未知")
|
||||
missing.append(f"吸筹评分是 {accum_age} 个交易日前的,太旧了"
|
||||
if isinstance(accum_age, int) else "吸筹评分有多旧算不出来")
|
||||
elif confirm_negative and "派发" not in accum_state:
|
||||
state_short = accum_state.split("·")[0].strip()
|
||||
missing.append(f"吸筹评分为「{state_short}」,未达确认线(不升格关注,归仅展示)")
|
||||
missing.append(f"吸筹只到「{state_short}」,没到明确吸筹这条线")
|
||||
if not gates["pointed"]:
|
||||
missing.append("无传导")
|
||||
missing.append("所在环节当日没有被传导指向")
|
||||
# 研报论断的质量提示。放进缺失项是因为它和"吸筹评分陈旧"是同一类事:证据还在,
|
||||
# 但已经旧到不该当新证据用。两者此前一个会过期一个不会,卡上看着一样新,
|
||||
# 这个不对称本身会误导人。这里只提示不作门槛——门槛仍是那三条可交易口径。
|
||||
|
|
@ -156,24 +164,32 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0,
|
|||
|
||||
all_gates = all(gates.values())
|
||||
failed = [k for k, ok in gates.items() if not ok]
|
||||
# 判决依据是这张卡上唯一一句"为什么",PMS 原样显示在页面上给人读。所以它必须是
|
||||
# 一句完整的中文,带上具体数字,不许出现门槛的变量名(started 这类)或信号的
|
||||
# 枚举值(SELL 这类)——那些是系统之间传递用的,不是给人看的。
|
||||
if risk:
|
||||
verdict = VERDICT_SHOW
|
||||
basis = "硬风险否决:" + ";".join(risk)
|
||||
basis = ";".join(risk)
|
||||
elif all_gates and confirm:
|
||||
verdict = VERDICT_CANDIDATE
|
||||
basis = "三门槛全过、无硬风险、明确吸筹且评分新鲜"
|
||||
basis = (f"三条门槛都过了,没有风险,且明确吸筹({accum_age} 个交易日前评的)"
|
||||
if isinstance(accum_age, int) else "三条门槛都过了,没有风险,且明确吸筹")
|
||||
elif all_gates and (confirm_missing or confirm_stale):
|
||||
verdict = VERDICT_WATCH
|
||||
basis = ("三门槛全过、无硬风险,但确认线"
|
||||
+ ("缺失(无吸筹评分)" if confirm_missing else "陈旧(明确吸筹但评分日龄超限或未知)")
|
||||
+ "——系统无法判断,交人裁决")
|
||||
why = ("这只票还没有吸筹评分" if confirm_missing
|
||||
else (f"吸筹评分是 {accum_age} 个交易日前的,太旧了"
|
||||
if isinstance(accum_age, int) else "吸筹评分有多旧算不出来"))
|
||||
basis = f"三条门槛都过了,也没有风险,但{why},系统判断不了,交人裁决"
|
||||
elif all_gates:
|
||||
verdict = VERDICT_SHOW
|
||||
failed.append("confirm")
|
||||
basis = f"三门槛全过,但吸筹评分为「{accum_state.split('·')[0].strip()}」,未达确认线,不升格关注"
|
||||
basis = (f"三条门槛都过了,但吸筹只到「{accum_state.split('·')[0].strip()}」,"
|
||||
f"没到明确吸筹这条线")
|
||||
else:
|
||||
verdict = VERDICT_SHOW
|
||||
basis = "门槛未过:" + "、".join(failed)
|
||||
basis = ";".join(gate_reasons(failed, pct0=pct0, start_pct=start_pct,
|
||||
covered=bool(ev.get("covered")), upside=upside,
|
||||
neg_tol=neg_tol))
|
||||
|
||||
return {"verdict": verdict, "reasons": reasons, "missing": missing, "risk": risk,
|
||||
"gates": gates, "confirm": confirm, "failed_gates": failed, "basis": basis,
|
||||
|
|
@ -246,3 +262,35 @@ def logic_notes(claims, stale_days: int = 90) -> list[str]:
|
|||
if q.get("n_disputed"):
|
||||
out.append(f"其中 {q['n_disputed']} 条处于未结的方向分歧")
|
||||
return out
|
||||
|
||||
|
||||
def gate_reasons(failed, *, pct0=None, start_pct: float = 3.0, covered: bool = False,
|
||||
upside=None, neg_tol: float = 0.0) -> list[str]:
|
||||
"""把没过的门槛翻成一句句人话,带上当时的实际数字。
|
||||
|
||||
门槛在代码里的名字是 pointed、started、covered,那是给程序读的。写进给人看的
|
||||
句子必须换成人话,并且带上数字——「当日跌 3.2%,没到 3% 的启动线」比「涨幅门槛
|
||||
未过」有用得多,因为前者能让人自己判断差多少。
|
||||
|
||||
认不出的门槛名原样带上而不是吞掉:真出现了新门槛,宁可页面上难看一次,
|
||||
也好过一句话里少了一条原因。
|
||||
"""
|
||||
out: list[str] = []
|
||||
for k in failed or []:
|
||||
if k == "pointed":
|
||||
out.append("所在环节当日没有被传导指向")
|
||||
elif k == "started":
|
||||
out.append(f"当日涨幅 {pct0:+.1f}%,没到 {start_pct:g}% 的启动线"
|
||||
if pct0 is not None else "没有当日行情,算不出涨幅")
|
||||
elif k == "covered":
|
||||
if not covered:
|
||||
out.append("没有券商覆盖")
|
||||
elif upside is not None:
|
||||
out.append(f"券商预期空间 {upside:+.0%},低于 {neg_tol:+.0%} 的容忍线")
|
||||
else:
|
||||
out.append("算不出券商预期空间")
|
||||
elif k == "confirm":
|
||||
out.append("吸筹没到明确吸筹这条线")
|
||||
else:
|
||||
out.append(f"门槛 {k} 未过")
|
||||
return out
|
||||
|
|
|
|||
75
test_card.py
75
test_card.py
|
|
@ -33,49 +33,57 @@ def main():
|
|||
|
||||
# ---- 关注只剩两种:确认线缺失(无评分)、确认线陈旧(明确吸筹但日龄超限或未知)----
|
||||
r = card.judge({**BASE, "accum_state": ""})
|
||||
t("缺评分 -> 仍关注且缺失文案", r["verdict"] == "关注" and "无吸筹评分(未入池)" in r["missing"])
|
||||
t("关注的判决依据写明无法判断、交人裁决", "无法判断" in r["basis"] and "交人裁决" in r["basis"])
|
||||
t("缺评分 -> 仍关注且缺失文案", r["verdict"] == "关注" and "还没有吸筹评分" in r["missing"][0])
|
||||
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"]))
|
||||
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"]))
|
||||
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"]))
|
||||
# 「不升格关注、归仅展示」是设计规则,不是给人看的话,2026-09-04 从文案里去掉了。
|
||||
# 所以这里钉的是判决本身与缺失项的实质内容,不再钉那句解释规则的话。
|
||||
t("只差覆盖 -> 仅展示,缺失项写明没有券商覆盖",
|
||||
r["verdict"] == "仅展示" and "covered" in r["failed_gates"]
|
||||
and "没有券商覆盖" 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"])
|
||||
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"]))
|
||||
and any("预期空间 -5%" in m and "容忍线" 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"])
|
||||
t("未启动 -> 仅展示,判决依据带上实际涨幅与门槛值",
|
||||
r["verdict"] == "仅展示" and "started" in r["failed_gates"]
|
||||
and "当日涨幅 +1.2%" in r["basis"] and "3% 的启动线" in r["basis"])
|
||||
|
||||
r = card.judge({**BASE, "pointed": False})
|
||||
t("未被指向 -> 仅展示且标无传导", r["verdict"] == "仅展示" and "无传导" in r["missing"])
|
||||
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"])
|
||||
t("昨夜卖出信号 -> 仅展示,信号代号翻成中文",
|
||||
r["verdict"] == "仅展示" and r["risk"]
|
||||
and r["basis"] == "择时决策系统昨夜给出卖出信号")
|
||||
|
||||
r = card.judge({**BASE, "stale_snapshot": True})
|
||||
t("快照日不符 -> 硬风险", r["verdict"] == "仅展示" and "传导快照日与计划日不符" in r["risk"])
|
||||
t("快照日不符 -> 硬风险", r["verdict"] == "仅展示" and "用的传导快照不是计划当天的" in r["risk"])
|
||||
|
||||
r = card.judge({**BASE, "accum_state": "⚠️ 高位派发"})
|
||||
t("高位派发 -> 硬风险,且不重复写进缺失", r["verdict"] == "仅展示" and "高位派发" in r["risk"]
|
||||
t("高位派发 -> 硬风险,且不重复写进缺失", r["verdict"] == "仅展示" and "资金在高位派发" in r["risk"]
|
||||
and not any("派发" in m for m in r["missing"]))
|
||||
|
||||
r = card.judge({**BASE, "risk_name": True})
|
||||
|
|
@ -153,6 +161,45 @@ 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)----
|
||||
# 起因:页面上出现过「选股系统判为仅展示,不出提议:门槛未过:started」。
|
||||
# started 是门槛在代码里的变量名,SELL 是决策系统的枚举值,两者都只该在系统之间
|
||||
# 传递。判决依据、缺失项、风险这三个字段会被 PMS 原样显示给人读,所以它们里面
|
||||
# 一个代码名都不许有。failed_gates 不在此列——那是给程序判断用的机器字段。
|
||||
CODE_WORDS = ("started", "pointed", "covered", "confirm",
|
||||
"SELL", "AVOID", "DROPPED", "BUY", "WATCH", "HOLD")
|
||||
probes = [
|
||||
dict(BASE), # 候选
|
||||
{**BASE, "accum_state": ""}, # 关注·没有评分
|
||||
{**BASE, "accum_age": 99}, # 关注·评分太旧
|
||||
{**BASE, "accum_age": None}, # 关注·日龄算不出
|
||||
{**BASE, "accum_state": "潜在吸筹·低位企稳"}, # 仅展示·没到确认线
|
||||
{**BASE, "pct0": 1.2}, # 仅展示·没启动
|
||||
{**BASE, "pct0": None}, # 仅展示·没有行情
|
||||
{**BASE, "pointed": False, "theme": None, "n_sources": None}, # 仅展示·没传导
|
||||
{**BASE, "covered": False, "upside": None}, # 仅展示·没覆盖
|
||||
{**BASE, "upside": -0.05}, # 仅展示·预期空间为负
|
||||
{**BASE, "y_signal": "SELL"}, # 仅展示·昨夜卖出信号
|
||||
{**BASE, "y_signal": "AVOID"}, # 仅展示·昨夜回避信号
|
||||
{**BASE, "stale_snapshot": True}, # 仅展示·快照日不符
|
||||
{**BASE, "accum_state": "高位派发"}, # 仅展示·资金派发
|
||||
{**BASE, "pointed": False, "pct0": None, "covered": False,
|
||||
"upside": None, "accum_state": ""}, # 全都不满足
|
||||
]
|
||||
for ev in probes:
|
||||
r = card.judge(ev)
|
||||
texts = [r["basis"], *r["missing"], *r["risk"], *r["reasons"]]
|
||||
for txt in texts:
|
||||
for w in CODE_WORDS:
|
||||
assert w not in txt, f"给人看的文字里出现了代码名 {w}:{txt}"
|
||||
assert r["basis"] and len(r["basis"]) >= 6, f"判决依据太短,说不清原因:{r['basis']!r}"
|
||||
t(f"给人看的三个字段里没有代码名({len(probes)} 种情形逐个扫过)", True)
|
||||
t("门槛翻译带上实际数字",
|
||||
card.gate_reasons(["started"], pct0=-3.21, start_pct=3.0)[0]
|
||||
== "当日涨幅 -3.2%,没到 3% 的启动线")
|
||||
t("认不出的门槛名原样带上,不吞掉",
|
||||
card.gate_reasons(["新门槛"]) == ["门槛 新门槛 未过"])
|
||||
|
||||
t("坏信号集合口径", card.BAD_SIGNALS == {"SELL", "AVOID", "DROPPED"})
|
||||
print("ALL OK — 候选卡判决 / 关注两种(缺失、陈旧)/ 只差覆盖与潜在吸筹归仅展示 / 硬风险三种 / "
|
||||
"因果论断只展示 / 论断质量标注四项 / 缺失文案与依据 / 旋钮 / 卡内序 全部通过")
|
||||
|
|
|
|||
Loading…
Reference in New Issue