본문으로 건너뛰기
ModuQuant
팩터 투자 완전정복 · 고급 기법

룩어헤드 바이어스

백테스트에 미래 정보가 섞이는 오류와 막는 법

21 / 21편 · 약 5분 소요

룩어헤드 바이어스는 백테스트 시점에 실제로는 알 수 없었던 미래 정보가 신호 생성에 사용되어 성과가 비현실적으로 부풀려지는 현상입니다. 팩터 투자에서 가장 흔하고 치명적인 오류 중 하나입니다.

발견하기 어려운 이유

룩어헤드 바이어스는 코드에서 명시적 오류가 아니라 데이터 타임스탬프의 암묵적 가정에서 발생합니다. 백테스트 샤프비율이 실전 대비 2배 이상 높다면 가장 먼저 의심해야 합니다.

주요 발생 유형과 방어 방법:

지표설명공식
재무 데이터 공시 지연결산 후 실제 DART 공시까지 30~90일 소요. 결산일 ≠ 사용 가능일공시일 + 1일 이후 사용
수정주가 역산 적용유상증자·액면분할 수정가를 과거에 소급 적용 → 미래 사건 반영이벤트 전 구간 원주가 사용
생존 편향현재 상장 종목만으로 과거 분석 → 상장폐지 종목 성과 누락전체 역사적 유니버스 포함
인덱스 편입 정보KOSPI200 편입 예정 종목을 편입 전 미리 보유편입 확정 발표일 기준 진입
python
import pandas as pd

def get_available_financial(
    df_financial: pd.DataFrame,   # 컬럼: ticker, fiscal_year_end, report_date, roe, pbr ...
    as_of: pd.Timestamp,          # 백테스트 신호 생성 시점
    lag_days: int = 90,           # 공시 지연 여유 기간
) -> pd.DataFrame:
    """as_of 시점에 실제로 알 수 있는 재무 데이터만 반환"""
    cutoff = as_of - pd.Timedelta(days=lag_days)
    available = df_financial[
        df_financial['report_date'] <= cutoff   # 공시일 기준 필터
    ]
    # 종목별 가장 최근 공시만 유지
    return available.sort_values('report_date').groupby('ticker').last()

# 생존 편향 방지: 상장폐지 종목 포함
# SELECT * FROM universe_history WHERE date = :rebal_date
#   → 해당 날짜에 상장 중이었던 모든 종목 포함 (상장폐지 포함)

# 수정주가 주의: 이벤트 전 구간 처리
# pykrx / FinanceDataReader의 수정주가는 현재 시점 소급 수정
# → 유상증자 이전 구간은 별도 원주가(raw price) 데이터로 교체 필요

실무 체크리스트

  • 재무 팩터: 공시일(report_date) 기준으로 데이터 필터링 (결산월 기준 X)
  • 가격 팩터: 신호 생성 날짜 당일 종가는 사용 금지, 다음 영업일 시가에 진입
  • 인덱스 리밸런싱: KRX 발표 이후 실제 편입일 기준으로 유니버스 갱신
  • 백테스트 엔진에 point_in_time=True 모드 구현 후 성과 차이 반드시 비교
  • 샤프비율이 실전 대비 50% 이상 높으면 데이터 파이프라인 전수 감사

Universe 플랫폼 적용

backtest_main.py의 팩터 계산 구간에서 재무 데이터를 로드할 때as_of_date 파라미터를 리밸런싱 날짜로 명시적으로 전달하고, 내부적으로 report_date <= as_of_date - 90days 조건을 강제하면 룩어헤드 바이어스의 가장 큰 원인을 원천 차단할 수 있습니다.

이 글은 교육용 정보이며 특정 종목·ETF 의 매수·매도를 권유하지 않습니다. 과거 성과는 미래 수익을 보장하지 않습니다.

팩터 투자 완전정복 전체 목록