test: define benchmark-relative performance contract
This commit is contained in:
@@ -10,6 +10,7 @@ from quant_engine.metrics import (
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
annualized_return,
|
||||
annualized_volatility,
|
||||
benchmark_summary,
|
||||
calmar_ratio,
|
||||
max_drawdown,
|
||||
sharpe_ratio,
|
||||
@@ -91,3 +92,50 @@ def test_short_and_empty_series_return_zero() -> None:
|
||||
assert annualized_volatility(pd.Series([0.01])) == 0.0
|
||||
assert max_drawdown(pd.Series([0.01])) == 0.0
|
||||
assert win_rate(pd.Series(dtype=float)) == 0.0
|
||||
|
||||
|
||||
def test_benchmark_summary_uses_aligned_active_returns_and_regression() -> None:
|
||||
dates = pd.date_range("2026-01-05", periods=4, freq="B")
|
||||
benchmark = pd.Series([-0.01, 0.0, 0.01, 0.02], index=dates)
|
||||
portfolio = 0.001 + 1.5 * benchmark
|
||||
active = portfolio - benchmark
|
||||
|
||||
result = benchmark_summary(portfolio, benchmark)
|
||||
|
||||
assert result["n_observations"] == 4
|
||||
assert result["tracking_error"] == pytest.approx(
|
||||
active.std() * np.sqrt(TRADING_DAYS_PER_YEAR)
|
||||
)
|
||||
assert result["information_ratio"] == pytest.approx(
|
||||
active.mean() / active.std() * np.sqrt(TRADING_DAYS_PER_YEAR)
|
||||
)
|
||||
assert result["beta"] == pytest.approx(1.5)
|
||||
assert result["alpha"] == pytest.approx(1.001**TRADING_DAYS_PER_YEAR - 1.0)
|
||||
|
||||
|
||||
def test_benchmark_summary_rejects_silent_calendar_alignment() -> None:
|
||||
portfolio = pd.Series([0.01, 0.02], index=pd.date_range("2026-01-05", periods=2))
|
||||
benchmark = pd.Series([0.01, 0.02], index=pd.date_range("2026-01-06", periods=2))
|
||||
|
||||
with pytest.raises(ValueError, match="matching indexes"):
|
||||
benchmark_summary(portfolio, benchmark)
|
||||
|
||||
|
||||
def test_benchmark_summary_rejects_missing_observations() -> None:
|
||||
dates = pd.date_range("2026-01-05", periods=2)
|
||||
portfolio = pd.Series([0.01, np.nan], index=dates)
|
||||
benchmark = pd.Series([0.0, 0.01], index=dates)
|
||||
|
||||
with pytest.raises(ValueError, match="finite"):
|
||||
benchmark_summary(portfolio, benchmark)
|
||||
|
||||
|
||||
def test_benchmark_summary_marks_constant_benchmark_regression_unestimable() -> None:
|
||||
dates = pd.date_range("2026-01-05", periods=3)
|
||||
portfolio = pd.Series([0.01, -0.01, 0.02], index=dates)
|
||||
benchmark = pd.Series([0.0, 0.0, 0.0], index=dates)
|
||||
|
||||
result = benchmark_summary(portfolio, benchmark)
|
||||
|
||||
assert np.isnan(result["alpha"])
|
||||
assert np.isnan(result["beta"])
|
||||
|
||||
@@ -265,3 +265,29 @@ def test_factor_backtest_starts_at_first_signal_instead_of_price_warmup() -> Non
|
||||
pd.Series([1.0, 1.1, 1.2], index=dates[2:], name="nav"),
|
||||
)
|
||||
assert result.stats()["n_days"] == 3
|
||||
|
||||
|
||||
def test_factor_backtest_exposes_net_benchmark_metrics() -> None:
|
||||
dates = _calendar()
|
||||
scores = pd.DataFrame({"A": [1.0]}, index=dates[:1])
|
||||
prices = pd.DataFrame({"A": [10.0, 10.0, 11.0, 11.0]}, index=dates)
|
||||
result = run_factor_backtest_research(
|
||||
scores,
|
||||
prices,
|
||||
prices,
|
||||
top_k=1,
|
||||
execution_price_field="open",
|
||||
valuation_price_field="close",
|
||||
config=ExecutionConfig(
|
||||
commission_bps=0,
|
||||
stamp_tax_bps=0,
|
||||
slippage_bps=0,
|
||||
min_trade_amount=0,
|
||||
),
|
||||
)
|
||||
benchmark = pd.Series([0.0, 0.01, -0.01, 0.0], index=dates)
|
||||
|
||||
relative = result.benchmark_stats(benchmark)
|
||||
|
||||
assert relative["n_observations"] == len(result.returns)
|
||||
assert relative["tracking_error"] > 0
|
||||
|
||||
Reference in New Issue
Block a user