test: add red contracts for quant core boundaries
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Mathematical contracts for the standard performance metrics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from quant_engine.metrics import (
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
annualized_return,
|
||||
annualized_volatility,
|
||||
calmar_ratio,
|
||||
max_drawdown,
|
||||
sharpe_ratio,
|
||||
summary,
|
||||
win_rate,
|
||||
)
|
||||
|
||||
|
||||
def test_annualized_return_uses_compounded_simple_returns() -> None:
|
||||
returns = pd.Series([0.10, -0.10])
|
||||
expected = 0.99 ** (TRADING_DAYS_PER_YEAR / 2) - 1.0
|
||||
|
||||
assert annualized_return(returns) == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_annualized_volatility_uses_sample_standard_deviation() -> None:
|
||||
returns = pd.Series([0.01, 0.03, 0.02])
|
||||
|
||||
assert annualized_volatility(returns) == pytest.approx(
|
||||
returns.std() * np.sqrt(TRADING_DAYS_PER_YEAR)
|
||||
)
|
||||
|
||||
|
||||
def test_sharpe_ratio_subtracts_annual_risk_free_rate() -> None:
|
||||
returns = pd.Series([0.01, -0.005, 0.02, 0.0])
|
||||
|
||||
result = sharpe_ratio(returns, rf=0.02)
|
||||
|
||||
assert result == pytest.approx(
|
||||
(annualized_return(returns) - 0.02) / annualized_volatility(returns)
|
||||
)
|
||||
|
||||
|
||||
def test_zero_volatility_metrics_return_zero() -> None:
|
||||
returns = pd.Series([0.0, 0.0, 0.0])
|
||||
|
||||
assert sharpe_ratio(returns) == 0.0
|
||||
assert calmar_ratio(returns) == 0.0
|
||||
|
||||
|
||||
def test_max_drawdown_includes_loss_from_initial_capital() -> None:
|
||||
returns = pd.Series([-0.20, 0.0])
|
||||
|
||||
assert max_drawdown(returns) == pytest.approx(-0.20)
|
||||
|
||||
|
||||
def test_max_drawdown_tracks_peak_to_trough_loss() -> None:
|
||||
returns = pd.Series([0.10, -0.20, 0.05])
|
||||
|
||||
assert max_drawdown(returns) == pytest.approx(-0.20)
|
||||
|
||||
|
||||
def test_metrics_clean_nan_and_infinite_values() -> None:
|
||||
returns = pd.Series([0.10, np.nan, np.inf, -0.05, -np.inf])
|
||||
|
||||
assert win_rate(returns) == 0.5
|
||||
assert summary(returns)["n_days"] == 2
|
||||
|
||||
|
||||
def test_summary_aliases_match_canonical_fields() -> None:
|
||||
result = summary(pd.Series([0.01, -0.02, 0.03]))
|
||||
|
||||
assert result["annual_yield"] == result["ann_return"]
|
||||
assert result["annual_sd"] == result["ann_volatility"]
|
||||
assert result["drawback"] == result["max_drawdown"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metric",
|
||||
[annualized_return, annualized_volatility, sharpe_ratio, max_drawdown, calmar_ratio, win_rate],
|
||||
)
|
||||
def test_metrics_reject_non_series_input(metric) -> None:
|
||||
with pytest.raises(TypeError, match="expected pd.Series"):
|
||||
metric([0.01, 0.02])
|
||||
|
||||
|
||||
def test_short_and_empty_series_return_zero() -> None:
|
||||
assert annualized_return(pd.Series(dtype=float)) == 0.0
|
||||
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
|
||||
Reference in New Issue
Block a user