mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
fix: handle uncovered walk-forward folds
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)验证。样本内漂亮、样本外崩溃即过拟合。"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user