@@ -77,6 +77,233 @@ def sharpe(
7777 return res * sqrt (1 if periods is None else periods ) if annualize else res
7878
7979
80+ @overload
81+ def rolling_sharpe (
82+ returns : Series ,
83+ rf : float = 0.0 ,
84+ rolling_period : int | None = 126 ,
85+ annualize : bool = True ,
86+ periods_per_year : int = 252 ,
87+ prepare_returns : bool = True ,
88+ ) -> Series : ...
89+ @overload
90+ def rolling_sharpe (
91+ returns : DataFrame ,
92+ rf : float = 0.0 ,
93+ rolling_period : int | None = 126 ,
94+ annualize : bool = True ,
95+ periods_per_year : int = 252 ,
96+ prepare_returns : bool = True ,
97+ ) -> DataFrame : ...
98+ def rolling_sharpe (
99+ returns : Series | DataFrame ,
100+ rf : float = 0.0 ,
101+ rolling_period : int | None = 126 ,
102+ annualize : bool = True ,
103+ periods_per_year : int = 252 ,
104+ prepare_returns : bool = True ,
105+ ) -> Series | DataFrame :
106+ """Calculate rolling Sharpe ratio over a specified window.
107+
108+ The rolling Sharpe ratio computes the Sharpe ratio using a rolling window,
109+ providing a time-varying measure of risk-adjusted performance. This allows
110+ you to track how the strategy's risk-adjusted returns evolve over time and
111+ adapt to changing market conditions.
112+
113+ Args:
114+ returns (Series | DataFrame): Portfolio returns data. If DataFrame, returns a DataFrame
115+ with rolling Sharpe values for each column.
116+ rf (float, optional): Risk-free rate (annualized). Defaults to 0.0.
117+ rolling_period (int, optional): Rolling window size in periods.
118+ Defaults to 126 (~6 months of daily data).
119+ annualize (bool, optional): Whether to annualize the ratio. Defaults to True.
120+ periods_per_year (int, optional): Number of periods per year for annualization.
121+ Defaults to 252 (daily data).
122+ prepare_returns (bool, optional): Whether to normalize returns before calculation.
123+ Defaults to True.
124+
125+ Returns:
126+ Series | DataFrame: Rolling Sharpe ratio time series. Returns Series for Series input,
127+ DataFrame for DataFrame input (one column per input column).
128+ Earlier values will be NaN until the rolling window is filled.
129+
130+ Raises:
131+ ValueError: If rf != 0 and rolling_period is None.
132+
133+ Examples:
134+ >>> returns = pd.Series([0.01, -0.02, 0.03, -0.01, 0.02, 0.01])
135+ >>> rolling_sharpe(returns, rolling_period=3, annualize=False)
136+ 0 NaN
137+ 1 NaN
138+ 2 0.577350
139+ 3 -0.707107
140+ 4 0.816497
141+ 5 0.707107
142+ dtype: float64
143+
144+ >>> df = pd.DataFrame({"strategy_a": [0.01, -0.01, 0.02, 0.01],
145+ ... "strategy_b": [0.02, -0.02, 0.03, 0.01]})
146+ >>> rolling_sharpe(df, rolling_period=3, periods_per_year=252)
147+ # Returns DataFrame with rolling Sharpe for both strategies
148+
149+ Notes:
150+ - First (rolling_period - 1) values will be NaN
151+ - Useful for monitoring strategy performance over time
152+ - Captures regime changes and varying market conditions
153+ - Annualization uses sqrt(periods_per_year) scaling
154+ - When prepare_returns=True, adjusts for risk-free rate
155+ - Standard rolling window is 126 days (~6 months) or 63 days (~3 months)
156+
157+ See Also:
158+ sharpe: Static Sharpe ratio for entire period
159+ rolling_sortino: Rolling Sortino ratio (downside risk focus)
160+ smart_sharpe: Sharpe with autocorrelation penalty
161+ """
162+ # Validate parameters for risk-free rate handling
163+ if rf != 0 and rolling_period is None :
164+ raise ValueError ("Must provide periods if rf != 0" )
165+
166+ normalized = (
167+ _utils .normalize_returns (data = returns , rf = rf , nperiods = rolling_period )
168+ if prepare_returns
169+ else returns
170+ )
171+
172+ # Calculate rolling mean and standard deviation
173+ res = (
174+ normalized .rolling (rolling_period ).mean ()
175+ / normalized .rolling (rolling_period ).std ()
176+ )
177+
178+ # Annualize if requested
179+ if annualize :
180+ res = res * _np .sqrt (1 if periods_per_year is None else periods_per_year )
181+
182+ return res
183+
184+
185+ @overload
186+ def rolling_sortino (
187+ returns : Series ,
188+ rf : float = 0.0 ,
189+ rolling_period : int | None = 126 ,
190+ annualize : bool = True ,
191+ periods_per_year : int = 252 ,
192+ prepare_returns : bool = True ,
193+ ) -> Series : ...
194+ @overload
195+ def rolling_sortino (
196+ returns : DataFrame ,
197+ rf : float = 0.0 ,
198+ rolling_period : int | None = 126 ,
199+ annualize : bool = True ,
200+ periods_per_year : int = 252 ,
201+ prepare_returns : bool = True ,
202+ ) -> DataFrame : ...
203+ def rolling_sortino (
204+ returns : Series | DataFrame ,
205+ rf : float = 0.0 ,
206+ rolling_period : int | None = 126 ,
207+ annualize : bool = True ,
208+ periods_per_year : int = 252 ,
209+ prepare_returns : bool = True ,
210+ ) -> Series | DataFrame :
211+ """Calculate rolling Sortino ratio over a specified window.
212+
213+ The rolling Sortino ratio computes the Sortino ratio using a rolling window,
214+ providing a time-varying measure of downside risk-adjusted performance. Unlike
215+ the rolling Sharpe ratio, it only penalizes downside volatility, making it
216+ more suitable for strategies with asymmetric return distributions.
217+
218+ Args:
219+ returns (Series | DataFrame): Portfolio returns data. If DataFrame, returns a DataFrame
220+ with rolling Sortino values for each column.
221+ rf (float, optional): Risk-free rate (annualized). Defaults to 0.0.
222+ rolling_period (int, optional): Rolling window size in periods.
223+ Defaults to 126 (~6 months of daily data).
224+ annualize (bool, optional): Whether to annualize the ratio. Defaults to True.
225+ periods_per_year (int, optional): Number of periods per year for annualization.
226+ Defaults to 252 (daily data).
227+ prepare_returns (bool, optional): Whether to normalize returns before calculation.
228+ Defaults to True.
229+
230+ Returns:
231+ Series | DataFrame: Rolling Sortino ratio time series. Returns Series for Series input,
232+ DataFrame for DataFrame input (one column per input column).
233+ Earlier values will be NaN until the rolling window is filled.
234+ May contain inf values when downside deviation is zero.
235+
236+ Raises:
237+ ValueError: If rf != 0 and rolling_period is None.
238+
239+ Examples:
240+ >>> returns = pd.Series([0.01, -0.02, 0.03, -0.01, 0.02, 0.01])
241+ >>> rolling_sortino(returns, rolling_period=3, annualize=False)
242+ 0 NaN
243+ 1 NaN
244+ 2 0.816497
245+ 3 -1.000000
246+ 4 1.154701
247+ 5 1.000000
248+ dtype: float64
249+
250+ >>> df = pd.DataFrame({"strategy_a": [0.01, -0.01, 0.02, 0.01],
251+ ... "strategy_b": [0.02, -0.02, 0.03, 0.01]})
252+ >>> rolling_sortino(df, rolling_period=3, periods_per_year=252)
253+ # Returns DataFrame with rolling Sortino for both strategies
254+
255+ Notes:
256+ - First (rolling_period - 1) values will be NaN
257+ - Only penalizes downside volatility (returns below zero)
258+ - More forgiving than rolling Sharpe for strategies with positive skew
259+ - May return inf when window has no negative returns (zero downside)
260+ - Annualization uses sqrt(periods_per_year) scaling
261+ - Downside deviation calculated from squared negative returns only
262+ - Useful for monitoring asymmetric risk profiles over time
263+
264+ See Also:
265+ sortino: Static Sortino ratio for entire period
266+ rolling_sharpe: Rolling Sharpe ratio (total risk focus)
267+ adjusted_sortino: Sortino adjusted for Sharpe comparability
268+ smart_sortino: Sortino with autocorrelation penalty
269+ """
270+ # Validate parameters for risk-free rate handling
271+ if rf != 0 and rolling_period is None :
272+ raise ValueError ("Must provide periods if rf != 0" )
273+
274+ normalized = (
275+ _utils .normalize_returns (data = returns , rf = rf , nperiods = rolling_period )
276+ if prepare_returns
277+ else returns
278+ )
279+
280+ # Optimized downside calculation using vectorized operations
281+ def calc_downside (x ):
282+ """
283+ Calculate downside variance more efficiently.
284+
285+ This function computes the sum of squared negative returns,
286+ which is used to calculate downside deviation.
287+ """
288+ negative_returns = x [x < 0 ]
289+ return (negative_returns ** 2 ).sum () if len (negative_returns ) > 0 else 0
290+
291+ # Calculate rolling downside deviation
292+ downside = (
293+ normalized .rolling (rolling_period ).apply (calc_downside , raw = True )
294+ / rolling_period
295+ )
296+
297+ # Calculate rolling Sortino ratio
298+ res = normalized .rolling (rolling_period ).mean () / _np .sqrt (downside )
299+
300+ # Annualize if requested
301+ if annualize :
302+ res = res * _np .sqrt (1 if periods_per_year is None else periods_per_year )
303+
304+ return res
305+
306+
80307@overload
81308def sortino (
82309 returns : Series ,
0 commit comments