diff --git a/docs/screening-explanations.md b/docs/screening-explanations.md index 80ac420ee..aefefa101 100644 --- a/docs/screening-explanations.md +++ b/docs/screening-explanations.md @@ -16,7 +16,7 @@ ## Why Selected -确定性本地解释优先使用 screening reason 和当前策略实际参与评分的因子;零权重或未配置的因子既不会进入缺省 `selection_reason`,也不会被写成“核心因子”,两处展示顺序都按“因子分数 × 策略权重”的真实贡献排列。`risk_summary` / `risk_level` 始终保留在独立风险展示,不会在缺少 reason 时提升为 `selection_reason`;行业标签也不会单独冒充入选依据。缺少 reason 时只回退到加权因子或确定性排名说明。来自 `post_analysis_summaries` 的 DSA/外部 analyzer 摘要保留 `post_analyzer:` 来源并标记为 inferred,不冒充本地 observed;纯本地确定性 `scorecard` 摘要保持 observed,但只要 scorecard 消费了 `llm_confidence`、`llm_catalysts` 或 `llm_risks`,其解释质量就保持 inferred。即使 LLM 未配置、超时或返回无效结构,候选仍至少返回确定性排序/入选说明;LLM 不是本地解释的前置条件。 +确定性本地解释优先使用 screening reason 和当前策略实际参与评分的因子;零权重或未配置的因子既不会进入缺省 `selection_reason`,也不会被写成“核心因子”,两处展示顺序都按“因子分数 × 策略权重”的真实贡献排列。`risk_summary` / `risk_level` 始终保留在独立风险展示,不会在缺少 reason 时提升为 `selection_reason`;行业标签也不会单独冒充入选依据。缺少 reason 和可核验加权因子时只确认“已进入当前选股候选结果”,不会把可能经过 LLM 排序、组合约束或后处理调整的最终名次误写成“确定性筛选排名”。来自 `post_analysis_summaries` 的 DSA/外部 analyzer 摘要保留 `post_analyzer:` 来源并标记为 inferred,不冒充本地 observed;纯本地确定性 `scorecard` 摘要保持 observed,但只要 scorecard 消费了 `llm_confidence`、`llm_catalysts` 或 `llm_risks`,其解释质量就保持 inferred。即使 LLM 未配置、超时或返回无效结构,候选仍至少返回入选结果说明;LLM 不是本地解释的前置条件。 ## Why Now diff --git a/src/services/screening_service.py b/src/services/screening_service.py index f340eed52..d5a46b69e 100644 --- a/src/services/screening_service.py +++ b/src/services/screening_service.py @@ -3918,11 +3918,10 @@ def _attach_candidate_explanations( ) if not any(item.get("quality") == "observed" for item in why_selected): - rank = candidate.get("rank") why_selected.append( _explanation_item( - "selection_rank", - f"通过确定性筛选并排在第 {rank} 位" if rank is not None else "通过确定性筛选", + "selection_outcome", + "已进入当前选股候选结果", source="screening", quality="observed", ) diff --git a/tests/test_screening_explanations.py b/tests/test_screening_explanations.py index c1602514d..11103e839 100644 --- a/tests/test_screening_explanations.py +++ b/tests/test_screening_explanations.py @@ -177,7 +177,8 @@ def test_llm_reason_does_not_replace_the_observed_local_selection_fallback() -> result = _attach_candidate_explanations(candidate) assert [item["quality"] for item in result["why_selected"]] == ["inferred", "observed"] - assert result["why_selected"][1]["code"] == "selection_rank" + assert result["why_selected"][1]["code"] == "selection_outcome" + assert result["why_selected"][1]["text"] == "已进入当前选股候选结果" assert result["explanation_quality"]["why_selected"] == "partial" @@ -196,7 +197,7 @@ def test_distinct_llm_ranking_reason_stays_inferred_and_keeps_rank_fallback() -> result = _attach_candidate_explanations(candidate) assert [item["quality"] for item in result["why_selected"]] == ["inferred", "observed"] - assert result["why_selected"][1]["code"] == "selection_rank" + assert result["why_selected"][1]["code"] == "selection_outcome" def test_news_and_events_without_provenance_are_not_observed() -> None: @@ -229,7 +230,7 @@ def test_llm_risk_summary_stays_inferred_and_keeps_rank_fallback() -> None: result = _attach_candidate_explanations(candidate) assert [item["quality"] for item in result["why_selected"]] == ["inferred", "observed"] - assert result["why_selected"][1]["code"] == "selection_rank" + assert result["why_selected"][1]["code"] == "selection_outcome" def test_risk_summary_is_not_promoted_to_selection_reason_when_reason_is_missing() -> None: @@ -243,7 +244,7 @@ def test_risk_summary_is_not_promoted_to_selection_reason_when_reason_is_missing assert candidate["risk_summary"] == "估值过高" assert candidate["reason"] == "" - assert [item["code"] for item in result["why_selected"]] == ["selection_rank"] + assert [item["code"] for item in result["why_selected"]] == ["selection_outcome"] assert all("估值过高" not in item["text"] for item in result["why_selected"]) @@ -260,7 +261,7 @@ def test_post_analyzer_summary_keeps_inferred_provenance() -> None: assert reason["code"] == "selection_reason" assert reason["source"] == "post_analyzer:dsa" assert reason["quality"] == "inferred" - assert result["why_selected"][1]["code"] == "selection_rank" + assert result["why_selected"][1]["code"] == "selection_outcome" def test_local_scorecard_summary_keeps_observed_provenance() -> None: @@ -306,7 +307,7 @@ def test_risk_level_is_not_promoted_to_selection_reason() -> None: result = _attach_candidate_explanations(candidate) assert candidate["reason"] == "" - assert [item["code"] for item in result["why_selected"]] == ["selection_rank"] + assert [item["code"] for item in result["why_selected"]] == ["selection_outcome"] assert "风险" not in result["why_selected"][0]["text"]