Files
tick-stock-panel/backend/app/backtest/guards.py
T
shy3130 da9dbca422 feat(backtest): 稳健参数筛选与滚动样本外检验重构 (P1-P4)
参数排名从单窗样本内得分改为 K 段折外稳健分 R = 0.6×中位 + 0.4×最差
(设计: docs/robust-optimizer-plan.md), 四阶段全部落地:

- guards 共享护栏: 主目标白名单 + 最小交易数门槛 (fail-closed, 缺交易数
  按不达标处理), 两模式一处实现
- robust_optimizer: 等分切窗 (2-6 段, 每段≥14天) / 稳健分 / 冠军收敛诊断
  (unanimous/majority/scattered) / Top-20 全窗 IS 细化与退化率 /
  高原判据 (邻域得分衰减, 零额外回测); worker 接线 robust_optimize 任务
- 滚动样本外检验补强: 每折 OOS 最小交易数门槛 (无效折透明跳过),
  前端折预览 / 参数漂移轨迹 / 采用最新折参数
- 前端: 冠军卡 (收敛/高原徽章/乐观偏差) + K 段排名表 + 验证窗预览 +
  应用参数闭环 (POST /strategies/config, 可逆); SSE 重连与跨设备接回
- P4 摒弃旧路径: 删 /optimize/stream 与 /optimize/cancel 端点及 worker
  optimize 任务类型; 白名单全面生效 (optimizer.VALID_OBJECTIVES 收敛到
  guards; walkforward 端点校验退化目标 400)
- 附带修复: 叠加条件×矩阵路径 SignalMatrix 只读违规 (np.where 产物
  可写), optimizer_trial 轻量统计恒保留 n_trades

实测: 126 组合全市场网格 1.71× 旧版耗时 (视图复用优化自 2.01×; 差额
为引擎层每 run 固定开销, 详见方案文档第九节); 全量回归 3131 通过。

api.ts 同时带入稳健优化折预览客户端与声效语音包 API 客户端
(语音包功能随后续提交落地界面)。

Signed-off-by: shy3130 <shy3130@qq.com>
2026-10-25 20:14:10 +08:00

43 lines
1.7 KiB
Python

"""优化/验证任务共享护栏: 主目标白名单与最小交易数门槛。
一处定义, 稳健优化 (robust_optimizer) 与滚动样本外检验 (walkforward) 共用;
optimizer.py 的 VALID_OBJECTIVES 已收敛到此 (P4)。设计见 docs/robust-optimizer-plan.md 第四节。
"""
from __future__ import annotations
# 可作主目标的指标: 方向语义明确、跨参数组可比、不随交易数退化。
# max_drawdown / mc_maxdd_* 为负值, 最大化带符号值 = 回撤越小越好。
PRIMARY_OBJECTIVES: frozenset[str] = frozenset({
"sortino", "sharpe", "calmar",
"annual_return", "total_return", "profit_factor",
"max_drawdown", "mc_maxdd_p50", "mc_maxdd_p95",
"avg_pnl", "median_pnl",
})
# 退化目标: 交易数少时可被运气推到极端 (3 笔全胜 = win_rate 100%), 只作展示列
# 或约束条件, 不得作主排名目标。
DEGENERATE_OBJECTIVES: frozenset[str] = frozenset({
"win_rate", "avg_holding_days", "n_trades",
})
def validate_primary_objective(objective: str) -> None:
"""主目标必须在白名单内, 否则抛 ValueError (端点转 400)。"""
if objective not in PRIMARY_OBJECTIVES:
raise ValueError(
f"'{objective}' 不适合作稳健优化的主目标 (交易数少时会退化); "
f"可选: {sorted(PRIMARY_OBJECTIVES)}"
)
def trades_ok(stats: dict, min_trades: int) -> bool:
"""最小交易数门槛: stats 的 n_trades 达标才有效; min_trades<=0 表示关闭。
缺失 n_trades 按不达标处理 (fail-closed: 宁可误杀也不让无交易组合登顶)。
"""
if min_trades is None or min_trades <= 0:
return True
n = stats.get("n_trades")
return isinstance(n, (int, float)) and n >= min_trades