如何建立回測 Sharpe 信賴區間
單一權益曲線只是一個樣本。透過 bootstrap 重抽樣報酬率序列,建立回測 Sharpe 的信賴區間,並學會解讀可能包含零值的結果。
回測信賴區間回答一個問題:一條權益曲線有多少部分來自策略本身,又有多少部分只是來自所使用的特定歷史區段。對回測進行 bootstrap,是透過反覆重抽樣報酬率序列來建立該區間;每次重抽樣後重新計算統計量,再觀察結果的百分位數。以單一年期的日資料計算出的 Sharpe 為 1.4,其 95% 信賴區間可能寬到包含零值。下方各面板則說明造成這種結果的原因。
為什麼一條權益曲線只是一個樣本
回測會為每項統計量提供一個數字:一個 Sharpe ratio、一個年化報酬率、一個最大回撤,以及一個命中率。這些數字都不是策略的真實價值。每個數字都是根據有限交易日所估算的結果;若取相同長度的另一段期間,結果可能完全不同。
下方圖表完全不納入策略因素。它衡量最簡單的持倉方式:每次持有 SPY 一個日曆年,並以收盤至收盤的價格報酬計算。
每個數據背後的精確 SQL 語法
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2011-12-01'
AND date < '2026-01-01'
)
SELECT
toString(toYear(date)) AS year,
count() AS obs_count,
round(avg(ret) * 252 * 100, 2) AS ann_return_pct,
round(stddevSamp(ret) * sqrt(252) * 100, 2) AS ann_vol_pct,
round(avg(ret) / stddevSamp(ret) * sqrt(252), 2) AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
AND ret IS NOT NULL
GROUP BY year
ORDER BY year每一列涵蓋約 250 個交易時段,接近標準交易年度。這些期間的持倉方式完全沒有改變。在 2012,年化 Sharpe ratio 為 1.06;在 2025,則為 0.88。圖表共納入 14 年資料。持有廣泛指數一年,就可能使標題數字產生大幅變動,這種幅度多數讀者不會視為雜訊;相同長度的策略回測至少也會承受同等程度的變動。該欄位所採用的年化計算方式,請參閱我們的 Sharpe ratio 指南;期間報酬的計算方式,請參閱月報酬如何計算。
回測 Sharpe 周邊的區間有多寬?
Sharpe 估計值的標準誤,大致會隨觀測值數量的平方根增加而下降。與其依賴這項公式,不如直接衡量分散程度。將二十年的交易日切分為固定長度、彼此不重疊的區間,計算每個區間內的年化 Sharpe,然後觀察這些數值彼此相差多遠。
每個數據背後的精確 SQL 語法
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
blocks AS
(
SELECT
w,
intDiv(i, w) AS blk,
count() AS n,
avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252) AS sharpe
FROM indexed
CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
GROUP BY w, blk
HAVING n = w
)
SELECT
concat(toString(w), ' sessions') AS horizon,
count() AS block_count,
round(quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_p05,
round(quantileDeterministic(0.50)(sharpe, blk), 2) AS sharpe_p50,
round(quantileDeterministic(0.95)(sharpe, blk), 2) AS sharpe_p95,
round(quantileDeterministic(0.95)(sharpe, blk)
- quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY w每個區間為 21 sessions 時,實測 Sharpe 的第5至第95百分位介於 -3.37 至 6.97 之間;在 238 個區間中,差距為 10.34 個 Sharpe 點。將每個區間延長至 504 sessions 後,差距縮小至 1.67 點,但此時僅以 8 個區間進行衡量。兩項因素同時變動。樣本數增加,估計值會更精確;但歷史資料能提供的獨立樣本數也會大幅減少。這項取捨正是本文的核心。
如何以 bootstrap 建立回測信賴區間
這項程序很簡短,可以完整列出。
- 從策略的已實現報酬序列開始。每期一個數值,共 N 個。
- 從該清單隨機抽取 N 個報酬,並允許重複抽樣。有些數值會出現兩次,有些則完全不會出現。
- 針對重新抽樣的結果重新計算統計量。
- 重複數千次,並保留每次的結果。
- 將保留的數值排序,讀取第2.5百分位數與第97.5百分位數,作為95%信賴區間。
在第2步前設定隨機數產生器的 seed,並將 seed 記錄在發布的區間旁。bootstrap 是 Monte Carlo 估計法:未設定 seed 的兩次執行,最後幾位數可能不同;而無法重新執行該區間的審閱者,也就無從查核。這與 可重現的回測設定所述的紀律相同。
平均報酬與 Sharpe ratio 都能順利通過第2步,因為兩者都將報酬清單視為一組資料。最大回撤則不同。回撤會按照路徑順序讀取資料。重新抽樣若改變報酬的排列順序,所得的最大回撤可能是實際交易序列任何排列都未曾產生的結果。這類 bootstrap 仍值得進行,但必須清楚標示其意義:它代表重新排列歷史路徑後的回撤分布,而不是對下一次回撤的預測。定義請見最大回撤。
為何以 IID bootstrap 估算市場報酬是錯的
第2步假設每筆報酬彼此獨立且同分布,也就是 IID 假設。每日報酬會以一種足以改變區間寬度的方式違反這項假設。有正負號的報酬僅帶有微弱的一日記憶,但報酬幅度會群聚:大幅波動往往相鄰出現,平靜交易日也會連續出現。
每個數據背後的精確 SQL 語法
WITH daily AS
(
SELECT
ticker,
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
AND date >= '2015-11-01'
AND date < '2026-01-01'
),
lagged AS
(
SELECT
ticker,
date,
ret,
lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
FROM daily
WHERE ret IS NOT NULL
AND abs(ret) < 0.35
)
SELECT
ticker,
count() AS obs_count,
round(corr(ret, ret_prev), 3) AS return_autocorr,
round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESC針對 SPY,在 2514 個交易日中測得的每日絕對報酬一期落後自我相關係數為 0.366;同期間有正負號報酬的數值則為 -0.133。請比較圖表中任一標的的兩個長條。將報酬序列隨機重排會破壞這種群聚特性;對這些資料執行 IID bootstrap,得到的區間會比樣本實際支持的範圍更窄。這項誤差偏向最不利的方向:它會讓策略看起來比實際更好。
移動區塊 bootstrap 與區塊長度的選擇
修正方法是重新抽樣連續區塊,而不是單一報酬。先選定區塊長度 L,再隨機抽取長度為 L 的連續報酬,允許重複抽樣,並將這些區塊首尾相接,直到合成序列達到 N。區塊內的相依性會完整保留,這些報酬仍維持原本的順序。只有區塊之間的銜接是人為形成的。
區塊長度是在兩種誤差之間取捨,無法同時避開兩者。短區塊的表現近似 IID bootstrap,會低估區間範圍,這屬於偏誤。長區塊能保留較多相依性,但可供抽取的不同區塊較少。因此,每次重新抽樣都會重複歷史資料中的大段內容,區間本身也會變得不穩定,這屬於變異數。已發表的經驗法則通常會讓區塊長度隨 N 的三分之一次方增加。應將這些法則視為起始點。
較省成本的診斷方式,是檢查變異數如何隨持有期間變化。在相互獨立的情況下,k 日報酬總和的變異數等於單日變異數的 k 倍,兩者的比值為 1。
每個數據背後的精確 SQL 語法
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
base AS
(
SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
SELECT
k,
intDiv(i, k) AS blk,
count() AS n,
sum(ret) AS block_ret
FROM indexed
CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
GROUP BY k, blk
HAVING n = k
)
SELECT
concat(toString(k), ' sessions') AS block_length,
count() AS block_count,
round(varSamp(block_ret) / (k * any(var_1d)), 3) AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY k在 2 sessions,該比值為 0.844;在 63 sessions,則為 0.584,計算基礎為 78 個不重疊區塊。接近 1 的數值表示,在該持有期間,總和的擴展方式與獨立抽樣相同。偏離 1 的數值則表示,相依性在這些持有期間仍發揮作用;區塊長度必須涵蓋這段範圍。選擇能涵蓋該範圍的最短區塊,接著檢查區塊數量已降至多少。
依分組重抽樣,以及每個區間旁的樣本數
依波動率情境或曆月在各分組內重抽樣,可以保留使問題具有分析意義的條件。若主張某項策略在高波動率情境下才取得其 Sharpe,則只從高波動率交易日抽樣建立的區間,才是真正檢驗該主張的方式。代價在於樣本數。將 250 筆觀測值分成四組後,每組約剩 60 筆;以 60 筆觀測值進行 bootstrap,所得區間約是全樣本版本的兩倍寬。
因此,每次都應在每個區間旁標示該分組的樣本數。上方各面板中的 block_count 欄,正是將這項原則具體呈現出來:從九個視窗讀取的第5百分位數,與從兩百個視窗讀取的第5百分位數,代表的是不同的統計對象,即使兩者都四捨五入至小數點後兩位。
Bootstrap 無法修正的問題
Bootstrap 只能量化一件事:根據現有資料計算出的統計量所含的抽樣噪音。它無法判斷這些資料是否真的可在當時取得。
含有 前視偏誤 的回測,會產生一段實際上無法交易的報酬序列;對這段序列進行 bootstrap,只會在虛構結果周圍給出一個狹窄且看似可靠的區間。若投資標的集合由今天的指數成分股組成,就會帶有 存活者偏誤,而該集合的每次重抽樣也都會繼承這項偏誤。第三個缺口是選擇效應:執行 200 個變體後只保留表現最佳者,該變體的 bootstrap 區間只能描述自身的抽樣噪音,卻忽略了用於選出它的另外 199 次結果。誠實呈現的區間很有用,但不能取代樣本外資料。
資料備註與方法
- 報酬是以日線資料計算的收盤至收盤價格報酬。不計入股息,因此每項報酬與 Sharpe 數值的水準都會受到低估,幅度約相當於股息殖利率。本文所探討的離散程度基本不受影響。
- 期間與區塊彼此不重疊,因此每項統計量都使用歷史資料中互不重疊的一段。若期間重疊,會高估樣本數。
- 分位數採用確定性估計器,因此重新執行同一面板時,會得到相同的百分位數,而不是新的近似值。
- 自相關面板使用六檔大型個股,且各股在觀察期間內均未進行股票分割;此外,剔除單日漲跌幅超過 35% 的資料,以排除價格調整造成的異常對相關性的影響。
常見問答
Bootstrap 信賴區間能告訴你什麼?
它會給出一個統計量的合理取值範圍,前提是以相同期間數、依相同程序再次抽樣。若 95 percent 信賴區間包含零,表示樣本期間太短,無法判定該策略是否真的具備超額優勢。
Bootstrap 重抽樣要做幾次才足夠?
若要建立 95 percent 信賴區間,通常幾千次重抽樣就能得到穩定結果;10,000 次是常見的預設值,成本也不高。若要估計更尾端的分位數,則需要更多次重抽樣。例如,在 1,000 次抽樣中,第1百分位數約由十個數值的位置決定。
移動區塊 bootstrap 應使用多長的區塊?
沒有放諸四海皆準的長度。經驗法則通常會讓區塊長度隨樣本數的三分之一次方調整。實務上的檢查方式,是觀察變異數不再呈線性擴張的持有期間;上方的 variance ratio 面板即用來衡量這點。發布結果時,應一併列出所使用的區塊長度與信賴區間。
可以對最大回撤進行 bootstrap 嗎?
可以,但要注意報酬順序。回撤取決於報酬的排列,因此以打亂順序建立的重抽樣,計算的是重新排列後歷史的回撤。區塊 bootstrap 能保留短期連續走勢,因此更適合用於路徑型統計量。
Bootstrap 能修正過度擬合嗎?
不能。它衡量的是單一報酬序列內的抽樣噪音。前視偏誤,以及測試多個策略變體所產生的選擇效果,都不在它能觀察的範圍內。
這裡的每個面板都附有產生該面板的 SQL。變更 ticker 或期間長度後,便可在 Strasmore terminal 上自行執行。