Files
tick-stock-panel/backend/tests/test_dimension_intraday.py
shy3130 fb7785ceb9 feat(abnormal,dashboard): 盘中异动聚合、维度 source_field 与板块分时端点
- abnormal_moves 新增 build_intraday: enriched 七类当日信号聚合,
  优先级(涨停>炸板>翘板>跌停>新高>新低>放量)+涨跌幅排序, GET /api/abnormal/intraday
- 维度排名项携带 group_source 与 source_field(configId.field),
  前端可精确判定概念/行业而非字符串包含
- ext_data 新增 dimension-intraday: 成分股×当日分钟分区等权聚合,
  prev_close 优先/首根退化基准、成分网格化 ffill、全市场对照线,
  小数制涨跌幅契约、60s 进程内缓存、点击触发不预计算
- 板块分时 7 测 + 盘中异动 4 测
2026-08-30 19:05:32 +08:00

159 lines
7.0 KiB
Python

"""板块分时 (dimension-intraday) 纯函数测试。
夹具: snapshot 扩展配置 (所属概念) + kline_minute/kline_daily 分区,
验证等权口径、停牌 ffill、prev_close/首根基准与各降级状态。
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import polars as pl
from app.api.ext_data import _dimension_intraday_compute
from app.services.ext_data import ExtConfig
def _mk_config() -> ExtConfig:
return ExtConfig(id="ext_gn", label="测试概念", mode="snapshot", fields=[])
def _write_ext(data_dir: Path, values: dict[str, str]) -> None:
"""snapshot 扩展数据: symbol → 所属概念 标签串。"""
cfg_dir = data_dir / "ext_data" / "ext_gn"
cfg_dir.mkdir(parents=True, exist_ok=True)
df = pl.DataFrame({
"symbol": list(values.keys()),
"所属概念": list(values.values()),
})
df.write_parquet(cfg_dir / "part.parquet")
def _write_minute(data_dir: Path, day: str, rows: list[tuple[str, str, float]]) -> None:
part = data_dir / "kline_minute" / f"date={day}" / "part.parquet"
part.parent.mkdir(parents=True, exist_ok=True)
df = pl.DataFrame(
{
"symbol": [r[0] for r in rows],
"datetime": [datetime.fromisoformat(r[1]) for r in rows],
"close": [r[2] for r in rows],
},
schema_overrides={"datetime": pl.Datetime("us")},
)
df.write_parquet(part)
def _write_daily(data_dir: Path, day: str, closes: dict[str, float]) -> None:
part = data_dir / "kline_daily" / f"date={day}" / "part.parquet"
part.parent.mkdir(parents=True, exist_ok=True)
pl.DataFrame({
"symbol": list(closes.keys()),
"close": list(closes.values()),
}).write_parquet(part)
def test_dimension_intraday_equal_weight_and_ffill(tmp_path: Path) -> None:
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能、芯片", "000002.SZ": "人工智能"})
# 前收: 000001=10.0 (+5%/+6%/+4%), 000002=20.0, 600000 非成分股
_write_daily(data_dir, "2026-08-27", {"000001.SZ": 10.0, "000002.SZ": 20.0, "600000.SH": 5.0})
# 000002 在 09:32 无成交 (停牌分钟) → ffill 沿用 20.4
_write_minute(data_dir, "2026-08-28", [
("000001.SZ", "2026-08-28T09:31:00", 10.5),
("000002.SZ", "2026-08-28T09:31:00", 20.4),
("600000.SH", "2026-08-28T09:31:00", 5.05),
("000001.SZ", "2026-08-28T09:32:00", 10.6),
("600000.SH", "2026-08-28T09:32:00", 5.10),
("000001.SZ", "2026-08-28T09:33:00", 10.4),
("000002.SZ", "2026-08-28T09:33:00", 20.8),
("600000.SH", "2026-08-28T09:33:00", 5.20),
])
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert payload["status"] == "ok"
assert payload["date"] == "2026-08-28"
assert payload["basis"] == "prev_close"
assert payload["member_count"] == 2
assert payload["members_with_minute"] == 2
points = payload["points"]
assert [p["time"] for p in points] == ["09:31", "09:32", "09:33"]
# 09:31: 成分等权 (5% + 2%)/2 = 3.5%; 全市场 (5+2+1)/3 ≈ 2.667% (小数制)
assert points[0]["sector"] == 0.035
assert points[0]["market"] == 0.0267
# 09:32: 000002 ffill 20.4 → (6% + 2%)/2 = 4.0% (无 ffill 会是 6.0%)
assert points[1]["sector"] == 0.04
assert points[1]["market"] == 0.04
# 09:33: (4% + 4%)/2 = 4.0%
assert points[2]["sector"] == 0.04
def test_dimension_intraday_tag_no_partial_match(tmp_path: Path) -> None:
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能体"})
_write_minute(data_dir, "2026-08-28", [("000001.SZ", "2026-08-28T09:31:00", 10.5)])
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert payload["status"] == "empty"
assert payload["reason"] == "no_members"
def test_dimension_intraday_no_minute_store(tmp_path: Path) -> None:
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能"})
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert payload["status"] == "no_data"
assert payload["reason"] == "minute_missing"
def test_dimension_intraday_requested_date_absent(tmp_path: Path) -> None:
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能"})
_write_minute(data_dir, "2026-08-28", [("000001.SZ", "2026-08-28T09:31:00", 10.5)])
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", "2026-08-27")
assert payload["status"] == "no_data"
assert payload["reason"] == "minute_missing"
def test_dimension_intraday_members_without_bars(tmp_path: Path) -> None:
"""成分股全是 ETF 等无分钟数据的标的 → empty/no_member_bars。"""
data_dir = tmp_path / "data"
_write_ext(data_dir, {"510050.SH": "人工智能"})
_write_daily(data_dir, "2026-08-27", {"510050.SH": 3.0, "000001.SZ": 10.0})
_write_minute(data_dir, "2026-08-28", [("000001.SZ", "2026-08-28T09:31:00", 10.5)])
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert payload["status"] == "empty"
assert payload["reason"] == "no_member_bars"
assert payload["member_count"] == 1
def test_dimension_intraday_first_close_basis(tmp_path: Path) -> None:
"""无前一交易日日K → 基准退化为当日首根 close, 曲线起点 ≈ 0。"""
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能"})
_write_minute(data_dir, "2026-08-28", [
("000001.SZ", "2026-08-28T09:31:00", 10.0),
("000001.SZ", "2026-08-28T09:32:00", 10.3),
])
payload = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert payload["status"] == "ok"
assert payload["basis"] == "first_close"
assert payload["points"][0]["sector"] == 0.0
assert payload["points"][1]["sector"] == 0.03
def test_dimension_intraday_explicit_date_uses_that_partition(tmp_path: Path) -> None:
data_dir = tmp_path / "data"
_write_ext(data_dir, {"000001.SZ": "人工智能"})
_write_daily(data_dir, "2026-08-26", {"000001.SZ": 10.0})
_write_daily(data_dir, "2026-08-27", {"000001.SZ": 11.0})
_write_minute(data_dir, "2026-08-27", [("000001.SZ", "2026-08-27T09:31:00", 10.5)])
_write_minute(data_dir, "2026-08-28", [("000001.SZ", "2026-08-28T09:31:00", 12.1)])
# 默认取最新分区 2026-08-28 → prev 为 08-27 的 11.0 → +10%
latest = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", None)
assert latest["date"] == "2026-08-28"
assert latest["points"][0]["sector"] == 0.1
# 显式指定 08-27 → prev 为 08-26 的 10.0 → +5%
explicit = _dimension_intraday_compute(_mk_config(), data_dir, "所属概念", "人工智能", "2026-08-27")
assert explicit["date"] == "2026-08-27"
assert explicit["points"][0]["sector"] == 0.05