Bootstrap przedziałów ufności w testach historycznych
Pojedyncza krzywa kapitału stanowi tylko jedną próbę statystyczną. Przedział ufności dla wskaźnika Sharpe w testach historycznych często obejmuje zero. Metoda budowy i interpretacji.
Przedział ufności w testach historycznych
Przedział ufności w testach historycznych odpowiada na jedno pytanie: w jakim stopniu krzywa kapitału wynika ze strategii, a w jakim z konkretnego wycinka historii, na którym została przetestowana. Metoda bootstrap w testach historycznych buduje ten przedział poprzez wielokrotne próbkowanie szeregu stóp zwrotu, ponowne obliczanie statystyki dla każdej próby i odczytywanie percentyli uzyskanych wyników. Wskaźnik Sharpe’a na poziomie jeden przecinek cztery, mierzony na podstawie rocznych obserwacji dziennych, charakteryzuje się przedziałem dziewięćdziesięciopięcioprocentowym na tyle szerokim, że zawiera zero, co wyjaśniają poniższe panele.
Dlaczego jedna krzywa kapitału to tylko jedna próba
Backtest dostarcza jednej wartości dla każdej statystyki: jednego wskaźnika Sharpe’a, jednej rocznej stopy zwrotu, jednego maksymalnego obsunięcia kapitału, jednej skuteczności. Żadna z nich nie stanowi rzeczywistej wartości strategii. Każda jest szacunkiem opartym na skończonej liczbie dni handlowych, a inny okres o tej samej długości przyniósłby odmienne wyniki.
Poniższy panel całkowicie abstrahuje od samej strategii. Mierzy on najprostszą możliwą pozycję, polegającą na utrzymywaniu SPY przez jeden rok kalendarzowy, w oparciu o stopy zwrotu z zamknięcia na zamknięcie.
Dokładny kod SQL dla każdej liczby
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 yearKażdy wiersz obejmuje około 250 sesji, co odpowiada standardowemu rokowi giełdowemu. Charakter pozycji nie uległ w żadnym z tych przypadków zmianie. W 2012 roczny wskaźnik Sharpe’a wyniósł 1.06; w 2025 osiągnął poziom 0.88, przy czym na wykresie naniesiono 14 lat. Jeden rok utrzymywania szerokiego indeksu zmienia główny wskaźnik w stopniu większym, niż większość czytelników uznałaby za szum informacyjny, a backtest strategii o tej samej długości dziedziczy co najmniej taką samą zmienność. Konwencja annualizacji zastosowana w kolumnie została opisana w naszym przewodniku po wskaźniku Sharpe’a, a mechanika pomiaru stóp zwrotu w okresach w sposobie mierzenia miesięcznych stóp zwrotu.
Jak szeroki jest przedział wokół wskaźnika Sharpe’a w backteście?
Błąd standardowy estymacji wskaźnika Sharpe’a maleje w przybliżeniu wraz z pierwiastkiem kwadratowym z liczby obserwacji. Zamiast polegać na tym wzorze, należy zmierzyć rozpiętość bezpośrednio. Należy podzielić dwadzieścia lat sesji na niepokrywające się okna o stałej długości, obliczyć zannualizowany wskaźnik Sharpe’a wewnątrz każdego okna i sprawdzić, jak bardzo różnią się uzyskane wartości.
Dokładny kod SQL dla każdej liczby
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 wPrzy 21 sessions na okno, przedział od piątego do dziewięćdziesiątego piątego percentyla zmierzonego wskaźnika Sharpe’a rozciąga się od -3.37 do 6.97, co daje rozpiętość 10.34 punktów wskaźnika Sharpe’a w ramach 238 okien. Po wydłużeniu każdego okna do 504 sessions rozpiętość spada do 1.67 punktów, mierzonych teraz w zaledwie 8 oknach. Dwie rzeczy zmieniają się jednocześnie. Estymacja staje się bardziej precyzyjna wraz z wielkością próby, a liczba niezależnych próbek, które może dostarczyć historia, maleje. To napięcie stanowi istotę zagadnienia.
Jak wyznaczyć przedział ufności dla backtestu metodą bootstrap
Procedura jest na tyle krótka, że można ją przedstawić w całości.
- Należy rozpocząć od szeregu zrealizowanych stóp zwrotu strategii, przyjmując jedną wartość na okres, łącznie N okresów.
- Należy wylosować N stóp zwrotu z tej listy w sposób losowy, ze zwracaniem. Niektóre wartości pojawią się dwukrotnie, inne wcale.
- Należy ponownie obliczyć statystykę dla tak przygotowanej próby.
- Należy powtórzyć tę czynność kilka tysięcy razy, zachowując każdą uzyskaną wartość.
- Należy posortować zapisane wartości i odczytać percentyl dwa i pół oraz dziewięćdziesiąt siedem i pół dla przedziału dziewięćdziesięciopięcioprocentowego.
Przed przejściem do kroku drugiego należy zainicjować generator liczb losowych i zapisać ziarno obok opublikowanego przedziału. Metoda bootstrap jest estymacją Monte Carlo: dwa uruchomienia bez ustalonego ziarna będą różnić się ostatnimi cyframi, a recenzent, który nie może ponownie wyliczyć przedziału, nie ma możliwości jego weryfikacji. Jest to ta sama dyscyplina, którą opisano w powtarzalnej konfiguracji backtestu.
Średnia stopa zwrotu oraz wskaźnik Sharpe przechodzą przez krok drugi bez zakłóceń, ponieważ obie te miary traktują listę stóp zwrotu jako zbiór. Maksymalne obsunięcie kapitału (maximum drawdown) nie podlega tej zasadzie. Obsunięcie kapitału odczytuje ścieżkę w określonej kolejności. Próba losowa, która zmienia kolejność stóp zwrotu, daje wynik maksymalnego obsunięcia, którego nie wygenerowałaby żadna sekwencja rzeczywistych transakcji. Stosowanie metody bootstrap w tym przypadku nadal ma sens, pod warunkiem, że wynik zostanie odpowiednio oznaczony: jako rozkład obsunięć w przetasowanych historiach, a nie jako prognoza kolejnego okresu. Definicja znajduje się w maksymalnym obsunięciu kapitału.
Dlaczego bootstrap IID jest błędny w przypadku stóp zwrotu z rynku
Krok drugi zakłada, że każda stopa zwrotu jest niezależna i ma ten sam rozkład, co stanowi założenie IID. Dzienne stopy zwrotu naruszają tę zasadę w sposób, który zmienia szerokość przedziału. Stopy zwrotu ze znakiem wykazują jedynie słabą pamięć jednodniową. Ich wartości bezwzględne ulegają jednak klastrowaniu: duże ruchy występują obok dużych ruchów, a spokojne dni pojawiają się seriami.
Dokładny kod SQL dla każdej liczby
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 DESCDla SPY autokorelacja pierwszego rzędu bezwzględnych dziennych stóp zwrotu wyniosła 0.366 w trakcie 2514 sesji, w porównaniu do -0.133 dla stóp zwrotu ze znakiem w tych samych dniach. Należy porównać dwa słupki dla dowolnej nazwy na wykresie. Tasowanie szeregu stóp zwrotu niszczy to klastrowanie, a bootstrap IID przeprowadzony na tych danych wskazuje przedział węższy, niż wynika to z próby. Błąd ten występuje w najmniej pożądanym kierunku: zawyża ocenę strategii.
Metoda ruchomych bloków bootstrap i dobór długości bloku
Rozwiązaniem jest losowanie ciągłych bloków zamiast pojedynczych stóp zwrotu. Należy wybrać długość bloku L, losować bloki złożone z L kolejnych stóp zwrotu ze zwracaniem, a następnie łączyć je ze sobą, aż syntetyczna szereg osiągnie długość N. Zależności wewnątrz bloku pozostają nienaruszone: stopy zwrotu zachowują pierwotną kolejność. Sztuczne są jedynie punkty łączenia bloków.
Długość bloku stanowi kompromis między dwoma rodzajami błędów, przy czym żadne ustawienie nie pozwala uniknąć obu. Krótkie bloki zachowują się jak bootstrap IID i zaniżają przedział, co stanowi obciążenie. Długie bloki zachowują więcej zależności, ale pozostawiają mniej odrębnych bloków do losowania, przez co każda próba powtarza duże fragmenty tej samej historii, a sam przedział staje się zaszumiony, co zwiększa wariancję. Publikowane reguły kciuka skalują długość bloku do potęgi jednej trzeciej liczby N. Należy traktować je jako punkty wyjścia.
Tańszą metodą diagnostyczną jest sprawdzenie, jak wariancja skaluje się wraz z horyzontem. W warunkach niezależności wariancja sumy stóp zwrotu z k dni jest równa k-krotności wariancji jednodniowej, a stosunek tych dwóch wartości wynosi jeden.
Dokładny kod SQL dla każdej liczby
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 kPrzy 2 sessions stosunek ten wyniósł 0.844; przy 63 sessions osiągnął 0.584, obliczone dla 78 niepokrywających się bloków. Wartości bliskie jeden oznaczają, że suma skaluje się w sposób typowy dla niezależnych losowań w danym horyzoncie. Wartości odbiegające od jeden wskazują horyzonty, w których zależności nadal odgrywają rolę, i jest to zakres, który musi pokryć długość bloku. Należy wybrać najkrótszy blok, który go obejmuje, a następnie sprawdzić, do jakiego poziomu spadła liczba bloków.
Resampling w podziale na koszyki oraz liczebność przy każdym przedziale
Resampling wewnątrz koszyków, według reżimu zmienności lub miesiąca kalendarzowego, pozwala zachować warunki, które czynią analizę istotną. Jeśli teza zakłada, że strategia osiąga swój wskaźnik Sharpe’a w reżimach wysokiej zmienności, przedział zbudowany wyłącznie na podstawie dni o wysokiej zmienności jest tym, który weryfikuje to założenie. Kosztem jest arytmetyka. Podział dwustu pięćdziesięciu obserwacji na cztery koszyki pozostawia około sześćdziesięciu obserwacji w każdym z nich, a bootstrap oparty na sześćdziesięciu obserwacjach daje przedział około dwukrotnie szerszy niż w przypadku pełnej próby.
Należy zatem podawać liczebność koszyka przy każdym przedziale, za każdym razem. Kolumna block_count w powyższych panelach jest wizualizacją tego nawyku: piąty percentyl odczytany z dziewięciu okien jest innym obiektem niż ten odczytany z dwustu, nawet jeśli oba są prezentowane z dokładnością do dwóch miejsc po przecinku.
Czego nie naprawi bootstrap
Bootstrap kwantyfikuje tylko jedno zjawisko: szum próbkowania w statystyce obliczonej na podstawie posiadanych danych. Metoda ta nie rozstrzyga jednak, czy dane te były kiedykolwiek możliwe do uzyskania w warunkach rynkowych.
Backtest obarczony błędem wyprzedzenia generuje szereg stóp zwrotu, którego nie dało się zrealizować w handlu, a bootstrap takiego szeregu zwraca jedynie wąski, pozornie wiarygodny przedział wokół fikcyjnych wyników. Uniwersum aktywów zbudowane z obecnych składników indeksu zawiera błąd przeżywalności, a każde ponowne próbkowanie tego uniwersum dziedziczy tę wadę. Trzecią luką jest efekt selekcji: po uruchomieniu dwustu wariantów strategii i wybraniu najlepszego, przedział bootstrapowy opisuje jedynie szum próbkowania tego konkretnego wariantu, ignorując sto dziewięćdziesiąt dziewięć prób, które doprowadziły do jego wyboru. Rzetelne przedziały ufności są użyteczne, lecz nie zastąpią danych spoza próby (out-of-sample).
Uwagi dotyczące danych i metodyki
- Stopy zwrotu obliczono jako zmiany cen od zamknięcia do zamknięcia na podstawie notowań dziennych. Dywidendy zostały wykluczone, co zaniża poziom każdej stopy zwrotu oraz wskaźnika Sharpe’a o około wartość rentowności dywidendy. Dyspersja, której dotyczy niniejszy wpis, pozostaje niemal niezmieniona.
- Okna i bloki nie nakładają się na siebie, dzięki czemu każda mierzona statystyka wykorzystuje rozłączne fragmenty historii. Zastosowanie nakładających się okien sztucznie zawyżyłoby liczbę próbek.
- Kwantyle wyznaczono za pomocą estymatora deterministycznego, dlatego ponowne uruchomienie panelu zwraca ten sam percentyl zamiast nowej aproksymacji.
- Panel autokorelacji wykorzystuje sześć dużych spółek, w których oknie czasowym nie wystąpił podział akcji, oraz pomija dni, w których zmiana ceny przekroczyła trzydzieści pięć procent, co eliminuje z korelacji artefakty wynikające z korekt cenowych.
Najczęściej zadawane pytania
Co przedział ufności wyznaczony metodą bootstrap mówi o wynikach backtestu?
Określa on zakres wartości, jakie dana statystyka mogłaby przyjąć, gdyby ten sam proces był próbkowany ponownie w tej samej liczbie okresów. Jeśli dziewięćdziesięciopięcioprocentowy przedział zawiera zero, oznacza to, że próba jest zbyt krótka, aby odróżnić strategię od braku jakiejkolwiek przewagi rynkowej.
Ile prób bootstrapowych jest wystarczających?
Dla przedziału dziewięćdziesięciopięcioprocentowego kilka tysięcy prób zazwyczaj zapewnia stabilność, a dziesięć tysięcy jest powszechnie stosowanym standardem, który nie generuje wysokich kosztów obliczeniowych. Analiza głębszych kwantyli ogonowych wymaga większej liczby prób: pierwszy percentyl z tysiąca losowań jest wyznaczany na podstawie zaledwie dziesięciu wartości.
Jaką długość bloku należy przyjąć w metodzie moving block bootstrap?
Nie istnieje uniwersalnie poprawna długość. Reguły praktyczne sugerują skalowanie jej względem wielkości próby do potęgi jednej trzeciej. Praktycznym testem jest wyznaczenie horyzontu, w którym wariancja przestaje rosnąć liniowo, co mierzy powyższy panel variance ratio. Należy publikować przyjętą długość bloku wraz z wyznaczonym przedziałem.
Czy można zastosować bootstrap do maksymalnego obsunięcia kapitału (maximum drawdown)?
Tak, z zastrzeżeniem dotyczącym kolejności danych. Obsunięcie zależy od sekwencji stóp zwrotu, więc próba stworzona poprzez losowe przemieszanie danych odzwierciedla obsunięcie dla przeorganizowanej historii. Metoda block bootstrap zachowuje ciągłość krótkich serii i jest lepszym narzędziem do analizy statystyk ścieżki cenowej.
Czy bootstrap koryguje skutki przeuczenia (overfitting)?
Nie. Metoda ta mierzy jedynie szum próbkowania wewnątrz jednej szeregu stóp zwrotu. Błąd wyprzedzenia (look-ahead bias) oraz efekt selekcji wynikający z testowania wielu wariantów strategii pozostają poza zakresem jej wykrywalności.
Każdy panel zawiera kod SQL, który wygenerował prezentowane dane. Zmień ticker lub długość okna i uruchom zapytanie samodzielnie w terminalu Strasmore.