동적 상관관계 자동 리밸런싱

동적 상관관계 리밸런싱이란?

전통적인 포트폴리오 리밸런싱은 고정 주기(월별·분기별)로 비중을 조정합니다. 하지만 위기 상황에서는 자산 간 상관관계가 급변하면서 분산 효과가 사라지는 상관관계 붕괴(Correlation Breakdown)가 발생합니다. 동적 상관관계 리밸런싱은 실시간으로 자산 간 상관관계를 모니터링하고, 변화가 감지되면 자동으로 비중을 조정하는 전략입니다.

2020년 3월 코로나 폭락, 2022년 루나-테라 붕괴 때 BTC와 ETH의 상관계수가 0.85에서 0.95 이상으로 급등하면서 분산 투자 효과가 무력화된 사례가 대표적입니다. 이런 상황을 사전에 감지하고 대응하는 것이 이 전략의 핵심입니다.

상관관계 측정 방법 비교

방법 장점 단점 적합 상황
피어슨 롤링 직관적, 구현 간단 과거 데이터에 지연 일반적 모니터링
EWMA 최근 데이터 가중 파라미터 민감 빠른 변화 감지
DCC-GARCH 시변 상관관계 모델링 계산 비용 높음 정밀 분석
코퓰러 꼬리 의존성 포착 복잡한 구현 극단 상황 분석

실전 자동매매에서는 EWMA(지수가중이동평균)가 계산 효율과 반응 속도의 균형이 가장 좋습니다.

파이썬으로 동적 상관관계 엔진 구현

EWMA 기반으로 실시간 상관관계를 추적하고, 급변 시 신호를 발생시키는 엔진을 구현합니다.

import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class CorrelationAlert:
    timestamp: str
    asset_pair: tuple[str, str]
    current_corr: float
    baseline_corr: float
    change: float
    alert_type: str  # 'spike', 'breakdown', 'regime_shift'


class DynamicCorrelationEngine:
    def __init__(
        self,
        assets: list[str],
        fast_span: int = 20,
        slow_span: int = 60,
        alert_threshold: float = 0.2
    ):
        self.assets = assets
        self.fast_span = fast_span
        self.slow_span = slow_span
        self.alert_threshold = alert_threshold
        self.returns_buffer: list[dict] = []

    def update(
        self, returns: dict[str, float], timestamp: str
    ) -> list[CorrelationAlert]:
        """새 수익률 데이터로 상관관계 갱신"""
        self.returns_buffer.append(returns)

        if len(self.returns_buffer) < self.slow_span + 5:
            return []

        df = pd.DataFrame(self.returns_buffer)
        alerts = []

        for i, a1 in enumerate(self.assets):
            for a2 in self.assets[i + 1:]:
                alert = self._check_pair(
                    df[a1], df[a2], (a1, a2), timestamp
                )
                if alert:
                    alerts.append(alert)

        return alerts

    def _check_pair(
        self, s1, s2, pair, timestamp
    ) -> Optional[CorrelationAlert]:
        fast_corr = self._ewma_corr(
            s1, s2, self.fast_span
        )
        slow_corr = self._ewma_corr(
            s1, s2, self.slow_span
        )
        change = fast_corr - slow_corr

        if abs(change) < self.alert_threshold:
            return None

        if change > 0 and fast_corr > 0.8:
            alert_type = 'spike'
        elif change < 0 and fast_corr < 0.2:
            alert_type = 'breakdown'
        else:
            alert_type = 'regime_shift'

        return CorrelationAlert(
            timestamp=timestamp,
            asset_pair=pair,
            current_corr=round(fast_corr, 4),
            baseline_corr=round(slow_corr, 4),
            change=round(change, 4),
            alert_type=alert_type
        )

    @staticmethod
    def _ewma_corr(
        s1: pd.Series, s2: pd.Series, span: int
    ) -> float:
        alpha = 2 / (span + 1)
        s1_mean = s1.ewm(span=span).mean()
        s2_mean = s2.ewm(span=span).mean()
        d1 = s1 - s1_mean
        d2 = s2 - s2_mean
        cov = (d1 * d2).ewm(span=span).mean().iloc[-1]
        var1 = (d1 ** 2).ewm(span=span).mean().iloc[-1]
        var2 = (d2 ** 2).ewm(span=span).mean().iloc[-1]
        denom = np.sqrt(var1 * var2)
        if denom == 0:
            return 0.0
        return float(np.clip(cov / denom, -1, 1))

    def correlation_matrix(self) -> pd.DataFrame:
        """현재 EWMA 상관관계 행렬"""
        df = pd.DataFrame(self.returns_buffer)
        n = len(self.assets)
        matrix = np.eye(n)
        for i in range(n):
            for j in range(i + 1, n):
                c = self._ewma_corr(
                    df[self.assets[i]],
                    df[self.assets[j]],
                    self.fast_span
                )
                matrix[i, j] = c
                matrix[j, i] = c
        return pd.DataFrame(
            matrix, index=self.assets, columns=self.assets
        )

자동 리밸런싱 시스템 구현

상관관계 변화를 감지하면 포트폴리오 비중을 자동으로 조정합니다. 핵심 원리는 상관관계가 높아지면 분산 효과가 줄어드므로 노출을 축소하고, 상관관계가 낮아지면 분산 효과가 커지므로 노출을 확대하는 것입니다.

class DynamicRebalancer:
    def __init__(
        self,
        assets: list[str],
        base_weights: dict[str, float],
        max_corr_threshold: float = 0.85,
        rebalance_band: float = 0.05
    ):
        self.assets = assets
        self.base_weights = base_weights
        self.max_corr = max_corr_threshold
        self.band = rebalance_band
        self.current_weights = base_weights.copy()

    def compute_target_weights(
        self,
        corr_matrix: pd.DataFrame,
        volatilities: dict[str, float]
    ) -> dict[str, float]:
        """상관관계·변동성 기반 목표 비중 계산"""
        n = len(self.assets)
        raw_weights = {}

        for asset in self.assets:
            # 다른 자산과의 평균 상관관계
            avg_corr = np.mean([
                abs(corr_matrix.loc[asset, other])
                for other in self.assets if other != asset
            ])

            # 상관관계 페널티: 높을수록 비중 축소
            corr_penalty = max(
                0, 1 - (avg_corr - 0.3) / (self.max_corr - 0.3)
            )

            # 변동성 역가중
            vol_weight = 1 / volatilities[asset]

            raw_weights[asset] = (
                self.base_weights[asset]
                * corr_penalty
                * vol_weight
            )

        # 정규화
        total = sum(raw_weights.values())
        if total == 0:
            return self.base_weights.copy()

        return {
            asset: round(w / total, 4)
            for asset, w in raw_weights.items()
        }

    def should_rebalance(
        self, target: dict[str, float]
    ) -> bool:
        """리밸런싱 밴드 초과 여부"""
        for asset in self.assets:
            diff = abs(
                target[asset] - self.current_weights[asset]
            )
            if diff > self.band:
                return True
        return False

    def generate_orders(
        self,
        target: dict[str, float],
        portfolio_value: float,
        current_prices: dict[str, float]
    ) -> list[dict]:
        """리밸런싱 주문 생성"""
        orders = []
        for asset in self.assets:
            current_alloc = (
                self.current_weights[asset] * portfolio_value
            )
            target_alloc = target[asset] * portfolio_value
            diff_usd = target_alloc - current_alloc

            if abs(diff_usd) < 10:
                continue

            qty = abs(diff_usd) / current_prices[asset]
            orders.append({
                'asset': asset,
                'side': 'buy' if diff_usd > 0 else 'sell',
                'quantity': round(qty, 6),
                'value_usd': round(abs(diff_usd), 2),
                'weight_change': round(
                    target[asset]
                    - self.current_weights[asset], 4
                )
            })

        return orders

상관관계 레짐 분류

시장의 상관관계 상태를 레짐(Regime)으로 분류하면, 레짐별로 다른 리밸런싱 전략을 적용할 수 있습니다.

def classify_regime(
    corr_matrix: pd.DataFrame,
    avg_vol: float
) -> dict:
    """상관관계·변동성 기반 레짐 분류"""
    upper = corr_matrix.values[
        np.triu_indices_from(corr_matrix.values, k=1)
    ]
    avg_corr = np.mean(upper)
    max_corr = np.max(upper)
    min_corr = np.min(upper)
    dispersion = max_corr - min_corr

    if avg_corr > 0.8 and avg_vol > 0.03:
        regime = 'crisis'
        action = '현금 비중 확대, 헤지 강화'
    elif avg_corr > 0.6:
        regime = 'high_corr'
        action = '분산 자산 추가, 비중 축소'
    elif avg_corr < 0.2:
        regime = 'decorrelated'
        action = '적극적 분산 투자, 비중 확대'
    elif dispersion > 0.5:
        regime = 'divergent'
        action = '섹터 로테이션 기회'
    else:
        regime = 'normal'
        action = '기본 비중 유지'

    return {
        'regime': regime,
        'avg_correlation': round(avg_corr, 4),
        'max_correlation': round(max_corr, 4),
        'dispersion': round(dispersion, 4),
        'avg_volatility': round(avg_vol, 4),
        'recommended_action': action
    }

전체 파이프라인 통합

상관관계 모니터링부터 주문 생성까지 전체 흐름을 하나로 연결합니다.

def run_rebalance_pipeline(
    engine: DynamicCorrelationEngine,
    rebalancer: DynamicRebalancer,
    returns: dict[str, float],
    volatilities: dict[str, float],
    prices: dict[str, float],
    portfolio_value: float,
    timestamp: str
) -> Optional[dict]:
    """리밸런싱 파이프라인 1사이클 실행"""

    # 1. 상관관계 업데이트 및 알림 확인
    alerts = engine.update(returns, timestamp)

    # 2. 상관관계 행렬 계산
    corr_matrix = engine.correlation_matrix()

    # 3. 레짐 분류
    avg_vol = np.mean(list(volatilities.values()))
    regime = classify_regime(corr_matrix, avg_vol)

    # 4. 위기 레짐이면 즉시 현금화 비율 증가
    if regime['regime'] == 'crisis':
        print(f"⚠️ 위기 레짐 감지: {regime}")
        # 모든 비중 50% 축소
        target = {
            a: w * 0.5
            for a, w in rebalancer.base_weights.items()
        }
        total = sum(target.values())
        target = {a: w / total for a, w in target.items()}
    else:
        target = rebalancer.compute_target_weights(
            corr_matrix, volatilities
        )

    # 5. 리밸런싱 필요 여부 확인
    if not rebalancer.should_rebalance(target):
        return None

    # 6. 주문 생성
    orders = rebalancer.generate_orders(
        target, portfolio_value, prices
    )

    return {
        'timestamp': timestamp,
        'regime': regime,
        'alerts': [vars(a) for a in alerts],
        'target_weights': target,
        'orders': orders
    }

실전 적용 시 주의사항

  • 리밸런싱 빈도: 너무 잦은 리밸런싱은 거래 비용을 증가시킵니다. 최소 리밸런싱 간격(예: 4시간)을 설정하세요.
  • 거래비용 반영: 리밸런싱 주문의 예상 슬리피지·수수료가 포지션 조정 이익보다 크면 실행하지 않아야 합니다.
  • EWMA 파라미터 튜닝: fast_span이 너무 짧으면 노이즈에 반응하고, 너무 길면 변화 감지가 늦습니다. 백테스트로 최적값을 찾으세요.
  • 소규모 포트폴리오 한계: 자산이 2~3개일 때는 상관관계 분석의 실효성이 떨어집니다. 최소 5개 이상의 자산을 권장합니다.
  • 위기 시 유동성: 상관관계 급등 시 유동성도 함께 감소하므로, 주문 분할 실행이 필수입니다.

마무리

동적 상관관계 리밸런싱은 고정 주기 리밸런싱의 한계를 극복하는 진화된 접근법입니다. EWMA로 실시간 상관관계를 추적하고, 레짐별로 차별화된 비중 조정을 자동화하면 위기 상황에서도 포트폴리오를 능동적으로 보호할 수 있습니다. 특히 암호화폐처럼 24시간 운영되는 시장에서는 자동화된 상관관계 모니터링이 필수적입니다.

관련 글도 함께 확인해 보세요:

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