"今天没建仓的"那一栏只写原因,右侧不再打印计数字典
两处都是用户指出来的。
第一处,那一栏原先写「选股系统判为仅展示,不出提议:门槛未过:started」。
「仅展示」「不出提议」是内部说法,started 是门槛在代码里的变量名,绕了两层还是
没说清为什么不买。现在直接用判决依据那一句完整中文,例如「当日涨幅 -3.2%,没到
3% 的启动线」。旧版计划没有这个字段时退回一句大白话,不留空。
第二处,右侧显示过「落选明细 {'held': 0, 'black': 0, ... 'display': 83}」——
把计数字典原样拼进了字符串,键全是英文,值为零的也全列出来,读的人要在十一个
数字里自己找哪个不是零。现在只念不为零的那几项,按筛选实际发生的顺序写成一句话:
「今天考察了 83 只票,一只都没留下:选股系统判定今天不买 83 只。」
测试:改两条钉旧文案的断言,新增一条钉住计数字典不许再原样打印,另在跳过原因那条
用例里加了对代码名与内部说法的逐词检查。例数从 653 到 654。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
15b6eea906
commit
d85fe6731b
|
|
@ -97,20 +97,18 @@ WHY_DISPLAY_ONLY = "选股系统判为仅展示"
|
||||||
|
|
||||||
|
|
||||||
def display_only_why(row) -> str:
|
def display_only_why(row) -> str:
|
||||||
"""仅展示的票为什么没建仓, 尽量写出**这一只**的真实原因。
|
"""这一栏回答的是「为什么今天没买这只票」。所以它只写原因, 不写系统内部的判决名。
|
||||||
|
|
||||||
只写「选股系统判为仅展示」是不够的: 仅展示至少有三种来路, 差一个当日涨幅、
|
2026-09-04 改过一次。改之前写的是「选股系统判为仅展示, 不出提议: 门槛未过: started」,
|
||||||
吸筹没到确认线、被昨夜坏信号一票否决, 三者的处置完全不同 —— 前两种明天可能就
|
三处毛病: 「仅展示」「不出提议」是内部说法, 读的人不知道是什么意思; started 是门槛在
|
||||||
翻过来, 第三种是技术面在走坏。三只票并排显示同一句话, 人看不出哪只该继续盯。
|
代码里的变量名, 不该出现在页面上; 整句话绕了两层还是没说清到底为什么不买。
|
||||||
|
|
||||||
判决依据 (basis) 是选股系统在判决时**当场写下**的一句话, 2026-09-04 起随计划下发。
|
现在直接用判决依据 —— 那是选股系统判决时当场写下的一句完整中文, 例如
|
||||||
它在就直接用它, 不在 (旧版计划没有这个键) 就退回原来那句固定文案 —— 少一个字段
|
「当日涨幅 -3.2%, 没到 3% 的启动线」或「择时决策系统昨夜给出卖出信号」。
|
||||||
不该让这一栏整个空掉。
|
旧版计划没有这个字段, 那时退回一句大白话, 不留空。
|
||||||
"""
|
"""
|
||||||
basis = (str(row.get("basis") or "").strip() if isinstance(row, dict) else "")
|
basis = (str(row.get("basis") or "").strip() if isinstance(row, dict) else "")
|
||||||
if not basis:
|
return basis or "选股系统今天不建议买入"
|
||||||
return WHY_DISPLAY_ONLY + ",不出提议"
|
|
||||||
return f"{WHY_DISPLAY_ONLY},不出提议:{basis}"
|
|
||||||
|
|
||||||
|
|
||||||
def verdict_confirm_why(verdict):
|
def verdict_confirm_why(verdict):
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,7 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
||||||
"why": ae.display_only_why(row)})
|
"why": ae.display_only_why(row)})
|
||||||
items = list(sel.get("items") or [])
|
items = list(sel.get("items") or [])
|
||||||
if not items:
|
if not items:
|
||||||
out["skipped"].append({"action": ae.A_OPEN,
|
out["skipped"].append({"action": ae.A_OPEN, "why": _no_candidate_why(sel)})
|
||||||
"why": f"候选池过滤后为空 (考察 {sel.get('considered')} 只, "
|
|
||||||
f"落选明细 {sel.get('dropped')})"})
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
codes = [x["ts_code"] for x in items]
|
codes = [x["ts_code"] for x in items]
|
||||||
|
|
@ -272,6 +270,42 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
||||||
return res["candidates"]
|
return res["candidates"]
|
||||||
|
|
||||||
|
|
||||||
|
# 候选一只都不剩时, 这一栏要说清是被哪几道筛掉的。键名是代码里的, 翻成人话再显示。
|
||||||
|
# 顺序就是筛选实际发生的顺序, 照着念下来正好是一条票走过的路。
|
||||||
|
_DROP_CN = [
|
||||||
|
("held", "已经持有"),
|
||||||
|
("black", "在黑名单里"),
|
||||||
|
("display", "选股系统判定今天不买"),
|
||||||
|
("st", "是 ST 类"),
|
||||||
|
("tier", "档位不够"),
|
||||||
|
("score", "分数不够"),
|
||||||
|
("sources", "传导源数不够"),
|
||||||
|
("upside", "券商预期空间不够"),
|
||||||
|
("theme", "同一主题已经选够"),
|
||||||
|
("dup", "和已选的重复"),
|
||||||
|
("capped", "超出今天的名额"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _no_candidate_why(sel) -> str:
|
||||||
|
"""今天一只候选都没有时的那句说明。
|
||||||
|
|
||||||
|
2026-09-04 改过一次。改之前是把落选计数那个字典直接拼进字符串, 页面上显示成
|
||||||
|
「落选明细 {'held': 0, 'black': 0, ... 'display': 83}」—— 键全是英文, 值为零的
|
||||||
|
也全列出来, 读的人要在十一个数字里自己找出哪个不是零。
|
||||||
|
|
||||||
|
现在只念不为零的那几项, 按筛选实际发生的顺序, 用中文写成一句话。
|
||||||
|
"""
|
||||||
|
n = sel.get("considered")
|
||||||
|
dropped = sel.get("dropped") or {}
|
||||||
|
parts = [f"{cn} {dropped[k]} 只" for k, cn in _DROP_CN
|
||||||
|
if isinstance(dropped.get(k), int) and dropped[k] > 0]
|
||||||
|
head = f"今天考察了 {n} 只票,一只都没留下" if n else "今天没有票可考察"
|
||||||
|
if not parts:
|
||||||
|
return head + "。"
|
||||||
|
return head + ":" + ",".join(parts) + "。"
|
||||||
|
|
||||||
|
|
||||||
def _inflight_open_commitments(view) -> dict:
|
def _inflight_open_commitments(view) -> dict:
|
||||||
"""在途的新建仓承诺: {names: {代码}, amount: 预估占用金额}。
|
"""在途的新建仓承诺: {names: {代码}, amount: 预估占用金额}。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,17 @@
|
||||||
开关四条件与任一缺失仍入队/信号来源区分/密钥名单
|
开关四条件与任一缺失仍入队/信号来源区分/密钥名单
|
||||||
含会话密钥/研判键放行与必答题/置信度进硬数字/
|
含会话密钥/研判键放行与必答题/置信度进硬数字/
|
||||||
仅展示的跳过原因写出这一只自己的判决依据/
|
仅展示的跳过原因写出这一只自己的判决依据/
|
||||||
页面静态守卫 (16 例)
|
候选一只不剩时的说明不打印计数字典/
|
||||||
|
页面静态守卫 (17 例)
|
||||||
test_batch21_units.py 减持的自动执行边界 (2026-09-03 安全修复): 保垫减仓照旧自动/
|
test_batch21_units.py 减持的自动执行边界 (2026-09-03 安全修复): 保垫减仓照旧自动/
|
||||||
强制入队对卖出同样有效/研判不可用的减持入队/研究证据走弱
|
强制入队对卖出同样有效/研判不可用的减持入队/研究证据走弱
|
||||||
来源必定交人裁决/风控高置信卖出与命令清仓不经提议分流 (15 例)
|
来源必定交人裁决/风控高置信卖出与命令清仓不经提议分流 (15 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||||
用户设的止损价与目标价单独成列显示 (70 例)
|
用户设的止损价与目标价单独成列显示 (70 例)
|
||||||
共 653 例
|
共 654 例
|
||||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因一例, 现为 653)
|
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 现为 654)
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
|
|
||||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ def _():
|
||||||
assert any(s["ts_code"] == "600001.SH" and "名额" in s["why"] for s in r["skipped"]), r["skipped"]
|
assert any(s["ts_code"] == "600001.SH" and "名额" in s["why"] for s in r["skipped"]), r["skipped"]
|
||||||
|
|
||||||
|
|
||||||
@case("判决分流·仅展示: 不产出、不占名额、不进任何闸, 跳过原因写明「选股系统判为仅展示」")
|
@case("判决分流·仅展示: 不产出、不占名额、不进任何闸, 跳过原因说清为什么不买")
|
||||||
def _():
|
def _():
|
||||||
rows = [cand("600000.SH", score=300, verdict="仅展示", reasons=["只差覆盖"], missing=["券商覆盖"],
|
rows = [cand("600000.SH", score=300, verdict="仅展示", reasons=["只差覆盖"], missing=["券商覆盖"],
|
||||||
risk=[]),
|
risk=[]),
|
||||||
|
|
@ -149,8 +149,10 @@ def _():
|
||||||
r = _scan(rows, slots=1, names_count=19)
|
r = _scan(rows, slots=1, names_count=19)
|
||||||
assert [c["ts_code"] for c in r["candidates"]] == ["600001.SH"], r["candidates"]
|
assert [c["ts_code"] for c in r["candidates"]] == ["600001.SH"], r["candidates"]
|
||||||
sk = {s["ts_code"]: s["why"] for s in r["skipped"]}
|
sk = {s["ts_code"]: s["why"] for s in r["skipped"]}
|
||||||
assert "仅展示" in sk["600000.SH"] and "名额" not in sk["600000.SH"], sk
|
# 这一栏回答的是「为什么今天没买」, 所以它只写原因, 不写判决名, 也不许写成
|
||||||
assert sk["600000.SH"].startswith(ae.WHY_DISPLAY_ONLY), sk
|
# 「名额用完」—— 仅展示的行排在名额判断之前, 跳过原因不能被名额那一刀盖掉。
|
||||||
|
assert "名额" not in sk["600000.SH"], sk
|
||||||
|
assert sk["600000.SH"] == "选股系统今天不建议买入", sk
|
||||||
# 名额没被仅展示占掉: 只有一个名额, 候选那只拿到了
|
# 名额没被仅展示占掉: 只有一个名额, 候选那只拿到了
|
||||||
assert r["candidates"][0]["needs_user_confirm"] is False
|
assert r["candidates"][0]["needs_user_confirm"] is False
|
||||||
|
|
||||||
|
|
@ -172,10 +174,30 @@ def _():
|
||||||
# 三句话必须互不相同 —— 这正是这条用例要守住的东西
|
# 三句话必须互不相同 —— 这正是这条用例要守住的东西
|
||||||
assert len({sk["600000.SH"], sk["600002.SH"], sk["600003.SH"]}) == 3, sk
|
assert len({sk["600000.SH"], sk["600002.SH"], sk["600003.SH"]}) == 3, sk
|
||||||
# 判决依据缺失时退回原来那句固定文案, 不许让这一栏空掉
|
# 判决依据缺失时退回原来那句固定文案, 不许让这一栏空掉
|
||||||
assert sk["600004.SH"] == ae.WHY_DISPLAY_ONLY + ",不出提议", sk["600004.SH"]
|
assert sk["600004.SH"] == "选股系统今天不建议买入", sk["600004.SH"]
|
||||||
# 每一句都仍以固定前缀开头, 页面按前缀分组的逻辑不受影响
|
# 四句话里一个代码名、一句内部说法都不许有 —— 这一栏是给人读的
|
||||||
for c in ("600000.SH", "600002.SH", "600003.SH", "600004.SH"):
|
for c in ("600000.SH", "600002.SH", "600003.SH", "600004.SH"):
|
||||||
assert sk[c].startswith(ae.WHY_DISPLAY_ONLY), sk[c]
|
for w in ("started", "pointed", "covered", "SELL", "AVOID",
|
||||||
|
"仅展示", "不出提议", "判决"):
|
||||||
|
assert w not in sk[c], (c, w, sk[c])
|
||||||
|
|
||||||
|
|
||||||
|
@case("候选一只不剩时的说明: 只念不为零的那几项, 不把计数字典原样打印出来")
|
||||||
|
def _():
|
||||||
|
from app.services import proposal_service as psvc
|
||||||
|
z = {"held": 0, "black": 0, "tier": 0, "score": 0, "sources": 0, "upside": 0,
|
||||||
|
"st": 0, "theme": 0, "dup": 0, "capped": 0, "display": 83}
|
||||||
|
w = psvc._no_candidate_why({"considered": 83, "dropped": z})
|
||||||
|
assert w == "今天考察了 83 只票,一只都没留下:选股系统判定今天不买 83 只。", w
|
||||||
|
# 页面上出现过 "落选明细 {'held': 0, ...}" 这种东西, 这条就是钉住不许再出现
|
||||||
|
for bad in ("{", "}", "'", "held", "display", "capped"):
|
||||||
|
assert bad not in w, (bad, w)
|
||||||
|
# 多项不为零时按筛选实际发生的顺序念
|
||||||
|
w2 = psvc._no_candidate_why({"considered": 50,
|
||||||
|
"dropped": {"held": 3, "display": 40, "capped": 7}})
|
||||||
|
assert w2.index("已经持有") < w2.index("不买") < w2.index("名额"), w2
|
||||||
|
# 一只都没考察过时不硬拼一个空清单
|
||||||
|
assert psvc._no_candidate_why({"considered": 0, "dropped": {}}) == "今天没有票可考察。"
|
||||||
|
|
||||||
|
|
||||||
@case("判决分流·没有判决 (旧版计划) 时开关开着与关着产出逐字一致, 五键全 None")
|
@case("判决分流·没有判决 (旧版计划) 时开关开着与关着产出逐字一致, 五键全 None")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue