"""runner_backtest 基准对齐/回撤/扩展指标 纯函数单测(B1)。""" import pytest from sanguo_portfolio.runner_backtest import ( _align_benchmark, _compute_extended_metrics, _extract_drawdown, ) EQ = [ {"date": "2024-01-01", "equity": 100.0}, {"date": "2024-01-02", "equity": 110.0}, {"date": "2024-01-03", "equity": 99.0}, {"date": "2024-01-04", "equity": 120.0}, {"date": "2024-01-05", "equity": 90.0}, ] BD = [ {"date": "2024-01-01", "close": 200.0}, {"date": "2024-01-02", "close": 220.0}, {"date": "2024-01-03", "close": 210.0}, {"date": "2024-01-04", "close": 260.0}, {"date": "2024-01-05", "close": 208.0}, ] def test_drawdown_series(): dd = _extract_drawdown(EQ) vals = [p["drawdown"] for p in dd] assert vals[0] == 0.0 and vals[1] == 0.0 and vals[3] == 0.0 assert vals[2] == pytest.approx(-10.0) assert vals[4] == pytest.approx(-25.0) def test_align_benchmark_normalizes_and_matches_length(): bench = _align_benchmark(BD, EQ) assert len(bench) == len(EQ) assert bench[0]["benchmark"] == pytest.approx(1.0) assert bench[4]["benchmark"] == pytest.approx(1.04) def test_align_benchmark_ffill_missing_dates(): bench = _align_benchmark(BD[:2], EQ) # 后 3 天无基准数据 → 前向填充 1.1 assert [p["benchmark"] for p in bench] == [1.0, 1.1, 1.1, 1.1, 1.1] def test_align_benchmark_empty_inputs(): assert _align_benchmark([], EQ) == [] assert _align_benchmark(BD, []) == [] def test_extended_metrics_values(): bench = _align_benchmark(BD, EQ) m = _compute_extended_metrics(EQ, bench) assert m["benchmark_return"] == pytest.approx(4.0) assert m["excess_return"] == pytest.approx(-14.0) # -10% 策略 - +4% 基准 assert m["beta"] == pytest.approx(1.0874, abs=1e-3) assert "annual_volatility" in m assert "sortino" in m assert "calmar" in m def test_extended_metrics_short_series(): assert _compute_extended_metrics([{"date": "d", "equity": 1.0}], []) == {} assert _compute_extended_metrics([], []) == {} def test_holdings_curve_aggregates_daily(): """每日持仓聚合:count=非零标的数,value=市值合计;零持仓行剔除。""" import pandas as pd from sanguo_portfolio.runner_backtest import _extract_holdings_curve df = pd.DataFrame([ {"date": "2024-01-01", "code": "600000", "amount": 100, "value": 1000.0}, {"date": "2024-01-01", "code": "000001", "amount": 200, "value": 2000.0}, {"date": "2024-01-01", "code": "510300", "amount": 0, "value": 0.0}, # 已清仓剔除 {"date": "2024-01-02", "code": "600000", "amount": 100, "value": 1100.0}, ]) curve = _extract_holdings_curve(df) assert curve == [ {"date": "2024-01-01", "count": 2, "value": 3000.0}, {"date": "2024-01-02", "count": 1, "value": 1100.0}, ] def test_holdings_curve_empty_inputs(): from sanguo_portfolio.runner_backtest import _extract_holdings_curve assert _extract_holdings_curve(None) == [] import pandas as pd assert _extract_holdings_curve(pd.DataFrame()) == [] assert _extract_holdings_curve(pd.DataFrame({"code": ["600000"]})) == [] # 无 date 列