Files
tick-stock-panel/backend/tests/test_backtest_warmup.py
T
shy3130 ef59df4a67 fix: 修复 6 个 issue (#225/#226/#201/#188/#200/#196)
后端:
- #225 自定义源分钟K字符串 datetime 不再被 cast 成 null:
  _normalize_minute 对 Utf8 列按常见格式链式解析 (参照 kline_sync 口径)
- #226 自定义源日K/除权因子单批失败只隔离该批 (重试 1 次 + 跳过 +
  warning 汇总), 不再丢弃已成功批次的全部进度
- #201 旧信号回测 _load_panel 加指标 warmup 窗口 (120 交易日保守日历日),
  计算后裁回 [start,end]; 数据不足时自然退化

前端:
- #188 因子回测单标的不再整面板空白: 外层条件改 !error, IC 卡片
  单独守卫并给出需 >=2 只的提示
- #200 因子回测支持调仓频率 (日/周/月) 与滑点 (bp) 配置
- #196 自选页板块筛选新增 ETF 分类, 旧偏好加载时补 ETF 键保持默认可见
2026-09-03 13:20:59 +08:00

67 lines
2.3 KiB
Python

"""#201 回归: 旧信号回测的 _load_panel 必须带指标 warmup 窗口。
直接按 [start,end] 过滤后 compute_all, 区间头部的 MA/MACD/RSI 会因缺
历史窗口而失真 (回测起始段信号不可信)。
"""
from __future__ import annotations
from datetime import date, timedelta
from unittest.mock import MagicMock
import polars as pl
from app.services.backtest import BacktestService
def _synthetic_enriched(n_days: int) -> pl.DataFrame:
base = date(2026, 1, 1)
days = [base + timedelta(days=i) for i in range(n_days)]
n = len(days)
closes = [10.0 + (i % 7) * 0.3 + i * 0.01 for i in range(n)]
return pl.DataFrame(
{
"symbol": ["600000.SH"] * n,
"date": days,
"open": [c - 0.05 for c in closes],
"high": [c + 0.1 for c in closes],
"low": [c - 0.1 for c in closes],
"close": closes,
"volume": [10000.0] * n,
"amount": [c * 10000.0 for c in closes],
"raw_close": closes,
"raw_high": [c + 0.1 for c in closes],
"raw_low": [c - 0.1 for c in closes],
}
)
def test_load_panel_warms_up_indicators(monkeypatch) -> None:
df = _synthetic_enriched(250)
monkeypatch.setattr(
"app.services.backtest.scan_enriched_parquet", lambda glob: df.lazy()
)
svc = BacktestService(repo=MagicMock())
start = df["date"][-30]
end = df["date"][-1]
panel = svc._load_panel(["600000.SH"], start, end)
# warmup 行不进入结果面板 (pandas datetime64 与 date 直接比较会类型不符)
assert str(panel["date"].min())[:10] == start.isoformat()
assert str(panel["date"].max())[:10] == end.isoformat()
# 区间首日的指标已有历史窗口可用, 不再是 NaN
first = panel.iloc[0]
assert first["rsi_14"] == first["rsi_14"] # NaN != NaN
def test_load_panel_insufficient_history_degrades_gracefully(monkeypatch) -> None:
# 数据起点晚于 warmup 起点时自然退化: 有多少算多少, 不抛异常
df = _synthetic_enriched(20)
monkeypatch.setattr(
"app.services.backtest.scan_enriched_parquet", lambda glob: df.lazy()
)
svc = BacktestService(repo=MagicMock())
panel = svc._load_panel(["600000.SH"], df["date"][0], df["date"][-1])
assert len(panel) == 20