PMS 补收研究理由:判决依据与因果论断到达人眼前
选股系统每行已带判决依据与因果论断,但计划解析只收五个键,研究结论走到 PMS 门口就断了。现在增收这两个键:判决依据是一句话解释,因果论断带出处与披露日、截三条(与上游封顶同数)。两者只进评审账本与提议卡给人看,不参与任何判定;绝不进送研判的键清单,产业逻辑不归择时决策系统判,单测钉住这条边界。提议卡新增一行显示研报说了什么,并标出还有几条。 空列表与缺键分得开:上游说这票没论断是空列表,上游没给这个字段才是 None,与既有五键一个口径。 测试:全套 ALL SUITES PASS,例数 652。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4589539a0c
commit
fd74dd197f
|
|
@ -522,6 +522,10 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float):
|
|||
# 到今天还成不成立」; 进评审账本与提议卡, 让人看得见系统为什么挑了它。
|
||||
"verdict": c.get("verdict"), "reasons": c.get("reasons"),
|
||||
"missing": c.get("missing"), "risk": c.get("risk"), "card_rank": c.get("card_rank"),
|
||||
# 研究理由两键 (2026-09-03): 判决依据是上游对自己这条判决的一句话解释, 因果论断是
|
||||
# 研报里说这家公司好在哪、出处是哪份研报哪一天。两者只进评审账本与提议卡给人看,
|
||||
# 不参与任何判定, 也不送研判 —— 产业逻辑归数据基座与选股系统, 不归择时决策系统。
|
||||
"basis": c.get("basis"), "logic": c.get("logic"),
|
||||
# ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉,
|
||||
# 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。
|
||||
"target_pct": target_pct, "target_amount": round(full_amt, 2),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,26 @@ top_n 截断**之前**剔掉 —— 那是为了不让它们白占候选池的
|
|||
「关注」强制人工确认与「候选」自动流程的分流不在本模块, 见 action_engine.scan_open 与
|
||||
proposal_service._route_one。旧版计划没有这些键时, 一切行为与 2026-09-02 之前一字不差。
|
||||
|
||||
**研究理由 (2026-09-03 起上游随每行下发, 同样向下兼容)**: 又多了两个键 ——
|
||||
`basis` (判决依据, 一句话说明这一行为什么落在这一档, 例如「三门槛全过、无硬风险、明确吸筹
|
||||
且评分新鲜」) 与 `logic` (因果论断, 字符串列表, 每行是研报里那句话加上「——出处:《文档
|
||||
标题》 披露日 · 论断编号」)。加这两个键要解决的是一件很具体的事: 在此之前 PMS 收下的
|
||||
`reasons` 只有四条可交易口径的理由 (门槛过了几道、覆盖几家、当日涨了多少), 人在提议卡上
|
||||
看不到研报究竟说了什么, 研究结论走到 PMS 门口就断了。
|
||||
|
||||
两个键的性质与那五个不同, 所以口径也不同:
|
||||
|
||||
* **它们只给人看, 不参与任何判定。** 判决分流、自动执行开关、规则闸、名额与金额, 一处
|
||||
都不读它们。`basis` 是上游对自己判决的解释, `logic` 是产业逻辑本身。
|
||||
* **绝不送研判。** `judge.OPEN_JUDGE_KEYS` 不收这两个键 (那份名单里有一条守卫单测钉着)。
|
||||
送过去等于请择时决策系统去读产业逻辑, 而它的活是判「此刻这只票买不买得下手」——
|
||||
四层分工里产业逻辑归数据基座与选股系统, 不归它。
|
||||
* **`logic` 的条数在这里截断** (`LOGIC_KEEP`), 与 `reasons` 走 `REASONS_KEEP` 同一个
|
||||
做法; `basis` 是一句话, 不截。
|
||||
* **论断的日期就写在文字里。** 上游把出处与披露日拼进了每一行, 所以页面照原样显示就能
|
||||
看出这条论断是哪年哪月的。上游那边正在给论断加陈旧度标注, 字段到了之前, 日期这件事
|
||||
先靠这行文字兑现 —— 一条 2024 年的论断与一条上周的论断, 人必须一眼分得出。
|
||||
|
||||
模块级只依赖 stdlib + `app.core.command_spec` / `app.core.action_engine` (都是纯逻辑),
|
||||
其余 (requests / param_store / pms_repo / plan_diff / tradedays) 一律函数内懒加载 ——
|
||||
让解析与筛选这两段纯逻辑可以零依赖单测。
|
||||
|
|
@ -116,6 +136,15 @@ def _text_or_none(v):
|
|||
# 进研判请求体、显示在提议卡上 —— 四个去处都只需要前几条最要紧的。
|
||||
REASONS_KEEP = 4
|
||||
|
||||
# 因果论断最多带几条进 PMS。取 3 有三条理由:
|
||||
# 一是上游 `card.logic_lines` 自己就按 3 条封顶, 取同一个数, 今天上游给了几条 PMS 就收
|
||||
# 几条 —— 截断不会静默丢掉已经给出来的研究结论 (那是这两个键存在的全部意义);
|
||||
# 二是它仍然是一道硬上限: 上游哪天把自己那个 3 调大, 提议卡、评审账本、提议确认表不会
|
||||
# 跟着一起被灌满, 这一层不用改码就挡得住;
|
||||
# 三是提议卡上放不下太多行 —— 论断行比理由长得多 (一句机制加一段出处), 卡片默认只显示
|
||||
# 第一条, 其余折叠, 3 条正好是「折开也还看得完」的量。
|
||||
LOGIC_KEEP = 3
|
||||
|
||||
|
||||
def _list_or_none(v, limit: int = 0):
|
||||
"""字符串列表归一: 列表逐项去空白、丢空项, 单个字符串当一项; 缺失或类型不对一律 None。
|
||||
|
|
@ -167,6 +196,9 @@ def _rows(raw, bucket: str) -> list:
|
|||
"missing": _list_or_none(it.get("missing")),
|
||||
"risk": _list_or_none(it.get("risk")),
|
||||
"card_rank": _int_or_none(it.get("card_rank")),
|
||||
# 研究理由 (2026-09-03): 只给人看, 不参与任何判定, 也不送研判。见模块说明。
|
||||
"basis": _text_or_none(it.get("basis")),
|
||||
"logic": _list_or_none(it.get("logic"), limit=LOGIC_KEEP),
|
||||
})
|
||||
return out
|
||||
|
||||
|
|
@ -349,7 +381,10 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N
|
|||
# 候选卡五键原样透传 (缺就是 None), 动作引擎把它们放进硬数字
|
||||
"verdict": r.get("verdict"), "reasons": r.get("reasons"),
|
||||
"missing": r.get("missing"), "risk": r.get("risk"),
|
||||
"card_rank": r.get("card_rank")}
|
||||
"card_rank": r.get("card_rank"),
|
||||
# 研究理由两键: 跟着候选一路带到提议卡与评审账本, 让人看得到研报说了什么。
|
||||
# 它们不进送研判的那份名单 (judge.OPEN_JUDGE_KEYS), 产业逻辑不归择时决策系统判。
|
||||
"basis": r.get("basis"), "logic": r.get("logic")}
|
||||
for r in passed[:n]]
|
||||
return {"date": plan.get("date"), "considered": len(pool), "eligible": len(passed),
|
||||
"items": items, "dropped": dropped, "st_unknown": st_unknown,
|
||||
|
|
|
|||
|
|
@ -1027,6 +1027,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
<span class="spacer"></span><span class="muted" style="font-size:11px">{{ p.expire_at }} 前有效</span></div>
|
||||
<div class="st" v-if="p.judge_reason"><span class="muted">决策系统:</span>{{ p.judge_reason }}<span class="muted" v-if="(p.hard_numbers||{}).judge_conf!=null">(置信度 {{ (p.hard_numbers||{}).judge_conf }})</span></div>
|
||||
<div class="st" v-if="(p.hard_numbers||{}).verdict"><span class="muted">选股系统:</span>判为「{{ (p.hard_numbers||{}).verdict }}」<span v-if="propReasons(p).length">,{{ propReasons(p).join(';') }}</span></div>
|
||||
<div class="st muted" v-if="propBasis(p)">{{ propBasis(p) }}</div>
|
||||
<div class="st" v-if="propLogic(p)"><span class="muted">研报说:</span>{{ propLogic(p) }}<span class="muted" v-if="propLogicMore(p)">(另有 {{ propLogicMore(p) }} 条)</span></div>
|
||||
<div class="st muted">{{ propWhy(p) }}</div>
|
||||
<div class="st">约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }}
|
||||
· 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}</div>
|
||||
|
|
@ -2365,6 +2367,18 @@ createApp({
|
|||
const r = (p.hard_numbers || {}).reasons;
|
||||
return Array.isArray(r) ? r.slice(0, 2) : [];
|
||||
}
|
||||
// 研究理由 (2026-09-03): 判决依据是上游对这条判决的一句话解释; 因果论断是研报里说这家
|
||||
// 公司好在哪, 每行自带「——出处:《研报标题》 披露日」。卡片窄, 只摆第一条论断,
|
||||
// 但那一条必须原样显示到出处与日期为止 —— 一条 2024 年的论断与一条上周的, 人得一眼分得出。
|
||||
function propBasis(p) { return (p.hard_numbers || {}).basis || ''; }
|
||||
function propLogic(p) {
|
||||
const l = (p.hard_numbers || {}).logic;
|
||||
return Array.isArray(l) && l.length ? l[0] : '';
|
||||
}
|
||||
function propLogicMore(p) {
|
||||
const l = (p.hard_numbers || {}).logic;
|
||||
return Array.isArray(l) && l.length > 1 ? (l.length - 1) : 0;
|
||||
}
|
||||
function nextStep(r) {
|
||||
if (r.frozen_reason && r.frozen_reason !== 'NONE') return '这只票现在只卖不买。';
|
||||
if (r.cushion_state === 'SOLID') return '浮盈够厚,条件符合就会自动加仓。';
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
test_batch5_units.py 决策系统信号流解析与消化口径 (8 例)
|
||||
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/弃洞/DDL/逐笔入账 (68 例)
|
||||
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫/
|
||||
候选卡五键透传与截断/仅展示在候选阶段剔除 (35 例)
|
||||
候选卡五键透传与截断/仅展示在候选阶段剔除/
|
||||
研究理由两键透传与不进送研判名单 (36 例)
|
||||
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例)
|
||||
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (49 例)
|
||||
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 +
|
||||
|
|
@ -53,8 +54,9 @@
|
|||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||
用户设的止损价与目标价单独成列显示 (70 例)
|
||||
共 651 例
|
||||
(上面这个总数此前写的 649 是笔误: 各批例数加起来一直是 650, 本次 +1 例后是 651)
|
||||
共 652 例
|
||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||
减持与研究理由两键各一例, 现为 652)
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
|
||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||
|
|
|
|||
|
|
@ -111,9 +111,37 @@ def _():
|
|||
for k in ("verdict", "reasons", "missing", "risk", "card_rank"):
|
||||
assert k in r and r[k] is None, (k, r.get(k))
|
||||
assert p["observe"][0][k] is None, (k, p["observe"][0][k])
|
||||
# 2026-09-03 起的研究理由两键同理: 旧样例没有, 必须是 None
|
||||
for k in ("basis", "logic"):
|
||||
assert k in r and r[k] is None, (k, r.get(k))
|
||||
assert p["plan_version"] is None and p["generated_at"] is None
|
||||
|
||||
|
||||
@case("解析·研究理由两键: 原样透传 / 论断截到三条 / 缺键为 None / 绝不进送研判的名单")
|
||||
def _():
|
||||
from app.services import judge as jd
|
||||
d = dict(SAMPLE)
|
||||
lines = [f"利好:机制{i}——出处:《某研报》 2026-06-23 · id{i}" for i in range(5)]
|
||||
d["main"] = [dict(SAMPLE["main"][0], basis="三门槛全过、无硬风险、明确吸筹且评分新鲜",
|
||||
logic=lines)]
|
||||
r = pf.parse_plan(d)["main"][0]
|
||||
assert r["basis"].startswith("三门槛全过"), r["basis"]
|
||||
# 上游给多少收多少, 但有硬上限: 论断行比理由长得多, 提议卡放不下
|
||||
assert r["logic"] == lines[:pf.LOGIC_KEEP] and len(r["logic"]) == 3, r["logic"]
|
||||
# 出处与披露日必须原样留在文字里 —— 一条 2024 年的论断与一条上周的, 人得一眼分得出
|
||||
assert "出处" in r["logic"][0] and "2026-06-23" in r["logic"][0], r["logic"][0]
|
||||
# 空列表与缺键要分得开: 上游说「这票没论断」是空列表, 「上游没给这个字段」才是 None。
|
||||
# 这与既有五键 (missing / risk) 一个口径, 空串则与缺键同义。
|
||||
d2 = dict(SAMPLE)
|
||||
d2["main"] = [dict(SAMPLE["main"][0], basis="", logic=[])]
|
||||
r2 = pf.parse_plan(d2)["main"][0]
|
||||
assert r2["basis"] is None, r2["basis"]
|
||||
assert r2["logic"] == [], r2["logic"]
|
||||
# 边界: 这两个键绝不能进送研判的名单。产业逻辑归数据基座与选股系统, 择时决策系统只判
|
||||
# 「此刻这只票买不买得下手」。以后有人顺手加进去, 这一条会拦住。
|
||||
assert "basis" not in jd.OPEN_JUDGE_KEYS and "logic" not in jd.OPEN_JUDGE_KEYS, jd.OPEN_JUDGE_KEYS
|
||||
|
||||
|
||||
@case("解析·themes 映射: 主榜优先于观察档, 同码不被观察档覆盖")
|
||||
def _():
|
||||
d = dict(SAMPLE)
|
||||
|
|
|
|||
Loading…
Reference in New Issue