From 72a0f51a641373ed666e402dc74cb501454dc9ef Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:47:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(optimizer):=20=E5=8F=82=E6=95=B0=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E6=8C=89=E6=AD=A5=E9=95=BF=E5=B1=95=E5=BC=80=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=B6=8A=E8=BF=87=E7=94=A8=E6=88=B7=E5=A1=AB=E5=86=99?= =?UTF-8?q?=E7=9A=84=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (max-min) 不是 step 整数倍时, round() 算步数会向上取整多造一个候选: 「1~20 步长 7」展开成 [1,8,15,22], 22 再被参数自身的 range 校验拒绝, 用户填的正是参数合法上限却直接报错;「0.01~0.05 步长 0.015」则静默 多扫一个 0.055。步数改为向下取整 (保留 1e-9 容差, 整除区间端点不丢)。 --- backend/app/backtest/optimizer.py | 4 +- .../test_optimizer_grid_step_bounds.py | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 backend/tests/backtest/test_optimizer_grid_step_bounds.py diff --git a/backend/app/backtest/optimizer.py b/backend/app/backtest/optimizer.py index 51323d5..d804b64 100644 --- a/backend/app/backtest/optimizer.py +++ b/backend/app/backtest/optimizer.py @@ -60,7 +60,9 @@ def _candidates_for(param_id: str, spec, pmeta: dict) -> list: raise ValueError(f"参数 '{param_id}' 的 max < min") step = float(step) # 整数计数生成候选, 避免浮点累加误差丢端点 (如 0.1/0.1 步长)。 - n_steps = round((hi - lo) / step) + # 步数向下取整: (hi-lo) 不是 step 整数倍时, 四舍五入会多造一个越过 hi 的候选 + # (1~20 步长 7 → 22), 用户填的上限反而被越界校验拒绝。1e-9 容差保住整除端点。 + n_steps = int((hi - lo) / step + 1e-9) raw = [round(lo + i * step, 10) for i in range(n_steps + 1)] else: raise ValueError(f"参数 '{param_id}' 的网格 spec 必须是列表或 {{min,max,step}} 字典") diff --git a/backend/tests/backtest/test_optimizer_grid_step_bounds.py b/backend/tests/backtest/test_optimizer_grid_step_bounds.py new file mode 100644 index 0000000..d0f862a --- /dev/null +++ b/backend/tests/backtest/test_optimizer_grid_step_bounds.py @@ -0,0 +1,53 @@ +"""参数网格 {min,max,step} 展开不得越过用户填写的上限。 + +(hi-lo) 不是 step 整数倍时, 旧实现用 round() 算步数会向上取整, 多造一个 +超过 max 的候选值: 1~20 步长 7 展开成 [1,8,15,22], 22 又被参数自身的 +range 校验拒绝 —— 用户填的上限就是参数合法上限, 却直接报错扫不了。 +""" +from __future__ import annotations + +import pytest + +from app.backtest.optimizer import count_combinations, expand_param_grid + +PARAMS_META = [ + {"id": "min_boards", "type": "int", "default": 2, "min": 1, "max": 20, "step": 1}, + {"id": "ma_proximity", "type": "float", "default": 0.02, "min": 0.01, "max": 0.50, "step": 0.005}, +] + + +def test_int_range_not_divisible_by_step_stays_within_max(): + """1~20 步长 7: 不得造出 22, 也不得因此报「超出范围」。""" + combos = expand_param_grid(PARAMS_META, {"min_boards": {"min": 1, "max": 20, "step": 7}}) + values = sorted(combo["min_boards"] for combo in combos) + assert values == [1, 8, 15] + assert count_combinations(PARAMS_META, {"min_boards": {"min": 1, "max": 20, "step": 7}}) == 3 + + +def test_float_range_not_divisible_by_step_stays_within_max(): + """0.01~0.05 步长 0.015: 末候选 0.055 越过用户填的 0.05。""" + combos = expand_param_grid( + PARAMS_META, {"ma_proximity": {"min": 0.01, "max": 0.05, "step": 0.015}} + ) + values = sorted(combo["ma_proximity"] for combo in combos) + assert values == pytest.approx([0.01, 0.025, 0.04]) + assert max(values) <= 0.05 + + +def test_divisible_range_still_keeps_both_endpoints(): + """整除区间的端点必须保留 (含浮点累加误差场景), 锁定既有行为。""" + int_values = sorted( + combo["min_boards"] + for combo in expand_param_grid(PARAMS_META, {"min_boards": {"min": 1, "max": 4, "step": 1}}) + ) + assert int_values == [1, 2, 3, 4] + + float_meta = [{"id": "p", "type": "float", "default": 0.2, "min": 0.1, "max": 0.3, "step": 0.1}] + float_values = sorted( + combo["p"] for combo in expand_param_grid(float_meta, {"p": {"min": 0.1, "max": 0.3, "step": 0.1}}) + ) + assert float_values == [0.1, 0.2, 0.3] + + assert count_combinations( + PARAMS_META, {"ma_proximity": {"min": 0.01, "max": 0.05, "step": 0.001}} + ) == 41