给人看的文字里不再出现代码名

页面上出现过「选股系统判为仅展示,不出提议:门槛未过:started」。started 是门槛在
代码里的变量名,SELL 是决策系统的枚举值,两者都只该在系统之间传递,不该印到页面上。
而且整句话没有说清到底为什么不买。

判决依据、缺失项、风险这三个字段会被 PMS 原样显示给人读,这次全部改写成完整的中文
句子,并且带上具体数字。举例:原先写「门槛未过:started」,现在写「当日涨幅 -3.2%,
没到 3% 的启动线」——后者能让人自己判断差多少。原先写「决策系统昨夜信号 SELL」,
现在写「择时决策系统昨夜给出卖出信号」。

同时删掉几处解释设计规则的话,例如「不升格关注,归仅展示」。那是给写代码的人看的
规则说明,不是给读页面的人看的原因。

新增一条守卫用例:造十五种情形,逐个检查这三个字段里一个代码名都没有,且判决依据
不许短到说不清原因。以后再往文案里写代码名,这条会当场失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-04 10:26:37 +08:00
parent 99b845eb30
commit 29621432f5
2 changed files with 127 additions and 32 deletions

84
card.py
View File

@ -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]:
"""把没过的门槛翻成一句句人话,带上当时的实际数字。
门槛在代码里的名字是 pointedstartedcovered那是给程序读的写进给人看的
句子必须换成人话并且带上数字当日跌 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

View File

@ -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 — 候选卡判决 / 关注两种(缺失、陈旧)/ 只差覆盖与潜在吸筹归仅展示 / 硬风险三种 / "
"因果论断只展示 / 论断质量标注四项 / 缺失文案与依据 / 旋钮 / 卡内序 全部通过")