칼만 필터란 무엇인가
칼만 필터(Kalman Filter)는 노이즈가 섞인 시계열 데이터에서 숨겨진 진짜 상태(true state)를 실시간으로 추정하는 최적 필터링 알고리즘입니다. 1960년 루돌프 칼만이 발표한 이래 항공우주, 로봇공학은 물론 퀀트 트레이딩 분야에서도 핵심 도구로 자리 잡았습니다.
금융 시장의 가격 데이터는 본질적으로 노이즈가 많습니다. 이동평균(MA)이나 지수이동평균(EMA)은 과거 데이터에 고정된 가중치를 부여하지만, 칼만 필터는 예측-보정(Predict-Update) 사이클을 통해 가중치를 동적으로 조절합니다. 그 결과 시장 변화에 훨씬 빠르게 적응하면서도 노이즈를 효과적으로 제거할 수 있습니다.
칼만 필터의 핵심 수학 구조
칼만 필터는 두 단계로 작동합니다.
1단계 — 예측(Predict)
- 상태 예측:
x̂(k|k-1) = F · x̂(k-1|k-1) + B · u(k) - 오차 공분산 예측:
P(k|k-1) = F · P(k-1|k-1) · Fᵀ + Q
2단계 — 보정(Update)
- 칼만 이득:
K(k) = P(k|k-1) · Hᵀ · (H · P(k|k-1) · Hᵀ + R)⁻¹ - 상태 갱신:
x̂(k|k) = x̂(k|k-1) + K(k) · (z(k) - H · x̂(k|k-1)) - 오차 공분산 갱신:
P(k|k) = (I - K(k) · H) · P(k|k-1)
여기서 Q는 프로세스 노이즈, R은 관측 노이즈입니다. Q/R 비율이 클수록 필터가 새 관측값을 더 신뢰하고, 작을수록 기존 추정을 유지합니다. 이 비율이 자동매매에서 민감도를 결정하는 핵심 하이퍼파라미터입니다.
이동평균 대비 칼만 필터의 장점
전통적인 이동평균과 비교했을 때 칼만 필터가 자동매매에서 우월한 이유는 명확합니다.
- 적응적 가중치 — 시장 상태에 따라 칼만 이득(K)이 자동 조절됩니다. 변동성이 커지면 관측값에 가중치를 높이고, 안정적이면 추세를 유지합니다.
- 지연(Lag) 최소화 — 이동평균은 윈도우 크기만큼 구조적 지연이 발생하지만, 칼만 필터는 1-스텝 예측을 통해 지연을 크게 줄입니다.
- 불확실성 정량화 — 오차 공분산 행렬 P가 추정의 불확실성을 수치로 제공합니다. 이를 포지션 사이징이나 리스크 관리에 직접 활용할 수 있습니다.
- 다변량 확장 — 가격, 거래량, 스프레드 등 여러 변수를 하나의 상태 벡터로 통합 모델링이 가능합니다.
파이썬으로 구현하는 칼만 필터 자동매매
아래는 pykalman 라이브러리를 활용한 실전 구현 예시입니다.
import numpy as np
import pandas as pd
from pykalman import KalmanFilter
def kalman_trend_strategy(prices: pd.Series,
observation_covariance: float = 1.0,
transition_covariance: float = 0.01):
"""
칼만 필터 기반 추세 추종 전략
- 칼만 추정 가격 > 이전 추정 가격: 롱
- 칼만 추정 가격 < 이전 추정 가격: 숏
"""
kf = KalmanFilter(
initial_state_mean=prices.iloc[0],
initial_state_covariance=1.0,
observation_covariance=observation_covariance,
transition_covariance=transition_covariance,
transition_matrices=[1],
observation_matrices=[1]
)
state_means, state_covs = kf.filter(prices.values)
state_means = state_means.flatten()
signals = pd.Series(0, index=prices.index)
for i in range(1, len(state_means)):
if state_means[i] > state_means[i-1]:
signals.iloc[i] = 1 # 롱
else:
signals.iloc[i] = -1 # 숏
return signals, state_means, state_covs.flatten()
# 실행 예시
df = pd.read_csv("btc_ohlcv.csv", parse_dates=["timestamp"])
signals, filtered, covs = kalman_trend_strategy(
df["close"],
observation_covariance=1.0,
transition_covariance=0.005
)
df["kalman_price"] = filtered
df["signal"] = signals
df["strategy_return"] = df["signal"].shift(1) * df["close"].pct_change()
cumulative = (1 + df["strategy_return"]).cumprod()
print(f"누적 수익률: {cumulative.iloc[-1]:.4f}")
칼만 필터 + 페어 트레이딩 응용
칼만 필터는 페어 트레이딩에서도 강력한 도구입니다. 두 자산 간 헤지 비율(hedge ratio)을 고정값 대신 칼만 필터로 동적 추정하면 공적분 관계가 시간에 따라 변해도 적응할 수 있습니다.
def kalman_hedge_ratio(asset_x: pd.Series, asset_y: pd.Series):
"""
칼만 필터로 동적 헤지 비율 추정
상태: hedge ratio (beta)
관측: y = beta * x + epsilon
"""
delta = 1e-4
trans_cov = delta / (1 - delta) * np.eye(2)
kf = KalmanFilter(
n_dim_obs=1, n_dim_state=2,
initial_state_mean=np.zeros(2),
initial_state_covariance=np.ones((2, 2)),
transition_matrices=np.eye(2),
observation_covariance=1.0,
transition_covariance=trans_cov
)
obs = asset_y.values
obs_matrices = np.column_stack([
asset_x.values, np.ones(len(asset_x))
]).reshape(-1, 1, 2)
state_means, state_covs = kf.filter(
obs, observation_matrices=obs_matrices
)
hedge_ratios = state_means[:, 0]
intercepts = state_means[:, 1]
spread = asset_y.values - hedge_ratios * asset_x.values - intercepts
return hedge_ratios, spread
이 방법의 핵심은 delta 파라미터입니다. 값이 작으면 헤지 비율이 천천히 변하고(안정적), 크면 빠르게 적응합니다(반응적). 실전에서는 1e-5 ~ 1e-3 범위에서 백테스트를 통해 최적값을 찾습니다.
하이퍼파라미터 최적화 전략
칼만 필터 자동매매의 성능은 Q(프로세스 노이즈)와 R(관측 노이즈) 설정에 크게 좌우됩니다. 실전에서 활용할 수 있는 최적화 방법을 정리합니다.
- EM 알고리즘 —
pykalman의em()메서드로 최대우도추정(MLE) 기반 자동 튜닝이 가능합니다. 다만 과거 데이터 전체를 사용하므로 과적합 위험이 있습니다. - 롤링 윈도우 EM — 최근 N일 데이터로만 EM을 수행해 시장 레짐 변화에 적응합니다. N은 보통 60~120일이 적절합니다.
- 베이지안 최적화 —
optuna등으로 Q, R을 탐색합니다. 목적함수는 샤프 비율이나 로그 우도를 사용합니다. - 적응적 Q 추정 — 이노베이션(innovation = 실제 관측 – 예측)의 분산을 실시간 추적해 Q를 동적으로 조절하는 방법도 있습니다.
import optuna
def objective(trial):
q = trial.suggest_float("q", 1e-6, 1.0, log=True)
r = trial.suggest_float("r", 0.1, 10.0, log=True)
signals, _, _ = kalman_trend_strategy(
train_prices,
observation_covariance=r,
transition_covariance=q
)
returns = signals.shift(1) * train_prices.pct_change()
sharpe = returns.mean() / returns.std() * np.sqrt(252)
return sharpe
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=200)
best_q = study.best_params["q"]
best_r = study.best_params["r"]
실전 적용 시 주의사항
칼만 필터 자동매매를 실제 운용할 때 반드시 고려해야 할 사항들입니다.
- 선형성 가정 — 기본 칼만 필터는 선형 시스템을 가정합니다. 비선형 시장 동학에는 확장 칼만 필터(EKF)나 언센티드 칼만 필터(UKF)를 고려하세요.
- 정규성 가정 — 금융 수익률은 팻테일 분포를 따릅니다. 칼만 이득이 극단값에 과민 반응할 수 있으므로 이상치(outlier) 처리 로직이 필요합니다.
- 초기값 민감도 — 초기 상태 추정값과 공분산 설정이 수렴까지의 시간에 영향을 줍니다. 보통 50~100개 관측치는 워밍업(burn-in) 기간으로 제외합니다.
- 거래 비용 — 칼만 필터의 빠른 적응이 과도한 매매 신호를 발생시킬 수 있습니다. 최소 신호 지속 기간이나 임계값 필터를 추가하세요.
- 레짐 전환 — 시장 레짐이 급변할 때 단일 칼만 필터는 적응이 느릴 수 있습니다. 엔트로피 기반 시장 감지와 결합하면 효과적입니다.
확장 칼만 필터(EKF)로 비선형 시장 모델링
가격의 로그 수익률이나 변동성 모델처럼 비선형 관계가 있을 때는 EKF를 사용합니다. 핵심 아이디어는 비선형 함수를 현재 추정치 주변에서 1차 테일러 전개(야코비안)로 선형화하는 것입니다.
def ekf_volatility_tracker(returns: np.ndarray,
q_vol: float = 0.001,
r_obs: float = 1.0):
"""
EKF로 로그 변동성을 실시간 추적
상태: log(sigma) — AR(1) 프로세스 가정
관측: r(t) = sigma(t) * epsilon(t)
"""
n = len(returns)
x = np.log(returns[:20].std()) # 초기 로그 변동성
P = 1.0
vol_estimates = np.zeros(n)
for t in range(n):
# Predict
x_pred = 0.98 * x # AR(1) 평균 회귀
P_pred = 0.98**2 * P + q_vol
# Update (비선형: z = exp(x) * eps)
sigma_pred = np.exp(x_pred)
H = returns[t] * np.exp(x_pred) # 야코비안
S = H**2 * P_pred + r_obs
K = P_pred * H / S
innovation = returns[t]**2 - sigma_pred**2
x = x_pred + K * innovation
P = (1 - K * H) * P_pred
vol_estimates[t] = np.exp(x)
return vol_estimates
마무리 — 칼만 필터 자동매매 체크리스트
칼만 필터 자동매매 시스템을 구축할 때 아래 체크리스트를 참고하세요.
- 목적 정의: 추세 추종인지, 스프레드 추적인지, 변동성 추정인지 명확히 합니다.
- 모델 선택: 선형이면 기본 KF, 비선형이면 EKF/UKF를 사용합니다.
- 하이퍼파라미터: Q/R을 EM 또는 베이지안 최적화로 튜닝하되, 워크포워드 검증으로 과적합을 방지합니다.
- 워밍업: 초기 50~100개 신호는 무시합니다.
- 거래 비용 반영: 수수료, 슬리피지를 포함한 넷 수익률로 평가합니다.
- 불확실성 활용: 오차 공분산 P가 크면 포지션을 줄이고, 작으면 확대합니다.
- 모니터링: 이노베이션 시퀀스의 자기상관을 체크해 모델 적합도를 실시간 검증합니다.
칼만 필터는 단순한 기술 지표를 넘어 확률적 프레임워크로 시장을 바라보는 시각을 제공합니다. 불확실성을 정량화하고 적응적으로 대응하는 이 접근법은 체계적인 퀀트 전략의 핵심 빌딩 블록이 될 것입니다.