@@ -109,16 +109,17 @@ def get_max_drawdown(snapshots: List[PortfolioSnapshot]) -> float:
109109
110110def get_max_daily_drawdown (snapshots : List [PortfolioSnapshot ]) -> float :
111111 """
112- Calculate the maximum daily drawdown of the portfolio as a percentage from the peak .
112+ Calculate the worst single-day decline of the portfolio as a percentage.
113113
114- This is the largest drop in equity ( in percentage) from a peak to a trough
115- during the backtest period, calculated on a daily basis .
114+ This is the largest day-over-day percentage drop in equity,
115+ NOT the peak-to-trough drawdown (use get_max_drawdown for that) .
116116
117117 Args:
118118 snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
119119
120120 Returns:
121- float: The maximum daily drawdown as a negative percentage (e.g., -5.0 for a 5% drawdown).
121+ float: The maximum single-day drawdown as a positive percentage
122+ (e.g., 0.05 for a 5% single-day decline).
122123 """
123124 # Create DataFrame from snapshots
124125 data = [(s .created_at , s .total_value ) for s in snapshots ]
@@ -136,54 +137,58 @@ def get_max_daily_drawdown(snapshots: List[PortfolioSnapshot]) -> float:
136137 # Filter out non-positive values
137138 positive_values = daily_df [daily_df ['total_value' ] > 0 ]['total_value' ]
138139
139- if positive_values .empty :
140+ if positive_values .empty or len ( positive_values ) < 2 :
140141 return 0.0
141142
142- peak = positive_values .iloc [0 ]
143- max_daily_drawdown_pct = 0.0
143+ # Compute day-over-day returns; the worst single-day decline
144+ # is the most negative return (ignore positive returns)
145+ daily_returns = positive_values .pct_change ().dropna ()
146+ negative_returns = daily_returns [daily_returns < 0 ]
144147
145- for equity in positive_values :
146- if equity > peak :
147- peak = equity
148-
149- # Avoid division by zero (shouldn't happen but extra safety)
150- if peak <= 0 :
151- continue
152-
153- drawdown_pct = (equity - peak ) / peak
154- max_daily_drawdown_pct = min (max_daily_drawdown_pct , drawdown_pct )
148+ if negative_returns .empty :
149+ return 0.0
155150
156- return abs (max_daily_drawdown_pct ) # Return as positive percentage
151+ return abs (negative_returns . min ())
157152
158153def get_max_drawdown_duration (snapshots : List [PortfolioSnapshot ]) -> int :
159154 """
160155 Calculate the maximum duration of drawdown in days.
161156
162- This is the longest period where the portfolio equity was below its peak.
157+ This is the longest period (in calendar days) where the portfolio
158+ equity was below its peak.
163159
164160 Args:
165161 snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
166162
167163 Returns:
168- int: The maximum drawdown duration in days.
164+ int: The maximum drawdown duration in calendar days.
169165 """
170166 equity_curve = get_equity_curve (snapshots )
171167 if not equity_curve :
172168 return 0
173169
174170 peak = equity_curve [0 ][0 ]
175171 max_duration = 0
176- current_duration = 0
172+ drawdown_start = None
177173
178- for equity , _ in equity_curve :
174+ for equity , timestamp in equity_curve :
179175 if equity < peak :
180- current_duration += 1
176+ # Entering or continuing a drawdown
177+ if drawdown_start is None :
178+ drawdown_start = timestamp
181179 else :
182- max_duration = max (max_duration , current_duration )
183- current_duration = 0
184- peak = equity # Reset peak to current equity
180+ # Recovered to or above the peak
181+ if drawdown_start is not None :
182+ elapsed = (timestamp - drawdown_start ).days
183+ max_duration = max (max_duration , elapsed )
184+ drawdown_start = None
185+ peak = equity
185186
186- max_duration = max (max_duration , current_duration ) # Final check
187+ # If still in drawdown at the end of the series
188+ if drawdown_start is not None and len (equity_curve ) > 0 :
189+ last_timestamp = equity_curve [- 1 ][1 ]
190+ elapsed = (last_timestamp - drawdown_start ).days
191+ max_duration = max (max_duration , elapsed )
187192
188193 return max_duration
189194
0 commit comments