fix(backtest): CTA metrics benchmark 缺失降级(不阻塞整组指标图)
benchmark 数据缺失时原实现 if 跳过整段 compute_metrics → {task_id}_metrics.json 不写 → 策略净值/回撤/波动图也空(不只基准图)。改为:benchmark 空时传空 Series + log WARN,compute_metrics 内部 reindex→fillna(0) 容空(基准类指标 NaN→None,策略指标正常),metrics.json 照写。效果:策略图照常出,只基准/Alpha/Beta 图空(前端 benchmark-curve/risk-series 已容错)。加测试 test_run_cta_backtest_benchmark_missing_degrades(mock read_index_daily 返空 → 验 compute_metrics 仍 called + metrics.json 仍写 + 策略指标有值)。6 测试全绿(Mac pytest)。
This commit is contained in:
@@ -360,4 +360,87 @@ class TestRunCtaBacktest:
|
||||
# Verify read_index_daily was called with hs300 code (sh000300)
|
||||
mock_read.assert_called_once()
|
||||
call_args = mock_read.call_args
|
||||
assert call_args[0][0] == "sh000300", "Default benchmark should be hs300 (sh000300)"
|
||||
assert call_args[0][0] == "sh000300", "Default benchmark should be hs300 (sh000300)"
|
||||
|
||||
def test_run_cta_backtest_benchmark_missing_degrades(self, temp_db_path):
|
||||
"""benchmark 缺失时 compute_metrics 仍执行、metrics.json 照写(降级)。
|
||||
|
||||
根因:read_index_daily 返空时原实现 if 跳过整段 compute_metrics →
|
||||
{task_id}_metrics.json 不写 → 策略净值/回撤/波动图也空(不只基准图)。
|
||||
降级:benchmark 空时传空 Series 给 compute_metrics(内部 reindex→fillna(0),
|
||||
基准类指标返 NaN→None,策略指标正常),metrics.json 照写。
|
||||
"""
|
||||
mock_strategy_class = Mock()
|
||||
mock_strategy_class.__name__ = "DegBmStrategy"
|
||||
|
||||
dates = pd.date_range("2024-01-01", "2024-03-31", freq="D")
|
||||
daily_df = pd.DataFrame(
|
||||
{"balance": [1_000_000.0 * (1.001 ** i) for i in range(len(dates))]},
|
||||
index=dates,
|
||||
)
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.calculate_result.return_value = daily_df
|
||||
mock_engine.calculate_statistics.return_value = {"total_return": 0.15}
|
||||
mock_engine.trades = {"t1": MagicMock()} # 非空 trades 避免 degenerate
|
||||
mock_engine.history_data = [] # 空 history → 定寸跳过
|
||||
|
||||
mock_cfg = Mock()
|
||||
mock_cfg.data_paths = {"daily_dir": "/mock/daily_dir"}
|
||||
|
||||
mock_ashare = MagicMock()
|
||||
mock_ashare.AShareBacktestingEngine = Mock(return_value=mock_engine)
|
||||
|
||||
mock_metrics_result = Mock()
|
||||
mock_metrics_result.scalars = {
|
||||
"total_return": 0.15, # 策略指标(不依赖基准)有值
|
||||
"max_drawdown": -0.05,
|
||||
"alpha": None, # 基准类指标 NaN→None(基准缺失)
|
||||
"benchmark_return": None,
|
||||
}
|
||||
mock_metrics_result.series = {
|
||||
"equity_curve": pd.Series([1.0, 1.05, 1.1]),
|
||||
"drawdown": pd.Series([0.0, -0.01, -0.02]),
|
||||
}
|
||||
|
||||
mock_tzlocal = MagicMock()
|
||||
mock_tzlocal.get_localzone_name = Mock(return_value="UTC")
|
||||
|
||||
with patch.dict("sys.modules", {
|
||||
"sanguo_backtest.ashare_engine": mock_ashare,
|
||||
"tzlocal": mock_tzlocal,
|
||||
"vnpy.trader.setting": MagicMock(),
|
||||
}):
|
||||
# read_index_daily 返空 DataFrame = benchmark 缺失
|
||||
with patch("sanguo_backtest.cta_engine.read_index_daily", return_value=pd.DataFrame()):
|
||||
with patch("sanguo_backtest.cta_engine.compute_metrics", return_value=mock_metrics_result) as mock_cm:
|
||||
result = run_cta_backtest(
|
||||
strategy_class=mock_strategy_class,
|
||||
symbol="600000",
|
||||
params={},
|
||||
start="2024-01-01",
|
||||
end="2024-03-31",
|
||||
cfg=mock_cfg,
|
||||
db_path=temp_db_path,
|
||||
)
|
||||
|
||||
# 1. compute_metrics 仍被调用(降级,没因 benchmark 空跳过)
|
||||
mock_cm.assert_called_once()
|
||||
# 2. 传入的 benchmark_returns 是空 Series(降级标记)
|
||||
benchmark_arg = mock_cm.call_args[0][1]
|
||||
assert isinstance(benchmark_arg, pd.Series)
|
||||
assert benchmark_arg.empty, "benchmark 缺失时应传空 Series 给 compute_metrics"
|
||||
|
||||
# 3. metrics.json 仍写(策略净值/回撤/波动图数据源)
|
||||
file_dir = Path(temp_db_path).parent
|
||||
metrics_file = file_dir / f"{result.task_id}_metrics.json"
|
||||
assert metrics_file.exists(), f"降级时 metrics.json 应照写: {metrics_file}"
|
||||
with open(metrics_file) as f:
|
||||
metrics_data = json.load(f)
|
||||
series_keys = list(metrics_data.get("series", {}).keys())
|
||||
assert "equity_curve" in series_keys
|
||||
assert "drawdown" in series_keys
|
||||
|
||||
# 4. 策略指标有值(基准类 None)
|
||||
assert result.statistics.get("total_return") == 0.15
|
||||
assert result.statistics.get("alpha") is None
|
||||
Reference in New Issue
Block a user