From 0a70082405f23c10622b39a1467efc03ed9af97f Mon Sep 17 00:00:00 2001 From: zlt Date: Fri, 4 Sep 2026 13:17:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=80=99=E9=80=89=E5=8D=A1=E6=96=87=E6=A1=88?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=89=B9=EF=BC=9A=E9=A2=84=E6=9C=9F=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E3=80=81=E4=BC=A0=E5=AF=BC=E9=82=A3=E5=8F=A5=E8=AF=9D?= =?UTF-8?q?=E3=80=81=E8=AE=BA=E6=96=AD=E5=87=BA=E5=A4=84=E3=80=81ST=20?= =?UTF-8?q?=E9=97=A8=E6=A7=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 四条都出自 09-04 的文案扫描,这四个字段被 PMS 原样显示给人读。 一,预期空间原先写「券商预期空间 -5%,低于 +0% 的容忍线」。两处毛病:默认配置下 容忍线就是 0,说「低于 0% 的容忍线」等于没说;两个数一个带正号一个带负号、方向 相反,人要在脑子里翻一次才比得了大小。改成同向同单位:「券商给的目标价比现价低 5%,目标价低于现价的不买」。两处同源的写法抽成一个函数,口径不再分叉。 二,传导那句原先写「所在环节「减速器」被 4 路指向」。「被 4 路指向」不是人话。 改成「这只票在「减速器」环节,今天有 4 个上游来源传导到这个环节」。 三,论断出处原先在行尾挂一个 36 位的内部编号,占掉半行且对读的人没有任何信息。 去掉它,换成距今多少天——旧不旧交给人自己判。追溯用的编号仍在论断原值里, 计划的那一支照旧带着。 四,ST 那道门槛没有对应文案,掉进兜底后页面上会出现「门槛 clean_name 未过」。 补专属文案,并把兜底改写成「还有一条买入条件没通过,暂时没写对应的说明(内部代号 xxx)」——内部代号退到括号里,句子主干永远是中文。 守卫扩到四个人话字段(判决依据、理由、缺失、风险),此前只管住判决依据一个。 另加一条:每句话至少六个字,半句话的电报体也算不合格。 Co-Authored-By: Claude Opus 5 --- card.py | 37 +++++++++++++++++++++++++++++++------ test_card.py | 26 +++++++++++++++++++++----- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/card.py b/card.py index 6315a5b..bcd5c40 100644 --- a/card.py +++ b/card.py @@ -135,8 +135,7 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0, if not ev.get("covered"): missing.append("没有券商覆盖") elif not gates["covered"]: - missing.append(f"券商预期空间 {upside:+.0%},低于 {neg_tol:+.0%} 的容忍线" - if upside is not None else "算不出券商预期空间") + missing.append(upside_text(upside, neg_tol)) if confirm_missing: missing.append("还没有吸筹评分(这只票没进过夜间扫描)") elif confirm_stale: @@ -156,7 +155,8 @@ def judge(ev: dict[str, Any], *, start_pct: float = 3.0, if gates["pointed"]: theme = ev.get("theme") or "环节" 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"] and require_started: reasons.append(f"当日已启动 {pct0:+.1f}%") elif not require_started and pct0 is not None: @@ -215,10 +215,15 @@ def logic_lines(claims, limit: int = 3) -> list[str]: head = f"{head}({c['horizon']})" if head else f"({c['horizon']})" if c.get("condition"): head = f"{head},条件:{c['condition']}" + # 出处只写研报名、披露日、距今多少天。论断编号是 36 位的内部标识, + # 原先挂在行尾占掉半行、对读的人没有任何信息;追溯用的编号仍在论断原值里, + # 计划的 card.logic_claims 那一支照旧带着,只是不进给人读的这一行。 + # 距今天数取取数层算好的质量画像——旧不旧交给人自己判,系统不替他判。 + age = (c.get("quality") or {}).get("age_days") src = "".join(x for x in ( f"《{c['doc_title']}》" if c.get("doc_title") else "", f" {c['disclosure_date']}" if c.get("disclosure_date") else "", - f" · {c['claim_id']}" if c.get("claim_id") else "") if x) + f"(距今 {age} 天)" if isinstance(age, int) else "") if x) line = head or "因果论断" if src: line = f"{line}——出处:{src.strip()}" @@ -292,11 +297,31 @@ def gate_reasons(failed, *, pct0=None, start_pct: float = 3.0, covered: bool = F if not covered: out.append("没有券商覆盖") elif upside is not None: - out.append(f"券商预期空间 {upside:+.0%},低于 {neg_tol:+.0%} 的容忍线") + out.append(upside_text(upside, neg_tol)) else: out.append("算不出券商预期空间") elif k == "confirm": out.append("吸筹没到明确吸筹这条线") + elif k == "clean_name": + out.append("简称带 ST(含 *ST 与退市整理),这类票一律不买") else: - out.append(f"门槛 {k} 未过") + # 兜底要让人看出"这是系统没写说明",而不是让人去猜一个英文词是什么意思。 + # 内部代号退到括号里,句子主干仍是中文。 + out.append(f"还有一条买入条件没通过,暂时没写对应的说明(内部代号 {k})") return out + + +def upside_text(upside, neg_tol: float = 0.0) -> str: + """券商预期空间没过门槛时的那句话。两处用到,口径必须一样。 + + 不写成"预期空间 -5% 低于 +0% 的容忍线",有两个原因。一是默认配置下容忍线就是 0, + 说"低于 0% 的容忍线"等于没说;二是两个数一个带正号一个带负号、方向相反, + 人要在脑子里翻一次才比得了大小。改成同向同单位:券商目标价比现价低多少, + 我们最多接受低多少。 + """ + if upside is None: + return "算不出券商预期空间(券商没给目标价,或现价取不到)" + gap = -float(upside) + if float(neg_tol) > 0: + return f"券商给的目标价比现价低 {gap:.0%},最多只接受低 {float(neg_tol):.0%}" + return f"券商给的目标价比现价低 {gap:.0%},目标价低于现价的不买" diff --git a/test_card.py b/test_card.py index fdc4b5b..0bcc01d 100644 --- a/test_card.py +++ b/test_card.py @@ -63,7 +63,7 @@ def main(): r = card.judge({**BASE, "upside": -0.05}) t("负容忍默认 0:轻微为负不过覆盖门槛 -> 仅展示", r["verdict"] == "仅展示" and "covered" in r["failed_gates"] - and any("预期空间 -5%" in m and "容忍线" in m for m in r["missing"])) + and any("目标价比现价低 5%" in m for m in r["missing"])) # ---- 其余仅展示与硬风险 ---- r = card.judge({**BASE, "pct0": 1.2}) @@ -92,7 +92,12 @@ def main(): # ---- 因果论断:只展示,不改判决 ---- 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]) + and "《散热行业深度》" in r["logic"][0] and "2026-08-20" in r["logic"][0]) + t("论断出处不再挂 36 位的内部编号(占半行且对读的人没有信息)", + "clm_001" not in r["logic"][0]) + r2 = card.judge({**BASE, "logic": [{**CLAIM, "quality": {"age_days": 137}}]}) + t("有质量画像时出处带上距今多少天,旧不旧交人自己判", + "(距今 137 天)" in r2["logic"][0]) t("论断不进理由", len(r["reasons"]) == 4) r = card.judge({**BASE, "pct0": 1.2, "logic": [CLAIM]}) t("论断不能把未启动拉成候选", r["verdict"] == "仅展示" and r["logic"]) @@ -215,19 +220,30 @@ def main(): {**BASE, "pointed": False, "pct0": None, "covered": False, "upside": None, "accum_state": ""}, # 全都不满足 ] + # 四个人话字段一视同仁:判决依据、理由、缺失、风险。这四个都被 PMS 原样显示, + # 纪律必须一样,不能只管住判决依据一个。 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}" + # 半句话也不行:每条都要能单独读懂,不能是「被 3 路指向」这种电报体。 + assert len(txt) >= 6, f"这句话太短,单独读不懂:{txt!r}" assert r["basis"] and len(r["basis"]) >= 6, f"判决依据太短,说不清原因:{r['basis']!r}" - t(f"给人看的三个字段里没有代码名({len(probes)} 种情形逐个扫过)", True) + 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.gate_reasons(["新门槛"]) == ["还有一条买入条件没通过,暂时没写对应的说明(内部代号 新门槛)"]) + t("ST 那道门槛有专属文案,不掉进兜底", + card.gate_reasons(["clean_name"]) == ["简称带 ST(含 *ST 与退市整理),这类票一律不买"]) + t("预期空间:默认容忍线为 0 时不说「低于 0% 的容忍线」这种废话", + card.upside_text(-0.05, 0.0) == "券商给的目标价比现价低 5%,目标价低于现价的不买") + t("预期空间:容忍线非 0 时两个数同向同单位,一眼能比大小", + card.upside_text(-0.05, 0.10) == "券商给的目标价比现价低 5%,最多只接受低 10%") + t("预期空间算不出时说清为什么", "券商没给目标价" in card.upside_text(None)) t("坏信号集合口径", card.BAD_SIGNALS == {"SELL", "AVOID", "DROPPED"}) print("ALL OK — 候选卡判决 / 关注两种(缺失、陈旧)/ 只差覆盖与潜在吸筹归仅展示 / 硬风险三种 / "