동적 상관관계 리밸런싱이란?
전통적인 포트폴리오 리밸런싱은 고정 주기(월별·분기별)로 비중을 조정합니다. 하지만 위기 상황에서는 자산 간 상관관계가 급변하면서 분산 효과가 사라지는 상관관계 붕괴(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시간 운영되는 시장에서는 자동화된 상관관계 모니터링이 필수적입니다.
관련 글도 함께 확인해 보세요: