fix: handle uncovered walk-forward folds

This commit is contained in:
shy3130
2026-07-17 14:00:16 +08:00
parent 1e38c8cebb
commit 34eaba3010
4 changed files with 68 additions and 12 deletions
+30 -8
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import logging
import time
from bisect import bisect_left
from dataclasses import dataclass, field
from datetime import date, timedelta
@@ -196,6 +197,15 @@ class WalkForwardService:
shared_market_data = (
shared_prepared.market_data if shared_prepared is not None else None
)
shared_date_labels = (
tuple(label[:10] for label in shared_market_data.timestamp_labels)
if shared_market_data is not None
else ()
)
def _shared_window_has_data(window_start: date, window_end: date) -> bool:
index = bisect_left(shared_date_labels, window_start.isoformat())
return index < len(shared_date_labels) and shared_date_labels[index] <= window_end.isoformat()
# 遥测: 首尾快照 PanelCache, 量化跨折重叠区间重复扫盘的 IO 占比 (是否值得进一步优化)。
cache_before = self.service.engine.cache_stats()
@@ -213,6 +223,26 @@ class WalkForwardService:
if cancel_event is not None and cancel_event.is_set():
break
base = {
"index": f.index,
"train_start": str(f.train_start),
"train_end": str(f.train_end),
"test_start": str(f.test_start),
"test_end": str(f.test_end),
}
missing_window = None
if shared_market_data is not None:
if not _shared_window_has_data(f.train_start, f.train_end):
missing_window = "训练区间无可用行情数据"
elif not _shared_window_has_data(f.test_start, f.test_end):
missing_window = "测试区间无可用行情数据"
if missing_window is not None:
skipped.append({**base, "reason": missing_window})
done += 1
if progress_cb is not None:
progress_cb({"type": "walkforward_progress", "done": done, "total": n_total, "fold": f.index})
continue
# 训练区间: 网格优化选最优参数
opt_cfg = OptimizeConfig(
strategy_id=cfg.strategy_id,
@@ -239,14 +269,6 @@ class WalkForwardService:
is_score = opt_res.get("best_score")
done += 1
base = {
"index": f.index,
"train_start": str(f.train_start),
"train_end": str(f.train_end),
"test_start": str(f.test_start),
"test_end": str(f.test_end),
}
# 训练区间没优化出参数 (全组失败/取消) -> 跳过, 不用默认参数硬跑 OOS 伪装成有效折
if best_params is None:
skipped.append({**base, "reason": "训练区间未优化出参数"})
@@ -186,3 +186,37 @@ def test_spawn_walkforward_reuses_shared_matrix_across_folds(tmp_path):
assert result["shared_market_data_bytes"] > 0
assert all(fold["oos_stats"]["shared_market_data"] for fold in result["folds"])
assert result["worker"]["worker_exitcode"] == 0
def test_spawn_walkforward_skips_folds_before_available_matrix_data(tmp_path):
configured_start = date(2024, 1, 1)
market_start = configured_start + timedelta(days=4)
data_dir = tmp_path / "data"
_write_worker_strategy(data_dir)
_write_market_data(data_dir, market_start, days=8)
config = WalkForwardConfig(
strategy_id="worker_always_entry",
symbols=["600000.SH"],
start=configured_start,
end=configured_start + timedelta(days=11),
param_grid={"gate": [1, 2]},
objective="total_return",
train_days=2,
test_days=1,
step_days=2,
overrides={"basic_filter": {"enabled": False}},
backtest_kwargs={
"matching": "close_t",
"fees_pct": 0,
"slippage_bps": 0,
"max_positions": 1,
},
)
result = run_worker_task(make_worker_task("walkforward", data_dir, config))
assert result["n_planned_folds"] == 4
assert result["n_skipped"] == 1
assert result["skipped"][0]["reason"] == "训练区间无可用行情数据"
assert result["n_folds"] == 3
assert result["worker"]["worker_exitcode"] == 0
+2 -2
View File
@@ -25,7 +25,7 @@ const MODES: Record<Tab, { title: string; subtitle: string; hint: string }> = {
hint: '在独立 worker 中复用基础数据并串行回测参数组合,按夏普/索提诺等目标排序。',
},
walkforward: {
title: 'Walk-forward',
title: '步进优化',
subtitle: '滚动窗口样本外验证',
hint: '每折训练区间优化、测试区间验证,看样本外是否退化以识别过拟合。',
},
@@ -58,7 +58,7 @@ export function Backtest() {
>
<Icon className="h-3.5 w-3.5" />
{MODES[tab].title}
{tab === 'optimizer' && (
{(tab === 'optimizer' || tab === 'walkforward') && (
<span className={`rounded border px-1 py-px text-[8px] font-semibold uppercase ${
active ? 'border-white/40 bg-white/15 text-white' : 'border-amber-400/30 bg-amber-400/10 text-amber-400'
}`}>
@@ -186,7 +186,7 @@ export function StrategyWalkForward() {
</button>
) : (
<button onClick={onRun} disabled={!canRun} className="inline-flex w-full items-center justify-center gap-1.5 rounded-btn bg-accent px-3 py-2 text-xs font-medium text-white hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed">
<Play className="h-3.5 w-3.5" /> Walk-forward
<Play className="h-3.5 w-3.5" />
</button>
)}
</div>
@@ -210,7 +210,7 @@ export function StrategyWalkForward() {
{!result && !task?.isPending && (
<EmptyState
title="Walk-forward 优化"
title="步进优化"
hint="每折在训练区间网格优化选最优参数,再在紧邻的测试区间做样本外(OOS)验证。样本内漂亮、样本外崩溃即过拟合。"
/>
)}