diff --git a/backend/app/services/concept_rotation_analyzer.py b/backend/app/services/concept_rotation_analyzer.py index 0b574c4..b94964e 100644 --- a/backend/app/services/concept_rotation_analyzer.py +++ b/backend/app/services/concept_rotation_analyzer.py @@ -206,11 +206,24 @@ def _compute_rotation_signals(dates: list[str], columns: dict) -> dict: # ================================================================ def _fmt_pct(v) -> str: + """概念/行业涨幅: 小数口径 (0.0522 = +5.22%), 展示前乘 100。""" if v is None: return "—" return f"{v*100:+.2f}%" +def _fmt_index_pct(v) -> str: + """指数涨跌幅: 百分数口径 (CONTRIBUTING §3.1), 直接展示, 不能再乘一次 100。 + + build_market_overview 的 indices[].change_pct 在数据边界已转成百分数 + (quote_service._build_index_quotes 与 _index_quotes 的 DB 兜底都已乘过 100), + 与 market_recap._build_indices_block 的展示口径一致。 + """ + if v is None: + return "—" + return f"{v:+.2f}%" + + def _build_market_block(overview: dict) -> str: """大盘背景精简块 (复用 market_overview 已算好的字段)。""" indices = overview.get("indices") or [] @@ -222,7 +235,7 @@ def _build_market_block(overview: dict) -> str: for idx in indices[:4]: name = idx.get("name") or idx.get("symbol") or "?" chg = idx.get("change_pct") - idx_lines.append(f"{name} {_fmt_pct(chg)}") + idx_lines.append(f"{name} {_fmt_index_pct(chg)}") idx_str = " / ".join(idx_lines) or "指数缺失" total_amount = (amt.get("total") or 0) / 1e8 # 元 → 亿 diff --git a/backend/tests/test_rotation_prompt_index_pct.py b/backend/tests/test_rotation_prompt_index_pct.py new file mode 100644 index 0000000..946a25b --- /dev/null +++ b/backend/tests/test_rotation_prompt_index_pct.py @@ -0,0 +1,56 @@ +"""AI 轮动分析提示词里的「大盘背景」指数涨跌幅必须按百分数口径。 + +CONTRIBUTING §3.1: build_market_overview 的 indices[].change_pct 是百分数 +(quote_service._build_index_quotes 在数据边界已乘过 100, DB 兜底路径同样乘过 100)。 +概念/行业涨幅则是小数。两者共用一个 _fmt_pct 会把指数再放大 100 倍, +喂给 LLM 的大盘背景变成「上证指数 +123.00%」。 +""" +from __future__ import annotations + +from app.services.concept_rotation_analyzer import ( + _build_market_block, + _build_signal_block, +) + + +def _overview() -> dict: + """与 build_market_overview 返回结构一致的最小切片。""" + return { + "indices": [ + {"symbol": "000001.SH", "name": "上证指数", "change_pct": 1.23}, + {"symbol": "399001.SZ", "name": "深证成指", "change_pct": -0.45}, + ], + "emotion": {"score": 62, "label": "偏暖"}, + "limit": {"limit_up": 68, "broken": 20, "limit_down": 3, "max_boards": 5}, + "amount": {"total": 1.2e12}, + } + + +def test_index_change_pct_is_not_scaled_again(): + block = _build_market_block(_overview()) + + assert "上证指数 +1.23%" in block + assert "深证成指 -0.45%" in block + assert "123.00%" not in block + + +def test_missing_index_change_pct_renders_placeholder(): + overview = _overview() + overview["indices"][0]["change_pct"] = None + + assert "上证指数 —" in _build_market_block(overview) + + +def test_dimension_pct_is_still_scaled_from_decimal(): + """概念/行业涨幅仍是小数口径, 必须乘 100 后展示 (不能被一起改掉)。""" + items = [{ + "concept": "人工智能", + "ranks": [3, 1], + "pcts": [0.05, 0.07], + "avg_rank": 2.0, + "rank_std": 1.0, + }] + + block = _build_signal_block("主线", items) + + assert "区间均涨 +6.00%" in block