mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 19:04:15 +08:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
afbf432eae
commit
8aa6e8d89b
@@ -158,7 +158,7 @@ def factor_run(req: FactorBacktestRequest, request: Request):
|
||||
svc = FactorBacktestService(engine)
|
||||
|
||||
end = req.end or date.today()
|
||||
start = _resolve_start(req, end, STRATEGY_DEFAULT_DAYS)
|
||||
start = _resolve_start(req, end, FACTOR_DEFAULT_DAYS)
|
||||
_guard_server_backtest_range(start, end)
|
||||
symbols = req.symbols if req.symbols else None
|
||||
if symbols is not None and len(symbols) > FACTOR_MAX_SYMBOLS:
|
||||
@@ -212,7 +212,7 @@ def factor_batch(req: FactorBatchRequest, request: Request):
|
||||
raise HTTPException(status_code=400, detail=f"不支持的因子: {', '.join(invalid)}")
|
||||
|
||||
end = req.end or date.today()
|
||||
start = _resolve_start(req, end, STRATEGY_DEFAULT_DAYS)
|
||||
start = _resolve_start(req, end, FACTOR_DEFAULT_DAYS)
|
||||
_guard_server_backtest_range(start, end)
|
||||
symbols = req.symbols if req.symbols else None
|
||||
if symbols is not None and len(symbols) > FACTOR_MAX_SYMBOLS:
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""因子回测默认区间回归测试。
|
||||
|
||||
覆盖 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)
|
||||
Reference in New Issue
Block a user