← 기사 목록으로
March 6, 2026
5분 소요

몬테카를로 부트스트랩: 10줄의 코드로 백테스트 신뢰구간을 얻는 방법

몬테카를로 부트스트랩: 10줄의 코드로 백테스트 신뢰구간을 얻는 방법
#algotrading
#backtest
#Monte Carlo
#bootstrap
#confidence intervals
#risk management
#statistics

전략을 백테스트에 돌렸습니다. PnL +42%, Sharpe 1.8, MaxDD -12%. 결과가 훌륭해 보입니다. 봇을 프로덕션에 투입하고, 한 달 후 드로다운이 이미 -28%에 달하고 PnL이 제로를 향해 가고 있다는 것을 발견합니다.

무엇이 잘못되었을까요? 버그도 아니고 "시장의 변화"도 아닙니다. 문제는 하나의 숫자 — 단일 점추정에 기반하여 결정을 내렸다는 것입니다. 전략이 +42%를 보여줬다는 것은 알게 되었지만, 그 숫자를 얼마나 신뢰할 수 있는지는 알지 못했습니다.

단일 점추정의 문제

단일 점추정 대 완전한 확률 분포 단일 데이터 포인트(왼쪽)는 오해를 불러일으키는 그림을 보여주고, 완전한 분포(오른쪽)는 가능한 결과의 실제 범위를 드러낸다.

과거 데이터에 대한 백테스트는 특정한 시장 이벤트 시퀀스를 통한 한 번의 실행입니다. 결과는 거래 순서에 의존합니다: 동일한 전략, 동일한 거래라도 다른 순서라면 완전히 다른 최대 드로다운을 보일 수 있습니다.

491번의 거래를 상상해 보세요. 각 거래는 특정 수익률 분포를 가진 무작위 이벤트입니다. 과거 데이터의 백테스트는 이 프로세스의 하나의 실현만을 보여줍니다. 주사위를 한 번 던지고 그 주사위는 항상 4가 나온다고 결론짓는 것과 같습니다.

우리가 실제로 필요한 것:

  • 점추정이 아닌 구간: "95% 확률로, 최종 PnL은 X와 Y 사이에 있다"
  • 단일 최대 드로다운이 아닌 분포: "최악의 5% 시나리오에서, 드로다운은 Z%를 초과한다"
  • 평균이 아닌 테일: 운이 따르지 않을 때 무슨 일이 일어나는가?

이것이 바로 몬테카를로 부트스트랩의 목적입니다.

몬테카를로 부트스트랩이란

몬테카를로 부트스트랩 리샘플링: 거래 데이터에서 생성된 수천 개의 대안 에쿼티 경로 부트스트랩은 원본 데이터셋에서 복원 추출로 거래를 리샘플링하여 수천 개의 대안 에쿼티 궤적을 생성한다.

부트스트랩은 1979년 Bradley Efron이 제안한 리샘플링 방법입니다. 아이디어는 우아합니다: 데이터 샘플이 있으면, 원본에서 복원 추출로 무작위로 요소를 선택하여 수천 개의 "새로운" 샘플을 생성할 수 있습니다.

백테스트 맥락에서 다음과 같이 작동합니다:

  1. 각 거래의 수익률 배열이 있다 — 예를 들어 491개의 값
  2. 이 배열에서 491개의 값을 복원 추출로 무작위 선택한다 — 일부 거래는 두 번 나타나고, 일부는 전혀 나타나지 않는다
  3. 이 새로운 샘플로 에쿼티 커브를 구성한다
  4. 이것을 10,000번 반복한다
  5. 단일 숫자가 아닌, 최종 지표의 분포를 얻는다

각 반복은 하나의 "대안 시나리오"입니다: 거래의 순서와 집합이 약간 달랐다면 무엇이 일어날 수 있었는가.

10줄로 구현하기

다음은 완전히 작동하는 구현입니다:

import numpy as np

def max_drawdown(equity_curve):
    """Calculate the maximum drawdown of an equity curve."""
    peak = np.maximum.accumulate(equity_curve)
    drawdown = (equity_curve - peak) / peak
    return drawdown.min()

trade_returns = [...]  # 491 values, e.g. [0.012, -0.005, 0.008, ...]

n_simulations = 10000
results = []

for _ in range(n_simulations):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    results.append({
        "final_pnl": equity[-1] - 1,
        "max_dd": max_drawdown(equity),
        "sharpe": np.mean(sampled) / np.std(sampled) * np.sqrt(252)
    })

실행 시간: 일반 노트북에서 약 2초. 전략의 10,000개 대안 이력.

신뢰구간 추출

PnL, MaxDD, Sharpe Ratio의 신뢰구간 (5번째, 50번째, 95번째 백분위수) 주요 전략 지표의 신뢰구간: PnL, MaxDD, Sharpe Ratio. 5번째(최악), 50번째(중앙값), 95번째(최선) 백분위수 밴드를 표시.

이제 하나의 숫자가 아닌 분포를 갖게 되었습니다. 여기서 유용한 정보를 추출하는 방법은 다음과 같습니다:

import pandas as pd

df = pd.DataFrame(results)

pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)

dd_5 = np.percentile(df['max_dd'], 5)    # 5th — worst case
dd_50 = np.percentile(df['max_dd'], 50)
dd_95 = np.percentile(df['max_dd'], 95)  # 95th — best case

print(f"PnL:   {pnl_5:.1%} | {pnl_50:.1%} | {pnl_95:.1%}")
print(f"MaxDD: {dd_5:.1%} | {dd_50:.1%} | {dd_95:.1%}")
print(f"Sharpe: {np.percentile(df['sharpe'], 5):.2f}{np.percentile(df['sharpe'], 95):.2f}")

실제 전략에 대한 출력 예시:

지표 5번째 백분위수 (최악) 중앙값 95번째 백분위수 (최선)
PnL +18.3% +41.7% +72.1%
MaxDD -23.4% -12.8% -5.1%
Sharpe 1.12 1.76 2.41

이제 차이가 명확합니다:

  • 백테스트는 PnL +42%를 보여줬다 — 하지만 최악의 5% 시나리오에서는 PnL이 겨우 +18.3%
  • 백테스트는 MaxDD -12%를 보여줬다 — 하지만 최악의 5% 시나리오에서는 드로다운이 -23.4%
  • Sharpe 1.8 — 하지만 하한은 1.12

5번째 백분위수가 당신의 "현실적인 최악의 경우"입니다. 5번째 백분위수에서 전략이 수익을 내지 못한다면, 프로덕션 배포는 위험합니다.

시각화: 팬 차트

몬테카를로 부트스트랩은 자연스럽게 팬 차트 — 에쿼티 커브의 부채꼴로 시각화됩니다:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

ax = axes[0]
for i in range(min(500, n_simulations)):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    ax.plot(equity, alpha=0.02, color='#4FC3F7')

all_equities = []
for _ in range(n_simulations):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    all_equities.append(equity)

all_equities = np.array(all_equities)
p5 = np.percentile(all_equities, 5, axis=0)
p50 = np.percentile(all_equities, 50, axis=0)
p95 = np.percentile(all_equities, 95, axis=0)

ax.fill_between(range(len(p5)), p5, p95, alpha=0.3, color='#7C4DFF', label='90% CI')
ax.plot(p50, color='#E040FB', linewidth=2, label='Median')
ax.set_title('Monte Carlo Bootstrap: Equity Curves')
ax.legend()

ax = axes[1]
ax.hist(df['final_pnl'] * 100, bins=80, color='#4FC3F7', alpha=0.7, edgecolor='#1A237E')
ax.axvline(pnl_5 * 100, color='#FF5252', linestyle='--', label=f'5th: {pnl_5:.1%}')
ax.axvline(pnl_50 * 100, color='#E040FB', linestyle='--', label=f'Median: {pnl_50:.1%}')
ax.axvline(pnl_95 * 100, color='#69F0AE', linestyle='--', label=f'95th: {pnl_95:.1%}')
ax.set_title('Distribution of Final PnL')
ax.set_xlabel('PnL, %')
ax.legend()

plt.tight_layout()
plt.savefig('monte_carlo_fan_chart.png', dpi=150)
plt.show()

팬 차트는 가능한 결과의 범위를 직관적으로 이해할 수 있게 해줍니다. 좁은 부채꼴은 전략이 안정적이라는 것을 의미합니다. 넓은 부채꼴은 결과가 거래 순서의 "운"에 크게 의존한다는 것을 의미합니다.

몬테카를로 시각화: 팬 차트와 분포 히스토그램 팬 차트(왼쪽)는 가능한 에쿼티 궤적의 범위를 보여주고, 히스토그램(오른쪽)은 신뢰구간(5%, 50%, 95%)이 강조된 최종 수익의 밀도 분포를 보여준다.

고급 분석: 파산 확률

부트스트랩을 통해 중요한 질문에 답할 수 있습니다: 전략이 자본의 X%를 잃을 확률은 얼마인가?

ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")

prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")

worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")

이 지표들은 단일 백테스트 실행으로는 얻을 수 없습니다. 하지만 전략 배포 결정에는 필수적입니다.

깊은 드로다운이 왜 수학적으로 위험하고 수익 비대칭성이 어떻게 작동하는지에 대해 더 알고 싶다면, 손실-수익 비대칭성 기사를 읽어보세요.

고전적 부트스트랩이 작동하지 않는 경우

이 방법에는 알아두어야 할 제한사항이 있습니다.

수익률의 자기상관

고전적 부트스트랩은 거래가 독립적이라고 가정합니다. 실제로는 그렇지 않은 경우가 많습니다 — 전략에는 연승과 연패 구간이 있을 수 있습니다. 자기상관이 유의미하다면 블록 부트스트랩을 사용하세요:

def block_bootstrap(returns, block_size=10, n_simulations=10000):
    """Bootstrap preserving local dependency structure."""
    n = len(returns)
    results = []

    for _ in range(n_simulations):
        starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
        sampled = np.concatenate([returns[s:s+block_size] for s in starts])[:n]
        equity = np.cumprod(1 + sampled)
        results.append({
            "final_pnl": equity[-1] - 1,
            "max_dd": max_drawdown(equity),
        })

    return pd.DataFrame(results)

블록 부트스트랩은 연속 거래 간의 지역적 의존성을 보존하여, MaxDD에 대해 더 현실적인 신뢰구간을 제공합니다.

시장 비정상성

부트스트랩은 원래의 거래 분포로 작동합니다. 시장이 구조적으로 변화한 경우 (예: 변동성 감소 또는 유동성 변화), 과거 거래가 대표성을 갖지 못할 수 있습니다. 이를 고려하려면:

  • 롤링 윈도우 사용: 최근 N개의 거래에 대해서만 부트스트랩
  • 최근 거래에 더 높은 가중치 부여: 가중 부트스트랩
  • 데이터를 시장 레짐별로 나누어 별도로 부트스트랩

거래 수가 적은 경우

부트스트랩은 n > 30 거래일 때 신뢰할 수 있습니다. 10개의 거래만 있다면 — 아무리 리샘플링해도 도움이 되지 않습니다. 491개의 거래는 훌륭한 샘플이며, 결과를 신뢰할 수 있습니다.

백테스트 견고성 평가 접근법 비교

방법 제공하는 것 복잡도 시간 사용 시기
단일 백테스트 하나의 점추정 최소 최종 결과로는 절대 사용하지 않음
워크포워드 아웃오브샘플 지표 중간 오버피팅 확인용
몬테카를로 부트스트랩 신뢰구간 최소 ~2초 항상 프로덕션 전에
몬테카를로 경로 새로운 가격 경로 높음 분~시간 스트레스 테스트용
교차검증 폴드 간 평균 지표 중간 파라미터 튜닝용

몬테카를로 부트스트랩은 최소한의 시간으로 리스크의 완전한 전체 그림을 제공하는 유일한 방법입니다.

체크리스트: 결과 해석

몬테카를로 부트스트랩 결과를 해석하는 권장 방법은 다음과 같습니다:

프로덕션 배포 조건:

  • 5번째 백분위수 PnL이 양수
  • 5번째 백분위수 MaxDD가 리스크 허용 범위 내에서 허용 가능
  • 파산 확률 < 1%
  • 5번째 백분위수 Sharpe > 0.5

개선 필요 경우:

  • 5번째 백분위수 PnL이 제로 근처
  • 5번째 백분위수 MaxDD가 50번째 백분위수보다 현저히 나쁨
  • 넓은 팬 차트 범위 — 전략이 불안정

배포하지 않음 경우:

  • 5번째 백분위수 PnL이 음수
  • 파산 확률 > 5%
  • Sharpe 신뢰구간에 0이 포함됨

marketmaker.cc에서의 경험

marketmaker.cc에서 우리는 자체 백테스트 엔진을 개발하고 있으며, 몬테카를로 부트스트랩은 파이프라인의 필수적인 부분입니다. 모든 전략은 라이브 트레이딩 승인 전에 자동으로 부트스트랩을 거칩니다.

부트스트랩을 백테스트 엔진에 직접 통합했습니다: 실행 후 최종 PnL만이 아니라, 신뢰구간, 팬 차트, 파산 확률, 블록 대 표준 부트스트랩 비교가 포함된 완전한 보고서를 얻습니다. 이는 추가로 2-3초가 걸립니다 — 실제 리스크를 이해하기 위한 미미한 비용입니다.

우리의 경험에서: 단일 점추정으로 매력적으로 보이는 **전략의 약 30%**가 몬테카를로 부트스트랩 후 필터링됩니다. 5번째 백분위수 PnL이 음수가 되거나, MaxDD가 허용 불가능한 수준으로 밝혀집니다. 부트스트랩이 없었다면 이 전략들은 프로덕션에 투입되어 거의 확실히 손실을 초래했을 것입니다.

결론

몬테카를로 부트스트랩은 약 10줄의 코드와 약 2초의 계산입니다. 백테스트의 단일 숫자를 신뢰구간이 있는 완전한 분포로 변환합니다. 이것은 아마도 모든 정량적 분석 도구 중 가장 높은 ROI일 것입니다:

  • 최소 비용: 30분에 구현
  • 최대 효과: 전략의 실제 리스크 이해
  • 의존성 없음: NumPy만 필요

아직 부트스트랩을 사용하지 않고 있다면 — 오늘 파이프라인에 추가하세요. 백테스트 결과를 얼마나 신뢰할 수 있는지 아는 유일한 방법입니다.


References

  1. Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
  2. Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
  3. Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
  4. QuantStart — Monte Carlo Simulation for Backtest Analysis
  5. Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  6. Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
  7. NumPy — numpy.random.choice

Citation

@software{soloviov2026montecarlobootstrap,
  author = {Soloviov, Eugen},
  title = {Monte Carlo Bootstrap: How to Get Confidence Intervals for a Backtest in 10 Lines of Code},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/monte-carlo-bootstrap-backtest},
  version = {0.1.0},
  description = {Why a single-point estimate from a backtest is a dangerous illusion. How Monte Carlo bootstrap in 2 seconds of computation gives you a 95\% confidence interval for PnL and MaxDD, and why this is a mandatory step before launching a strategy in production.}
}
blog.disclaimer

MarketMaker.cc Team

퀀트 리서치 및 전략

Telegram에서 토론하기
Newsletter

시장에서 앞서 나가세요

뉴스레터를 구독하여 독점적인 AI 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.