백테스트 부트스트래핑 신뢰구간 구축 및 해석 방법
단일 주식 수익률 곡선은 하나의 표본에 불과합니다. 백테스트 결과인 샤프지수(Sharpe ratio)에 부트스트래핑 신뢰구간을 적용하여 전략의 통계적 유의성을 검증하고, 결과값이 영을 포함할 만큼 넓게 나타나는 이유를 분석합니다.
백테스트 신뢰구간의 의미
백테스트 신뢰구간은 단 하나의 질문에 답합니다. 주식 수익률 곡선에서 전략 자체의 비중은 얼마이며, 전략이 실행된 특정 역사적 구간의 비중은 얼마인가 하는 점입니다. 백테스트에 부트스트래핑(bootstrapping)을 적용하면 수익률 시계열을 여러 번 재표본 추출하여 해당 구간을 구축합니다. 각 재표본에서 통계치를 다시 계산하고, 그 결과값의 백분위수를 확인하는 방식입니다. 일간 관측치를 기준으로 1년 동안 측정한 샤프지수(Sharpe ratio) 1.4는 0을 포함할 만큼 넓은 95퍼센트 신뢰구간을 가집니다. 아래 패널들은 그 이유를 측정합니다.
왜 하나의 주식 곡선이 하나의 표본에 불과한가
백테스트는 통계치당 하나의 숫자만을 제시합니다. 샤프지수(Sharpe ratio) 하나, 연율화 수익률 하나, 최대 낙폭(worst drawdown) 하나, 승률(hit rate) 하나가 그것입니다. 이 중 어느 것도 전략의 진정한 가치를 나타내지 않습니다. 각 수치는 유한한 거래일 동안의 데이터를 바탕으로 추정된 값일 뿐이며, 같은 기간이라도 다른 시점을 선택했다면 결과는 달라졌을 것입니다.
아래 패널은 전략이라는 변수를 완전히 배제합니다. 가장 단순한 포지션인 SPY 보유 전략을 대상으로, 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 >= '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에는 연율화 샤프지수가 1.06로 측정되었으나, 2025에는 0.88로 나타났으며, 차트에는 14년의 데이터가 그려져 있습니다. 광범위한 지수를 1년 보유하는 것만으로도 주요 지표가 독자들이 흔히 '노이즈'로 치부하는 수준 이상으로 변동하며, 동일한 기간의 전략 백테스트 역시 최소한 그 정도의 변동성을 내포하게 됩니다. 해당 열의 연율화 산출 방식은 샤프지수 가이드에, 기간 수익률 측정 원리는 월간 수익률 측정 방법에 설명되어 있습니다.
백테스트 샤프 지수(Sharpe ratio)의 신뢰 구간은 어느 정도인가?
샤프 지수 추정치의 표준 오차는 관측치 수의 제곱근에 비례하여 대략적으로 감소합니다. 해당 공식에만 의존하기보다는 스프레드(spread)를 직접 측정하는 것이 좋습니다. 20년 치의 거래 세션을 고정된 길이의 겹치지 않는 구간으로 나눈 뒤, 각 구간 내에서 연율화된 샤프 지수를 계산하여 그 수치들이 얼마나 떨어져 있는지 확인하십시오.
모든 수치 뒤에 숨겨진 정확한 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일 때, 측정된 샤프 지수의 5백분위수에서 95백분위수까지의 범위는 -3.37에서 6.97까지이며, 이는 238개의 구간에 걸쳐 10.34 샤프 포인트의 스프레드를 나타냅니다. 각 구간을 504 sessions로 늘리면 스프레드는 1.67 포인트로 줄어들며, 이제 측정 대상은 8개의 구간으로 감소합니다. 두 가지 현상이 동시에 발생합니다. 표본 크기가 커질수록 추정치는 더 정밀해지지만, 과거 데이터가 제공할 수 있는 독립적인 표본의 수는 줄어듭니다. 이러한 긴장 관계가 바로 이 주제의 핵심입니다.
백테스트 신뢰구간을 부트스트래핑하는 방법
이 절차는 전체를 기술하기에 충분할 만큼 간결합니다.
- 전략의 실현 수익률 시계열에서 시작합니다. 각 기간별로 하나의 수치가 있으며, 총 N개의 데이터가 있습니다.
- 해당 목록에서 N개의 수익률을 복원 추출 방식으로 무작위로 뽑습니다. 어떤 값은 두 번 선택될 수도 있고, 전혀 선택되지 않을 수도 있습니다.
- 재표본(resample)을 바탕으로 통계량을 다시 계산합니다.
- 이 과정을 수천 번 반복하며 모든 값을 기록합니다.
- 저장된 값들을 정렬하고 95퍼센트 신뢰구간을 위해 2.5번째와 97.5번째 백분위수 값을 확인합니다.
2단계 이전에 난수 생성기(random number generator)의 시드(seed)를 설정하고, 게시된 구간 옆에 해당 시드를 기록하십시오. 부트스트랩은 몬테카를로 추정치입니다. 시드를 설정하지 않은 두 번의 실행은 마지막 자릿수에서 결과가 달라지며, 귀하의 구간을 재실행할 수 없는 검토자는 이를 검증할 방법이 없습니다. 이는 재현 가능한 백테스트 설정에서 설명한 것과 동일한 원칙입니다.
평균 수익률과 Sharpe(샤프지수)는 2단계를 무리 없이 통과합니다. 두 지표 모두 수익률 목록을 하나의 집합으로 읽기 때문입니다. 하지만 Maximum drawdown(최대 낙폭)은 그렇지 않습니다. 낙폭은 경로를 순서대로 읽습니다. 수익률의 순서를 뒤섞은 재표본은 실제 거래 순서에서는 발생하지 않았던 최악의 낙폭을 산출합니다. 그럼에도 불구하고 부트스트래핑은 수행할 가치가 있습니다. 단, 그 결과값이 다음 거래에 대한 예측이 아니라 재구성된 과거 이력 전반에 걸친 낙폭의 분포임을 명시해야 합니다. 정의는 maximum drawdown에 있습니다.
시장 수익률에 IID 부트스트랩이 부적절한 이유
2단계는 모든 수익률이 독립적이고 동일한 분포를 따른다는 IID(independent and identically distributed) 가정을 전제로 합니다. 일간 수익률은 이 가정을 깨뜨리며, 이는 구간 폭의 변화를 야기합니다. 부호가 있는 수익률은 하루 정도의 미미한 기억 효과만을 가질 뿐입니다. 반면 수익률의 크기는 군집화되는 경향이 있어, 큰 변동 뒤에는 큰 변동이 따르고 조용한 날들이 연속해서 나타납니다.
모든 수치 뒤에 숨겨진 정확한 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 DESCSPY의 경우, 2514 세션 동안 측정된 일간 절대 수익률의 1차 시차 자기상관(lag-one autocorrelation)은 0.366이며, 같은 기간 부호가 있는 수익률의 자기상관은 -0.133입니다. 차트에서 특정 종목의 두 막대를 비교해 보십시오. 수익률 시계열을 무작위로 섞으면 이러한 군집 현상이 사라지며, 이 데이터로 IID 부트스트랩을 실행하면 실제 표본이 지지하는 것보다 더 좁은 구간이 산출됩니다. 이 오류는 가장 바람직하지 않은 방향으로 발생하는데, 바로 전략의 성과를 실제보다 좋게 포장하는 것입니다.
이동 블록 부트스트랩과 블록 길이 선정
이 방법의 보완책은 개별 수익률 대신 연속된 블록을 재표본 추출하는 것입니다. 블록 길이 L을 정한 뒤, L개의 연속된 수익률로 구성된 블록을 복원 추출 방식으로 무작위로 뽑아 이들을 이어 붙여 N 길이의 합성 시계열을 만드는 방식입니다. 블록 내부의 의존성은 그대로 유지되는데, 이는 수익률이 원래의 순서를 유지하기 때문입니다. 블록과 블록이 연결되는 지점만이 인위적일 뿐입니다.
블록 길이는 두 가지 오류 사이의 상충 관계를 가지며, 어떤 설정도 이 두 가지를 모두 피할 수는 없습니다. 짧은 블록은 독립항등분포(IID) 부트스트랩과 유사하게 작동하여 구간을 과소평가하는 편향(bias)을 발생시킵니다. 긴 블록은 의존성을 더 많이 보존하지만 추출할 수 있는 서로 다른 블록의 수가 줄어듭니다. 이로 인해 각 재표본이 동일한 과거 데이터의 큰 덩어리를 반복하게 되어 구간 자체에 노이즈가 발생하는 분산(variance) 문제가 생깁니다. 통상적인 경험칙은 블록 길이를 N의 3분의 1 제곱에 비례하도록 설정합니다. 이는 어디까지나 시작점으로 간주해야 합니다.
더 저렴한 진단법은 기간에 따라 분산이 어떻게 변하는지 확인하는 것입니다. 독립성 가정 하에서 k일 수익률 합의 분산은 1일 분산의 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 k2 sessions에서 측정된 비율은 0.844였으며, 63 sessions에서는 78개의 비중첩 블록에 대해 계산한 결과 0.584로 나타났습니다. 1에 가까운 값은 해당 기간 동안 합계가 독립적인 추출과 동일한 방식으로 확장됨을 의미합니다. 1에서 멀어진 값은 의존성이 여전히 작용하는 기간을 나타내며, 블록 길이는 바로 이 범위를 포괄해야 합니다. 해당 범위를 해소하는 가장 짧은 블록 길이를 선택한 뒤, 그에 따라 블록의 개수가 얼마나 줄어들었는지 확인하십시오.
버킷 리샘플링과 각 구간 옆의 표본 수
변동성 체제나 달력상의 월별로 버킷 내에서 리샘플링을 수행하면, 해당 질문을 흥미롭게 만들었던 조건부 특성이 유지됩니다. 만약 특정 전략이 고변동성 체제에서 샤프지수(Sharpe ratio)를 달성한다는 주장이 있다면, 고변동성 일자만을 추출하여 구성한 구간이 해당 주장을 검증하는 적절한 도구가 됩니다. 이에 따른 비용은 산술적입니다. 250개의 관측치를 4개의 버킷으로 나누면 버킷당 약 60개의 관측치가 남게 되며, 60개 관측치를 활용한 부트스트랩(bootstrap)은 전체 표본을 사용한 버전보다 약 두 배 넓은 구간을 생성합니다.
따라서 모든 구간 옆에는 항상 버킷의 표본 수를 명시해야 합니다. 위 패널의 block_count 열은 이러한 습관을 가시화한 것입니다. 9개의 윈도우에서 도출한 5백분위수(5th percentile)는 200개의 윈도우에서 도출한 것과 다른 성격의 지표이며, 두 값 모두 소수점 둘째 자리까지 출력되더라도 그 의미는 다릅니다.
부트스트랩으로 해결할 수 없는 것들
부트스트랩은 보유한 데이터로 계산된 통계량의 표본 추출 노이즈라는 단 한 가지만을 정량화합니다. 해당 데이터가 실제로 달성 가능한 것이었는지에 대해서는 아무런 답을 주지 않습니다.
미래 정보 편향(look-ahead bias)에 오염된 백테스트는 결코 거래할 수 없었던 수익률 시계열을 생성하며, 이에 대한 부트스트랩은 허구의 수치 주변에 좁고 자신감 있는 신뢰 구간을 제시할 뿐입니다. 오늘날의 지수 구성 종목으로만 구성된 유니버스는 생존 편향(survivorship bias)을 내포하며, 해당 유니버스를 재표본 추출할 때마다 이 편향은 그대로 계승됩니다. 세 번째 결함은 선택 효과입니다. 200개의 전략 변형을 실행하여 가장 좋은 것을 선택했다면, 그 부트스트랩 구간은 선택 과정에 투입된 199개의 시도는 무시한 채 선택된 단 하나의 변형에 대한 표본 추출 노이즈만을 설명할 뿐입니다. 정직한 신뢰 구간은 유용합니다. 하지만 이는 표본 외(out-of-sample) 데이터를 결코 대체할 수 없습니다.
데이터 주석 및 방법론
- 수익률은 일봉 기준 종가 대비 종가(close-to-close) 수익률입니다. 배당은 제외되었으며, 이로 인해 모든 수익률과 샤프지수(Sharpe figure) 수준은 대략 배당 수익률만큼 낮게 측정됩니다. 본 게시물이 다루는 분산에는 거의 영향을 미치지 않습니다.
- 윈도우와 블록은 중첩되지 않으므로, 측정된 각 통계량은 과거 데이터의 서로 겹치지 않는 구간을 사용합니다. 중첩된 윈도우를 사용할 경우 표본 수가 부풀려질 수 있습니다.
- 분위수(Quantile)는 결정론적 추정치를 사용하므로, 패널을 다시 실행해도 새로운 근사치가 아닌 동일한 백분위수가 반환됩니다.
- 자기상관 패널은 윈도우 내 주식 분할이 없는 6개의 대형주를 사용하며, 가격 조정으로 인한 왜곡을 방지하기 위해 35%를 초과하는 변동이 발생한 날은 제외합니다.
자주 묻는 질문(FAQ)
부트스트랩 신뢰구간은 백테스트에 대해 무엇을 알려주는가?
동일한 프로세스를 동일한 기간 동안 다시 샘플링했을 때 해당 통계치가 가질 수 있는 타당한 값의 범위를 제시합니다. 95퍼센트 신뢰구간에 0이 포함되어 있다면, 이는 표본 기간이 너무 짧아 해당 전략의 우위(edge)를 통계적으로 유의미하게 입증할 수 없음을 의미합니다.
부트스트랩 재표본(resample)은 몇 번이 적당한가?
95퍼센트 신뢰구간의 경우, 수천 번의 재표본이면 대개 안정적인 결과를 얻을 수 있으며, 비용 부담이 적은 1만 번을 기본값으로 흔히 사용합니다. 더 깊은 꼬리 분위수(tail quantiles)를 분석하려면 더 많은 횟수가 필요합니다. 예를 들어 1,000번의 추출에서 1퍼센트 분위수는 약 10개의 값만을 바탕으로 산출되기 때문입니다.
이동 블록 부트스트랩(moving block bootstrap)은 어느 정도의 블록 길이를 사용해야 하는가?
보편적으로 정해진 올바른 길이는 없습니다. 경험칙에 따르면 표본 크기의 3분의 1 제곱에 비례하여 설정하며, 실무적인 확인 방법은 분산이 선형적으로 증가하기를 멈추는 시점을 찾는 것입니다. 이는 위쪽의 분산 비율 패널에서 측정할 수 있습니다. 사용한 블록 길이는 신뢰구간과 함께 명시해야 합니다.
최대 낙폭(maximum drawdown)도 부트스트랩할 수 있는가?
가능하지만 순서와 관련된 주의사항이 있습니다. 낙폭은 수익률의 순서에 의존하므로, 단순히 무작위로 섞어서 만든 재표본은 재구성된 과거 데이터의 낙폭만을 보여줍니다. 블록 부트스트랩은 짧은 기간의 흐름을 그대로 유지하므로 경로 통계(path statistics)를 분석하는 데 더 적합한 도구입니다.
부트스트랩으로 과적합(overfitting)을 보정할 수 있는가?
아니요. 부트스트랩은 단일 수익률 시계열 내부의 샘플링 노이즈를 측정할 뿐입니다. 미래 정보를 미리 반영하는 편향(look-ahead bias)이나 여러 변형 모델을 테스트하며 발생하는 선택 효과(selection effect)는 부트스트랩이 감지할 수 있는 범위를 벗어납니다.
이곳의 모든 패널에는 해당 데이터를 생성한 SQL이 포함되어 있습니다. 티커(ticker)나 윈도우 길이를 변경하여 Strasmore 터미널에서 직접 실행해 보십시오.