From 7755ab3a7d5d7d62830d36ea8ae99fe9b8380716 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:32:13 +0900 Subject: [PATCH] =?UTF-8?q?fix(overview):=20=E6=9D=BF=E5=9D=97=E9=A2=86?= =?UTF-8?q?=E6=B6=A8=E8=82=A1=E4=B8=8D=E5=86=8D=E6=8A=8A=200.00%=20?= =?UTF-8?q?=E5=BD=93=E6=88=90=E7=BC=BA=E5=A4=B1=E6=B6=A8=E8=B7=8C=E5=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _dimension_rank 用 `_finite(...) or -999` 作为排序键, 0.00% 是假值, 会被替换成 -999 排到所有下跌股之后。板块整体下跌、最强的一只恰好平盘时, 看板/复盘的「领涨」显示成跌幅最小的下跌股, 而不是那只平盘股。 改为显式区分 None 与 0.0 (缺涨跌幅 → -inf), 与 sector_monitor _dimension_snapshot 的 `max(valid_rows, key=lambda row: row["change_pct"])` 同口径。 --- .../app/services/market_overview_builder.py | 12 ++- .../tests/test_overview_dimension_leader.py | 89 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_overview_dimension_leader.py diff --git a/backend/app/services/market_overview_builder.py b/backend/app/services/market_overview_builder.py index f879fb4..9f65931 100644 --- a/backend/app/services/market_overview_builder.py +++ b/backend/app/services/market_overview_builder.py @@ -237,6 +237,16 @@ def _symbol_keys(row: dict, config: ExtConfig) -> list[str]: return keys +def _leader_sort_key(row: dict) -> float: + """领涨股排序键: 缺涨跌幅的成分股排最后。 + + 0.00% 是有效涨跌幅, 不能与"无行情"合并成同一个哨兵值 —— 板块整体下跌时 + 平盘股就是领涨股。 + """ + value = _finite(row.get("change_pct")) + return value if value is not None else float("-inf") + + def _dimension_rank(rows: list[dict], repo, kind: str, limit: int = 5, level: int | None = None) -> dict: if not rows: return {"leading": [], "lagging": []} @@ -281,7 +291,7 @@ def _dimension_rank(rows: list[dict], repo, kind: str, limit: int = 5, level: in changes = [v for v in changes if v is not None] if not changes: continue - leader = max(stocks, key=lambda s: _finite(s.get("change_pct")) or -999) + leader = max(stocks, key=_leader_sort_key) items.append({ "name": name, "count": len(stocks), diff --git a/backend/tests/test_overview_dimension_leader.py b/backend/tests/test_overview_dimension_leader.py new file mode 100644 index 0000000..004b4ef --- /dev/null +++ b/backend/tests/test_overview_dimension_leader.py @@ -0,0 +1,89 @@ +"""市场总览「领涨股」选取: 涨跌幅 0.00% 是有效值, 不能被当成缺失值。""" +from __future__ import annotations + +import json + +import polars as pl + +from app.services.market_overview_builder import _dimension_rank + + +def _fake_repo(tmp_path): + import types + + return types.SimpleNamespace(store=types.SimpleNamespace(data_dir=tmp_path)) + + +def _write_concept_ext(tmp_path, mapping: dict[str, str]) -> None: + """写一张 snapshot 模式的概念扩展表 (symbol → 所属概念)。""" + cfg_dir = tmp_path / "ext_data" / "concept_tbl" + cfg_dir.mkdir(parents=True, exist_ok=True) + cfg_dir.joinpath("config.json").write_text( + json.dumps({ + "id": "concept_tbl", + "label": "概念表", + "mode": "snapshot", + "fields": [{"name": "所属概念", "dtype": "string", "label": "所属概念"}], + }, ensure_ascii=False), + encoding="utf-8", + ) + pl.DataFrame({ + "symbol": list(mapping.keys()), + "所属概念": list(mapping.values()), + }).write_parquet(cfg_dir / "part.parquet") + + +def test_flat_stock_can_be_leader_of_a_falling_concept(tmp_path): + """全概念下跌、最强的一只恰好平盘(0.00%)时, 领涨股必须是那只平盘股。""" + _write_concept_ext(tmp_path, { + "000001.SZ": "人工智能", + "000002.SZ": "人工智能", + "000003.SZ": "人工智能", + }) + rows = [ + {"symbol": "000001.SZ", "name": "跌一", "change_pct": -0.01, "amount": 1e8}, + {"symbol": "000002.SZ", "name": "平盘", "change_pct": 0.0, "amount": 2e8}, + {"symbol": "000003.SZ", "name": "跌三", "change_pct": -0.03, "amount": 3e8}, + ] + + result = _dimension_rank(rows, _fake_repo(tmp_path), "concept") + + items = {item["name"]: item for item in result["lagging"]} + assert "人工智能" in items + leader = items["人工智能"]["leader"] + assert leader["name"] == "平盘" + assert leader["change_pct"] == 0.0 + + +def test_leader_falls_back_to_none_pct_last(tmp_path): + """change_pct 缺失(None)的成分股仍排在所有有值的成分股之后。""" + _write_concept_ext(tmp_path, { + "000001.SZ": "芯片", + "000002.SZ": "芯片", + }) + rows = [ + {"symbol": "000001.SZ", "name": "无行情", "change_pct": None, "amount": 1e8}, + {"symbol": "000002.SZ", "name": "微跌", "change_pct": -0.02, "amount": 1e8}, + ] + + result = _dimension_rank(rows, _fake_repo(tmp_path), "concept") + + items = {item["name"]: item for item in result["lagging"]} + assert items["芯片"]["leader"]["name"] == "微跌" + + +def test_leader_still_picks_max_when_all_positive(tmp_path): + """普通情形不受影响: 全部上涨时仍取涨幅最大的一只。""" + _write_concept_ext(tmp_path, { + "000001.SZ": "光伏", + "000002.SZ": "光伏", + }) + rows = [ + {"symbol": "000001.SZ", "name": "涨多", "change_pct": 0.05, "amount": 1e8}, + {"symbol": "000002.SZ", "name": "涨少", "change_pct": 0.01, "amount": 1e8}, + ] + + result = _dimension_rank(rows, _fake_repo(tmp_path), "concept") + + items = {item["name"]: item for item in result["leading"]} + assert items["光伏"]["leader"]["name"] == "涨多"