fix(mining): 市场环境数据缺失时挖掘启动即失败并前置提示

用户在未计算市场环境的数据目录跑挖掘: 搜索阶段的 regime 评估被
evaluator 软捕获成 error 字符串, 整轮嵌套搜索照常跑完后, artifacts
阶段的 _regime_date_count 硬抛 ValueError, 计算全部浪费且只在最后
给出指引。

- mining_runtime: require_regime 时在因子面板就绪后立即用与
  artifacts 相同的 fold 口径预构建一次 regime mask (required 区间取
  全部外层 fold 测试窗并集), 数据为空/覆盖不完整即刻带指引消息终止
- MiningWorkbench: 启动前查询 /api/regime/latest, 未计算时侧栏显示
  警告横幅(直达数据页)并在开始时 toast 阻止, 避免产生必败的运行记录
- 测试: 缺数据 fail-fast / 覆盖充足通过 / fold 窗口内缺口报错 三例
This commit is contained in:
shy3130
2026-08-26 12:55:38 +08:00
parent 196af2fa06
commit de51929a44
3 changed files with 129 additions and 2 deletions
+79 -1
View File
@@ -6,7 +6,11 @@ from types import SimpleNamespace
import polars as pl
import pytest
from app.backtest.mining import MiningCandidate
from app.backtest.mining import (
MiningCandidate,
NestedValidationConfig,
generate_nested_folds,
)
from app.backtest.mining_runtime import (
TrainingMetricProvider,
_decode_runtime_request,
@@ -14,6 +18,7 @@ from app.backtest.mining_runtime import (
_prepare_base_market,
_rank_artifact_candidates,
_regime_date_count,
_validate_regime_availability,
attach_single_forward_return,
)
from app.services import regime_builder
@@ -312,3 +317,76 @@ def test_regime_date_count_uses_t_minus_one_market_labels(tmp_path) -> None:
assert _regime_date_count(panel, fold, "strong", tmp_path) == 2
assert _regime_date_count(panel, fold, "weak", tmp_path) == 1
def _small_validation() -> NestedValidationConfig:
return NestedValidationConfig(
outer_train_bars=10,
outer_test_bars=3,
outer_step_bars=5,
inner_train_bars=5,
inner_test_bars=2,
inner_step_bars=3,
purge_bars=1,
embargo_bars=1,
min_train_bars=3,
)
def _regime_panel(n: int = 20) -> tuple[list[date], pl.DataFrame]:
labels = [date(2024, 1, 2) + timedelta(days=offset) for offset in range(n)]
return labels, pl.DataFrame({"date": labels})
def _upsert_regime(tmp_path, labels, states: list[str]) -> None:
regime_builder.upsert_regime_history(tmp_path, pl.DataFrame({
"date": labels,
"state": states,
"score": [50] * len(labels),
}))
def test_validate_regime_availability_fails_fast_when_regime_data_missing(
tmp_path,
) -> None:
labels, panel = _regime_panel()
request = SimpleNamespace(
mining_request=SimpleNamespace(validation=_small_validation()),
)
with pytest.raises(ValueError, match="市场环境数据为空"):
_validate_regime_availability(panel, request, tmp_path)
def test_validate_regime_availability_passes_when_regime_covers_fold_windows(
tmp_path,
) -> None:
labels, panel = _regime_panel()
_upsert_regime(
tmp_path,
labels[:-1],
["range"] * (len(labels) - 1),
)
request = SimpleNamespace(
mining_request=SimpleNamespace(validation=_small_validation()),
)
_validate_regime_availability(panel, request, tmp_path)
def test_validate_regime_availability_reports_coverage_gaps_in_fold_windows(
tmp_path,
) -> None:
labels, panel = _regime_panel()
nested = generate_nested_folds(
[value.isoformat() for value in labels], _small_validation()
)
gap_date = date.fromisoformat(nested[0].outer.test_start) + timedelta(days=1)
covered = [value for value in labels if value != gap_date]
_upsert_regime(tmp_path, covered, ["range"] * len(covered))
request = SimpleNamespace(
mining_request=SimpleNamespace(validation=_small_validation()),
)
with pytest.raises(ValueError, match="市场环境数据覆盖不完整"):
_validate_regime_availability(panel, request, tmp_path)