fix: 寻优跳过参数范围/对比支持组合/日线x轴年份/寻优跳转填充参数

4 个独立修复:

1. 寻优跳过参数范围检查:Param.validate 加 skip_bounds 参数,
   RegisteredStrategy.build 透传,ParamGridOptimizer 传 skip_bounds=True。
   修复寻优时 fast=250 被 max_value=60 拦截的问题(探索超范围值是寻优目的)。

2. 对比页支持组合回测:CompareView 新增 extractComparable() 统一提取净值+指标,
   支持单标的(performance/equity_curve)和组合(total_performance/combined_equity)。
   修复勾选组合回测任务报「非单标的回测」错误。

3. K线x轴日线显示完整年份:isIntraday 判断从 length>10 改为检查时分秒非零
   (日线归一化后带 T00:00:00 后缀,长度也 >10 导致误判为分钟线,slice 砍年份)。

4. 寻优跳转填充参数:BacktestView 加 useRoute(),onMounted 读 query.strategy +
   query.params,await nextTick 后覆盖(避免 StrategyPicker watch 重置)。
   修复寻优点「查看」跳转后参数未填充、仍用默认值的问题。

测试:823 passed, mypy 211 files OK, vue-tsc OK
This commit is contained in:
Justin Gu
2026-07-03 11:32:44 +08:00
parent 80d62aec56
commit 8323223937
5 changed files with 110 additions and 34 deletions
+3 -2
View File
@@ -143,7 +143,7 @@ class ParamGridOptimizer:
strategy_name: str,
param_grid: dict[str, list[Any]],
df: pd.DataFrame,
cash: float = 100_000.0,
cash: float = 1_000_000.0,
commission: float = 0.0003,
min_commission: float = 5.0,
stamp_tax: float = 0.001,
@@ -181,7 +181,8 @@ class ParamGridOptimizer:
for combo in itertools.product(*value_lists):
params = dict(zip(param_names, combo, strict=True))
try:
strategy = entry.build(params)
# 寻优时跳过参数范围检查——探索超范围值是寻优的目的
strategy = entry.build(params, skip_bounds=True)
engine = BacktestEngine(
strategy=strategy,
cash=self._cash,
+34 -14
View File
@@ -114,7 +114,7 @@ class Param:
schema["description"] = self.description
return schema
def validate(self, value: Any) -> Any:
def validate(self, value: Any, *, skip_bounds: bool = False) -> Any:
"""校验并强制转换取值,不合法抛 ValueError。
NaN/Inf 的 float 输入会被拒绝(NaN 绕过所有比较,Inf 仅在有界时被拦,
@@ -150,14 +150,25 @@ class Param:
f"参数 '{self.name}' 期望 {self.type.__name__},得到 {value!r}"
) from exc
if self.choices is not None and self.type is str and converted not in self.choices:
raise ValueError(
f"参数 '{self.name}' 取值 {converted!r} 不在可选范围 {list(self.choices)}"
)
if self.type in (int, float) and self.min_value is not None and converted < self.min_value:
raise ValueError(f"参数 '{self.name}'={converted} 小于下限 {self.min_value}")
if self.type in (int, float) and self.max_value is not None and converted > self.max_value:
raise ValueError(f"参数 '{self.name}'={converted} 大于上限 {self.max_value}")
# skip_bounds=True 时跳过范围/取值集合检查(供寻优器探索超范围值),
# 仍保留类型转换 + NaN/Inf 拦截。
if not skip_bounds:
if self.choices is not None and self.type is str and converted not in self.choices:
raise ValueError(
f"参数 '{self.name}' 取值 {converted!r} 不在可选范围 {list(self.choices)}"
)
if (
self.type in (int, float)
and self.min_value is not None
and converted < self.min_value
):
raise ValueError(f"参数 '{self.name}'={converted} 小于下限 {self.min_value}")
if (
self.type in (int, float)
and self.max_value is not None
and converted > self.max_value
):
raise ValueError(f"参数 '{self.name}'={converted} 大于上限 {self.max_value}")
return converted
@@ -180,10 +191,11 @@ class ParametrizedStrategy(Strategy):
# 实例属性:已校验的参数值字典(init/next 中通过 self.p[name] 访问)。
p: dict[str, Any]
def __init__(self, **kwargs: Any) -> None:
def __init__(self, *, skip_bounds: bool = False, **kwargs: Any) -> None:
"""从 kwargs 构造策略参数。
多余的未知参数抛 ValueError;缺失参数取默认值。
skip_bounds=True 时跳过参数范围检查(供寻优器探索超范围值)。
"""
super().__init__()
declared = {param.name: param for param in self.params}
@@ -194,7 +206,7 @@ class ParametrizedStrategy(Strategy):
resolved: dict[str, Any] = {}
for name, param in declared.items():
raw = kwargs.get(name, param.default)
resolved[name] = param.validate(raw)
resolved[name] = param.validate(raw, skip_bounds=skip_bounds)
self.p = resolved
@@ -220,9 +232,17 @@ class RegisteredStrategy:
"params": [p.to_schema() for p in self.params],
}
def build(self, params: dict[str, Any] | None = None) -> ParametrizedStrategy:
"""用给定参数构造策略实例,缺失参数取默认值。"""
return self.strategy_cls(**(params or {}))
def build(
self,
params: dict[str, Any] | None = None,
*,
skip_bounds: bool = False,
) -> ParametrizedStrategy:
"""用给定参数构造策略实例,缺失参数取默认值。
skip_bounds=True 时跳过参数范围检查(供寻优器探索超范围值)。
"""
return self.strategy_cls(**(params or {}), skip_bounds=skip_bounds)
class StrategyRegistry: