候选卡文案第一批:预期空间、传导那句话、论断出处、ST 门槛
四条都出自 09-04 的文案扫描,这四个字段被 PMS 原样显示给人读。 一,预期空间原先写「券商预期空间 -5%,低于 +0% 的容忍线」。两处毛病:默认配置下 容忍线就是 0,说「低于 0% 的容忍线」等于没说;两个数一个带正号一个带负号、方向 相反,人要在脑子里翻一次才比得了大小。改成同向同单位:「券商给的目标价比现价低 5%,目标价低于现价的不买」。两处同源的写法抽成一个函数,口径不再分叉。 二,传导那句原先写「所在环节「减速器」被 4 路指向」。「被 4 路指向」不是人话。 改成「这只票在「减速器」环节,今天有 4 个上游来源传导到这个环节」。 三,论断出处原先在行尾挂一个 36 位的内部编号,占掉半行且对读的人没有任何信息。 去掉它,换成距今多少天——旧不旧交给人自己判。追溯用的编号仍在论断原值里, 计划的那一支照旧带着。 四,ST 那道门槛没有对应文案,掉进兜底后页面上会出现「门槛 clean_name 未过」。 补专属文案,并把兜底改写成「还有一条买入条件没通过,暂时没写对应的说明(内部代号 xxx)」——内部代号退到括号里,句子主干永远是中文。 守卫扩到四个人话字段(判决依据、理由、缺失、风险),此前只管住判决依据一个。 另加一条:每句话至少六个字,半句话的电报体也算不合格。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5de97173d1
commit
0a70082405
37
card.py
37
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%},目标价低于现价的不买"
|
||||
|
|
|
|||
26
test_card.py
26
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 — 候选卡判决 / 关注两种(缺失、陈旧)/ 只差覆盖与潜在吸筹归仅展示 / 硬风险三种 / "
|
||||
|
|
|
|||
Loading…
Reference in New Issue