백테스트 오버피팅 방지법

백테스트란 무엇인가

퀀트 투자와 자동매매 시스템을 개발할 때, 백테스트(Backtest)는 과거 데이터를 기반으로 전략의 수익성을 검증하는 핵심 단계입니다. 그러나 많은 트레이더가 백테스트 결과만 보고 실전에 투입했다가 큰 손실을 경험합니다. 그 원인 대부분은 오버피팅(Overfitting)에 있습니다.

오버피팅이란 전략이 과거 데이터에 지나치게 최적화되어, 미래 데이터에서는 전혀 통하지 않는 현상을 말합니다. 백테스트 수익률이 높을수록 오히려 의심해야 하는 이유가 바로 여기에 있습니다.

오버피팅이 발생하는 5가지 원인

1. 파라미터 과다 최적화

이동평균선 기간, RSI 임계값, 손절 비율 등 파라미터를 지나치게 세밀하게 튜닝하면 과거 데이터의 노이즈까지 학습하게 됩니다. 예를 들어 “21일 이동평균 + RSI 67.3 + 손절 2.17%”처럼 소수점까지 맞춘 전략은 거의 확실히 오버피팅입니다.

  • 해결책: 파라미터는 라운드 넘버(20일, 70, 2%)로 설정하고, 소수점 최적화를 피하세요.
  • 파라미터 수가 적을수록 일반화 성능이 높습니다.

2. 데이터 스누핑 바이어스

같은 데이터셋으로 수십 개의 전략을 테스트하면, 우연히 좋은 결과를 보이는 전략이 반드시 나타납니다. 이것은 전략의 실력이 아니라 확률의 장난입니다. 100개 전략을 테스트하면 5%의 유의수준에서도 5개는 “유의미”하게 나옵니다.

3. 생존자 편향

상장폐지된 종목을 제외하고 백테스트하면 결과가 과대 평가됩니다. 현재 살아남은 종목만으로 테스트하는 것은 “성공한 사람만 인터뷰하는 것”과 같습니다. 반드시 상장폐지 종목을 포함한 데이터를 사용해야 합니다.

4. 거래 비용 무시

슬리피지, 수수료, 스프레드를 반영하지 않은 백테스트는 환상에 불과합니다. 특히 고빈도 전략에서는 거래당 0.1%의 비용 차이가 연간 수익률을 완전히 뒤집을 수 있습니다.

  • 코인 선물: 메이커 0.02%, 테이커 0.05% 이상 반영
  • 주식: 수수료 + 슬리피지 최소 0.1% 가정
  • 스프레드: 유동성 낮은 종목일수록 크게 설정

5. 짧은 테스트 기간

상승장 1~2년 데이터만으로 검증한 전략은 하락장에서 무너집니다. 최소 10년 이상, 상승·하락·횡보 구간을 모두 포함한 데이터로 테스트해야 합니다. 암호화폐의 경우 데이터 기간이 짧으므로, 다양한 시장 사이클을 포함하는 것이 중요합니다.

오버피팅을 방지하는 실전 기법

워크포워드 분석 (Walk-Forward Analysis)

전체 데이터를 학습 구간(In-Sample)검증 구간(Out-of-Sample)으로 나누어 반복 테스트하는 방법입니다. 이 방식을 사용하면 전략이 미지의 데이터에서도 작동하는지 확인할 수 있습니다.

# 워크포워드 분석 예시 (Python)
import pandas as pd

def walk_forward(data, train_pct=0.7, windows=5):
    window_size = len(data) // windows
    results = []
    
    for i in range(windows):
        start = i * window_size
        end = start + window_size
        train_end = start + int(window_size * train_pct)
        
        train = data[start:train_end]
        test = data[train_end:end]
        
        # 학습 구간에서 파라미터 최적화
        best_params = optimize(train)
        # 검증 구간에서 성과 측정
        perf = evaluate(test, best_params)
        results.append(perf)
    
    return pd.DataFrame(results)

워크포워드 분석에서 학습 구간과 검증 구간의 성과 차이가 클수록 오버피팅 가능성이 높습니다. 두 구간의 샤프 비율 차이가 0.5 이상이면 경고 신호입니다.

교차 검증 (Cross-Validation)

시계열 데이터에서는 일반적인 K-Fold 교차 검증 대신, 시간 순서를 유지하는 Purged K-Fold를 사용해야 합니다. 미래 데이터가 학습에 유입되는 것(Look-Ahead Bias)을 방지하기 위해 학습 구간과 검증 구간 사이에 갭을 두는 것이 핵심입니다.

몬테카를로 시뮬레이션

백테스트 결과의 통계적 유의성을 검증하는 방법입니다. 거래 순서를 무작위로 섞어 수천 번 시뮬레이션한 후, 실제 결과가 랜덤보다 유의미하게 좋은지 확인합니다.

  • 실제 수익이 랜덤 시뮬레이션의 상위 5% 이내에 들어야 유의미
  • 최대 낙폭(MDD)의 분포도 함께 확인
  • 1,000회 이상 시뮬레이션 권장

백테스트 체크리스트

실전 투입 전 반드시 확인해야 할 항목들입니다.

  • 파라미터 수: 5개 이하로 유지하고 있는가?
  • 테스트 기간: 최소 5년(암호화폐는 3년) 이상인가?
  • 거래 비용: 슬리피지·수수료·스프레드를 보수적으로 반영했는가?
  • Out-of-Sample 검증: 워크포워드 분석을 수행했는가?
  • 시장 사이클: 상승·하락·횡보 구간이 모두 포함되어 있는가?
  • 샤프 비율: In-Sample과 Out-of-Sample 차이가 0.5 미만인가?
  • 거래 횟수: 통계적 유의성을 확보할 만큼 충분한가? (최소 100회 이상)
  • 생존자 편향: 상장폐지 종목을 포함했는가?

실전에서 백테스트 결과가 무너지는 이유

오버피팅 외에도 백테스트와 실전의 괴리를 만드는 요인들이 있습니다.

  • 유동성 변화: 백테스트에서는 항상 원하는 가격에 체결되지만, 실전에서는 주문량이 가격을 움직입니다.
  • 레짐 변화: 시장의 구조적 특성(변동성, 상관관계)이 시간에 따라 변합니다. 2020년 코로나 폭락장에서 만든 전략이 2024년 AI 랠리에서 통할 보장은 없습니다.
  • 심리적 요인: 자동매매라도 시스템을 끄고 싶은 유혹, 파라미터를 바꾸고 싶은 충동이 생깁니다. 감정매매 체크리스트를 참고하세요.

건강한 백테스트를 위한 마인드셋

백테스트는 “이 전략이 돈을 벌 수 있다”를 증명하는 도구가 아닙니다. 오히려 “이 전략이 결함이 있는가”를 검증하는 도구로 사용해야 합니다. 결함을 찾지 못했다고 해서 완벽한 전략은 아닙니다. 단지 아직 결함을 발견하지 못한 것뿐입니다.

퀀트 투자에서 장기적으로 살아남으려면, 화려한 백테스트 수익률보다 견고한 검증 프로세스가 훨씬 중요합니다. 계좌 생존 규칙을 함께 지키면서, 오버피팅의 함정을 피해가시기 바랍니다.

다음 글에서는 실전에서 바로 적용할 수 있는 파이썬 기반 백테스트 프레임워크 구축 방법을 다루겠습니다.

7) 오버피팅 방지 실전 코드: Combinatorial Purged CV

금융 데이터는 시계열 특성상 일반 교차검증(K-Fold)을 쓰면 미래 정보가 누출됩니다. Purged + Embargo 방식으로 시간 순서를 보장하면서 검증해야 합니다.

import numpy as np
import pandas as pd

class PurgedKFold:
    """시계열 데이터용 Purged K-Fold CV"""
    
    def __init__(self, n_splits=5, purge_gap=5, embargo_pct=0.01):
        self.n_splits = n_splits
        self.purge_gap = purge_gap      # train-test 사이 제거할 샘플 수
        self.embargo_pct = embargo_pct  # test 이후 제거 비율
    
    def split(self, X):
        n = len(X)
        fold_size = n // self.n_splits
        embargo_size = int(n * self.embargo_pct)
        
        for i in range(self.n_splits):
            test_start = i * fold_size
            test_end = min((i + 1) * fold_size, n)
            
            # Purge: test 직전 데이터 제거
            train_end = max(0, test_start - self.purge_gap)
            # Embargo: test 직후 데이터도 제거
            train_start_after = min(n, test_end + embargo_size)
            
            train_idx = list(range(0, train_end)) + list(range(train_start_after, n))
            test_idx = list(range(test_start, test_end))
            
            yield np.array(train_idx), np.array(test_idx)

def validate_strategy(returns_by_fold: list[float]) -> dict:
    """폴드별 수익률로 전략 로버스트성 판단"""
    mean_ret = np.mean(returns_by_fold)
    std_ret = np.std(returns_by_fold)
    pct_positive = sum(1 for r in returns_by_fold if r > 0) / len(returns_by_fold)
    
    is_robust = pct_positive >= 0.6 and mean_ret > 0 and std_ret < abs(mean_ret)
    
    return {
        "mean_return": round(mean_ret, 4),
        "std_return": round(std_ret, 4),
        "pct_positive_folds": round(pct_positive, 2),
        "is_robust": is_robust,
        "verdict": "✅ 로버스트" if is_robust else "⚠️ 오버피팅 의심"
    }

8) 파라미터 수 줄이기: 실전 팁

  • Occam’s Razor: 성능이 비슷하면 파라미터가 적은 모델을 선택.
  • 파라미터당 거래 10회 규칙: 파라미터 3개 → 최소 30회 거래 필요.
  • 이산 파라미터: 연속값(20.5일 이동평균) 대신 이산값(20일) 사용 → 과적합 감소.
  • 민감도 분석: 파라미터를 ±20% 변경해도 수익이 유지되는지 확인. 유지되면 로버스트.

9) 관련 글

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux