什麼是最大回撤?跌幅深度與回升時間
最大回撤是數值序列從高點跌至低點的最大跌幅。了解如何計算、何謂深度回撤,以及為何回升至新高往往需要更久時間。
最大回撤(maximum drawdown)是數值序列從高點跌至低點的最大跌幅,以當時持續更新的高點為基準計算百分比。假設某帳戶升至 120,000 美元,之後跌至 84,000 美元,再度回升,其最大回撤就是 30%。這個數字會永久保留,因為該統計值記錄的是歷史上最嚴重的一段跌勢,而非目前狀態。多數人只會引用跌幅,便就此打住。同一項統計其實還包含另外兩個數字:從高點跌至低點所需的時間,以及從低點回升至創下新高所需、通常長得多的時間。
如何計算最大回撤?
整個計算只需對序列走訪一次。在每個觀測值出現時,更新累計最高點,將目前值與該最高點比較,並保留截至當時最差的讀數。
- 將累計高點設為第一個數值。
- 每出現一個新數值,若該數值較高,就更新高點。
- 計算該點的回撤:數值除以高點,再減一。
- 保留最負的讀數。這就是最大回撤。
計算不需要報酬率序列、波動率估計或任何分布假設。這個迴圈只使用 Python 標準函式庫,處理一份單純的數字清單。在全新安裝的 Ubuntu 環境中,先安裝直譯器:
apt-get update && apt-get install -y python3
將這段迴圈儲存為 maxdd.py。以下價格是由十個點組成的虛構序列,數值刻意保持簡單,方便手算:
prices = [100.0, 104.0, 98.0, 92.0, 95.0, 88.0, 90.0, 101.0, 99.0, 106.0]
peak = prices[0]
max_dd = 0.0
for price in prices:
if price > peak:
peak = price
drawdown = price / peak - 1.0
if drawdown < max_dd:
max_dd = drawdown
print(f"maximum drawdown: {max_dd * 100:.1f}%")
執行 python3 maxdd.py 後,會輸出 maximum drawdown: -15.4%。可以手算驗證:低點之前的高點是 104,低點是 88,而 88 除以 104 等於 0.846。序列最後以 106 收尾,創下歷史新高,但 15.4% 的最大回撤仍然成立。最大回撤是樣本中最糟時點的永久紀錄,並不代表序列目前位於何處。在同一個迴圈中再加入兩個變數,就能記錄時點:高點形成時的索引,以及最差讀數出現時的索引。
回撤曲線的樣貌
回撤曲線會在每個時間點保留第3步的結果,而不是將其縮減為單一的最差值。當序列創下新高時,曲線會維持在零;在兩個新高之間則低於零,因此這段差距被稱為「水下」。以下是 SPY 的回撤曲線。SPY 是追蹤 S&P 500 的 ETF,資料採用自2016年1月起的月底收盤價。
每個數據背後的精確 SQL 語法
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
),
runs AS (
SELECT month_start,
close_px,
max(close_px) OVER (ORDER BY month_start
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM monthly
)
SELECT formatDateTime(month_start, '%Y-%m') AS month,
round(100 * (close_px / running_peak - 1), 2) AS drawdown_pct
FROM runs
ORDER BY month_start在 127 個月底讀值中,曲線長時間貼近零,並數度跌入深幅凹口。資料中最後一個月份相對於累計高點為 -1.27%。曲線形狀有兩項特徵最能說明問題。回升是緩慢爬升,而不是快速反彈;每次創下新高都會將曲線重設為零,抹去此前走勢留下的視覺記憶。
回撤需要多久才能修復?
一次回撤包含三個不同數字,若將它們視為同一件事,是誤讀績效表最常見的原因:
- 幅度:價值低於前高的幅度,以百分比表示。
- 觸底時間:從前高至最低點的日曆天數。
- 修復時間:從最低點回升至創下新高的日曆天數。
第三項通常最長,也是標題數字從未呈現的部分。下方面板根據每日收盤價,重建 SPY 自2016年以來每一段已完成的水下期間。某段期間從創下前高的當天開始,至基金首次收盤高於該前高的當天結束。
每個數據背後的精確 SQL 語法
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY session_date
),
runs AS (
SELECT session_date,
close_px,
max(close_px) OVER (ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM daily
),
episodes AS (
SELECT running_peak AS peak_px,
min(session_date) AS peak_date,
argMin(session_date, close_px) AS trough_date,
min(close_px) AS trough_px
FROM runs
GROUP BY running_peak
),
sequenced AS (
SELECT peak_px,
peak_date,
trough_date,
trough_px,
leadInFrame(peak_date, 1) OVER (ORDER BY peak_date
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS recovery_date
FROM episodes
)
SELECT formatDateTime(peak_date, '%b %Y') AS episode_label,
round(100 * (1 - trough_px / peak_px), 1) AS fall_from_peak_pct,
dateDiff('day', peak_date, trough_date) AS days_to_trough,
dateDiff('day', trough_date, recovery_date) AS days_to_new_high,
dateDiff('day', peak_date, recovery_date) AS days_underwater
FROM sequenced
WHERE recovery_date > trough_date
AND round(100 * (1 - trough_px / peak_px), 1) >= 5
ORDER BY fall_from_peak_pct DESC
LIMIT 8清單中幅度最大的已完成期間始於 Feb 2020,並跌至低於前高 34.2%。該期間下跌了 33 個日曆日,之後花了 148 個日曆日回升,總計有 181 天處於水下。下一筆期間始於 Jan 2022,回撤幅度較小,為 25.4%,但基金仍有 746 天低於原先的高點。
回撤幅度與持續時間完全是兩回事。劇烈下跌後迅速收復,以及幅度較淺但持續數年的緩慢下跌,都會被壓縮成同一個百分比。2020年3月的崩跌是前一種型態的典型案例,而市場如何從崩跌中修復則涉及這段歷程的第二階段。
為何觀察期間越長,最大回撤越嚴重
最大回撤是取最大值,因此加入更多歷史資料,只會讓數值維持不變或進一步加深。在較長期間內,任一時點的累計高點至少與較短期間相同,因此計算結果必然如此。以截至2026年7月31日的五個期間觀察同一檔基金,即可看出這項影響。
每個數據背後的精確 SQL 語法
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-07-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY session_date
),
scoped AS (
SELECT arrayJoin([1, 2, 3, 5, 10]) AS lookback_years,
session_date,
close_px
FROM daily
),
windowed AS (
SELECT lookback_years, session_date, close_px
FROM scoped
WHERE session_date >= subtractYears(toDate('2026-07-31'), lookback_years)
),
runs AS (
SELECT lookback_years,
session_date,
close_px,
max(close_px) OVER (PARTITION BY lookback_years ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM windowed
)
SELECT concat(toString(lookback_years), if(lookback_years = 1, ' year', ' years')) AS lookback,
round(100 * max(1 - close_px / running_peak), 1) AS max_drawdown_pct,
formatDateTime(argMax(session_date, 1 - close_px / running_peak), '%b %Y') AS worst_point
FROM runs
GROUP BY lookback_years
ORDER BY lookback_years在過去1 year期間,量得的最大跌幅為9.1%。將相同的價格序列延長至10 years後,最大回撤變為34.2%,最嚴重的時點落在Mar 2020。基金的風險並未增加,只是樣本期間變長了。
相同的計算方式也會扭曲策略比較。三年回測與二十年回測可能描述完全相同的行為,卻仍呈現截然不同的回撤數值。因此,若不標明觀察期間,這項指標的意義有限。回測中的前視偏誤介紹了另一種讓回測結果看似漂亮、實際上卻高估自身表現的方式。
最大回撤與波動率
波動率衡量每日向上或向下變動的典型幅度。最大回撤則衡量一段特定的下跌路徑。兩者都是風險指標,但回答的問題不同。本圖比較八檔知名股票在截至2026年7月31日的五年期間內的兩項指標;波動率則根據每日收盤價年化計算。
每個數據背後的精確 SQL 語法
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'AAPL', 'MSFT', 'KO', 'JNJ', 'CVX', 'VZ', 'PG')
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2021-08-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY ticker, session_date
),
runs AS (
SELECT ticker,
session_date,
close_px,
max(close_px) OVER (PARTITION BY ticker ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak,
lagInFrame(close_px, 1) OVER (PARTITION BY ticker ORDER BY session_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM daily
)
SELECT ticker,
round(100 * max(1 - close_px / running_peak), 1) AS max_drawdown_pct,
round(100 * sqrt(252) * stddevSampIf(close_px / prev_close - 1, prev_close > 0), 1) AS annualized_volatility_pct
FROM runs
GROUP BY ticker
HAVING countIf(prev_close > 0) > 20
ORDER BY max_drawdown_pct DESC請一併查看兩欄數據。VZ的跌幅最深,最大回撤為45.4%,年化波動率則為22.6%。跌幅最小的是KO,下跌20.9%,波動率為16.7%。波動率是觀察期間每一天變動的平均值,而平均值可能淡化長期緩步下跌。回撤則是價格實際走過的最差單一路徑。某項持股若在兩年內安靜地逐步下跌,可能出現深度回撤,但波動率讀數並不突出。
低波動率異象探討低波動率股票在長期期間的表現,而Kelly 準則部位配置則說明如何將可承受的回撤幅度轉換為部位規模。
數字無法告訴你的事
以下四項限制值得牢記:
- 這只是一次已實現路徑的結果。同一個過程若走出不同路徑,數值可能差得更糟。
- 數值取決於樣本長度。因此,只有在觀察期間相同時,兩次回撤才能比較。
- 數值取決於取樣頻率。在相同期間內,月末收盤價會掩蓋盤中低點,因此算出的數值會小於以每日收盤價計算的結果。
- 數值不包含任何未來資訊。價格位於高點下方30%,並不能說明第31個點是否會在明天出現。
最後一項限制,正是自動化系統需要回撤政策,而不只是回撤門檻的原因。「回撤達20%時停機」只回答了一個問題,卻留下兩個問題未解:停機時如何處理未平倉部位,以及需要哪些證據才能重新啟動系統。交易機器人的熔斷機制說明了這類政策應如何設計。任意設定固定規則的成本,會在復甦階段顯現:如果系統在接近低點時停止,卻延後至較晚時點才重新啟動,就會錯過上漲行情。錯過最佳交易日則以數字說明這項算術關係。
在比較兩個數值前,還有一項衡量細節很重要:只要現金流入或流出,或收到股息,基金價格序列的回撤就可能不同於實際帳戶的回撤;月報酬如何衡量對此有完整說明。
最大回撤常見問題
什麼樣的最大回撤算合理?
沒有適用於所有情況的單一數值,必須同時看比較基準與衡量期間。截至2026年7月31日的十年間,SPY從滾動高點回落的最大跌幅,以每日收盤價計算為 34.2%。若某項策略在較短期間內公布較小的數值,不代表其風險一定較低。
最大回撤是否等同於波動率?
不是。波動率描述一般日內價格變動;最大回撤則描述從高點到低點的單一路徑中,最嚴重的跌幅。某項低波動持有部位若持續下跌兩年,其回撤可能深於另一項價格波動較大的部位,即使後者持續創下新高。
最大回撤需要多久才能收復?
這取決於具體情況,而回升至前高的時間往往更長。自2016年以來,SPY跌破前高後、直到創下新高的最長完整期間為 181 個日曆日,其中 33 天處於下跌階段。
最大回撤能否預測未來損失?
不能。最大回撤只描述已經發生的一條價格路徑,而且樣本期間越長,通常越容易出現更大的數值。它反映策略截至目前承受過的損失,但無法限制未來可能出現的跌幅。
以上每個圖表都儲存了背後使用的 SQL。開啟其中一個圖表,替換 ticker 或衡量期間,即可在 Strasmore terminal 上執行相同的回撤計算。