diff --git a/src/multi_swarm/metrics/__init__.py b/src/multi_swarm/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/multi_swarm/metrics/basic.py b/src/multi_swarm/metrics/basic.py new file mode 100644 index 0000000..eea934c --- /dev/null +++ b/src/multi_swarm/metrics/basic.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import numpy as np +import pandas as pd # type: ignore[import-untyped] + + +def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float: + """Sharpe annualizzato. periods_per_year=8760 per dati orari.""" + excess = returns - rf / periods_per_year + std = excess.std(ddof=1) + if std == 0 or np.isnan(std): + return 0.0 + return float(np.sqrt(periods_per_year) * excess.mean() / std) + + +def max_drawdown(equity: pd.Series) -> float: + """Max drawdown percentuale (positivo).""" + peak = equity.cummax() + dd = (peak - equity) / peak.replace(0, np.nan) + dd = dd.fillna(0.0) + return float(dd.max()) + + +def total_return(equity: pd.Series) -> float: + if equity.iloc[0] == 0: + return float(equity.iloc[-1]) + return float(equity.iloc[-1] / equity.iloc[0] - 1.0) diff --git a/tests/unit/test_metrics_basic.py b/tests/unit/test_metrics_basic.py new file mode 100644 index 0000000..8ed2626 --- /dev/null +++ b/tests/unit/test_metrics_basic.py @@ -0,0 +1,40 @@ +import numpy as np +import pandas as pd +import pytest + +from multi_swarm.metrics.basic import max_drawdown, sharpe_ratio, total_return + + +def test_sharpe_zero_returns(): + r = pd.Series([0.0] * 100) + assert sharpe_ratio(r, periods_per_year=8760) == 0.0 + + +def test_sharpe_positive_returns(): + np.random.seed(42) + r = pd.Series(np.random.normal(0.001, 0.01, 1000)) + s = sharpe_ratio(r, periods_per_year=8760) + assert s > 0 + + +def test_sharpe_negative_returns(): + np.random.seed(42) + r = pd.Series(np.random.normal(-0.001, 0.01, 1000)) + s = sharpe_ratio(r, periods_per_year=8760) + assert s < 0 + + +def test_max_drawdown_monotonic_up(): + eq = pd.Series([100.0, 105.0, 110.0, 115.0, 120.0]) + assert max_drawdown(eq) == pytest.approx(0.0) + + +def test_max_drawdown_known_curve(): + eq = pd.Series([100.0, 110.0, 90.0, 95.0, 105.0]) + # peak 110, trough 90, drawdown = (110-90)/110 ≈ 0.1818 + assert max_drawdown(eq) == pytest.approx(20.0 / 110.0) + + +def test_total_return(): + eq = pd.Series([100.0, 110.0, 105.0, 120.0]) + assert total_return(eq) == pytest.approx(0.20)