"""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=r"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