fix(backtest): 策略参数加跨语义约束,杜绝寻优选出快慢倒挂组合(issue #39)

寻优器网格探索不区分语义:ma_cross 的 fast∈[5..60] × slow∈[10..250]
笛卡尔积包含 {"fast":30,"slow":20} 这类倒挂组合,倒挂的双均线交叉
本质是反向策略,回测成绩可能反而突出,从而被选为"最优参数"展示
(即 issue #39 截图中 {"fast":30,"slow":20} 的来源)。presets.py 的
注释"快线<慢线才有意义;去重无效组合"早已写下意图但从未实现。

根因是参数校验只有单参数 min/max,缺跨参数语义约束:

- ParametrizedStrategy 新增 param_constraints 类属性 [(a,b), ...]
  表示要求 a<b,在 __init__ 解析后统一校验,报错带中文标签;
  语义约束不受 skip_bounds 影响(寻优跳过的只是数值边界)
- 7 个策略声明约束:ma_cross/ema_cross(fast<slow)、macd
  (short<long)、triple_ma(short<mid<long)、rsi_reversal/cci/
  wr_reversal(oversold<overbought)
- 寻优器 build 阶段的 ValueError 降为 info 级跳过(无堆栈噪音),
  回测异常仍走 warning;预设网格 36 组合 → 有效 25 个
- 新增 10 个回归测试(旧代码全失败、新代码全通过)
This commit is contained in:
GitHub
2026-08-19 18:55:48 +08:00
parent cc17ab650d
commit 3180ab3002
5 changed files with 94 additions and 5 deletions
+16 -1
View File
@@ -79,7 +79,8 @@ class TestParamGridOptimizer:
assert result.heatmap["y_name"] == "slow"
assert len(result.heatmap["x"]) == 3
assert len(result.heatmap["y"]) == 3
assert len(result.heatmap["data"]) == 9 # 3×3
# 3×3=9 中 fast=20×slow=15(倒挂)与 20×20(相等)被语义约束跳过(issue #39)
assert len(result.heatmap["data"]) == 7
def test_no_heatmap_for_single_param(self) -> None:
"""1 参数时 heatmap 应为 None。"""
@@ -92,6 +93,20 @@ class TestParamGridOptimizer:
assert result.heatmap is None
assert len(result.results) == 3
def test_inverted_combos_skipped(self) -> None:
"""网格含倒挂组合(fast≥slow)时应全部跳过,best 不可能是倒挂(issue #39)。"""
opt = ParamGridOptimizer(
strategy_name="ma_cross",
param_grid={"fast": [5, 20, 30], "slow": [10, 20]},
df=_make_df(),
)
result = opt.run()
# 6 组合中仅 (5,10) 与 (5,20) 语义有效
assert len(result.results) == 2
assert all(r.params["fast"] < r.params["slow"] for r in result.results)
assert result.best is not None
assert result.best.params["fast"] < result.best.params["slow"]
def test_grid_size_limit_exceeded(self) -> None:
"""超过 MAX_GRID_POINTS 应抛 ValueError。"""
big_grid = {f"p{i}": list(range(5)) for i in range(6)} # 5^6 = 15625