你真正感受到的亏损
波动率把上涨和下跌一视同仁,但投资者面对的是另一个数字:账户比它曾达到的最高值低多少。这就是回撤。如果一个组合涨到 110,现在是 98,那么无论每日波动率怎么说,它都比高点低 10.9%。最大回撤是历史上最深的坑,水下时间是爬出来所用的时长。客户是在回撤时离场,风险经理是在回撤时压缩限额,而不是在标准差上。
import pandas as pd
prices = pd.Series(
[100, 105, 103, 98, 104, 110, 108, 107, 111, 95], dtype=float,
index=pd.bdate_range("2026-03-02", periods=10),
)
peak = prices.cummax() # the highest price so far, day by day
drawdown = prices / peak - 1 # 0 on a new high, negative below it
print(peak.tolist()) # [100.0, 105.0, 105.0, 105.0, 105.0, 110.0, 110.0, 110.0, 111.0, 111.0]
print(drawdown.round(4).tolist()) # [0.0, 0.0, -0.019, -0.0667, -0.0095, 0.0, -0.0182, -0.0273, 0.0, -0.1441]最深的坑
cummax() 一次调用就给出滚动高点。用价格除以它再减 1,就得到每天的回撤,永远为零或负。最大回撤就是这个序列的最小值,idxmin() 告诉你它发生在何时。回撤有一种重要的不对称性:亏 20% 需要赚 25% 才能回本,即 1 / (1 - 0.20) - 1,亏 50% 需要赚 100%。这就是风险经理拼命让回撤保持浅的原因。
print(round(drawdown.min(), 4), drawdown.idxmin().date()) # -0.1441 2026-03-13 111 down to 95
# The climb back is steeper than the fall
for loss in (0.10, 0.20, 0.50):
print(loss, round(1 / (1 - loss) - 1, 3))
# 0.1 0.111
# 0.2 0.25
# 0.5 1.0水下时间
深度只是故事的一半,持续时间是另一半。一个策略可能只亏 8%,却在旧高点下方待上三年,这会考验任何投资者的耐心。回撤为负的日子就是水下,创出新高则结束这一段。用 pandas 找最长的一段:先用比较标记水下的日子,再用 (flag != flag.shift()).cumsum() 给每段相同值编号,然后在每段内计数。一个计数并重置的简单循环同样有效,而且易读。
underwater = drawdown < 0 # True while below the running peak
run_id = (underwater != underwater.shift()).cumsum() # a new id each time the flag changes
streak = underwater.astype(int).groupby(run_id).cumsum() # 1, 2, 3, ... inside each underwater run
print(streak.tolist()) # [0, 0, 1, 2, 3, 0, 1, 2, 0, 1]
print(int(streak.max())) # 3 the longest wait for a new high, in days# Return divided by the depth of the worst hole: the Calmar ratio
annual_return = 0.12
print(round(annual_return / abs(drawdown.min()), 2)) # 0.83过去的回撤不是最坏的回测的最大回撤只是该样本中发生过的最坏情况,而不是可能发生的最坏情况:更长的历史几乎总会包含更深的坑。把它当作下限,与同期市场的最大回撤比较,并在确定仓位时留出余地。
peak = prices.cummax()与drawdown = prices / peak - 1:创新高时为 0,之下为负。- 最大回撤是
drawdown.min();亏损x需要1 / (1 - x) - 1的收益才能回本。 - 水下时间是回撤为负的最长连续天数;回测的最大回撤是下限,而非保证。
下一节:随机数生成器与种子,所有模拟的基础。
实验:评分内容
- 用
running_peak跟踪迄今为止的最高价25 分 - 用
drawdown_series衡量与高点的距离25 分 - 用
max_drawdown找出最大跌幅25 分 - 用
longest_underwater统计等待新高的最长时间25 分