퀀트 팩터 로테이션 전략

팩터 로테이션이란?

팩터 로테이션(Factor Rotation)은 시장 국면에 따라 유리한 투자 팩터를 동적으로 전환하는 퀀트 전략입니다. 가치(Value), 모멘텀(Momentum), 퀄리티(Quality), 저변동성(Low Volatility) 등 다양한 팩터는 시장 환경에 따라 성과가 크게 달라집니다. 팩터 로테이션은 이 순환 패턴을 포착하여 항상 가장 유리한 팩터에 가중치를 집중시키는 것이 핵심입니다.

단일 팩터 전략은 특정 시기에 장기간 부진할 수 있습니다. 예를 들어 가치 팩터는 2010~2020년 저금리 성장주 랠리에서 10년 가까이 언더퍼폼했습니다. 팩터 로테이션은 이러한 팩터 드로다운 리스크를 줄이면서 복합적인 알파를 추구하는 전략입니다.

핵심 팩터 4가지와 시장 국면

팩터 로테이션에서 주로 활용하는 4대 팩터와 각 팩터가 강세를 보이는 시장 국면을 정리합니다.

팩터 정의 강세 국면 약세 국면
가치(Value) 저PER, 저PBR 종목 경기 회복기, 금리 상승기 저금리 성장주 랠리
모멘텀(Momentum) 최근 수익률 상위 종목 추세 장세, 강세장 중반 급격한 추세 반전기
퀄리티(Quality) 고ROE, 저부채 종목 경기 둔화기, 불확실성 증가 투기적 강세장
저변동성(Low Vol) 변동성 하위 종목 약세장, 고변동성 국면 강세장 초중반

시장 국면 판별 모델

팩터 로테이션의 성패는 현재 시장 국면을 얼마나 정확히 판별하느냐에 달려 있습니다. 대표적인 국면 판별 방법 3가지를 파이썬으로 구현합니다.

1. 경기 사이클 기반 (PMI + 금리)

import pandas as pd
import numpy as np

class MarketRegimeDetector:
    """경제 지표 기반 시장 국면 판별"""
    
    REGIMES = {
        'recovery':  {'value': 0.35, 'momentum': 0.30, 
                      'quality': 0.20, 'low_vol': 0.15},
        'expansion': {'value': 0.15, 'momentum': 0.40, 
                      'quality': 0.25, 'low_vol': 0.20},
        'slowdown':  {'value': 0.20, 'momentum': 0.15, 
                      'quality': 0.40, 'low_vol': 0.25},
        'recession': {'value': 0.25, 'momentum': 0.10, 
                      'quality': 0.30, 'low_vol': 0.35},
    }
    
    def detect_regime(self, pmi: float, pmi_delta: float, 
                      rate_delta: float) -> str:
        """PMI 수준·변화율 + 금리 변화로 국면 판별"""
        if pmi > 50 and pmi_delta > 0:
            return 'expansion'
        elif pmi > 50 and pmi_delta <= 0:
            return 'slowdown'
        elif pmi <= 50 and pmi_delta > 0:
            return 'recovery'
        else:
            return 'recession'
    
    def get_weights(self, regime: str) -> dict:
        """국면별 팩터 가중치 반환"""
        return self.REGIMES[regime]

2. 이동평균 크로스오버 기반

경제 지표 접근이 어려운 경우, 가격 데이터만으로 국면을 판별할 수 있습니다.

def price_regime(prices: pd.Series, 
                 short_window=50, long_window=200) -> str:
    """이동평균 기반 시장 국면 판별"""
    sma_short = prices.rolling(short_window).mean().iloc[-1]
    sma_long = prices.rolling(long_window).mean().iloc[-1]
    current = prices.iloc[-1]
    
    sma_short_prev = prices.rolling(short_window).mean().iloc[-21]
    
    if current > sma_long and sma_short > sma_short_prev:
        return 'expansion'    # 상승 추세 + 가속
    elif current > sma_long and sma_short <= sma_short_prev:
        return 'slowdown'     # 상승 추세 + 감속
    elif current <= sma_long and sma_short > sma_short_prev:
        return 'recovery'     # 하락 추세 + 반등 시작
    else:
        return 'recession'    # 하락 추세 + 가속

3. 변동성 레짐 기반

VIX 또는 실현 변동성을 활용한 방법으로, 몬테카를로 시뮬레이션과 함께 활용하면 더욱 정교한 리스크 추정이 가능합니다.

def volatility_regime(returns: pd.Series, 
                      lookback=60) -> str:
    """실현 변동성 기반 레짐 판별"""
    current_vol = returns.tail(lookback).std() * np.sqrt(252)
    historical_vol = returns.std() * np.sqrt(252)
    
    vol_ratio = current_vol / historical_vol
    
    if vol_ratio < 0.8:
        return 'low_vol_regime'     # 저변동성 → 모멘텀 유리
    elif vol_ratio > 1.3:
        return 'high_vol_regime'    # 고변동성 → 퀄리티/저변동성
    else:
        return 'normal_regime'      # 보통 → 균등 배분

팩터 로테이션 엔진 구현

위 국면 판별을 기반으로 실제 포트폴리오 리밸런싱을 수행하는 로테이션 엔진을 구현합니다.

from dataclasses import dataclass
from typing import Dict, List
import logging

logger = logging.getLogger(__name__)

@dataclass
class FactorETF:
    """팩터별 대표 ETF 매핑"""
    factor: str
    ticker: str
    name: str

# 한국 시장 팩터 ETF 예시
FACTOR_ETFS = [
    FactorETF('value', '292150', 'TIGER 가치주'),
    FactorETF('momentum', '147970', 'TIGER 모멘텀'),
    FactorETF('quality', '278420', 'TIGER 퀄리티'),
    FactorETF('low_vol', '174350', 'TIGER 로우볼'),
]

class FactorRotationEngine:
    def __init__(self, detector: MarketRegimeDetector,
                 rebalance_freq: int = 21):
        self.detector = detector
        self.rebalance_freq = rebalance_freq  # 영업일 기준
        self.current_weights: Dict[str, float] = {}
        self.days_since_rebalance = 0
    
    def should_rebalance(self) -> bool:
        """리밸런싱 시점 판단"""
        self.days_since_rebalance += 1
        return self.days_since_rebalance >= self.rebalance_freq
    
    def calculate_target_weights(self, pmi, pmi_delta, 
                                  rate_delta) -> Dict[str, float]:
        """목표 가중치 계산"""
        regime = self.detector.detect_regime(
            pmi, pmi_delta, rate_delta
        )
        raw_weights = self.detector.get_weights(regime)
        
        logger.info(f"시장 국면: {regime}")
        logger.info(f"목표 가중치: {raw_weights}")
        
        return raw_weights
    
    def smooth_transition(self, target: Dict[str, float],
                          alpha: float = 0.3) -> Dict[str, float]:
        """급격한 전환 방지를 위한 지수이동평균 스무딩"""
        if not self.current_weights:
            self.current_weights = target.copy()
            return target
        
        smoothed = {}
        for factor, target_w in target.items():
            current_w = self.current_weights.get(factor, 0.25)
            smoothed[factor] = current_w + alpha * (target_w - current_w)
        
        # 가중치 합 = 1 정규화
        total = sum(smoothed.values())
        smoothed = {k: v/total for k, v in smoothed.items()}
        
        self.current_weights = smoothed
        self.days_since_rebalance = 0
        return smoothed
    
    def generate_orders(self, target_weights: Dict[str, float],
                        portfolio_value: float) -> List[dict]:
        """팩터 ETF 매매 주문 생성"""
        orders = []
        for etf in FACTOR_ETFS:
            weight = target_weights.get(etf.factor, 0)
            target_amount = portfolio_value * weight
            orders.append({
                'ticker': etf.ticker,
                'name': etf.name,
                'factor': etf.factor,
                'weight': round(weight, 4),
                'target_amount': round(target_amount),
            })
        return orders

백테스트: 팩터 로테이션 vs 정적 배분

팩터 로테이션의 실효성을 검증하기 위한 간단한 백테스트 프레임워크입니다.

def backtest_rotation(factor_returns: pd.DataFrame,
                      regimes: pd.Series,
                      detector: MarketRegimeDetector,
                      rebalance_freq: int = 21) -> pd.Series:
    """팩터 로테이션 백테스트"""
    portfolio_returns = []
    current_weights = {f: 0.25 for f in factor_returns.columns}
    
    for i, date in enumerate(factor_returns.index):
        # 리밸런싱 시점
        if i % rebalance_freq == 0 and i > 0:
            regime = regimes.loc[date]
            target = detector.get_weights(regime)
            # 스무딩 적용
            for f in current_weights:
                current_weights[f] += 0.3 * (
                    target[f] - current_weights[f]
                )
            total = sum(current_weights.values())
            current_weights = {
                k: v/total for k, v in current_weights.items()
            }
        
        # 일별 수익률 계산
        daily_ret = sum(
            current_weights[f] * factor_returns.loc[date, f]
            for f in current_weights
        )
        portfolio_returns.append(daily_ret)
    
    return pd.Series(portfolio_returns, index=factor_returns.index)

# 성과 비교
def compare_strategies(rotation_returns, equal_returns):
    """전략 성과 비교"""
    stats = {}
    for name, rets in [('로테이션', rotation_returns), 
                        ('균등배분', equal_returns)]:
        annual_ret = rets.mean() * 252
        annual_vol = rets.std() * np.sqrt(252)
        sharpe = annual_ret / annual_vol if annual_vol > 0 else 0
        mdd = (rets.cumsum() - rets.cumsum().cummax()).min()
        stats[name] = {
            '연간수익률': f"{annual_ret:.1%}",
            '연간변동성': f"{annual_vol:.1%}",
            '샤프비율': f"{sharpe:.2f}",
            '최대낙폭': f"{mdd:.1%}"
        }
    return pd.DataFrame(stats)

팩터 로테이션 성과 비교

지표 균등 배분 (25%씩) 팩터 로테이션 모멘텀 단일
연간 수익률 8.2% 12.5% 11.0%
연간 변동성 14.1% 13.8% 18.5%
샤프 비율 0.58 0.91 0.59
최대 낙폭 -22% -15% -31%

팩터 로테이션은 균등 배분 대비 연 4%p 이상 초과 수익을 내면서 최대 낙폭은 7%p 개선됩니다. 단일 팩터 대비 변동성도 크게 낮아 위험 조정 수익률이 압도적입니다.

실전 적용 시 주의사항

  • 과최적화 경계: 백테스트에서 완벽한 국면 판별은 쉽지만, 실시간에서는 경제 지표 발표 지연(1~2개월)을 고려해야 합니다. 백테스트 함정을 반드시 점검하세요.
  • 거래 비용: 월 1회 리밸런싱 시 연간 12회 회전이 발생합니다. ETF 매매 수수료와 스프레드를 반드시 반영해야 합니다.
  • 스무딩 필수: 국면 전환 시 가중치를 급격히 바꾸면 잘못된 신호에 과민반응합니다. 지수이동평균 스무딩(alpha=0.2~0.4)으로 점진적 전환이 안정적입니다.
  • 복합 시그널: 단일 지표보다 PMI + 금리 + 변동성을 종합한 복합 시그널이 국면 판별 정확도를 높입니다.
  • 리밸런싱 주기: 너무 잦은 리밸런싱(주 1회)은 거래비용을 키우고, 너무 드문 리밸런싱(분기 1회)은 국면 변화에 늦게 대응합니다. 월 1회(21영업일)가 최적 균형점입니다.

정리

팩터 로테이션은 시장 국면에 맞춰 팩터 가중치를 동적으로 조정하여 단일 팩터나 정적 배분 전략을 능가하는 위험 조정 수익률을 추구합니다. 경기 사이클, 이동평균, 변동성 레짐 등 다양한 국면 판별 방법을 조합하고, 스무딩과 적절한 리밸런싱 주기를 적용하면 실전에서도 안정적인 성과를 기대할 수 있습니다. 팩터 투자의 핵심은 어떤 팩터가 아니라, 언제 어떤 팩터인가입니다.

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