페어 트레이딩이란?
페어 트레이딩(Pairs Trading)은 통계적으로 유사한 움직임을 보이는 두 자산의 가격 괴리를 이용하는 시장 중립 전략입니다. 한 자산을 매수하고 다른 자산을 매도해 방향성 리스크를 제거하면서, 두 자산의 스프레드가 평균으로 회귀할 때 수익을 얻습니다.
이 전략의 핵심은 공적분(Cointegration) 관계입니다. 단순 상관관계와 달리, 공적분은 두 시계열의 장기적 균형 관계를 의미합니다. 상관관계가 높아도 공적분이 없으면 스프레드가 발산할 수 있어 위험합니다.
상관관계 vs 공적분: 차이점
많은 초보 퀀트 투자자들이 상관관계와 공적분을 혼동합니다. 둘은 근본적으로 다른 개념입니다.
| 구분 | 상관관계 | 공적분 |
|---|---|---|
| 측정 대상 | 수익률의 동조성 | 가격 수준의 장기 균형 |
| 시계열 조건 | 정상성 불필요 | 개별 비정상 + 선형결합 정상 |
| 평균 회귀 | 보장하지 않음 | 스프레드 평균 회귀 보장 |
| 페어 트레이딩 적합성 | 낮음 | 높음 |
| 대표 검정 | 피어슨 상관계수 | Engle-Granger, Johansen |
예를 들어, 두 주식이 상관관계 0.95라도 공적분이 없다면 스프레드가 점점 벌어질 수 있습니다. 반대로 상관관계가 0.7 정도여도 공적분이 존재하면 안정적인 페어 트레이딩이 가능합니다.
파이썬으로 공적분 검정하기
파이썬 자동매매 시스템에서 공적분 페어를 찾는 방법을 살펴보겠습니다.
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import coint, adfuller
from itertools import combinations
class PairsFinder:
def __init__(self, significance=0.05):
self.significance = significance
def engle_granger_test(self, series_a: pd.Series,
series_b: pd.Series) -> dict:
"""Engle-Granger 공적분 검정"""
score, pvalue, _ = coint(series_a, series_b)
return {
'statistic': score,
'pvalue': pvalue,
'cointegrated': pvalue < self.significance
}
def find_pairs(self, price_df: pd.DataFrame) -> list:
"""모든 자산 조합에서 공적분 페어 탐색"""
symbols = price_df.columns.tolist()
pairs = []
for s1, s2 in combinations(symbols, 2):
result = self.engle_granger_test(
price_df[s1], price_df[s2]
)
if result['cointegrated']:
pairs.append({
'asset_a': s1,
'asset_b': s2,
'pvalue': result['pvalue'],
'statistic': result['statistic']
})
# p-value 기준 정렬
pairs.sort(key=lambda x: x['pvalue'])
return pairs
def hedge_ratio(self, series_a: pd.Series,
series_b: pd.Series) -> float:
"""OLS 회귀로 헤지 비율 계산"""
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(series_b.values.reshape(-1, 1),
series_a.values)
return model.coef_[0]
스프레드 계산과 Z-Score 시그널
공적분 페어를 찾은 후, 스프레드의 Z-Score를 기반으로 매매 시그널을 생성합니다.
class SpreadTrader:
def __init__(self, entry_z=2.0, exit_z=0.5,
stop_z=3.5, lookback=60):
"""
entry_z: 진입 Z-Score 임계값
exit_z: 청산 Z-Score 임계값
stop_z: 손절 Z-Score 임계값
lookback: 이동 평균/표준편차 계산 기간
"""
self.entry_z = entry_z
self.exit_z = exit_z
self.stop_z = stop_z
self.lookback = lookback
def compute_spread(self, prices_a: pd.Series,
prices_b: pd.Series,
hedge_ratio: float) -> pd.Series:
"""헤지 비율 적용 스프레드 계산"""
return prices_a - hedge_ratio * prices_b
def zscore(self, spread: pd.Series) -> pd.Series:
"""롤링 Z-Score 계산"""
mean = spread.rolling(self.lookback).mean()
std = spread.rolling(self.lookback).std()
return (spread - mean) / std
def generate_signals(self, spread: pd.Series) -> pd.Series:
"""Z-Score 기반 매매 시그널 생성"""
z = self.zscore(spread)
signals = pd.Series(0, index=spread.index)
# 롱 스프레드: Z < -entry (A 저평가)
signals[z < -self.entry_z] = 1
# 숏 스프레드: Z > entry (A 고평가)
signals[z > self.entry_z] = -1
# 청산: |Z| < exit
signals[abs(z) < self.exit_z] = 0
# 손절: |Z| > stop
signals[abs(z) > self.stop_z] = 0
return signals
Kalman 필터로 동적 헤지 비율 추정
고정 헤지 비율은 시간이 지나면 변할 수 있습니다. Kalman 필터를 사용하면 헤지 비율을 실시간으로 업데이트할 수 있습니다.
class KalmanHedgeRatio:
def __init__(self, delta=1e-4, ve=1e-3):
self.delta = delta # 상태 전이 공분산
self.ve = ve # 관측 노이즈
def estimate(self, prices_a: pd.Series,
prices_b: pd.Series) -> pd.Series:
"""Kalman 필터로 시변 헤지 비율 추정"""
n = len(prices_a)
hedge_ratios = np.zeros(n)
# 초기화
theta = 0.0 # 헤지 비율 (상태)
P = 1.0 # 상태 공분산
R = self.ve # 관측 노이즈
for i in range(n):
x = prices_b.iloc[i]
y = prices_a.iloc[i]
# 예측
P_pred = P + self.delta
# 업데이트
y_hat = theta * x
error = y - y_hat
S = x * P_pred * x + R
K = P_pred * x / S # 칼만 이득
theta = theta + K * error
P = (1 - K * x) * P_pred
hedge_ratios[i] = theta
return pd.Series(hedge_ratios, index=prices_a.index)
Kalman 필터를 적용하면 구조적 변화(structural break)에도 빠르게 적응할 수 있어, 장기 운용에서 고정 OLS 대비 우수한 성과를 보입니다.
백테스트와 성과 평가
성과 어트리뷰션 분석과 결합해 페어 트레이딩의 실제 수익성을 검증합니다.
def backtest_pairs(prices_a, prices_b, hedge_ratio,
entry_z=2.0, exit_z=0.5, cost_bps=10):
"""페어 트레이딩 백테스트"""
trader = SpreadTrader(entry_z=entry_z, exit_z=exit_z)
spread = trader.compute_spread(prices_a, prices_b,
hedge_ratio)
signals = trader.generate_signals(spread)
# 스프레드 수익률
spread_returns = spread.pct_change()
strategy_returns = signals.shift(1) * spread_returns
# 거래비용 차감
trades = signals.diff().abs()
costs = trades * cost_bps / 10000
net_returns = strategy_returns - costs
# 성과 지표
annual_return = net_returns.mean() * 252
annual_vol = net_returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol if annual_vol > 0 else 0
max_dd = (net_returns.cumsum() -
net_returns.cumsum().cummax()).min()
return {
'annual_return': f"{annual_return:.2%}",
'annual_vol': f"{annual_vol:.2%}",
'sharpe_ratio': f"{sharpe:.2f}",
'max_drawdown': f"{max_dd:.2%}",
'total_trades': int(trades.sum()),
'win_rate': f"{(strategy_returns > 0).mean():.1%}"
}
실전 운용 체크리스트
페어 트레이딩을 실제 자동매매에 적용할 때 반드시 확인해야 할 사항들입니다:
- 공적분 재검정: 공적분 관계는 영원하지 않습니다. 최소 월 1회 재검정하고, 깨진 페어는 즉시 청산하세요.
- 유동성 확인: 양쪽 자산 모두 충분한 거래량이 있어야 슬리피지를 최소화할 수 있습니다.
- 섹터 논리: 같은 산업 내 기업(예: 삼성전자-SK하이닉스)이 공적분 관계를 가질 확률이 높고, 논리적 근거도 명확합니다.
- 동시 체결: 두 레그를 가능한 동시에 체결해야 합니다. 한쪽만 체결되면 방향성 리스크에 노출됩니다.
- 자금 관리: 여러 페어에 분산 투자해 개별 페어 실패 리스크를 줄이세요.
- 거래비용: 잦은 진입/청산으로 수수료가 누적됩니다. 진입 Z-Score를 높이면 거래 빈도를 줄일 수 있습니다.
암호화폐 페어 트레이딩
암호화폐 시장은 24시간 운영되고 공매도가 용이해 페어 트레이딩에 적합한 환경을 제공합니다. BTC-ETH, BNB-SOL 같은 시가총액 상위 페어나, 동일 섹터 토큰(DeFi 프로토콜 간) 등에서 공적분 관계를 탐색할 수 있습니다.
다만 암호화폐의 높은 변동성과 구조적 변화 빈도를 고려해, 룩백 기간을 주식보다 짧게(20~30일) 설정하고 재검정 주기도 주간 단위로 단축하는 것이 좋습니다.
마무리
공적분 기반 페어 트레이딩은 시장 방향에 관계없이 수익을 추구할 수 있는 대표적인 시장 중립 전략입니다. Engle-Granger 검정으로 페어를 발굴하고, Z-Score 시그널로 진입/청산하며, Kalman 필터로 헤지 비율을 동적 관리하면 체계적인 자동매매 시스템을 구축할 수 있습니다. 파이썬 생태계의 statsmodels, sklearn 등을 활용하면 비교적 적은 코드로 강력한 페어 트레이딩 엔진을 만들 수 있습니다.