@@ -241,3 +241,148 @@ def test_cagr_dataframe_and_edge_cases(sample_returns):
241241 empty = pd .Series ([], dtype = float )
242242 assert math .isnan (stats .cagr (empty , periods = 1 ))
243243 assert math .isnan (stats .cagr (pd .Series ([- 1.1 , 0.05 ]), periods = 2 ))
244+
245+
246+ def test_drawdown_details_basic ():
247+ """Test basic drawdown_details functionality with simple drawdown pattern."""
248+ dates = pd .date_range ("2024-01-01" , periods = 10 , freq = "D" )
249+ # Create a simple drawdown: 0, -0.05, -0.10, -0.08, 0, 0, -0.03, -0.05, -0.02, 0
250+ drawdown = pd .Series (
251+ [0.0 , - 0.05 , - 0.10 , - 0.08 , 0.0 , 0.0 , - 0.03 , - 0.05 , - 0.02 , 0.0 ], index = dates
252+ )
253+
254+ result = stats .drawdown_details (drawdown )
255+
256+ # Should identify 2 drawdown periods
257+ assert len (result ) == 2
258+ assert list (result .columns ) == [
259+ "start" ,
260+ "valley" ,
261+ "end" ,
262+ "days" ,
263+ "max drawdown" ,
264+ "99% max drawdown" ,
265+ ]
266+
267+ # First drawdown: index 1-3 (Jan 2-4), recovers on index 4 (Jan 5)
268+ assert result .iloc [0 ]["days" ] == 3
269+ assert result .iloc [0 ]["max drawdown" ] == pytest .approx (10.0 ) # 10% drawdown
270+
271+ # Second drawdown: index 6-8 (Jan 7-9), recovers on index 9 (Jan 10)
272+ assert result .iloc [1 ]["days" ] == 3
273+ assert result .iloc [1 ]["max drawdown" ] == pytest .approx (5.0 ) # 5% drawdown
274+
275+
276+ def test_drawdown_details_no_drawdowns ():
277+ """Test drawdown_details with no drawdowns (all zeros)."""
278+ dates = pd .date_range ("2024-01-01" , periods = 5 , freq = "D" )
279+ drawdown = pd .Series ([0.0 , 0.0 , 0.0 , 0.0 , 0.0 ], index = dates )
280+
281+ result = stats .drawdown_details (drawdown )
282+
283+ assert len (result ) == 0
284+ assert list (result .columns ) == [
285+ "start" ,
286+ "valley" ,
287+ "end" ,
288+ "days" ,
289+ "max drawdown" ,
290+ "99% max drawdown" ,
291+ ]
292+
293+
294+ def test_drawdown_details_starts_in_drawdown ():
295+ """Test drawdown_details when series starts in drawdown."""
296+ dates = pd .date_range ("2024-01-01" , periods = 5 , freq = "D" )
297+ # Starts at -0.05 (in drawdown), gets worse, then recovers
298+ drawdown = pd .Series ([- 0.05 , - 0.08 , - 0.10 , - 0.05 , 0.0 ], index = dates )
299+
300+ result = stats .drawdown_details (drawdown )
301+
302+ assert len (result ) == 1
303+ # Should use first date as start
304+ assert result .iloc [0 ]["start" ] == dates [0 ].strftime ("%Y-%m-%d" )
305+ # Days in drawdown: Jan 1-4 (4 days), recovers on Jan 5
306+ assert result .iloc [0 ]["days" ] == 4
307+ assert result .iloc [0 ]["max drawdown" ] == pytest .approx (10.0 )
308+
309+
310+ def test_drawdown_details_ends_in_drawdown ():
311+ """Test drawdown_details when series ends in drawdown."""
312+ dates = pd .date_range ("2024-01-01" , periods = 5 , freq = "D" )
313+ # Starts at 0, enters drawdown, never recovers
314+ drawdown = pd .Series ([0.0 , - 0.02 , - 0.05 , - 0.08 , - 0.10 ], index = dates )
315+
316+ result = stats .drawdown_details (drawdown )
317+
318+ assert len (result ) == 1
319+ # Should use last date as end
320+ assert result .iloc [0 ]["end" ] == dates [- 1 ].strftime ("%Y-%m-%d" )
321+ assert result .iloc [0 ]["days" ] == 4
322+ assert result .iloc [0 ]["max drawdown" ] == pytest .approx (10.0 )
323+
324+
325+ def test_drawdown_details_dataframe_input ():
326+ """Test drawdown_details with DataFrame input (multiple columns)."""
327+ dates = pd .date_range ("2024-01-01" , periods = 6 , freq = "D" )
328+ df = pd .DataFrame (
329+ {
330+ "strategy_a" : [0.0 , - 0.05 , - 0.10 , 0.0 , - 0.03 , 0.0 ],
331+ "strategy_b" : [0.0 , - 0.02 , - 0.04 , - 0.03 , 0.0 , 0.0 ],
332+ },
333+ index = dates ,
334+ )
335+
336+ result = stats .drawdown_details (df )
337+
338+ # Should have multi-level columns
339+ assert isinstance (result .columns , pd .MultiIndex )
340+ assert "strategy_a" in result .columns .get_level_values (0 )
341+ assert "strategy_b" in result .columns .get_level_values (0 )
342+
343+ # Each strategy should have 2 drawdown periods
344+ assert len (result ) == 2
345+
346+
347+ def test_drawdown_details_valley_identification ():
348+ """Test that valley (max drawdown point) is correctly identified."""
349+ dates = pd .date_range ("2024-01-01" , periods = 7 , freq = "D" )
350+ # Drawdown that gets progressively worse then recovers
351+ drawdown = pd .Series ([0.0 , - 0.02 , - 0.05 , - 0.10 , - 0.08 , - 0.03 , 0.0 ], index = dates )
352+
353+ result = stats .drawdown_details (drawdown )
354+
355+ assert len (result ) == 1
356+ # Valley should be at index 3 (where -0.10 occurs)
357+ assert result .iloc [0 ]["valley" ] == dates [3 ].strftime ("%Y-%m-%d" )
358+
359+
360+ def test_drawdown_details_percentages_positive ():
361+ """Test that drawdown percentages are returned as positive values."""
362+ dates = pd .date_range ("2024-01-01" , periods = 5 , freq = "D" )
363+ drawdown = pd .Series ([0.0 , - 0.15 , - 0.20 , - 0.10 , 0.0 ], index = dates )
364+
365+ result = stats .drawdown_details (drawdown )
366+
367+ # All drawdown values should be positive (easier interpretation)
368+ assert result .iloc [0 ]["max drawdown" ] > 0
369+ assert result .iloc [0 ]["99% max drawdown" ] > 0
370+ assert result .iloc [0 ]["max drawdown" ] == pytest .approx (20.0 ) # 20%
371+
372+
373+ def test_drawdown_details_99th_percentile ():
374+ """Test that 99% max drawdown excludes outliers."""
375+ dates = pd .date_range ("2024-01-01" , periods = 100 , freq = "D" )
376+ # Create drawdown with mostly small values and one outlier
377+ drawdown_values = [- 0.01 ] * 98 + [- 0.50 , 0.0 ] # One extreme outlier
378+ drawdown_values [0 ] = 0.0 # Start at no drawdown
379+ drawdown = pd .Series (drawdown_values , index = dates )
380+
381+ result = stats .drawdown_details (drawdown )
382+
383+ assert len (result ) == 1
384+ # Max drawdown should include outlier
385+ assert result .iloc [0 ]["max drawdown" ] == pytest .approx (50.0 )
386+ # 99% max drawdown should exclude it and be much smaller
387+ assert result .iloc [0 ]["99% max drawdown" ] < result .iloc [0 ]["max drawdown" ]
388+ assert result .iloc [0 ]["99% max drawdown" ] == pytest .approx (1.0 , rel = 0.5 )
0 commit comments