페어트레이딩이란?
페어트레이딩(Pairs Trading)은 통계적 차익거래(Statistical Arbitrage)의 대표 전략입니다. 상관관계가 높은 두 자산의 가격 스프레드가 평균에서 벗어났을 때, 고평가 자산을 매도하고 저평가 자산을 매수하여 스프레드가 다시 평균으로 회귀할 때 수익을 실현합니다. 시장 방향에 관계없이 수익을 추구하는 시장 중립(Market Neutral) 전략이라는 점이 핵심 장점입니다.
이 글에서는 파이썬으로 페어트레이딩 전략을 구현하는 전 과정을 다룹니다. 공적분 검정부터 시그널 생성, 백테스트, 리스크 관리까지 실전에서 바로 쓸 수 있는 코드를 제공합니다.
페어트레이딩의 핵심 원리: 공적분
단순 상관관계가 높다고 페어트레이딩이 가능한 것은 아닙니다. 중요한 것은 공적분(Cointegration) 관계입니다. 공적분이란 두 비정상(non-stationary) 시계열의 선형 결합이 정상(stationary) 시계열이 되는 관계를 말합니다.
- 상관관계: 두 자산의 수익률이 함께 움직이는 정도 → 시간에 따라 변할 수 있음
- 공적분: 두 자산의 가격 차이(스프레드)가 일정 범위 안에서 움직임 → 평균 회귀 보장
쉽게 비유하면, 줄에 묶인 두 마리 개와 같습니다. 각자 이리저리 돌아다니지만(비정상), 줄의 길이 때문에 일정 거리 이상 벌어지지 않습니다(정상).
파이썬 환경 설정
필요한 라이브러리를 설치합니다.
pip install numpy pandas statsmodels yfinance matplotlib scikit-learn
1단계: 페어 후보 선정과 공적분 검정
같은 섹터에서 페어 후보를 찾는 것이 일반적입니다. Engle-Granger 공적분 검정을 사용하여 통계적으로 유의미한 페어를 선별합니다.
import numpy as np
import pandas as pd
import yfinance as yf
from statsmodels.tsa.stattools import coint
from itertools import combinations
# 같은 섹터 종목 후보
tickers = ['AAPL', 'MSFT', 'GOOGL', 'META', 'AMZN', 'NVDA']
data = yf.download(tickers, start='2023-01-01', end='2025-12-31')['Close']
def find_cointegrated_pairs(data, significance=0.05):
"""공적분 검정으로 유의미한 페어 탐색"""
pairs = []
n = data.shape[1]
keys = data.columns
for i, j in combinations(range(n), 2):
s1 = data[keys[i]].dropna()
s2 = data[keys[j]].dropna()
idx = s1.index.intersection(s2.index)
score, pvalue, _ = coint(s1[idx], s2[idx])
if pvalue < significance:
pairs.append({
'asset1': keys[i],
'asset2': keys[j],
'pvalue': round(pvalue, 4),
'score': round(score, 2)
})
return pd.DataFrame(pairs).sort_values('pvalue')
result = find_cointegrated_pairs(data)
print(result)
p-value가 0.05 미만인 페어만 선택합니다. p-value가 낮을수록 공적분 관계가 강하며, 평균 회귀 가능성이 높습니다.
2단계: 스프레드 계산과 Z-Score 시그널
선정된 페어의 스프레드를 계산하고, Z-Score 기반 진입/청산 시그널을 만듭니다. 헤지 비율은 OLS 회귀로 구합니다.
from sklearn.linear_model import LinearRegression
def calculate_spread(s1, s2):
"""OLS 헤지 비율로 스프레드 계산"""
model = LinearRegression()
model.fit(s2.values.reshape(-1, 1), s1.values)
hedge_ratio = model.coef_[0]
spread = s1 - hedge_ratio * s2
return spread, hedge_ratio
def generate_signals(spread, window=60, entry_z=2.0, exit_z=0.5):
"""Z-Score 기반 매매 시그널 생성"""
mean = spread.rolling(window).mean()
std = spread.rolling(window).std()
zscore = (spread - mean) / std
signals = pd.DataFrame(index=spread.index)
signals['zscore'] = zscore
signals['position'] = 0
# 진입: |Z| > entry_z, 청산: |Z| < exit_z
signals.loc[zscore > entry_z, 'position'] = -1 # 숏 스프레드
signals.loc[zscore < -entry_z, 'position'] = 1 # 롱 스프레드
signals.loc[abs(zscore) < exit_z, 'position'] = 0 # 청산
# forward-fill로 포지션 유지
signals['position'] = signals['position'].replace(0, np.nan)
signals['position'] = signals['position'].ffill().fillna(0)
signals.loc[abs(zscore) < exit_z, 'position'] = 0
return signals
# 예시: 선정된 페어
asset1 = data['AAPL']
asset2 = data['MSFT']
spread, hedge_ratio = calculate_spread(asset1, asset2)
signals = generate_signals(spread)
print(f"헤지 비율: {hedge_ratio:.4f}")
print(f"총 거래 횟수: {signals['position'].diff().abs().sum() / 2:.0f}")
Z-Score가 +2 이상이면 스프레드가 과도하게 벌어진 것으로 판단해 숏 진입하고, -2 이하면 롱 진입합니다. 0.5 이내로 돌아오면 청산합니다.
3단계: 백테스트 엔진 구현
시그널 기반으로 실제 수익률을 계산하는 백테스트 코드입니다. 거래 비용과 슬리피지를 반영합니다.
def backtest_pairs(asset1, asset2, signals, hedge_ratio,
commission=0.001, slippage=0.0005):
"""페어트레이딩 백테스트"""
ret1 = asset1.pct_change()
ret2 = asset2.pct_change()
# 스프레드 수익률: 롱 asset1 + 숏 asset2 (or 반대)
spread_return = signals['position'].shift(1) * (ret1 - hedge_ratio * ret2)
# 거래 비용 차감
trades = signals['position'].diff().abs()
costs = trades * (commission + slippage)
net_return = spread_return - costs
# 성과 지표 계산
cumulative = (1 + net_return).cumprod()
total_return = cumulative.iloc[-1] - 1
annual_return = (1 + total_return) ** (252 / len(net_return)) - 1
sharpe = net_return.mean() / net_return.std() * np.sqrt(252)
max_dd = (cumulative / cumulative.cummax() - 1).min()
print(f"=== 백테스트 결과 ===")
print(f"총 수익률: {total_return:.2%}")
print(f"연환산 수익률: {annual_return:.2%}")
print(f"샤프 비율: {sharpe:.2f}")
print(f"최대 낙폭(MDD): {max_dd:.2%}")
return cumulative, net_return
cumulative, returns = backtest_pairs(asset1, asset2, signals, hedge_ratio)
백테스트 오버피팅 방지법에서 다룬 것처럼, 과최적화를 주의해야 합니다. 훈련 기간과 검증 기간을 반드시 분리하세요.
4단계: 롤링 공적분과 동적 헤지
공적분 관계는 시간이 지나면 약해질 수 있습니다. 롤링 윈도우로 공적분을 주기적으로 재검정하고, 헤지 비율도 동적으로 업데이트해야 합니다.
def rolling_cointegration(s1, s2, window=252, step=20):
"""롤링 공적분 검정 + 동적 헤지 비율"""
results = []
for end in range(window, len(s1), step):
start = end - window
seg1 = s1.iloc[start:end]
seg2 = s2.iloc[start:end]
_, pvalue, _ = coint(seg1, seg2)
model = LinearRegression()
model.fit(seg2.values.reshape(-1, 1), seg1.values)
results.append({
'date': s1.index[end],
'pvalue': pvalue,
'hedge_ratio': model.coef_[0],
'is_cointegrated': pvalue < 0.05
})
df = pd.DataFrame(results).set_index('date')
return df
rolling = rolling_cointegration(asset1, asset2)
coint_ratio = rolling['is_cointegrated'].mean()
print(f"공적분 유지 비율: {coint_ratio:.1%}")
공적분 유지 비율이 70% 이하로 떨어지면 해당 페어의 전략을 중단하는 것이 안전합니다.
5단계: 리스크 관리 규칙
페어트레이딩도 리스크 관리가 필수입니다. 다음 규칙을 반드시 적용하세요.
| 규칙 | 기준값 | 설명 |
|---|---|---|
| 손절 Z-Score | ±4.0 | 스프레드 발산 시 즉시 청산 |
| 최대 보유 기간 | 20영업일 | 회귀 실패 시 타임아웃 청산 |
| 포지션 크기 | 계좌의 5% | 단일 페어 리스크 제한 |
| 공적분 재검정 | 월 1회 | 관계 약화 시 전략 중단 |
| 일일 손실 한도 | -2% | 당일 전 포지션 청산 |
def apply_risk_rules(signals, spread, zscore,
stop_z=4.0, max_hold=20):
"""리스크 관리 규칙 적용"""
position = signals['position'].copy()
# 1) 손절: Z-Score 4 초과 시 강제 청산
position[abs(zscore) > stop_z] = 0
# 2) 최대 보유 기간 제한
hold_count = 0
for i in range(1, len(position)):
if position.iloc[i] != 0:
hold_count += 1
if hold_count > max_hold:
position.iloc[i] = 0
hold_count = 0
else:
hold_count = 0
return position
켈리 기준 자금 배분 전략을 참고하여 최적 포지션 크기를 결정할 수 있습니다.
실전 적용 시 주의사항
- 거래 비용: 페어트레이딩은 양쪽 포지션이므로 거래 비용이 2배입니다. 수수료가 낮은 브로커를 선택하세요.
- 공매도 제약: 한국 시장은 공매도 제한이 있어 선물/ETF를 활용하거나, 해외 시장에서 실행하는 것이 유리합니다.
- 체제 전환(Regime Change): M&A, 사업 구조 변경 등으로 공적분 관계가 영구히 깨질 수 있습니다.
- 유동성: 두 자산 모두 충분한 유동성이 있어야 합니다. 호가 스프레드가 넓으면 수익이 거래 비용에 잠식됩니다.
- 데이터 스누핑: 수백 개 페어를 검정하면 우연히 공적분으로 나올 수 있습니다. 경제적 논리가 뒷받침되는 페어를 우선하세요.
마무리
페어트레이딩은 시장 방향에 무관한 수익을 추구하는 강력한 퀀트 전략입니다. 핵심은 공적분 관계의 확인과 유지입니다. 단순히 상관관계만 보고 진입하면 실패할 확률이 높습니다. 롤링 공적분 검정으로 관계를 모니터링하고, 철저한 리스크 관리를 적용해야 안정적인 수익을 기대할 수 있습니다.
위 코드를 기반으로 자신만의 유니버스에서 페어를 탐색하고, 충분한 아웃오브샘플 검증을 거친 후 실전에 적용해 보세요.