평균회귀 자동매매 전략

평균회귀(Mean Reversion) 자동매매란?

평균회귀 전략은 가격이 평균에서 벗어나면 다시 돌아온다는 통계적 성질을 이용한 퀀트 트레이딩 기법입니다. 주가, 코인, 환율 등 거의 모든 금융자산에 적용할 수 있으며, 모멘텀 전략과 반대되는 개념으로 과매수 시 매도, 과매도 시 매수하는 역추세 방식입니다.

특히 자동매매 봇으로 구현하면 감정 없이 기계적으로 진입·청산할 수 있어, 개인 투자자에게도 강력한 무기가 됩니다. 이 글에서는 파이썬으로 평균회귀 자동매매 시스템을 구축하는 전 과정을 다룹니다.

평균회귀 전략의 핵심 원리

평균회귀의 수학적 근거는 정상성(Stationarity)입니다. 정상 시계열은 평균과 분산이 시간에 따라 일정하므로, 가격이 평균에서 크게 벗어나면 되돌아올 확률이 높습니다.

구분 모멘텀 전략 평균회귀 전략
시장 가정 추세가 지속된다 가격이 평균으로 돌아온다
진입 시점 돌파 시 매수 과매도 시 매수
유리한 장세 강한 추세장 횡보·박스권
리스크 횡보장 휩소 추세장 역행 손실

핵심 지표로는 볼린저 밴드, Z-Score, RSI, 켈트너 채널 등이 있으며, 이들을 조합하면 신호의 신뢰도를 높일 수 있습니다.

Z-Score 기반 평균회귀 시그널

Z-Score는 현재 가격이 이동평균에서 표준편차 몇 배만큼 떨어져 있는지 나타냅니다. 절대값이 2 이상이면 통계적으로 극단 영역이므로 회귀 가능성이 높습니다.

import numpy as np
import pandas as pd

def calculate_zscore(series: pd.Series, window: int = 20) -> pd.Series:
    """이동평균 대비 Z-Score 계산"""
    mean = series.rolling(window=window).mean()
    std = series.rolling(window=window).std()
    zscore = (series - mean) / std
    return zscore

def generate_signals(df: pd.DataFrame, 
                     entry_z: float = -2.0, 
                     exit_z: float = 0.0) -> pd.DataFrame:
    """Z-Score 기반 매매 시그널 생성"""
    df['zscore'] = calculate_zscore(df['close'])
    
    df['signal'] = 0
    df.loc[df['zscore'] <= entry_z, 'signal'] = 1   # 과매도 → 매수
    df.loc[df['zscore'] >= -entry_z, 'signal'] = -1  # 과매수 → 매도
    df.loc[abs(df['zscore']) <= abs(exit_z), 'signal'] = 0  # 평균 복귀 → 청산
    
    return df

위 코드에서 entry_z = -2.0은 가격이 평균보다 2σ 아래일 때 매수, 반대로 +2σ일 때 매도합니다. exit_z = 0은 평균으로 돌아오면 포지션을 청산합니다.

볼린저 밴드 + Z-Score 복합 전략

Z-Score 단독보다 볼린저 밴드와 결합하면 거짓 신호를 크게 줄일 수 있습니다. 가격이 볼린저 하단 밴드 아래이면서 Z-Score도 -2 이하일 때만 진입하는 방식입니다.

def bollinger_zscore_strategy(df: pd.DataFrame, 
                               bb_window: int = 20, 
                               bb_std: float = 2.0,
                               z_threshold: float = -2.0) -> pd.DataFrame:
    """볼린저 밴드 + Z-Score 복합 전략"""
    # 볼린저 밴드 계산
    df['bb_mid'] = df['close'].rolling(bb_window).mean()
    df['bb_upper'] = df['bb_mid'] + bb_std * df['close'].rolling(bb_window).std()
    df['bb_lower'] = df['bb_mid'] - bb_std * df['close'].rolling(bb_window).std()
    
    # Z-Score
    df['zscore'] = calculate_zscore(df['close'], bb_window)
    
    # 복합 시그널: 볼린저 하단 이탈 AND Z-Score 극단
    df['signal'] = 0
    buy_condition = (df['close'] < df['bb_lower']) & (df['zscore'] <= z_threshold)
    sell_condition = (df['close'] > df['bb_upper']) & (df['zscore'] >= -z_threshold)
    exit_condition = abs(df['zscore']) < 0.5
    
    df.loc[buy_condition, 'signal'] = 1
    df.loc[sell_condition, 'signal'] = -1
    df.loc[exit_condition, 'signal'] = 0
    
    return df

정상성 검정: ADF 테스트

평균회귀 전략을 적용하기 전, 해당 자산이 실제로 평균회귀 성질을 갖는지 검증해야 합니다. ADF(Augmented Dickey-Fuller) 테스트가 가장 널리 쓰입니다.

from statsmodels.tsa.stattools import adfuller

def check_mean_reversion(series: pd.Series, significance: float = 0.05) -> dict:
    """ADF 테스트로 평균회귀 가능성 검정"""
    result = adfuller(series.dropna())
    
    return {
        'adf_statistic': result[0],
        'p_value': result[1],
        'is_stationary': result[1] < significance,
        'half_life': calculate_half_life(series)
    }

def calculate_half_life(series: pd.Series) -> float:
    """평균회귀 반감기 계산 (Ornstein-Uhlenbeck)"""
    lag = series.shift(1).dropna()
    delta = series.diff().dropna()
    
    from sklearn.linear_model import LinearRegression
    model = LinearRegression()
    model.fit(lag.values.reshape(-1, 1), delta.values)
    
    half_life = -np.log(2) / model.coef_[0]
    return max(half_life, 1)

반감기(Half-Life)는 가격이 평균까지 절반 돌아오는 데 걸리는 기간입니다. 반감기가 너무 길면(30일 이상) 실전 트레이딩에 비효율적이고, 너무 짧으면(1~2일) 수수료 대비 수익이 낮을 수 있습니다. 5~20일이 이상적입니다.

자동매매 봇 구현

실전 자동매매 봇은 시그널 생성 → 리스크 관리 → 주문 실행의 파이프라인으로 동작합니다. 아래는 ccxt 라이브러리를 활용한 코인 자동매매 예시입니다.

import ccxt
import time

class MeanReversionBot:
    def __init__(self, exchange_id: str, symbol: str, 
                 timeframe: str = '1h', lookback: int = 20):
        self.exchange = getattr(ccxt, exchange_id)({
            'apiKey': 'YOUR_API_KEY',
            'secret': 'YOUR_SECRET',
        })
        self.symbol = symbol
        self.timeframe = timeframe
        self.lookback = lookback
        self.position = 0  # 현재 포지션
    
    def fetch_data(self) -> pd.DataFrame:
        """OHLCV 데이터 조회"""
        ohlcv = self.exchange.fetch_ohlcv(
            self.symbol, self.timeframe, limit=self.lookback * 3
        )
        df = pd.DataFrame(ohlcv, columns=['ts','open','high','low','close','vol'])
        return df
    
    def execute(self):
        """메인 실행 루프"""
        df = self.fetch_data()
        df = bollinger_zscore_strategy(df)
        
        latest = df.iloc[-1]
        
        if latest['signal'] == 1 and self.position <= 0:
            # 매수 진입
            amount = self.calculate_position_size()
            self.exchange.create_market_buy_order(self.symbol, amount)
            self.position = 1
            
        elif latest['signal'] == -1 and self.position >= 0:
            # 매도 진입
            amount = self.calculate_position_size()
            self.exchange.create_market_sell_order(self.symbol, amount)
            self.position = -1
            
        elif latest['signal'] == 0 and self.position != 0:
            # 포지션 청산
            self.close_position()
            self.position = 0

리스크 관리: 손절과 포지션 사이징

평균회귀 전략의 최대 위험은 평균이 이동하는 구조적 변화(레짐 전환)입니다. 반드시 손절을 설정해야 합니다.

리스크 관리 요소 권장 설정 설명
손절 Z-Score ±3.0σ 진입 후 추가 1σ 이탈 시 손절
최대 포지션 비율 총 자산의 5~10% 단일 포지션 리스크 제한
최대 동시 포지션 3~5개 분산 투자 효과
최대 보유 기간 반감기 × 3 회귀 실패 시 타임아웃 청산

켈리 기준(Kelly Criterion)을 활용한 포지션 사이징도 효과적입니다. 승률과 손익비를 기반으로 최적 베팅 비율을 계산합니다.

def kelly_fraction(win_rate: float, avg_win: float, avg_loss: float) -> float:
    """켈리 기준 최적 포지션 비율"""
    if avg_loss == 0:
        return 0
    b = avg_win / abs(avg_loss)  # 손익비
    f = (win_rate * b - (1 - win_rate)) / b
    return max(0, min(f * 0.5, 0.25))  # Half-Kelly, 최대 25%

백테스트 결과 해석 시 주의점

평균회귀 전략의 백테스트에서 흔히 발생하는 함정들이 있습니다:

  • 미래 정보 편향(Look-Ahead Bias): 이동평균 계산 시 미래 데이터가 포함되지 않도록 주의
  • 생존 편향: 상장폐지된 종목을 제외하면 수익률이 과대평가됨
  • 거래비용 무시: 평균회귀는 매매 빈도가 높아 수수료 영향이 큼
  • 레짐 변화 무시: 횡보장에서만 유효하므로 추세장 필터 필수

관련하여 백테스트 과최적화 방지법시장 레짐 감지 퀀트 전략도 함께 참고하시면 더 견고한 시스템을 구축할 수 있습니다.

실전 적용 팁

평균회귀 자동매매를 실전에 적용할 때 기억해야 할 핵심 포인트입니다:

  1. 페어 트레이딩과 결합: 단일 자산보다 상관관계 높은 두 자산의 스프레드에 적용하면 정상성이 더 높음
  2. 다중 타임프레임 확인: 1시간봉에서 과매도여도 일봉이 하락 추세면 진입 보류
  3. 변동성 필터: ATR이 극단적으로 높을 때는 레짐 전환 가능성이 있으므로 진입 자제
  4. 점진적 진입: Z-Score -2에서 전량 매수 대신, -2, -2.5, -3에서 분할 매수
  5. ADF 재검정: 주기적으로(월 1회) 정상성을 재검증하여 전략 유효성 확인

평균회귀 전략은 단순하지만, 정상성 검증 → 시그널 설계 → 리스크 관리 → 자동화라는 체계적 프로세스를 따르면 안정적인 수익원이 될 수 있습니다. 특히 횡보장이 길어지는 구간에서 모멘텀 전략의 보완재로 활용하면 포트폴리오 전체의 샤프 비율을 개선할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux