From c289ac57676625adf9218ccec0300f082f9410ec Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Sat, 5 Sep 2026 20:33:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(backtest):=20=E6=AD=A5=E8=BF=9B=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=94=AF=E6=8C=81=20python=5Fhistory=5Flegacy/polars?= =?UTF-8?q?=5Fexpr=20=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: WalkForwardService._prepare_shared_matrix 对非 matrix_native 策略直接抛 "步进优化暂仅支持矩阵策略", filter_history 类自定义策略 (如 everbloom 系列) 无法做步进优化。 根因: 闸门把非矩阵后端一刀切, 但 run() 内部本就存在 shared_market_data=None 的通用路径 (每折独立优化 + 独立 OOS 回测), 只是永远不会被走到。 方案: 闸门改分流 —— matrix_native 仍走共享矩阵加速路径; python_history_legacy 与 polars_expr 返回 None 走通用每折回测路径 (正确但无矩阵加速); composite / minute_filter 维持 fail-closed 拒绝, 错误文案同步更新。 兼容: matrix_native 行为不变; 结果字段 shared_market_data=false 标识通用路径。 性能: 非矩阵策略每组合×每折一次完整回测, 大网格耗时线性放大 (UI 已有耗时提示)。 验证: uv run --frozen pytest tests/backtest/test_walkforward.py -q (23 passed, 含新增 3 例: legacy 走通用路径/polars_expr 放行/minute_filter 拒绝); tests/backtest 全套 282 passed; ruff 无新增告警; 真实 API 端到端: everbloom_tupengpan 2x2 网格 120/30 步进 8/8 折全部完成。 --- backend/app/backtest/walkforward.py | 13 ++++-- backend/tests/backtest/test_walkforward.py | 52 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/backend/app/backtest/walkforward.py b/backend/app/backtest/walkforward.py index dfdb7c1..1f2bc19 100644 --- a/backend/app/backtest/walkforward.py +++ b/backend/app/backtest/walkforward.py @@ -149,14 +149,21 @@ class WalkForwardService: self.strategy_engine = strategy_engine def _prepare_shared_matrix(self, cfg: WalkForwardConfig, folds: list[Fold]): - """Build one immutable superset matrix for every matrix-native fold.""" + """Build one immutable superset matrix for every matrix-native fold. + + 返回 None 时 run() 走通用路径: 每折独立优化 + OOS 回测, 正确但无共享矩阵加速。 + python_history_legacy (filter_history) 与 polars_expr (内置) 策略无法装入 + 共享矩阵, 走通用路径; composite / minute_filter 仍不支持, 保持 fail-closed。 + """ if self.strategy_engine is None or not folds: return None strategy = self.strategy_engine.get(cfg.strategy_id) if strategy.execution_backend != "matrix_native": + if strategy.execution_backend in ("python_history_legacy", "polars_expr"): + return None raise ValueError( - f"步进优化暂仅支持矩阵(matrix_native)策略; " - f"{cfg.strategy_id} 是 {strategy.execution_backend}" + f"步进优化暂仅支持矩阵(matrix_native)/日线历史(python_history_legacy/" + f"polars_expr)策略; {cfg.strategy_id} 是 {strategy.execution_backend}" ) from app.backtest.optimizer import expand_param_grid diff --git a/backend/tests/backtest/test_walkforward.py b/backend/tests/backtest/test_walkforward.py index 4c0d02c..3971aeb 100644 --- a/backend/tests/backtest/test_walkforward.py +++ b/backend/tests/backtest/test_walkforward.py @@ -239,6 +239,58 @@ def test_walkforward_reports_degradation(): assert abs(out["summary"]["degradation"] - 1.0) < 1e-9 +# --------------------------------------------------------------- +# 非矩阵后端的闸门: filter_history/内置走通用路径, 其余 fail-closed +# --------------------------------------------------------------- + +class _FakeStrategyDef: + def __init__(self, backend): + self.execution_backend = backend + + +class _FakeStrategyEngine: + def get(self, strategy_id): + return _FakeStrategyDef(self._backend) + + def __init__(self, backend): + self._backend = backend + + +def test_walkforward_supports_filter_history_strategy(): + """python_history_legacy (filter_history) 策略走通用路径: + 不建共享矩阵, 每折在训练区间独立优化、测试区间独立 OOS。""" + opt, svc = _FakeOptimizer(), _FakeService() + wf = WalkForwardService(opt, svc, strategy_engine=_FakeStrategyEngine("python_history_legacy")) + out = wf.run(_wf_cfg()) + + assert out["n_folds"] > 0 + assert out["shared_market_data"] is False + assert len(opt.train_ranges) == out["n_folds"] + assert len(svc.calls) == out["n_folds"] + first_fold = out["folds"][0] + assert svc.calls[0]["params"] == first_fold["best_params"] + assert svc.calls[0]["start"] >= opt.train_ranges[0][1] + + +def test_walkforward_supports_polars_expr_strategy(): + """内置 polars_expr 策略同样走通用路径 (与矩阵策略共用同一套折编排)。""" + opt, svc = _FakeOptimizer(), _FakeService() + wf = WalkForwardService(opt, svc, strategy_engine=_FakeStrategyEngine("polars_expr")) + out = wf.run(_wf_cfg()) + assert out["n_folds"] > 0 + assert out["shared_market_data"] is False + + +def test_walkforward_rejects_minute_filter_strategy(): + """minute_filter / composite 无法用日线折编排评估: 保持 fail-closed。""" + wf = WalkForwardService( + _FakeOptimizer(), _FakeService(), + strategy_engine=_FakeStrategyEngine("minute_filter"), + ) + with pytest.raises(ValueError, match=r"步进优化暂仅支持|minute_filter"): + wf.run(_wf_cfg()) + + class _NoParamsOptimizer(_FakeOptimizer): """模拟训练区间全组失败: best_params=None。""" def optimize(self, cfg, progress_cb=None, cancel_event=None):