feat(metrics): Sharpe + max drawdown + total return
Metriche base per valutazione strategie: Sharpe annualizzato (default 8760 periodi/anno per dati orari), max drawdown percentuale dalla curva equity, total return cumulativo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user