Coverage for src / monte_neo / metrics / sharpe.py: 70%
54 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
1"""Sharpe and Sortino ratio metrics.
3Risk-adjusted return metrics for evaluating trading performance.
4"""
6from __future__ import annotations
8import numpy as np
10from monte_neo.utils.logger import get_logger
12logger = get_logger(__name__)
15class SharpeRatioMetric:
16 """Sharpe Ratio calculator."""
18 def __init__(
19 self,
20 risk_free_rate: float = 0.0,
21 periods_per_year: int = 252,
22 ) -> None:
23 """Initialize Sharpe calculator.
25 Args:
26 risk_free_rate: Annual risk-free rate.
27 periods_per_year: Trading periods per year.
28 """
29 self.risk_free_rate = risk_free_rate
30 self.periods_per_year = periods_per_year
32 def calculate(self, returns: list[float] | np.ndarray) -> float:
33 """Calculate Sharpe ratio.
35 Sharpe = (Mean Return - Risk Free) / Std(Returns) * sqrt(periods)
37 Values > 1 are good, > 2 are excellent.
39 Args:
40 returns: List of period returns.
42 Returns:
43 Annualized Sharpe ratio.
44 """
45 if not len(returns):
46 return 0.0
48 returns = np.array(returns)
50 if len(returns) < 2 or np.std(returns) == 0:
51 return 0.0
53 # Convert annual risk-free to period risk-free
54 period_rf = (1 + self.risk_free_rate) ** (1 / self.periods_per_year) - 1
56 excess_returns = returns - period_rf
57 mean_excess = np.mean(excess_returns)
58 std_returns = np.std(returns, ddof=1)
60 # Annualize
61 sharpe = (mean_excess / std_returns) * np.sqrt(self.periods_per_year)
63 return float(sharpe)
65 def calculate_rolling(
66 self,
67 returns: list[float] | np.ndarray,
68 window: int = 20,
69 ) -> np.ndarray:
70 """Calculate rolling Sharpe ratio.
72 Args:
73 returns: List of period returns.
74 window: Rolling window size.
76 Returns:
77 Array of rolling Sharpe ratios.
78 """
79 if len(returns) < window:
80 return np.array([self.calculate(returns)])
82 returns = np.array(returns)
83 rolling_sharpe = []
85 for i in range(window, len(returns) + 1):
86 window_returns = returns[i - window : i]
87 rolling_sharpe.append(self.calculate(window_returns))
89 return np.array(rolling_sharpe)
92class SortinoRatioMetric:
93 """Sortino Ratio calculator (downside risk adjusted)."""
95 def __init__(
96 self,
97 risk_free_rate: float = 0.0,
98 periods_per_year: int = 252,
99 ) -> None:
100 """Initialize Sortino calculator.
102 Args:
103 risk_free_rate: Annual risk-free rate.
104 periods_per_year: Trading periods per year.
105 """
106 self.risk_free_rate = risk_free_rate
107 self.periods_per_year = periods_per_year
109 def calculate(self, returns: list[float] | np.ndarray) -> float:
110 """Calculate Sortino ratio.
112 Sortino = (Mean Return - Risk Free) / Downside Deviation * sqrt(periods)
114 Uses only negative returns for volatility calculation.
116 Args:
117 returns: List of period returns.
119 Returns:
120 Annualized Sortino ratio.
121 """
122 if not len(returns):
123 return 0.0
125 returns = np.array(returns)
127 # Convert annual risk-free to period risk-free
128 period_rf = (1 + self.risk_free_rate) ** (1 / self.periods_per_year) - 1
130 excess_returns = returns - period_rf
131 mean_excess = np.mean(excess_returns)
133 # Downside deviation (only negative returns)
134 downside_returns = returns[returns < 0]
136 if len(downside_returns) < 2:
137 return float("inf") if mean_excess > 0 else 0.0
139 downside_std = np.std(downside_returns, ddof=1)
141 if downside_std == 0:
142 return float("inf") if mean_excess > 0 else 0.0
144 # Annualize
145 sortino = (mean_excess / downside_std) * np.sqrt(self.periods_per_year)
147 return float(sortino)
149 def calculate_downside_deviation(
150 self,
151 returns: list[float] | np.ndarray,
152 target: float = 0,
153 ) -> float:
154 """Calculate downside deviation.
156 Args:
157 returns: List of period returns.
158 target: Target return (default: 0).
160 Returns:
161 Downside deviation.
162 """
163 returns = np.array(returns)
164 below_target = returns[returns < target]
166 if len(below_target) < 2:
167 return 0.0
169 return float(np.std(below_target, ddof=1))