功能
回撤分析
通过记录衡量每次回撤的深度、持续时间与恢复
策略下跌了多深,低迷持续多久,恢复速度如何?VBT 将每次回撤转换为一条包含峰值、谷底和恢复情况的记录,让 Python 中的回撤分析成为可排序、筛选和绘图的表格,并与熟悉的最大回撤和水下曲线一起使用。
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
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,
"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 Recovereddd.plot(top_n=5).show()2022 年的回撤比 2008 年浅,但恢复耗时更久:475 个交易日,而非 388 个,因为股票和债券同时下跌。
回撤记录
pf.drawdowns 为价值序列中的每次回撤保存一条记录。记录包含峰值、谷底、结束位置、这些时点的价值,以及状态:已恢复,或截至数据末尾仍未恢复。由于它们是记录,因此可以筛选,例如
dd.status_recovered,映射到任意字段,并通过 dd.stats() 生成统计。注意命名:不带“s”的 pf.drawdown 是当前价值低于累计峰值的百分比时间序列。
回撤深度以峰值价值的百分比表示。如果策略从大账户中拿出固定金额交易,大部分价值是闲置现金,百分比便显得很小。货币金额回撤只需访问相应字段:(dd.start_val - dd.valley_val).max()
返回以账户币种计的最大峰值跌幅。
同样的记录适用于任意序列,而非仅限投资组合。price.vbt.drawdowns 可以查找价格、指标或其他来源权益曲线的回撤。
持续时间与恢复
深度只是风险的一半。下跌持续时间统计从峰值到谷底的 K 线数,恢复持续时间则统计从谷底回到峰值的 K 线数。水下曲线同时显示两者:
pf.plot_underwater().show()低于此前峰值的时间占比为 (pf.drawdown < 0).mean(),该投资组合为 87.3% 的 K 线。如果策略应尽量缩短水下时间,这可以作为有用的优化目标。
未恢复回撤与平均回撤
截至数据末尾仍未恢复的回撤属于活跃回撤。它已经有深度和持续时间,但尚无恢复过程的完整结果。pf.stats() 在最大回撤中包含活跃回撤,而 dd.stats() 默认不将其纳入汇总,除非在
vbt.settings.drawdowns 中启用 incl_active。您也可通过 dd.stats(settings=dict(incl_active=True))
仅在某次报告中将其纳入。
活跃回撤可能已经开始恢复。最差深度说明账户曾下跌多少,而当前深度说明还需恢复多少:
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%,因为深回撤持续更久。报告时请说明使用哪种定义。
参数网格中的回撤
最大回撤与其他指标一样,可以跨参数比较。此处以夏普比率和最大回撤,展示绩效与风险指标页面中的 100 组均线参数:
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()夏普比率最高的组合为 5 天和 120 天,其最差跌幅为 38.6%;而 5 天和 160 天的组合在网格中回撤最浅,为 30.1%。在整个网格中,较高夏普比率往往伴随更深回撤。
结合两张图,寻找收益与回撤平衡值得进一步研究的参数区域。稳健性与过拟合介绍如何检验这些结果能否超越最佳回测而成立。
权益等于或低于零
回撤相对于峰值价值衡量,因此只有价值保持为正时才以 100% 为上限。使用杠杆或空头持仓时,价值可能跌至零以下,此时回撤会超过 100%。模拟器不会在这一刻自动清算账户。如需在此结束分析,请将模拟结束位置设为价值首次达到零的 K 线。
版权所有 © 2021–2026 Oleg Polakow。保留所有权利。
本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。