mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
_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"])` 同口径。
90 lines
3.2 KiB
Python
90 lines
3.2 KiB
Python
"""市场总览「领涨股」选取: 涨跌幅 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"] == "涨多"
|