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

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

了解最大回撤如何計算、30%跌幅代表什麼,以及為何從低點回到新高通常比下跌更久,並用 Python 範例自行核對結果。

最大回撤是價值序列從高點跌至低點的最大跌幅,以當時滾動高點的百分比表示。某個帳戶升至120,000美元,之後跌至84,000美元,再度回升,最大回撤就是30%。這個數值會永久保留,因為該統計量記錄的是歷史上最糟糕的階段,而非目前狀態。多數人只引用跌幅,便就此打住。同一項統計量還包含另外兩個數字:從高點下跌至低點所需的時間,以及從低點回升至創下新高所需、通常長得多的時間。

如何計算最大回撤?

整個計算只需掃描一次序列。在每個觀測值出現時,更新目前的最高點,計算當前值相對於該最高點的跌幅,並保留截至當下最差的結果。

  1. 將第一個值設為目前最高點。
  2. 每出現一個新值,若該值較高,就更新最高點。
  3. 計算該時點的回撤:以當前值除以最高點,再減去 one。
  4. 保留最負的結果。這就是最大回撤。

計算不需要報酬率序列、波動率估計或任何分配假設。這個迴圈只使用 Python 標準函式庫,處理一串普通數字。在全新的 Ubuntu 環境中,先安裝直譯器:

apt-get update && apt-get install -y python3

將這段迴圈儲存為 maxdd.py。以下價格是虛構的 ten-point 序列,數值刻意維持精簡,方便手動核對:

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