# 回撤分析 (/zh/features/analytics/drawdown-analysis)

策略下跌了多深，低迷持续多久，恢复速度如何？VBT 将每次回撤转换为一条包含峰值、谷底和恢复情况的记录，让 Python
中的回撤分析成为可排序、筛选和绘图的表格，并与熟悉的最大回撤和水下曲线一起使用。

```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.  每次回撤对应一条记录，从峰值经谷底直到价值重新达到峰值的 K 线。
2.  持续时间以 K 线数量计，此处为交易日。

2005 至 2024 年按月再平衡的 SPY 与 TLT 60/40 投资组合价值，突出显示五次最大回撤. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-top5.eed6f01cff26.json)

2022 年的回撤比 2008 年浅，但恢复耗时更久：475 个交易日，而非 388 个，因为股票和债券同时下跌。

## 回撤记录 \[#drawdown-records]

`pf.drawdowns` 为价值序列中的每次回撤保存一条记录。记录包含峰值、谷底、结束位置、这些时点的价值，以及状态：已恢复，或截至数据末尾仍未恢复。由于它们是记录，因此可以筛选，例如
`dd.status_recovered`，映射到任意字段，并通过 `dd.stats()` 生成统计。注意命名：不带“s”的 `pf.drawdown` 是当前价值低于累计峰值的百分比时间序列。

回撤深度以峰值价值的百分比表示。如果策略从大账户中拿出固定金额交易，大部分价值是闲置现金，百分比便显得很小。货币金额回撤只需访问相应字段：`(dd.start_val - dd.valley_val).max()`
返回以账户币种计的最大峰值跌幅。

同样的记录适用于任意序列，而非仅限投资组合。`price.vbt.drawdowns` 可以查找价格、指标或其他来源权益曲线的回撤。

## 持续时间与恢复 \[#duration-and-recovery]

深度只是风险的一半。下跌持续时间统计从峰值到谷底的 K 线数，恢复持续时间则统计从谷底回到峰值的 K 线数。水下曲线同时显示两者：

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

2005 至 2024 年按月再平衡的 SPY 与 TLT 60/40 投资组合水下曲线. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-underwater.62efb61aaa32.json)

低于此前峰值的时间占比为 `(pf.drawdown < 0).mean()`，该投资组合为 87.3% 的 K 线。如果策略应尽量缩短水下时间，这可以作为有用的优化目标。

## 未恢复回撤与平均回撤 \[#active-and-average-drawdowns]

截至数据末尾仍未恢复的回撤属于活跃回撤。它已经有深度和持续时间，但尚无恢复过程的完整结果。`pf.stats()` 在最大回撤中包含活跃回撤，而 `dd.stats()` 默认不将其纳入汇总，除非在
`vbt.settings.drawdowns` 中启用 `incl_active`。您也可通过 `dd.stats(settings=dict(incl_active=True))`
仅在某次报告中将其纳入。

活跃回撤可能已经开始恢复。最差深度说明账户曾下跌多少，而当前深度说明还需恢复多少：

```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
```

第一次从 100 跌至 90 后已完全恢复。第二次从 120 跌至 90，亏损 25%，仅恢复到 108。账户目前低于峰值
10%，但排除这次未完成回撤会掩盖最严重的损失。这既适用于外部权益曲线，也适用于 VBT 投资组合，因此可以使用相同工具比较它们的回撤历史。

平均回撤有两种含义。记录深度的平均值 `dd.avg_drawdown` 在此为 1.2%，因为数百次浅回撤与深回撤具有同样权重。回撤序列的时间平均值 `pf.drawdown.mean()` 则为
4.5%，因为深回撤持续更久。报告时请说明使用哪种定义。

## 参数网格中的回撤 \[#drawdowns-across-a-grid]

最大回撤与其他指标一样，可以跨参数比较。此处以夏普比率和最大回撤，展示[绩效与风险指标](/zh/features/analytics/performance-metrics/)页面中的 100
组均线参数：

```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()
```

100 组比特币均线窗口参数的夏普比率和最大回撤热图. [Figure data (JSON)](/assets/figures/features/analytics/drawdown-analysis-grid.f3c3a6b19188.json)

夏普比率最高的组合为 5 天和 120 天，其最差跌幅为 38.6%；而 5 天和 160 天的组合在网格中回撤最浅，为 30.1%。在整个网格中，较高夏普比率往往伴随更深回撤。

结合两张图，寻找收益与回撤平衡值得进一步研究的参数区域。[稳健性与过拟合](/zh/features/optimization/robustness-and-overfitting/)介绍如何检验这些结果能否超越最佳回测而成立。

## 权益等于或低于零 \[#equity-at-or-below-zero]

回撤相对于峰值价值衡量，因此只有价值保持为正时才以 100% 为上限。使用杠杆或空头持仓时，价值可能跌至零以下，此时回撤会超过
100%。模拟器不会在这一刻自动清算账户。如需在此结束分析，请将模拟结束位置设为价值首次达到零的 K 线。
