Backtesting Series – Episode 2: Cross-Validation techniques
0 조회
백테스트에서 연 30% 수익률이 나왔는데 실전에선 -5%로 바뀌는 경험, 한 번쯤 있으시죠? 저는 2024년 초 KOSPI 종목 회전 전략을 워크포워드로 검증했을 때 샤프 비율 1.8이 나왔는데, 실계좌에서 3개월 돌렸더니 0.4로 떨어졌어요. 문제는 검증 구간이 운 좋게 상승장과 겹쳤다는 거였죠.
이 글을 읽고 나면 워크포워드와 CPCV(조합형 퍼지드 교차검증) 두 가지를 직접 설정해보고, 자신의 전략에 어느 방법이 더 엄격한 검증인지 판단할 수 있습니다. 각 방법의 파라미터를 조정하면서 과최적화를 실제로 잡아내는 과정을 단계별로 따라가 보세요.
준비물
다음 환경이 필요합니다.
- Python 3.8 이상
- pandas, numpy 기본 패키지
- mlfinlab 라이브러리 (CPCV 구현체 포함) 또는 scikit-learn으로 직접 구현
- 최소 3년 이상 일봉 데이터 (저는 KOSPI200 구성종목 5년치를 썼어요)
- 백테스트 엔진 (backtrader, zipline, 또는 자체 구현)
mlfinlab은 유료지만 CPCV 로직만 뽑아서 직접 짤 수도 있습니다.
워크포워드 검증은 어떻게 설정하나요?
워크포워드는 가장 간단한 시계열 검증 방법입니다. 훈련 구간에서 파라미터를 최적화하고, 바로 다음 검증 구간에서 성과를 측정하죠.
제가 2024년 1월에 돌린 설정은 이랬어요.
- 훈련 기간: 2021-01-01 ~ 2023-06-30 (30개월)
- 검증 기간: 2023-07-01 ~ 2023-12-31 (6개월)
- 최적화 대상: 이동평균 교차 기간 (5~60일 범위)
코드로 표현하면 이렇습니다.
train = data['2021-01-01':'2023-06-30']
test = data['2023-07-01':'2023-12-31']
best_params = optimize(train, param_grid)
result = backtest(test, best_params)
→ 이렇게 돌렸더니 검증 구간에서 수익률 +18%, MDD -7%가 나왔어요. 문제는 2023년 하반기가 반등장이었다는 점이죠. 이 결과만 보고 실전에 투입했다가 2024년 1~3월 조정장에서 -12% 손실을 봤습니다.
워크포워드의 핵심 함정은 검증 구간이 단 한 가지 경로라는 겁니다. 운 좋게 상승장과 겹치면 과최적화를 전혀 못 잡아요.
CPCV 파라미터는 어떻게 잡아야 하나요?
CPCV는 전체 기간을 N개 폴드로 나눈 뒤, 가능한 모든 훈련-검증 조합을 만들어 테스트합니다. 예를 들어 10개 폴드를 8:2로 나누면 45가지 조합이 나오죠(출처: quantbeckman.com 블로그).
제가 같은 데이터로 CPCV를 설정한 과정입니다.
- n_splits: 20 (전체를 20개 블록으로 분할)
- test_splits: 4 (각 테스트는 4개 블록 = 약 3개월)
- purge_size: 5일 (훈련 마지막과 검증 시작 사이 갭)
- embargo: 2일 (검증 종료 후 정보 누수 방지)
이렇게 설정하니 총 120가지 훈련-검증 조합이 나왔어요.
from sklearn.model_selection import TimeSeriesSplit
cv = CombPurgedKFoldCV(
n_splits=20,
n_test_splits=4,
purge_size=5,
embargo=2
)
scores = []
for train_idx, test_idx in cv.split(data):
train_data = data.iloc[train_idx]
test_data = data.iloc[test_idx]
params = optimize(train_data)
score = backtest(test_data, params)
scores.append(score)
mean_score = np.mean(scores)
std_score = np.std(scores)
→ 결과는 평균 수익률 +5.2%, 표준편차 ±8.3%였습니다. 워크포워드 +18%보다 훨씬 낮지만, 120가지 시나리오에서 나온 평균이라 신뢰도가 높아요. 실전 3개월 결과는 +4.8%로 예측 범위 안에 들었습니다.
중요한 건 purge_size예요. 5일보다 짧게 잡으니 훈련 마지막 날 종가로 다음 날 시가를 예측하는 정보 누수가 생겼어요. 최소 거래일 기준 3~5일은 띄워야 합니다.
두 방법으로 동일 전략을 비교하면 어떤 차이가 나나요?
같은 이동평균 교차 전략을 두 방법으로 돌려본 결과입니다(2021~2023 KOSPI200, 수수료 0.015%, 슬리피지 0.02% 가정).
워크포워드 결과
- 검증 구간 수익률: +18.3%
- 샤프 비율: 1.8
- 최대 낙폭: -7.1%
- 검증 기간: 6개월 (단일 경로)
CPCV 결과
- 평균 수익률: +5.2%
- 샤프 비율 평균: 0.9
- 최대 낙폭 평균: -11.3%
- 조합 수: 120가지
워크포워드가 더 좋아 보이죠? 하지만 실전에선 CPCV가 예측한 범위 안에 들었어요. 워크포워드는 운 좋은 한 구간을 본 거고, CPCV는 다양한 시장 국면을 골고루 본 겁니다.
차이가 극명하게 드러난 건 변동성 돌파 전략이었어요. 워크포워드에선 +25%가 나왔지만 CPCV 평균은 -2.1%였습니다. 120개 조합 중 78개가 마이너스였죠.
흔한 실수와 해결법
제가 초기에 저지른 실수 세 가지입니다.
1. CPCV에서 n_splits를 너무 적게 잡음
처음엔 nsplits=10, testsplits=2로 설정했어요. 그랬더니 각 테스트 구간이 6개월씩이 되면서 조합 수가 45개밖에 안 나왔습니다. n_splits를 20으로 올리니 조합이 120개로 늘고, 각 검증 구간도 3개월로 적당해졌습니다.
2. purge와 embargo를 생략
purge 없이 돌렸더니 훈련 마지막 날과 검증 첫날이 붙어서 정보 누수가 생겼어요. 특히 일봉 종가 기반 지표는 다음 날 시가에 영향을 주니까, 최소 3~5일은 띄워야 합니다.
3. 메타-과최적화 함정
CPCV로 전략 30개를 테스트해서 가장 좋은 걸 골랐어요. 그런데 이것도 일종의 과최적화입니다. 30개 중 운 좋게 높은 점수를 받은 전략을 고른 거거든요(출처: ScienceDirect 연구 논문). 해결책은 완전히 손대지 않은 홀드아웃 구간(예: 최근 6개월)에서 한 번 더 검증하는 겁니다.
실전에선 어느 방법을 써야 할까요?
제 경험상 다음 기준으로 선택하면 됩니다.
- 빠른 프로토타이핑이나 데이터가 3년 미만이면 워크포워드를 씁니다.
- 실전 투입 직전 최종 검증이라면 CPCV는 필수입니다. 특히 변동성이 큰 전략일수록 효과가 커요.
- CPCV에서 평균과 표준편차를 동시에 봐야 합니다. 평균이 높아도 표준편차가 크면 불안정한 전략이에요.
두 방법 모두 메타-과최적화는 막지 못합니다. 최종 방어선은 홀드아웃 검증입니다. 저는 항상 최근 6개월을 완전히 손대지 않고 남겨둡니다.
자주 묻는 질문
Q. CPCV를 돌리면 시간이 얼마나 걸리나요? A. 제 환경(Python 3.10, 5년 일봉 데이터, 30개 파라미터 그리드)에서 워크포워드는 5분, CPCV는 2시간 걸렸어요. 조합이 120개니까 단순 계산으로 120배죠. 병렬 처리를 쓰면 30분 정도로 줄일 수 있습니다.
Q. 워크포워드로 좋은 결과가 나왔는데 CPCV에서 나쁘면 어느 걸 믿어야 하나요? A. CPCV를 믿으세요. 워크포워드가 좋은 건 운 좋은 검증 구간을 만난 겁니다. CPCV가 나쁘면 다양한 시장 국면에서 일관성이 없다는 뜻이에요. 실전에선 CPCV 예측에 가깝게 나올 확률이 높습니다.
Q. CPCV에서도 과최적화가 생기나요? A. 네, 메타-과최적화는 막지 못합니다. 여러 전략을 CPCV로 비교해서 가장 좋은 걸 고르면, 그것도 일종의 과최적화예요. 최종 방어는 홀드아웃 검증입니다. 완전히 손대지 않은 최근 데이터를 남겨두고 마지막에 한 번만 테스트하세요.