仅展示的跳过原因写出这一只自己的判决依据

页面上"今天没建仓的"那一栏,三只票原因完全不同却写成同一句话"选股系统判为
仅展示,不出提议"。09-04 实测的三只:天合光能是差一个当日涨幅(跌 3.21%,
门槛要涨 3%)、蓝黛科技是吸筹只到"潜在吸筹"没过确认线、上海港湾是被择时决策
系统昨夜的 SELL 信号一票否决。前两种明天可能就翻过来,第三种是技术面在走坏,
处置完全不同,而人从页面上看不出来。

判决依据是选股系统判决时当场写下的一句话,09-04 上午已经随计划下发到 PMS。
这次把它接到跳过原因上。改了三处:计划筛选层的仅展示名单从一串代码改成带判决
依据的行;提议服务照新形状读,旧形状兼容着读;动作引擎新增一个纯函数拼这句话,
判决依据缺失时退回原来那句固定文案,不让这一栏空掉。

测试:新增一例钉住"三种来路必须写出三句互不相同的话",另改一条断言跟上新形状。
例数从 652 到 653,测试入口的例数表同改。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-04 10:10:16 +08:00
parent fd74dd197f
commit 68b8046540
6 changed files with 58 additions and 9 deletions

View File

@ -96,6 +96,23 @@ WHY_WATCH_CONFIRM = "选股系统判为关注(无法判断),交人裁决"
WHY_DISPLAY_ONLY = "选股系统判为仅展示"
def display_only_why(row) -> str:
"""仅展示的票为什么没建仓, 尽量写出**这一只**的真实原因。
只写选股系统判为仅展示是不够的: 仅展示至少有三种来路, 差一个当日涨幅
吸筹没到确认线被昨夜坏信号一票否决, 三者的处置完全不同 前两种明天可能就
翻过来, 第三种是技术面在走坏三只票并排显示同一句话, 人看不出哪只该继续盯
判决依据 (basis) 是选股系统在判决时**当场写下**的一句话, 2026-09-04 起随计划下发
它在就直接用它, 不在 (旧版计划没有这个键) 就退回原来那句固定文案 少一个字段
不该让这一栏整个空掉
"""
basis = (str(row.get("basis") or "").strip() if isinstance(row, dict) else "")
if not basis:
return WHY_DISPLAY_ONLY + ",不出提议"
return f"{WHY_DISPLAY_ONLY},不出提议:{basis}"
def verdict_confirm_why(verdict):
"""按判决要不要强制人工确认: 要则回原因, 不要回 None。
@ -614,7 +631,7 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
# 仅展示的行不占名额、不占金额, 也不进后面任何一道闸; 放在名额判断之前,
# 是为了让它的跳过原因永远是「仅展示」而不是碰巧的「名额用完」。
skipped.append({"ts_code": code, "action": A_OPEN,
"why": WHY_DISPLAY_ONLY + ",不出提议"})
"why": display_only_why(c)})
continue
if slots <= 0:
skipped.append({"ts_code": code, "action": A_OPEN,

View File

@ -343,7 +343,10 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N
continue
if route_by_verdict and r.get("verdict") == VERDICT_DISPLAY:
dropped["display"] += 1 # 上游判为仅展示: 不进池, 单独列出让人看得见
display_only.append(c)
# 带上判决依据: 仅展示至少有三种来路 (差一个当日涨幅、吸筹没到确认线、
# 被昨夜坏信号一票否决), 三者处置完全不同。只记代码的话, 下游只能对所有
# 仅展示的票写同一句话, 人看不出哪只该继续盯。
display_only.append({"ts_code": c, "basis": r.get("basis")})
continue
if exclude_st:
nm = (r.get("name") or "").strip()

View File

@ -197,9 +197,11 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
return []
# 仅展示的票逐只写跳过原因: disposition_snapshot 复用本函数, 页面「系统在盯的候选」
# 由此显示「选股系统判为仅展示」而不是一个说不清的空白。
for code in (sel.get("display_only") or []):
out["skipped"].append({"ts_code": code, "action": ae.A_OPEN,
"why": ae.WHY_DISPLAY_ONLY + ",不出提议"})
for row in (sel.get("display_only") or []):
# 2026-09-04 起这里是带判决依据的行; 早先只是一串代码, 兼容着读。
row = row if isinstance(row, dict) else {"ts_code": row}
out["skipped"].append({"ts_code": row.get("ts_code"), "action": ae.A_OPEN,
"why": ae.display_only_why(row)})
items = list(sel.get("items") or [])
if not items:
out["skipped"].append({"action": ae.A_OPEN,

View File

@ -47,16 +47,17 @@
无判决回退/判决认不出交人/仅展示不占名额/自动执行
开关四条件与任一缺失仍入队/信号来源区分/密钥名单
含会话密钥/研判键放行与必答题/置信度进硬数字/
页面静态守卫 (15 )
仅展示的跳过原因写出这一只自己的判决依据/
页面静态守卫 (16 )
test_batch21_units.py 减持的自动执行边界 (2026-09-03 安全修复): 保垫减仓照旧自动/
强制入队对卖出同样有效/研判不可用的减持入队/研究证据走弱
来源必定交人裁决/风控高置信卖出与命令清仓不经提议分流 (15 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) +
目标价到价必定入队 (档位 full 也不自动卖) +
用户设的止损价与目标价单独成列显示 (70 )
652
653
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
减持与研究理由两键各一例, 现为 652)
减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因一例, 现为 653)
任一子集失败即整体失败 (退出码 1)
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):

View File

@ -155,6 +155,29 @@ def _():
assert r["candidates"][0]["needs_user_confirm"] is False
@case("判决分流·仅展示的跳过原因写出这一只自己的判决依据 (三种来路要分得开)")
def _():
# 三只都是仅展示, 但来路完全不同: 一只差当日涨幅、一只吸筹没到确认线、
# 一只被昨夜坏信号一票否决。写成同一句话的话, 人看不出哪只该继续盯。
rows = [cand("600000.SH", score=300, verdict="仅展示", basis="门槛未过started"),
cand("600002.SH", score=290, verdict="仅展示",
basis="三门槛全过,但吸筹评分为「潜在吸筹」,未达确认线,不升格关注"),
cand("600003.SH", score=280, verdict="仅展示",
basis="硬风险否决:决策系统昨夜信号 SELL"),
cand("600004.SH", score=270, verdict="仅展示")] # 旧版计划: 没有判决依据
sk = {x["ts_code"]: x["why"] for x in _scan(rows, slots=3)["skipped"]}
assert sk["600000.SH"].endswith("门槛未过started"), sk["600000.SH"]
assert "潜在吸筹" in sk["600002.SH"], sk["600002.SH"]
assert "SELL" in sk["600003.SH"], sk["600003.SH"]
# 三句话必须互不相同 —— 这正是这条用例要守住的东西
assert len({sk["600000.SH"], sk["600002.SH"], sk["600003.SH"]}) == 3, sk
# 判决依据缺失时退回原来那句固定文案, 不许让这一栏空掉
assert sk["600004.SH"] == ae.WHY_DISPLAY_ONLY + ",不出提议", sk["600004.SH"]
# 每一句都仍以固定前缀开头, 页面按前缀分组的逻辑不受影响
for c in ("600000.SH", "600002.SH", "600003.SH", "600004.SH"):
assert sk[c].startswith(ae.WHY_DISPLAY_ONLY), sk[c]
@case("判决分流·没有判决 (旧版计划) 时开关开着与关着产出逐字一致, 五键全 None")
def _():
rows = [cand("600000.SH", score=300), cand("600001.SH", score=200, rank=2)]

View File

@ -523,7 +523,10 @@ def _():
assert r["considered"] == r["eligible"] + dr["held"] + dr["black"] + dr["tier"] \
+ dr["score"] + dr["sources"] + dr["upside"] + dr["theme"] + dr["dup"] \
+ dr["display"], (r, dr)
assert dr["display"] == 1 and r["display_only"] == ["600600.SH"], (dr, r["display_only"])
# 2026-09-04 起 display_only 是带判决依据的行, 不再是一串代码 —— 下游要靠它写出
# 这一只票**自己**的跳过原因。
assert dr["display"] == 1 and [x["ts_code"] for x in r["display_only"]] == ["600600.SH"], \
(dr, r["display_only"])
assert len(r["items"]) == r["eligible"] - dr["capped"] == 2
# 分流开关关着: 那只仅展示的票照常进池 (分数高, 会排到第一), 计数里 display 是 0
r0 = pf.select_candidates(p, held=["SH600418"], black=["SZ300952"], tiers=["强传导"],