Skip to content

Drawdown Analysis

Understanding and analyzing peak-to-trough declines in portfolio value.

Overview

Drawdown measures the decline from a historical peak to a subsequent trough. It's one of the most important risk metrics for traders because it represents actual loss experienced.

Maximum Drawdown

Definition

Maximum Drawdown (MDD) is the largest peak-to-trough decline in portfolio value.

from rustybt.analytics import RiskAnalytics

risk = RiskAnalytics(backtest_result)
metrics = risk.calculate_risk_metrics()

max_dd = metrics['max_drawdown']
max_dd_duration = metrics['max_drawdown_duration']

print(f"Maximum Drawdown: {max_dd:.2%}")
print(f"Duration: {max_dd_duration} days")

Calculation

def calculate_max_drawdown(equity_curve):
    """Calculate maximum drawdown from equity curve."""
    # Calculate running maximum
    running_max = equity_curve.expanding().max()

    # Calculate drawdown at each point
    drawdown = (equity_curve - running_max) / running_max

    # Maximum drawdown
    max_dd = drawdown.min()

    return max_dd

# Example
equity = backtest_result['portfolio_value']
max_dd = calculate_max_drawdown(equity)
print(f"Max Drawdown: {max_dd:.2%}")  # e.g., -23.5%

Drawdown Series

Computing Drawdowns

# Get full drawdown series
drawdowns = risk.calculate_drawdown_series()

# Plot
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.fill_between(drawdowns.index, 0, drawdowns.values * 100,
                 alpha=0.3, color='red', label='Drawdown')
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
plt.ylabel('Drawdown (%)')
plt.xlabel('Date')
plt.title('Portfolio Drawdown Over Time')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Underwater Plot

Shows how long portfolio stays below previous peak:

def plot_underwater(equity_curve):
    """Plot underwater periods (time below peak)."""
    running_max = equity_curve.expanding().max()
    drawdown = (equity_curve - running_max) / running_max

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

    # Equity curve with peaks
    ax1.plot(equity_curve.index, equity_curve.values, label='Portfolio')
    ax1.plot(running_max.index, running_max.values,
            'r--', alpha=0.5, label='Peak')
    ax1.set_ylabel('Portfolio Value ($)')
    ax1.set_title('Equity Curve with Peaks')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # Underwater plot
    ax2.fill_between(drawdown.index, 0, drawdown.values * 100,
                    color='red', alpha=0.3)
    ax2.set_ylabel('Drawdown (%)')
    ax2.set_xlabel('Date')
    ax2.set_title('Underwater Plot')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    return fig

Drawdown Periods

Identifying Drawdown Periods

# Get N largest drawdown periods
top_drawdowns = risk.get_largest_drawdowns(n=5)

for i, dd in enumerate(top_drawdowns, 1):
    print(f"\nDrawdown #{i}:")
    print(f"  Magnitude: {dd['drawdown']:.2%}")
    print(f"  Peak: {dd['start_date']}")
    print(f"  Trough: {dd['end_date']}")
    print(f"  Recovery: {dd['recovery_date']}")
    print(f"  Duration: {dd['duration']} days")

Drawdown Characteristics

def analyze_drawdown_period(start_date, end_date, equity_curve):
    """Analyze specific drawdown period."""
    period_equity = equity_curve.loc[start_date:end_date]

    # Peak and trough
    peak_value = period_equity.iloc[0]
    trough_value = period_equity.min()
    trough_date = period_equity.idxmin()

    # Metrics
    magnitude = (trough_value - peak_value) / peak_value
    duration = (trough_date - start_date).days

    # Recovery
    recovery_mask = period_equity >= peak_value
    if recovery_mask.any():
        recovery_date = period_equity[recovery_mask].index[0]
        recovery_days = (recovery_date - trough_date).days
    else:
        recovery_date = None
        recovery_days = None

    return {
        'peak_value': peak_value,
        'trough_value': trough_value,
        'trough_date': trough_date,
        'magnitude': magnitude,
        'duration_to_trough': duration,
        'recovery_date': recovery_date,
        'recovery_duration': recovery_days
    }

Drawdown Metrics

Average Drawdown

def calculate_average_drawdown(equity_curve):
    """Calculate average drawdown."""
    running_max = equity_curve.expanding().max()
    drawdown = (equity_curve - running_max) / running_max

    # Average of all negative drawdowns
    avg_dd = drawdown[drawdown < 0].mean()
    return avg_dd

Drawdown Duration

def calculate_avg_drawdown_duration(equity_curve):
    """Calculate average time underwater."""
    running_max = equity_curve.expanding().max()
    at_peak = (equity_curve >= running_max)

    # Identify drawdown periods
    underwater_periods = []
    current_period = 0

    for is_peak in at_peak:
        if not is_peak:
            current_period += 1
        elif current_period > 0:
            underwater_periods.append(current_period)
            current_period = 0

    if current_period > 0:
        underwater_periods.append(current_period)

    return np.mean(underwater_periods) if underwater_periods else 0

Calmar Ratio

Return divided by maximum drawdown:

def calculate_calmar_ratio(returns, max_drawdown):
    """Calculate Calmar ratio: Annual return / |Max drawdown|."""
    annual_return = returns.mean() * 252
    calmar = annual_return / abs(max_drawdown)
    return calmar

# Example
calmar = metrics['calmar_ratio']
print(f"Calmar Ratio: {calmar:.2f}")

# Interpretation:
# > 1.0: Return exceeds worst drawdown (good)
# > 2.0: Excellent risk-adjusted return

Practical Applications

Risk Assessment

# Evaluate if drawdown is acceptable
max_acceptable_dd = 0.25  # 25%

if abs(max_dd) > max_acceptable_dd:
    print(f"⚠ WARNING: Drawdown {abs(max_dd):.1%} exceeds limit {max_acceptable_dd:.1%}")
    print("Consider:")
    print("- Reducing position sizes")
    print("- Adding stop losses")
    print("- Diversifying strategies")
else:
    print(f"✓ Drawdown within acceptable limits")

Position Sizing

def kelly_criterion_with_dd(win_rate, avg_win, avg_loss, max_dd_tolerance):
    """Kelly criterion adjusted for drawdown tolerance."""
    # Standard Kelly
    kelly_fraction = win_rate - (1 - win_rate) / (avg_win / abs(avg_loss))

    # Adjust for drawdown tolerance
    # Conservative: Use fraction of Kelly
    dd_adjustment = max_dd_tolerance / 0.30  # Normalize to 30% DD
    adjusted_kelly = kelly_fraction * dd_adjustment * 0.5  # Half-Kelly

    return adjusted_kelly

Strategy Comparison

# Compare strategies by drawdown characteristics
strategies = ['momentum', 'mean_reversion']

for strategy_name in strategies:
    result = run_backtest(strategy_name)
    risk = RiskAnalytics(result)

    metrics = risk.calculate_risk_metrics()

    print(f"\n{strategy_name}:")
    print(f"  Max Drawdown: {metrics['max_drawdown']:.2%}")
    print(f"  Avg Drawdown: {calculate_average_drawdown(result):.2%}")
    print(f"  Max DD Duration: {metrics['max_drawdown_duration']} days")
    print(f"  Calmar Ratio: {metrics['calmar_ratio']:.2f}")

Drawdown Guidelines

Acceptable Levels

Drawdown Assessment Action
< 10% Excellent None
10-20% Good Monitor
20-30% Acceptable Review strategy
30-40% High Reduce risk
> 40% Severe Major changes needed

Duration Guidelines

# Maximum acceptable time underwater
max_acceptable_duration = 365  # 1 year

if max_dd_duration > max_acceptable_duration:
    print(f"⚠ WARNING: Underwater for {max_dd_duration} days")
    print("Strategy may take too long to recover")

Best Practices

1. Monitor Multiple Drawdown Metrics

# Don't just look at maximum drawdown
metrics = {
    'Max Drawdown': abs(metrics['max_drawdown']),
    'Avg Drawdown': abs(calculate_average_drawdown(equity)),
    'Max Duration': metrics['max_drawdown_duration'],
    'Avg Duration': calculate_avg_drawdown_duration(equity),
    'Calmar Ratio': metrics['calmar_ratio']
}

for metric, value in metrics.items():
    print(f"{metric}: {value:.2f}")

2. Analyze Drawdown Context

# Understand what caused major drawdowns
for dd in top_drawdowns[:3]:
    print(f"\nDrawdown during {dd['start_date']} to {dd['end_date']}")
    print(f"Magnitude: {dd['drawdown']:.2%}")

    # Analyze market conditions during period
    market_return = get_market_return(dd['start_date'], dd['end_date'])
    print(f"Market return: {market_return:.2%}")

    if dd['drawdown'] < market_return:
        print("Strategy underperformed market significantly")

3. Stress Test for Future Drawdowns

# Estimate potential future drawdowns
historical_max_dd = abs(metrics['max_drawdown'])

# Conservative estimate: 1.5x historical
estimated_future_dd = historical_max_dd * 1.5

print(f"Historical max DD: {historical_max_dd:.1%}")
print(f"Estimated future DD: {estimated_future_dd:.1%}")
print(f"\nCan you tolerate a {estimated_future_dd:.1%} loss?")

4. Set Stop-Loss Levels

# Set portfolio-level stop-loss based on drawdown tolerance
def set_stop_loss(current_peak, max_drawdown_tolerance):
    """Calculate stop-loss level."""
    stop_loss_level = current_peak * (1 - max_drawdown_tolerance)
    return stop_loss_level

current_portfolio_value = 100000
max_tolerance = 0.20  # 20%

stop_loss = set_stop_loss(current_portfolio_value, max_tolerance)
print(f"Current value: ${current_portfolio_value:,.0f}")
print(f"Stop-loss level: ${stop_loss:,.0f}")
print(f"If portfolio falls below ${stop_loss:,.0f}, liquidate positions")

See Also