파이썬 백테스트 시작하기: 첫 전략을 검증하는 방법
백테스트가 뭔지, 왜 필요한지, 파이썬으로 어떻게 하는지 처음부터 설명합니다. 이동평균 전략으로 실습하며 수익률, 샤프비율, 최대낙폭까지 계산합니다.
백테스트가 뭔가요
백테스트(Backtest)는 투자 전략을 과거 데이터에 적용해서 수익이 났는지 확인하는 것입니다.
예를 들어 “20일 이동평균이 60일 이동평균을 위로 돌파하면 매수”라는 규칙을 만들었다면, 이 규칙을 과거 5년 데이터에 적용해서 실제로 돈을 벌었을지 시뮬레이션합니다.
왜 필요한가?
- 머릿속으로 “이거 괜찮겠다”는 생각과 실제 결과는 다릅니다
- 수수료, 슬리피지를 반영하면 수익이 사라지는 전략이 많습니다
- 데이터로 검증하지 않은 전략은 도박과 같습니다
준비
pip install pandas yfinance matplotlib numpy
1단계: 데이터 준비
import pandas as pd
import numpy as np
import yfinance as yf
# 비트코인 일봉 3년
df = yf.download("BTC-USD", period="3y")
df = df[['Close']].copy()
df.columns = ['close']
print(f"데이터: {len(df)}일, {df.index[0].date()} ~ {df.index[-1].date()}")
2단계: 전략 정의
이동평균 교차 전략으로 시작합니다.
# 이동평균 계산
df['ma20'] = df['close'].rolling(20).mean()
df['ma60'] = df['close'].rolling(60).mean()
# 매매 신호 생성
# 1 = 매수(보유), -1 = 매도(현금), 0 = 대기
df['signal'] = 0
df.loc[df['ma20'] > df['ma60'], 'signal'] = 1 # MA20 > MA60 → 매수
df.loc[df['ma20'] <= df['ma60'], 'signal'] = -1 # MA20 ≤ MA60 → 매도
# 실제 포지션 (다음 날 시가에 진입한다고 가정)
df['position'] = df['signal'].shift(1)
print(f"매수 신호 일수: {(df['position'] == 1).sum()}")
print(f"매도 신호 일수: {(df['position'] == -1).sum()}")
3단계: 수익률 계산
# 일별 수익률
df['daily_return'] = df['close'].pct_change()
# 전략 수익률 (보유 시에만 수익 반영)
df['strategy_return'] = df['position'] * df['daily_return']
# 수수료 반영 (매매 시 0.1%)
df['trade'] = df['position'].diff().abs() # 포지션 변경 횟수
df['strategy_return'] -= df['trade'] * 0.001 # 수수료 차감
# 누적 수익률
df['cumulative_market'] = (1 + df['daily_return']).cumprod()
df['cumulative_strategy'] = (1 + df['strategy_return']).cumprod()
# 결과
market_return = df['cumulative_market'].iloc[-1] - 1
strategy_return = df['cumulative_strategy'].iloc[-1] - 1
print(f"시장 수익률 (바이앤홀드): {market_return:.2%}")
print(f"전략 수익률: {strategy_return:.2%}")
4단계: 성과 지표 계산
수익률만 보면 안 됩니다. 얼마나 위험했는지도 봐야 합니다.
샤프비율
수익을 위험(변동성)으로 나눈 값입니다. 1 이상이면 괜찮고, 2 이상이면 좋습니다.
# 연간화된 샤프비율
returns = df['strategy_return'].dropna()
sharpe = returns.mean() / returns.std() * np.sqrt(252)
print(f"샤프비율: {sharpe:.2f}")
최대낙폭 (MDD)
최고점에서 최저점까지 얼마나 떨어졌는지입니다. 투자자가 실제로 견뎌야 하는 고통의 크기입니다.
cumulative = df['cumulative_strategy']
peak = cumulative.cummax()
drawdown = (cumulative - peak) / peak
mdd = drawdown.min()
print(f"최대낙폭(MDD): {mdd:.2%}")
승률과 평균 손익
# 매매별 수익률
trades = df[df['trade'] > 0]['strategy_return']
win_rate = (trades > 0).mean()
avg_win = trades[trades > 0].mean()
avg_loss = trades[trades < 0].mean()
print(f"승률: {win_rate:.1%}")
print(f"평균 수익: {avg_win:.3%}")
print(f"평균 손실: {avg_loss:.3%}")
5단계: 차트로 확인
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), sharex=True)
# 누적 수익률 비교
ax1.plot(df.index, df['cumulative_market'], label='시장 (바이앤홀드)')
ax1.plot(df.index, df['cumulative_strategy'], label='전략')
ax1.set_title('누적 수익률 비교')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 낙폭
ax2.fill_between(df.index, drawdown, 0, alpha=0.3, color='red')
ax2.set_title('전략 낙폭 (Drawdown)')
ax2.set_ylabel('낙폭 (%)')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('backtest_result.png', dpi=150)
plt.show()
백테스트 결과를 해석할 때 주의할 점
과적합
파라미터(20일, 60일)를 바꿔가며 수익이 최대인 조합을 찾으면, 그건 과거에만 맞는 전략일 가능성이 높습니다. 파라미터를 조금만 바꿔도 결과가 크게 달라지면 위험 신호입니다.
미래 정보 사용 금지
오늘 종가로 오늘 매매한다고 가정하면 안 됩니다. 실제로는 오늘 종가를 알 수 없으니, 항상 신호 발생 다음 날 시가에 진입한다고 가정해야 합니다. shift(1)을 빠뜨리면 룩어헤드 바이어스가 생깁니다.
수수료와 슬리피지
수수료를 빼지 않으면 수익이 크게 부풀려집니다. 특히 자주 매매하는 전략일수록 수수료 영향이 큽니다.
생존 편향
“비트코인으로 백테스트하면 잘 나온다”는 건 비트코인이 살아남았기 때문입니다. 망한 코인으로 백테스트하면 결과가 다릅니다.
다음 단계
이 가이드는 가장 기본적인 수동 백테스트입니다. 더 발전시키려면:
- 백테스트 프레임워크 사용: Backtrader, Vectorbt, Zipline
- Walk-forward 검증: 학습 기간과 테스트 기간을 나눠서 반복 검증
- 여러 자산 포트폴리오: 한 종목이 아닌 다수 종목 전략
- 리스크 관리: 포지션 사이징, 손절 로직
백테스트는 “돈을 벌 수 있는 전략”을 찾는 도구가 아니라, “이 전략이 통하지 않는다”를 확인하는 도구입니다. 통과하지 못한 전략을 걸러내는 것이 핵심입니다.