"""事件回测纯函数:入场/出场索引语义、汇总统计(含空样本与分年)。""" from __future__ import annotations from datetime import datetime import pytest from app.backtest.events import _entry_exit_indices, _stats_block from app.schemas import EventBacktestSpec def _spec(holding_days: int = 5) -> EventBacktestSpec: return EventBacktestSpec.model_validate({ "holding_days": holding_days, "entry": {"indicator": []}, }) def test_entry_exit_next_day_and_hold(): spec = _spec(holding_days=5) assert _entry_exit_indices(10, spec, n=100) == (11, 16) # 次日入,持有 5 日出 def test_entry_exit_out_of_range_none(): spec = _spec(holding_days=5) # 出场索引越界(exit_i >= n) assert _entry_exit_indices(94, spec, n=100) is None assert _entry_exit_indices(99, spec, n=100) is None def test_entry_exit_boundary_exact_fit(): spec = _spec(holding_days=5) # exit_i == n-1 恰好可用 assert _entry_exit_indices(93, spec, n=100) == (94, 99) def test_stats_block_empty(): s = _stats_block([]) assert s["samples"] == 0 and s["stocks"] == 0 assert s["by_year"] == [] assert s["win_rate"] == 0.0 def test_stats_block_aggregates(): trades = [ {"ts_code": "000001.SZ", "ret_pct": 10.0, "entry_date": datetime(2024, 1, 5), "entry_price": 10, "exit_price": 11}, {"ts_code": "000001.SZ", "ret_pct": -4.0, "entry_date": datetime(2024, 3, 6), "entry_price": 10, "exit_price": 9.6}, {"ts_code": "600519.SH", "ret_pct": 2.0, "entry_date": datetime(2023, 5, 10), "entry_price": 10, "exit_price": 10.2}, ] s = _stats_block(trades) assert s["samples"] == 3 assert s["stocks"] == 2 assert s["mean_pct"] == pytest.approx((10.0 - 4.0 + 2.0) / 3, abs=1e-3) # 统计块保留 3 位小数 assert s["win_rate"] == round(2 / 3 * 100, 2) assert [y["year"] for y in s["by_year"]] == [2023, 2024] # 分年升序 assert s["by_year"][1]["samples"] == 2 assert s["max_pct"] == 10.0 and s["min_pct"] == -4.0