모멘텀 팩터 투자 전략

모멘텀 팩터란?

모멘텀 팩터(Momentum Factor)는 최근 수익률이 높았던 자산이 계속 상승하고, 낮았던 자산이 계속 하락하는 경향을 체계적으로 포착하는 퀀트 투자 전략입니다. 1993년 Jegadeesh & Titman의 논문 이후 가장 견고한 시장 이상 현상(anomaly) 중 하나로 인정받고 있습니다.

글로벌 주식, 채권, 원자재, 암호화폐 등 거의 모든 자산군에서 모멘텀 효과가 관찰되며, AQR·Dimensional·BlackRock 같은 대형 자산운용사들이 핵심 팩터로 활용하고 있습니다.

모멘텀의 두 가지 유형

구분 횡단면 모멘텀 시계열 모멘텀
비교 대상 자산 간 상대 성과 자산 자체의 과거 수익률
매매 방식 상위 매수 + 하위 매도(롱숏) 양(+)이면 매수, 음(-)이면 매도
시장 노출 시장 중립 가능 방향성 노출 있음
위기 대응 모멘텀 크래시 취약 하락장 자동 헷지
대표 연구 Jegadeesh & Titman (1993) Moskowitz, Ooi & Pedersen (2012)

횡단면 모멘텀 전략 구현

여러 종목 중 최근 수익률 상위를 매수하고 하위를 매도하는 전략입니다. 파이썬 자동매매 시스템에 적용할 수 있는 코드를 살펴봅니다.

import numpy as np
import pandas as pd

class CrossSectionalMomentum:
    def __init__(self, formation=12, skip=1, holding=1,
                 top_pct=0.2, bottom_pct=0.2):
        """
        formation: 모멘텀 측정 기간 (월)
        skip: 최근 제외 기간 (월) — 단기 반전 회피
        holding: 보유 기간 (월)
        top_pct: 상위 비율 (롱)
        bottom_pct: 하위 비율 (숏)
        """
        self.formation = formation
        self.skip = skip
        self.holding = holding
        self.top_pct = top_pct
        self.bottom_pct = bottom_pct

    def compute_momentum(self, prices: pd.DataFrame) -> pd.DataFrame:
        """12-1 모멘텀 스코어 계산"""
        total_period = self.formation + self.skip
        past = prices.shift(self.skip * 21)  # skip 기간 이전
        start = prices.shift(total_period * 21)  # formation 시작

        momentum = (past - start) / start
        return momentum

    def rank_and_select(self, momentum: pd.Series) -> dict:
        """모멘텀 기준 상위/하위 종목 선정"""
        valid = momentum.dropna()
        n = len(valid)
        top_n = max(1, int(n * self.top_pct))
        bottom_n = max(1, int(n * self.bottom_pct))

        ranked = valid.sort_values(ascending=False)
        longs = ranked.head(top_n).index.tolist()
        shorts = ranked.tail(bottom_n).index.tolist()

        return {'long': longs, 'short': shorts}

    def generate_weights(self, prices: pd.DataFrame,
                         date_idx: int) -> dict:
        """특정 시점의 포트폴리오 비중 생성"""
        momentum = self.compute_momentum(prices)
        mom_scores = momentum.iloc[date_idx]
        selection = self.rank_and_select(mom_scores)

        weights = {}
        long_w = 1.0 / len(selection['long'])
        short_w = -1.0 / len(selection['short'])

        for s in selection['long']:
            weights[s] = long_w
        for s in selection['short']:
            weights[s] = short_w

        return weights

시계열 모멘텀 전략 구현

각 자산의 과거 수익률 부호에 따라 방향을 결정하는 전략입니다. 하락장에서 자동으로 숏 포지션을 잡아 위기 방어 능력이 뛰어납니다.

class TimeSeriesMomentum:
    def __init__(self, lookbacks=[1, 3, 6, 12],
                 vol_target=0.10):
        """
        lookbacks: 여러 룩백 기간 (월) — 앙상블
        vol_target: 자산별 변동성 타겟
        """
        self.lookbacks = lookbacks
        self.vol_target = vol_target

    def signal(self, prices: pd.Series,
               lookback_months: int) -> float:
        """단일 룩백 시계열 모멘텀 시그널"""
        days = lookback_months * 21
        if len(prices) < days:
            return 0.0
        ret = prices.iloc[-1] / prices.iloc[-days] - 1
        return np.sign(ret)  # +1 또는 -1

    def ensemble_signal(self, prices: pd.Series) -> float:
        """여러 룩백 기간 앙상블 시그널"""
        signals = [self.signal(prices, lb)
                   for lb in self.lookbacks]
        return np.mean(signals)  # -1 ~ +1 연속 시그널

    def position_size(self, prices: pd.Series) -> float:
        """변동성 조정 포지션 크기"""
        sig = self.ensemble_signal(prices)

        # 실현 변동성 (60일)
        returns = prices.pct_change().dropna()
        realized_vol = returns.tail(60).std() * np.sqrt(252)

        if realized_vol == 0:
            return 0.0

        # 시그널 × 변동성 타겟 / 실현 변동성
        weight = sig * (self.vol_target / realized_vol)
        return np.clip(weight, -2.0, 2.0)  # 레버리지 상한

모멘텀 크래시 방어

모멘텀 전략의 가장 큰 리스크는 모멘텀 크래시입니다. 급격한 시장 반전 시 과거 패자(loser)가 급등하고 과거 승자(winner)가 급락하면서 큰 손실이 발생합니다. 2009년 3월이 대표적인 사례입니다.

class MomentumCrashDefense:
    def __init__(self, base_strategy, crash_lookback=126):
        self.strategy = base_strategy
        self.crash_lookback = crash_lookback

    def market_stress_indicator(self,
                                market_prices: pd.Series) -> float:
        """시장 스트레스 지표 (변동성 + 하락폭 기반)"""
        returns = market_prices.pct_change().dropna()
        recent = returns.tail(self.crash_lookback)

        # 실현 변동성 대비 장기 변동성
        short_vol = returns.tail(21).std()
        long_vol = returns.tail(252).std()
        vol_ratio = short_vol / long_vol if long_vol > 0 else 1

        # 최근 최대 낙폭
        cum = (1 + recent).cumprod()
        drawdown = (cum / cum.cummax() - 1).min()

        # 스트레스 스코어 (0~1, 높을수록 위험)
        stress = min(1.0, max(0.0,
            0.5 * (vol_ratio - 1) + 0.5 * abs(drawdown)))
        return stress

    def adjusted_weight(self, raw_weight: float,
                        market_prices: pd.Series) -> float:
        """스트레스 비례 포지션 축소"""
        stress = self.market_stress_indicator(market_prices)

        # 스트레스 높으면 포지션 축소
        scaling = max(0.2, 1.0 - stress)
        return raw_weight * scaling

서킷브레이커와 함께 사용하면 모멘텀 크래시 시 자동으로 거래를 중단하는 추가 방어선을 구축할 수 있습니다.

듀얼 모멘텀: 횡단면 + 시계열 결합

Gary Antonacci가 제안한 듀얼 모멘텀은 두 유형을 결합해 성과를 극대화합니다.

class DualMomentum:
    def __init__(self, assets, safe_asset='BIL',
                 lookback=12):
        """
        assets: 투자 대상 자산 리스트
        safe_asset: 안전자산 (단기채 등)
        lookback: 모멘텀 측정 기간 (월)
        """
        self.assets = assets
        self.safe_asset = safe_asset
        self.lookback = lookback

    def select(self, prices: pd.DataFrame) -> str:
        """듀얼 모멘텀 자산 선택"""
        days = self.lookback * 21

        # 1단계: 횡단면 — 상대 모멘텀 최강 자산
        mom_scores = {}
        for asset in self.assets:
            p = prices[asset]
            if len(p) >= days:
                mom_scores[asset] = p.iloc[-1] / p.iloc[-days] - 1

        if not mom_scores:
            return self.safe_asset

        best_asset = max(mom_scores, key=mom_scores.get)

        # 2단계: 시계열 — 절대 모멘텀 필터
        if mom_scores[best_asset] > 0:
            return best_asset  # 양의 모멘텀 → 투자
        else:
            return self.safe_asset  # 음의 모멘텀 → 안전자산

    def backtest(self, prices: pd.DataFrame,
                 rebal_freq=21) -> pd.Series:
        """듀얼 모멘텀 백테스트"""
        all_assets = self.assets + [self.safe_asset]
        returns = prices[all_assets].pct_change().dropna()
        portfolio_returns = []

        for i in range(self.lookback * 21, len(returns)):
            if (i - self.lookback * 21) % rebal_freq == 0:
                hist = prices.iloc[:i+1]
                selected = self.select(hist)

            portfolio_returns.append(returns[selected].iloc[i])

        return pd.Series(portfolio_returns,
                         index=returns.index[self.lookback*21:])

모멘텀 팩터 성과 특성

학술 연구와 실증 데이터 기반 모멘텀 팩터의 주요 특성입니다:

  • 연평균 초과 수익: 미국 주식 기준 연 6~10% 롱숏 프리미엄 (1927~2024)
  • 글로벌 보편성: 40개국 이상에서 통계적으로 유의한 모멘텀 효과 확인
  • 최적 기간: 12개월 형성기간, 최근 1개월 제외(12-1)가 표준
  • 최대 약점: 시장 급반전 시 모멘텀 크래시 (2009년 3월: 한 달 -40% 이상)
  • 다른 팩터와 상관관계: 가치(Value) 팩터와 음의 상관 → 결합 시 분산 효과

실전 적용 가이드

  • 리밸런스 주기: 월 1회가 표준. 주간 리밸런스는 거래비용 대비 개선 효과가 미미합니다.
  • 유니버스: 유동성 충분한 대형주 위주로 구성. 소형주는 슬리피지 리스크가 큽니다.
  • 거래비용: 모멘텀 전략은 회전율이 높아(연 200%+) 거래비용 관리가 핵심입니다.
  • 팩터 결합: 모멘텀 단독보다 가치·퀄리티 팩터와 결합하면 위험 대비 수익이 개선됩니다.
  • 암호화폐 적용: 1~4주 단기 모멘텀이 효과적. 장기 모멘텀은 구조 변화가 잦아 불안정합니다.

마무리

모멘텀 팩터는 가장 오래되고 견고한 퀀트 투자 전략 중 하나입니다. 횡단면과 시계열 모멘텀을 이해하고, 듀얼 모멘텀으로 결합하며, 모멘텀 크래시 방어까지 갖추면 체계적인 자동매매 시스템의 핵심 알파 엔진으로 활용할 수 있습니다. 파이썬으로 구현하면 데이터 수집부터 백테스트, 실시간 시그널 생성까지 전 과정을 자동화할 수 있습니다.

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