# Drawdown analysis (/features/analytics/drawdown-analysis)

How deep did the strategy fall, how long did it stay down, and how fast did it recover? VBT turns
every drawdown into a record with its peak, valley, and recovery, so drawdown analysis in Python
becomes a table you can sort, filter, and plot, next to the familiar max drawdown and underwater
curve.

```python title="Find the five worst drawdowns of a 60/40 portfolio"
>>> data = vbt.YFData.pull(["SPY", "TLT"], start="2005-01-01", end="2025-01-01")
>>> rebalance = ~data.index.tz_localize(None).to_period("M").duplicated()
>>> weights = data.symbol_wrapper.fill(np.nan)
>>> weights[rebalance] = [0.6, 0.4]
>>> pf = vbt.PF.from_orders(
...     data,
...     size=weights,
...     size_type="targetpercent",
...     group_by=True,
...     cash_sharing=True,
...     call_seq="auto",
... )

>>> dd = pf.drawdowns  # (1)
>>> pd.DataFrame({
...     "peak": dd.readable["Start Index"].dt.date,
...     "valley": dd.readable["Valley Index"].dt.date,
...     "depth": dd.drawdown.values,
...     "decline": dd.decline_duration.values,  # (2)
...     "recovery": dd.recovery_duration.values,
...     "status": dd.readable["Status"],
... }).sort_values("depth").head(5).round(3)
           peak      valley  depth  decline  recovery     status
48   2007-12-06  2009-03-09 -0.311    314.0     388.0  Recovered
298  2021-12-27  2022-10-20 -0.275    206.0     475.0  Recovered
254  2020-02-20  2020-03-18 -0.184     19.0      48.0  Recovered
226  2018-08-29  2018-12-24 -0.107     80.0      55.0  Recovered
176  2015-03-20  2015-08-25 -0.078    109.0     142.0  Recovered

>>> dd.plot(top_n=5).show()
```

1.  One record per drawdown, from its peak through its valley to the bar where value regained the
    peak.
2.  Durations are counted in bars, here trading days.

Value of a monthly rebalanced 60/40 SPY and TLT portfolio from 2005 to 2024 with its five largest drawdowns highlighted. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-top5.eed6f01cff26.json)

The 2022 drawdown was shallower than 2008 but took longer to recover, 475 trading days against 388,
because stocks and bonds fell together.

## Drawdown records \[#drawdown-records]

`pf.drawdowns` holds one record for each drawdown in the value series. Each record has the peak, the
valley, the end, the values at those points, and a status: recovered, or still active at the end of
the data. Because they are records, they can be filtered, for example `dd.status_recovered`, mapped
to any field, and turned into statistics with `dd.stats()`. Note the naming: `pf.drawdown` without
the "s" is the time series of the current percentage below the running peak.

Depths are percentages of the peak value. When a strategy trades a fixed amount out of a large
account, most of that value is idle cash, and the percentages look small. The drawdown in money is a
field away: `(dd.start_val - dd.valley_val).max()` gives the largest fall from a peak in account
currency.

The same records work on any series, not only on a portfolio. `price.vbt.drawdowns` finds the
drawdowns of a price, an indicator, or an equity curve from somewhere else.

## Duration and recovery \[#duration-and-recovery]

Depth is only half of the risk. The decline duration counts the bars from peak to valley, and the
recovery duration the bars from valley back to the peak. The underwater curve shows both at once:

```python title="Plot the underwater curve"
>>> pf.plot_underwater().show()
```

Underwater curve of a monthly rebalanced 60/40 SPY and TLT portfolio from 2005 to 2024. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-underwater.62efb61aaa32.json)

The share of time spent below a previous peak is `(pf.drawdown < 0).mean()`, 87.3% of the bars for
this portfolio. It makes a useful optimization objective for strategies that should spend little
time underwater.

## Active and average drawdowns \[#active-and-average-drawdowns]

A drawdown that has not recovered by the end of the data is active. It has a depth and a duration so
far, but no recovery yet. `pf.stats()` includes active drawdowns in the maximum drawdown, while
`dd.stats()` leaves them out of its summaries unless `incl_active` is enabled in
`vbt.settings.drawdowns`. You can also include them for a single report with
`dd.stats(settings=dict(incl_active=True))`.

An active drawdown can already be recovering. Its worst depth tells you how far the account fell,
while its current depth tells you how far it still has to recover:

```python title="Separate the worst loss from the loss still to recover"
>>> equity = pd.Series([100.0, 90.0, 100.0, 120.0, 90.0, 108.0], index=pd.date_range("2026-01-01", periods=6))
>>> equity_dd = equity.vbt.drawdowns
>>> pd.Series({
...     "Worst recovered [%]": equity_dd.status_recovered.max_drawdown * 100,
...     "Worst overall [%]": equity_dd.max_drawdown * 100,
...     "Current drawdown [%]": equity_dd.active_drawdown * 100,
... }).round(1)
Worst recovered [%]    -10.0
Worst overall [%]      -25.0
Current drawdown [%]   -10.0
dtype: float64
```

The first fall from 100 to 90 recovered fully. The second fell from 120 to 90, a 25% loss, and only
recovered to 108. The account is now 10% below its peak, but excluding the unfinished drawdown would
hide the worst loss. This works on an external equity curve as well as a VBT portfolio, so you can
compare their drawdown histories with the same tools.

Average drawdown has two meanings. The average depth of the records, `dd.avg_drawdown`, was 1.2%
here, because hundreds of shallow drawdowns count as much as the deep ones. The average of the
drawdown series over time, `pf.drawdown.mean()`, was 4.5%, because the deep drawdowns lasted longer.
State which one you report.

## Drawdowns across a grid \[#drawdowns-across-a-grid]

Maximum drawdown is a metric like any other, so it can be compared across parameters. Here the 100
moving average pairs from the
[Performance and risk metrics](/features/analytics/performance-metrics/) page are shown by Sharpe
ratio and by maximum drawdown:

```python title="Map Sharpe ratio and maximum drawdown across 100 parameter pairs"
>>> btc = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
>>> fast_w, slow_w = np.meshgrid(np.arange(5, 55, 5), np.arange(60, 260, 20))
>>> fast = vbt.MA.run(btc.close, window=fast_w.ravel(), short_name="fast")
>>> slow = vbt.MA.run(btc.close, window=slow_w.ravel(), short_name="slow")
>>> grid_pf = vbt.PF.from_signals(
...     btc,
...     fast.ma_crossed_above(slow),
...     fast.ma_crossed_below(slow),
...     fees=0.001,
... )

>>> fig = vbt.make_subplots(
...     rows=1,
...     cols=2,
...     subplot_titles=["Sharpe ratio", "Max drawdown"],
...     horizontal_spacing=0.15,
... )
>>> grid_pf.sharpe_ratio.vbt.heatmap(
...     x_level="fast_window",
...     y_level="slow_window",
...     fig=fig,
...     add_trace_kwargs=dict(row=1, col=1),
...     trace_kwargs=dict(colorbar=dict(x=0.43, len=0.9)),
... )
>>> grid_pf.max_drawdown.vbt.heatmap(
...     x_level="fast_window",
...     y_level="slow_window",
...     fig=fig,
...     add_trace_kwargs=dict(row=1, col=2),
...     trace_kwargs=dict(colorbar=dict(x=1.0, len=0.9)),
... )
>>> fig.show()
```

Heatmaps of Sharpe ratio and maximum drawdown for 100 Bitcoin moving average window pairs. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-grid.f3c3a6b19188.json)

The pair with the best Sharpe ratio, 5 and 120 days, fell 38.6% at its worst, while 5 and 160 days
had the shallowest drawdown in the grid at 30.1%. Across the grid, a higher Sharpe ratio came with
deeper drawdowns more often than not.

Use the two maps together to find parameter regions with a return and drawdown balance worth
investigating. [Robustness and overfitting](/features/optimization/robustness-and-overfitting/)
covers checking whether those results hold up beyond the best backtest.

## Equity at or below zero \[#equity-at-or-below-zero]

Drawdowns are measured relative to the peak value, so they are capped at 100% only while the value
stays positive. With leverage or short positions the value can fall below zero, and the drawdown
then exceeds 100%. The simulator does not liquidate the account at that point. To end the analysis
there, set the simulation end to the first bar where the value reaches zero.


## Related pages

*   [Performance and risk metrics](/features/analytics/performance-metrics/): Compute returns, Sharpe, Sortino, benchmark, and rolling metrics on any backtest
*   [Portfolio accounting](/features/backtesting/portfolio-accounting/): Track shared cash, deposits, dividends, positions, records, and exposure per bar
*   [Trade analytics](/features/analytics/trade-analytics/): Analyze trades and positions with MAE, MFE, edge ratio, profit factor, and plots
*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once