워크포워드 분석이란?
퀀트 트레이딩에서 워크포워드 분석(Walk-Forward Analysis, WFA)은 백테스트의 과최적화를 방지하는 가장 실전적인 검증 방법입니다. 단순 백테스트가 과거 데이터에 과적합(overfitting)되는 문제를 해결하기 위해, 데이터를 인샘플(In-Sample)과 아웃오브샘플(Out-of-Sample) 구간으로 나누어 반복 검증합니다.
일반적인 백테스트는 전체 기간에 대해 한 번만 최적화하므로, 미래 데이터에 대한 예측력이 떨어질 수 있습니다. 워크포워드 분석은 이 한계를 극복하여 실전 성과와 가장 가까운 결과를 제공합니다.
워크포워드 분석의 핵심 구조
워크포워드 분석은 다음과 같은 단계로 진행됩니다:
- 1단계: 전체 데이터를 시간순으로 여러 구간(Window)으로 분할
- 2단계: 각 구간에서 인샘플 데이터로 전략 파라미터 최적화
- 3단계: 최적화된 파라미터를 아웃오브샘플 구간에 적용하여 성과 측정
- 4단계: 윈도우를 앞으로 이동(roll forward)하며 반복
- 5단계: 모든 아웃오브샘플 결과를 연결하여 종합 성과 평가
이 과정에서 앵커드(Anchored) 방식과 롤링(Rolling) 방식 두 가지 변형이 존재합니다. 앵커드 방식은 시작점을 고정하고 인샘플 구간을 점점 넓히는 반면, 롤링 방식은 고정된 크기의 윈도우를 이동시킵니다.
파이썬으로 워크포워드 분석 구현하기
아래는 이동평균 크로스오버 전략에 워크포워드 분석을 적용하는 파이썬 코드입니다.
import numpy as np
import pandas as pd
from itertools import product
def generate_signals(df, short_window, long_window):
"""이동평균 크로스오버 시그널 생성"""
signals = pd.DataFrame(index=df.index)
signals['price'] = df['close']
signals['short_ma'] = df['close'].rolling(window=short_window).mean()
signals['long_ma'] = df['close'].rolling(window=long_window).mean()
signals['signal'] = 0
signals.loc[signals['short_ma'] > signals['long_ma'], 'signal'] = 1
signals.loc[signals['short_ma'] <= signals['long_ma'], 'signal'] = -1
signals['returns'] = df['close'].pct_change()
signals['strategy_returns'] = signals['signal'].shift(1) * signals['returns']
return signals
def optimize_parameters(df, short_range, long_range):
"""인샘플 구간에서 최적 파라미터 탐색"""
best_sharpe = -np.inf
best_params = (20, 60)
for short_w, long_w in product(short_range, long_range):
if short_w >= long_w:
continue
signals = generate_signals(df, short_w, long_w)
returns = signals['strategy_returns'].dropna()
if len(returns) < 30:
continue
sharpe = returns.mean() / returns.std() * np.sqrt(252)
if sharpe > best_sharpe:
best_sharpe = sharpe
best_params = (short_w, long_w)
return best_params, best_sharpe
def walk_forward_analysis(df, n_splits=5, in_sample_ratio=0.7,
short_range=range(5, 50, 5),
long_range=range(20, 120, 10)):
"""워크포워드 분석 실행"""
total_len = len(df)
window_size = total_len // n_splits
results = []
for i in range(n_splits):
# 구간 분할
start = i * window_size
end = min((i + 1) * window_size, total_len)
window_data = df.iloc[start:end]
split_point = int(len(window_data) * in_sample_ratio)
in_sample = window_data.iloc[:split_point]
out_sample = window_data.iloc[split_point:]
# 인샘플 최적화
best_params, is_sharpe = optimize_parameters(
in_sample, short_range, long_range
)
# 아웃오브샘플 적용
oos_signals = generate_signals(
out_sample, best_params[0], best_params[1]
)
oos_returns = oos_signals['strategy_returns'].dropna()
oos_sharpe = (oos_returns.mean() / oos_returns.std()
* np.sqrt(252)) if len(oos_returns) > 0 else 0
results.append({
'window': i + 1,
'in_sample_period': f"{in_sample.index[0]} ~ {in_sample.index[-1]}",
'out_sample_period': f"{out_sample.index[0]} ~ {out_sample.index[-1]}",
'best_short_ma': best_params[0],
'best_long_ma': best_params[1],
'is_sharpe': round(is_sharpe, 3),
'oos_sharpe': round(oos_sharpe, 3),
'oos_total_return': round(oos_returns.sum() * 100, 2)
})
return pd.DataFrame(results)
워크포워드 효율성 비율(WFE)
워크포워드 분석의 품질을 평가하는 핵심 지표가 워크포워드 효율성 비율(Walk-Forward Efficiency, WFE)입니다. 이 비율은 아웃오브샘플 성과를 인샘플 성과로 나눈 값입니다.
def calculate_wfe(results_df):
"""워크포워드 효율성 비율 계산"""
wfe_values = []
for _, row in results_df.iterrows():
if row['is_sharpe'] != 0:
wfe = row['oos_sharpe'] / row['is_sharpe'] * 100
else:
wfe = 0
wfe_values.append(round(wfe, 1))
results_df['wfe_pct'] = wfe_values
avg_wfe = np.mean(wfe_values)
return results_df, avg_wfe
WFE 해석 기준:
- 50% 이상: 우수한 전략 — 실전 적용 가능성 높음
- 30~50%: 양호한 전략 — 추가 검증 후 적용 권장
- 30% 미만: 과적합 위험 — 전략 재설계 필요
앵커드 vs 롤링 워크포워드 비교
def anchored_walk_forward(df, n_splits=5,
short_range=range(5, 50, 5),
long_range=range(20, 120, 10)):
"""앵커드 워크포워드: 시작점 고정, 인샘플 확장"""
total_len = len(df)
oos_size = total_len // (n_splits + 1)
results = []
for i in range(n_splits):
# 인샘플: 처음부터 현재 분할점까지 (점점 확장)
is_end = (i + 1) * oos_size + oos_size
in_sample = df.iloc[:is_end]
# 아웃오브샘플: 다음 구간
oos_start = is_end
oos_end = min(oos_start + oos_size, total_len)
out_sample = df.iloc[oos_start:oos_end]
if len(out_sample) < 20:
continue
best_params, is_sharpe = optimize_parameters(
in_sample, short_range, long_range
)
oos_signals = generate_signals(
out_sample, best_params[0], best_params[1]
)
oos_returns = oos_signals['strategy_returns'].dropna()
oos_sharpe = (oos_returns.mean() / oos_returns.std()
* np.sqrt(252)) if len(oos_returns) > 1 else 0
results.append({
'window': i + 1,
'method': 'anchored',
'is_size': len(in_sample),
'oos_size': len(out_sample),
'best_params': best_params,
'is_sharpe': round(is_sharpe, 3),
'oos_sharpe': round(oos_sharpe, 3)
})
return pd.DataFrame(results)
두 방식의 차이점을 정리하면:
- 롤링 방식: 최근 시장 상황에 더 민감하게 반응, 레짐 변화에 적응 가능
- 앵커드 방식: 데이터가 누적되어 더 안정적인 최적화, 하지만 오래된 데이터 영향 잔존
- 실전 팁: 시장 레짐이 자주 변하는 암호화폐는 롤링, 비교적 안정적인 주식 시장은 앵커드가 유리
실전 적용 시 주의사항
워크포워드 분석을 실전에 적용할 때 반드시 고려해야 할 사항들입니다.
- 충분한 데이터: 각 인샘플 구간에 최소 200거래일 이상의 데이터 확보 필요
- 거래 비용 반영: 아웃오브샘플 결과에 슬리피지와 수수료를 반드시 포함
- 다중 시장 테스트: 하나의 시장에서 좋은 WFE가 나와도 다른 시장에서 교차 검증
- 파라미터 안정성: 윈도우마다 최적 파라미터가 크게 변하면 전략의 강건성(robustness) 의심
- 표본 외 기간 비율: 아웃오브샘플 구간은 전체의 최소 20~30%를 유지
워크포워드 분석은 퀀트 백테스트 과적합 방지법과 함께 사용하면 더욱 효과적입니다. 또한 몬테카를로 포트폴리오 리스크 분석을 병행하여 전략의 위험을 다각도로 평가할 수 있습니다.
결론
워크포워드 분석은 단순 백테스트의 한계를 넘어 실전 트레이딩 성과를 사전에 예측할 수 있는 강력한 도구입니다. WFE 비율을 통해 전략의 실전 적용 가능성을 객관적으로 판단하고, 롤링과 앵커드 방식을 상황에 맞게 선택하세요. 과적합 없는 견고한 퀀트 전략을 만드는 첫걸음은 바로 워크포워드 분석입니다.