Files
kevin9327andClaude Opus 4.8 8aa6e8d89b fix(backtest): 因子回测默认区间改用 180 天而非 3 年 (#202)
问题: `/api/backtest/factor/run` 与 `/api/backtest/factor/batch` 省略 start
时, 使用了策略默认区间 STRATEGY_DEFAULT_DAYS(3 年), 而非为因子回测定义的
FACTOR_DEFAULT_DAYS(180 天)。以 end=2026-08-24 为例, 会解析成 2023-08-25。

影响: 用户只选因子直接运行会加载约 3 年数据, 徒增内存与耗时; 也与前端
「默认最近 3 个月」的提示不一致。且服务器区间守卫上限为 186 天, 该默认区间
在开启守卫时会被直接拒绝。

根因: factor_run / factor_batch 调用 _resolve_start 时误传 STRATEGY_DEFAULT_DAYS。
同文件的 strategy_run 已正确使用 FACTOR_DEFAULT_DAYS, 仅两个因子入口写反。

修复: 两处改为 FACTOR_DEFAULT_DAYS。显式传 start=null(全部历史)与显式日期的
语义保持不变(由 _resolve_start 的 model_fields_set 判定, 未改动)。

验证: 新增 tests/backtest/test_factor_default_range.py, 用替身 Service 捕获
传入配置, 覆盖省略 start、显式 null、显式日期三种情况(factor_run + factor_batch)。
修复前 2 例失败(解析为 2023-08-25), 修复后 4 例全过; 相邻 factor/optimizer
回归 28 passed; ruff 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:42:32 +09:00

90 lines
3.0 KiB
Python

"""因子回测默认区间回归测试。
覆盖 issue #202: `/api/backtest/factor/run` 与 `/api/backtest/factor/batch`
在省略 `start` 时错误使用了策略默认区间(3 年)而非因子默认区间(180 天)。
只关注「起始日解析」这一关注点, 用替身 Service 捕获传入配置, 不做真实数据加载。
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
from typing import ClassVar
import pytest
from app.api import backtest as bt
@dataclass
class _DummyResult:
ok: bool = True
class _CapturingService:
"""替身 FactorBacktestService: 捕获配置, 不触真实引擎。"""
captured: ClassVar[list] = []
def __init__(self, engine):
pass
def run(self, cfg):
_CapturingService.captured.append(cfg)
return _DummyResult()
def run_batch(self, cfg):
_CapturingService.captured.append(cfg)
return _DummyResult()
class _Req:
"""占位 request; _get_engine 已被 patch, 不会真正访问其属性。"""
@pytest.fixture
def patched(monkeypatch):
_CapturingService.captured = []
monkeypatch.setattr(bt, "_get_engine", lambda request: object())
# factor_run / factor_batch 内部 `from app.backtest.factor import FactorBacktestService`
# 读取的是模块属性, patch 源模块即可命中。
import app.backtest.factor as factor_mod
monkeypatch.setattr(factor_mod, "FactorBacktestService", _CapturingService)
# 隔离守卫, 单测「起始日解析」。
monkeypatch.setattr(bt, "_guard_server_backtest_range", lambda start, end: None)
return _CapturingService
def test_factor_run_omitted_start_uses_factor_default(patched):
end = date(2026, 8, 24)
bt.factor_run(bt.FactorBacktestRequest(factor_name="momentum_5d", end=end), _Req())
cfg = patched.captured[-1]
assert cfg.start == end - timedelta(days=bt.FACTOR_DEFAULT_DAYS)
# 回归保护: 不得再退回策略默认的 3 年。
assert cfg.start != end - timedelta(days=bt.STRATEGY_DEFAULT_DAYS)
def test_factor_batch_omitted_start_uses_factor_default(patched):
end = date(2026, 8, 24)
bt.factor_batch(bt.FactorBatchRequest(factor_names=["momentum_5d"], end=end), _Req())
cfg = patched.captured[-1]
assert cfg.start == end - timedelta(days=bt.FACTOR_DEFAULT_DAYS)
assert cfg.start != end - timedelta(days=bt.STRATEGY_DEFAULT_DAYS)
def test_factor_run_explicit_null_start_means_all_history(patched):
"""显式传 start=null 语义不变: 代表全部历史。"""
end = date(2026, 8, 24)
bt.factor_run(bt.FactorBacktestRequest(factor_name="momentum_5d", start=None, end=end), _Req())
assert patched.captured[-1].start == date(1900, 1, 1)
def test_factor_run_explicit_start_passthrough(patched):
"""显式日期原样透传。"""
end = date(2026, 8, 24)
bt.factor_run(
bt.FactorBacktestRequest(factor_name="momentum_5d", start=date(2026, 1, 1), end=end),
_Req(),
)
assert patched.captured[-1].start == date(2026, 1, 1)