본문 바로가기
경제 금융 포털사이트 경제 금융 포털사이트

LSTM과 GARCH 예측 성능 비교

금알남 읽는 시간 약 23분
ai thumbnail illustration LSTM과 GARCH 예측 성능 비교 1789199492

LSTM이 복잡한 모델이라고 해서 전통적인 GARCH보다 항상 예측력이 높은 것은 아닙니다. 같은 데이터와 같은 평가 구간에서 두 모델의 변동성 예측 결과를 직접 비교해봐야 딥러닝과 통계 모델의 차이는 이론보다 동일한 조건의 아웃오브샘플 테스트에서 더욱 더 명확하게 확인할 수 있습니다. 실제 금융 시장 데이터를 넣고 돌려보면 오히려 단순한 GARCH 모델이 예상외로 선방하거나, 반대로 LSTM이 복잡한 비선형 패턴을 잘 잡아내어 압도적인 성능을 보여주기도 합니다. 결국 모델의 우수성은 이름값이나 복잡성이 아니라, 우리가 풀고자 하는 데이터의 특성과 얼마나 잘 맞아떨어지느냐에 따라 결정되는 셈입니다.

주식 데이터를 분석하다 보면 서로 상당히 다른 두 모델을 자주 만나게 됩니다.

하나는 딥러닝 기반의 LSTM이고, 다른 하나는 금융 시계열에서 오랫동안 사용되어 온 GARCH입니다.

LSTM은 복잡한 비선형 관계를 학습할 수 있으니 GARCH보다 성능이 더 좋을 것이라고 생각하기 쉽습니다.

그런데 실제 금융 데이터에서는 이야기가 그렇게 단순하지 않습니다.

모델이 복잡하다는 것과 미래 데이터에 대한 예측력이 높다는 것은 같은 의미가 아니기 때문입니다.

더 중요한 문제가 있습니다.

LSTM으로 내일의 주가를 예측하고 GARCH로 내일의 변동성을 예측한 뒤 두 모델의 성능을 비교한다면 애초에 서로 다른 문제를 비교하는 셈입니다.

따라서 이번 실습에서는 비교 대상을 동일하게 맞추겠습니다.

두 모델 모두 과거 수익률을 이용해 다음 거래일의 변동성을 예측하도록 만들고, 학습에 사용하지 않은 테스트 구간에서 MAE와 RMSE를 비교해보겠습니다.

특정 금융상품의 가격 방향을 전망하려는 목적이 아니라 금융 시계열 모델을 객관적으로 검증하는 방법을 이해하기 위한 실습입니다.

image 16

LSTM과 GARCH는 무엇이 다를까?

LSTM은 Long Short-Term Memory의 약자로 순서가 있는 데이터를 처리하기 위해 사용되는 순환신경망 계열 모델입니다.

Keras의 LSTM 레이어 역시 시간 단계와 특성으로 구성된 시계열 입력을 받아 과거의 연속적인 패턴을 학습할 수 있도록 설계되어 있습니다.

주가 분석에서는 최근 여러 거래일의 수익률, 거래량, 금리, 기술적 지표 등을 한꺼번에 입력해 다음 값을 예측하는 방식으로 응용할 수 있습니다.

반면 GARCH는 목적이 조금 더 명확합니다.

GARCH는 수익률의 조건부 분산, 즉 시간이 흐르면서 달라지는 변동성을 모델링하는 데 사용됩니다.

Python의 arch 공식 문서에서 소개하는 기본적인 GARCH(1,1)의 구조는 다음과 같습니다.

수익률 = 평균 + 오차

현재 변동성
= 상수
+ 이전 시점의 충격
+ 이전 시점의 변동성

수식으로 표현하면 대표적인 GARCH(1,1)의 조건부 분산은 다음 구조를 가집니다.

σ²(t)
= ω
+ αε²(t-1)
+ βσ²(t-1)

arch 라이브러리에서도 arch_model()을 이용해 GARCH(1,1)을 구성하고 조건부 분산을 예측할 수 있습니다.

두 모델의 성격을 간단히 비교하면 다음과 같습니다.

비교 항목LSTMGARCH
모델 종류딥러닝통계적 시계열 모델
주요 강점복잡한 비선형 패턴 학습조건부 변동성 모델링
입력 변수여러 변수 사용 가능기본적으로 수익률 중심
모델 구조비교적 복잡상대적으로 단순
학습 비용높은 편낮은 편
해석 용이성상대적으로 낮음상대적으로 높음
하이퍼파라미터비교적 많음비교적 적음
활용 목적범용 시계열 예측변동성·리스크 모델링

여기서 중요한 결론은 아직 “어느 모델이 좋다”가 아닙니다.

GARCH는 변동성 자체를 모델링하도록 설계됐고, LSTM은 더 범용적인 시계열 학습 모델입니다.

그래서 실제 성능은 동일한 조건에서 직접 검증해야 합니다.

같은 데이터로 LSTM과 GARCH를 구현해보자

이번 실습에서는 SPY의 일별 가격을 예제로 사용하겠습니다.

먼저 필요한 패키지를 설치합니다.

pip install yfinance pandas numpy matplotlib scikit-learn tensorflow arch

필요한 라이브러리를 불러옵니다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf

from arch import arch_model

from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (
    mean_absolute_error,
    mean_squared_error
)

from tensorflow import keras
from tensorflow.keras import layers

재현성을 조금 높이기 위해 랜덤 시드도 설정합니다.

keras.utils.set_random_seed(42)

이제 가격 데이터를 가져옵니다.

df = yf.download(
    "SPY",
    start="2015-01-01",
    auto_adjust=True,
    progress=False,
    multi_level_index=False
)

close = df["Close"].dropna()

returns = (
    close.pct_change()
    .dropna()
    * 100
)

print(returns.head())

현재 yfinance의 download() 함수는 시작 날짜와 종료 날짜, interval, 자동 가격조정 등의 옵션을 제공합니다.

여기서는 GARCH에서 흔히 사용하는 형태에 맞춰 일별 수익률을 퍼센트 단위로 변환했습니다.

이제 중요한 부분입니다.

이번 비교에서는 두 모델이 모두 다음 날의 분산을 예측하도록 하겠습니다.

실제 조건부 변동성은 직접 관측할 수 없기 때문에 간단한 실습에서는 다음 날 수익률의 제곱을 변동성의 대용값으로 사용합니다.

realized_var = returns ** 2

다만 수익률 제곱은 실제 잠재 변동성을 완벽하게 측정하는 값이 아니라 상당히 노이즈가 많은 proxy라는 점을 기억해야 합니다.

보다 정교한 연구에서는 장중 고빈도 데이터를 이용해 realized variance를 계산하기도 합니다.

이번 목적은 모델 구현과 비교 방법을 이해하는 것이므로 일별 수익률 제곱을 사용하겠습니다.

테스트 데이터는 가장 최근 252거래일을 별도로 남겨두겠습니다.

TEST_SIZE = 252

split_idx = len(returns) - TEST_SIZE

train_returns = returns.iloc[:split_idx]
test_returns = returns.iloc[split_idx:]

print(
    "Train:",
    train_returns.index[0],
    "~",
    train_returns.index[-1]
)

print(
    "Test:",
    test_returns.index[0],
    "~",
    test_returns.index[-1]
)

여기서 테스트 데이터를 모델 학습에 사용해서는 안 됩니다.

미래 데이터를 학습 과정에 섞으면 실제 투자 시점에서는 알 수 없는 정보를 사용하게 되고 예측 성능이 과장될 수 있습니다.

먼저 LSTM 모델을 만들겠습니다.

최근 30거래일 수익률을 보고 다음 날 분산을 예측하도록 합니다.

LOOKBACK = 30

x_scaler = StandardScaler()

x_scaler.fit(
    train_returns.values.reshape(-1, 1)
)

scaled_returns = x_scaler.transform(
    returns.values.reshape(-1, 1)
)

X_train = []
y_train = []

for i in range(
    LOOKBACK,
    split_idx
):
    X_train.append(
        scaled_returns[
            i - LOOKBACK:i
        ]
    )

    y_train.append(
        returns.iloc[i] ** 2
    )

X_train = np.array(X_train)
y_train = np.array(y_train).reshape(-1, 1)

타깃값도 학습 데이터 기준으로 변환합니다.

y_scaler = StandardScaler()

y_scaler.fit(y_train)

y_train_scaled = y_scaler.transform(
    y_train
)

테스트 데이터도 동일한 방식으로 만듭니다.

X_test = []

for i in range(
    split_idx,
    len(returns)
):
    X_test.append(
        scaled_returns[
            i - LOOKBACK:i
        ]
    )

X_test = np.array(X_test)

LSTM 모델은 너무 복잡하게 만들지 않겠습니다.

model = keras.Sequential([
    keras.Input(
        shape=(LOOKBACK, 1)
    ),

    layers.LSTM(
        32
    ),

    layers.Dropout(
        0.2
    ),

    layers.Dense(
        1
    )
])

model.compile(
    optimizer="adam",
    loss="mse"
)

LSTM이 받는 입력은 기본적으로 시간 단계가 포함된 3차원 형태입니다. Keras 공식 문서에서도 LSTM 입력 구조와 units, dropout, return_sequences 등의 주요 옵션을 확인할 수 있습니다.

학습 데이터 뒤쪽 20%를 validation 구간으로 따로 나눕니다.

val_size = int(
    len(X_train) * 0.2
)

X_fit = X_train[:-val_size]
y_fit = y_train_scaled[:-val_size]

X_val = X_train[-val_size:]
y_val = y_train_scaled[-val_size:]

EarlyStopping을 적용합니다.

early_stop = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=10,
    restore_best_weights=True
)

모델을 학습합니다.

history = model.fit(
    X_fit,
    y_fit,

    validation_data=(
        X_val,
        y_val
    ),

    epochs=100,
    batch_size=32,

    shuffle=False,

    callbacks=[
        early_stop
    ],

    verbose=1
)

이제 학습 과정에서 사용하지 않은 테스트 구간을 예측합니다.

lstm_scaled = model.predict(
    X_test,
    verbose=0
)

lstm_var = y_scaler.inverse_transform(
    lstm_scaled
).flatten()

분산은 음수가 될 수 없으므로 음수 예측값은 0으로 제한합니다.

lstm_var = np.clip(
    lstm_var,
    0,
    None
)

이것으로 LSTM 예측값을 만들었습니다.

image 15

GARCH 예측과 실제 성능을 비교해보자

이번에는 동일한 테스트 기간에 GARCH(1,1)를 적용합니다.

중요한 것은 테스트 날짜의 수익률을 미리 사용하지 않는 것입니다.

따라서 각 날짜를 예측할 때 그 직전까지 관측된 데이터만 이용하는 expanding window 방식으로 구현하겠습니다.

garch_var = []

for i in range(
    split_idx,
    len(returns)
):

    history = returns.iloc[:i]

    garch = arch_model(
        history,
        mean="Constant",
        vol="GARCH",
        p=1,
        q=1,
        dist="normal",
        rescale=False
    )

    result = garch.fit(
        disp="off"
    )

    forecast = result.forecast(
        horizon=1,
        reindex=False
    )

    pred = float(
        forecast.variance.iloc[-1, 0]
    )

    garch_var.append(
        max(pred, 0)
    )

garch_var = np.array(
    garch_var
)

arch에서는 GARCH 모델의 조건부 분산을 분석적으로 예측할 수 있으며, forecast 결과에서 평균과 분산 등의 값을 구분해 확인할 수 있습니다.

이 방식은 매 거래일마다 GARCH 파라미터를 다시 추정하기 때문에 컴퓨터 환경에 따라 시간이 걸릴 수 있습니다.

실무에서는 일정 주기마다 모델을 재추정하거나 고정된 파라미터를 활용하는 방식도 고려할 수 있습니다.

이제 실제 테스트 구간의 값을 만듭니다.

actual_var = (
    test_returns.values ** 2
)

세 값을 하나의 데이터프레임으로 정리합니다.

comparison = pd.DataFrame(
    {
        "Actual": actual_var,
        "LSTM": lstm_var,
        "GARCH": garch_var
    },
    index=test_returns.index
)

print(
    comparison.head()
)

이제 MAE와 RMSE를 비교합니다.

def evaluate(
    actual,
    predicted
):
    mae = mean_absolute_error(
        actual,
        predicted
    )

    rmse = np.sqrt(
        mean_squared_error(
            actual,
            predicted
        )
    )

    return mae, rmse

두 모델을 평가합니다.

lstm_mae, lstm_rmse = evaluate(
    comparison["Actual"],
    comparison["LSTM"]
)

garch_mae, garch_rmse = evaluate(
    comparison["Actual"],
    comparison["GARCH"]
)

result_table = pd.DataFrame(
    {
        "MAE": [
            lstm_mae,
            garch_mae
        ],

        "RMSE": [
            lstm_rmse,
            garch_rmse
        ]
    },
    index=[
        "LSTM",
        "GARCH"
    ]
)

print(result_table)

MAE는 실제값과 예측값 차이의 절댓값을 평균낸 지표입니다.

RMSE는 큰 예측 오차에 상대적으로 더 큰 영향을 받는 지표입니다. scikit-learn에서는 MAE, MSE, RMSE를 포함한 다양한 회귀 평가 지표를 제공합니다.

두 지표 모두 이 실습에서는 낮을수록 실제 값에 가까운 예측을 했다고 해석할 수 있습니다.

예측 결과를 그래프로 비교할 수도 있습니다.

plt.figure(
    figsize=(14, 7)
)

plt.plot(
    comparison.index,
    np.sqrt(
        comparison["Actual"]
    ),
    label="Actual |Return|",
    alpha=0.6
)

plt.plot(
    comparison.index,
    np.sqrt(
        comparison["LSTM"]
    ),
    label="LSTM Forecast"
)

plt.plot(
    comparison.index,
    np.sqrt(
        comparison["GARCH"]
    ),
    label="GARCH Forecast"
)

plt.title(
    "LSTM vs GARCH Volatility Forecast"
)

plt.ylabel(
    "Volatility Proxy (%)"
)

plt.legend()
plt.grid(True)
plt.show()

이 코드에서 중요한 것은 특정 숫자를 미리 기대하지 않는 것입니다.

데이터 기간과 모델 설정을 변경하면 LSTM이 좋은 기간도 있을 수 있고 GARCH의 오차가 더 작은 기간도 나타날 수 있습니다.

실제 실행 결과를 그대로 기록하는 것이 모델 비교의 출발점입니다.

결국 LSTM과 GARCH 중 어느 것이 더 좋을까?

이 실습만으로 한 모델이 항상 우수하다고 결론을 내릴 수는 없습니다.

오히려 두 모델의 특성을 이해하는 것이 중요합니다.

GARCH의 장점은 목적이 명확하다는 것입니다.

변동성이 큰 시기에 큰 움직임이 연속해서 나타나고, 안정적인 기간에는 작은 움직임이 이어지는 금융 데이터의 특성을 조건부 분산 구조로 직접 모델링합니다.

구조가 비교적 단순하고 추정 결과도 해석하기 쉽습니다.

변동성 예측만 필요하다면 매우 중요한 기준 모델이 될 수 있습니다.

LSTM의 장점은 유연성입니다.

가격 수익률만 사용할 필요가 없습니다.

거래량, 금리, 환율, 변동성 지수, 여러 자산의 수익률과 같은 다양한 변수를 동시에 넣어 비선형 관계를 학습하도록 확장할 수 있습니다.

하지만 이런 자유에는 비용도 따릅니다.

데이터 양과 전처리 방법, lookback 길이, LSTM unit 수, 학습률, epoch, dropout 등 여러 조건에 따라 결과가 달라질 수 있습니다.

모델이 복잡해질수록 과적합 가능성도 함께 확인해야 합니다.

따라서 두 모델을 비교할 때는 최소한 다음 사항은 동일하게 맞추는 것이 좋습니다.

  1. 동일한 예측 대상
  2. 동일한 테스트 기간
  3. 미래 데이터를 사용하지 않는 전처리
  4. 동일한 평가 지표
  5. 아웃오브샘플 예측
  6. 반복되는 시장 구간에서의 추가 검증
  7. 단순 기준 모델과의 비교

특히 한 번의 테스트 결과만 보고 모델을 선택하면 위험합니다.

예를 들어 시장이 조용했던 시기와 금융시장 충격이 컸던 시기에서는 결과가 달라질 수 있습니다.

다음 단계에서는 테스트 구간을 이동시키는 Walk-Forward Validation을 적용해 여러 구간에서 두 모델을 반복 평가하는 것이 좋습니다.

또 하나 기억해야 할 점이 있습니다.

높은 예측 성능이 곧 높은 투자 수익률을 의미하는 것은 아닙니다.

이번 실습은 변동성을 예측한 것이지 가격 상승과 하락 방향을 예측한 것이 아닙니다.

변동성 예측은 포지션 크기 조정, 위험관리, VaR 분석, 변동성 전략 등으로 연결할 수 있지만 실제 투자전략으로 활용하려면 별도의 매매 규칙과 거래비용, 슬리피지, 리스크 관리가 필요합니다.

GARCH 공식 문서에서도 조건부 평균, 조건부 변동성과 잔차 분포를 바탕으로 VaR와 같은 위험 측정으로 확장하는 사례를 확인할 수 있습니다.

결국 LSTM과 GARCH의 비교에서 중요한 질문은 “누가 이겼는가?”가 아닙니다.

“어떤 데이터를 사용했고, 무엇을 예측했으며, 미래 정보를 차단한 상태에서도 그 결과가 유지됐는가?”가 더 중요합니다.

딥러닝 모델이 더 최신 기술이라는 이유만으로 기존 통계 모델보다 자동으로 좋은 것은 아닙니다.

반대로 GARCH가 오랫동안 사용됐다는 이유만으로 새로운 모델보다 항상 좋은 것도 아닙니다.

가장 좋은 방법은 단순합니다.

동일한 데이터와 동일한 테스트 환경을 만든 뒤 직접 결과를 비교하는 것입니다.

그리고 한 번 좋은 결과가 나왔다고 끝내지 않고 기간을 바꾸고, 입력 변수를 바꾸고, 검증 구간을 이동시키면서 결과가 얼마나 안정적인지를 확인해야 합니다.

그 과정이 금융 시계열 분석에서 모델 이름보다 훨씬 중요한 부분입니다.

참고 자료

Keras 공식 LSTM 문서에서는 LSTM 레이어의 입력과 주요 설정값을 확인할 수 있습니다.

Python arch 공식 문서에서는 GARCH(1,1)의 구조와 조건부 변동성 예측 방법을 확인할 수 있습니다.

scikit-learn 공식 문서에서는 MAE, MSE, RMSE 등 회귀 모델 평가에 사용하는 지표를 확인할 수 있습니다.

yfinance 공식 문서에서는 예제에서 사용한 download() 함수의 날짜, interval, 가격조정 등 주요 옵션을 확인할 수 있습니다. yfinance 측은 해당 도구가 연구·교육 목적의 오픈소스 도구이며 실제 데이터 이용 권한은 Yahoo의 이용 조건을 별도로 확인하도록 안내하고 있습니다.

투자 및 데이터 이용 유의사항

본 글은 Python을 활용한 통계·머신러닝 시계열 분석 방법을 설명하기 위한 교육 및 정보 제공 목적의 콘텐츠입니다. 특정 금융상품의 매수, 매도 또는 보유를 권유하기 위한 자료가 아닙니다.

본문의 모델, 종목, 기간, 입력값은 프로그래밍 실습을 위한 예시입니다. 모델의 예측 결과는 데이터 기간, 학습 조건, 시장 환경과 모델 설정에 따라 달라질 수 있으며 과거 데이터에서 확인된 성능이 미래 투자 성과를 보장하지 않습니다.

실제 금융 의사결정에는 거래비용, 세금, 슬리피지, 유동성, 개인의 투자 목적과 위험 감수 수준 등 추가적인 요소를 함께 검토해야 합니다.

금알남

금알남
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.