mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 23:44:16 +08:00
问题: `/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>
90 lines
3.0 KiB
Python
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)
|