룩어헤드 바이어스는 백테스트 시점에 실제로는 알 수 없었던 미래 정보가 신호 생성에 사용되어 성과가 비현실적으로 부풀려지는 현상입니다. 팩터 투자에서 가장 흔하고 치명적인 오류 중 하나입니다.
발견하기 어려운 이유
룩어헤드 바이어스는 코드에서 명시적 오류가 아니라 데이터 타임스탬프의 암묵적 가정에서 발생합니다. 백테스트 샤프비율이 실전 대비 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 조건을 강제하면 룩어헤드 바이어스의 가장 큰 원인을 원천 차단할 수 있습니다.