Strasmore Research
學習 Matt Connor作者: Matt Connor · 更新於 2026-08-08

什麼是最大回撤?跌幅深度與回升時間

最大回撤是數值序列從高點跌至低點的最大跌幅。了解如何計算、何謂深度回撤,以及為何回升至新高往往需要更久時間。

最大回撤(maximum drawdown)是數值序列從高點跌至低點的最大跌幅,以當時持續更新的高點為基準計算百分比。假設某帳戶升至 120,000 美元,之後跌至 84,000 美元,再度回升,其最大回撤就是 30%。這個數字會永久保留,因為該統計值記錄的是歷史上最嚴重的一段跌勢,而非目前狀態。多數人只會引用跌幅,便就此打住。同一項統計其實還包含另外兩個數字:從高點跌至低點所需的時間,以及從低點回升至創下新高所需、通常長得多的時間。

如何計算最大回撤?

整個計算只需對序列走訪一次。在每個觀測值出現時,更新累計最高點,將目前值與該最高點比較,並保留截至當時最差的讀數。

  1. 將累計高點設為第一個數值。
  2. 每出現一個新數值,若該數值較高,就更新高點。
  3. 計算該點的回撤:數值除以高點,再減一。
  4. 保留最負的讀數。這就是最大回撤。

計算不需要報酬率序列、波動率估計或任何分布假設。這個迴圈只使用 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月起的月底收盤價。

查詢SPY 水下曲線:月底收盤價相對於歷史高點,2016年至2026年
每個數據背後的精確 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
Run this yourself

127 個月底讀值中,曲線長時間貼近零,並數度跌入深幅凹口。資料中最後一個月份相對於累計高點為 -1.27%。曲線形狀有兩項特徵最能說明問題。回升是緩慢爬升,而不是快速反彈;每次創下新高都會將曲線重設為零,抹去此前走勢留下的視覺記憶。

回撤需要多久才能修復?

一次回撤包含三個不同數字,若將它們視為同一件事,是誤讀績效表最常見的原因:

  1. 幅度:價值低於前高的幅度,以百分比表示。
  2. 觸底時間:從前高至最低點的日曆天數。
  3. 修復時間:從最低點回升至創下新高的日曆天數。

第三項通常最長,也是標題數字從未呈現的部分。下方面板根據每日收盤價,重建 SPY 自2016年以來每一段已完成的水下期間。某段期間從創下前高的當天開始,至基金首次收盤高於該前高的當天結束。

查詢2016年以來已完成的 SPY 回撤:幅度、下跌天數、回升天數
每個數據背後的精確 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
Run this yourself

清單中幅度最大的已完成期間始於 Feb 2020,並跌至低於前高 34.2%。該期間下跌了 33 個日曆日,之後花了 148 個日曆日回升,總計有 181 天處於水下。下一筆期間始於 Jan 2022,回撤幅度較小,為 25.4%,但基金仍有 746 天低於原先的高點。

回撤幅度與持續時間完全是兩回事。劇烈下跌後迅速收復,以及幅度較淺但持續數年的緩慢下跌,都會被壓縮成同一個百分比。2020年3月的崩跌是前一種型態的典型案例,而市場如何從崩跌中修復則涉及這段歷程的第二階段。

為何觀察期間越長,最大回撤越嚴重

最大回撤是取最大值,因此加入更多歷史資料,只會讓數值維持不變或進一步加深。在較長期間內,任一時點的累計高點至少與較短期間相同,因此計算結果必然如此。以截至2026年7月31日的五個期間觀察同一檔基金,即可看出這項影響。

查詢同一檔基金、五種回溯期間:截至2026年7月31日的 SPY 最大回撤(依樣本長度)
每個數據背後的精確 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
Run this yourself

在過去1 year期間,量得的最大跌幅為9.1%。將相同的價格序列延長至10 years後,最大回撤變為34.2%,最嚴重的時點落在Mar 2020。基金的風險並未增加,只是樣本期間變長了。

相同的計算方式也會扭曲策略比較。三年回測與二十年回測可能描述完全相同的行為,卻仍呈現截然不同的回撤數值。因此,若不標明觀察期間,這項指標的意義有限。回測中的前視偏誤介紹了另一種讓回測結果看似漂亮、實際上卻高估自身表現的方式。

最大回撤與波動率

波動率衡量每日向上或向下變動的典型幅度。最大回撤則衡量一段特定的下跌路徑。兩者都是風險指標,但回答的問題不同。本圖比較八檔知名股票在截至2026年7月31日的五年期間內的兩項指標;波動率則根據每日收盤價年化計算。

查詢最大回撤與年化波動率比較:八檔大型股,截至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
Run this yourself

請一併查看兩欄數據。VZ的跌幅最深,最大回撤為45.4%,年化波動率則為22.6%。跌幅最小的是KO,下跌20.9%,波動率為16.7%。波動率是觀察期間每一天變動的平均值,而平均值可能淡化長期緩步下跌。回撤則是價格實際走過的最差單一路徑。某項持股若在兩年內安靜地逐步下跌,可能出現深度回撤,但波動率讀數並不突出。

低波動率異象探討低波動率股票在長期期間的表現,而Kelly 準則部位配置則說明如何將可承受的回撤幅度轉換為部位規模。

數字無法告訴你的事

以下四項限制值得牢記:

  1. 這只是一次已實現路徑的結果。同一個過程若走出不同路徑,數值可能差得更糟。
  2. 數值取決於樣本長度。因此,只有在觀察期間相同時,兩次回撤才能比較。
  3. 數值取決於取樣頻率。在相同期間內,月末收盤價會掩蓋盤中低點,因此算出的數值會小於以每日收盤價計算的結果。
  4. 數值不包含任何未來資訊。價格位於高點下方30%,並不能說明第31個點是否會在明天出現。

最後一項限制,正是自動化系統需要回撤政策,而不只是回撤門檻的原因。「回撤達20%時停機」只回答了一個問題,卻留下兩個問題未解:停機時如何處理未平倉部位,以及需要哪些證據才能重新啟動系統。交易機器人的熔斷機制說明了這類政策應如何設計。任意設定固定規則的成本,會在復甦階段顯現:如果系統在接近低點時停止,卻延後至較晚時點才重新啟動,就會錯過上漲行情。錯過最佳交易日則以數字說明這項算術關係。

在比較兩個數值前,還有一項衡量細節很重要:只要現金流入或流出,或收到股息,基金價格序列的回撤就可能不同於實際帳戶的回撤;月報酬如何衡量對此有完整說明。

最大回撤常見問題

什麼樣的最大回撤算合理?

沒有適用於所有情況的單一數值,必須同時看比較基準與衡量期間。截至2026年7月31日的十年間,SPY從滾動高點回落的最大跌幅,以每日收盤價計算為 34.2%。若某項策略在較短期間內公布較小的數值,不代表其風險一定較低。

最大回撤是否等同於波動率?

不是。波動率描述一般日內價格變動;最大回撤則描述從高點到低點的單一路徑中,最嚴重的跌幅。某項低波動持有部位若持續下跌兩年,其回撤可能深於另一項價格波動較大的部位,即使後者持續創下新高。

最大回撤需要多久才能收復?

這取決於具體情況,而回升至前高的時間往往更長。自2016年以來,SPY跌破前高後、直到創下新高的最長完整期間為 181 個日曆日,其中 33 天處於下跌階段。

最大回撤能否預測未來損失?

不能。最大回撤只描述已經發生的一條價格路徑,而且樣本期間越長,通常越容易出現更大的數值。它反映策略截至目前承受過的損失,但無法限制未來可能出現的跌幅。


以上每個圖表都儲存了背後使用的 SQL。開啟其中一個圖表,替換 ticker 或衡量期間,即可在 Strasmore terminal 上執行相同的回撤計算。

#risk#drawdown#backtesting#performance metrics#python