Datawhale量化入门笔记:最大回撤和仓位,决定策略能否拿住

只看 Sharpe,还是会漏掉一段很难熬的过程

一个策略历史 Sharpe=1.8,乍看不错;但如果中间最大回撤是 −40%,结果又是另一回事。净值从高点跌下来时,人真正面对的不是年化收益,而是"这次下跌还要多久、自己是否能按原计划继续持有"。

这一节把"最惨的时候亏过多少"单独量化出来,再用半仓和满仓的净值曲线对照。这样看策略时,除了终点在哪里,也开始看中间走得有多难受。

最大回撤不是最大单日跌幅

回撤是当前净值相对历史最高净值的跌幅:

Drawdown t = Equity t Peak t − 1 \text{Drawdown}_t = \frac{\text{Equity}_t}{\text{Peak}_t} - 1 Drawdownt=PeaktEquityt−1

其中 Peak t = max ⁡ s ≤ t Equity s \text{Peak}t = \max{s \leq t} \text{Equity}_s Peakt=maxs≤tEquitys,表示截至 t t t 时刻出现过的净值高点。最大回撤就是整段回撤序列中最小的那个值:

MDD = min ⁡ t Drawdown t \text{MDD} = \min_t \text{Drawdown}_t MDD=tminDrawdownt

它的起点是一个历史高点,终点是其后的低点,所以不能简单等同于"最大单日跌幅"。同样为 −20% 的最大回撤,一种情况可能一个月就修复,另一种情况可能两年都在水下。数值一样,持有体验并不一样。

python 复制代码
def compute_drawdown(equity: pd.Series) -> pd.Series:
    """回撤序列 = 当前净值 / 历史最高净值 − 1(≤ 0)。"""
    running_peak = equity.cummax()
    return equity / running_peak - 1

def max_drawdown(equity: pd.Series) -> float:
    """最大回撤 = 回撤序列中的最小值(最深的那次坑)。"""
    return compute_drawdown(equity).min()
python 复制代码
prices = fetch_us_close('AAPL', 3)       # 近 3 年 AAPL 收盘价
returns = prices.pct_change().dropna()
equity = buy_hold_equity(returns)         # 买入持有净值曲线
max_dd = max_drawdown(equity)
print(f'AAPL 最大回撤: {max_dd:.2%}')     # 例如:−26.3%

这里的输入应是按时间顺序排列的净值序列。若日期没有排序,cummax() 得到的"历史高点"就不再是历史上的高点,回撤计算会失真。

水下曲线:除了深度,还要看恢复期

把每日回撤画成填充图,就是水下曲线。上图的净值和历史高点用来定位回撤来源,下图的面积更适合观察回撤持续时间。

python 复制代码
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 7), sharex=True,
                                gridspec_kw={'height_ratios': [2, 1]})

# 上图:净值 + 历史最高
ax1.plot(equity.index, equity, label='买入持有净值')
ax1.plot(running_peak.index, running_peak, '--', alpha=0.55, label='历史最高')

# 下图:回撤填充
ax2.fill_between(drawdown.index, drawdown, 0, alpha=0.45)
ax2.axhline(max_dd, ':', label=f'最大回撤 {max_dd:.1%}')

读这张图时,我会先看最深处确认最大回撤,再看填充区域有多宽。窄而浅的回撤通常恢复得更快;宽而深的区域则说明净值长期未创新高。最大回撤本身没有告诉我们恢复时间,所以水下曲线正好补上这一部分。

仓位改变的是净值曲线的尺度

仓位管理不是"预测要跌就空仓",它先回答的是:信号出现时,账户中有多少资金参与。对同一条日收益序列,半仓会把每天的收益和损失都缩小一半;长期复利下,最大回撤不一定严格减半,但通常会明显下降。

仓位 含义 直观影响
满仓 100% 全部资金跟着策略 收益与回撤都完整承受
半仓 50% 一半资金参与 每日涨跌幅按比例缩小

下面用双均线策略的收益列,分别构造满仓、半仓和买入持有的净值:

python 复制代码
MA_SHORT, MA_LONG = 5, 20
HALF_SIZE = 0.5

df['Signal'] = (df['MA_S'] > df['MA_L']).astype(float)
df['StratRet'] = df['Signal'].shift(1) * df['Return']

df['Equity_100'] = buy_hold_equity(df['StratRet'])           # 满仓
df['Equity_50']  = buy_hold_equity(df['StratRet'] * 0.5)    # 半仓
df['Equity_BH']  = buy_hold_equity(df['Return'])             # 买入持有

结果示例:

策略 最大回撤
双均线 · 满仓 −18.5%
双均线 · 半仓 −9.2%
买入持有 −26.3%
python 复制代码
fig, ax = plt.subplots(figsize=(12, 5))
for col, kw in style.items():
    ax.plot(df.index, df[col], **kw)
ax.legend(loc='upper left')
ax.set_ylabel('净值')
ax.set_xlabel('日期')

这组示例不是在证明"半仓更优"。半仓减少了回撤,也会放弃一部分上涨;是否合适取决于可承受的风险预算和策略本身。要是只比较最终净值,很容易忽略这个取舍。

本节的检查清单

最大回撤计算时先确认净值按日期排序;看图时把深度与持续时间分开;比较仓位时,不能只看最终收益。下面几条公式把本节用到的计算集中在一起:

公式 含义
Drawdown t = Equity t Peak t − 1 \text{Drawdown}_t = \frac{\text{Equity}_t}{\text{Peak}_t} - 1 Drawdownt=PeaktEquityt−1 回撤
MDD = min ⁡ t Drawdown t \text{MDD} = \min_t \text{Drawdown}_t MDD=mintDrawdownt 最大回撤
Equity t = ∏ ( 1 + r t ) \text{Equity}_t = \prod (1 + r_t) Equityt=∏(1+rt) 买入持有净值
StratRet = Signal t − 1 × r t \text{StratRet} = \text{Signal}_{t-1} \times r_t StratRet=Signalt−1×rt 策略收益

本节解决的是:一条策略在样本期里曾经从高点跌多深,以及同一策略在不同仓位下呈现怎样的风险尺度。还没有解决的是,怎样根据目标波动率、交易成本和再平衡规则确定仓位;这需要在更完整的回测框架里继续验证。

下一章将从单一标的转向组合,看看相关性为什么会影响分散效果。

参考资源

相关推荐
Daisy Lee4 小时前
Datawhale 量化入门笔记:夏普比率和Beta
datawhale
Daisy Lee1 个月前
量化学习-第1章-什么是量化金融
学习·金融·datawhale
IRevers1 个月前
【大模型】Gemma4在ROCm和vLLM部署
人工智能·pytorch·深度学习·大模型·datawhale·vllm·amdev
安如衫1 个月前
【Hello-ROCm】vLLM 跑通 Gemma4-E4B
datawhale·vllm·amdev
yzqy_2 个月前
AMD AI 开发者计划学习笔记:从 ROCm 到 Ryzen AI,理解 AMD 的 AI 开发生态
人工智能·笔记·学习·datawhale·amdev
10WTW012 个月前
AMD Hello-ROCm 环境配置(一)
datawhale·gemma4·amdev
尘汐筠竹2 个月前
Day1-2 学习笔记:在 AMD 云环境上部署 Gemma 4 大模型
笔记·学习·datawhale·amdev
Mocode2 个月前
【2026】Datawhale X AMD · Hello ROCm - Part1 - 配置云环境&部署大模型
datawhale·amdev