최근 새로운 모멘텀 기반 자산 배분 전략을 파이썬(Python)으로 구현하여 백테스트를 돌렸습니다. 결과는 놀라웠습니다. MDD는 -5%에 불과하고 누적 수익률은 무려 1000%를 가볍게 돌파했습니다. “드디어 시장을 이길 성배를 찾았구나!” 하며 환호했지만, 기쁨은 단 하루를 가지 못했습니다. 코드를 한 줄씩 뜯어보니 퀀트 개발 최악의 실수인 ‘미래 참조 편향(Look-ahead Bias)’이 숨어 있었습니다.
오늘은 금융 데이터 백테스트에서 흔히 발생하는 데이터 누수(Data Leakage) 현상의 원인과, 이를 Pandas를 통해 어떻게 기술적으로 방어해야 하는지 그 뼈아픈 경험을 공유합니다.
🚨 문제 상황: 타임머신을 타고 투자를 하다
문제의 발단은 매매 시그널(Signal)을 생성하고 수익률을 계산하는 로직에 있었습니다. 초기 코드는 대략 다음과 같이 작성되었습니다.
Python
# 오류가 숨어있는 초기 백테스트 코드
import pandas as pd
# 1. 시그널 생성: 오늘 종가(Close)가 20일 이동평균선보다 높으면 1(매수), 아니면 0
df['MA20'] = df['Close'].rolling(window=20).mean()
df['Signal'] = (df['Close'] > df['MA20']).astype(int)
# 2. 수익률 계산: '오늘'의 시그널을 바탕으로 '오늘'의 일일 수익률을 취함
df['Daily_Return'] = df['Close'].pct_change()
df['Strategy_Return'] = df['Signal'] * df['Daily_Return']
언뜻 보면 논리적인 것 같지만, 여기에는 치명적인 시간적 모순이 존재합니다. ‘오늘의 종가’가 20일 이동평균선을 넘었는지 확인하려면 오늘 주식 시장이 완전히 마감(오후 3시 30분)되어야 합니다. 그런데 위 코드는 ‘오늘 종가’를 기준으로 만든 매수 시그널로 ‘오늘 장중의 수익률(Daily_Return)’을 고스란히 챙기고 있습니다. 즉, 장 마감 시점의 데이터를 미리 알고 아침에 매수하는 ‘타임머신 매매’를 한 셈입니다.
🛠️ 해결 과정: Pandas .shift(1)을 활용한 시간 지연 설계
이러한 미래 참조 편향을 방어하려면, 시그널이 생성된 시점과 실제 매매(Execution)가 이루어지는 시점을 물리적으로 분리해야 합니다.
오늘 종가 기준으로 시그널이 나왔다면, 실제 매수는 ‘내일의 시가(Open)’ 또는 ‘내일의 종가(Close)’에 이루어져야 현실과 일치합니다. 이를 위해 Pandas의 .shift() 메서드를 활용하여 시그널을 한 칸(1일) 뒤로 미루는 작업을 적용했습니다.
Python
# 미래 참조 편향을 제거한 수정된 코드
# 1. 시그널 생성 (오늘 장 마감 기준)
df['MA20'] = df['Close'].rolling(window=20).mean()
df['Signal'] = (df['Close'] > df['MA20']).astype(int)
# 2. 시그널 지연 적용: 오늘의 시그널은 내일의 매매에 적용됨
# shift(1)을 통해 시그널을 하루 뒤로 밀어줍니다.
df['Execution_Signal'] = df['Signal'].shift(1)
# 3. 실제 수익률 계산: '어제' 발생한 시그널을 바탕으로 '오늘'의 수익률을 획득
df['Daily_Return'] = df['Close'].pct_change()
df['Strategy_Return'] = df['Execution_Signal'] * df['Daily_Return']
# 4. 결측치(NaN) 처리: shift로 인해 발생한 첫째 날의 빈 데이터 제거
df.dropna(inplace=True)
이렇게 코드를 수정하고 나니 1000%였던 누적 수익률은 현실적인 수준(연평균 8~10% 내외)으로 곤두박질쳤습니다. 하지만 이것이 실제 라이브 트레이딩에 투입했을 때 계좌가 녹아내리는 것을 막아준 ‘진짜 수익률’입니다.
💡 결론 및 실무 인사이트
머신러닝이나 강화학습을 금융 데이터에 적용할 때, 모델이 과거 데이터를 학습하면서 미래의 정답(Target)을 훔쳐보는 ‘데이터 누수(Data Leakage)’는 모델을 완전히 망가뜨립니다. 백테스트 엔진을 직접 구축하시거나 시계열 데이터를 다루실 때는, 반드시 로직 내부에 .shift() 처리가 정확히 들어가 있는지, 그리고 장 마감 데이터(Close)와 장 시작 데이터(Open)가 논리적으로 엉키지 않았는지 두 번, 세 번 교차 검증하시기 바랍니다. 성배는 없습니다. 철저한 데이터 검증만이 퀀트의 생명줄입니다.