94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
"""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
|