02. 첫 모델의 탄생과 첫 번째 벽
코드를 치기 전에
사실 코드를 치기 전에, 꽤 긴 시간을 공부에 썼다. 이 시리즈에서는 v1.0부터 v2.7까지의 개발 과정을 다루지만, 그 이전에 글로 정리하기엔 지루한 준비 과정이 있었다.
다행히 완전한 맨땅은 아니었다. 재직 중인 회사에서 시계열 데이터 기반 ML을 다뤄본 경험이 큰 도움이 됐다. 정상성(stationarity)이 왜 중요한지, 시계열에서 train/test split을 함부로 하면 안 되는 이유, 피처 엔지니어링이 모델 아키텍처보다 중요하다는 것 -- 이런 감각들은 업무에서 부딪히며 체득한 것이었다.
그 위에 금융 도메인 지식을 쌓아야 했다. Marcos López de Prado의 Advances in Financial Machine Learning은 내 사고방식을 바꿔놓은 책이었다. 금융 ML에 관심 있는 사람이라면 꼭 읽어보길 강추한다. (협찬 아님) 금융 ML이 일반 ML과 어떻게 다른지, look-ahead bias가 얼마나 교묘하게 숨어드는지, 그리고 Triple-Barrier 레이블링이라는 개념도 이 책에서 처음 배웠다.
논문도 꽤 읽었다. 처음에는 "LSTM으로 주가 예측" 류의 논문을 찾아 읽었는데, 대부분 비현실적으로 좋은 결과만 보여주고 실전 적용에 대한 이야기는 없었다. 오히려 도움이 된 건 금융 시계열의 비정상성이나 백테스트 편향에 대한 비판적인 연구들이었다. "이건 왜 안 되는가"를 아는 게 실전에서 훨씬 유용했다.
그래도 v1.0은 실수투성이었다. 공부한 것과 직접 해보는 것 사이에는 메울 수 없는 간극이 있다. 이 시리즈에 적은 건 v1.0에서 v2.7까지 개선하면서 있었던 수많은 시행착오 중 중요한 것만 추린 것이다. 실제로는 여기 적지 못한 삽질이 훨씬 더 많았다.
55.58%
2025년 10월 23일. 첫 번째 모델의 첫 번째 에폭이 끝났을 때, 터미널에 찍힌 숫자가 이것이었다.
Epoch 1/80
- Train Accuracy: 55.58%
- Validation Accuracy: 55.38%
55.58%. 동전 던지기가 50%니까, 내 모델은 동전보다 5.58% 낫다. 웃어야 할지 울어야 할지 모르겠는 숫자였다. 하지만 동시에 희망적이기도 했다. 첫 에폭에서 이 정도면, 학습이 진행되면 더 올라가지 않을까?
나는 80 에폭이 끝나기를 기다리면서 커피를 내렸다.
v1.0의 구조
v1.0은 순진함의 결정체였다. 지금 돌이켜보면 교과서적인 실수를 여러 개 저질렀지만, 그때는 최선이라고 생각했다.
LSTM + XGBoost + LightGBM 앙상블
세 모델을 합쳐 쓰기로 했다. 각각의 역할 분담이 있었다.
LSTM: 90개 봉(7.5시간)의 시퀀스를 보고 시계열 패턴을 학습
XGBoost: 33개 기술적 지표의 비선형 관계를 포착
LightGBM: XGBoost와 다른 방식의 부스팅으로 다양성 확보
앙상블 가중치는 LSTM 35% : XGBoost 40% : LightGBM 25%로 설정했다. XGBoost에 가장 높은 가중치를 준 이유는, 테이블 형태의 데이터에서는 gradient boosting이 LSTM보다 강하다는 경험적 근거가 있었기 때문이다.
피처 구성
# v1.0 LSTM 피처 (18개)
lstm_features = [
"Close", "Volume", # 원시 가격/거래량
"RSI", "MACD", "BB_Position", # 기술지표
"Momentum_5", "Momentum_10", # 모멘텀
"Return_5m", "Return_15m", # 수익률
"Volatility_5", "Volatility_10" # 변동성
# ... 등등
]
여기서 보이는가? Close와 Volume이 피처 목록 맨 위에 떡하니 앉아 있다. 원시 가격을 그대로 모델에 넣었다. 이것이 v1.0의 원죄였다.
첫 번째 벽: 비정상성
모델 학습이 끝나고 결과를 분석하면서, 뭔가 이상하다는 느낌이 들기 시작했다. 검증 정확도가 학습 정확도를 따라가지 못했다. 과적합의 징후였다.
원인을 찾기 시작했다. 그리고 비정상성(Non-Stationarity) 이라는 벽에 부딪혔다.
문제의 본질
비트코인 가격은 비정상 시계열이다. 평균과 분산이 시간에 따라 끊임없이 변한다.
# 원시가격 시계열
Close = [30000, 31000, 50000, 45000, 80000, ...]
# 평균? 분산? 시간에 따라 계속 바뀐다.
내 모델은 이런 데이터를 보고 "$45,000이면 상승"같은 절대값 패턴을 학습하고 있었다. 하지만 6개월 전의 $45,000과 오늘의 $45,000은 시장 맥락이 완전히 다르다. 모델이 학습한 패턴은 과거에만 유효한 가짜 패턴이었던 것이다.
정상성의 수학적 정의
여기서 잠깐, "정상성"이 정확히 무엇인지 짚고 넘어가겠다. 시계열 분석에서 말하는 약정상성(Weak Stationarity) 은 다음 세 가지 조건을 만족하는 확률 과정이다:
조건 1. 평균이 상수이다:
$$E[X_t] = \mu \quad \text{for all } t$$
시점에 관계없이 평균이 동일해야 한다. 비트코인 가격은 $30,000에서 $80,000으로 이동했다. 명백히 이 조건을 위반한다.
조건 2. 분산이 유한한 상수이다:
$$\text{Var}(X_t) = E[(X_t - \mu)^2] = \sigma^2 < \infty \quad \text{for all } t$$
분산 역시 시간에 따라 변하면 안 된다. 하지만 비트코인은 2023년에는 하루 변동이 $500이었다가, 2024년에는 $3,000이 되기도 한다.
조건 3. 자기공분산이 시차에만 의존한다:
$$\text{Cov}(X_t, X_{t+h}) = \gamma(h) \quad \text{for all } t$$
시점 $t$와 $t+h$ 사이의 공분산은 시차 $h$에만 의존하고, 시점 $t$ 자체에는 의존하지 않아야 한다.
약정상성의 세 조건을 학교 시험에 비유하면 이렇다. 조건 1은 "매 시험의 평균 난이도가 일정해야 한다", 조건 2는 "점수 편차가 매번 비슷해야 한다", 조건 3은 "중간고사와 기말고사 사이의 상관관계가, 그게 1학기든 2학기든 같아야 한다"는 것이다. 비트코인 가격은 세 조건 다 어기는, 매번 규칙이 바뀌는 시험인 셈이다.
원시 가격 데이터는 세 조건 모두를 위반한다. 이런 데이터를 그대로 LSTM에 넣는 것은, 끊임없이 변하는 규칙으로 게임을 학습시키는 것과 같다. 모델이 배운 규칙은 이미 옛날 규칙이다.
스케일러로는 부족하다
"RobustScaler로 정규화했는데?"라고 반문할 수 있다. 나도 그랬다.
RobustScaler().fit_transform(df['Close'])
# 정규화해도 절대값 자체가 의미 없음
# $30,000 -> $60,000 (100% 상승)과
# $60,000 -> $90,000 (50% 상승)은
# 달러 기준으로는 같은 $30,000 상승이지만
# 의미는 완전히 다르다
스케일러는 값의 범위를 조정할 뿐, 데이터의 본질적인 비정상성을 해결하지 못한다. 정규화된 가격도 여전히 시간에 따라 의미가 달라진다.
수익률 계산도 틀렸다
변동성 피처도 문제였다.
# v1: 원시가격 기반 표준편차
Volatility_5 = df['Close'].rolling(5).std()
# $30,000에서 $1,000 변동 vs $60,000에서 $1,000 변동
# 전자는 3.3%, 후자는 1.7%. 실제 변동성은 다른데 std는 같다.
심지어 단순 수익률(pct_change)도 수학적으로 불완전했다. +10% 후 -10%를 하면 원금으로 돌아와야 하는데, 단순 수익률에서는 100 * 1.1 * 0.9 = 99가 된다. 1%가 증발한다.
깨달음이 왔다. 모델이 학습해야 하는 건 "가격"이 아니라 "변화의 패턴"이다.
v2.0: 로그수익률의 도입
깨달음을 코드로 옮겼다.
핵심 변경: 원시가격 OUT, 로그수익률 IN
# v1 (단순 수익률) -- 비정상, 비대칭
Return_5m = df['Close'].pct_change(1)
# v2 (로그수익률) -- 정상, 대칭
Log_Return_1 = np.log(df['Close'] / df['Close'].shift(1))
로그수익률의 수학적 정의
로그수익률(log return)의 정의는 간단하다:
$$r_t = \ln\left(\frac{P_t}{P_{t-1}}\right) = \ln(P_t) - \ln(P_{t-1})$$
비유하자면, 로그수익률은 "몇 층 올라갔느냐"를 재는 것과 같다. 1층에서 2층(100%)이나 5층에서 10층(100%)이나 "배율"이 같으면 같은 값이 나온다. 절대적인 높이(가격)가 아니라 상대적인 변화를 측정하는 셈이다.
여기서 $P_t$는 시점 $t$의 가격이다. 단순 수익률 $R_t = (P_t - P_{t-1})/P_{t-1}$과의 관계는:
$$r_t = \ln(1 + R_t)$$
$R_t$가 작을 때(예: $|R_t| < 0.1$), 테일러 전개에 의해 $r_t \approx R_t$이므로 두 수익률은 거의 같다. 하지만 변동이 클 때는 로그수익률이 수학적으로 우월한 성질을 여러 개 갖는다.
로그수익률이 풀어주는 문제들
로그수익률이 왜 금융 시계열 분석의 표준인지, 하나씩 수학으로 설명하겠다.
1. 가법성 (Additivity)
여러 기간의 수익률을 합산할 때, 단순 수익률은 곱해야 한다. 하지만 로그수익률은 더하면 된다:
$$r_{t:t+n} = \ln\left(\frac{P_{t+n}}{P_t}\right) = \ln\left(\frac{P_{t+1}}{P_t} \cdot \frac{P_{t+2}}{P_{t+1}} \cdots \frac{P_{t+n}}{P_{t+n-1}}\right)$$
$$= \sum_{k=1}^{n} \ln\left(\frac{P_{t+k}}{P_{t+k-1}}\right) = \sum_{k=1}^{n} r_{t+k}$$
가법성을 레고에 비유하면, 단순 수익률은 "블록을 곱셈으로 끼워 맞추는" 복잡한 조립이고, 로그수익률은 "블록을 옆에 나란히 붙이기만 하면 되는" 직선 조립이다. 1시간 동안의 총 변화를 알고 싶으면 5분짜리 로그수익률 12개를 그냥 더하면 끝이다.
이 성질이 왜 중요한가? 내 시스템에서 LSTM은 90개 시점의 피처를 입력받는다. 로그수익률을 쓰면 "최근 1시간의 총 변화"를 12개 봉의 로그수익률을 단순히 더해서 구할 수 있다. 이 연산의 선형성은 모델이 패턴을 학습하기 훨씬 쉽게 만든다.
2. 대칭성
단순 수익률의 비대칭 문제를 로그수익률이 어떻게 해결하는지 보자:
가격이 100에서 110으로: 단순 $R = +10\%$, 로그 $r = \ln(1.1) = +0.0953$
가격이 110에서 100으로: 단순 $R = -9.09\%$, 로그 $r = \ln(1/1.1) = -0.0953$
단순 수익률에서는 "올라간 만큼 내려오는" 것이 $+10\%$와 $-9.09\%$로 비대칭이다. 하지만 로그수익률에서는 $+0.0953$과 $-0.0953$으로 완벽하게 대칭이다. 수학적으로:
$$\ln\left(\frac{P_t}{P_{t-1}}\right) = -\ln\left(\frac{P_{t-1}}{P_t}\right)$$
이 대칭성은 이진 분류 모델에서 Long/Short 방향의 균형 잡힌 학습을 가능하게 한다.
3. 정상성 확보와 ADF 검정
로그수익률로 변환하면 비정상 시계열이 정상 시계열에 가까워진다. 이것을 통계적으로 검증하는 방법이 ADF 검정(Augmented Dickey-Fuller Test)이다.
ADF 검정의 회귀 모형은 다음과 같다:
$$\Delta y_t = \alpha + \beta t + \gamma y_{t-1} + \sum_{i=1}^{p} \delta_i \Delta y_{t-i} + \epsilon_t$$
여기서:
$\Delta y_t = y_t - y_{t-1}$ (1차 차분)
$\gamma$가 검정의 핵심 파라미터
$\sum \delta_i \Delta y_{t-i}$는 고차 자기상관을 흡수하기 위한 시차항
귀무가설 $H_0$: $\gamma = 0$ (단위근이 존재한다, 즉 비정상이다) 대립가설 $H_1$: $\gamma < 0$ (단위근이 없다, 즉 정상이다)
원시 가격에 ADF 검정을 하면 대부분 $p > 0.05$로 귀무가설을 기각하지 못한다(비정상). 하지만 로그수익률에 ADF 검정을 하면 $p \approx 0$으로 귀무가설이 기각된다(정상). 이것은 "로그수익률이 정상 시계열이다"라는 내 직관을 통계적으로 확인해준다.
ADF 검정은 건강검진의 혈액 검사와 비슷하다. "이 시계열, 혹시 단위근이라는 병에 걸려 있지 않습니까?"라고 검사를 돌리는 것이다. p-value가 낮으면 "아니요, 건강합니다(정상입니다)"라는 진단이 나오고, 높으면 "비정상 의심, 치료(차분/로그변환)가 필요합니다"가 되는 셈이다.
4. 중심극한정리(CLT)와의 관계
왜 하필 "로그"인가? 이것은 중심극한정리(Central Limit Theorem)와 연결된다.
가격 형성 과정을 생각해보자. 시점 $T$의 가격은 각 시점의 수익률이 곱해진 결과다:
$$P_T = P_0 \cdot \prod_{t=1}^{T}(1 + R_t)$$
양변에 로그를 취하면:
$$\ln(P_T) = \ln(P_0) + \sum_{t=1}^{T} \ln(1 + R_t) = \ln(P_0) + \sum_{t=1}^{T} r_t$$
만약 각 기간의 로그수익률 $r_t$가 독립이고 동일한 분포를 따른다면(i.i.d.), CLT에 의해:
$$\frac{\sum_{t=1}^{T} r_t - T\mu}{\sigma\sqrt{T}} \xrightarrow{d} N(0,1) \quad \text{as } T \to \infty$$
즉, 충분히 많은 기간의 로그수익률 합은 정규분포에 수렴한다. 이것이 금융 모델링에서 로그수익률이 정규분포를 따른다고 가정하는 이론적 근거이며, Black-Scholes 모델의 기하 브라운 운동(Geometric Brownian Motion)도 이 가정에 기반한다.
5. Black-Scholes와 기하 브라운 운동
잠깐 옆길로 새서, 로그수익률이 금융 이론에서 얼마나 근본적인 위치를 차지하는지 언급하겠다.
Black-Scholes 모델에서 자산 가격은 기하 브라운 운동(GBM)을 따른다고 가정한다:
$$dS_t = \mu S_t \, dt + \sigma S_t \, dW_t$$
여기서 $\mu$는 기대수익률, $\sigma$는 변동성, $W_t$는 위너 과정이다. 이토의 보조정리(Ito's Lemma)를 적용하면:
$$d(\ln S_t) = \left(\mu - \frac{\sigma^2}{2}\right) dt + \sigma \, dW_t$$
기하 브라운 운동과 이토 보조정리를 술집 비유로 풀면 이렇다. 원래 주가의 움직임(GBM)은 "술 취한 사람이 비탈길을 걷는 것"처럼, 방향성(드리프트)과 비틀거림(변동성)이 뒤섞여 있다. 이토 보조정리는 이 비틀거리는 경로에 로그라는 렌즈를 씌워서, 평평한 길 위의 랜덤 워크로 변환해주는 수학적 도구다. 분석하기가 훨씬 쉬워진다.
즉, 로그 가격이 드리프트와 확산을 가진 산술 브라운 운동을 따른다. 이것은 로그수익률이 정규분포를 따른다는 것과 동치이다:
$$r_t = \ln\left(\frac{S_t}{S_{t-1}}\right) \sim N\left(\left(\mu - \frac{\sigma^2}{2}\right)\Delta t, \, \sigma^2 \Delta t\right)$$
물론 실제 금융 시장의 수익률은 정규분포보다 꼬리가 두꺼운(fat-tailed) 분포를 보인다. 완벽한 모델은 아니다. 하지만 "가격 수준과 무관한 상대적 변화"를 측정한다는 로그수익률의 본질적 성질은, 이론적 가정이 완벽하지 않아도 유효하다.
내가 v2.0에서 로그수익률로 전환한 것은, 단순히 "성능이 좋아진다더라"가 아니라 이런 수학적 근거 위에 서 있었다.
피처 전면 교체
# v2.0 LSTM 피처 (16개) -- 원시가격 완전 제거
lstm_features = [
"Log_Return_1", # 5분 로그수익률
"Log_Return_5", # 25분
"Log_Return_12", # 1시간
"Log_Volume_Change", # 거래량 변화율
"RSI", "BB_Position", "ATR_Percent",
"MACD_Normalized", # MACD를 ATR로 나눔 (NEW)
"Volatility_5", # 로그수익률 기반 변동성 (NEW)
# ...
]
Close와 Volume이 사라졌다. 대신 Log_Return_1, Log_Volume_Change가 자리를 채웠다. 모델이 보는 것은 더 이상 "비트코인이 $45,000이다"가 아니라 "최근 5분간 0.3% 상승했다"이다.
MACD도 ATR로 정규화했다. 가격 수준과 무관하게 모멘텀의 강도를 측정할 수 있게 되었다. 수학적으로 표현하면:
$$\text{MACD\_Normalized} = \frac{\text{MACD}}{\text{ATR} + \epsilon}$$
여기서 $\epsilon = 10^{-8}$은 0으로 나누는 것을 방지하는 작은 상수다. ATR(Average True Range) 자체가 변동성의 척도이므로, MACD를 ATR로 나누면 "현재 변동성 대비 모멘텀의 상대적 강도"가 된다.
그래서 결과는?
v2.0의 첫 에폭 결과를 보고 나는 의자에서 살짝 뛸 뻔했다.
v1.0 검증 정확도: 55.38%
v2.0 검증 정확도: 57.35% (+1.97%)
1.97%가 뭐 대수냐고? 이진 분류에서 2%는 크다. 특히 금융 데이터처럼 노이즈가 많은 도메인에서, 첫 에폭 기준으로 2% 향상은 방향이 맞다는 강한 신호다.
그리고 더 중요한 건 숫자가 아니었다. 모델이 학습하는 것의 본질이 달라졌다는 점이었다. 절대적 가격 수준에 의존하는 가짜 패턴 대신, 시간에 무관한 상대적 변화 패턴을 학습하게 된 것이다.
돌이켜보며
v1.0에서 v2.0으로의 전환은 기술적으로는 "원시가격을 로그수익률로 바꿨다"는 한 줄로 요약할 수 있다. 하지만 내가 배운 교훈은 이것보다 훨씬 깊었다.
금융 시계열에서는 데이터를 어떻게 표현하느냐가 모델 아키텍처를 어떻게 설계하느냐만큼, 어쩌면 그보다 더 중요하다. 아무리 복잡한 LSTM을 쌓아도, 비정상 데이터를 넣으면 비정상적인 결과가 나온다.
로그수익률이라는 개념은 금융 이론에서는 기초 중의 기초다. Black-Scholes 모델도, GARCH 모델도 모두 로그수익률을 사용한다. 하지만 나는 머신러닝 관점에서 시작했기 때문에, 이 기초를 놓쳤다. ML의 언어와 금융의 언어를 모두 이해해야 한다는 것. 이것이 v1.0에서 v2.0 사이에서 배운 가장 큰 교훈이다.
v2.0은 출발선이었다. 비정상성이라는 첫 번째 벽을 넘었지만, 앞에는 더 높은 벽들이 기다리고 있었다. Transformer의 유혹, 32,687%라는 환상적인 숫자, 그리고 "정확도가 높으면 돈을 벌 수 있을까?"라는 근본적인 질문이.